Преглед на файлове

Merge branch 'assessment-details' of LInE/ivprog into master

Lucas de Souza преди 4 години
родител
ревизия
04e73d458b

+ 3 - 0
.eslintignore

@@ -0,0 +1,3 @@
+/js/visualUI/
+/js/Sortable.js
+/js/iassign-integration-functions.js

+ 17 - 0
.eslintrc.json

@@ -0,0 +1,17 @@
+{
+    "env": {
+        "browser": true,
+        "es6": true
+    },
+    "extends": "eslint:recommended",
+    "globals": {
+        "Atomics": "readonly",
+        "SharedArrayBuffer": "readonly"
+    },
+    "parserOptions": {
+        "ecmaVersion": 2018,
+        "sourceType": "module"
+    },
+    "rules": {
+    }
+}

+ 21 - 1
README.md

@@ -1,5 +1,25 @@
 # iVProg
+
 Módulo interativo de aprendizagem para o ensino de lógica de programação desenvolvido pelo [LInE](https://usp.br/line)
 
 # Downloads
-As versão estáveis podem ser baixaas através do seguinte [link](http://200.144.254.107/release/ivprog). O projeto usa uma numeração de versão seguindo a formatação: YYYY-MM-DD_HH-MM-SS
+
+As versões estáveis podem ser baixadas através do seguinte [link](http://200.144.254.107/release/ivprog). O projeto usa uma numeração de versão seguindo a formatação: YYYY-MM-DD_HH-MM-SS.
+
+# Credits
+
+- "Empty" icon by amante de icono, from thenounproject.com
+
+# Desenvolvimento
+
+O projeto utiliza o npm como gerenciador de pacotes e ferramenta de construção. Para montar o programa a partir do código fonte é necessário ter instalado o Java (para o analisador léxico baseado em antlr4) e nodejs(^10.16.0) com npm(^6.9.0).
+Após clone este repositório execute os seguintes comandos a partir da pasta raiz:
+
+```
+npm install
+npm run build
+npm run start
+```
+
+Após a execução desses comandos, você poderá acessar o localhost na porta 8080 para acessar a sua versão local do iVProg.
+Existem também o comando _npm run watch_ para compilar os arquivos enquanto você faz modificações no código

BIN
css/fonts/NimbusSanLConBold.ttf


BIN
css/fonts/NimbusSanLConRegular.ttf


BIN
css/fonts/texgyreheros-regular.otf


+ 118 - 0
css/ivprog-assessment.css

@@ -0,0 +1,118 @@
+@font-face {
+  font-family: 'NimbusSanLConRegular';
+  src: url(fonts/NimbusSanLConRegular.ttf) format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'NimbusSanLConBold';
+  src: url(fonts/NimbusSanLConBold.ttf) format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'TeXGyreHerosRegular';
+  src: url(fonts/texgyreheros-regular.otf) format('opentype');
+  font-weight: normal;
+  font-style: normal;
+}
+body {
+  font-family: 'TeXGyreHerosRegular';
+  background-color: #b9c7ca;
+  margin-left: 2rem;
+}
+.details-body > .details-header > h2 {margin-bottom: 0.5rem;}
+.details-body > .details-header > p {
+  padding-left: 1rem;
+  margin: 0;
+}
+table td { font-size: 14pt;}
+.stringdiff-delete, .stringdiff-insert {font-weight: bold;}
+.stringdiff-insert, .assessment-input-read {color: #22a222}
+.stringdiff-delete {
+  text-decoration: line-through;
+  color: #d02929
+}
+.details-body h3 {
+  margin-top: 0.5rem;
+  margin-bottom: 0.5rem;
+}
+.detaisl-div-table {
+  padding-left: 1rem;
+  margin-top: 1rem;
+}
+.assessment-output-table {
+  border-collapse: collapse;
+  border-style: hidden;
+}
+.assessment-output-table tr:nth-child(odd) {background: #CCC}
+.assessment-output-table tr:nth-child(even) {background: #f7f2c9}
+.assessment-output-table tr > th {
+  background-color: #000;
+  color: #fff;
+  font-family: 'NimbusSansLBoldCond';
+  font-weight: bold;
+  font-style: normal; 
+  letter-spacing: .05rem;
+}
+.assessment-output-table td, .assessment-output-table th {
+  border-left: 1px solid black;
+  padding: 0.5rem;
+  text-align: center;
+  max-width: 16rem;
+  font-family: 'NimbusSanLConRegular';
+  vertical-align: middle;
+  letter-spacing: .05rem;
+}
+.assessment-string-expected, .assessment-string-generated, .assessment-string-diff {text-align: left;}
+p.assessment-failed-execution {padding-left: 1rem;}
+.assessment-failed-case {color:#FF1212}
+.assessment-input-unread {color: #d02929}
+.assessment-number-result-failed, .assessment-bool-result-failed {color: #d02929}
+.assessment-number-result, .assessment-bool-result, .assessment-string-result {color: #22a222}
+.assessment-popup {
+  position: relative;
+  display: inline-block;
+  cursor: pointer;
+}
+.assessment-popup .assessment-popuptext {
+  visibility: hidden;
+  width: 160px;
+  background-color: #555;
+  color: #fff;
+  text-align: center;
+  border-radius: 6px;
+  padding: 8px 0;
+  position: absolute;
+  z-index: 1;
+  bottom: 125%;
+  left: 50%;
+  margin-left: -80px;
+}
+.assessment-popup .assessment-popuptext::after {
+  content: "";
+  position: absolute;
+  top: 100%;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px;
+  border-style: solid;
+  border-color: #555 transparent transparent transparent;
+}
+.assessment-popup:hover .assessment-popuptext {
+  visibility: visible;
+  -webkit-animation: fadeIn 1s;
+  animation: fadeIn 1s;
+}
+@-webkit-keyframes fadeIn {
+  from {opacity: 0;} 
+  to {opacity: 1;}
+}
+@keyframes fadeIn {
+  from {opacity: 0;}
+  to {opacity:1 ;}
+}
+.assessment-empty-output {
+  height: 1.5rem;
+  width: 1.5rem;
+}

+ 7 - 0
css/ivprog-term.css

@@ -185,4 +185,11 @@
   padding: 0;
   margin: 0;
   line-height: 1rem;
+}
+
+.assessment-div-detail:hover {
+  cursor: pointer;
+}
+.assessment-div-detail:hover > span {
+  text-decoration: underline;
 }

+ 3 - 2
i18n/pt/error.json

@@ -84,8 +84,9 @@
   "invalid_array_literal_column": "Esperava-se $0 colunas mas encontrou $1.",
   "exceeded_input_request": "A quantidade de leituras requisitadas execedeu a quantidade de entradas disponíveis.",
   "test_case_few_reads": "Caso de teste $0 falhou: ainda restam entradas!",
-  "test_case_failed": "Caso de teste $0 falhou: <ul> <li>entrada(s): $1</li> <li>saída(s) esperada(s): $2</li> <li>saída(s): $3</li></ul>",
-  "test_case_failed_exception": "Caso de teste $0 falhou: $1",
+  "test_case_failed": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$1\"> <span>Caso de teste $0 não executou com sucesso.</span></div>",
+  "test_case_failed_exception": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$2\"> <span>Caso de teste $0 falhou</span>: $1",
+  "test_case_exception": "Ocorreu uma exceção no caso de teste $0: $1",
   "invalid_type_conversion": "O valor $0 não pode ser convertido para o tipo $1",
   "invalid_read_type":"A entrada \"$0\" não é do tipo $1, que é o tipo da variável <span class='ivprog-error-varname'>$2</span>.",
   "invalid_read_type_array":"A entrada \"$0\" não é do tipo $1, que é o tipo aceito pela variável <span class='ivprog-error-varname'>$2</span> que é um $3.",

+ 3 - 1
i18n/pt/message.json

@@ -1,8 +1,10 @@
 {
-  "test_case_success": "Caso de teste $0: OK",
+  "test_case_success": "<div class='assessment-div-detail' onClick='ivprogCore.openAssessmentDetail(event)' data-page=\"$1\"><span>Caso de teste $0</span>: OK</div>",
   "test_case_duration": "Levou $0ms",
   "test_suite_grade": "A sua solução alcançou $0% da nota.",
   "awaiting_input_message": "O seu programa está em execução e aguardando uma entrada! Digite algo e pressione ENTER...",
+  "assessment-empty-expected-tooltip": "A saída gerada foi além do esperado",
+  "assessment-empty-generated-tooltip": "O programa não gerou saídas suficientes",
   "testcase_autogen_unused_input": "O caso de teste $0 possui mais entradas do que as leituras feitas no programa.",
   "testcase_autogen_empty": "O caso de teste $0 não gerou qualquer saída."
 }

+ 8 - 0
i18n/pt/ui.json

@@ -117,6 +117,14 @@
   "text_teacher_filter_active": "Ativado",
   "text_teacher_filter_help": "Ao ativar o filtro, as modificações do iVProg estarão bloqueadas.",
   "text_join_assessment_outputs": " ; ",
+  "assessment-detail-time-label": "Duração",
+  "assessment-detail-grade-label": "Nota",
+  "assessment-detail-input-label": "Entradas",
+  "assessment-detail-output-label": "Saídas",
+  "assessment-detail-expected-label": "Esperava",
+  "assessment-detail-generated-label": "Gerou",
+  "assessment-detail-result-label": "Resultado",
+  "assessment-detail-title": "Caso de Teste $0",
   "text_teacher_generate_outputs": "Gerar saídas",
   "text_teacher_generate_outputs_algorithm": "Antes de gerar as saídas, elabore um algoritmo!"
 }

Файловите разлики са ограничени, защото са твърде много
+ 1 - 0
img/empty.svg


+ 33 - 102
js/assessment/ivprogAssessment.js

@@ -1,46 +1,60 @@
-import { Decimal } from 'decimal.js';
 import line_i18n from 'line-i18n'
-import { SemanticAnalyser } from "./../processor/semantic/semanticAnalyser";
 import { IVProgProcessor } from "./../processor/ivprogProcessor";
-import { InputTest } from "./../util/inputTest";
-import { OutputTest } from "./../util/outputTest";
 import { DOMConsole} from "./../io/domConsole";
 import * as LocalizedStringsService from "../services/localizedStringsService";
-import { Config } from "../util/config";
+import { OutputMatching } from './output_matching/output_matching';
 
 
 const LocalizedStrings = LocalizedStringsService.getInstance();
-const list_joiner = LocalizedStrings.getUI("text_join_assessment_outputs");
 
 const StringTypes = line_i18n.StringTypes;
 
 export class IVProgAssessment {
 
-  constructor (textCode, testCases, domConsole) {
-    this.textCode = textCode;
+  constructor (ast_code, testCases, domConsole) {
+    this.ast_code = ast_code;
     this.testCases = testCases;
     this.domConsole = domConsole;
   }
 
   runTest () {
-    const outerRef = this
+    const outerRef = this;
     try {
-      const validTree = SemanticAnalyser.analyseFromSource(this.textCode);
       // loop test cases and show messages through domconsole
       const partialTests = this.testCases.map( (t, name) => {
-        return outerRef.partialEvaluateTestCase(new IVProgProcessor(validTree), t.input, t.output, name);
+        return new OutputMatching(new IVProgProcessor(outerRef.ast_code), t.input, t.output, name);
       });
-      const testResult = partialTests.reduce((acc, curr) => acc.then(curr), Promise.resolve(0));
-      return testResult.then(function (total) {
-        const grade = total / outerRef.testCases.length;
+      const testResult = partialTests.map(om => om.eval());
+      return Promise.all(testResult).then(results => {
+        let grade = 0;
+        for(let i = 0; i < results.length; ++i) {
+          const result = results[i];
+          grade += result.grade;
+          if(result.grade == 1) {
+            outerRef.writeToConsole(DOMConsole.INFO, StringTypes.MESSAGE,'test_case_success',
+              result.name + 1, result.generateOutput());
+          } else if (result.status == 1) {
+            outerRef.writeToConsole(DOMConsole.ERR, StringTypes.ERROR,'test_case_failed_exception',
+              result.name + 1, result.error_msg, result.generateOutput());
+          } else {
+            outerRef.writeToConsole(DOMConsole.ERR, StringTypes.ERROR,'test_case_failed',
+              result.name + 1, result.generateOutput());
+          }
+        }
+        grade /= results.length;
         const channel = grade == 1 ? DOMConsole.INFO : DOMConsole.ERR;
         outerRef.writeToConsole(channel, StringTypes.MESSAGE, "test_suite_grade", (grade * 100).toFixed(2));
-        return Promise.resolve(grade)
-      }).catch(err => {
-        outerRef.domConsole.err("Erro inesperado durante o cálculo da nota.");// try and show error messages through domconsole
-        outerRef.domConsole.err(err.message);
-        return Promise.resolve(0);
       });
+      // return testResult.then(function (total) {
+      //   const grade = total / outerRef.testCases.length;
+      //   const channel = grade == 1 ? DOMConsole.INFO : DOMConsole.ERR;
+      //   outerRef.writeToConsole(channel, StringTypes.MESSAGE, "test_suite_grade", (grade * 100).toFixed(2));
+      //   return Promise.resolve(grade)
+      // }).catch(err => {
+      //   outerRef.domConsole.err("Erro inesperado durante o cálculo da nota.");// try and show error messages through domconsole
+      //   outerRef.domConsole.err(err.message);
+      //   return Promise.resolve(0);
+      // });
     } catch (error) {
       outerRef.domConsole.err("Erro inesperado durante a execução do programa");// try and show error messages through domconsole
       outerRef.domConsole.err(error.message);
@@ -48,89 +62,6 @@ export class IVProgAssessment {
     }
   }
 
-  evaluateTestCase (prog, inputList, expectedOutputs, name, accumulator) {
-    const outerThis = this;
-    const input = new InputTest(inputList);
-    const output = new OutputTest();
-    prog.registerInput(input);
-    prog.registerOutput(output);
-    const startTime = Date.now()
-    return prog.interpretAST().then( _ => {
-      const millis = Date.now() - startTime;
-      if (input.inputList.length !== input.index) {
-        outerThis.showErrorMessage('test_case_few_reads', name+1);
-        outerThis.showInfoMessage('test_case_duration', millis);
-        return Promise.resolve(accumulator);
-      } else if (output.list.length != expectedOutputs.length) {
-        outerThis.showErrorMessage('test_case_failed', name + 1, inputList.join(list_joiner),
-          expectedOutputs.join(list_joiner), output.list.join(list_joiner));
-        outerThis.showInfoMessage('test_case_duration', millis);
-        // must check for a partial match of the generated output
-        const numMatchedOutputs = output.list.reduce((acc, actualOutput, index) => {
-          if(outerThis.checkOutputValues(actualOutput, expectedOutputs[index])) {
-            return acc + 1;
-          } else {
-            return acc;
-          }
-        }, 0);
-        const maxLength = Math.max(expectedOutputs.length, output.list.length);
-        return Promise.resolve(accumulator + (numMatchedOutputs/maxLength));
-      } else {
-        const isOk = outerThis.checkOutputLists(output.list, expectedOutputs);
-        if(!isOk) {
-          outerThis.showErrorMessage('test_case_failed', name + 1, inputList.join(list_joiner),
-            expectedOutputs.join(list_joiner), output.list.join(list_joiner));
-          outerThis.showInfoMessage('test_case_duration', millis);
-          return Promise.resolve(accumulator);
-        } else {
-          outerThis.showInfoMessage('test_case_success', name + 1);
-          outerThis.showInfoMessage('test_case_duration', millis);
-          return Promise.resolve(accumulator + 1);
-        }
-      }
-    }).catch( error => {
-      outerThis.showErrorMessage('test_case_failed_exception', name + 1, error.message);
-      return Promise.resolve(accumulator);
-    });
-  }
-
-  partialEvaluateTestCase (prog, inputList, expectedOutputs, name) {
-    return this.evaluateTestCase.bind(this, prog, inputList, expectedOutputs, name);
-  }
-
-  checkOutputLists (actualOutputs, expectedOutputs) {
-    for (let i = 0; i < actualOutputs.length; i++) {
-      const outValue = actualOutputs[i];
-      const expectedValue = expectedOutputs[i];
-      if(!this.checkOutputValues(outValue, expectedValue)) {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  checkOutputValues (actualValue, expectedValue) {
-    let castNumberA = parseFloat(actualValue);
-    if(!Number.isNaN(castNumberA)) {
-      let castNumberB = parseFloat(expectedValue);
-      if(Number.isNaN(castNumberB)) {
-        return false;
-      }
-      castNumberA = new Decimal(castNumberA);
-      castNumberB = new Decimal(castNumberB);
-      const decimalPlaces = Math.min(castNumberB.dp(), Config.decimalPlaces);
-      castNumberA = new Decimal(castNumberA.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
-      castNumberB = new Decimal(castNumberB.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
-      const aEqualsB = castNumberA.eq(castNumberB);
-      if (!aEqualsB) {
-        return false;
-      }
-    } else if(actualValue != expectedValue) {
-      return false;
-    }
-    return true;
-  }
-
   showErrorMessage (errorID, ...args) {
     this.domConsole.err(LocalizedStrings.getError(errorID, args));
   }

+ 227 - 0
js/assessment/output_matching/assessment_result.js

@@ -0,0 +1,227 @@
+import StringDiff from "./../../util/string_diff";
+import { LocalizedStrings } from './../../services/localizedStringsService'
+
+export class OutputAssessmentResult {
+
+  static get PAGE_TEMPLATE () {
+    return `<!DOCTYPE html>
+    <html>
+      <head>
+        <meta http-equiv='content-type' content='text/html; charset=UTF-8'>
+        <link rel='stylesheet' href='css/ivprog-assessment.css' type='text/css'/>
+      </head>
+    <body>
+      :assessment-result:
+    </body>
+    </html>`;
+  }
+
+  static get DETAIL_TEMPLATE () {
+    return `<div class='details-body'>
+        <div class='details-header'>
+          <h2>:test-name:</h2>
+          <p>:time-label:: <span>:time:ms</span></p>
+          <p>:grade-label:: <span>:grade:%</span></p>
+        </div>
+        <div>
+          <h3>:input-label:</h3>
+          <ul>
+            <li>:input-list:</li>
+          </ul>
+        </div>
+        <div>
+          <h3>:output-label:</h3>
+          :output-result:
+        </div>
+      </div>`;
+  }
+
+  static get OUPUT_TABLE_TEMPLATE () {
+    return `<div class='detaisl-div-table'>
+        <table class='assessment-output-table'>
+          <tr>
+            <th>:expected-label:</th>
+            <th>:generated-label:</th>
+            <th>:result-label:</th>
+          </tr>
+          :results:
+        </table>
+      </div>`;
+  }
+
+  static get OUTPUT_TEMPLATE () {
+    return `<tr><td class=':class-expected:'>$0</td>
+            <td class=':class-generated:'>$1</td>
+            <td class=':class-result:'>$2</td></tr>`;
+  }
+
+  static get EMPTY_OUTPUT_TEMPLATE () {
+    return `<div class='assessment-popup'><img class='assessment-empty-output' src='img/empty.svg'>
+      <span class='assessment-popuptext'>$0</span></div>`;
+  }
+
+  static get FAILED_TEMPLATE () {
+    return `<p class='assessment-failed-execution'><span class='assessment-failed-case'>✗</span>$0</p>`;
+  }
+
+  static get INPUT_INFO_TEMPLATE () {
+    return `<span class='$0'>$1</span>`;
+  }
+
+  // Status code - it is not grade related!
+  // 0 - Succesful execution
+  // 1 - failed execution
+  constructor (name, status, inputs, result, store, time, error_msg = '') {
+    this.name = name;
+    this.status = status;
+    this.inputs = inputs;
+    this.results = result;
+    this.store = store;
+    this.time = time;
+    this.error_msg = error_msg;
+  }
+
+  get grade () {
+    if(this.results == null) {
+      return 0;
+    }
+    return this.results.reduce((prev, val) => prev + val.grade, 0) / this.results.length;
+  }
+
+  prepareResults () {
+    let template = OutputAssessmentResult.DETAIL_TEMPLATE;
+    const grade = (this.grade * 100).toFixed(2);
+    const time = this.time || "-";
+    template = template.replace(':test-name:', LocalizedStrings.getUI('assessment-detail-title', [this.name + 1]));
+    template = template.replace(':time-label:', LocalizedStrings.getUI('assessment-detail-time-label'));
+    template = template.replace(':time:', time);
+    template = template.replace(':grade-label:', LocalizedStrings.getUI('assessment-detail-grade-label'));
+    template = template.replace(':grade:', grade);
+    const input_spans = this.prepareInputList(this.inputs);
+    template = template.replace(':input-label:', LocalizedStrings.getUI('assessment-detail-input-label'));
+    template = template.replace(':input-list:', input_spans);
+    template = template.replace(':output-label:', LocalizedStrings.getUI('assessment-detail-output-label'));
+    if(this.status == 0) {
+      const output_rows = this.results.map(result => {
+        if(result.type == "string") {
+          return this.formatString(result);
+        } else if (result.type == "number") {
+          return this.formatNumber(result);
+        } else {
+          return this.formatBool(result);
+        }
+      }, this);
+      template = template.replace(':output-result:', this.prepareOutputTable(output_rows));
+    } else {
+      let failed_text =  OutputAssessmentResult.FAILED_TEMPLATE;
+      failed_text = failed_text.replace("$0", this.error_msg);
+      template = template.replace(":output-result:", failed_text);
+    }
+    return template;
+  }
+
+  prepareInputList (input_list) {
+    const list = input_list.map(input => {
+      let template = OutputAssessmentResult.INPUT_INFO_TEMPLATE;
+      template = template.replace("$1", input.value);
+      if(input.read) {
+        template = template.replace("$0", "assessment-input-read");
+      } else {
+        template = template.replace("$0", "assessment-input-unread");
+      }
+      return template;
+    }, this);
+    return list.join(LocalizedStrings.getUI('text_join_assessment_outputs'));
+  }
+
+  prepareOutputTable (output_rows) {
+    let template = OutputAssessmentResult.OUPUT_TABLE_TEMPLATE;
+    template = template.replace(':expected-label:', LocalizedStrings.getUI('assessment-detail-expected-label'));
+    template = template.replace(':generated-label:', LocalizedStrings.getUI('assessment-detail-generated-label'));
+    template = template.replace(':result-label:', LocalizedStrings.getUI('assessment-detail-result-label'));
+    template = template.replace(':results:', output_rows.join(''));
+    return template;
+  }
+
+  generateOutput () {
+    const assessment_result =  this.prepareResults();
+    let page = OutputAssessmentResult.PAGE_TEMPLATE;
+    page = page.replace(':assessment-result:', assessment_result);
+    page = page.replace(/(\r|\n|\t)/gm,'').replace(/> *</g, '><');
+    return page;
+  }
+
+  formatNumber (result) {
+    const result_class = result.grade == 1 ? 'assessment-number-result' : 'assessment-number-result-failed'; 
+    let template = this.formatOutput('assessment-number-expected',
+      'assessment-number-generated', result_class, result);
+    return template
+  }
+
+  formatBool (result) {
+    const result_class = result.grade == 1 ? 'assessment-bool-result' : 'assessment-bool-result-failed';
+    let template = this.formatOutput('assessment-bool-expected',
+      'assessment-bool-generated', result_class, result);
+    return template
+  }
+
+  formatOutput (expected_class, generated_class, result_class, result) {
+    let template = OutputAssessmentResult.OUTPUT_TEMPLATE;
+    template = template.replace(":class-expected:", expected_class);
+    template = template.replace(":class-generated:", generated_class);
+    template = template.replace(":class-result:", result_class);
+    let expected_tmpl = result.expected;
+    let generated_tmpl = result.generated;
+    if(expected_tmpl == null) {
+      expected_tmpl = OutputAssessmentResult.EMPTY_OUTPUT_TEMPLATE.replace('$0',
+        LocalizedStrings.getMessage('assessment-empty-expected-tooltip'));
+    } else if(generated_tmpl == null) {
+      generated_tmpl = OutputAssessmentResult.EMPTY_OUTPUT_TEMPLATE.replace('$0',
+        LocalizedStrings.getMessage('assessment-empty-generated-tooltip'));
+    }
+    template = template.replace("$0", expected_tmpl);
+    template = template.replace("$1", generated_tmpl);
+    const final_result = result.grade == 1 ? "✓" : "✗"
+    template = template.replace("$2", final_result);
+    return template
+  }
+
+  formatString (result) {
+    const expected_class = 'assessment-string-expected';
+    const generated_class = 'assessment-string-generated';
+    //const result_class = 'assessment-string-result';
+
+    let template = OutputAssessmentResult.OUTPUT_TEMPLATE;
+    template = template.replace(":class-expected:", expected_class);
+    template = template.replace(":class-generated:", generated_class);
+    //template = template.replace(":class-result:", result_class);
+
+    const g_string = result.generated || "";
+    const e_string = result.expected || "";
+    template = template.replace("$0", result.expected);
+    template = template.replace("$1", result.generated);
+    if(result.grade == 1) {
+      template = template.replace("$2", "✓");
+      template = template.replace(":class-result:", 'assessment-string-result');
+    } else {
+      const diff = StringDiff(g_string, e_string);
+      const diff_vec = diff.map(part => this.getDiffStringStyle(part[1], part[0]), this);
+      const diff_string = diff_vec.reduce((prev, actual) => prev + actual, "");
+      template = template.replace("$2", "<span class='assessment-failed-case'>✗</span>" + diff_string);
+      template = template.replace(":class-result:", "assessment-string-diff");
+    }
+    return template;
+  }
+
+  getDiffStringStyle (text, action) {
+    const template = "<span class='$0'>$1</span>"
+    switch(action) {
+      case StringDiff.INSERT:
+        return template.replace("$0", "stringdiff-insert").replace("$1", text);
+      case StringDiff.DELETE:
+        return template.replace("$0", "stringdiff-delete").replace("$1", text);
+      case StringDiff.EQUAL:
+        return template.replace("$0", "stringdiff-equal").replace("$1", text);
+    }
+  }
+}

+ 175 - 0
js/assessment/output_matching/output_matching.js

@@ -0,0 +1,175 @@
+import { Decimal } from 'decimal.js';
+import { InputAssessment } from "../../util/input_assessment";
+import { OutputTest } from "../../util/outputTest";
+import { Config } from "../../util/config";
+import { levenshteinDistance } from "../../util/utils";
+import { OutputAssessmentResult } from './assessment_result';
+import * as TypeParser from "./../../typeSystem/parsers";
+import * as LocalizedStringsService from "../../services/localizedStringsService";
+import * as OutputResult from "./output_result";
+
+const LocalizedStrings = LocalizedStringsService.getInstance();
+
+export class OutputMatching {
+
+  static get NUM_REGEX () {
+    return /^[+-]?([0-9]+([.][0-9]*)?(e[+-]?[0-9]+)?)$/;
+  } 
+
+  static get NUM_IN_STRING_REGEX () {
+    return /[+-]?([0-9]+([.][0-9]*)?(e[+-]?[0-9]+)?)/g;
+  }
+
+  static get BOOLEAN_REGEX () {
+    const str = `^(${LocalizedStrings.getUI("logic_value_true")}|${LocalizedStrings.getUI("logic_value_false")})$`;
+    return new RegExp(str);
+  }
+
+  static get BOOLEAN_IN_STRING_REGEX () {
+    const str = `(${LocalizedStrings.getUI("logic_value_true")}|${LocalizedStrings.getUI("logic_value_false")})`;
+    return new RegExp(str, 'g');
+  }
+  
+  constructor (program, input_list, expected_output, test_name) {
+    this.program = program;
+    this.name = test_name;
+    this.input_list = input_list;
+    this.expected_output = expected_output;
+  }
+
+  eval () {
+    const refThis = this;
+    const input = new InputAssessment(this.input_list);
+    const gen_output = new OutputTest();
+    this.program.registerInput(input);
+    this.program.registerOutput(gen_output);
+    const start_time = Date.now();
+    return this.program.interpretAST().then( sto => {
+      const final_time = Date.now() - start_time;
+      if(input.isInputAvailable()) {
+        return new OutputAssessmentResult(this.name, 1, input.input_list,
+          null, sto, final_time, refThis.getErrorMessage('test_case_few_reads', this.name+1))
+      }
+      const result = gen_output.list.map((g_out, i) => {
+        if(i >= this.expected_output.length) {
+          return new OutputResult.OutputMatchResult(null, g_out, 0, this.getPotentialOutputType(g_out));
+        }
+        return this.outputMatch(g_out, this.expected_output[i]);
+      }, this);
+      if(this.expected_output.length > gen_output.list.length) {
+        for(let i = gen_output.list.length; i < this.expected_output.length; ++i) {
+          const e_out = this.expected_output[i];
+          result.push(new OutputResult.OutputMatchResult(e_out, null, 0, this.getPotentialOutputType(e_out)));
+        }
+      }
+      return new OutputAssessmentResult(this.name, 0,  input.input_list, result, sto, final_time);
+    }).catch(error => {
+      return new OutputAssessmentResult(this.name, 1,  input.input_list, null, null,
+        null, refThis.getErrorMessage('test_case_exception', this.name + 1, error.message))
+    });
+  }
+
+  getPotentialOutputType (output) {
+    if(OutputMatching.NUM_REGEX.test(output)) {
+      return "number";
+    } else if (OutputMatching.BOOLEAN_REGEX.test(output)) {
+      return "bool";
+    } else {
+      return "string";
+    }
+  }
+
+  outputMatch (g_output, e_output) {
+    if(OutputMatching.NUM_REGEX.test(e_output)) {
+      if(!OutputMatching.NUM_REGEX.test(g_output)) {
+        return OutputResult.createNumberResult(e_output, g_output, 0);
+      }
+      const g_num = new Decimal(g_output);
+      const e_num = new Decimal(e_output);
+      return this.checkNumbers(g_num, e_num);
+    } else if (OutputMatching.BOOLEAN_REGEX.test(e_output)) {
+      if (!OutputMatching.BOOLEAN_REGEX.test(g_output)) {
+        return OutputResult.createBoolResult(e_output, g_output, 0);
+      }
+      const g_bool = TypeParser.toBool(g_output);
+      const e_bool = TypeParser.toBool(e_output);
+      return this.checkBoolean(g_bool, e_bool);
+    } else {
+      return this.checkStrings(g_output, e_output);
+    }
+  }
+
+  checkNumbers (g_num, e_num) {
+    const decimalPlaces = Math.min(e_num.dp(), Config.decimalPlaces);
+    g_num = new Decimal(g_num.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
+    e_num = new Decimal(e_num.toFixed(decimalPlaces, Decimal.ROUND_FLOOR));
+    const result = g_num.eq(e_num);
+    const grade = result ? 1 : 0;
+    return OutputResult.createNumberResult(e_num.toNumber(), g_num.toNumber(), grade);
+  }
+
+  checkBoolean (g_bool, e_bool) {
+    const grade = g_bool == e_bool ? 1 : 0;
+    const g_bool_text = TypeParser.convertBoolToString(g_bool);
+    const e_bool_text = TypeParser.convertBoolToString(e_bool);
+    return OutputResult.createBoolResult(e_bool_text, g_bool_text, grade);
+  }
+
+  checkStrings (g_output, e_ouput) {
+    const assessmentList = []
+    let e_output_clean = e_ouput;
+    let g_output_clean = g_output;
+    if (OutputMatching.NUM_IN_STRING_REGEX.test(e_ouput)) {
+      const expected_numbers = e_ouput.match(OutputMatching.NUM_IN_STRING_REGEX);
+      const generated_numbers = g_output.match(OutputMatching.NUM_IN_STRING_REGEX) || [];
+      const result = generated_numbers.map((val, i) => {
+        if(i >= expected_numbers.length) {
+          return OutputResult.createNumberResult(null, val, 0);
+        }
+        const g_val = new Decimal(val)
+        const e_val = new Decimal(expected_numbers[i]);
+        return this.checkNumbers(g_val, e_val);
+      }, this);
+      if(expected_numbers.length > generated_numbers.length) {
+        for(let i = generated_numbers.length; i < expected_numbers.length; ++i) {
+          result.push(OutputResult.createNumberResult(expected_numbers[i], null, 0));
+        }
+      }
+      e_output_clean = e_output_clean.replace(OutputMatching.NUM_IN_STRING_REGEX, '').trim();
+      g_output_clean = g_output_clean.replace(OutputMatching.NUM_IN_STRING_REGEX, '').trim();
+      const numberGrade = result.reduce((prev, r) => prev + r.grade, 0) / result.length;
+      assessmentList.push(numberGrade);
+    } 
+    if(OutputMatching.BOOLEAN_IN_STRING_REGEX.test(e_ouput)) {
+      const expected_bools = e_ouput.match(OutputMatching.BOOLEAN_IN_STRING_REGEX);
+      const generated_bools = g_output.match(OutputMatching.BOOLEAN_IN_STRING_REGEX) || [];
+      const result = generated_bools.map((val, i) => {
+        if(i >= expected_bools.length) {
+          return OutputResult.createBoolResult(null, val, 0);
+        }
+        const g_bool = TypeParser.toBool(val);
+        const e_bool = TypeParser.toBool(expected_bools[i]);
+        return this.checkBoolean(g_bool, e_bool );
+      }, this);
+      if(expected_bools.length > generated_bools.length) {
+        for(let i = generated_bools.length; i < expected_bools.length; ++i) {
+          result.push(OutputResult.createBoolResult(expected_bools[i], null, 0));
+        }
+      }
+      e_output_clean = e_output_clean.replace(OutputMatching.BOOLEAN_IN_STRING_REGEX, '').trim();
+      g_output_clean = g_output_clean.replace(OutputMatching.BOOLEAN_IN_STRING_REGEX, '').trim();
+      const boolGrade = result.reduce((prev, r) => prev + r.grade, 0) / result.length;
+      assessmentList.push(boolGrade);
+    }
+    const dist = levenshteinDistance(g_output_clean, e_output_clean);
+    const gradeDiff = Math.max(0, e_output_clean.length - dist)/e_output_clean.length;
+    const assessment_size = assessmentList.length + 1;
+    const gradeAcc = assessmentList.reduce((prev, val) => prev + val/assessment_size, 0);
+    const finalGrade = 1 * (gradeDiff/assessment_size + gradeAcc);
+    return OutputResult.createStringResult(e_ouput, g_output, finalGrade);
+  }
+  
+  getErrorMessage (errorID, ...args) {
+    return LocalizedStrings.getError(errorID, args);
+  }
+}

+ 22 - 0
js/assessment/output_matching/output_result.js

@@ -0,0 +1,22 @@
+
+export function createNumberResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "number");
+}
+
+export function createBoolResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "bool");
+}
+
+export function createStringResult (expected, generated, grade) {
+  return new OutputMatchResult(expected, generated, grade, "string");
+}
+
+export class OutputMatchResult {
+
+  constructor (expected, generated, grade, type) {
+    this.expected = expected;
+    this.generated = generated;
+    this.grade = grade;
+    this.type = type;
+  }
+}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 2
js/jquery-3.3.1.min.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 18706
js/jquery-ui.js


+ 2 - 0
js/main.js

@@ -6,6 +6,7 @@ import * as LocalizedStringsService from './services/localizedStringsService';
 import { i18nHelper } from "./services/i18nHelper";
 import { ActionTypes, getLogs, getLogsAsString, registerClick, registerUserEvent, parseLogs } from "./services/userLog";
 import { prepareActivityToStudentHelper, autoEval } from "./util/iassignHelpers";
+import { openAssessmentDetail } from "./util/utils";
 import * as CodeEditorAll from "./visualUI/text_editor";
 import {autoGenerateTestCaseOutput} from './util/auto_gen_output';
 
@@ -38,5 +39,6 @@ export {
   parseLogs,
   ActionTypes,
   CodeEditor,
+  openAssessmentDetail,
   autoGenerateTestCaseOutput
 }

+ 3 - 3
js/runner.js

@@ -19,15 +19,15 @@ const ivprogLexer = LanguageService.getCurrentLexer();
 //     i++;
 // }
 // const anaSin = new IVProgParser(input, ivprogLexer);
-const editor = new JsonEditor('#json-renderer', {});
+const editor = new window.JsonEditor('#json-renderer', {});
 const domConsole = new DOMConsole("#console", true);
 domConsole.hide();
 // proc.interpretAST().then( sto => {
 //   console.log(sto.applyStore('a'));
 // }).catch(e => console.log(e));
 try {
-  $('#btn').click( () => {
-    const input = $('#input').val();
+  window.$('#btn').click( () => {
+    const input = window.$('#input').val();
     const analiser = new IVProgParser(input, ivprogLexer);
     try {
       const data = analiser.parseTree();

+ 4 - 4
js/services/localizedStringsService.js

@@ -11,15 +11,15 @@ class IVProgLocalizedStrings extends line_i18n.LocalizedStrings {
   }
 
   translateType (type, dim) {
+    const type_string = this.getUI(`type_${type}`);
     switch (dim) {
       case 0:
-        return this.getUI(`type_${type}`);
+        return type_string;
       default:
-        const transType = this.getUI(`type_${type}`);
         if(dim === 1)
-          return this.getUI("matrix_info_string", [transType])
+          return this.getUI("matrix_info_string", [type_string])
         else
-          return this.getUI("vector_info_string", [transType])
+          return this.getUI("vector_info_string", [type_string])
     }
   }
   

+ 2 - 3
js/typeSystem/parsers.js

@@ -14,8 +14,8 @@ export function toString (str) {
   value = value.replace(/\\t/g, "\t");
   value = value.replace(/\\n/g, "\n");
   value = value.replace(/\\r/g, "\r");
-  value = value.replace(/\\\"/g, "\"");
-  value = value.replace(/\\\'/g, "\'");
+  value = value.replace(/\\"/g, "\"");
+  value = value.replace(/\\'/g, '\'');
   value = value.replace(/\\\\/g, "\\");
   return value;
 }
@@ -51,7 +51,6 @@ export function convertBoolToString (bool) {
 }
 
 export function convertToString(value, type) {
-  console.log(value);
   switch (type.ord) {
     case Types.INTEGER.ord:
       return value.toString();

+ 135 - 0
js/util/base64.js

@@ -0,0 +1,135 @@
+/**
+*
+*  Base64 encode / decode
+*  Source: http://www.webtoolkit.info/
+*  Modified by: @lucascalion - 24/07/2019
+*
+**/
+
+const _keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+  
+export function encode (input) {
+      let output = "";
+      let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
+      let i = 0;
+  
+      input = _utf8_encode(input);
+  
+      while (i < input.length) {
+  
+          chr1 = input.charCodeAt(i++);
+          chr2 = input.charCodeAt(i++);
+          chr3 = input.charCodeAt(i++);
+  
+          enc1 = chr1 >> 2;
+          enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+          enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+          enc4 = chr3 & 63;
+  
+          if (isNaN(chr2)) {
+              enc3 = enc4 = 64;
+          } else if (isNaN(chr3)) {
+              enc4 = 64;
+          }
+  
+          output = output +
+            _keyStr.charAt(enc1) + _keyStr.charAt(enc2) +
+            _keyStr.charAt(enc3) + _keyStr.charAt(enc4);
+  
+      }
+  
+      return output;
+}
+
+export function decode (input) {
+      let output = "";
+      let chr1, chr2, chr3;
+      let enc1, enc2, enc3, enc4;
+      let i = 0;
+  
+      input = input.replace(/[^A-Za-z0-9+/=]/g, "");
+  
+      while (i < input.length) {
+  
+          enc1 = _keyStr.indexOf(input.charAt(i++));
+          enc2 = _keyStr.indexOf(input.charAt(i++));
+          enc3 = _keyStr.indexOf(input.charAt(i++));
+          enc4 = _keyStr.indexOf(input.charAt(i++));
+  
+          chr1 = (enc1 << 2) | (enc2 >> 4);
+          chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
+          chr3 = ((enc3 & 3) << 6) | enc4;
+  
+          output = output + String.fromCharCode(chr1);
+  
+          if (enc3 != 64) {
+              output = output + String.fromCharCode(chr2);
+          }
+          if (enc4 != 64) {
+              output = output + String.fromCharCode(chr3);
+          }
+  
+      }
+  
+      output = _utf8_decode(output);
+  
+      return output;
+  
+}
+
+function  _utf8_encode (string) {
+      string = string.replace(/\r\n/g,"\n");
+      let utftext = "";
+  
+      for (let n = 0; n < string.length; n++) {
+  
+          const c = string.charCodeAt(n);
+  
+          if (c < 128) {
+              utftext += String.fromCharCode(c);
+          }
+          else if((c > 127) && (c < 2048)) {
+              utftext += String.fromCharCode((c >> 6) | 192);
+              utftext += String.fromCharCode((c & 63) | 128);
+          }
+          else {
+              utftext += String.fromCharCode((c >> 12) | 224);
+              utftext += String.fromCharCode(((c >> 6) & 63) | 128);
+              utftext += String.fromCharCode((c & 63) | 128);
+          }
+  
+      }
+  
+      return utftext;
+}
+
+function _utf8_decode (utftext) {
+      let string = "";
+      let i = 0;
+      let c, c2, c3;
+      c = c2 = c3 = 0;
+  
+      while ( i < utftext.length ) {
+  
+          c = utftext.charCodeAt(i);
+  
+          if (c < 128) {
+              string += String.fromCharCode(c);
+              i++;
+          }
+          else if((c > 191) && (c < 224)) {
+              c2 = utftext.charCodeAt(i+1);
+              string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
+              i += 2;
+          }
+          else {
+              c2 = utftext.charCodeAt(i+1);
+              c3 = utftext.charCodeAt(i+2);
+              string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
+              i += 3;
+          }
+  
+      }
+  
+      return string;
+}

+ 28 - 0
js/util/input_assessment.js

@@ -0,0 +1,28 @@
+import { Input } from './../io/input';
+import { LocalizedStrings } from '../services/localizedStringsService';
+
+export class InputAssessment extends Input {
+
+  constructor (input_list) {
+    super();
+    this.index = 0;
+    this.input_list = input_list.map((val) => {
+      return {"value": val, "read": false};
+    });
+  }
+
+  requestInput (callback) {
+    if(this.index < this.input_list.length) {
+      const input = this.input_list[this.index];
+      input.read = true;
+      this.index += 1;
+      callback(input.value);
+    } else {
+      throw new Error(LocalizedStrings.getError("exceeded_input_request"));
+    }
+  }
+
+  isInputAvailable () {
+    return this.index < this.input_list.length;
+  }
+}

+ 7 - 1
js/util/outputTest.js

@@ -8,6 +8,12 @@ export class OutputTest extends Output {
   }
 
   sendOutput (text) {
-    this.list.push(text);
+    const output = ''+text;
+    output.split("\n").forEach(t => {
+      t = t.replace(/\t/g,'  ');
+      t = t.replace(/\s/g," ");
+      this.list.push(t);
+    },this);
+    
   }
 }

+ 776 - 0
js/util/string_diff.js

@@ -0,0 +1,776 @@
+/**
+ * Author: @jhchen - https://github.com/jhchen
+ * Modified by: @lucascalion - 23/07/2019
+ * This library modifies the diff-patch-match library by Neil Fraser
+ * by removing the patch and match functionality and certain advanced
+ * options in the diff function. The original license is as follows:
+ *
+ * ===
+ *
+ * Diff Match and Patch
+ *
+ * Copyright 2006 Google Inc.
+ * http://code.google.com/p/google-diff-match-patch/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+/**
+ * The data structure representing a diff is an array of tuples:
+ * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
+ * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
+ */
+const DIFF_DELETE = -1;
+const DIFF_INSERT = 1;
+const DIFF_EQUAL = 0;
+
+
+/**
+ * Find the differences between two texts.  Simplifies the problem by stripping
+ * any common prefix or suffix off the texts before diffing.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {Int|Object} [cursor_pos] Edit position in text1 or object with more info
+ * @return {Array} Array of diff tuples.
+ */
+function diff_main(text1, text2, cursor_pos, _fix_unicode) {
+  // Check for equality
+  if (text1 === text2) {
+    if (text1) {
+      return [[DIFF_EQUAL, text1]];
+    }
+    return [];
+  }
+
+  if (cursor_pos != null) {
+    const editdiff = find_cursor_edit_diff(text1, text2, cursor_pos);
+    if (editdiff) {
+      return editdiff;
+    }
+  }
+
+  // Trim off common prefix (speedup).
+  let commonlength = diff_commonPrefix(text1, text2);
+  const commonprefix = text1.substring(0, commonlength);
+  text1 = text1.substring(commonlength);
+  text2 = text2.substring(commonlength);
+
+  // Trim off common suffix (speedup).
+  commonlength = diff_commonSuffix(text1, text2);
+  const commonsuffix = text1.substring(text1.length - commonlength);
+  text1 = text1.substring(0, text1.length - commonlength);
+  text2 = text2.substring(0, text2.length - commonlength);
+
+  // Compute the diff on the middle block.
+  const diffs = diff_compute_(text1, text2);
+
+  // Restore the prefix and suffix.
+  if (commonprefix) {
+    diffs.unshift([DIFF_EQUAL, commonprefix]);
+  }
+  if (commonsuffix) {
+    diffs.push([DIFF_EQUAL, commonsuffix]);
+  }
+  diff_cleanupMerge(diffs, _fix_unicode);
+  return diffs;
+}
+
+
+/**
+ * Find the differences between two texts.  Assumes that the texts do not
+ * have any common prefix or suffix.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @return {Array} Array of diff tuples.
+ */
+function diff_compute_(text1, text2) {
+  let diffs;
+
+  if (!text1) {
+    // Just add some text (speedup).
+    return [[DIFF_INSERT, text2]];
+  }
+
+  if (!text2) {
+    // Just delete some text (speedup).
+    return [[DIFF_DELETE, text1]];
+  }
+
+  const longtext = text1.length > text2.length ? text1 : text2;
+  const shorttext = text1.length > text2.length ? text2 : text1;
+  const i = longtext.indexOf(shorttext);
+  if (i !== -1) {
+    // Shorter text is inside the longer text (speedup).
+    diffs = [
+      [DIFF_INSERT, longtext.substring(0, i)],
+      [DIFF_EQUAL, shorttext],
+      [DIFF_INSERT, longtext.substring(i + shorttext.length)]
+    ];
+    // Swap insertions for deletions if diff is reversed.
+    if (text1.length > text2.length) {
+      diffs[0][0] = diffs[2][0] = DIFF_DELETE;
+    }
+    return diffs;
+  }
+
+  if (shorttext.length === 1) {
+    // Single character string.
+    // After the previous speedup, the character can't be an equality.
+    return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+  }
+
+  // Check to see if the problem can be split in two.
+  var hm = diff_halfMatch_(text1, text2);
+  if (hm) {
+    // A half-match was found, sort out the return data.
+    var text1_a = hm[0];
+    var text1_b = hm[1];
+    var text2_a = hm[2];
+    var text2_b = hm[3];
+    var mid_common = hm[4];
+    // Send both pairs off for separate processing.
+    var diffs_a = diff_main(text1_a, text2_a);
+    var diffs_b = diff_main(text1_b, text2_b);
+    // Merge the results.
+    return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
+  }
+
+  return diff_bisect_(text1, text2);
+}
+
+
+/**
+ * Find the 'middle snake' of a diff, split the problem in two
+ * and return the recursively constructed diff.
+ * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @return {Array} Array of diff tuples.
+ * @private
+ */
+function diff_bisect_(text1, text2) {
+  // Cache the text lengths to prevent multiple calls.
+  const text1_length = text1.length;
+  const text2_length = text2.length;
+  const max_d = Math.ceil((text1_length + text2_length) / 2);
+  const v_offset = max_d;
+  const v_length = 2 * max_d;
+  const v1 = new Array(v_length);
+  const v2 = new Array(v_length);
+  // Setting all elements to -1 is faster in Chrome & Firefox than mixing
+  // integers and undefined.
+  for (var x = 0; x < v_length; x++) {
+    v1[x] = -1;
+    v2[x] = -1;
+  }
+  v1[v_offset + 1] = 0;
+  v2[v_offset + 1] = 0;
+  const delta = text1_length - text2_length;
+  // If the total number of characters is odd, then the front path will collide
+  // with the reverse path.
+  const front = (delta % 2 !== 0);
+  // Offsets for start and end of k loop.
+  // Prevents mapping of space beyond the grid.
+  let k1start = 0;
+  let k1end = 0;
+  let k2start = 0;
+  let k2end = 0;
+  for (let d = 0; d < max_d; d++) {
+    // Walk the front path one step.
+    for (let k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
+      const k1_offset = v_offset + k1;
+      let x1;
+      if (k1 === -d || (k1 !== d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
+        x1 = v1[k1_offset + 1];
+      } else {
+        x1 = v1[k1_offset - 1] + 1;
+      }
+      let y1 = x1 - k1;
+      while (
+        x1 < text1_length && y1 < text2_length &&
+        text1.charAt(x1) === text2.charAt(y1)
+      ) {
+        x1++;
+        y1++;
+      }
+      v1[k1_offset] = x1;
+      if (x1 > text1_length) {
+        // Ran off the right of the graph.
+        k1end += 2;
+      } else if (y1 > text2_length) {
+        // Ran off the bottom of the graph.
+        k1start += 2;
+      } else if (front) {
+        const k2_offset = v_offset + delta - k1;
+        if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] !== -1) {
+          // Mirror x2 onto top-left coordinate system.
+          const x2 = text1_length - v2[k2_offset];
+          if (x1 >= x2) {
+            // Overlap detfrontected.
+            return diff_bisectSplit_(text1, text2, x1, y1);
+          }
+        }
+      }
+    }
+
+    // Walk the reverse path one step.
+    for (let k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
+      const k2_offset = v_offset + k2;
+      let x2;
+      if (k2 === -d || (k2 !== d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
+        x2 = v2[k2_offset + 1];
+      } else {
+        x2 = v2[k2_offset - 1] + 1;
+      }
+      let y2 = x2 - k2;
+      while (
+        x2 < text1_length && y2 < text2_length &&
+        text1.charAt(text1_length - x2 - 1) === text2.charAt(text2_length - y2 - 1)
+      ) {
+        x2++;
+        y2++;
+      }
+      v2[k2_offset] = x2;
+      if (x2 > text1_length) {
+        // Ran off the left of the graph.
+        k2end += 2;
+      } else if (y2 > text2_length) {
+        // Ran off the top of the graph.
+        k2start += 2;
+      } else if (!front) {
+        const k1_offset = v_offset + delta - k2;
+        if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] !== -1) {
+          const x1 = v1[k1_offset];
+          const y1 = v_offset + x1 - k1_offset;
+          // Mirror x2 onto top-left coordinate system.
+          x2 = text1_length - x2;
+          if (x1 >= x2) {
+            // Overlap detected.
+            return diff_bisectSplit_(text1, text2, x1, y1);
+          }
+        }
+      }
+    }
+  }
+  // Diff took too long and hit the deadline or
+  // number of diffs equals number of characters, no commonality at all.
+  return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
+}
+
+
+/**
+ * Given the location of the 'middle snake', split the diff in two parts
+ * and recurse.
+ * @param {string} text1 Old string to be diffed.
+ * @param {string} text2 New string to be diffed.
+ * @param {number} x Index of split point in text1.
+ * @param {number} y Index of split point in text2.
+ * @return {Array} Array of diff tuples.
+ */
+function diff_bisectSplit_(text1, text2, x, y) {
+  const text1a = text1.substring(0, x);
+  const text2a = text2.substring(0, y);
+  const text1b = text1.substring(x);
+  const text2b = text2.substring(y);
+
+  // Compute both diffs serially.
+  const diffs = diff_main(text1a, text2a);
+  const diffsb = diff_main(text1b, text2b);
+
+  return diffs.concat(diffsb);
+}
+
+
+/**
+ * Determine the common prefix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the start of each
+ *     string.
+ */
+function diff_commonPrefix(text1, text2) {
+  // Quick check for common null cases.
+  if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) {
+    return 0;
+  }
+  // Binary search.
+  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+  let pointermin = 0;
+  let pointermax = Math.min(text1.length, text2.length);
+  let pointermid = pointermax;
+  let pointerstart = 0;
+  while (pointermin < pointermid) {
+    if (
+      text1.substring(pointerstart, pointermid) ==
+      text2.substring(pointerstart, pointermid)
+    ) {
+      pointermin = pointermid;
+      pointerstart = pointermin;
+    } else {
+      pointermax = pointermid;
+    }
+    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+  }
+
+  if (is_surrogate_pair_start(text1.charCodeAt(pointermid - 1))) {
+    pointermid--;
+  }
+
+  return pointermid;
+}
+
+
+/**
+ * Determine the common suffix of two strings.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {number} The number of characters common to the end of each string.
+ */
+function diff_commonSuffix(text1, text2) {
+  // Quick check for common null cases.
+  if (!text1 || !text2 || text1.slice(-1) !== text2.slice(-1)) {
+    return 0;
+  }
+  // Binary search.
+  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
+  let pointermin = 0;
+  let pointermax = Math.min(text1.length, text2.length);
+  let pointermid = pointermax;
+  let pointerend = 0;
+  while (pointermin < pointermid) {
+    if (
+      text1.substring(text1.length - pointermid, text1.length - pointerend) ==
+      text2.substring(text2.length - pointermid, text2.length - pointerend)
+    ) {
+      pointermin = pointermid;
+      pointerend = pointermin;
+    } else {
+      pointermax = pointermid;
+    }
+    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
+  }
+
+  if (is_surrogate_pair_end(text1.charCodeAt(text1.length - pointermid))) {
+    pointermid--;
+  }
+
+  return pointermid;
+}
+
+
+/**
+ * Do the two texts share a substring which is at least half the length of the
+ * longer text?
+ * This speedup can produce non-minimal diffs.
+ * @param {string} text1 First string.
+ * @param {string} text2 Second string.
+ * @return {Array.<string>} Five element Array, containing the prefix of
+ *     text1, the suffix of text1, the prefix of text2, the suffix of
+ *     text2 and the common middle.  Or null if there was no match.
+ */
+function diff_halfMatch_(text1, text2) {
+  const longtext = text1.length > text2.length ? text1 : text2;
+  const shorttext = text1.length > text2.length ? text2 : text1;
+  if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
+    return null;  // Pointless.
+  }
+
+  /**
+   * Does a substring of shorttext exist within longtext such that the substring
+   * is at least half the length of longtext?
+   * Closure, but does not reference any external variables.
+   * @param {string} longtext Longer string.
+   * @param {string} shorttext Shorter string.
+   * @param {number} i Start index of quarter length substring within longtext.
+   * @return {Array.<string>} Five element Array, containing the prefix of
+   *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
+   *     of shorttext and the common middle.  Or null if there was no match.
+   * @private
+   */
+  function diff_halfMatchI_(longtext, shorttext, i) {
+    // Start with a 1/4 length substring at position i as a seed.
+    const seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
+    let j = -1;
+    let best_common = '';
+    let best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
+    while ((j = shorttext.indexOf(seed, j + 1)) !== -1) {
+      const prefixLength = diff_commonPrefix(
+        longtext.substring(i), shorttext.substring(j));
+      const suffixLength = diff_commonSuffix(
+        longtext.substring(0, i), shorttext.substring(0, j));
+      if (best_common.length < suffixLength + prefixLength) {
+        best_common = shorttext.substring(
+          j - suffixLength, j) + shorttext.substring(j, j + prefixLength);
+        best_longtext_a = longtext.substring(0, i - suffixLength);
+        best_longtext_b = longtext.substring(i + prefixLength);
+        best_shorttext_a = shorttext.substring(0, j - suffixLength);
+        best_shorttext_b = shorttext.substring(j + prefixLength);
+      }
+    }
+    if (best_common.length * 2 >= longtext.length) {
+      return [
+        best_longtext_a, best_longtext_b,
+        best_shorttext_a, best_shorttext_b, best_common
+      ];
+    } else {
+      return null;
+    }
+  }
+
+  // First check if the second quarter is the seed for a half-match.
+  const hm1 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 4));
+  // Check again based on the third quarter.
+  const hm2 = diff_halfMatchI_(longtext, shorttext, Math.ceil(longtext.length / 2));
+  let hm;
+  if (!hm1 && !hm2) {
+    return null;
+  } else if (!hm2) {
+    hm = hm1;
+  } else if (!hm1) {
+    hm = hm2;
+  } else {
+    // Both matched.  Select the longest.
+    hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
+  }
+
+  // A half-match was found, sort out the return data.
+  let text1_a, text1_b, text2_a, text2_b;
+  if (text1.length > text2.length) {
+    text1_a = hm[0];
+    text1_b = hm[1];
+    text2_a = hm[2];
+    text2_b = hm[3];
+  } else {
+    text2_a = hm[0];
+    text2_b = hm[1];
+    text1_a = hm[2];
+    text1_b = hm[3];
+  }
+  const mid_common = hm[4];
+  return [text1_a, text1_b, text2_a, text2_b, mid_common];
+}
+
+
+/**
+ * Reorder and merge like edit sections.  Merge equalities.
+ * Any edit section can move as long as it doesn't cross an equality.
+ * @param {Array} diffs Array of diff tuples.
+ * @param {boolean} fix_unicode Whether to normalize to a unicode-correct diff
+ */
+function diff_cleanupMerge(diffs, fix_unicode) {
+  diffs.push([DIFF_EQUAL, '']);  // Add a dummy entry at the end.
+  let pointer = 0;
+  let count_delete = 0;
+  let count_insert = 0;
+  let text_delete = '';
+  let text_insert = '';
+  let commonlength;
+  let previous_equality;
+  while (pointer < diffs.length) {
+    if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
+      diffs.splice(pointer, 1);
+      continue;
+    }
+    switch (diffs[pointer][0]) {
+      case DIFF_INSERT:
+        count_insert++;
+        text_insert += diffs[pointer][1];
+        pointer++;
+        break;
+      case DIFF_DELETE:
+        count_delete++;
+        text_delete += diffs[pointer][1];
+        pointer++;
+        break;
+      case DIFF_EQUAL:
+        previous_equality = pointer - count_insert - count_delete - 1;
+        if (fix_unicode) {
+          // prevent splitting of unicode surrogate pairs.  when fix_unicode is true,
+          // we assume that the old and new text in the diff are complete and correct
+          // unicode-encoded JS strings, but the tuple boundaries may fall between
+          // surrogate pairs.  we fix this by shaving off stray surrogates from the end
+          // of the previous equality and the beginning of this equality.  this may create
+          // empty equalities or a common prefix or suffix.  for example, if AB and AC are
+          // emojis, `[[0, 'A'], [-1, 'BA'], [0, 'C']]` would turn into deleting 'ABAC' and
+          // inserting 'AC', and then the common suffix 'AC' will be eliminated.  in this
+          // particular case, both equalities go away, we absorb any previous inequalities,
+          // and we keep scanning for the next equality before rewriting the tuples.
+          if (previous_equality >= 0 && ends_with_pair_start(diffs[previous_equality][1])) {
+            const stray = diffs[previous_equality][1].slice(-1);
+            diffs[previous_equality][1] = diffs[previous_equality][1].slice(0, -1);
+            text_delete = stray + text_delete;
+            text_insert = stray + text_insert;
+            if (!diffs[previous_equality][1]) {
+              // emptied out previous equality, so delete it and include previous delete/insert
+              diffs.splice(previous_equality, 1);
+              pointer--;
+              var k = previous_equality - 1;
+              if (diffs[k] && diffs[k][0] === DIFF_INSERT) {
+                count_insert++;
+                text_insert = diffs[k][1] + text_insert;
+                k--;
+              }
+              if (diffs[k] && diffs[k][0] === DIFF_DELETE) {
+                count_delete++;
+                text_delete = diffs[k][1] + text_delete;
+                k--;
+              }
+              previous_equality = k;
+            }
+          }
+          if (starts_with_pair_end(diffs[pointer][1])) {
+            const stray = diffs[pointer][1].charAt(0);
+            diffs[pointer][1] = diffs[pointer][1].slice(1);
+            text_delete += stray;
+            text_insert += stray;
+          }
+        }
+        if (pointer < diffs.length - 1 && !diffs[pointer][1]) {
+          // for empty equality not at end, wait for next equality
+          diffs.splice(pointer, 1);
+          break;
+        }
+        if (text_delete.length > 0 || text_insert.length > 0) {
+          // note that diff_commonPrefix and diff_commonSuffix are unicode-aware
+          if (text_delete.length > 0 && text_insert.length > 0) {
+            // Factor out any common prefixes.
+            commonlength = diff_commonPrefix(text_insert, text_delete);
+            if (commonlength !== 0) {
+              if (previous_equality >= 0) {
+                diffs[previous_equality][1] += text_insert.substring(0, commonlength);
+              } else {
+                diffs.splice(0, 0, [DIFF_EQUAL, text_insert.substring(0, commonlength)]);
+                pointer++;
+              }
+              text_insert = text_insert.substring(commonlength);
+              text_delete = text_delete.substring(commonlength);
+            }
+            // Factor out any common suffixes.
+            commonlength = diff_commonSuffix(text_insert, text_delete);
+            if (commonlength !== 0) {
+              diffs[pointer][1] =
+                text_insert.substring(text_insert.length - commonlength) + diffs[pointer][1];
+              text_insert = text_insert.substring(0, text_insert.length - commonlength);
+              text_delete = text_delete.substring(0, text_delete.length - commonlength);
+            }
+          }
+          // Delete the offending records and add the merged ones.
+          const n = count_insert + count_delete;
+          if (text_delete.length === 0 && text_insert.length === 0) {
+            diffs.splice(pointer - n, n);
+            pointer = pointer - n;
+          } else if (text_delete.length === 0) {
+            diffs.splice(pointer - n, n, [DIFF_INSERT, text_insert]);
+            pointer = pointer - n + 1;
+          } else if (text_insert.length === 0) {
+            diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete]);
+            pointer = pointer - n + 1;
+          } else {
+            diffs.splice(pointer - n, n, [DIFF_DELETE, text_delete], [DIFF_INSERT, text_insert]);
+            pointer = pointer - n + 2;
+          }
+        }
+        if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) {
+          // Merge this equality with the previous one.
+          diffs[pointer - 1][1] += diffs[pointer][1];
+          diffs.splice(pointer, 1);
+        } else {
+          pointer++;
+        }
+        count_insert = 0;
+        count_delete = 0;
+        text_delete = '';
+        text_insert = '';
+        break;
+    }
+  }
+  if (diffs[diffs.length - 1][1] === '') {
+    diffs.pop();  // Remove the dummy entry at the end.
+  }
+
+  // Second pass: look for single edits surrounded on both sides by equalities
+  // which can be shifted sideways to eliminate an equality.
+  // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
+  let changes = false;
+  pointer = 1;
+  // Intentionally ignore the first and last element (don't need checking).
+  while (pointer < diffs.length - 1) {
+    if (diffs[pointer - 1][0] === DIFF_EQUAL &&
+      diffs[pointer + 1][0] === DIFF_EQUAL) {
+      // This is a single edit surrounded by equalities.
+      if (diffs[pointer][1].substring(diffs[pointer][1].length -
+        diffs[pointer - 1][1].length) === diffs[pointer - 1][1]) {
+        // Shift the edit over the previous equality.
+        diffs[pointer][1] = diffs[pointer - 1][1] +
+          diffs[pointer][1].substring(0, diffs[pointer][1].length -
+            diffs[pointer - 1][1].length);
+        diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
+        diffs.splice(pointer - 1, 1);
+        changes = true;
+      } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
+        diffs[pointer + 1][1]) {
+        // Shift the edit over the next equality.
+        diffs[pointer - 1][1] += diffs[pointer + 1][1];
+        diffs[pointer][1] =
+          diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
+          diffs[pointer + 1][1];
+        diffs.splice(pointer + 1, 1);
+        changes = true;
+      }
+    }
+    pointer++;
+  }
+  // If shifts were made, the diff needs reordering and another shift sweep.
+  if (changes) {
+    diff_cleanupMerge(diffs, fix_unicode);
+  }
+}
+
+function is_surrogate_pair_start(charCode) {
+  return charCode >= 0xD800 && charCode <= 0xDBFF;
+}
+
+function is_surrogate_pair_end(charCode) {
+  return charCode >= 0xDC00 && charCode <= 0xDFFF;
+}
+
+function starts_with_pair_end(str) {
+  return is_surrogate_pair_end(str.charCodeAt(0));
+}
+
+function ends_with_pair_start(str) {
+  return is_surrogate_pair_start(str.charCodeAt(str.length - 1));
+}
+
+function remove_empty_tuples(tuples) {
+  const ret = [];
+  for (let i = 0; i < tuples.length; i++) {
+    if (tuples[i][1].length > 0) {
+      ret.push(tuples[i]);
+    }
+  }
+  return ret;
+}
+
+function make_edit_splice(before, oldMiddle, newMiddle, after) {
+  if (ends_with_pair_start(before) || starts_with_pair_end(after)) {
+    return null;
+  }
+  return remove_empty_tuples([
+    [DIFF_EQUAL, before],
+    [DIFF_DELETE, oldMiddle],
+    [DIFF_INSERT, newMiddle],
+    [DIFF_EQUAL, after]
+  ]);
+}
+
+function find_cursor_edit_diff(oldText, newText, cursor_pos) {
+  // note: this runs after equality check has ruled out exact equality
+  const oldRange = typeof cursor_pos === 'number' ?
+    { index: cursor_pos, length: 0 } : cursor_pos.oldRange;
+  const newRange = typeof cursor_pos === 'number' ?
+    null : cursor_pos.newRange;
+  // take into account the old and new selection to generate the best diff
+  // possible for a text edit.  for example, a text change from "xxx" to "xx"
+  // could be a delete or forwards-delete of any one of the x's, or the
+  // result of selecting two of the x's and typing "x".
+  const oldLength = oldText.length;
+  const newLength = newText.length;
+  if (oldRange.length === 0 && (newRange === null || newRange.length === 0)) {
+    // see if we have an insert or delete before or after cursor
+    const oldCursor = oldRange.index;
+    const oldBefore = oldText.slice(0, oldCursor);
+    const oldAfter = oldText.slice(oldCursor);
+    const maybeNewCursor = newRange ? newRange.index : null;
+    editBefore: {
+      // is this an insert or delete right before oldCursor?
+      const newCursor = oldCursor + newLength - oldLength;
+      if (maybeNewCursor !== null && maybeNewCursor !== newCursor) {
+        break editBefore;
+      }
+      if (newCursor < 0 || newCursor > newLength) {
+        break editBefore;
+      }
+      const newBefore = newText.slice(0, newCursor);
+      const newAfter = newText.slice(newCursor);
+      if (newAfter !== oldAfter) {
+        break editBefore;
+      }
+      const prefixLength = Math.min(oldCursor, newCursor);
+      const oldPrefix = oldBefore.slice(0, prefixLength);
+      const newPrefix = newBefore.slice(0, prefixLength);
+      if (oldPrefix !== newPrefix) {
+        break editBefore;
+      }
+      const oldMiddle = oldBefore.slice(prefixLength);
+      const newMiddle = newBefore.slice(prefixLength);
+      return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldAfter);
+    }
+    editAfter: {
+      // is this an insert or delete right after oldCursor?
+      if (maybeNewCursor !== null && maybeNewCursor !== oldCursor) {
+        break editAfter;
+      }
+      const cursor = oldCursor;
+      const newBefore = newText.slice(0, cursor);
+      const newAfter = newText.slice(cursor);
+      if (newBefore !== oldBefore) {
+        break editAfter;
+      }
+      const suffixLength = Math.min(oldLength - cursor, newLength - cursor);
+      const oldSuffix = oldAfter.slice(oldAfter.length - suffixLength);
+      const newSuffix = newAfter.slice(newAfter.length - suffixLength);
+      if (oldSuffix !== newSuffix) {
+        break editAfter;
+      }
+      const oldMiddle = oldAfter.slice(0, oldAfter.length - suffixLength);
+      const newMiddle = newAfter.slice(0, newAfter.length - suffixLength);
+      return make_edit_splice(oldBefore, oldMiddle, newMiddle, oldSuffix);
+    }
+  }
+  if (oldRange.length > 0 && newRange && newRange.length === 0) {
+    replaceRange: {
+      // see if diff could be a splice of the old selection range
+      const oldPrefix = oldText.slice(0, oldRange.index);
+      const oldSuffix = oldText.slice(oldRange.index + oldRange.length);
+      const prefixLength = oldPrefix.length;
+      const suffixLength = oldSuffix.length;
+      if (newLength < prefixLength + suffixLength) {
+        break replaceRange;
+      }
+      const newPrefix = newText.slice(0, prefixLength);
+      const newSuffix = newText.slice(newLength - suffixLength);
+      if (oldPrefix !== newPrefix || oldSuffix !== newSuffix) {
+        break replaceRange;
+      }
+      const oldMiddle = oldText.slice(prefixLength, oldLength - suffixLength);
+      const newMiddle = newText.slice(prefixLength, newLength - suffixLength);
+      return make_edit_splice(oldPrefix, oldMiddle, newMiddle, oldSuffix);
+    }
+  }
+
+  return null;
+}
+
+function diff(text1, text2, cursor_pos) {
+  // only pass fix_unicode=true at the top level, not when diff_main is
+  // recursively invoked
+  return diff_main(text1, text2, cursor_pos, true);
+}
+
+diff.INSERT = DIFF_INSERT;
+diff.DELETE = DIFF_DELETE;
+diff.EQUAL = DIFF_EQUAL;
+
+export default diff;

+ 52 - 1
js/util/utils.js

@@ -153,7 +153,7 @@ function fillCache () {
 export function getCodeEditorModeConfig () {
   const blockList = ["RK_SWITCH", "RK_PROGRAM","RK_CASE","RK_DEFAULT","RK_FOR",
     "RK_FUNCTION","RK_DO","RK_WHILE","RK_IF","RK_ELSE"]
-  const keywordsList = [,"RK_CONST","RK_RETURN","RK_BREAK"];
+  const keywordsList = ["RK_CONST","RK_RETURN","RK_BREAK"];
   const typeList = ["RK_REAL","RK_VOID","RK_BOOLEAN","RK_STRING","RK_INTEGER"];
   const atomList = ["RK_FALSE", "RK_TRUE"];
 
@@ -215,3 +215,54 @@ export function getCodeEditorModeConfig () {
     blocks: blocks
   }
 }
+
+/**
+ * Source: https://gist.github.com/andrei-m/982927
+ * @param {string} a 
+ * @param {string} b 
+ */
+export function levenshteinDistance (a, b) {
+  if(a.length == 0) return b.length; 
+  if(b.length == 0) return a.length; 
+
+  const matrix = [];
+
+  // increment along the first column of each row
+  let i;
+  for(i = 0; i <= b.length; i++){
+    matrix[i] = [i];
+  }
+
+  // increment each column in the first row
+  let j;
+  for(j = 0; j <= a.length; j++){
+    matrix[0][j] = j;
+  }
+
+  // Fill in the rest of the matrix
+  for(i = 1; i <= b.length; i++){
+    for(j = 1; j <= a.length; j++){
+      if(b.charCodeAt(i-1) == a.charCodeAt(j-1)){
+        matrix[i][j] = matrix[i-1][j-1];
+      } else {
+        matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
+                        Math.min(matrix[i][j-1] + 1, // insertion
+                          matrix[i-1][j] + 1)); // deletion
+      }
+    }
+  }
+  return matrix[b.length][a.length];
+}
+
+let win = null
+export function openAssessmentDetail (event) {
+  event.preventDefault();
+  const page_code = event.currentTarget.dataset.page;
+  if(win != null) {
+    win.close()
+  }
+  win = window.open("", "DetailWindow", "width=550,height=600");
+  win.document.open();
+  win.document.write(page_code);
+  win.document.close();
+}

+ 22 - 13
js/visualUI/functions.js

@@ -750,8 +750,8 @@ export function initVisualUI () {
   });
 
   $('.assessment').on('click', () => {
-    runCodeAssessment();
     is_iassign = true;
+    runCodeAssessment();
   });
 
   $('.div_toggle_console').on('click', () => {
@@ -919,22 +919,31 @@ function runCodeAssessment () {
 
   toggleConsole(true);
 
-  // if(domConsole == null)
-  //   domConsole = new DOMConsole("#ivprog-term");
-  // $("#ivprog-term").slideDown(500);
-  const runner = new IVProgAssessment(strCode, _testCases, domConsole);
-  isRunning = true;
-  runner.runTest().then(grade => {
+  try {
+    const data = SemanticAnalyser.analyseFromSource(strCode);
+    isRunning = true;
+    const runner = new IVProgAssessment(data, _testCases, domConsole);
+    runner.runTest().then(grade => {
+      if (!is_iassign) {
+        parent.getEvaluationCallback(grade);
+      } else {
+        is_iassign = false;
+      }
+      isRunning = false;
+    }).catch( err => {
+      console.log(err);
+      isRunning = false;
+    });
+  } catch (error) {
+    isRunning = false;
+    domConsole.err(error.message);
+    console.log(error);
     if (!is_iassign) {
-      parent.getEvaluationCallback(grade);
+      parent.getEvaluationCallback(0);
     } else {
       is_iassign = false;
     }
-    isRunning = false;
-  }).catch( err => {
-    console.log(err);
-    isRunning = false;
-  });
+  }
   return 0;
 }
 

Файловите разлики са ограничени, защото са твърде много
+ 739 - 34
package-lock.json


+ 1 - 0
package.json

@@ -32,6 +32,7 @@
     "babel-loader": "^8.0.5",
     "clean-webpack-plugin": "^2.0.1",
     "copy-webpack-plugin": "^5.0.2",
+    "eslint": "^6.1.0",
     "html-webpack-plugin": "^4.0.0-beta.5",
     "jasmine-core": "^3.4.0",
     "karma": "^4.1.0",

+ 2 - 0
webpack.config.js

@@ -59,11 +59,13 @@ module.exports = {
         {from:'js/iassign-integration-functions.js', to:path.resolve(__dirname, 'build/js')},
         {from:"css/ivprog-visual-1.0.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/ivprog-term.css", to:path.resolve(__dirname, 'build/css')},
+        {from:"css/ivprog-assessment.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/ivprog-editor.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/roboto.css", to:path.resolve(__dirname, 'build/css')},
         {from:"css/fonts/", to:path.resolve(__dirname, 'build/css/fonts')},
         {from:'js/Sortable.js', to:path.resolve(__dirname, 'build/js')},
         {from: 'img/trash-icon.png', to:path.resolve(__dirname, 'build/img')},
+        {from: 'img/empty.svg', to:path.resolve(__dirname, 'build/img')},
         {from:'js/jquery.json-editor.min.js', to:path.resolve(__dirname, 'build/js')},
         {from:'node_modules/codemirror/lib/codemirror.css', to:path.resolve(__dirname, 'build/css')},
         {from:'node_modules/codemirror/addon/hint/show-hint.css', to:path.resolve(__dirname, 'build/css')},