From 4f607633ddb43374e803fdbce49273cf33e676fa Mon Sep 17 00:00:00 2001 From: Rodrigo Fernandes Date: Sat, 12 Oct 2019 22:45:49 +0100 Subject: [PATCH] wip: Code and Tests working --- .eslintignore | 3 +- .gitignore | 2 +- README.md | 2 +- package.json | 45 +- scripts/build-templates.sh | 6 +- scripts/hulk.ts | 40 +- ...f-parser-tests.js => diff-parser-tests.ts} | 502 +++++++++--------- ...{diff2html-tests.js => diff2html-tests.ts} | 126 ++--- ...er-tests.js => file-list-printer-tests.ts} | 62 ++- src/__tests__/hogan-cache-tests.js | 73 --- src/__tests__/hogan-cache-tests.ts | 66 +++ ...by-line-tests.js => line-by-line-tests.ts} | 278 +++++----- ...-utils-tests.js => printer-utils-tests.ts} | 61 +-- ...tests.js => side-by-side-printer-tests.ts} | 164 +++--- .../{utils-tests.js => utils-tests.ts} | 10 +- src/diff-parser.js | 448 ---------------- src/diff-parser.ts | 436 +++++++++++++++ src/diff2html.d.ts | 70 --- src/diff2html.js | 113 ---- src/diff2html.ts | 50 ++ src/file-list-printer.js | 54 -- src/file-list-renderer.ts | 32 ++ src/hoganjs-utils.js | 89 ---- src/hoganjs-utils.ts | 54 ++ src/html-printer.js | 31 -- src/line-by-line-printer.js | 256 --------- src/line-by-line-renderer.ts | 290 ++++++++++ src/printer-utils.js | 264 --------- src/rematch.js | 145 ----- src/rematch.ts | 137 +++++ src/render-utils.ts | 343 ++++++++++++ src/side-by-side-printer.js | 329 ------------ src/side-by-side-renderer.ts | 352 ++++++++++++ .../generic-column-line-number.mustache | 6 +- src/templates/generic-empty-diff.mustache | 4 +- src/ui/js/diff2html-ui.js | 223 -------- src/ui/js/diff2html-ui.ts | 220 ++++++++ src/ui/js/highlight.js-internals.js | 142 ----- src/ui/js/highlight.js-internals.ts | 134 +++++ src/utils.js | 48 -- src/utils.ts | 68 +++ tsconfig.json | 6 +- tsconfig.scripts.json | 2 +- typings/hoganjs.d.ts | 93 ++++ typings/merge.d.ts | 3 + .../pages/demo/demo-scripts.partial.mustache | 4 - website/templates/pages/demo/demo.js | 429 +++++++-------- yarn.lock | 227 ++++---- 48 files changed, 3318 insertions(+), 3224 deletions(-) rename src/__tests__/{diff-parser-tests.js => diff-parser-tests.ts} (53%) rename src/__tests__/{diff2html-tests.js => diff2html-tests.ts} (85%) rename src/__tests__/{file-list-printer-tests.js => file-list-printer-tests.ts} (81%) delete mode 100644 src/__tests__/hogan-cache-tests.js create mode 100644 src/__tests__/hogan-cache-tests.ts rename src/__tests__/{line-by-line-tests.js => line-by-line-tests.ts} (70%) rename src/__tests__/{printer-utils-tests.js => printer-utils-tests.ts} (63%) rename src/__tests__/{side-by-side-printer-tests.js => side-by-side-printer-tests.ts} (73%) rename src/__tests__/{utils-tests.js => utils-tests.ts} (71%) delete mode 100644 src/diff-parser.js create mode 100644 src/diff-parser.ts delete mode 100644 src/diff2html.d.ts delete mode 100644 src/diff2html.js create mode 100644 src/diff2html.ts delete mode 100644 src/file-list-printer.js create mode 100644 src/file-list-renderer.ts delete mode 100644 src/hoganjs-utils.js create mode 100644 src/hoganjs-utils.ts delete mode 100644 src/html-printer.js delete mode 100644 src/line-by-line-printer.js create mode 100644 src/line-by-line-renderer.ts delete mode 100644 src/printer-utils.js delete mode 100644 src/rematch.js create mode 100644 src/rematch.ts create mode 100644 src/render-utils.ts delete mode 100644 src/side-by-side-printer.js create mode 100644 src/side-by-side-renderer.ts delete mode 100644 src/ui/js/diff2html-ui.js create mode 100644 src/ui/js/diff2html-ui.ts delete mode 100644 src/ui/js/highlight.js-internals.js create mode 100644 src/ui/js/highlight.js-internals.ts delete mode 100644 src/utils.js create mode 100644 src/utils.ts create mode 100644 typings/hoganjs.d.ts create mode 100644 typings/merge.d.ts diff --git a/.eslintignore b/.eslintignore index 256e583..92c7d7c 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,5 @@ coverage/** build/** docs/** node_modules/** -src/diff2html-templates.js +src/diff2html-templates.* +typings/** diff --git a/.gitignore b/.gitignore index 05edf8a..7bf2904 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ bower_components/ /docs/ /dist/ /build/ -/src/diff2html-templates.js +/src/diff2html-templates.* diff --git a/README.md b/README.md index c5f5840..01022db 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ The HTML output accepts a Javascript object with configuration. Possible options - `matchingMaxComparisons`: perform at most this much comparisons for line matching a block of changes, default is `2500` - `maxLineSizeInBlockForComparison`: maximum number os characters of the bigger line in a block to apply comparison, default is `200` - `maxLineLengthHighlight`: only perform diff changes highlight if lines are smaller than this, default is `10000` - - `templates`: object with previously compiled templates to replace parts of the html + - `compiledTemplates`: object with previously compiled templates to replace parts of the html - `rawTemplates`: object with raw not compiled templates to replace parts of the html - `renderNothingWhenEmpty`: render nothing if the diff shows no change in its comparison: `true` or `false`, default is `false` > For more information regarding the possible templates look into [src/templates](https://github.com/rtfpessoa/diff2html/tree/master/src/templates) diff --git a/package.json b/package.json index f307b84..89b0f8f 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "coverage": "jest --collectCoverage", "coverage-html": "yarn run coverage && open ./coverage/index.html", "codacy": "cat ./coverage/lcov.info | codacy-coverage", - "build": "rm -rf build; yarn run build-scripts && yarn run build-css && yarn run build-templates && yarn run build-library && yarn run build-browser-bundle && yarn run build-website", + "build": "rm -rf build docs; yarn run build-scripts && yarn run build-css && yarn run build-templates && yarn run build-library && yarn run build-browser-bundle && yarn run build-website", "build-scripts": "tsc -p tsconfig.scripts.json", "build-css": "./scripts/build-css.sh", "build-templates": "./scripts/build-templates.sh", @@ -57,23 +57,22 @@ "fs": false }, "dependencies": { - "diff": "^4.0.1", - "hogan.js": "^3.0.2", - "merge": "^1.2.1", - "whatwg-fetch": "^3.0.0" + "diff": "4.0.1", + "hogan.js": "3.0.2" }, "devDependencies": { - "@types/hogan.js": "^3.0.0", + "@types/diff": "4.0.2", + "@types/highlight.js": "9.12.3", "@types/jest": "24.0.18", - "@types/mkdirp": "^0.5.2", - "@types/node": "^12.7.2", - "@types/nopt": "^3.0.29", + "@types/mkdirp": "0.5.2", + "@types/node": "12.7.2", + "@types/nopt": "3.0.29", "@typescript-eslint/eslint-plugin": "2.0.0", "@typescript-eslint/parser": "2.0.0", - "autoprefixer": "^9.6.0", - "browserify": "^16.5.0", - "clean-css-cli": "^4.3.0", - "codacy-coverage": "^3.4.0", + "autoprefixer": "9.6.0", + "browserify": "16.5.0", + "clean-css-cli": "4.3.0", + "codacy-coverage": "3.4.0", "eslint": "6.2.2", "eslint-config-prettier": "6.1.0", "eslint-config-standard": "14.0.1", @@ -81,17 +80,19 @@ "eslint-plugin-jest": "22.15.2", "eslint-plugin-node": "9.1.0", "eslint-plugin-prettier": "3.1.0", - "eslint-plugin-promise": "^4.2.1", - "eslint-plugin-standard": "^4.0.1", - "fast-html-parser": "^1.0.1", + "eslint-plugin-promise": "4.2.1", + "eslint-plugin-standard": "4.0.1", + "fast-html-parser": "1.0.1", + "highlight.js": "9.15.10", "jest": "24.9.0", - "mkdirp": "^0.5.1", - "nopt": "^4.0.1", - "postcss-cli": "^6.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "postcss-cli": "6.1.3", "prettier": "1.18.2", - "terser": "^4.3.8", - "ts-jest": "24.0.2", - "typescript": "^3.6.3" + "terser": "4.3.8", + "ts-jest": "24.1.0", + "typescript": "3.6.4", + "whatwg-fetch": "3.0.0" }, "resolutions": { "lodash": "4.17.15" diff --git a/scripts/build-templates.sh b/scripts/build-templates.sh index e8f2ce4..6b8fdb0 100755 --- a/scripts/build-templates.sh +++ b/scripts/build-templates.sh @@ -5,6 +5,6 @@ set -e SCRIPT_DIRECTORY="$( cd "$( dirname "$0" )" && pwd )" node ${SCRIPT_DIRECTORY}/../build/scripts/hulk.js \ - --wrapper node \ - --variable 'browserTemplates' \ - ${SCRIPT_DIRECTORY}/../src/templates/*.mustache > ${SCRIPT_DIRECTORY}/../src/diff2html-templates.js + --wrapper ts \ + --variable 'defaultTemplates' \ + ${SCRIPT_DIRECTORY}/../src/templates/*.mustache > ${SCRIPT_DIRECTORY}/../src/diff2html-templates.ts diff --git a/scripts/hulk.ts b/scripts/hulk.ts index b668fb6..f078abe 100755 --- a/scripts/hulk.ts +++ b/scripts/hulk.ts @@ -19,7 +19,7 @@ import * as path from "path"; import * as fs from "fs"; import * as hogan from "hogan.js"; -import * as nopt from "nopt"; +import nopt from "nopt"; import * as mkderp from "mkdirp"; const options = nopt( @@ -52,10 +52,9 @@ function cyan(text: string): string { } function extractFiles(files: string[]): string[] { - const usage = ` - ${cyan( - "USAGE:" - )} hulk [--wrapper wrapper] [--outputdir outputdir] [--namespace namespace] [--variable variable] FILES + const usage = `${cyan( + "USAGE:" + )} hulk [--wrapper wrapper] [--outputdir outputdir] [--namespace namespace] [--variable variable] FILES ${cyan("OPTIONS:")} [-w, --wrapper] :: wraps the template (i.e. amd) [-o, --outputdir] :: outputs the templates as individual files to a directory @@ -130,6 +129,8 @@ function wrap(file: string, name: string, openedFile: string): string { // If we have a template per file the export will expose the template directly return options.outputdir ? `global.${objectStmt};\nmodule.exports = ${objectAccessor};` : `global.${objectStmt}`; + case "ts": + return `// @ts-ignore\n${objectStmt}`; default: return objectStmt; } @@ -141,19 +142,18 @@ function prepareOutput(content: string): string { case "amd": return content; case "node": - return ( - "(function() {\n" + - "if (!!!global." + - variableName + - ") global." + - variableName + - " = {};\n" + - 'var Hogan = require("hogan.js");' + - content + - "\n" + - (!options.outputdir ? "module.exports = global." + variableName + ";\n" : "") + - "})();" - ); + return `(function() { +if (!!!global.${variableName}) global.${variableName} = {}; +var Hogan = require("hogan.js"); +${content} +${!options.outputdir ? `module.exports = global.${variableName};\n` : ""})();`; + + case "ts": + return `import * as Hogan from "hogan.js"; +type CompiledTemplates = { [name: string]: Hogan.Template }; +export const ${variableName}: CompiledTemplates = {}; +${content}`; + default: return "if (!!!" + variableName + ") var " + variableName + " = {};\n" + content; } @@ -181,7 +181,9 @@ const templates = extractFiles(options.argv.remain) if (!options.outputdir) return cleanFileContents; - return fs.writeFileSync(path.join(options.outputdir, `${name}.js`), prepareOutput(cleanFileContents)); + const fileExtension = options.wrapper === "ts" ? "ts" : "js"; + + return fs.writeFileSync(path.join(options.outputdir, `${name}.${fileExtension}`), prepareOutput(cleanFileContents)); }) .filter(templateContents => typeof templateContents !== "undefined"); diff --git a/src/__tests__/diff-parser-tests.js b/src/__tests__/diff-parser-tests.ts similarity index 53% rename from src/__tests__/diff-parser-tests.js rename to src/__tests__/diff-parser-tests.ts index c0de992..34a8db3 100644 --- a/src/__tests__/diff-parser-tests.js +++ b/src/__tests__/diff-parser-tests.ts @@ -1,19 +1,19 @@ -const DiffParser = require("../diff-parser.js").DiffParser; +import { parse } from "../diff-parser"; -function checkDiffSample(diff) { - const result = DiffParser.generateDiffJson(diff); +function checkDiffSample(diff: string): void { + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample").toEqual(file1.oldName); - expect("sample").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample"); + expect(file1.newName).toEqual("sample"); + expect(file1.blocks.length).toEqual(1); } -describe("DiffParser", function() { - describe("generateDiffJson", function() { - it("should parse unix with \n diff", function() { +describe("DiffParser", () => { + describe("generateDiffJson", () => { + it("should parse unix with \n diff", () => { const diff = "diff --git a/sample b/sample\n" + "index 0000001..0ddf2ba\n" + @@ -25,7 +25,7 @@ describe("DiffParser", function() { checkDiffSample(diff); }); - it("should parse windows with \r\n diff", function() { + it("should parse windows with \r\n diff", () => { const diff = "diff --git a/sample b/sample\r\n" + "index 0000001..0ddf2ba\r\n" + @@ -37,7 +37,7 @@ describe("DiffParser", function() { checkDiffSample(diff); }); - it("should parse old os x with \r diff", function() { + it("should parse old os x with \r diff", () => { const diff = "diff --git a/sample b/sample\r" + "index 0000001..0ddf2ba\r" + @@ -49,7 +49,7 @@ describe("DiffParser", function() { checkDiffSample(diff); }); - it("should parse mixed eols diff", function() { + it("should parse mixed eols diff", () => { const diff = "diff --git a/sample b/sample\n" + "index 0000001..0ddf2ba\r\n" + @@ -61,7 +61,7 @@ describe("DiffParser", function() { checkDiffSample(diff); }); - it("should parse diff with special characters", function() { + it("should parse diff with special characters", () => { const diff = 'diff --git "a/bla with \ttab.scala" "b/bla with \ttab.scala"\n' + "index 4c679d7..e9bd385 100644\n" + @@ -72,17 +72,17 @@ describe("DiffParser", function() { "+cenas com ananas\n" + "+bananas"; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(2).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("bla with \ttab.scala").toEqual(file1.oldName); - expect("bla with \ttab.scala").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("bla with \ttab.scala"); + expect(file1.newName).toEqual("bla with \ttab.scala"); + expect(file1.blocks.length).toEqual(1); }); - it("should parse diff with prefix", function() { + it("should parse diff with prefix", () => { const diff = 'diff --git "\tbla with \ttab.scala" "\tbla with \ttab.scala"\n' + "index 4c679d7..e9bd385 100644\n" + @@ -93,17 +93,17 @@ describe("DiffParser", function() { "+cenas com ananas\n" + "+bananas"; - const result = DiffParser.generateDiffJson(diff, { srcPrefix: "\t", dstPrefix: "\t" }); + const result = parse(diff, { srcPrefix: "\t", dstPrefix: "\t" }); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(2).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("bla with \ttab.scala").toEqual(file1.oldName); - expect("bla with \ttab.scala").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("bla with \ttab.scala"); + expect(file1.newName).toEqual("bla with \ttab.scala"); + expect(file1.blocks.length).toEqual(1); }); - it("should parse diff with deleted file", function() { + it("should parse diff with deleted file", () => { const diff = "diff --git a/src/var/strundefined.js b/src/var/strundefined.js\n" + "deleted file mode 100644\n" + @@ -111,26 +111,26 @@ describe("DiffParser", function() { "--- a/src/var/strundefined.js\n" + "+++ /dev/null\n" + "@@ -1,3 +0,0 @@\n" + - "-define(function() {\n" + + "-define(() => {\n" + "- return typeof undefined;\n" + "-});\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(false).toEqual(file1.isCombined); - expect(0).toEqual(file1.addedLines); - expect(3).toEqual(file1.deletedLines); - expect("src/var/strundefined.js").toEqual(file1.oldName); - expect("/dev/null").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(true).toEqual(file1.isDeleted); - expect("04e16b0").toEqual(file1.checksumBefore); - expect("0000000").toEqual(file1.checksumAfter); + expect(file1.isCombined).toEqual(false); + expect(file1.addedLines).toEqual(0); + expect(file1.deletedLines).toEqual(3); + expect(file1.oldName).toEqual("src/var/strundefined.js"); + expect(file1.newName).toEqual("/dev/null"); + expect(file1.blocks.length).toEqual(1); + expect(file1.isDeleted).toEqual(true); + expect(file1.checksumBefore).toEqual("04e16b0"); + expect(file1.checksumAfter).toEqual("0000000"); }); - it("should parse diff with new file", function() { + it("should parse diff with new file", () => { const diff = "diff --git a/test.js b/test.js\n" + "new file mode 100644\n" + @@ -144,23 +144,23 @@ describe("DiffParser", function() { "+\n" + "+console.log(parser.parsePatchDiffResult(text, patchLineList));\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(false).toEqual(file1.isCombined); - expect(5).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("/dev/null").toEqual(file1.oldName); - expect("test.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(true).toEqual(file1.isNew); - expect("100644").toEqual(file1.newFileMode); - expect("0000000").toEqual(file1.checksumBefore); - expect("e1e22ec").toEqual(file1.checksumAfter); + expect(file1.isCombined).toEqual(false); + expect(file1.addedLines).toEqual(5); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("/dev/null"); + expect(file1.newName).toEqual("test.js"); + expect(file1.blocks.length).toEqual(1); + expect(file1.isNew).toEqual(true); + expect(file1.newFileMode).toEqual("100644"); + expect(file1.checksumBefore).toEqual("0000000"); + expect(file1.checksumAfter).toEqual("e1e22ec"); }); - it("should parse diff with nested diff", function() { + it("should parse diff with nested diff", () => { const diff = "diff --git a/src/offset.js b/src/offset.js\n" + "index cc6ffb4..fa51f18 100644\n" + @@ -174,22 +174,22 @@ describe("DiffParser", function() { "+\n" + "+console.log(parser.parsePatchDiffResult(text, patchLineList));\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(false).toEqual(file1.isCombined); - expect(6).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("src/offset.js").toEqual(file1.oldName); - expect("src/offset.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(6).toEqual(file1.blocks[0].lines.length); - expect("cc6ffb4").toEqual(file1.checksumBefore); - expect("fa51f18").toEqual(file1.checksumAfter); + expect(file1.isCombined).toEqual(false); + expect(file1.addedLines).toEqual(6); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("src/offset.js"); + expect(file1.newName).toEqual("src/offset.js"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(6); + expect(file1.checksumBefore).toEqual("cc6ffb4"); + expect(file1.checksumAfter).toEqual("fa51f18"); }); - it("should parse diff with multiple blocks", function() { + it("should parse diff with multiple blocks", () => { const diff = "diff --git a/src/attributes/classes.js b/src/attributes/classes.js\n" + "index c617824..c8d1393 100644\n" + @@ -217,23 +217,23 @@ describe("DiffParser", function() { " // store className if set\n" + ' dataPriv.set( this, "__className__", this.className );\n'; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(false).toEqual(file1.isCombined); - expect(2).toEqual(file1.addedLines); - expect(3).toEqual(file1.deletedLines); - expect("src/attributes/classes.js").toEqual(file1.oldName); - expect("src/attributes/classes.js").toEqual(file1.newName); - expect(2).toEqual(file1.blocks.length); - expect(11).toEqual(file1.blocks[0].lines.length); - expect(8).toEqual(file1.blocks[1].lines.length); - expect("c617824").toEqual(file1.checksumBefore); - expect("c8d1393").toEqual(file1.checksumAfter); + expect(file1.isCombined).toEqual(false); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(3); + expect(file1.oldName).toEqual("src/attributes/classes.js"); + expect(file1.newName).toEqual("src/attributes/classes.js"); + expect(file1.blocks.length).toEqual(2); + expect(file1.blocks[0].lines.length).toEqual(11); + expect(file1.blocks[1].lines.length).toEqual(8); + expect(file1.checksumBefore).toEqual("c617824"); + expect(file1.checksumAfter).toEqual("c8d1393"); }); - it("should parse diff with multiple files", function() { + it("should parse diff with multiple files", () => { const diff = "diff --git a/src/core/init.js b/src/core/init.js\n" + "index e49196a..50f310c 100644\n" + @@ -260,33 +260,33 @@ describe("DiffParser", function() { ' "./var/hasOwn",\n' + ' "./var/slice",\n'; - const result = DiffParser.generateDiffJson(diff); - expect(2).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(2); const file1 = result[0]; - expect(false).toEqual(file1.isCombined); - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("src/core/init.js").toEqual(file1.oldName); - expect("src/core/init.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(8).toEqual(file1.blocks[0].lines.length); - expect("e49196a").toEqual(file1.checksumBefore); - expect("50f310c").toEqual(file1.checksumAfter); + expect(file1.isCombined).toEqual(false); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("src/core/init.js"); + expect(file1.newName).toEqual("src/core/init.js"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(8); + expect(file1.checksumBefore).toEqual("e49196a"); + expect(file1.checksumAfter).toEqual("50f310c"); const file2 = result[1]; - expect(false).toEqual(file2.isCombined); - expect(0).toEqual(file2.addedLines); - expect(1).toEqual(file2.deletedLines); - expect("src/event.js").toEqual(file2.oldName); - expect("src/event.js").toEqual(file2.newName); - expect(1).toEqual(file2.blocks.length); - expect(6).toEqual(file2.blocks[0].lines.length); - expect("7336f4d").toEqual(file2.checksumBefore); - expect("6183f70").toEqual(file2.checksumAfter); + expect(file2.isCombined).toEqual(false); + expect(file2.addedLines).toEqual(0); + expect(file2.deletedLines).toEqual(1); + expect(file2.oldName).toEqual("src/event.js"); + expect(file2.newName).toEqual("src/event.js"); + expect(file2.blocks.length).toEqual(1); + expect(file2.blocks[0].lines.length).toEqual(6); + expect(file2.checksumBefore).toEqual("7336f4d"); + expect(file2.checksumAfter).toEqual("6183f70"); }); - it("should parse combined diff", function() { + it("should parse combined diff", () => { const diff = "diff --combined describe.c\n" + "index fabadb8,cc95eb0..4866510\n" + @@ -316,62 +316,62 @@ describe("DiffParser", function() { " initialized = 1;\n" + " for_each_ref(get_name);\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(true).toEqual(file1.isCombined); + expect(file1.isCombined).toEqual(true); expect(9).toEqual(file1.addedLines); expect(2).toEqual(file1.deletedLines); - expect("describe.c").toEqual(file1.oldName); - expect("describe.c").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(22).toEqual(file1.blocks[0].lines.length); - expect(["4866510", "cc95eb0"].sort()).toEqual(file1.checksumBefore.sort()); - expect("fabadb8").toEqual(file1.checksumAfter); + expect(file1.oldName).toEqual("describe.c"); + expect(file1.newName).toEqual("describe.c"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(22); + expect(file1.checksumBefore).toEqual(["cc95eb0", "4866510"]); + expect(file1.checksumAfter).toEqual("fabadb8"); }); - it("should parse diffs with copied files", function() { + it("should parse diffs with copied files", () => { const diff = "diff --git a/index.js b/more-index.js\n" + "dissimilarity index 5%\n" + "copy from index.js\n" + "copy to more-index.js\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(0).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("index.js").toEqual(file1.oldName); - expect("more-index.js").toEqual(file1.newName); - expect(0).toEqual(file1.blocks.length); - expect(true).toEqual(file1.isCopy); - expect("5").toEqual(file1.changedPercentage); + expect(file1.addedLines).toEqual(0); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("index.js"); + expect(file1.newName).toEqual("more-index.js"); + expect(file1.blocks.length).toEqual(0); + expect(file1.isCopy).toEqual(true); + expect(file1.changedPercentage).toEqual(5); }); - it("should parse diffs with moved files", function() { + it("should parse diffs with moved files", () => { const diff = "diff --git a/more-index.js b/other-index.js\n" + "similarity index 86%\n" + "rename from more-index.js\n" + "rename to other-index.js\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(0).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("more-index.js").toEqual(file1.oldName); - expect("other-index.js").toEqual(file1.newName); - expect(0).toEqual(file1.blocks.length); - expect(true).toEqual(file1.isRename); - expect("86").toEqual(file1.unchangedPercentage); + expect(file1.addedLines).toEqual(0); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("more-index.js"); + expect(file1.newName).toEqual("other-index.js"); + expect(file1.blocks.length).toEqual(0); + expect(file1.isRename).toEqual(true); + expect(file1.unchangedPercentage).toEqual(86); }); - it("should parse diffs correct line numbers", function() { + it("should parse diffs correct line numbers", () => { const diff = "diff --git a/sample b/sample\n" + "index 0000001..0ddf2ba\n" + @@ -381,23 +381,23 @@ describe("DiffParser", function() { "-test\n" + "+test1r\n"; - const result = DiffParser.generateDiffJson(diff); - expect(1).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(1); const file1 = result[0]; - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample").toEqual(file1.oldName); - expect("sample").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(2).toEqual(file1.blocks[0].lines.length); - expect(1).toEqual(file1.blocks[0].lines[0].oldNumber); - expect(null).toEqual(file1.blocks[0].lines[0].newNumber); - expect(null).toEqual(file1.blocks[0].lines[1].oldNumber); - expect(1).toEqual(file1.blocks[0].lines[1].newNumber); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample"); + expect(file1.newName).toEqual("sample"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(2); + expect(file1.blocks[0].lines[0].oldNumber).toEqual(1); + expect(file1.blocks[0].lines[0].newNumber).toBeUndefined(); + expect(file1.blocks[0].lines[1].oldNumber).toBeUndefined(); + expect(file1.blocks[0].lines[1].newNumber).toEqual(1); }); - it("should parse unified non git diff and strip timestamps off the headers", function() { + it("should parse unified non git diff and strip timestamps off the headers", () => { const diffs = [ // 2 hours ahead of GMT "--- a/sample.js 2016-10-25 11:37:14.000000000 +0200\n" + @@ -416,42 +416,42 @@ describe("DiffParser", function() { ]; diffs.forEach(function(diff) { - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(2).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample.js").toEqual(file1.oldName); - expect("sample.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample.js"); + expect(file1.newName).toEqual("sample.js"); + expect(file1.blocks.length).toEqual(1); const linesContent = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual(["-test", "+test1r", "+test2r"]); + expect(["-test", "+test1r", "+test2r"]).toEqual(linesContent); }); }); - it("should parse unified non git diff", function() { + it("should parse unified non git diff", () => { const diff = "--- a/sample.js\n" + "+++ b/sample.js\n" + "@@ -1 +1,2 @@\n" + "-test\n" + "+test1r\n" + "+test2r\n"; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(2).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample.js").toEqual(file1.oldName); - expect("sample.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample.js"); + expect(file1.newName).toEqual("sample.js"); + expect(file1.blocks.length).toEqual(1); const linesContent = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual(["-test", "+test1r", "+test2r"]); + expect(["-test", "+test1r", "+test2r"]).toEqual(linesContent); }); - it("should parse unified diff with multiple hunks and files", function() { + it("should parse unified diff with multiple hunks and files", () => { const diff = "--- sample.js\n" + "+++ sample.js\n" + @@ -464,40 +464,40 @@ describe("DiffParser", function() { "@@ -1 +1,2 @@\n" + "+test1"; - const result = DiffParser.generateDiffJson(diff); - expect(2).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(2); const file1 = result[0]; - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample.js").toEqual(file1.oldName); - expect("sample.js").toEqual(file1.newName); - expect(2).toEqual(file1.blocks.length); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample.js"); + expect(file1.newName).toEqual("sample.js"); + expect(file1.blocks.length).toEqual(2); const linesContent1 = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent1).toEqual(["-test"]); + expect(["-test"]).toEqual(linesContent1); const linesContent2 = file1.blocks[1].lines.map(function(line) { return line.content; }); - expect(linesContent2).toEqual(["+test"]); + expect(["+test"]).toEqual(linesContent2); const file2 = result[1]; - expect(1).toEqual(file2.addedLines); - expect(0).toEqual(file2.deletedLines); - expect("sample1.js").toEqual(file2.oldName); - expect("sample1.js").toEqual(file2.newName); - expect(1).toEqual(file2.blocks.length); + expect(file2.addedLines).toEqual(1); + expect(file2.deletedLines).toEqual(0); + expect(file2.oldName).toEqual("sample1.js"); + expect(file2.newName).toEqual("sample1.js"); + expect(file2.blocks.length).toEqual(1); const linesContent = file2.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual(["+test1"]); + expect(["+test1"]).toEqual(linesContent); }); - it("should parse diff with --- and +++ in the context lines", function() { + it("should parse diff with --- and +++ in the context lines", () => { const diff = "--- sample.js\n" + "+++ sample.js\n" + @@ -512,40 +512,40 @@ describe("DiffParser", function() { "+++ 2\n" + "++++ 2"; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(3).toEqual(file1.addedLines); - expect(3).toEqual(file1.deletedLines); - expect("sample.js").toEqual(file1.oldName); - expect("sample.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(3); + expect(file1.deletedLines).toEqual(3); + expect(file1.oldName).toEqual("sample.js"); + expect(file1.newName).toEqual("sample.js"); + expect(file1.blocks.length).toEqual(1); const linesContent = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual([" test", " ", "-- 1", "--- 1", "---- 1", " ", "++ 2", "+++ 2", "++++ 2"]); + expect([" test", " ", "-- 1", "--- 1", "---- 1", " ", "++ 2", "+++ 2", "++++ 2"]).toEqual(linesContent); }); - it("should parse diff without proper hunk headers", function() { + it("should parse diff without proper hunk headers", () => { const diff = "--- sample.js\n" + "+++ sample.js\n" + "@@ @@\n" + " test"; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(0).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("sample.js").toEqual(file1.oldName); - expect("sample.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(0); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("sample.js"); + expect(file1.newName).toEqual("sample.js"); + expect(file1.blocks.length).toEqual(1); const linesContent = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual([" test"]); + expect([" test"]).toEqual(linesContent); }); - it("should parse binary file diff", function() { + it("should parse binary file diff", () => { const diff = "diff --git a/last-changes-config.png b/last-changes-config.png\n" + "index 322248b..56fc1f2 100644\n" + @@ -553,19 +553,19 @@ describe("DiffParser", function() { "+++ b/last-changes-config.png\n" + "Binary files differ"; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(0).toEqual(file1.addedLines); - expect(0).toEqual(file1.deletedLines); - expect("last-changes-config.png").toEqual(file1.oldName); - expect("last-changes-config.png").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(0).toEqual(file1.blocks[0].lines.length); - expect("Binary files differ").toEqual(file1.blocks[0].header); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(0); + expect(file1.deletedLines).toEqual(0); + expect(file1.oldName).toEqual("last-changes-config.png"); + expect(file1.newName).toEqual("last-changes-config.png"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(0); + expect(file1.blocks[0].header).toEqual("Binary files differ"); }); - it("should parse diff with --find-renames", function() { + it("should parse diff with --find-renames", () => { const diff = "diff --git a/src/test-bar.js b/src/test-baz.js\n" + "similarity index 98%\n" + @@ -581,22 +581,22 @@ describe("DiffParser", function() { " }\n" + " "; - const result = DiffParser.generateDiffJson(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("src/test-bar.js").toEqual(file1.oldName); - expect("src/test-baz.js").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(5).toEqual(file1.blocks[0].lines.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("src/test-bar.js"); + expect(file1.newName).toEqual("src/test-baz.js"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(5); const linesContent = file1.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]); + expect([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]).toEqual(linesContent); }); - it("should parse diff with prefix 2", function() { + it("should parse diff with prefix 2", () => { const diff = 'diff --git "\tTest.scala" "\tScalaTest.scala"\n' + "similarity index 88%\n" + @@ -640,36 +640,36 @@ describe("DiffParser", function() { " }\n" + " "; - const result = DiffParser.generateDiffJson(diff, { srcPrefix: "\t", dstPrefix: "\t" }); - expect(3).toEqual(result.length); + const result = parse(diff, { srcPrefix: "\t", dstPrefix: "\t" }); + expect(result.length).toEqual(3); const file1 = result[0]; - expect(2).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("Test.scala").toEqual(file1.oldName); - expect("ScalaTest.scala").toEqual(file1.newName); - expect(2).toEqual(file1.blocks.length); - expect(8).toEqual(file1.blocks[0].lines.length); - expect(7).toEqual(file1.blocks[1].lines.length); + expect(file1.addedLines).toEqual(2); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("Test.scala"); + expect(file1.newName).toEqual("ScalaTest.scala"); + expect(file1.blocks.length).toEqual(2); + expect(file1.blocks[0].lines.length).toEqual(8); + expect(file1.blocks[1].lines.length).toEqual(7); const file2 = result[1]; - expect("/dev/null").toEqual(file2.oldName); - expect("tardis.png").toEqual(file2.newName); + expect(file2.oldName).toEqual("/dev/null"); + expect(file2.newName).toEqual("tardis.png"); const file3 = result[2]; - expect(1).toEqual(file3.addedLines); - expect(1).toEqual(file3.deletedLines); - expect("src/test-bar.js").toEqual(file3.oldName); - expect("src/test-baz.js").toEqual(file3.newName); - expect(1).toEqual(file3.blocks.length); - expect(5).toEqual(file3.blocks[0].lines.length); + expect(file3.addedLines).toEqual(1); + expect(file3.deletedLines).toEqual(1); + expect(file3.oldName).toEqual("src/test-bar.js"); + expect(file3.newName).toEqual("src/test-baz.js"); + expect(file3.blocks.length).toEqual(1); + expect(file3.blocks[0].lines.length).toEqual(5); const linesContent = file3.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]); + expect([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]).toEqual(linesContent); }); - it("should parse binary with content", function() { + it("should parse binary with content", () => { const diff = "diff --git a/favicon.png b/favicon.png\n" + "deleted file mode 100644\n" + @@ -703,26 +703,26 @@ describe("DiffParser", function() { " }\n" + " "; - const result = DiffParser.generateDiffJson(diff); - expect(2).toEqual(result.length); + const result = parse(diff); + expect(result.length).toEqual(2); const file1 = result[0]; - expect("favicon.png").toEqual(file1.oldName); - expect("favicon.png").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); - expect(0).toEqual(file1.blocks[0].lines.length); + expect(file1.oldName).toEqual("favicon.png"); + expect(file1.newName).toEqual("favicon.png"); + expect(file1.blocks.length).toEqual(1); + expect(file1.blocks[0].lines.length).toEqual(0); const file2 = result[1]; - expect(1).toEqual(file2.addedLines); - expect(1).toEqual(file2.deletedLines); - expect("src/test-bar.js").toEqual(file2.oldName); - expect("src/test-baz.js").toEqual(file2.newName); - expect(1).toEqual(file2.blocks.length); - expect(5).toEqual(file2.blocks[0].lines.length); + expect(file2.addedLines).toEqual(1); + expect(file2.deletedLines).toEqual(1); + expect(file2.oldName).toEqual("src/test-bar.js"); + expect(file2.newName).toEqual("src/test-baz.js"); + expect(file2.blocks.length).toEqual(1); + expect(file2.blocks[0].lines.length).toEqual(5); const linesContent = file2.blocks[0].lines.map(function(line) { return line.content; }); - expect(linesContent).toEqual([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]); + expect([" function foo() {", '-var bar = "Whoops!";', '+var baz = "Whoops!";', " }", " "]).toEqual(linesContent); }); }); }); diff --git a/src/__tests__/diff2html-tests.js b/src/__tests__/diff2html-tests.ts similarity index 85% rename from src/__tests__/diff2html-tests.js rename to src/__tests__/diff2html-tests.ts index 04810e7..e859b05 100644 --- a/src/__tests__/diff2html-tests.js +++ b/src/__tests__/diff2html-tests.ts @@ -1,4 +1,5 @@ -const Diff2Html = require("../diff2html.js").Diff2Html; +import { parse, html } from "../diff2html"; +import { DiffFile, LineType } from "../render-utils"; const diffExample1 = "diff --git a/sample b/sample\n" + @@ -9,27 +10,27 @@ const diffExample1 = "-test\n" + "+test1\n"; -const jsonExample1 = [ +const jsonExample1: DiffFile[] = [ { blocks: [ { lines: [ { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 1, - newNumber: null + newNumber: undefined }, { content: "+test1", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 1 } ], - oldStartLine: "1", - oldStartLine2: null, - newStartLine: "1", + oldStartLine: 1, + oldStartLine2: undefined, + newStartLine: 1, header: "@@ -1 +1 @@" } ], @@ -38,9 +39,10 @@ const jsonExample1 = [ checksumBefore: "0000001", checksumAfter: "0ddf2ba", oldName: "sample", - language: undefined, newName: "sample", - isCombined: false + language: "", + isCombined: false, + isGitDiff: true } ]; @@ -183,9 +185,9 @@ const htmlSideExample1 = const htmlSideExample1WithFilesSummary = filesExample1 + htmlSideExample1; -describe("Diff2Html", function() { - describe("getJsonFromDiff", function() { - it("should parse simple diff to json", function() { +describe("Diff2Html", () => { + describe("getJsonFromDiff", () => { + it("should parse simple diff to json", () => { const diff = "diff --git a/sample b/sample\n" + "index 0000001..0ddf2ba\n" + @@ -194,19 +196,19 @@ describe("Diff2Html", function() { "@@ -1 +1 @@\n" + "-test\n" + "+test1\n"; - const result = Diff2Html.getJsonFromDiff(diff); + const result = parse(diff); const file1 = result[0]; - expect(1).toEqual(result.length); - expect(1).toEqual(file1.addedLines); - expect(1).toEqual(file1.deletedLines); - expect("sample").toEqual(file1.oldName); - expect("sample").toEqual(file1.newName); - expect(1).toEqual(file1.blocks.length); + expect(result.length).toEqual(1); + expect(file1.addedLines).toEqual(1); + expect(file1.deletedLines).toEqual(1); + expect(file1.oldName).toEqual("sample"); + expect(file1.newName).toEqual("sample"); + expect(file1.blocks.length).toEqual(1); }); // Test case for issue #49 - it("should parse diff with added EOF", function() { + it("should parse diff with added EOF", () => { const diff = "diff --git a/sample.scala b/sample.scala\n" + "index b583263..8b2fc3e 100644\n" + @@ -223,67 +225,67 @@ describe("Diff2Html", function() { "+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,\n" + "+ PluginError, CodeParserError, EngineError = Value\n" + "+}\n"; - const result = Diff2Html.getJsonFromDiff(diff); + const result = parse(diff); - expect(50).toEqual(result[0].blocks[0].lines[0].oldNumber); - expect(50).toEqual(result[0].blocks[0].lines[0].newNumber); + expect(result[0].blocks[0].lines[0].oldNumber).toEqual(50); + expect(result[0].blocks[0].lines[0].newNumber).toEqual(50); - expect(51).toEqual(result[0].blocks[0].lines[1].oldNumber); - expect(51).toEqual(result[0].blocks[0].lines[1].newNumber); + expect(result[0].blocks[0].lines[1].oldNumber).toEqual(51); + expect(result[0].blocks[0].lines[1].newNumber).toEqual(51); - expect(52).toEqual(result[0].blocks[0].lines[2].oldNumber); - expect(52).toEqual(result[0].blocks[0].lines[2].newNumber); + expect(result[0].blocks[0].lines[2].oldNumber).toEqual(52); + expect(result[0].blocks[0].lines[2].newNumber).toEqual(52); - expect(53).toEqual(result[0].blocks[0].lines[3].oldNumber); - expect(null).toEqual(result[0].blocks[0].lines[3].newNumber); + expect(result[0].blocks[0].lines[3].oldNumber).toEqual(53); + expect(result[0].blocks[0].lines[3].newNumber).toBeUndefined(); - expect(54).toEqual(result[0].blocks[0].lines[4].oldNumber); - expect(null).toEqual(result[0].blocks[0].lines[4].newNumber); + expect(result[0].blocks[0].lines[4].oldNumber).toEqual(54); + expect(result[0].blocks[0].lines[4].newNumber).toBeUndefined(); - expect(null).toEqual(result[0].blocks[0].lines[5].oldNumber); - expect(53).toEqual(result[0].blocks[0].lines[5].newNumber); + expect(result[0].blocks[0].lines[5].oldNumber).toBeUndefined(); + expect(result[0].blocks[0].lines[5].newNumber).toEqual(53); - expect(null).toEqual(result[0].blocks[0].lines[6].oldNumber); - expect(54).toEqual(result[0].blocks[0].lines[6].newNumber); + expect(result[0].blocks[0].lines[6].oldNumber).toBeUndefined(); + expect(result[0].blocks[0].lines[6].newNumber).toEqual(54); - expect(null).toEqual(result[0].blocks[0].lines[7].oldNumber); - expect(55).toEqual(result[0].blocks[0].lines[7].newNumber); + expect(result[0].blocks[0].lines[7].oldNumber).toBeUndefined(); + expect(result[0].blocks[0].lines[7].newNumber).toEqual(55); - expect(null).toEqual(result[0].blocks[0].lines[8].oldNumber); - expect(56).toEqual(result[0].blocks[0].lines[8].newNumber); + expect(result[0].blocks[0].lines[8].oldNumber).toBeUndefined(); + expect(result[0].blocks[0].lines[8].newNumber).toEqual(56); }); - it("should generate pretty line by line html from diff", function() { - const result = Diff2Html.getPrettyHtmlFromDiff(diffExample1); - expect(htmlLineExample1).toEqual(result); + it("should generate pretty line by line html from diff", () => { + const result = html(diffExample1); + expect(result).toEqual(htmlLineExample1); }); - it("should generate pretty line by line html from json", function() { - const result = Diff2Html.getPrettyHtmlFromJson(jsonExample1); - expect(htmlLineExample1).toEqual(result); + it("should generate pretty line by line html from json", () => { + const result = html(jsonExample1); + expect(result).toEqual(htmlLineExample1); }); - it("should generate pretty diff with files summary", function() { - const result = Diff2Html.getPrettyHtmlFromDiff(diffExample1, { showFiles: true }); - expect(htmlLineExample1WithFilesSummary).toEqual(result); + it("should generate pretty diff with files summary", () => { + const result = html(diffExample1, { showFiles: true }); + expect(result).toEqual(htmlLineExample1WithFilesSummary); }); - it("should generate pretty side by side html from diff", function() { - const result = Diff2Html.getPrettySideBySideHtmlFromDiff(diffExample1); - expect(htmlSideExample1).toEqual(result); + it("should generate pretty side by side html from diff", () => { + const result = html(diffExample1, { outputFormat: "side-by-side" }); + expect(result).toEqual(htmlSideExample1); }); - it("should generate pretty side by side html from json", function() { - const result = Diff2Html.getPrettySideBySideHtmlFromJson(jsonExample1); - expect(htmlSideExample1).toEqual(result); + it("should generate pretty side by side html from json", () => { + const result = html(jsonExample1, { outputFormat: "side-by-side" }); + expect(result).toEqual(htmlSideExample1); }); - it("should generate pretty side by side html from diff 2", function() { - const result = Diff2Html.getPrettySideBySideHtmlFromDiff(diffExample1, { showFiles: true }); - expect(htmlSideExample1WithFilesSummary).toEqual(result); + it("should generate pretty side by side html from diff 2", () => { + const result = html(diffExample1, { outputFormat: "side-by-side", showFiles: true }); + expect(result).toEqual(htmlSideExample1WithFilesSummary); }); - it("should generate pretty side by side html from diff with html on headers", function() { + it("should generate pretty side by side html from diff with html on headers", () => { const diffExample2 = "diff --git a/CHANGELOG.md b/CHANGELOG.md\n" + "index fc3e3f4..b486d10 100644\n" + @@ -516,8 +518,8 @@ describe("Diff2Html", function() { "\n" + ""; - const result = Diff2Html.getPrettyHtmlFromDiff(diffExample2); - expect(result).toEqual(htmlExample2); + const result = html(diffExample2); + expect(htmlExample2).toEqual(result); }); }); }); diff --git a/src/__tests__/file-list-printer-tests.js b/src/__tests__/file-list-printer-tests.ts similarity index 81% rename from src/__tests__/file-list-printer-tests.js rename to src/__tests__/file-list-printer-tests.ts index 9361474..102c11f 100644 --- a/src/__tests__/file-list-printer-tests.js +++ b/src/__tests__/file-list-printer-tests.ts @@ -1,17 +1,30 @@ -const FileListPrinter = require("../file-list-printer.js").FileListPrinter; +import { render } from "../file-list-renderer"; +import HoganJsUtils from "../hoganjs-utils"; -describe("FileListPrinter", function() { - describe("generateFileList", function() { - it("should expose old and new files to templates", function() { +describe("FileListPrinter", () => { + describe("generateFileList", () => { + it("should expose old and new files to templates", () => { + const hoganUtils = new HoganJsUtils({ + rawTemplates: { + "file-summary-wrapper": "{{{files}}}", + "file-summary-line": "{{oldName}}, {{newName}}, {{fileName}}" + } + }); const files = [ { - addedlines: 12, - deletedlines: 41, + isCombined: false, + isGitDiff: false, + blocks: [], + addedLines: 12, + deletedLines: 41, language: "js", oldName: "my/file/name.js", newName: "my/file/name.js" }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 12, deletedLines: 41, language: "js", @@ -19,6 +32,9 @@ describe("FileListPrinter", function() { newName: "my/file/name2.js" }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 12, deletedLines: 0, language: "js", @@ -27,6 +43,9 @@ describe("FileListPrinter", function() { isNew: true }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 0, deletedLines: 41, language: "js", @@ -36,26 +55,23 @@ describe("FileListPrinter", function() { } ]; - const fileListPrinter = new FileListPrinter({ - rawTemplates: { - "file-summary-wrapper": "{{{files}}}", - "file-summary-line": "{{oldName}}, {{newName}}, {{fileName}}" - } - }); - - const fileHtml = fileListPrinter.generateFileList(files); + const fileHtml = render(files, hoganUtils); const expected = "my/file/name.js, my/file/name.js, my/file/name.js\n" + "my/file/name1.js, my/file/name2.js, my/file/{name1.js → name2.js}\n" + "dev/null, my/file/name.js, my/file/name.js\n" + "my/file/name.js, dev/null, my/file/name.js"; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for all kinds of files", function() { + it("should work for all kinds of files", () => { + const hoganUtils = new HoganJsUtils({}); const files = [ { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 12, deletedLines: 41, language: "js", @@ -63,6 +79,9 @@ describe("FileListPrinter", function() { newName: "my/file/name.js" }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 12, deletedLines: 41, language: "js", @@ -70,6 +89,9 @@ describe("FileListPrinter", function() { newName: "my/file/name2.js" }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 12, deletedLines: 0, language: "js", @@ -78,6 +100,9 @@ describe("FileListPrinter", function() { isNew: true }, { + isCombined: false, + isGitDiff: false, + blocks: [], addedLines: 0, deletedLines: 41, language: "js", @@ -87,8 +112,7 @@ describe("FileListPrinter", function() { } ]; - const fileListPrinter = new FileListPrinter(); - const fileHtml = fileListPrinter.generateFileList(files); + const fileHtml = render(files, hoganUtils); const expected = '
\n' + @@ -149,7 +173,7 @@ describe("FileListPrinter", function() { " \n" + "
"; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); }); diff --git a/src/__tests__/hogan-cache-tests.js b/src/__tests__/hogan-cache-tests.js deleted file mode 100644 index f1b40fe..0000000 --- a/src/__tests__/hogan-cache-tests.js +++ /dev/null @@ -1,73 +0,0 @@ -const HoganJsUtils = new (require("../hoganjs-utils.js")).HoganJsUtils(); -const diffParser = require("../diff-parser.js").DiffParser; - -describe("HoganJsUtils", function() { - describe("render", function() { - const emptyDiffHtml = - "\n" + - ' \n' + - '
\n' + - " File without changes\n" + - "
\n" + - " \n" + - ""; - - it("should render view", function() { - const result = HoganJsUtils.render("generic", "empty-diff", { - contentClass: "d2h-code-line", - diffParser: diffParser - }); - expect(emptyDiffHtml).toEqual(result); - }); - - it("should render view without cache", function() { - const result = HoganJsUtils.render( - "generic", - "empty-diff", - { - contentClass: "d2h-code-line", - diffParser: diffParser - }, - { noCache: true } - ); - expect(emptyDiffHtml).toEqual(result); - }); - - it("should return null if template is missing", function() { - const hoganUtils = new (require("../hoganjs-utils.js")).HoganJsUtils({ noCache: true }); - const result = hoganUtils.render("generic", "missing-template", {}); - expect(null).toEqual(result); - }); - - it("should allow templates to be overridden with compiled templates", function() { - const emptyDiffTemplate = HoganJsUtils.compile("

{{myName}}

"); - - const config = { templates: { "generic-empty-diff": emptyDiffTemplate } }; - const hoganUtils = new (require("../hoganjs-utils.js")).HoganJsUtils(config); - const result = hoganUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); - expect("

Rodrigo Fernandes

").toEqual(result); - }); - - it("should allow templates to be overridden with uncompiled templates", function() { - const emptyDiffTemplate = "

{{myName}}

"; - - const config = { rawTemplates: { "generic-empty-diff": emptyDiffTemplate } }; - const hoganUtils = new (require("../hoganjs-utils.js")).HoganJsUtils(config); - const result = hoganUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); - expect("

Rodrigo Fernandes

").toEqual(result); - }); - - it("should allow templates to be overridden giving priority to compiled templates", function() { - const emptyDiffTemplate = HoganJsUtils.compile("

{{myName}}

"); - const emptyDiffTemplateUncompiled = "

Not used!

"; - - const config = { - templates: { "generic-empty-diff": emptyDiffTemplate }, - rawTemplates: { "generic-empty-diff": emptyDiffTemplateUncompiled } - }; - const hoganUtils = new (require("../hoganjs-utils.js")).HoganJsUtils(config); - const result = hoganUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); - expect("

Rodrigo Fernandes

").toEqual(result); - }); - }); -}); diff --git a/src/__tests__/hogan-cache-tests.ts b/src/__tests__/hogan-cache-tests.ts new file mode 100644 index 0000000..d292bab --- /dev/null +++ b/src/__tests__/hogan-cache-tests.ts @@ -0,0 +1,66 @@ +import HoganJsUtils from "../hoganjs-utils"; +import { CSSLineClass } from "../render-utils"; + +describe("HoganJsUtils", () => { + describe("render", () => { + const emptyDiffHtml = + "\n" + + ' \n' + + '
\n' + + " File without changes\n" + + "
\n" + + " \n" + + ""; + + it("should render view", () => { + const hoganJsUtils = new HoganJsUtils({}); + const result = hoganJsUtils.render("generic", "empty-diff", { + contentClass: "d2h-code-line", + CSSLineClass: CSSLineClass + }); + expect(result).toEqual(emptyDiffHtml); + }); + + it("should render view without cache", () => { + const hoganJsUtils = new HoganJsUtils({}); + const result = hoganJsUtils.render("generic", "empty-diff", { + contentClass: "d2h-code-line", + CSSLineClass: CSSLineClass + }); + expect(result).toEqual(emptyDiffHtml); + }); + + it("should throw exception if template is missing", () => { + const hoganJsUtils = new HoganJsUtils({}); + expect(() => hoganJsUtils.render("generic", "missing-template", {})).toThrow(Error); + }); + + it("should allow templates to be overridden with compiled templates", () => { + const emptyDiffTemplate = HoganJsUtils.compile("

{{myName}}

"); + const hoganJsUtils = new HoganJsUtils({ compiledTemplates: { "generic-empty-diff": emptyDiffTemplate } }); + + const result = hoganJsUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); + expect(result).toEqual("

Rodrigo Fernandes

"); + }); + + it("should allow templates to be overridden with uncompiled templates", () => { + const emptyDiffTemplate = "

{{myName}}

"; + const hoganJsUtils = new HoganJsUtils({ rawTemplates: { "generic-empty-diff": emptyDiffTemplate } }); + + const result = hoganJsUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); + expect(result).toEqual("

Rodrigo Fernandes

"); + }); + + it("should allow templates to be overridden giving priority to raw templates", () => { + const emptyDiffTemplate = HoganJsUtils.compile("

Not used!

"); + const emptyDiffTemplateUncompiled = "

{{myName}}

"; + const hoganJsUtils = new HoganJsUtils({ + compiledTemplates: { "generic-empty-diff": emptyDiffTemplate }, + rawTemplates: { "generic-empty-diff": emptyDiffTemplateUncompiled } + }); + + const result = hoganJsUtils.render("generic", "empty-diff", { myName: "Rodrigo Fernandes" }); + expect(result).toEqual("

Rodrigo Fernandes

"); + }); + }); +}); diff --git a/src/__tests__/line-by-line-tests.js b/src/__tests__/line-by-line-tests.ts similarity index 70% rename from src/__tests__/line-by-line-tests.js rename to src/__tests__/line-by-line-tests.ts index 60b5836..0c840bd 100644 --- a/src/__tests__/line-by-line-tests.js +++ b/src/__tests__/line-by-line-tests.ts @@ -1,10 +1,13 @@ -const LineByLinePrinter = require("../line-by-line-printer.js").LineByLinePrinter; +import LineByLineRenderer from "../line-by-line-renderer"; +import HoganJsUtils from "../hoganjs-utils"; +import { LineType, CSSLineClass, DiffLine, DiffFile } from "../render-utils"; -describe("LineByLinePrinter", function() { - describe("_generateEmptyDiff", function() { - it("should return an empty diff", function() { - const lineByLinePrinter = new LineByLinePrinter({}); - const fileHtml = lineByLinePrinter._generateEmptyDiff(); +describe("LineByLineRenderer", () => { + describe("_generateEmptyDiff", () => { + it("should return an empty diff", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + const fileHtml = lineByLineRenderer.generateEmptyDiff(); const expected = "\n" + ' \n' + @@ -14,15 +17,15 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); - describe("makeLineHtml", function() { - it("should work for insertions", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const lineByLinePrinter = new LineByLinePrinter({}); - let fileHtml = lineByLinePrinter.makeLineHtml(false, diffParser.LINE_TYPE.INSERTS, "", 30, "test", "+"); + describe("makeLineHtml", () => { + it("should work for insertions", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + let fileHtml = lineByLineRenderer.makeLineHtml(false, CSSLineClass.INSERTS, "test", undefined, 30, "+"); fileHtml = fileHtml.replace(/\n\n+/g, "\n"); const expected = "\n" + @@ -38,13 +41,13 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for deletions", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const lineByLinePrinter = new LineByLinePrinter({}); - let fileHtml = lineByLinePrinter.makeLineHtml(false, diffParser.LINE_TYPE.DELETES, 30, "", "test", "-"); + it("should work for deletions", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + let fileHtml = lineByLineRenderer.makeLineHtml(false, CSSLineClass.DELETES, "test", 30, undefined, "-"); fileHtml = fileHtml.replace(/\n\n+/g, "\n"); const expected = "\n" + @@ -60,13 +63,13 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should convert indents into non breakin spaces (2 white spaces)", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const lineByLinePrinter = new LineByLinePrinter({}); - let fileHtml = lineByLinePrinter.makeLineHtml(false, diffParser.LINE_TYPE.INSERTS, "", 30, " test", "+"); + it("should convert indents into non breakin spaces (2 white spaces)", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + let fileHtml = lineByLineRenderer.makeLineHtml(false, CSSLineClass.INSERTS, " test", undefined, 30, "+"); fileHtml = fileHtml.replace(/\n\n+/g, "\n"); const expected = "\n" + @@ -82,13 +85,13 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should convert indents into non breakin spaces (4 white spaces)", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const lineByLinePrinter = new LineByLinePrinter({}); - let fileHtml = lineByLinePrinter.makeLineHtml(false, diffParser.LINE_TYPE.INSERTS, "", 30, " test", "+"); + it("should convert indents into non breakin spaces (4 white spaces)", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + let fileHtml = lineByLineRenderer.makeLineHtml(false, CSSLineClass.INSERTS, " test", undefined, 30, "+"); fileHtml = fileHtml.replace(/\n\n+/g, "\n"); const expected = "\n" + @@ -104,13 +107,13 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should preserve tabs", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const lineByLinePrinter = new LineByLinePrinter({}); - let fileHtml = lineByLinePrinter.makeLineHtml(false, diffParser.LINE_TYPE.INSERTS, "", 30, "\ttest", "+"); + it("should preserve tabs", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + let fileHtml = lineByLineRenderer.makeLineHtml(false, CSSLineClass.INSERTS, "\ttest", undefined, 30, "+"); fileHtml = fileHtml.replace(/\n\n+/g, "\n"); const expected = "\n" + @@ -127,24 +130,28 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); - describe("makeFileDiffHtml", function() { - it("should work for simple file", function() { - const lineByLinePrinter = new LineByLinePrinter({}); + describe("makeFileDiffHtml", () => { + it("should work for simple file", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); const file = { addedLines: 12, deletedLines: 41, language: "js", oldName: "my/file/name.js", - newName: "my/file/name.js" + newName: "my/file/name.js", + isCombined: false, + isGitDiff: false, + blocks: [] }; const diffs = "Random Html"; - const fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs); + const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs); const expected = '
\n' + @@ -166,10 +173,11 @@ describe("LineByLinePrinter", function() { "
\n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for simple added file", function() { - const lineByLinePrinter = new LineByLinePrinter({}); + it("should work for simple added file", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); const file = { addedLines: 12, @@ -177,11 +185,14 @@ describe("LineByLinePrinter", function() { language: "js", oldName: "dev/null", newName: "my/file/name.js", - isNew: true + isNew: true, + isCombined: false, + isGitDiff: false, + blocks: [] }; const diffs = "Random Html"; - const fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs); + const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs); const expected = '
\n' + @@ -203,10 +214,11 @@ describe("LineByLinePrinter", function() { "
\n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for simple deleted file", function() { - const lineByLinePrinter = new LineByLinePrinter({}); + it("should work for simple deleted file", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); const file = { addedLines: 0, @@ -214,11 +226,14 @@ describe("LineByLinePrinter", function() { language: "js", oldName: "my/file/name.js", newName: "dev/null", - isDeleted: true + isDeleted: true, + isCombined: false, + isGitDiff: false, + blocks: [] }; const diffs = "Random Html"; - const fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs); + const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs); const expected = '
\n' + @@ -240,10 +255,11 @@ describe("LineByLinePrinter", function() { "
\n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for simple renamed file", function() { - const lineByLinePrinter = new LineByLinePrinter({}); + it("should work for simple renamed file", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); const file = { addedLines: 12, @@ -251,11 +267,14 @@ describe("LineByLinePrinter", function() { language: "js", oldName: "my/file/name1.js", newName: "my/file/name2.js", - isRename: true + isRename: true, + isCombined: false, + isGitDiff: false, + blocks: [] }; const diffs = "Random Html"; - const fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs); + const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs); const expected = '
\n' + @@ -277,61 +296,71 @@ describe("LineByLinePrinter", function() { "
\n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should return empty when option renderNothingWhenEmpty is true and file blocks not present", function() { - const lineByLinePrinter = new LineByLinePrinter({ + it("should return empty when option renderNothingWhenEmpty is true and file blocks not present", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, { renderNothingWhenEmpty: true }); const file = { + addedLines: 0, + deletedLines: 0, + language: "js", + oldName: "my/file/name1.js", + newName: "my/file/name2.js", + isRename: true, + isCombined: false, + isGitDiff: false, blocks: [] }; const diffs = "Random Html"; - const fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs); + const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs); const expected = ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); - describe("makeLineByLineHtmlWrapper", function() { - it("should work for simple content", function() { - const lineByLinePrinter = new LineByLinePrinter({}); - const fileHtml = lineByLinePrinter.makeLineByLineHtmlWrapper("Random Html"); + describe("makeLineByLineHtmlWrapper", () => { + it("should work for simple content", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + const fileHtml = lineByLineRenderer.makeLineByLineHtmlWrapper("Random Html"); const expected = '
\n' + " Random Html\n" + "
"; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); - describe("generateLineByLineJsonHtml", function() { - it("should work for list of files", function() { - const exampleJson = [ + describe("generateLineByLineJsonHtml", () => { + it("should work for list of files", () => { + const exampleJson: DiffFile[] = [ { blocks: [ { lines: [ { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 1, - newNumber: null + newNumber: undefined }, { content: "+test1r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 1 } ], - oldStartLine: "1", - oldStartLine2: null, - newStartLine: "1", + oldStartLine: 1, + oldStartLine2: undefined, + newStartLine: 1, header: "@@ -1 +1 @@" } ], @@ -340,17 +369,21 @@ describe("LineByLinePrinter", function() { checksumBefore: "0000001", checksumAfter: "0ddf2ba", oldName: "sample", - language: undefined, newName: "sample", - isCombined: false + language: "txt", + isCombined: false, + isGitDiff: true } ]; - const lineByLinePrinter = new LineByLinePrinter({ matching: "lines" }); - const html = lineByLinePrinter.generateLineByLineJsonHtml(exampleJson); + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, { + matching: "lines" + }); + const html = lineByLineRenderer.render(exampleJson); const expected = '
\n' + - '
\n' + + '
\n' + '
\n' + ' \n' + '
@@ -1 +1 @@
\n' + " \n" + "\n" + - ' \n' + + ' \n' + '
1
\n' + '
\n' + " \n" + - ' \n' + - '
\n' + + ' \n' + + '
\n' + ' -\n' + ' test\n' + "
\n" + " \n" + "\n" + - ' \n' + + ' \n' + '
\n' + '
1
\n' + " \n" + - ' \n' + - '
\n' + + ' \n' + + '
\n' + ' +\n' + ' test1r\n' + "
\n" + @@ -397,10 +430,10 @@ describe("LineByLinePrinter", function() { "
\n" + "
"; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); - it("should work for empty blocks", function() { + it("should work for empty blocks", () => { const exampleJson = [ { blocks: [], @@ -409,12 +442,16 @@ describe("LineByLinePrinter", function() { oldName: "sample", language: "js", newName: "sample", - isCombined: false + isCombined: false, + isGitDiff: false } ]; - const lineByLinePrinter = new LineByLinePrinter({ renderNothingWhenEmpty: false }); - const html = lineByLinePrinter.generateLineByLineJsonHtml(exampleJson); + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, { + renderNothingWhenEmpty: false + }); + const html = lineByLineRenderer.render(exampleJson); const expected = '
\n' + '
\n' + @@ -443,31 +480,32 @@ describe("LineByLinePrinter", function() { "
\n" + "
"; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); }); - describe("_processLines", function() { - it("should work for simple block header", function() { - const lineByLinePrinter = new LineByLinePrinter({}); - const oldLines = [ + describe("_processLines", () => { + it("should work for simple block header", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + const oldLines: DiffLine[] = [ { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 1, - newNumber: null + newNumber: undefined } ]; - const newLines = [ + const newLines: DiffLine[] = [ { content: "+test1r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 1 } ]; - const html = lineByLinePrinter._processLines(false, oldLines, newLines); + const html = lineByLineRenderer.processLines(false, oldLines, newLines); const expected = "\n" + @@ -494,45 +532,46 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); }); - describe("_generateFileHtml", function() { - it("should work for simple file", function() { - const lineByLinePrinter = new LineByLinePrinter({}); - const file = { + describe("_generateFileHtml", () => { + it("should work for simple file", () => { + const hoganUtils = new HoganJsUtils({}); + const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {}); + const file: DiffFile = { blocks: [ { lines: [ { content: " one context line", - type: "d2h-cntx", + type: LineType.CONTEXT, oldNumber: 1, newNumber: 1 }, { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 2, - newNumber: null + newNumber: undefined }, { content: "+test1r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 2 }, { content: "+test2r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 3 } ], - oldStartLine: "1", - oldStartLine2: null, - newStartLine: "1", + oldStartLine: 1, + oldStartLine2: undefined, + newStartLine: 1, header: "@@ -1 +1 @@" } ], @@ -541,12 +580,13 @@ describe("LineByLinePrinter", function() { checksumBefore: "0000001", checksumAfter: "0ddf2ba", oldName: "sample", - language: undefined, + language: "txt", newName: "sample", - isCombined: false + isCombined: false, + isGitDiff: true }; - const html = lineByLinePrinter._generateFileHtml(file); + const html = lineByLineRenderer.generateFileHtml(file); const expected = "\n" + @@ -600,7 +640,7 @@ describe("LineByLinePrinter", function() { " \n" + ""; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); }); }); diff --git a/src/__tests__/printer-utils-tests.js b/src/__tests__/printer-utils-tests.ts similarity index 63% rename from src/__tests__/printer-utils-tests.js rename to src/__tests__/printer-utils-tests.ts index 5e92a6b..dce72c9 100644 --- a/src/__tests__/printer-utils-tests.js +++ b/src/__tests__/printer-utils-tests.ts @@ -1,16 +1,16 @@ -const PrinterUtils = require("../printer-utils.js").PrinterUtils; +import * as renderUtils from "../render-utils"; describe("Utils", function() { describe("getHtmlId", function() { it("should generate file unique id", function() { - const result = PrinterUtils.getHtmlId({ + const result = renderUtils.getHtmlId({ oldName: "sample.js", newName: "sample.js" }); expect("d2h-960013").toEqual(result); }); it("should generate file unique id for empty hashes", function() { - const result = PrinterUtils.getHtmlId({ + const result = renderUtils.getHtmlId({ oldName: "sample.js", newName: "sample.js" }); @@ -20,105 +20,102 @@ describe("Utils", function() { describe("getDiffName", function() { it("should generate the file name for a changed file", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "sample.js", newName: "sample.js" }); expect("sample.js").toEqual(result); }); it("should generate the file name for a changed file and full rename", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "sample1.js", newName: "sample2.js" }); expect("sample1.js → sample2.js").toEqual(result); }); it("should generate the file name for a changed file and prefix rename", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "src/path/sample.js", newName: "source/path/sample.js" }); expect("{src → source}/path/sample.js").toEqual(result); }); it("should generate the file name for a changed file and suffix rename", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "src/path/sample1.js", newName: "src/path/sample2.js" }); expect("src/path/{sample1.js → sample2.js}").toEqual(result); }); it("should generate the file name for a changed file and middle rename", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "src/really/big/path/sample.js", newName: "src/small/path/sample.js" }); expect("src/{really/big → small}/path/sample.js").toEqual(result); }); it("should generate the file name for a deleted file", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "src/my/file.js", newName: "/dev/null" }); expect("src/my/file.js").toEqual(result); }); it("should generate the file name for a new file", function() { - const result = PrinterUtils.getDiffName({ + const result = renderUtils.filenameDiff({ oldName: "/dev/null", newName: "src/my/file.js" }); expect("src/my/file.js").toEqual(result); }); - it("should generate handle undefined filename", function() { - const result = PrinterUtils.getDiffName({}); - expect("unknown/file/path").toEqual(result); - }); }); describe("diffHighlight", function() { it("should highlight two lines", function() { - const result = PrinterUtils.diffHighlight("-var myVar = 2;", "+var myVariable = 3;", { matching: "words" }); + const result = renderUtils.diffHighlight("-var myVar = 2;", "+var myVariable = 3;", false, { + matching: "words" + }); - expect({ - first: { + expect(result).toEqual({ + oldLine: { prefix: "-", - line: "var myVar = 2;" + content: "var myVar = 2;" }, - second: { + newLine: { prefix: "+", - line: "var myVariable = 3;" + content: "var myVariable = 3;" } - }).toEqual(result); + }); }); it("should highlight two lines char by char", function() { - const result = PrinterUtils.diffHighlight("-var myVar = 2;", "+var myVariable = 3;", { diffStyle: "char" }); + const result = renderUtils.diffHighlight("-var myVar = 2;", "+var myVariable = 3;", false, { diffStyle: "char" }); expect({ - first: { + oldLine: { prefix: "-", - line: "var myVar = 2;" + content: "var myVar = 2;" }, - second: { + newLine: { prefix: "+", - line: "var myVariable = 3;" + content: "var myVariable = 3;" } }).toEqual(result); }); it("should highlight combined diff lines", function() { - const result = PrinterUtils.diffHighlight(" -var myVar = 2;", " +var myVariable = 3;", { + const result = renderUtils.diffHighlight(" -var myVar = 2;", " +var myVariable = 3;", true, { diffStyle: "word", - isCombined: true, matching: "words", matchWordsThreshold: 1.0 }); expect({ - first: { + oldLine: { prefix: " -", - line: 'var myVar = 2;' + content: 'var myVar = 2;' }, - second: { + newLine: { prefix: " +", - line: 'var myVariable = 3;' + content: 'var myVariable = 3;' } }).toEqual(result); }); diff --git a/src/__tests__/side-by-side-printer-tests.js b/src/__tests__/side-by-side-printer-tests.ts similarity index 73% rename from src/__tests__/side-by-side-printer-tests.js rename to src/__tests__/side-by-side-printer-tests.ts index fb2b007..1ec2341 100644 --- a/src/__tests__/side-by-side-printer-tests.js +++ b/src/__tests__/side-by-side-printer-tests.ts @@ -1,10 +1,13 @@ -const SideBySidePrinter = require("../side-by-side-printer.js").SideBySidePrinter; +import SideBySideRenderer from "../side-by-side-renderer"; +import HoganJsUtils from "../hoganjs-utils"; +import { LineType, CSSLineClass, DiffLine, DiffFile } from "../render-utils"; -describe("SideBySidePrinter", function() { - describe("generateEmptyDiff", function() { - it("should return an empty diff", function() { - const sideBySidePrinter = new SideBySidePrinter({}); - const fileHtml = sideBySidePrinter.generateEmptyDiff(); +describe("SideBySideRenderer", () => { + describe("generateEmptyDiff", () => { + it("should return an empty diff", () => { + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const fileHtml = sideBySideRenderer.generateEmptyDiff(); const expectedRight = ""; const expectedLeft = "\n" + @@ -15,51 +18,53 @@ describe("SideBySidePrinter", function() { " \n" + ""; - expect(expectedRight).toEqual(fileHtml.right); - expect(expectedLeft).toEqual(fileHtml.left); + expect(fileHtml.right).toEqual(expectedRight); + expect(fileHtml.left).toEqual(expectedLeft); }); }); - describe("generateSideBySideFileHtml", function() { - it("should generate lines with the right prefixes", function() { - const sideBySidePrinter = new SideBySidePrinter({}); + describe("generateSideBySideFileHtml", () => { + it("should generate lines with the right prefixes", () => { + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); - const file = { + const file: DiffFile = { + isGitDiff: true, blocks: [ { lines: [ { content: " context", - type: "d2h-cntx", + type: LineType.CONTEXT, oldNumber: 19, newNumber: 19 }, { content: "-removed", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 20, - newNumber: null + newNumber: undefined }, { content: "+added", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 20 }, { content: "+another added", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 21 } ], - oldStartLine: "19", - newStartLine: "19", + oldStartLine: 19, + newStartLine: 19, header: "@@ -19,7 +19,7 @@" } ], deletedLines: 1, - addedLines: 1, + addedLines: 2, checksumBefore: "fc56817", checksumAfter: "e8e7e49", mode: "100644", @@ -69,7 +74,7 @@ describe("SideBySidePrinter", function() { isCombined: false }; - const fileHtml = sideBySidePrinter.generateSideBySideFileHtml(file); + const fileHtml = sideBySideRenderer.generateSideBySideFileHtml(file); const expectedLeft = "\n" + @@ -148,16 +153,16 @@ describe("SideBySidePrinter", function() { " \n" + ""; - expect(expectedLeft).toEqual(fileHtml.left); - expect(expectedRight).toEqual(fileHtml.right); + expect(fileHtml.left).toEqual(expectedLeft); + expect(fileHtml.right).toEqual(expectedRight); }); }); - describe("generateSingleLineHtml", function() { - it("should work for insertions", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const sideBySidePrinter = new SideBySidePrinter({}); - const fileHtml = sideBySidePrinter.generateSingleLineHtml(false, diffParser.LINE_TYPE.INSERTS, 30, "test", "+"); + describe("generateSingleLineHtml", () => { + it("should work for insertions", () => { + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const fileHtml = sideBySideRenderer.generateSingleLineHtml(false, CSSLineClass.INSERTS, "test", 30, "+"); const expected = "\n" + ' \n' + @@ -171,12 +176,12 @@ describe("SideBySidePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); - it("should work for deletions", function() { - const diffParser = require("../diff-parser.js").DiffParser; - const sideBySidePrinter = new SideBySidePrinter({}); - const fileHtml = sideBySidePrinter.generateSingleLineHtml(false, diffParser.LINE_TYPE.DELETES, 30, "test", "-"); + it("should work for deletions", () => { + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const fileHtml = sideBySideRenderer.generateSingleLineHtml(false, CSSLineClass.DELETES, "test", 30, "-"); const expected = "\n" + ' \n' + @@ -190,33 +195,33 @@ describe("SideBySidePrinter", function() { " \n" + ""; - expect(expected).toEqual(fileHtml); + expect(fileHtml).toEqual(expected); }); }); - describe("generateSideBySideJsonHtml", function() { - it("should work for list of files", function() { - const exampleJson = [ + describe("generateSideBySideJsonHtml", () => { + it("should work for list of files", () => { + const exampleJson: DiffFile[] = [ { blocks: [ { lines: [ { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 1, - newNumber: null + newNumber: undefined }, { content: "+test1r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 1 } ], - oldStartLine: "1", - oldStartLine2: null, - newStartLine: "1", + oldStartLine: 1, + oldStartLine2: undefined, + newStartLine: 1, header: "@@ -1 +1 @@" } ], @@ -225,17 +230,19 @@ describe("SideBySidePrinter", function() { checksumBefore: "0000001", checksumAfter: "0ddf2ba", oldName: "sample", - language: undefined, + language: "txt", newName: "sample", - isCombined: false + isCombined: false, + isGitDiff: true } ]; - const sideBySidePrinter = new SideBySidePrinter({ matching: "lines" }); - const html = sideBySidePrinter.generateSideBySideJsonHtml(exampleJson); + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, { matching: "lines" }); + const html = sideBySideRenderer.render(exampleJson); const expected = '
\n' + - '
\n' + + '
\n' + '
\n' + ' \n' + '
@@ -1 +1 @@
\n' + " \n" + "\n" + - ' \n' + + ' \n' + " 1\n" + " \n" + - ' \n' + - '
\n' + + ' \n' + + '
\n' + ' -\n' + ' test\n' + "
\n" + @@ -278,11 +285,11 @@ describe("SideBySidePrinter", function() { '
\n' + " \n" + "\n" + - ' \n' + + ' \n' + " 1\n" + " \n" + - ' \n' + - '
\n' + + ' \n' + + '
\n' + ' +\n' + ' test1r\n' + "
\n" + @@ -296,21 +303,25 @@ describe("SideBySidePrinter", function() { "
\n" + "
"; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); - it("should work for files without blocks", function() { - const exampleJson = [ + it("should work for files without blocks", () => { + const exampleJson: DiffFile[] = [ { blocks: [], oldName: "sample", language: "js", newName: "sample", - isCombined: false + isCombined: false, + addedLines: 0, + deletedLines: 0, + isGitDiff: false } ]; - const sideBySidePrinter = new SideBySidePrinter(); - const html = sideBySidePrinter.generateSideBySideJsonHtml(exampleJson); + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {}); + const html = sideBySideRenderer.render(exampleJson); const expected = '
\n' + '
\n' + @@ -350,32 +361,33 @@ describe("SideBySidePrinter", function() { "
\n" + "
"; - expect(expected).toEqual(html); + expect(html).toEqual(expected); }); }); - describe("processLines", function() { - it("should process file lines", function() { - const oldLines = [ + describe("processLines", () => { + it("should process file lines", () => { + const oldLines: DiffLine[] = [ { content: "-test", - type: "d2h-del", + type: LineType.DELETE, oldNumber: 1, - newNumber: null + newNumber: undefined } ]; - const newLines = [ + const newLines: DiffLine[] = [ { content: "+test1r", - type: "d2h-ins", - oldNumber: null, + type: LineType.INSERT, + oldNumber: undefined, newNumber: 1 } ]; - const sideBySidePrinter = new SideBySidePrinter({ matching: "lines" }); - const html = sideBySidePrinter.processLines(false, oldLines, newLines); + const hoganUtils = new HoganJsUtils({}); + const sideBySideRenderer = new SideBySideRenderer(hoganUtils, { matching: "lines" }); + const html = sideBySideRenderer.processLines(false, oldLines, newLines); const expectedLeft = "\n" + ' \n' + @@ -402,8 +414,8 @@ describe("SideBySidePrinter", function() { " \n" + ""; - expect(expectedLeft).toEqual(html.left); - expect(expectedRight).toEqual(html.right); + expect(html.left).toEqual(expectedLeft); + expect(html.right).toEqual(expectedRight); }); }); }); diff --git a/src/__tests__/utils-tests.js b/src/__tests__/utils-tests.ts similarity index 71% rename from src/__tests__/utils-tests.js rename to src/__tests__/utils-tests.ts index 295b098..f64e0e2 100644 --- a/src/__tests__/utils-tests.js +++ b/src/__tests__/utils-tests.ts @@ -1,21 +1,21 @@ -const Utils = require("../utils.js").Utils; +import { escapeForHtml } from "../utils"; describe("Utils", function() { describe("escape", function() { it("should escape & with &", function() { - const result = Utils.escape("&"); + const result = escapeForHtml("&"); expect("&").toEqual(result); }); it("should escape < with <", function() { - const result = Utils.escape("<"); + const result = escapeForHtml("<"); expect("<").toEqual(result); }); it("should escape > with >", function() { - const result = Utils.escape(">"); + const result = escapeForHtml(">"); expect(">").toEqual(result); }); it("should escape a string with multiple problematic characters", function() { - const result = Utils.escape('\tlink text'); + const result = escapeForHtml('\tlink text'); const expected = "<a href="#">\tlink text</a>"; expect(expected).toEqual(result); }); diff --git a/src/diff-parser.js b/src/diff-parser.js deleted file mode 100644 index faaa4ef..0000000 --- a/src/diff-parser.js +++ /dev/null @@ -1,448 +0,0 @@ -/* - * - * Diff Parser (diff-parser.js) - * Author: rtfpessoa - * - */ - -(function() { - const utils = require("./utils.js").Utils; - - const LINE_TYPE = { - INSERTS: "d2h-ins", - DELETES: "d2h-del", - INSERT_CHANGES: "d2h-ins d2h-change", - DELETE_CHANGES: "d2h-del d2h-change", - CONTEXT: "d2h-cntx", - INFO: "d2h-info" - }; - - function DiffParser() {} - - DiffParser.prototype.LINE_TYPE = LINE_TYPE; - - DiffParser.prototype.generateDiffJson = function(diffInput, configuration) { - const config = configuration || {}; - - const files = []; - let currentFile = null; - let currentBlock = null; - let oldLine = null; - let oldLine2 = null; // Used for combined diff - let newLine = null; - - let possibleOldName; - let possibleNewName; - - /* Diff Header */ - const oldFileNameHeader = "--- "; - const newFileNameHeader = "+++ "; - const hunkHeaderPrefix = "@@"; - - /* Add previous block(if exists) before start a new file */ - function saveBlock() { - if (currentBlock) { - currentFile.blocks.push(currentBlock); - currentBlock = null; - } - } - - /* - * Add previous file(if exists) before start a new one - * if it has name (to avoid binary files errors) - */ - function saveFile() { - if (currentFile) { - if (!currentFile.oldName) { - currentFile.oldName = possibleOldName; - } - - if (!currentFile.newName) { - currentFile.newName = possibleNewName; - } - - if (currentFile.newName) { - files.push(currentFile); - currentFile = null; - } - } - - possibleOldName = undefined; - possibleNewName = undefined; - } - - /* Create file structure */ - function startFile() { - saveBlock(); - saveFile(); - - currentFile = {}; - currentFile.blocks = []; - currentFile.deletedLines = 0; - currentFile.addedLines = 0; - } - - function startBlock(line) { - saveBlock(); - - let values; - - /** - * From Range: - * -[,] - * - * To Range: - * +[,] - * - * @@ from-file-range to-file-range @@ - * - * @@@ from-file-range from-file-range to-file-range @@@ - * - * number of lines is optional, if omited consider 0 - */ - - if ((values = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@.*/.exec(line))) { - currentFile.isCombined = false; - oldLine = values[1]; - newLine = values[2]; - } else if ((values = /^@@@ -(\d+)(?:,\d+)? -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@@.*/.exec(line))) { - currentFile.isCombined = true; - oldLine = values[1]; - oldLine2 = values[2]; - newLine = values[3]; - } else { - if (utils.startsWith(line, hunkHeaderPrefix)) { - console.error("Failed to parse lines, starting in 0!"); - } - - oldLine = 0; - newLine = 0; - currentFile.isCombined = false; - } - - /* Create block metadata */ - currentBlock = {}; - currentBlock.lines = []; - currentBlock.oldStartLine = oldLine; - currentBlock.oldStartLine2 = oldLine2; - currentBlock.newStartLine = newLine; - currentBlock.header = line; - } - - function createLine(line) { - const currentLine = {}; - currentLine.content = line; - - const newLinePrefixes = !currentFile.isCombined ? ["+"] : ["+", " +"]; - const delLinePrefixes = !currentFile.isCombined ? ["-"] : ["-", " -"]; - - /* Fill the line data */ - if (utils.startsWith(line, newLinePrefixes)) { - currentFile.addedLines++; - - currentLine.type = LINE_TYPE.INSERTS; - currentLine.oldNumber = null; - currentLine.newNumber = newLine++; - - currentBlock.lines.push(currentLine); - } else if (utils.startsWith(line, delLinePrefixes)) { - currentFile.deletedLines++; - - currentLine.type = LINE_TYPE.DELETES; - currentLine.oldNumber = oldLine++; - currentLine.newNumber = null; - - currentBlock.lines.push(currentLine); - } else { - currentLine.type = LINE_TYPE.CONTEXT; - currentLine.oldNumber = oldLine++; - currentLine.newNumber = newLine++; - - currentBlock.lines.push(currentLine); - } - } - - /* - * Checks if there is a hunk header coming before a new file starts - * - * Hunk header is a group of three lines started by ( `--- ` , `+++ ` , `@@` ) - */ - function existHunkHeader(line, lineIdx) { - let idx = lineIdx; - - while (idx < diffLines.length - 3) { - if (utils.startsWith(line, "diff")) { - return false; - } - - if ( - utils.startsWith(diffLines[idx], oldFileNameHeader) && - utils.startsWith(diffLines[idx + 1], newFileNameHeader) && - utils.startsWith(diffLines[idx + 2], hunkHeaderPrefix) - ) { - return true; - } - - idx++; - } - - return false; - } - - var diffLines = diffInput - .replace(/\\ No newline at end of file/g, "") - .replace(/\r\n?/g, "\n") - .split("\n"); - - /* Diff */ - const oldMode = /^old mode (\d{6})/; - const newMode = /^new mode (\d{6})/; - const deletedFileMode = /^deleted file mode (\d{6})/; - const newFileMode = /^new file mode (\d{6})/; - - const copyFrom = /^copy from "?(.+)"?/; - const copyTo = /^copy to "?(.+)"?/; - - const renameFrom = /^rename from "?(.+)"?/; - const renameTo = /^rename to "?(.+)"?/; - - const similarityIndex = /^similarity index (\d+)%/; - const dissimilarityIndex = /^dissimilarity index (\d+)%/; - const index = /^index ([0-9a-z]+)\.\.([0-9a-z]+)\s*(\d{6})?/; - - const binaryFiles = /^Binary files (.*) and (.*) differ/; - const binaryDiff = /^GIT binary patch/; - - /* Combined Diff */ - const combinedIndex = /^index ([0-9a-z]+),([0-9a-z]+)\.\.([0-9a-z]+)/; - const combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/; - const combinedNewFile = /^new file mode (\d{6})/; - const combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/; - - diffLines.forEach(function(line, lineIndex) { - // Unmerged paths, and possibly other non-diffable files - // https://github.com/scottgonzalez/pretty-diff/issues/11 - // Also, remove some useless lines - if (!line || utils.startsWith(line, "*")) { - return; - } - - // Used to store regex capture groups - let values; - - const prevLine = diffLines[lineIndex - 1]; - const nxtLine = diffLines[lineIndex + 1]; - const afterNxtLine = diffLines[lineIndex + 2]; - - if (utils.startsWith(line, "diff")) { - startFile(); - - // diff --git a/blocked_delta_results.png b/blocked_delta_results.png - const gitDiffStart = /^diff --git "?(.+)"? "?(.+)"?/; - if ((values = gitDiffStart.exec(line))) { - possibleOldName = _getFilename(null, values[1], config.dstPrefix); - possibleNewName = _getFilename(null, values[2], config.srcPrefix); - } - - currentFile.isGitDiff = true; - return; - } - - if ( - !currentFile || // If we do not have a file yet, we should crete one - (!currentFile.isGitDiff && - currentFile && // If we already have some file in progress and - (utils.startsWith(line, oldFileNameHeader) && // If we get to an old file path header line - // And is followed by the new file path header line and the hunk header line - utils.startsWith(nxtLine, newFileNameHeader) && - utils.startsWith(afterNxtLine, hunkHeaderPrefix))) - ) { - startFile(); - } - - /* - * We need to make sure that we have the three lines of the header. - * This avoids cases like the ones described in: - * - https://github.com/rtfpessoa/diff2html/issues/87 - */ - if ( - (utils.startsWith(line, oldFileNameHeader) && utils.startsWith(nxtLine, newFileNameHeader)) || - (utils.startsWith(line, newFileNameHeader) && utils.startsWith(prevLine, oldFileNameHeader)) - ) { - /* - * --- Date Timestamp[FractionalSeconds] TimeZone - * --- 2002-02-21 23:30:39.942229878 -0800 - */ - if ( - currentFile && - !currentFile.oldName && - utils.startsWith(line, "--- ") && - (values = getSrcFilename(line, config)) - ) { - currentFile.oldName = values; - currentFile.language = getExtension(currentFile.oldName, currentFile.language); - return; - } - - /* - * +++ Date Timestamp[FractionalSeconds] TimeZone - * +++ 2002-02-21 23:30:39.942229878 -0800 - */ - if ( - currentFile && - !currentFile.newName && - utils.startsWith(line, "+++ ") && - (values = getDstFilename(line, config)) - ) { - currentFile.newName = values; - currentFile.language = getExtension(currentFile.newName, currentFile.language); - return; - } - } - - if ( - (currentFile && utils.startsWith(line, hunkHeaderPrefix)) || - (currentFile.isGitDiff && currentFile && currentFile.oldName && currentFile.newName && !currentBlock) - ) { - startBlock(line); - return; - } - - /* - * There are three types of diff lines. These lines are defined by the way they start. - * 1. New line starts with: + - * 2. Old line starts with: - - * 3. Context line starts with: - */ - if (currentBlock && (utils.startsWith(line, "+") || utils.startsWith(line, "-") || utils.startsWith(line, " "))) { - createLine(line); - return; - } - - const doesNotExistHunkHeader = !existHunkHeader(line, lineIndex); - - /* - * Git diffs provide more information regarding files modes, renames, copies, - * commits between changes and similarity indexes - */ - if ((values = oldMode.exec(line))) { - currentFile.oldMode = values[1]; - } else if ((values = newMode.exec(line))) { - currentFile.newMode = values[1]; - } else if ((values = deletedFileMode.exec(line))) { - currentFile.deletedFileMode = values[1]; - currentFile.isDeleted = true; - } else if ((values = newFileMode.exec(line))) { - currentFile.newFileMode = values[1]; - currentFile.isNew = true; - } else if ((values = copyFrom.exec(line))) { - if (doesNotExistHunkHeader) { - currentFile.oldName = values[1]; - } - currentFile.isCopy = true; - } else if ((values = copyTo.exec(line))) { - if (doesNotExistHunkHeader) { - currentFile.newName = values[1]; - } - currentFile.isCopy = true; - } else if ((values = renameFrom.exec(line))) { - if (doesNotExistHunkHeader) { - currentFile.oldName = values[1]; - } - currentFile.isRename = true; - } else if ((values = renameTo.exec(line))) { - if (doesNotExistHunkHeader) { - currentFile.newName = values[1]; - } - currentFile.isRename = true; - } else if ((values = binaryFiles.exec(line))) { - currentFile.isBinary = true; - currentFile.oldName = _getFilename(null, values[1], config.srcPrefix); - currentFile.newName = _getFilename(null, values[2], config.dstPrefix); - startBlock("Binary file"); - } else if ((values = binaryDiff.exec(line))) { - currentFile.isBinary = true; - startBlock(line); - } else if ((values = similarityIndex.exec(line))) { - currentFile.unchangedPercentage = values[1]; - } else if ((values = dissimilarityIndex.exec(line))) { - currentFile.changedPercentage = values[1]; - } else if ((values = index.exec(line))) { - currentFile.checksumBefore = values[1]; - currentFile.checksumAfter = values[2]; - values[3] && (currentFile.mode = values[3]); - } else if ((values = combinedIndex.exec(line))) { - currentFile.checksumBefore = [values[2], values[3]]; - currentFile.checksumAfter = values[1]; - } else if ((values = combinedMode.exec(line))) { - currentFile.oldMode = [values[2], values[3]]; - currentFile.newMode = values[1]; - } else if ((values = combinedNewFile.exec(line))) { - currentFile.newFileMode = values[1]; - currentFile.isNew = true; - } else if ((values = combinedDeletedFile.exec(line))) { - currentFile.deletedFileMode = values[1]; - currentFile.isDeleted = true; - } - }); - - saveBlock(); - saveFile(); - - return files; - }; - - function getExtension(filename, language) { - const nameSplit = filename.split("."); - if (nameSplit.length > 1) { - return nameSplit[nameSplit.length - 1]; - } - - return language; - } - - function getSrcFilename(line, cfg) { - return _getFilename("---", line, cfg.srcPrefix); - } - - function getDstFilename(line, cfg) { - return _getFilename("\\+\\+\\+", line, cfg.dstPrefix); - } - - function _getFilename(linePrefix, line, extraPrefix) { - const prefixes = ["a/", "b/", "i/", "w/", "c/", "o/"]; - if (extraPrefix) { - prefixes.push(extraPrefix); - } - - let FilenameRegExp; - if (linePrefix) { - FilenameRegExp = new RegExp("^" + linePrefix + ' "?(.+?)"?$'); - } else { - FilenameRegExp = new RegExp('^"?(.+?)"?$'); - } - - let filename; - const values = FilenameRegExp.exec(line); - if (values && values[1]) { - filename = values[1]; - const matchingPrefixes = prefixes.filter(function(p) { - return filename.indexOf(p) === 0; - }); - - if (matchingPrefixes[0]) { - // Remove prefix if exists - filename = filename.slice(matchingPrefixes[0].length); - } - - // Cleanup timestamps generated by the unified diff (diff command) as specified in - // https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html - // Ie: 2016-10-25 11:37:14.000000000 +0200 - filename = filename.replace(/\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [-+]\d{4}.*$/, ""); - } - - return filename; - } - - module.exports.DiffParser = new DiffParser(); -})(); diff --git a/src/diff-parser.ts b/src/diff-parser.ts new file mode 100644 index 0000000..1410109 --- /dev/null +++ b/src/diff-parser.ts @@ -0,0 +1,436 @@ +import { DiffFile, DiffBlock, DiffLine, LineType } from "./render-utils"; +import { escapeForRegExp } from "./utils"; + +export interface DiffParserConfig { + srcPrefix?: string; + dstPrefix?: string; +} + +function getExtension(filename: string, language: string): string { + const filenameParts = filename.split("."); + return filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : language; +} + +function startsWithAny(str: string, prefixes: string[]): boolean { + return prefixes.reduce((startsWith, prefix) => startsWith || str.startsWith(prefix), false); +} + +const baseDiffFilenamePrefixes = ["a/", "b/", "i/", "w/", "c/", "o/"]; +function getFilename(line: string, linePrefix?: string, extraPrefix?: string): string { + const prefixes = extraPrefix !== undefined ? [...baseDiffFilenamePrefixes, extraPrefix] : baseDiffFilenamePrefixes; + + const FilenameRegExp = linePrefix + ? new RegExp(`^${escapeForRegExp(linePrefix)} "?(.+?)"?$`) + : new RegExp('^"?(.+?)"?$'); + + const [, filename = ""] = FilenameRegExp.exec(line) || []; // TODO: Check if this is safe + const matchingPrefix = prefixes.find(p => filename.indexOf(p) === 0); + const fnameWithoutPrefix = matchingPrefix ? filename.slice(matchingPrefix.length) : filename; + + // Cleanup timestamps generated by the unified diff (diff command) as specified in + // https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html + // Ie: 2016-10-25 11:37:14.000000000 +0200 + return fnameWithoutPrefix.replace(/\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [-+]\d{4}.*$/, ""); +} + +function getSrcFilename(line: string, srcPrefix?: string): string | undefined { + return getFilename(line, "---", srcPrefix); +} + +function getDstFilename(line: string, dstPrefix?: string): string | undefined { + return getFilename(line, "+++", dstPrefix); +} + +/** + * + * Docs: + * - Unified: https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html + * - Git Diff: https://git-scm.com/docs/git-diff-tree#_raw_output_format + * - Git Combined Diff: https://git-scm.com/docs/git-diff-tree#_combined_diff_format + * + */ +export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFile[] { + const files: DiffFile[] = []; + let currentFile: DiffFile | null = null; + let currentBlock: DiffBlock | null = null; + let oldLine: number | null = null; + let oldLine2: number | null = null; // Used for combined diff + let newLine: number | null = null; + + let possibleOldName: string | null = null; + let possibleNewName: string | null = null; + + /* Diff Header */ + const oldFileNameHeader = "--- "; + const newFileNameHeader = "+++ "; + const hunkHeaderPrefix = "@@"; + + /* Diff */ + const oldMode = /^old mode (\d{6})/; + const newMode = /^new mode (\d{6})/; + const deletedFileMode = /^deleted file mode (\d{6})/; + const newFileMode = /^new file mode (\d{6})/; + + const copyFrom = /^copy from "?(.+)"?/; + const copyTo = /^copy to "?(.+)"?/; + + const renameFrom = /^rename from "?(.+)"?/; + const renameTo = /^rename to "?(.+)"?/; + + const similarityIndex = /^similarity index (\d+)%/; + const dissimilarityIndex = /^dissimilarity index (\d+)%/; + const index = /^index ([0-9a-z]+)\.\.([0-9a-z]+)\s*(\d{6})?/; + + const binaryFiles = /^Binary files (.*) and (.*) differ/; + const binaryDiff = /^GIT binary patch/; + + /* Combined Diff */ + const combinedIndex = /^index ([0-9a-z]+),([0-9a-z]+)\.\.([0-9a-z]+)/; + const combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/; + const combinedNewFile = /^new file mode (\d{6})/; + const combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/; + + const diffLines = diffInput + .replace(/\\ No newline at end of file/g, "") + .replace(/\r\n?/g, "\n") + .split("\n"); + + /* Add previous block(if exists) before start a new file */ + function saveBlock(): void { + if (currentBlock !== null && currentFile !== null) { + currentFile.blocks.push(currentBlock); + currentBlock = null; + } + } + + /* + * Add previous file(if exists) before start a new one + * if it has name (to avoid binary files errors) + */ + function saveFile(): void { + if (currentFile !== null) { + if (!currentFile.oldName && possibleOldName !== null) { + currentFile.oldName = possibleOldName; + } + + if (!currentFile.newName && possibleNewName !== null) { + currentFile.newName = possibleNewName; + } + + if (currentFile.newName) { + files.push(currentFile); + currentFile = null; + } + } + + possibleOldName = null; + possibleNewName = null; + } + + /* Create file structure */ + function startFile(): void { + saveBlock(); + saveFile(); + + // TODO: Avoid disabling types + // eslint-disable-next-line + // @ts-ignore + currentFile = { + blocks: [], + deletedLines: 0, + addedLines: 0 + }; + } + + function startBlock(line: string): void { + saveBlock(); + + let values; + + /** + * From Range: + * -[,] + * + * To Range: + * +[,] + * + * @@ from-file-range to-file-range @@ + * + * @@@ from-file-range from-file-range to-file-range @@@ + * + * number of lines is optional, if omited consider 0 + */ + + if (currentFile !== null) { + if ((values = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@.*/.exec(line))) { + currentFile.isCombined = false; + oldLine = parseInt(values[1], 10); + newLine = parseInt(values[2], 10); + } else if ((values = /^@@@ -(\d+)(?:,\d+)? -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@@.*/.exec(line))) { + currentFile.isCombined = true; + oldLine = parseInt(values[1], 10); + oldLine2 = parseInt(values[2], 10); + newLine = parseInt(values[3], 10); + } else { + if (line.startsWith(hunkHeaderPrefix)) { + console.error("Failed to parse lines, starting in 0!"); + } + + oldLine = 0; + newLine = 0; + currentFile.isCombined = false; + } + } + + /* Create block metadata */ + // TODO: Avoid disabling types + // eslint-disable-next-line + // @ts-ignore + currentBlock = { + lines: [], + oldStartLine: oldLine, + oldStartLine2: oldLine2, + newStartLine: newLine, + header: line + }; + } + + function createLine(line: string): void { + if (currentFile === null || currentBlock === null || oldLine === null || newLine === null) return; + + // TODO: Avoid disabling types + // eslint-disable-next-line + // @ts-ignore + const currentLine: DiffLine = { + content: line + }; + + const addedPrefixes = currentFile.isCombined ? ["+ ", " +", "++"] : ["+"]; + const deletedPrefixes = currentFile.isCombined ? ["- ", " -", "--"] : ["-"]; + + // TODO: Check if this makes sense for combined diff + if (startsWithAny(line, addedPrefixes)) { + currentFile.addedLines++; + currentLine.type = LineType.INSERT; + currentLine.oldNumber = undefined; + currentLine.newNumber = newLine++; + } else if (startsWithAny(line, deletedPrefixes)) { + currentFile.deletedLines++; + currentLine.type = LineType.DELETE; + currentLine.oldNumber = oldLine++; + currentLine.newNumber = undefined; + } else { + currentLine.type = LineType.CONTEXT; + currentLine.oldNumber = oldLine++; + currentLine.newNumber = newLine++; + } + currentBlock.lines.push(currentLine); + } + + /* + * Checks if there is a hunk header coming before a new file starts + * + * Hunk header is a group of three lines started by ( `--- ` , `+++ ` , `@@` ) + */ + function existHunkHeader(line: string, lineIdx: number): boolean { + let idx = lineIdx; + + while (idx < diffLines.length - 3) { + if (line.startsWith("diff")) { + return false; + } + + if ( + diffLines[idx].startsWith(oldFileNameHeader) && + diffLines[idx + 1].startsWith(newFileNameHeader) && + diffLines[idx + 2].startsWith(hunkHeaderPrefix) + ) { + return true; + } + + idx++; + } + + return false; + } + + diffLines.forEach(function(line, lineIndex) { + // Unmerged paths, and possibly other non-diffable files + // https://github.com/scottgonzalez/pretty-diff/issues/11 + // Also, remove some useless lines + if (!line || line.startsWith("*")) { + return; + } + + // Used to store regex capture groups + let values; + + const prevLine = diffLines[lineIndex - 1]; + const nxtLine = diffLines[lineIndex + 1]; + const afterNxtLine = diffLines[lineIndex + 2]; + + if (line.startsWith("diff")) { + startFile(); + + // diff --git a/blocked_delta_results.png b/blocked_delta_results.png + const gitDiffStart = /^diff --git "?(.+)"? "?(.+)"?/; + if ((values = gitDiffStart.exec(line))) { + possibleOldName = getFilename(values[1], undefined, config.dstPrefix); + possibleNewName = getFilename(values[2], undefined, config.srcPrefix); + } + + if (currentFile === null) { + throw new Error("Where is my file !!!"); + } + + currentFile.isGitDiff = true; + return; + } + + if ( + !currentFile || // If we do not have a file yet, we should crete one + (!currentFile.isGitDiff && + currentFile && // If we already have some file in progress and + (line.startsWith(oldFileNameHeader) && // If we get to an old file path header line + // And is followed by the new file path header line and the hunk header line + nxtLine.startsWith(newFileNameHeader) && + afterNxtLine.startsWith(hunkHeaderPrefix))) + ) { + startFile(); + } + + /* + * We need to make sure that we have the three lines of the header. + * This avoids cases like the ones described in: + * - https://github.com/rtfpessoa/diff2html/issues/87 + */ + if ( + (line.startsWith(oldFileNameHeader) && nxtLine.startsWith(newFileNameHeader)) || + (line.startsWith(newFileNameHeader) && prevLine.startsWith(oldFileNameHeader)) + ) { + /* + * --- Date Timestamp[FractionalSeconds] TimeZone + * --- 2002-02-21 23:30:39.942229878 -0800 + */ + if ( + currentFile && + !currentFile.oldName && + line.startsWith("--- ") && + (values = getSrcFilename(line, config.srcPrefix)) + ) { + currentFile.oldName = values; + currentFile.language = getExtension(currentFile.oldName, currentFile.language); + return; + } + + /* + * +++ Date Timestamp[FractionalSeconds] TimeZone + * +++ 2002-02-21 23:30:39.942229878 -0800 + */ + if ( + currentFile && + !currentFile.newName && + line.startsWith("+++ ") && + (values = getDstFilename(line, config.dstPrefix)) + ) { + currentFile.newName = values; + currentFile.language = getExtension(currentFile.newName, currentFile.language); + return; + } + } + + if ( + (currentFile && line.startsWith(hunkHeaderPrefix)) || + (currentFile && currentFile.isGitDiff && currentFile.oldName && currentFile.newName && !currentBlock) + ) { + startBlock(line); + return; + } + + /* + * There are three types of diff lines. These lines are defined by the way they start. + * 1. New line starts with: + + * 2. Old line starts with: - + * 3. Context line starts with: + */ + if (currentBlock && (line.startsWith("+") || line.startsWith("-") || line.startsWith(" "))) { + createLine(line); + return; + } + + const doesNotExistHunkHeader = !existHunkHeader(line, lineIndex); + + if (currentFile === null) { + throw new Error("Where is my file !!!"); + } + + /* + * Git diffs provide more information regarding files modes, renames, copies, + * commits between changes and similarity indexes + */ + if ((values = oldMode.exec(line))) { + currentFile.oldMode = values[1]; + } else if ((values = newMode.exec(line))) { + currentFile.newMode = values[1]; + } else if ((values = deletedFileMode.exec(line))) { + currentFile.deletedFileMode = values[1]; + currentFile.isDeleted = true; + } else if ((values = newFileMode.exec(line))) { + currentFile.newFileMode = values[1]; + currentFile.isNew = true; + } else if ((values = copyFrom.exec(line))) { + if (doesNotExistHunkHeader) { + currentFile.oldName = values[1]; + } + currentFile.isCopy = true; + } else if ((values = copyTo.exec(line))) { + if (doesNotExistHunkHeader) { + currentFile.newName = values[1]; + } + currentFile.isCopy = true; + } else if ((values = renameFrom.exec(line))) { + if (doesNotExistHunkHeader) { + currentFile.oldName = values[1]; + } + currentFile.isRename = true; + } else if ((values = renameTo.exec(line))) { + if (doesNotExistHunkHeader) { + currentFile.newName = values[1]; + } + currentFile.isRename = true; + } else if ((values = binaryFiles.exec(line))) { + currentFile.isBinary = true; + currentFile.oldName = getFilename(values[1], undefined, config.srcPrefix); + currentFile.newName = getFilename(values[2], undefined, config.dstPrefix); + startBlock("Binary file"); + } else if ((values = binaryDiff.exec(line))) { + currentFile.isBinary = true; + startBlock(line); + } else if ((values = similarityIndex.exec(line))) { + currentFile.unchangedPercentage = parseInt(values[1], 10); + } else if ((values = dissimilarityIndex.exec(line))) { + currentFile.changedPercentage = parseInt(values[1], 10); + } else if ((values = index.exec(line))) { + currentFile.checksumBefore = values[1]; + currentFile.checksumAfter = values[2]; + values[3] && (currentFile.mode = values[3]); + } else if ((values = combinedIndex.exec(line))) { + currentFile.checksumBefore = [values[2], values[3]]; + currentFile.checksumAfter = values[1]; + } else if ((values = combinedMode.exec(line))) { + currentFile.oldMode = [values[2], values[3]]; + currentFile.newMode = values[1]; + } else if ((values = combinedNewFile.exec(line))) { + currentFile.newFileMode = values[1]; + currentFile.isNew = true; + } else if ((values = combinedDeletedFile.exec(line))) { + currentFile.deletedFileMode = values[1]; + currentFile.isDeleted = true; + } + }); + + saveBlock(); + saveFile(); + + return files; +} diff --git a/src/diff2html.d.ts b/src/diff2html.d.ts deleted file mode 100644 index de77dbf..0000000 --- a/src/diff2html.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Type definitions for diff2html -// Project: https://github.com/rtfpessoa/diff2html -// Definitions by: rtfpessoa -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -declare namespace Diff2Html { - export interface Options { - inputFormat?: "diff" | "json"; - outputFormat?: "line-by-line" | "side-by-side"; - showFiles?: boolean; - diffStyle?: "word" | "char"; - matching?: "lines" | "words" | "none"; - matchWordsThreshold?: number; - matchingMaxComparisons?: number; - maxLineSizeInBlockForComparison?: number; - maxLineLengthHighlight?: number; - templates?: object; - rawTemplates?: object; - renderNothingWhenEmpty?: boolean; - } - - export interface Line { - content: string; - type: string; - oldNumber: number; - newNumber: number; - } - - export interface Block { - oldStartLine: number; - oldStartLine2?: number; - newStartLine: number; - header: string; - lines: Line[]; - } - - export interface Result { - addedLines: number; - deletedLines: number; - isCombined: boolean; - isGitDiff: boolean; - oldName: string; - newName: string; - language: string; - blocks: Block[]; - oldMode?: string; - newMode?: string; - deletedFileMode?: string; - newFileMode?: string; - isDeleted?: boolean; - isNew?: boolean; - isCopy?: boolean; - isRename?: boolean; - unchangedPercentage?: number; - changedPercentage?: number; - checksumBefore?: string; - checksumAfter?: string; - mode?: string; - } - - export interface Diff2Html { - getJsonFromDiff(input: string, configuration?: Options): Result[]; - getPrettyHtml(input: any, configuration?: Options): string; - } -} - -declare module "diff2html" { - var d2h: { Diff2Html: Diff2Html.Diff2Html }; - export = d2h; -} diff --git a/src/diff2html.js b/src/diff2html.js deleted file mode 100644 index 2b89372..0000000 --- a/src/diff2html.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * - * Diff to HTML (diff2html.js) - * Author: rtfpessoa - * - */ - -(function() { - const diffParser = require("./diff-parser.js").DiffParser; - const htmlPrinter = require("./html-printer.js").HtmlPrinter; - const utils = require("./utils.js").Utils; - - function Diff2Html() {} - - const defaultConfig = { - inputFormat: "diff", - outputFormat: "line-by-line", - showFiles: false, - diffStyle: "word", - matching: "none", - matchWordsThreshold: 0.25, - matchingMaxComparisons: 2500, - maxLineSizeInBlockForComparison: 200, - maxLineLengthHighlight: 10000, - templates: {}, - rawTemplates: {}, - renderNothingWhenEmpty: false - }; - - /* - * Generates json object from string diff input - */ - Diff2Html.prototype.getJsonFromDiff = function(diffInput, config) { - const cfg = utils.safeConfig(config, defaultConfig); - return diffParser.generateDiffJson(diffInput, cfg); - }; - - /* - * Generates the html diff. The config parameter configures the output/input formats and other options - */ - Diff2Html.prototype.getPrettyHtml = function(diffInput, config) { - const cfg = utils.safeConfig(config, defaultConfig); - - let diffJson = diffInput; - if (!cfg.inputFormat || cfg.inputFormat === "diff") { - diffJson = diffParser.generateDiffJson(diffInput, cfg); - } - - let fileList = ""; - if (cfg.showFiles === true) { - fileList = htmlPrinter.generateFileListSummary(diffJson, cfg); - } - - let diffOutput = ""; - if (cfg.outputFormat === "side-by-side") { - diffOutput = htmlPrinter.generateSideBySideJsonHtml(diffJson, cfg); - } else { - diffOutput = htmlPrinter.generateLineByLineJsonHtml(diffJson, cfg); - } - - return fileList + diffOutput; - }; - - /* - * Deprecated methods - The following methods exist only to maintain compatibility with previous versions - */ - - /* - * Generates pretty html from string diff input - */ - Diff2Html.prototype.getPrettyHtmlFromDiff = function(diffInput, config) { - const cfg = utils.safeConfig(config, defaultConfig); - cfg.inputFormat = "diff"; - cfg.outputFormat = "line-by-line"; - return this.getPrettyHtml(diffInput, cfg); - }; - - /* - * Generates pretty html from a json object - */ - Diff2Html.prototype.getPrettyHtmlFromJson = function(diffJson, config) { - const cfg = utils.safeConfig(config, defaultConfig); - cfg.inputFormat = "json"; - cfg.outputFormat = "line-by-line"; - return this.getPrettyHtml(diffJson, cfg); - }; - - /* - * Generates pretty side by side html from string diff input - */ - Diff2Html.prototype.getPrettySideBySideHtmlFromDiff = function(diffInput, config) { - const cfg = utils.safeConfig(config, defaultConfig); - cfg.inputFormat = "diff"; - cfg.outputFormat = "side-by-side"; - return this.getPrettyHtml(diffInput, cfg); - }; - - /* - * Generates pretty side by side html from a json object - */ - Diff2Html.prototype.getPrettySideBySideHtmlFromJson = function(diffJson, config) { - const cfg = utils.safeConfig(config, defaultConfig); - cfg.inputFormat = "json"; - cfg.outputFormat = "side-by-side"; - return this.getPrettyHtml(diffJson, cfg); - }; - - const diffObject = new Diff2Html(); - module.exports.Diff2Html = diffObject; - - // Expose diff2html in the browser - global.Diff2Html = diffObject; -})(); diff --git a/src/diff2html.ts b/src/diff2html.ts new file mode 100644 index 0000000..7154258 --- /dev/null +++ b/src/diff2html.ts @@ -0,0 +1,50 @@ +import * as DiffParser from "./diff-parser"; +import * as fileListPrinter from "./file-list-renderer"; +import LineByLineRenderer, { LineByLineRendererConfig, defaultLineByLineRendererConfig } from "./line-by-line-renderer"; +import SideBySideRenderer, { SideBySideRendererConfig, defaultSideBySideRendererConfig } from "./side-by-side-renderer"; +import { DiffFile } from "./render-utils"; +import HoganJsUtils, { HoganJsUtilsConfig } from "./hoganjs-utils"; + +type OutputFormatType = "line-by-line" | "side-by-side"; + +export interface Diff2HtmlConfig + extends DiffParser.DiffParserConfig, + LineByLineRendererConfig, + SideBySideRendererConfig, + HoganJsUtilsConfig { + outputFormat?: OutputFormatType; + showFiles?: boolean; +} + +export const defaultDiff2HtmlConfig = { + ...defaultLineByLineRendererConfig, + ...defaultSideBySideRendererConfig, + outputFormat: "line-by-line" as OutputFormatType, + showFiles: false +}; + +export function parse(diffInput: string, configuration: Diff2HtmlConfig = {}): DiffFile[] { + return DiffParser.parse(diffInput, { ...defaultDiff2HtmlConfig, ...configuration }); +} + +export function html(diffInput: string | DiffFile[], configuration: Diff2HtmlConfig = {}): string { + const config = { ...defaultDiff2HtmlConfig, ...configuration }; + + const diffJson = typeof diffInput === "string" ? DiffParser.parse(diffInput, config) : diffInput; + + const hoganUtils = new HoganJsUtils(config); + + const fileList = config.showFiles ? fileListPrinter.render(diffJson, hoganUtils) : ""; + + const diffOutput = + config.outputFormat === "side-by-side" + ? new SideBySideRenderer(hoganUtils, config).render(diffJson) + : new LineByLineRenderer(hoganUtils, config).render(diffJson); + + // TODO: Review error handling + if (diffOutput === undefined) { + throw new Error("OMG we haz no diff. Why???"); + } + + return fileList + diffOutput; +} diff --git a/src/file-list-printer.js b/src/file-list-printer.js deleted file mode 100644 index 97e31b7..0000000 --- a/src/file-list-printer.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * - * FileListPrinter (file-list-printer.js) - * Author: nmatpt - * - */ - -(function() { - const printerUtils = require("./printer-utils.js").PrinterUtils; - - let hoganUtils; - - const baseTemplatesPath = "file-summary"; - const iconsBaseTemplatesPath = "icon"; - - function FileListPrinter(config) { - this.config = config; - - const HoganJsUtils = require("./hoganjs-utils.js").HoganJsUtils; - hoganUtils = new HoganJsUtils(config); - } - - FileListPrinter.prototype.generateFileList = function(diffFiles) { - const lineTemplate = hoganUtils.template(baseTemplatesPath, "line"); - - const files = diffFiles - .map(function(file) { - const fileTypeName = printerUtils.getFileTypeIcon(file); - const iconTemplate = hoganUtils.template(iconsBaseTemplatesPath, fileTypeName); - - return lineTemplate.render( - { - fileHtmlId: printerUtils.getHtmlId(file), - oldName: file.oldName, - newName: file.newName, - fileName: printerUtils.getDiffName(file), - deletedLines: "-" + file.deletedLines, - addedLines: "+" + file.addedLines - }, - { - fileIcon: iconTemplate - } - ); - }) - .join("\n"); - - return hoganUtils.render(baseTemplatesPath, "wrapper", { - filesNumber: diffFiles.length, - files: files - }); - }; - - module.exports.FileListPrinter = FileListPrinter; -})(); diff --git a/src/file-list-renderer.ts b/src/file-list-renderer.ts new file mode 100644 index 0000000..6fd0a6c --- /dev/null +++ b/src/file-list-renderer.ts @@ -0,0 +1,32 @@ +import * as renderUtils from "./render-utils"; +import HoganJsUtils from "./hoganjs-utils"; + +const baseTemplatesPath = "file-summary"; +const iconsBaseTemplatesPath = "icon"; + +export function render(diffFiles: renderUtils.DiffFile[], hoganUtils: HoganJsUtils): string { + const files = diffFiles + .map(file => + hoganUtils.render( + baseTemplatesPath, + "line", + { + fileHtmlId: renderUtils.getHtmlId(file), + oldName: file.oldName, + newName: file.newName, + fileName: renderUtils.filenameDiff(file), + deletedLines: "-" + file.deletedLines, + addedLines: "+" + file.addedLines + }, + { + fileIcon: hoganUtils.template(iconsBaseTemplatesPath, renderUtils.getFileIcon(file)) + } + ) + ) + .join("\n"); + + return hoganUtils.render(baseTemplatesPath, "wrapper", { + filesNumber: diffFiles.length, + files: files + }); +} diff --git a/src/hoganjs-utils.js b/src/hoganjs-utils.js deleted file mode 100644 index 9727557..0000000 --- a/src/hoganjs-utils.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * - * Utils (hoganjs-utils.js) - * Author: rtfpessoa - * - */ - -(function() { - const fs = require("fs"); - const path = require("path"); - const hogan = require("hogan.js"); - - const hoganTemplates = require("./diff2html-templates.js"); - - let extraTemplates; - - function HoganJsUtils(configuration) { - this.config = configuration || {}; - extraTemplates = this.config.templates || {}; - - const rawTemplates = this.config.rawTemplates || {}; - for (const templateName in rawTemplates) { - if (rawTemplates.hasOwnProperty(templateName)) { - if (!extraTemplates[templateName]) extraTemplates[templateName] = this.compile(rawTemplates[templateName]); - } - } - } - - HoganJsUtils.prototype.render = function(namespace, view, params) { - const template = this.template(namespace, view); - if (template) { - return template.render(params); - } - - return null; - }; - - HoganJsUtils.prototype.template = function(namespace, view) { - const templateKey = this._templateKey(namespace, view); - - return this._getTemplate(templateKey); - }; - - HoganJsUtils.prototype._getTemplate = function(templateKey) { - let template; - - if (!this.config.noCache) { - template = this._readFromCache(templateKey); - } - - if (!template) { - template = this._loadTemplate(templateKey); - } - - return template; - }; - - HoganJsUtils.prototype._loadTemplate = function(templateKey) { - let template; - - try { - if (fs.readFileSync) { - const templatesPath = path.resolve(__dirname, "templates"); - const templatePath = path.join(templatesPath, templateKey); - const templateContent = fs.readFileSync(templatePath + ".mustache", "utf8"); - template = hogan.compile(templateContent); - hoganTemplates[templateKey] = template; - } - } catch (e) { - console.error("Failed to read (template: " + templateKey + ") from fs: " + e.message); - } - - return template; - }; - - HoganJsUtils.prototype._readFromCache = function(templateKey) { - return extraTemplates[templateKey] || hoganTemplates[templateKey]; - }; - - HoganJsUtils.prototype._templateKey = function(namespace, view) { - return namespace + "-" + view; - }; - - HoganJsUtils.prototype.compile = function(templateStr) { - return hogan.compile(templateStr); - }; - - module.exports.HoganJsUtils = HoganJsUtils; -})(); diff --git a/src/hoganjs-utils.ts b/src/hoganjs-utils.ts new file mode 100644 index 0000000..4ae987d --- /dev/null +++ b/src/hoganjs-utils.ts @@ -0,0 +1,54 @@ +import * as Hogan from "hogan.js"; + +import { defaultTemplates } from "./diff2html-templates"; + +export interface RawTemplates { + [name: string]: string; +} + +export interface CompiledTemplates { + [name: string]: Hogan.Template; +} + +export interface HoganJsUtilsConfig { + compiledTemplates?: CompiledTemplates; + rawTemplates?: RawTemplates; +} + +export default class HoganJsUtils { + private preCompiledTemplates: CompiledTemplates; + + constructor({ compiledTemplates = {}, rawTemplates = {} }: HoganJsUtilsConfig) { + const compiledRawTemplates = Object.entries(rawTemplates).reduce( + (previousTemplates, [name, templateString]) => { + const compiledTemplate: Hogan.Template = Hogan.compile(templateString, { asString: false }); + return { ...previousTemplates, [name]: compiledTemplate }; + }, + {} + ); + + this.preCompiledTemplates = { ...defaultTemplates, ...compiledTemplates, ...compiledRawTemplates }; + } + + static compile(templateString: string): Hogan.Template { + return Hogan.compile(templateString, { asString: false }); + } + + render(namespace: string, view: string, params: Hogan.Context, partials?: Hogan.Partials, indent?: string): string { + const templateKey = this.templateKey(namespace, view); + try { + const template = this.preCompiledTemplates[templateKey]; + return template.render(params, partials, indent); + } catch (e) { + throw new Error(`Could not find template to render '${templateKey}'`); + } + } + + template(namespace: string, view: string): Hogan.Template { + return this.preCompiledTemplates[this.templateKey(namespace, view)]; + } + + private templateKey(namespace: string, view: string): string { + return `${namespace}-${view}`; + } +} diff --git a/src/html-printer.js b/src/html-printer.js deleted file mode 100644 index 2c76d83..0000000 --- a/src/html-printer.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * - * HtmlPrinter (html-printer.js) - * Author: rtfpessoa - * - */ - -(function() { - const LineByLinePrinter = require("./line-by-line-printer.js").LineByLinePrinter; - const SideBySidePrinter = require("./side-by-side-printer.js").SideBySidePrinter; - const FileListPrinter = require("./file-list-printer.js").FileListPrinter; - - function HtmlPrinter() {} - - HtmlPrinter.prototype.generateLineByLineJsonHtml = function(diffFiles, config) { - const lineByLinePrinter = new LineByLinePrinter(config); - return lineByLinePrinter.generateLineByLineJsonHtml(diffFiles); - }; - - HtmlPrinter.prototype.generateSideBySideJsonHtml = function(diffFiles, config) { - const sideBySidePrinter = new SideBySidePrinter(config); - return sideBySidePrinter.generateSideBySideJsonHtml(diffFiles); - }; - - HtmlPrinter.prototype.generateFileListSummary = function(diffJson, config) { - const fileListPrinter = new FileListPrinter(config); - return fileListPrinter.generateFileList(diffJson); - }; - - module.exports.HtmlPrinter = new HtmlPrinter(); -})(); diff --git a/src/line-by-line-printer.js b/src/line-by-line-printer.js deleted file mode 100644 index 8a0f5ac..0000000 --- a/src/line-by-line-printer.js +++ /dev/null @@ -1,256 +0,0 @@ -/* - * - * LineByLinePrinter (line-by-line-printer.js) - * Author: rtfpessoa - * - */ - -(function() { - const diffParser = require("./diff-parser.js").DiffParser; - const printerUtils = require("./printer-utils.js").PrinterUtils; - const utils = require("./utils.js").Utils; - const Rematch = require("./rematch.js").Rematch; - - let hoganUtils; - - const genericTemplatesPath = "generic"; - const baseTemplatesPath = "line-by-line"; - const iconsBaseTemplatesPath = "icon"; - const tagsBaseTemplatesPath = "tag"; - - function LineByLinePrinter(config) { - this.config = config; - - const HoganJsUtils = require("./hoganjs-utils.js").HoganJsUtils; - hoganUtils = new HoganJsUtils(config); - } - - LineByLinePrinter.prototype.makeFileDiffHtml = function(file, diffs) { - if (this.config.renderNothingWhenEmpty && file.blocks && !file.blocks.length) return ""; - - const fileDiffTemplate = hoganUtils.template(baseTemplatesPath, "file-diff"); - const filePathTemplate = hoganUtils.template(genericTemplatesPath, "file-path"); - const fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, "file"); - const fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file)); - - return fileDiffTemplate.render({ - file: file, - fileHtmlId: printerUtils.getHtmlId(file), - diffs: diffs, - filePath: filePathTemplate.render( - { - fileDiffName: printerUtils.getDiffName(file) - }, - { - fileIcon: fileIconTemplate, - fileTag: fileTagTemplate - } - ) - }); - }; - - LineByLinePrinter.prototype.makeLineByLineHtmlWrapper = function(content) { - return hoganUtils.render(genericTemplatesPath, "wrapper", { content: content }); - }; - - LineByLinePrinter.prototype.generateLineByLineJsonHtml = function(diffFiles) { - const that = this; - const htmlDiffs = diffFiles.map(function(file) { - let diffs; - if (file.blocks.length) { - diffs = that._generateFileHtml(file); - } else { - diffs = that._generateEmptyDiff(); - } - return that.makeFileDiffHtml(file, diffs); - }); - - return this.makeLineByLineHtmlWrapper(htmlDiffs.join("\n")); - }; - - const matcher = Rematch.rematch(function(a, b) { - const amod = a.content.substr(1); - const bmod = b.content.substr(1); - - return Rematch.distance(amod, bmod); - }); - - LineByLinePrinter.prototype.makeColumnLineNumberHtml = function(block) { - return hoganUtils.render(genericTemplatesPath, "column-line-number", { - diffParser: diffParser, - blockHeader: utils.escape(block.header), - lineClass: "d2h-code-linenumber", - contentClass: "d2h-code-line" - }); - }; - - LineByLinePrinter.prototype._generateFileHtml = function(file) { - const that = this; - return file.blocks - .map(function(block) { - let lines = that.makeColumnLineNumberHtml(block); - let oldLines = []; - let newLines = []; - - function processChangeBlock() { - let matches; - let insertType; - let deleteType; - - const comparisons = oldLines.length * newLines.length; - - const maxLineSizeInBlock = Math.max.apply( - null, - [0].concat( - oldLines.concat(newLines).map(function(elem) { - return elem.content.length; - }) - ) - ); - - const doMatching = - comparisons < that.config.matchingMaxComparisons && - maxLineSizeInBlock < that.config.maxLineSizeInBlockForComparison && - (that.config.matching === "lines" || that.config.matching === "words"); - - if (doMatching) { - matches = matcher(oldLines, newLines); - insertType = diffParser.LINE_TYPE.INSERT_CHANGES; - deleteType = diffParser.LINE_TYPE.DELETE_CHANGES; - } else { - matches = [[oldLines, newLines]]; - insertType = diffParser.LINE_TYPE.INSERTS; - deleteType = diffParser.LINE_TYPE.DELETES; - } - - matches.forEach(function(match) { - oldLines = match[0]; - newLines = match[1]; - - let processedOldLines = []; - let processedNewLines = []; - - const common = Math.min(oldLines.length, newLines.length); - - let oldLine, newLine; - for (let j = 0; j < common; j++) { - oldLine = oldLines[j]; - newLine = newLines[j]; - - that.config.isCombined = file.isCombined; - const diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config); - - processedOldLines += that.makeLineHtml( - file.isCombined, - deleteType, - oldLine.oldNumber, - oldLine.newNumber, - diff.first.line, - diff.first.prefix - ); - processedNewLines += that.makeLineHtml( - file.isCombined, - insertType, - newLine.oldNumber, - newLine.newNumber, - diff.second.line, - diff.second.prefix - ); - } - - lines += processedOldLines + processedNewLines; - lines += that._processLines(file.isCombined, oldLines.slice(common), newLines.slice(common)); - }); - - oldLines = []; - newLines = []; - } - - for (let i = 0; i < block.lines.length; i++) { - const line = block.lines[i]; - const escapedLine = utils.escape(line.content); - - if ( - line.type !== diffParser.LINE_TYPE.INSERTS && - (newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0)) - ) { - processChangeBlock(); - } - - if (line.type === diffParser.LINE_TYPE.CONTEXT) { - lines += that.makeLineHtml(file.isCombined, line.type, line.oldNumber, line.newNumber, escapedLine); - } else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) { - lines += that.makeLineHtml(file.isCombined, line.type, line.oldNumber, line.newNumber, escapedLine); - } else if (line.type === diffParser.LINE_TYPE.DELETES) { - oldLines.push(line); - } else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) { - newLines.push(line); - } else { - console.error("Unknown state in html line-by-line generator"); - processChangeBlock(); - } - } - - processChangeBlock(); - - return lines; - }) - .join("\n"); - }; - - LineByLinePrinter.prototype._processLines = function(isCombined, oldLines, newLines) { - let lines = ""; - - for (let i = 0; i < oldLines.length; i++) { - const oldLine = oldLines[i]; - const oldEscapedLine = utils.escape(oldLine.content); - lines += this.makeLineHtml(isCombined, oldLine.type, oldLine.oldNumber, oldLine.newNumber, oldEscapedLine); - } - - for (let j = 0; j < newLines.length; j++) { - const newLine = newLines[j]; - const newEscapedLine = utils.escape(newLine.content); - lines += this.makeLineHtml(isCombined, newLine.type, newLine.oldNumber, newLine.newNumber, newEscapedLine); - } - - return lines; - }; - - LineByLinePrinter.prototype.makeLineHtml = function(isCombined, type, oldNumber, newNumber, content, possiblePrefix) { - const lineNumberTemplate = hoganUtils.render(baseTemplatesPath, "numbers", { - oldNumber: utils.valueOrEmpty(oldNumber), - newNumber: utils.valueOrEmpty(newNumber) - }); - - let lineWithoutPrefix = content; - let prefix = possiblePrefix; - - if (!prefix) { - const lineWithPrefix = printerUtils.separatePrefix(isCombined, content); - prefix = lineWithPrefix.prefix; - lineWithoutPrefix = lineWithPrefix.line; - } - - if (prefix === " ") { - prefix = " "; - } - - return hoganUtils.render(genericTemplatesPath, "line", { - type: type, - lineClass: "d2h-code-linenumber", - contentClass: "d2h-code-line", - prefix: prefix, - content: lineWithoutPrefix, - lineNumber: lineNumberTemplate - }); - }; - - LineByLinePrinter.prototype._generateEmptyDiff = function() { - return hoganUtils.render(genericTemplatesPath, "empty-diff", { - contentClass: "d2h-code-line", - diffParser: diffParser - }); - }; - - module.exports.LineByLinePrinter = LineByLinePrinter; -})(); diff --git a/src/line-by-line-renderer.ts b/src/line-by-line-renderer.ts new file mode 100644 index 0000000..339322d --- /dev/null +++ b/src/line-by-line-renderer.ts @@ -0,0 +1,290 @@ +import * as utils from "./utils"; +import HoganJsUtils from "./hoganjs-utils"; +import * as Rematch from "./rematch"; +import * as renderUtils from "./render-utils"; + +export interface LineByLineRendererConfig extends renderUtils.RenderConfig { + renderNothingWhenEmpty?: boolean; + matchingMaxComparisons?: number; + maxLineSizeInBlockForComparison?: number; +} + +export const defaultLineByLineRendererConfig = { + renderNothingWhenEmpty: false, + matchingMaxComparisons: 2500, + maxLineSizeInBlockForComparison: 200, + ...renderUtils.defaultRenderConfig +}; + +const genericTemplatesPath = "generic"; +const baseTemplatesPath = "line-by-line"; +const iconsBaseTemplatesPath = "icon"; +const tagsBaseTemplatesPath = "tag"; + +export default class LineByLineRenderer { + private readonly hoganUtils: HoganJsUtils; + private readonly config: typeof defaultLineByLineRendererConfig; + + constructor(hoganUtils: HoganJsUtils, config: LineByLineRendererConfig) { + this.hoganUtils = hoganUtils; + this.config = { ...defaultLineByLineRendererConfig, ...config }; + } + + render(diffFiles: renderUtils.DiffFile[]): string | undefined { + const htmlDiffs = diffFiles.map(file => { + let diffs; + if (file.blocks.length) { + diffs = this.generateFileHtml(file); + } else { + diffs = this.generateEmptyDiff(); + } + return this.makeFileDiffHtml(file, diffs); + }); + + return this.makeLineByLineHtmlWrapper(htmlDiffs.join("\n")); + } + + // TODO: Make this private after improving tests + makeFileDiffHtml(file: renderUtils.DiffFile, diffs: string): string { + if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return ""; + + const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, "file-diff"); + const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, "file-path"); + const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, "file"); + const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file)); + + return fileDiffTemplate.render({ + file: file, + fileHtmlId: renderUtils.getHtmlId(file), + diffs: diffs, + filePath: filePathTemplate.render( + { + fileDiffName: renderUtils.filenameDiff(file) + }, + { + fileIcon: fileIconTemplate, + fileTag: fileTagTemplate + } + ) + }); + } + + // TODO: Make this private after improving tests + makeLineByLineHtmlWrapper(content: string): string { + return this.hoganUtils.render(genericTemplatesPath, "wrapper", { content: content }); + } + + // TODO: Make this private after improving tests + makeColumnLineNumberHtml(block: renderUtils.DiffBlock): string { + return this.hoganUtils.render(genericTemplatesPath, "column-line-number", { + CSSLineClass: renderUtils.CSSLineClass, + blockHeader: utils.escapeForHtml(block.header), + lineClass: "d2h-code-linenumber", + contentClass: "d2h-code-line" + }); + } + + // TODO: Make this private after improving tests + makeLineHtml( + isCombined: boolean, + type: renderUtils.CSSLineClass, + content: string, + oldNumber?: number, + newNumber?: number, + possiblePrefix?: string + ): string { + const lineNumberTemplate = this.hoganUtils.render(baseTemplatesPath, "numbers", { + oldNumber: oldNumber || "", + newNumber: newNumber || "" + }); + + let lineWithoutPrefix = content; + let prefix = possiblePrefix; + + if (!prefix) { + const lineWithPrefix = renderUtils.deconstructLine(content, isCombined); + prefix = lineWithPrefix.prefix; + lineWithoutPrefix = lineWithPrefix.line; + } + + if (prefix === " ") { + prefix = " "; + } + + return this.hoganUtils.render(genericTemplatesPath, "line", { + type: type, + lineClass: "d2h-code-linenumber", + contentClass: "d2h-code-line", + prefix: prefix, + content: lineWithoutPrefix, + lineNumber: lineNumberTemplate + }); + } + + // TODO: Make this private after improving tests + generateEmptyDiff(): string { + return this.hoganUtils.render(genericTemplatesPath, "empty-diff", { + contentClass: "d2h-code-line", + CSSLineClass: renderUtils.CSSLineClass + }); + } + + // TODO: Make this private after improving tests + processLines(isCombined: boolean, oldLines: renderUtils.DiffLine[], newLines: renderUtils.DiffLine[]): string { + let lines = ""; + + for (let i = 0; i < oldLines.length; i++) { + const oldLine = oldLines[i]; + const oldEscapedLine = utils.escapeForHtml(oldLine.content); + lines += this.makeLineHtml( + isCombined, + renderUtils.toCSSClass(oldLine.type), + oldEscapedLine, + oldLine.oldNumber, + oldLine.newNumber + ); + } + + for (let j = 0; j < newLines.length; j++) { + const newLine = newLines[j]; + const newEscapedLine = utils.escapeForHtml(newLine.content); + lines += this.makeLineHtml( + isCombined, + renderUtils.toCSSClass(newLine.type), + newEscapedLine, + newLine.oldNumber, + newLine.newNumber + ); + } + + return lines; + } + + // TODO: Make this private after improving tests + generateFileHtml(file: renderUtils.DiffFile): string { + const prefixSize = renderUtils.prefixLength(file.isCombined); + const distance = Rematch.newDistanceFn((e: renderUtils.DiffLine) => e.content.substring(prefixSize)); + const matcher = Rematch.newMatcherFn(distance); + + return file.blocks + .map(block => { + let lines = this.makeColumnLineNumberHtml(block); + let oldLines: renderUtils.DiffLine[] = []; + let newLines: renderUtils.DiffLine[] = []; + + const processChangeBlock = (): void => { + let matches; + let insertType: renderUtils.CSSLineClass; + let deleteType: renderUtils.CSSLineClass; + + const comparisons = oldLines.length * newLines.length; + + const maxLineSizeInBlock = Math.max.apply( + null, + [0].concat(oldLines.concat(newLines).map(elem => elem.content.length)) + ); + + const doMatching = + comparisons < this.config.matchingMaxComparisons && + maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison && + (this.config.matching === "lines" || this.config.matching === "words"); + + if (doMatching) { + matches = matcher(oldLines, newLines); + insertType = renderUtils.CSSLineClass.INSERT_CHANGES; + deleteType = renderUtils.CSSLineClass.DELETE_CHANGES; + } else { + matches = [[oldLines, newLines]]; + insertType = renderUtils.CSSLineClass.INSERTS; + deleteType = renderUtils.CSSLineClass.DELETES; + } + + matches.forEach(match => { + oldLines = match[0]; + newLines = match[1]; + + let processedOldLines = ""; + let processedNewLines = ""; + + const common = Math.min(oldLines.length, newLines.length); + + let oldLine, newLine; + for (let j = 0; j < common; j++) { + oldLine = oldLines[j]; + newLine = newLines[j]; + + const diff = renderUtils.diffHighlight(oldLine.content, newLine.content, file.isCombined, this.config); + + processedOldLines += this.makeLineHtml( + file.isCombined, + deleteType, + diff.oldLine.content, + oldLine.oldNumber, + oldLine.newNumber, + diff.oldLine.prefix + ); + processedNewLines += this.makeLineHtml( + file.isCombined, + insertType, + diff.newLine.content, + newLine.oldNumber, + newLine.newNumber, + diff.newLine.prefix + ); + } + + lines += processedOldLines + processedNewLines; + lines += this.processLines(file.isCombined, oldLines.slice(common), newLines.slice(common)); + }); + + oldLines = []; + newLines = []; + }; + + for (let i = 0; i < block.lines.length; i++) { + const diffLine = block.lines[i]; + const { prefix, line } = renderUtils.deconstructLine(diffLine.content, file.isCombined); + const escapedLine = utils.escapeForHtml(line); + + if ( + diffLine.type !== renderUtils.LineType.INSERT && + (newLines.length > 0 || (diffLine.type !== renderUtils.LineType.DELETE && oldLines.length > 0)) + ) { + processChangeBlock(); + } + + if (diffLine.type === renderUtils.LineType.CONTEXT) { + lines += this.makeLineHtml( + file.isCombined, + renderUtils.toCSSClass(diffLine.type), + escapedLine, + diffLine.oldNumber, + diffLine.newNumber, + prefix + ); + } else if (diffLine.type === renderUtils.LineType.INSERT && !oldLines.length) { + lines += this.makeLineHtml( + file.isCombined, + renderUtils.toCSSClass(diffLine.type), + escapedLine, + diffLine.oldNumber, + diffLine.newNumber, + prefix + ); + } else if (diffLine.type === renderUtils.LineType.DELETE) { + oldLines.push(diffLine); + } else if (diffLine.type === renderUtils.LineType.INSERT && Boolean(oldLines.length)) { + newLines.push(diffLine); + } else { + console.error("Unknown state in html line-by-line generator"); + processChangeBlock(); + } + } + + processChangeBlock(); + + return lines; + }) + .join("\n"); + } +} diff --git a/src/printer-utils.js b/src/printer-utils.js deleted file mode 100644 index 9e2ee49..0000000 --- a/src/printer-utils.js +++ /dev/null @@ -1,264 +0,0 @@ -/* - * - * PrinterUtils (printer-utils.js) - * Author: rtfpessoa - * - */ - -(function() { - const jsDiff = require("diff"); - const utils = require("./utils.js").Utils; - const Rematch = require("./rematch.js").Rematch; - - const separator = "/"; - - function PrinterUtils() {} - - PrinterUtils.prototype.separatePrefix = function(isCombined, line) { - let prefix; - let lineWithoutPrefix; - - if (isCombined) { - prefix = line.substring(0, 2); - lineWithoutPrefix = line.substring(2); - } else { - prefix = line.substring(0, 1); - lineWithoutPrefix = line.substring(1); - } - - return { - prefix: prefix, - line: lineWithoutPrefix - }; - }; - - PrinterUtils.prototype.getHtmlId = function(file) { - const hashCode = function(text) { - let i, chr, len; - let hash = 0; - - for (i = 0, len = text.length; i < len; i++) { - chr = text.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; // Convert to 32bit integer - } - - return hash; - }; - - return ( - "d2h-" + - hashCode(this.getDiffName(file)) - .toString() - .slice(-6) - ); - }; - - PrinterUtils.prototype.getDiffName = function(file) { - const oldFilename = unifyPath(file.oldName); - const newFilename = unifyPath(file.newName); - - if ( - oldFilename && - newFilename && - oldFilename !== newFilename && - !isDevNullName(oldFilename) && - !isDevNullName(newFilename) - ) { - const prefixPaths = []; - const suffixPaths = []; - - const oldFilenameParts = oldFilename.split(separator); - const newFilenameParts = newFilename.split(separator); - - const oldFilenamePartsSize = oldFilenameParts.length; - const newFilenamePartsSize = newFilenameParts.length; - - let i = 0; - let j = oldFilenamePartsSize - 1; - let k = newFilenamePartsSize - 1; - - while (i < j && i < k) { - if (oldFilenameParts[i] === newFilenameParts[i]) { - prefixPaths.push(newFilenameParts[i]); - i += 1; - } else { - break; - } - } - - while (j > i && k > i) { - if (oldFilenameParts[j] === newFilenameParts[k]) { - suffixPaths.unshift(newFilenameParts[k]); - j -= 1; - k -= 1; - } else { - break; - } - } - - const finalPrefix = prefixPaths.join(separator); - const finalSuffix = suffixPaths.join(separator); - - const oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator); - const newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator); - - if (finalPrefix.length && finalSuffix.length) { - return ( - finalPrefix + separator + "{" + oldRemainingPath + " → " + newRemainingPath + "}" + separator + finalSuffix - ); - } else if (finalPrefix.length) { - return finalPrefix + separator + "{" + oldRemainingPath + " → " + newRemainingPath + "}"; - } else if (finalSuffix.length) { - return "{" + oldRemainingPath + " → " + newRemainingPath + "}" + separator + finalSuffix; - } - - return oldFilename + " → " + newFilename; - } else if (newFilename && !isDevNullName(newFilename)) { - return newFilename; - } else if (oldFilename) { - return oldFilename; - } - - return "unknown/file/path"; - }; - - PrinterUtils.prototype.getFileTypeIcon = function(file) { - let templateName = "file-changed"; - - if (file.isRename) { - templateName = "file-renamed"; - } else if (file.isCopy) { - templateName = "file-renamed"; - } else if (file.isNew) { - templateName = "file-added"; - } else if (file.isDeleted) { - templateName = "file-deleted"; - } else if (file.newName !== file.oldName) { - // If file is not Added, not Deleted and the names changed it must be a rename :) - templateName = "file-renamed"; - } - - return templateName; - }; - - PrinterUtils.prototype.diffHighlight = function(diffLine1, diffLine2, config) { - let linePrefix1, linePrefix2, unprefixedLine1, unprefixedLine2; - - let prefixSize = 1; - - if (config.isCombined) { - prefixSize = 2; - } - - linePrefix1 = diffLine1.substr(0, prefixSize); - linePrefix2 = diffLine2.substr(0, prefixSize); - unprefixedLine1 = diffLine1.substr(prefixSize); - unprefixedLine2 = diffLine2.substr(prefixSize); - - if ( - unprefixedLine1.length > config.maxLineLengthHighlight || - unprefixedLine2.length > config.maxLineLengthHighlight - ) { - return { - first: { - prefix: linePrefix1, - line: utils.escape(unprefixedLine1) - }, - second: { - prefix: linePrefix2, - line: utils.escape(unprefixedLine2) - } - }; - } - - let diff; - if (config.diffStyle === "char") { - diff = jsDiff.diffChars(unprefixedLine1, unprefixedLine2); - } else { - diff = jsDiff.diffWordsWithSpace(unprefixedLine1, unprefixedLine2); - } - - let highlightedLine = ""; - - const changedWords = []; - if (config.diffStyle === "word" && config.matching === "words") { - let treshold = 0.25; - - if (typeof config.matchWordsThreshold !== "undefined") { - treshold = config.matchWordsThreshold; - } - - const matcher = Rematch.rematch(function(a, b) { - const amod = a.value; - const bmod = b.value; - - return Rematch.distance(amod, bmod); - }); - - const removed = diff.filter(function isRemoved(element) { - return element.removed; - }); - - const added = diff.filter(function isAdded(element) { - return element.added; - }); - - const chunks = matcher(added, removed); - chunks.forEach(function(chunk) { - if (chunk[0].length === 1 && chunk[1].length === 1) { - const dist = Rematch.distance(chunk[0][0].value, chunk[1][0].value); - if (dist < treshold) { - changedWords.push(chunk[0][0]); - changedWords.push(chunk[1][0]); - } - } - }); - } - - diff.forEach(function(part) { - const addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : ""; - const elemType = part.added ? "ins" : part.removed ? "del" : null; - const escapedValue = utils.escape(part.value); - - if (elemType !== null) { - highlightedLine += "<" + elemType + addClass + ">" + escapedValue + ""; - } else { - highlightedLine += escapedValue; - } - }); - - return { - first: { - prefix: linePrefix1, - line: removeIns(highlightedLine) - }, - second: { - prefix: linePrefix2, - line: removeDel(highlightedLine) - } - }; - }; - - function unifyPath(path) { - if (path) { - return path.replace("\\", "/"); - } - - return path; - } - - function isDevNullName(name) { - return name.indexOf("dev/null") !== -1; - } - - function removeIns(line) { - return line.replace(/(]*>((.|\n)*?)<\/ins>)/g, ""); - } - - function removeDel(line) { - return line.replace(/(]*>((.|\n)*?)<\/del>)/g, ""); - } - - module.exports.PrinterUtils = new PrinterUtils(); -})(); diff --git a/src/rematch.js b/src/rematch.js deleted file mode 100644 index b8b26a5..0000000 --- a/src/rematch.js +++ /dev/null @@ -1,145 +0,0 @@ -/* - * - * Rematch (rematch.js) - * Matching two sequences of objects by similarity - * Author: W. Illmeyer, Nexxar GmbH - * - */ - -(function() { - const Rematch = {}; - - /* - Copyright (c) 2011 Andrei Mackenzie - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated - documentation files (the "Software"), to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, - and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO - THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - function levenshtein(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.charAt(i - 1) === a.charAt(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]; - } - - Rematch.levenshtein = levenshtein; - - Rematch.distance = function distance(x, y) { - x = x.trim(); - y = y.trim(); - const lev = levenshtein(x, y); - const score = lev / (x.length + y.length); - - return score; - }; - - Rematch.rematch = function rematch(distanceFunction) { - function findBestMatch(a, b, cache) { - let bestMatchDist = Infinity; - let bestMatch; - for (let i = 0; i < a.length; ++i) { - for (let j = 0; j < b.length; ++j) { - const cacheKey = JSON.stringify([a[i], b[j]]); - var md; - if (cache.hasOwnProperty(cacheKey)) { - md = cache[cacheKey]; - } else { - md = distanceFunction(a[i], b[j]); - cache[cacheKey] = md; - } - if (md < bestMatchDist) { - bestMatchDist = md; - bestMatch = { indexA: i, indexB: j, score: bestMatchDist }; - } - } - } - - return bestMatch; - } - - function group(a, b, level, cache) { - if (typeof cache === "undefined") { - cache = {}; - } - - const bm = findBestMatch(a, b, cache); - - if (!level) { - level = 0; - } - - if (!bm || a.length + b.length < 3) { - return [[a, b]]; - } - - const a1 = a.slice(0, bm.indexA); - const b1 = b.slice(0, bm.indexB); - const aMatch = [a[bm.indexA]]; - const bMatch = [b[bm.indexB]]; - const tailA = bm.indexA + 1; - const tailB = bm.indexB + 1; - const a2 = a.slice(tailA); - const b2 = b.slice(tailB); - - const group1 = group(a1, b1, level + 1, cache); - const groupMatch = group(aMatch, bMatch, level + 1, cache); - const group2 = group(a2, b2, level + 1, cache); - let result = groupMatch; - - if (bm.indexA > 0 || bm.indexB > 0) { - result = group1.concat(result); - } - - if (a.length > tailA || b.length > tailB) { - result = result.concat(group2); - } - - return result; - } - - return group; - }; - - module.exports.Rematch = Rematch; -})(); diff --git a/src/rematch.ts b/src/rematch.ts new file mode 100644 index 0000000..5e7ea3b --- /dev/null +++ b/src/rematch.ts @@ -0,0 +1,137 @@ +/* + * Matching two sequences of objects by similarity + * Author: W. Illmeyer, Nexxar GmbH + */ + +export type BestMatch = { + indexA: number; + indexB: number; + score: number; +}; + +/* + Copyright (c) 2011 Andrei Mackenzie + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + documentation files (the "Software"), to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +export function levenshtein(a: string, b: string): number { + 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.charAt(i - 1) === a.charAt(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]; +} + +export type DistanceFn = (x: T, y: T) => number; + +export function newDistanceFn(str: (value: T) => string): DistanceFn { + return (x: T, y: T): number => { + const xValue = str(x).trim(); + const yValue = str(y).trim(); + const lev = levenshtein(xValue, yValue); + const score = lev / (xValue.length + yValue.length); + + return score; + }; +} + +export type MatcherFn = (a: T[], b: T[], level?: number, cache?: Map) => T[][][]; + +export function newMatcherFn(distance: (x: T, y: T) => number): MatcherFn { + function findBestMatch(a: T[], b: T[], cache: Map = new Map()): BestMatch | undefined { + let bestMatchDist = Infinity; + let bestMatch; + + for (let i = 0; i < a.length; ++i) { + for (let j = 0; j < b.length; ++j) { + const cacheKey = JSON.stringify([a[i], b[j]]); + let md; + if (!(cache.has(cacheKey) && (md = cache.get(cacheKey)))) { + md = distance(a[i], b[j]); + cache.set(cacheKey, md); + } + if (md < bestMatchDist) { + bestMatchDist = md; + bestMatch = { indexA: i, indexB: j, score: bestMatchDist }; + } + } + } + + return bestMatch; + } + + function group(a: T[], b: T[], level = 0, cache: Map = new Map()): T[][][] { + const bm = findBestMatch(a, b, cache); + + if (!bm || a.length + b.length < 3) { + return [[a, b]]; + } + + const a1 = a.slice(0, bm.indexA); + const b1 = b.slice(0, bm.indexB); + const aMatch = [a[bm.indexA]]; + const bMatch = [b[bm.indexB]]; + const tailA = bm.indexA + 1; + const tailB = bm.indexB + 1; + const a2 = a.slice(tailA); + const b2 = b.slice(tailB); + + const group1 = group(a1, b1, level + 1, cache); + const groupMatch = group(aMatch, bMatch, level + 1, cache); + const group2 = group(a2, b2, level + 1, cache); + let result = groupMatch; + + if (bm.indexA > 0 || bm.indexB > 0) { + result = group1.concat(result); + } + + if (a.length > tailA || b.length > tailB) { + result = result.concat(group2); + } + + return result; + } + + return group; +} diff --git a/src/render-utils.ts b/src/render-utils.ts new file mode 100644 index 0000000..b2219bc --- /dev/null +++ b/src/render-utils.ts @@ -0,0 +1,343 @@ +import * as jsDiff from "diff"; + +import { unifyPath, escapeForHtml, hashCode } from "./utils"; +import * as rematch from "./rematch"; + +export type DiffLineParts = { + prefix: string; + line: string; +}; + +export enum CSSLineClass { + INSERTS = "d2h-ins", + DELETES = "d2h-del", + CONTEXT = "d2h-cntx", + INFO = "d2h-info", + INSERT_CHANGES = "d2h-ins d2h-change", + DELETE_CHANGES = "d2h-del d2h-change" +} + +export enum LineType { + INSERT = "insert", + DELETE = "delete", + CONTEXT = "context" +} + +interface DiffLineDeleted { + type: LineType.DELETE; + oldNumber: number; + newNumber: undefined; +} + +interface DiffLineInserted { + type: LineType.INSERT; + oldNumber: undefined; + newNumber: number; +} + +interface DiffLineContext { + type: LineType.CONTEXT; + oldNumber: number; + newNumber: number; +} + +export type DiffLine = (DiffLineDeleted | DiffLineInserted | DiffLineContext) & { + content: string; +}; + +export interface DiffBlock { + oldStartLine: number; + oldStartLine2?: number; + newStartLine: number; + header: string; + lines: DiffLine[]; +} + +interface DiffFileName { + oldName: string; + newName: string; +} + +export interface DiffFile extends DiffFileName { + addedLines: number; + deletedLines: number; + isCombined: boolean; + isGitDiff: boolean; + language: string; + blocks: DiffBlock[]; + oldMode?: string | string[]; + newMode?: string; + deletedFileMode?: string; + newFileMode?: string; + isDeleted?: boolean; + isNew?: boolean; + isCopy?: boolean; + isRename?: boolean; + isBinary?: boolean; + unchangedPercentage?: number; + changedPercentage?: number; + checksumBefore?: string | string[]; + checksumAfter?: string; + mode?: string; +} + +export type LineMatchingType = "lines" | "words" | "none"; +export type DiffStyleType = "word" | "char"; + +export interface RenderConfig { + matching?: LineMatchingType; + matchWordsThreshold?: number; + maxLineLengthHighlight?: number; + diffStyle?: DiffStyleType; +} + +export const defaultRenderConfig = { + matching: "none" as LineMatchingType, + matchWordsThreshold: 0.25, + maxLineLengthHighlight: 10000, + diffStyle: "word" as DiffStyleType +}; + +type HighlightedLines = { + oldLine: { + prefix: string; + content: string; + }; + newLine: { + prefix: string; + content: string; + }; +}; + +const separator = "/"; +const distance = rematch.newDistanceFn((change: jsDiff.Change) => change.value); +const matcher = rematch.newMatcherFn(distance); + +function isDevNullName(name: string): boolean { + return name.indexOf("dev/null") !== -1; +} + +function removeInsElements(line: string): string { + return line.replace(/(]*>((.|\n)*?)<\/ins>)/g, ""); +} + +function removeDelElements(line: string): string { + return line.replace(/(]*>((.|\n)*?)<\/del>)/g, ""); +} + +/** + * Convert from LineType to CSSLineClass + */ +export function toCSSClass(lineType: LineType): CSSLineClass { + switch (lineType) { + case LineType.CONTEXT: + return CSSLineClass.CONTEXT; + case LineType.INSERT: + return CSSLineClass.INSERTS; + case LineType.DELETE: + return CSSLineClass.DELETES; + } +} + +/** + * Prefix length of the hunk lines in the diff + */ +export function prefixLength(isCombined: boolean): number { + return isCombined ? 2 : 1; +} + +/** + * Deconstructs diff @line by separating the content from the prefix type + */ +export function deconstructLine(line: string, isCombined: boolean): DiffLineParts { + const indexToSplit = prefixLength(isCombined); + return { + prefix: line.substring(0, indexToSplit), + line: line.substring(indexToSplit) + }; +} + +/** + * Generates pretty filename diffs + * + * e.g.: + * 1. file = { oldName: "my/path/to/file.js", newName: "my/path/to/new-file.js" } + * returns "my/path/to/{file.js → new-file.js}" + * 2. file = { oldName: "my/path/to/file.js", newName: "very/new/path/to/new-file.js" } + * returns "my/path/to/file.js → very/new/path/to/new-file.js" + * 3. file = { oldName: "my/path/to/file.js", newName: "my/path/for/file.js" } + * returns "my/path/{to → for}/file.js" + */ +export function filenameDiff(file: DiffFileName): string { + // TODO: Review this huuuuuge piece of code, do we need this? + // TODO: Move unify path to parsing + const oldFilename = unifyPath(file.oldName); + const newFilename = unifyPath(file.newName); + + if (oldFilename !== newFilename && !isDevNullName(oldFilename) && !isDevNullName(newFilename)) { + const prefixPaths = []; + const suffixPaths = []; + + const oldFilenameParts = oldFilename.split(separator); + const newFilenameParts = newFilename.split(separator); + + const oldFilenamePartsSize = oldFilenameParts.length; + const newFilenamePartsSize = newFilenameParts.length; + + let i = 0; + let j = oldFilenamePartsSize - 1; + let k = newFilenamePartsSize - 1; + + while (i < j && i < k) { + if (oldFilenameParts[i] === newFilenameParts[i]) { + prefixPaths.push(newFilenameParts[i]); + i += 1; + } else { + break; + } + } + + while (j > i && k > i) { + if (oldFilenameParts[j] === newFilenameParts[k]) { + suffixPaths.unshift(newFilenameParts[k]); + j -= 1; + k -= 1; + } else { + break; + } + } + + const finalPrefix = prefixPaths.join(separator); + const finalSuffix = suffixPaths.join(separator); + + const oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator); + const newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator); + + if (finalPrefix.length && finalSuffix.length) { + return ( + finalPrefix + separator + "{" + oldRemainingPath + " → " + newRemainingPath + "}" + separator + finalSuffix + ); + } else if (finalPrefix.length) { + return finalPrefix + separator + "{" + oldRemainingPath + " → " + newRemainingPath + "}"; + } else if (finalSuffix.length) { + return "{" + oldRemainingPath + " → " + newRemainingPath + "}" + separator + finalSuffix; + } + + return oldFilename + " → " + newFilename; + } else if (!isDevNullName(newFilename)) { + return newFilename; + } else { + return oldFilename; + } +} + +/** + * Generates a unique string numerical identifier based on the names of the file diff + */ +export function getHtmlId(file: DiffFileName): string { + return `d2h-${hashCode(filenameDiff(file)) + .toString() + .slice(-6)}`; +} + +/** + * Selects the correct icon name for the file + */ +export function getFileIcon(file: DiffFile): string { + let templateName = "file-changed"; + + if (file.isRename) { + templateName = "file-renamed"; + } else if (file.isCopy) { + templateName = "file-renamed"; + } else if (file.isNew) { + templateName = "file-added"; + } else if (file.isDeleted) { + templateName = "file-deleted"; + } else if (file.newName !== file.oldName) { + // If file is not Added, not Deleted and the names changed it must be a rename :) + templateName = "file-renamed"; + } + + return templateName; +} + +/** + * Generates a unique string numerical identifier based on the names of the file diff + */ +export function diffHighlight( + diffLine1: string, + diffLine2: string, + isCombined: boolean, + config: RenderConfig +): HighlightedLines { + const { matching, maxLineLengthHighlight, matchWordsThreshold, diffStyle } = { ...defaultRenderConfig, ...config }; + const prefixLengthVal = prefixLength(isCombined); + + const linePrefix1 = diffLine1.substr(0, prefixLengthVal); + const unprefixedLine1 = diffLine1.substr(prefixLengthVal); + + const linePrefix2 = diffLine2.substr(0, prefixLengthVal); + const unprefixedLine2 = diffLine2.substr(prefixLengthVal); + + if (unprefixedLine1.length > maxLineLengthHighlight || unprefixedLine2.length > maxLineLengthHighlight) { + return { + oldLine: { + prefix: linePrefix1, + content: escapeForHtml(unprefixedLine1) + }, + newLine: { + prefix: linePrefix2, + content: escapeForHtml(unprefixedLine2) + } + }; + } + + const diff = + diffStyle === "char" + ? jsDiff.diffChars(unprefixedLine1, unprefixedLine2) + : jsDiff.diffWordsWithSpace(unprefixedLine1, unprefixedLine2); + + const changedWords: jsDiff.Change[] = []; + if (diffStyle === "word" && matching === "words") { + let treshold = 0.25; + + if (typeof matchWordsThreshold !== "undefined") { + treshold = matchWordsThreshold; + } + + const removed = diff.filter(element => element.removed); + const added = diff.filter(element => element.added); + const chunks = matcher(added, removed); + chunks.forEach(chunk => { + if (chunk[0].length === 1 && chunk[1].length === 1) { + const dist = distance(chunk[0][0], chunk[1][0]); + if (dist < treshold) { + changedWords.push(chunk[0][0]); + changedWords.push(chunk[1][0]); + } + } + }); + } + + const highlightedLine = diff.reduce((highlightedLine, part) => { + const elemType = part.added ? "ins" : part.removed ? "del" : null; + const addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : ""; + const escapedValue = escapeForHtml(part.value); + + return elemType !== null + ? `${highlightedLine}<${elemType}${addClass}>${escapedValue}` + : `${highlightedLine}${escapedValue}`; + }, ""); + + return { + oldLine: { + prefix: linePrefix1, + content: removeInsElements(highlightedLine) + }, + newLine: { + prefix: linePrefix2, + content: removeDelElements(highlightedLine) + } + }; +} diff --git a/src/side-by-side-printer.js b/src/side-by-side-printer.js deleted file mode 100644 index 7655081..0000000 --- a/src/side-by-side-printer.js +++ /dev/null @@ -1,329 +0,0 @@ -/* - * - * HtmlPrinter (html-printer.js) - * Author: rtfpessoa - * - */ - -(function() { - const diffParser = require("./diff-parser.js").DiffParser; - const printerUtils = require("./printer-utils.js").PrinterUtils; - const utils = require("./utils.js").Utils; - const Rematch = require("./rematch.js").Rematch; - - let hoganUtils; - - const genericTemplatesPath = "generic"; - const baseTemplatesPath = "side-by-side"; - const iconsBaseTemplatesPath = "icon"; - const tagsBaseTemplatesPath = "tag"; - - const matcher = Rematch.rematch(function(a, b) { - const amod = a.content.substr(1); - const bmod = b.content.substr(1); - - return Rematch.distance(amod, bmod); - }); - - function SideBySidePrinter(config) { - this.config = config; - - const HoganJsUtils = require("./hoganjs-utils.js").HoganJsUtils; - hoganUtils = new HoganJsUtils(config); - } - - SideBySidePrinter.prototype.makeDiffHtml = function(file, diffs) { - const fileDiffTemplate = hoganUtils.template(baseTemplatesPath, "file-diff"); - const filePathTemplate = hoganUtils.template(genericTemplatesPath, "file-path"); - const fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, "file"); - const fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file)); - - return fileDiffTemplate.render({ - file: file, - fileHtmlId: printerUtils.getHtmlId(file), - diffs: diffs, - filePath: filePathTemplate.render( - { - fileDiffName: printerUtils.getDiffName(file) - }, - { - fileIcon: fileIconTemplate, - fileTag: fileTagTemplate - } - ) - }); - }; - - SideBySidePrinter.prototype.generateSideBySideJsonHtml = function(diffFiles) { - const that = this; - - const content = diffFiles - .map(function(file) { - let diffs; - if (file.blocks.length) { - diffs = that.generateSideBySideFileHtml(file); - } else { - diffs = that.generateEmptyDiff(); - } - - return that.makeDiffHtml(file, diffs); - }) - .join("\n"); - - return hoganUtils.render(genericTemplatesPath, "wrapper", { content: content }); - }; - - SideBySidePrinter.prototype.makeSideHtml = function(blockHeader) { - return hoganUtils.render(genericTemplatesPath, "column-line-number", { - diffParser: diffParser, - blockHeader: utils.escape(blockHeader), - lineClass: "d2h-code-side-linenumber", - contentClass: "d2h-code-side-line" - }); - }; - - SideBySidePrinter.prototype.generateSideBySideFileHtml = function(file) { - const that = this; - const fileHtml = {}; - fileHtml.left = ""; - fileHtml.right = ""; - - file.blocks.forEach(function(block) { - fileHtml.left += that.makeSideHtml(block.header); - fileHtml.right += that.makeSideHtml(""); - - let oldLines = []; - let newLines = []; - - function processChangeBlock() { - let matches; - let insertType; - let deleteType; - - const comparisons = oldLines.length * newLines.length; - - const maxLineSizeInBlock = Math.max.apply( - null, - oldLines.concat(newLines).map(function(elem) { - return elem.length; - }) - ); - - const doMatching = - comparisons < that.config.matchingMaxComparisons && - maxLineSizeInBlock < that.config.maxLineSizeInBlockForComparison && - (that.config.matching === "lines" || that.config.matching === "words"); - - if (doMatching) { - matches = matcher(oldLines, newLines); - insertType = diffParser.LINE_TYPE.INSERT_CHANGES; - deleteType = diffParser.LINE_TYPE.DELETE_CHANGES; - } else { - matches = [[oldLines, newLines]]; - insertType = diffParser.LINE_TYPE.INSERTS; - deleteType = diffParser.LINE_TYPE.DELETES; - } - - matches.forEach(function(match) { - oldLines = match[0]; - newLines = match[1]; - - const common = Math.min(oldLines.length, newLines.length); - const max = Math.max(oldLines.length, newLines.length); - - for (let j = 0; j < common; j++) { - const oldLine = oldLines[j]; - const newLine = newLines[j]; - - that.config.isCombined = file.isCombined; - - const diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config); - - fileHtml.left += that.generateSingleLineHtml( - file.isCombined, - deleteType, - oldLine.oldNumber, - diff.first.line, - diff.first.prefix - ); - fileHtml.right += that.generateSingleLineHtml( - file.isCombined, - insertType, - newLine.newNumber, - diff.second.line, - diff.second.prefix - ); - } - - if (max > common) { - const oldSlice = oldLines.slice(common); - const newSlice = newLines.slice(common); - - const tmpHtml = that.processLines(file.isCombined, oldSlice, newSlice); - fileHtml.left += tmpHtml.left; - fileHtml.right += tmpHtml.right; - } - }); - - oldLines = []; - newLines = []; - } - - for (let i = 0; i < block.lines.length; i++) { - const line = block.lines[i]; - const prefix = line.content[0]; - const escapedLine = utils.escape(line.content.substr(1)); - - if ( - line.type !== diffParser.LINE_TYPE.INSERTS && - (newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0)) - ) { - processChangeBlock(); - } - - if (line.type === diffParser.LINE_TYPE.CONTEXT) { - fileHtml.left += that.generateSingleLineHtml(file.isCombined, line.type, line.oldNumber, escapedLine, prefix); - fileHtml.right += that.generateSingleLineHtml( - file.isCombined, - line.type, - line.newNumber, - escapedLine, - prefix - ); - } else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) { - fileHtml.left += that.generateSingleLineHtml(file.isCombined, diffParser.LINE_TYPE.CONTEXT, "", "", ""); - fileHtml.right += that.generateSingleLineHtml( - file.isCombined, - line.type, - line.newNumber, - escapedLine, - prefix - ); - } else if (line.type === diffParser.LINE_TYPE.DELETES) { - oldLines.push(line); - } else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) { - newLines.push(line); - } else { - console.error("unknown state in html side-by-side generator"); - processChangeBlock(); - } - } - - processChangeBlock(); - }); - - return fileHtml; - }; - - SideBySidePrinter.prototype.processLines = function(isCombined, oldLines, newLines) { - const that = this; - const fileHtml = {}; - fileHtml.left = ""; - fileHtml.right = ""; - - const maxLinesNumber = Math.max(oldLines.length, newLines.length); - for (let i = 0; i < maxLinesNumber; i++) { - const oldLine = oldLines[i]; - const newLine = newLines[i]; - var oldContent; - var newContent; - var oldPrefix; - var newPrefix; - - if (oldLine) { - oldContent = utils.escape(oldLine.content.substr(1)); - oldPrefix = oldLine.content[0]; - } - - if (newLine) { - newContent = utils.escape(newLine.content.substr(1)); - newPrefix = newLine.content[0]; - } - - if (oldLine && newLine) { - fileHtml.left += that.generateSingleLineHtml( - isCombined, - oldLine.type, - oldLine.oldNumber, - oldContent, - oldPrefix - ); - fileHtml.right += that.generateSingleLineHtml( - isCombined, - newLine.type, - newLine.newNumber, - newContent, - newPrefix - ); - } else if (oldLine) { - fileHtml.left += that.generateSingleLineHtml( - isCombined, - oldLine.type, - oldLine.oldNumber, - oldContent, - oldPrefix - ); - fileHtml.right += that.generateSingleLineHtml(isCombined, diffParser.LINE_TYPE.CONTEXT, "", "", ""); - } else if (newLine) { - fileHtml.left += that.generateSingleLineHtml(isCombined, diffParser.LINE_TYPE.CONTEXT, "", "", ""); - fileHtml.right += that.generateSingleLineHtml( - isCombined, - newLine.type, - newLine.newNumber, - newContent, - newPrefix - ); - } else { - console.error("How did it get here?"); - } - } - - return fileHtml; - }; - - SideBySidePrinter.prototype.generateSingleLineHtml = function(isCombined, type, number, content, possiblePrefix) { - let lineWithoutPrefix = content; - let prefix = possiblePrefix; - let lineClass = "d2h-code-side-linenumber"; - let contentClass = "d2h-code-side-line"; - - if (!number && !content) { - lineClass += " d2h-code-side-emptyplaceholder"; - contentClass += " d2h-code-side-emptyplaceholder"; - type += " d2h-emptyplaceholder"; - prefix = " "; - lineWithoutPrefix = " "; - } else if (!prefix) { - const lineWithPrefix = printerUtils.separatePrefix(isCombined, content); - prefix = lineWithPrefix.prefix; - lineWithoutPrefix = lineWithPrefix.line; - } - - if (prefix === " ") { - prefix = " "; - } - - return hoganUtils.render(genericTemplatesPath, "line", { - type: type, - lineClass: lineClass, - contentClass: contentClass, - prefix: prefix, - content: lineWithoutPrefix, - lineNumber: number - }); - }; - - SideBySidePrinter.prototype.generateEmptyDiff = function() { - const fileHtml = {}; - fileHtml.right = ""; - - fileHtml.left = hoganUtils.render(genericTemplatesPath, "empty-diff", { - contentClass: "d2h-code-side-line", - diffParser: diffParser - }); - - return fileHtml; - }; - - module.exports.SideBySidePrinter = SideBySidePrinter; -})(); diff --git a/src/side-by-side-renderer.ts b/src/side-by-side-renderer.ts new file mode 100644 index 0000000..7af8edd --- /dev/null +++ b/src/side-by-side-renderer.ts @@ -0,0 +1,352 @@ +import * as utils from "./utils"; +import HoganJsUtils from "./hoganjs-utils"; +import * as Rematch from "./rematch"; +import * as renderUtils from "./render-utils"; + +export interface SideBySideRendererConfig extends renderUtils.RenderConfig { + renderNothingWhenEmpty?: boolean; + matchingMaxComparisons?: number; + maxLineSizeInBlockForComparison?: number; +} + +export const defaultSideBySideRendererConfig = { + ...renderUtils.defaultRenderConfig, + renderNothingWhenEmpty: false, + matchingMaxComparisons: 2500, + maxLineSizeInBlockForComparison: 200 +}; + +type FileHtml = { + right: string; + left: string; +}; + +const genericTemplatesPath = "generic"; +const baseTemplatesPath = "side-by-side"; +const iconsBaseTemplatesPath = "icon"; +const tagsBaseTemplatesPath = "tag"; + +export default class SideBySideRenderer { + private readonly hoganUtils: HoganJsUtils; + private readonly config: typeof defaultSideBySideRendererConfig; + + constructor(hoganUtils: HoganJsUtils, config: SideBySideRendererConfig) { + this.hoganUtils = hoganUtils; + this.config = { ...defaultSideBySideRendererConfig, ...config }; + } + + render(diffFiles: renderUtils.DiffFile[]): string | undefined { + const content = diffFiles + .map(file => { + let diffs; + if (file.blocks.length) { + diffs = this.generateSideBySideFileHtml(file); + } else { + diffs = this.generateEmptyDiff(); + } + + return this.makeDiffHtml(file, diffs); + }) + .join("\n"); + + return this.hoganUtils.render(genericTemplatesPath, "wrapper", { content: content }); + } + + // TODO: Make this private after improving tests + generateEmptyDiff(): FileHtml { + return { + right: "", + left: + this.hoganUtils.render(genericTemplatesPath, "empty-diff", { + contentClass: "d2h-code-side-line", + CSSLineClass: renderUtils.CSSLineClass + }) || "" + }; + } + + // TODO: Make this private after improving tests + makeDiffHtml(file: renderUtils.DiffFile, diffs: FileHtml): string { + const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, "file-diff"); + const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, "file-path"); + const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, "file"); + const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file)); + + return fileDiffTemplate.render({ + file: file, + fileHtmlId: renderUtils.getHtmlId(file), + diffs: diffs, + filePath: filePathTemplate.render( + { + fileDiffName: renderUtils.filenameDiff(file) + }, + { + fileIcon: fileIconTemplate, + fileTag: fileTagTemplate + } + ) + }); + } + + // TODO: Make this private after improving tests + makeSideHtml(blockHeader: string): string { + return this.hoganUtils.render(genericTemplatesPath, "column-line-number", { + CSSLineClass: renderUtils.CSSLineClass, + blockHeader: utils.escapeForHtml(blockHeader), + lineClass: "d2h-code-side-linenumber", + contentClass: "d2h-code-side-line" + }); + } + + // TODO: Make this private after improving tests + generateSideBySideFileHtml(file: renderUtils.DiffFile): FileHtml { + const prefixSize = renderUtils.prefixLength(file.isCombined); + const distance = Rematch.newDistanceFn((e: renderUtils.DiffLine) => e.content.substring(prefixSize)); + const matcher = Rematch.newMatcherFn(distance); + + const fileHtml = { + right: "", + left: "" + }; + + file.blocks.forEach(block => { + fileHtml.left += this.makeSideHtml(block.header); + fileHtml.right += this.makeSideHtml(""); + + let oldLines: renderUtils.DiffLine[] = []; + let newLines: renderUtils.DiffLine[] = []; + + const processChangeBlock = (): void => { + let matches; + let insertType: renderUtils.CSSLineClass; + let deleteType: renderUtils.CSSLineClass; + + const comparisons = oldLines.length * newLines.length; + + const maxLineSizeInBlock = Math.max.apply(null, oldLines.concat(newLines).map(elem => elem.content.length)); + + const doMatching = + comparisons < this.config.matchingMaxComparisons && + maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison && + (this.config.matching === "lines" || this.config.matching === "words"); + + if (doMatching) { + matches = matcher(oldLines, newLines); + insertType = renderUtils.CSSLineClass.INSERT_CHANGES; + deleteType = renderUtils.CSSLineClass.DELETE_CHANGES; + } else { + matches = [[oldLines, newLines]]; + insertType = renderUtils.CSSLineClass.INSERTS; + deleteType = renderUtils.CSSLineClass.DELETES; + } + + matches.forEach(match => { + oldLines = match[0]; + newLines = match[1]; + + const common = Math.min(oldLines.length, newLines.length); + const max = Math.max(oldLines.length, newLines.length); + + for (let j = 0; j < common; j++) { + const oldLine = oldLines[j]; + const newLine = newLines[j]; + + const diff = renderUtils.diffHighlight(oldLine.content, newLine.content, file.isCombined, this.config); + + fileHtml.left += this.generateSingleLineHtml( + file.isCombined, + deleteType, + diff.oldLine.content, + oldLine.oldNumber, + diff.oldLine.prefix + ); + fileHtml.right += this.generateSingleLineHtml( + file.isCombined, + insertType, + diff.newLine.content, + newLine.newNumber, + diff.newLine.prefix + ); + } + + if (max > common) { + const oldSlice = oldLines.slice(common); + const newSlice = newLines.slice(common); + + const tmpHtml = this.processLines(file.isCombined, oldSlice, newSlice); + fileHtml.left += tmpHtml.left; + fileHtml.right += tmpHtml.right; + } + }); + + oldLines = []; + newLines = []; + }; + + for (let i = 0; i < block.lines.length; i++) { + const diffLine = block.lines[i]; + const { prefix, line } = renderUtils.deconstructLine(diffLine.content, file.isCombined); + const escapedLine = utils.escapeForHtml(line); + + if ( + diffLine.type !== renderUtils.LineType.INSERT && + (newLines.length > 0 || (diffLine.type !== renderUtils.LineType.DELETE && oldLines.length > 0)) + ) { + processChangeBlock(); + } + + if (diffLine.type === renderUtils.LineType.CONTEXT) { + fileHtml.left += this.generateSingleLineHtml( + file.isCombined, + renderUtils.toCSSClass(diffLine.type), + escapedLine, + diffLine.oldNumber, + prefix + ); + fileHtml.right += this.generateSingleLineHtml( + file.isCombined, + renderUtils.toCSSClass(diffLine.type), + escapedLine, + diffLine.newNumber, + prefix + ); + } else if (diffLine.type === renderUtils.LineType.INSERT && !oldLines.length) { + fileHtml.left += this.generateSingleLineHtml(file.isCombined, renderUtils.CSSLineClass.CONTEXT, ""); + fileHtml.right += this.generateSingleLineHtml( + file.isCombined, + renderUtils.toCSSClass(diffLine.type), + escapedLine, + diffLine.newNumber, + prefix + ); + } else if (diffLine.type === renderUtils.LineType.DELETE) { + oldLines.push(diffLine); + } else if (diffLine.type === renderUtils.LineType.INSERT && Boolean(oldLines.length)) { + newLines.push(diffLine); + } else { + console.error("unknown state in html side-by-side generator"); + processChangeBlock(); + } + } + + processChangeBlock(); + }); + + return fileHtml; + } + + // TODO: Make this private after improving tests + processLines(isCombined: boolean, oldLines: renderUtils.DiffLine[], newLines: renderUtils.DiffLine[]): FileHtml { + const fileHtml = { + right: "", + left: "" + }; + + const maxLinesNumber = Math.max(oldLines.length, newLines.length); + for (let i = 0; i < maxLinesNumber; i++) { + const oldLine = oldLines[i]; + const newLine = newLines[i]; + + let oldContent; + let newContent; + let oldPrefix; + let newPrefix; + + if (oldLine) { + const { prefix, line } = renderUtils.deconstructLine(oldLine.content, isCombined); + oldContent = utils.escapeForHtml(line); + oldPrefix = prefix; + } else { + oldContent = ""; + oldPrefix = ""; + } + + if (newLine) { + const { prefix, line } = renderUtils.deconstructLine(newLine.content, isCombined); + newContent = utils.escapeForHtml(line); + newPrefix = prefix; + } else { + newContent = ""; + oldPrefix = ""; + } + + if (oldLine && newLine) { + fileHtml.left += this.generateSingleLineHtml( + isCombined, + renderUtils.toCSSClass(oldLine.type), + oldContent, + oldLine.oldNumber, + oldPrefix + ); + fileHtml.right += this.generateSingleLineHtml( + isCombined, + renderUtils.toCSSClass(newLine.type), + newContent, + newLine.newNumber, + newPrefix + ); + } else if (oldLine) { + fileHtml.left += this.generateSingleLineHtml( + isCombined, + renderUtils.toCSSClass(oldLine.type), + oldContent, + oldLine.oldNumber, + oldPrefix + ); + fileHtml.right += this.generateSingleLineHtml(isCombined, renderUtils.CSSLineClass.CONTEXT, ""); + } else if (newLine) { + fileHtml.left += this.generateSingleLineHtml(isCombined, renderUtils.CSSLineClass.CONTEXT, ""); + fileHtml.right += this.generateSingleLineHtml( + isCombined, + renderUtils.toCSSClass(newLine.type), + newContent, + newLine.newNumber, + newPrefix + ); + } else { + console.error("How did it get here?"); + } + } + + return fileHtml; + } + + // TODO: Make this private after improving tests + generateSingleLineHtml( + isCombined: boolean, + type: renderUtils.CSSLineClass, + content: string, + number?: number, + possiblePrefix?: string + ): string { + let lineWithoutPrefix = content; + let prefix = possiblePrefix; + let lineClass = "d2h-code-side-linenumber"; + let contentClass = "d2h-code-side-line"; + let preparedType: string = type; + + if (!number && !content) { + lineClass += " d2h-code-side-emptyplaceholder"; + contentClass += " d2h-code-side-emptyplaceholder"; + preparedType += " d2h-emptyplaceholder"; + prefix = " "; + lineWithoutPrefix = " "; + } else if (!prefix) { + const lineWithPrefix = renderUtils.deconstructLine(content, isCombined); + prefix = lineWithPrefix.prefix; + lineWithoutPrefix = lineWithPrefix.line; + } + + if (prefix === " ") { + prefix = " "; + } + + return this.hoganUtils.render(genericTemplatesPath, "line", { + type: preparedType, + lineClass: lineClass, + contentClass: contentClass, + prefix: prefix, + content: lineWithoutPrefix, + lineNumber: number + }); + } +} diff --git a/src/templates/generic-column-line-number.mustache b/src/templates/generic-column-line-number.mustache index 181d651..caf1c29 100644 --- a/src/templates/generic-column-line-number.mustache +++ b/src/templates/generic-column-line-number.mustache @@ -1,6 +1,6 @@ - - -
{{{blockHeader}}}
+ + +
{{{blockHeader}}}
diff --git a/src/templates/generic-empty-diff.mustache b/src/templates/generic-empty-diff.mustache index c6b1abd..362e31f 100644 --- a/src/templates/generic-empty-diff.mustache +++ b/src/templates/generic-empty-diff.mustache @@ -1,6 +1,6 @@ - -
+ +
File without changes
diff --git a/src/ui/js/diff2html-ui.js b/src/ui/js/diff2html-ui.js deleted file mode 100644 index f7c1138..0000000 --- a/src/ui/js/diff2html-ui.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * - * Diff to HTML (diff2html-ui.js) - * Author: rtfpessoa - * - * Depends on: [ jQuery ] - * Optional dependencies on: [ highlight.js ] - * - */ - -/* global $, hljs, Diff2Html */ - -(function() { - const highlightJS = require("./highlight.js-internals.js").HighlightJS; - - let diffJson = null; - const defaultTarget = "body"; - let currentSelectionColumnId = -1; - - function Diff2HtmlUI(config) { - const cfg = config || {}; - - if (cfg.diff) { - diffJson = Diff2Html.getJsonFromDiff(cfg.diff); - } else if (cfg.json) { - diffJson = cfg.json; - } - - this._initSelection(); - } - - Diff2HtmlUI.prototype.draw = function(targetId, config) { - const cfg = config || {}; - cfg.inputFormat = "json"; - const $target = this._getTarget(targetId); - $target.html(Diff2Html.getPrettyHtml(diffJson, cfg)); - - if (cfg.synchronisedScroll) { - this.synchronisedScroll($target, cfg); - } - }; - - Diff2HtmlUI.prototype.synchronisedScroll = function(targetId) { - const $target = this._getTarget(targetId); - $target.find(".d2h-file-side-diff").scroll(function() { - const $this = $(this); - $this - .closest(".d2h-file-wrapper") - .find(".d2h-file-side-diff") - .scrollLeft($this.scrollLeft()); - }); - }; - - Diff2HtmlUI.prototype.fileListCloseable = function(targetId, startVisible) { - const $target = this._getTarget(targetId); - - const hashTag = this._getHashTag(); - - const $showBtn = $target.find(".d2h-show"); - const $hideBtn = $target.find(".d2h-hide"); - const $fileList = $target.find(".d2h-file-list"); - - if (hashTag === "files-summary-show") show(); - else if (hashTag === "files-summary-hide") hide(); - else if (startVisible) show(); - else hide(); - - $showBtn.click(show); - $hideBtn.click(hide); - - function show() { - $showBtn.hide(); - $hideBtn.show(); - $fileList.show(); - } - - function hide() { - $hideBtn.hide(); - $showBtn.show(); - $fileList.hide(); - } - }; - - Diff2HtmlUI.prototype.highlightCode = function(targetId) { - const that = this; - - const $target = that._getTarget(targetId); - - // collect all the diff files and execute the highlight on their lines - const $files = $target.find(".d2h-file-wrapper"); - $files.map(function(_i, file) { - let oldLinesState; - let newLinesState; - const $file = $(file); - const language = $file.data("lang"); - - // collect all the code lines and execute the highlight on them - const $codeLines = $file.find(".d2h-code-line-ctn"); - $codeLines.map(function(_j, line) { - const $line = $(line); - const text = line.textContent; - const lineParent = line.parentNode; - - let lineState; - if (lineParent.className.indexOf("d2h-del") !== -1) { - lineState = oldLinesState; - } else { - lineState = newLinesState; - } - - const result = hljs.getLanguage(language) - ? hljs.highlight(language, text, true, lineState) - : hljs.highlightAuto(text); - - if (lineParent.className.indexOf("d2h-del") !== -1) { - oldLinesState = result.top; - } else if (lineParent.className.indexOf("d2h-ins") !== -1) { - newLinesState = result.top; - } else { - oldLinesState = result.top; - newLinesState = result.top; - } - - const originalStream = highlightJS.nodeStream(line); - if (originalStream.length) { - const resultNode = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); - resultNode.innerHTML = result.value; - result.value = highlightJS.mergeStreams(originalStream, highlightJS.nodeStream(resultNode), text); - } - - $line.addClass("hljs"); - $line.addClass(result.language); - $line.html(result.value); - }); - }); - }; - - Diff2HtmlUI.prototype._getTarget = function(targetId) { - let $target; - - if (typeof targetId === "object" && targetId instanceof jQuery) { - $target = targetId; - } else if (typeof targetId === "string") { - $target = $(targetId); - } else { - console.error("Wrong target provided! Falling back to default value 'body'."); - console.log("Please provide a jQuery object or a valid DOM query string."); - $target = $(defaultTarget); - } - - return $target; - }; - - Diff2HtmlUI.prototype._getHashTag = function() { - const docUrl = document.URL; - const hashTagIndex = docUrl.indexOf("#"); - - let hashTag = null; - if (hashTagIndex !== -1) { - hashTag = docUrl.substr(hashTagIndex + 1); - } - - return hashTag; - }; - - Diff2HtmlUI.prototype._distinct = function(collection) { - return collection.filter(function(v, i) { - return collection.indexOf(v) === i; - }); - }; - - Diff2HtmlUI.prototype._initSelection = function() { - const body = $("body"); - const that = this; - - body.on("mousedown", ".d2h-diff-table", function(event) { - const target = $(event.target); - const table = target.closest(".d2h-diff-table"); - - if (target.closest(".d2h-code-line,.d2h-code-side-line").length) { - table.removeClass("selecting-left"); - table.addClass("selecting-right"); - currentSelectionColumnId = 1; - } else if (target.closest(".d2h-code-linenumber,.d2h-code-side-linenumber").length) { - table.removeClass("selecting-right"); - table.addClass("selecting-left"); - currentSelectionColumnId = 0; - } - }); - - body.on("copy", ".d2h-diff-table", function(event) { - const clipboardData = event.originalEvent.clipboardData; - const text = that._getSelectedText(); - clipboardData.setData("text", text); - event.preventDefault(); - }); - }; - - Diff2HtmlUI.prototype._getSelectedText = function() { - const sel = window.getSelection(); - const range = sel.getRangeAt(0); - const doc = range.cloneContents(); - const nodes = doc.querySelectorAll("tr"); - let text = ""; - const idx = currentSelectionColumnId; - - if (nodes.length === 0) { - text = doc.textContent; - } else { - [].forEach.call(nodes, function(tr, i) { - const td = tr.cells[tr.cells.length === 1 ? 0 : idx]; - text += (i ? "\n" : "") + td.textContent.replace(/(?:\r\n|\r|\n)/g, ""); - }); - } - - return text; - }; - - module.exports.Diff2HtmlUI = Diff2HtmlUI; - - // Expose diff2html in the browser - global.Diff2HtmlUI = Diff2HtmlUI; -})(); diff --git a/src/ui/js/diff2html-ui.ts b/src/ui/js/diff2html-ui.ts new file mode 100644 index 0000000..5533807 --- /dev/null +++ b/src/ui/js/diff2html-ui.ts @@ -0,0 +1,220 @@ +import HighlightJS from "highlight.js"; +import * as HighlightJSInternals from "./highlight.js-internals"; +import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from "../../diff2html"; +import { DiffFile } from "../../render-utils"; + +interface Diff2HtmlUIConfig extends Diff2HtmlConfig { + synchronisedScroll?: boolean; +} + +const defaultDiff2HtmlUIConfig = { + ...defaultDiff2HtmlConfig, + synchronisedScroll: true +}; + +export default class Diff2HtmlUI { + readonly config: typeof defaultDiff2HtmlUIConfig; + readonly diffHtml: string; + targetElement: HTMLElement; + currentSelectionColumnId = -1; + + constructor(diffInput: string | DiffFile[], target: HTMLElement, config: Diff2HtmlUIConfig = {}) { + this.config = { ...defaultDiff2HtmlUIConfig, ...config }; + this.diffHtml = html(diffInput, this.config); + this.targetElement = target; + } + + draw(): void { + this.targetElement.innerHTML = this.diffHtml; + this.initSelection(); + if (this.config.synchronisedScroll) this.synchronisedScroll(); + } + + synchronisedScroll(): void { + this.targetElement.querySelectorAll(".d2h-file-wrapper").forEach(wrapper => { + const [left, right] = [].slice.call(wrapper.querySelectorAll(".d2h-file-side-diff")) as HTMLElement[]; + + if (left === undefined || right === undefined) return; + + const onScroll = (event: Event): void => { + if (event === null || event.target === null) return; + + if (event.target === left) { + right.scrollTop = left.scrollTop; + } else { + left.scrollTop = right.scrollTop; + } + }; + + left.addEventListener("scroll", onScroll); + right.addEventListener("scroll", onScroll); + }); + } + + fileListCloseable(startVisible: boolean): void { + const hashTag = this.getHashTag(); + + const showBtn = this.targetElement.querySelector(".d2h-show") as HTMLElement; + const hideBtn = this.targetElement.querySelector(".d2h-hide") as HTMLElement; + const fileList = this.targetElement.querySelector(".d2h-file-list") as HTMLElement; + + if (showBtn === null || hideBtn === null || fileList === null) return; + + function show(): void { + showBtn.style.display = ""; + hideBtn.style.display = ""; + fileList.style.display = ""; + } + + function hide(): void { + showBtn.style.display = "none"; + hideBtn.style.display = "none"; + fileList.style.display = "none"; + } + + showBtn.addEventListener("click", () => show()); + hideBtn.addEventListener("click", () => hide()); + + if (hashTag === "files-summary-show") show(); + else if (hashTag === "files-summary-hide") hide(); + else if (startVisible) show(); + else hide(); + } + + highlightCode(): void { + // Collect all the diff files and execute the highlight on their lines + const files = this.targetElement.querySelectorAll(".d2h-file-wrapper"); + files.forEach(file => { + let oldLinesState: HighlightJS.ICompiledMode; + let newLinesState: HighlightJS.ICompiledMode; + + // Collect all the code lines and execute the highlight on them + const codeLines = file.querySelectorAll(".d2h-code-line-ctn"); + codeLines.forEach(line => { + const text = line.textContent; + const lineParent = line.parentNode as HTMLElement; + + if (lineParent === null || text === null) return; + + const lineState = lineParent.className.indexOf("d2h-del") !== -1 ? oldLinesState : newLinesState; + + const language = file.getAttribute("data-lang"); + const result = + language && HighlightJS.getLanguage(language) + ? HighlightJS.highlight(language, text, true, lineState) + : HighlightJS.highlightAuto(text); + + if (this.instanceOfIHighlightResult(result)) { + if (lineParent.className.indexOf("d2h-del") !== -1) { + oldLinesState = result.top; + } else if (lineParent.className.indexOf("d2h-ins") !== -1) { + newLinesState = result.top; + } else { + oldLinesState = result.top; + newLinesState = result.top; + } + } + + const originalStream = HighlightJSInternals.nodeStream(line); + if (originalStream.length) { + const resultNode = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + resultNode.innerHTML = result.value; + result.value = HighlightJSInternals.mergeStreams( + originalStream, + HighlightJSInternals.nodeStream(resultNode), + text + ); + } + + line.classList.add("hljs"); + line.classList.add("result.language"); + line.innerHTML = result.value; + }); + }); + } + + private instanceOfIHighlightResult( + object: HighlightJS.IHighlightResult | HighlightJS.IAutoHighlightResult + ): object is HighlightJS.IHighlightResult { + return "top" in object; + } + + private getHashTag(): string | null { + const docUrl = document.URL; + const hashTagIndex = docUrl.indexOf("#"); + + let hashTag = null; + if (hashTagIndex !== -1) { + hashTag = docUrl.substr(hashTagIndex + 1); + } + + return hashTag; + } + + private initSelection(): void { + const body = document.getElementsByTagName("body")[0]; + const diffTable = body.getElementsByClassName("d2h-diff-table")[0]; + + diffTable.addEventListener("mousedown", event => { + if (event === null || event.target === null) return; + + const mouseEvent = event as MouseEvent; + const target = mouseEvent.target as HTMLElement; + const table = target.closest(".d2h-diff-table"); + + if (table !== null) { + if (target.closest(".d2h-code-line,.d2h-code-side-line") !== null) { + table.classList.remove("selecting-left"); + table.classList.add("selecting-right"); + this.currentSelectionColumnId = 1; + } else if (target.closest(".d2h-code-linenumber,.d2h-code-side-linenumber") !== null) { + table.classList.remove("selecting-right"); + table.classList.add("selecting-left"); + this.currentSelectionColumnId = 0; + } + } + }); + + diffTable.addEventListener("copy", event => { + const clipboardEvent = event as ClipboardEvent; + const clipboardData = clipboardEvent.clipboardData; + const text = this.getSelectedText(); + + if (clipboardData === null || text === undefined) return; + + clipboardData.setData("text", text); + event.preventDefault(); + }); + } + + private getSelectedText(): string | undefined { + const sel = window.getSelection(); + + if (sel === null) return; + + const range = sel.getRangeAt(0); + const doc = range.cloneContents(); + const nodes = doc.querySelectorAll("tr"); + const idx = this.currentSelectionColumnId; + + let text = ""; + if (nodes.length === 0) { + text = doc.textContent || ""; + } else { + nodes.forEach((tr, i) => { + const td = tr.cells[tr.cells.length === 1 ? 0 : idx]; + + if (td === null || td.textContent === null) return; + + text += (i ? "\n" : "") + td.textContent.replace(/(?:\r\n|\r|\n)/g, ""); + }); + } + + return text; + } +} + +// TODO: Avoid disabling types +// eslint-disable-next-line +// @ts-ignore +global.Diff2HtmlUI = Diff2HtmlUI; diff --git a/src/ui/js/highlight.js-internals.js b/src/ui/js/highlight.js-internals.js deleted file mode 100644 index 3bf89f0..0000000 --- a/src/ui/js/highlight.js-internals.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * - * highlight.js - * Author: isagalaev - * - */ - -(function() { - function HighlightJS() {} - - /* - * Copied from Highlight.js Private API - * Will be removed when this part of the API is exposed - */ - - /* Utility vars */ - - const ArrayProto = []; - - /* Utility functions */ - - function escape(value) { - return value - .replace(/&/gm, "&") - .replace(//gm, ">"); - } - - function tag(node) { - return node.nodeName.toLowerCase(); - } - - /* Stream merging */ - - HighlightJS.prototype.nodeStream = function(node) { - const result = []; - (function _nodeStream(node, offset) { - for (let child = node.firstChild; child; child = child.nextSibling) { - if (child.nodeType === 3) { - offset += child.nodeValue.length; - } else if (child.nodeType === 1) { - result.push({ - event: "start", - offset: offset, - node: child - }); - offset = _nodeStream(child, offset); - // Prevent void elements from having an end tag that would actually - // double them in the output. There are more void elements in HTML - // but we list only those realistically expected in code display. - if (!tag(child).match(/br|hr|img|input/)) { - result.push({ - event: "stop", - offset: offset, - node: child - }); - } - } - } - return offset; - })(node, 0); - return result; - }; - - HighlightJS.prototype.mergeStreams = function(original, highlighted, value) { - let processed = 0; - let result = ""; - const nodeStack = []; - - function selectStream() { - if (!original.length || !highlighted.length) { - return original.length ? original : highlighted; - } - if (original[0].offset !== highlighted[0].offset) { - return original[0].offset < highlighted[0].offset ? original : highlighted; - } - - /* - To avoid starting the stream just before it should stop the order is - ensured that original always starts first and closes last: - if (event1 == 'start' && event2 == 'start') - return original; - if (event1 == 'start' && event2 == 'stop') - return highlighted; - if (event1 == 'stop' && event2 == 'start') - return original; - if (event1 == 'stop' && event2 == 'stop') - return highlighted; - ... which is collapsed to: - */ - return highlighted[0].event === "start" ? original : highlighted; - } - - function open(node) { - function attr_str(a) { - return " " + a.nodeName + '="' + escape(a.value) + '"'; - } - - result += "<" + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join("") + ">"; - } - - function close(node) { - result += ""; - } - - function render(event) { - (event.event === "start" ? open : close)(event.node); - } - - while (original.length || highlighted.length) { - let stream = selectStream(); - result += escape(value.substring(processed, stream[0].offset)); - processed = stream[0].offset; - if (stream === original) { - /* - On any opening or closing tag of the original markup we first close - the entire highlighted node stack, then render the original tag along - with all the following original tags at the same offset and then - reopen all the tags on the highlighted stack. - */ - nodeStack.reverse().forEach(close); - do { - render(stream.splice(0, 1)[0]); - stream = selectStream(); - } while (stream === original && stream.length && stream[0].offset === processed); - nodeStack.reverse().forEach(open); - } else { - if (stream[0].event === "start") { - nodeStack.push(stream[0].node); - } else { - nodeStack.pop(); - } - render(stream.splice(0, 1)[0]); - } - } - return result + escape(value.substr(processed)); - }; - - /* **** Highlight.js Private API **** */ - - module.exports.HighlightJS = new HighlightJS(); -})(); diff --git a/src/ui/js/highlight.js-internals.ts b/src/ui/js/highlight.js-internals.ts new file mode 100644 index 0000000..740d75d --- /dev/null +++ b/src/ui/js/highlight.js-internals.ts @@ -0,0 +1,134 @@ +/* + * Copied from Highlight.js Private API + * Will be removed when this part of the API is exposed + */ + +/* Utility functions */ + +function escape(value: string): string { + return value + .replace(/&/gm, "&") + .replace(//gm, ">"); +} + +function tag(node: Node): string { + return node.nodeName.toLowerCase(); +} + +/* Stream merging */ + +type NodeEvent = { + event: "start" | "stop"; + offset: number; + node: Node; +}; + +export function nodeStream(node: Node): NodeEvent[] { + const result: NodeEvent[] = []; + + const nodeStream = (node: Node, offset: number): number => { + for (let child = node.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 3 && child.nodeValue !== null) { + offset += child.nodeValue.length; + } else if (child.nodeType === 1) { + result.push({ + event: "start", + offset: offset, + node: child + }); + offset = nodeStream(child, offset); + // Prevent void elements from having an end tag that would actually + // double them in the output. There are more void elements in HTML + // but we list only those realistically expected in code display. + if (!tag(child).match(/br|hr|img|input/)) { + result.push({ + event: "stop", + offset: offset, + node: child + }); + } + } + } + return offset; + }; + + nodeStream(node, 0); + + return result; +} + +export function mergeStreams(original: NodeEvent[], highlighted: NodeEvent[], value: string): string { + let processed = 0; + let result = ""; + const nodeStack = []; + + function selectStream(): NodeEvent[] { + if (!original.length || !highlighted.length) { + return original.length ? original : highlighted; + } + if (original[0].offset !== highlighted[0].offset) { + return original[0].offset < highlighted[0].offset ? original : highlighted; + } + + /* + To avoid starting the stream just before it should stop the order is + ensured that original always starts first and closes last: + if (event1 == 'start' && event2 == 'start') + return original; + if (event1 == 'start' && event2 == 'stop') + return highlighted; + if (event1 == 'stop' && event2 == 'start') + return original; + if (event1 == 'stop' && event2 == 'stop') + return highlighted; + ... which is collapsed to: + */ + return highlighted[0].event === "start" ? original : highlighted; + } + + function open(node: Node): void { + const htmlNode = node as HTMLElement; + result += `<${tag(node)} ${[].map + .call(htmlNode.attributes, (attr: Attr) => `${attr.nodeName}="${escape(attr.value)}"`) + .join(" ")}>`; + } + + function close(node: Node): void { + result += ""; + } + + function render(event: NodeEvent): void { + (event.event === "start" ? open : close)(event.node); + } + + while (original.length || highlighted.length) { + let stream = selectStream(); + result += escape(value.substring(processed, stream[0].offset)); + processed = stream[0].offset; + if (stream === original) { + /* + On any opening or closing tag of the original markup we first close + the entire highlighted node stack, then render the original tag along + with all the following original tags at the same offset and then + reopen all the tags on the highlighted stack. + */ + nodeStack.reverse().forEach(close); + do { + render(stream.splice(0, 1)[0]); + stream = selectStream(); + } while (stream === original && stream.length && stream[0].offset === processed); + nodeStack.reverse().forEach(open); + } else { + if (stream[0].event === "start") { + nodeStack.push(stream[0].node); + } else { + nodeStack.pop(); + } + render(stream.splice(0, 1)[0]); + } + } + return result + escape(value.substr(processed)); +} + +/* **** Highlight.js Private API **** */ diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 4b62912..0000000 --- a/src/utils.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * - * Utils (utils.js) - * Author: rtfpessoa - * - */ - -(function() { - const merge = require("merge"); - - function Utils() {} - - Utils.prototype.escape = function(str) { - return str - .slice(0) - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'") - .replace(/\//g, "/"); - }; - - Utils.prototype.startsWith = function(str, start) { - if (typeof start === "object") { - let result = false; - start.forEach(function(s) { - if (str.indexOf(s) === 0) { - result = true; - } - }); - - return result; - } - - return str && str.indexOf(start) === 0; - }; - - Utils.prototype.valueOrEmpty = function(value) { - return value || ""; - }; - - Utils.prototype.safeConfig = function(cfg, defaultConfig) { - return merge.recursive(true, defaultConfig, cfg); - }; - - module.exports.Utils = new Utils(); -})(); diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d9d829b --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,68 @@ +const specials = [ + // Order matters for these + "-", + "[", + "]", + // Order doesn't matter for any of these + "/", + "{", + "}", + "(", + ")", + "*", + "+", + "?", + ".", + "\\", + "^", + "$", + "|" +]; + +// All characters will be escaped with '\' +// even though only some strictly require it when inside of [] +const regex = RegExp("[" + specials.join("\\") + "]", "g"); + +/** + * Escapes all required characters for safe usage inside a RegExp + */ +export function escapeForRegExp(str: string): string { + return str.replace(regex, "\\$&"); +} + +/** + * Escapes all required characters for safe HTML rendering + */ +export function escapeForHtml(str: string): string { + return str + .slice(0) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'") + .replace(/\//g, "/"); +} + +/** + * Converts all '\' in @path to unix style '/' + */ +export function unifyPath(path: string): string { + return path ? path.replace("\\", "/") : path; +} + +/** + * Create unique number identifier for @text + */ +export function hashCode(text: string): number { + let i, chr, len; + let hash = 0; + + for (i = 0, len = text.length; i < len; i++) { + chr = text.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + + return hash; +} diff --git a/tsconfig.json b/tsconfig.json index ead2b45..271fe7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "target": "es5", "module": "commonjs", "moduleResolution": "node", + "lib": ["es5", "es6", "es7", "esnext", "dom", "dom.iterable"], // TODO: Change to true after migration to TS is complete "allowJs": true, "declaration": false, @@ -21,8 +22,9 @@ "noUnusedLocals": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true }, - "include": ["./src/**/*"], + "include": ["./src/**/*", "./typings/**/*"], "exclude": ["node_modules", "./src/__tests__/*"] } diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json index cae940e..5fb1620 100644 --- a/tsconfig.scripts.json +++ b/tsconfig.scripts.json @@ -6,5 +6,5 @@ "declarationMap": false, "sourceMap": false }, - "include": ["./scripts/**/*"] + "include": ["./scripts/**/*", "./typings/**/*"] } diff --git a/typings/hoganjs.d.ts b/typings/hoganjs.d.ts new file mode 100644 index 0000000..4958cd8 --- /dev/null +++ b/typings/hoganjs.d.ts @@ -0,0 +1,93 @@ +// Type definitions for hogan.js 3.0 +// Project: http://twitter.github.com/hogan.js/ +// Definitions by: Andrew Leedham +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.2 + +declare module "hogan.js" { + export interface Context { + [key: string]: any; + } + + export interface SectionTags { + o: string; + c: string; + } + + export interface HoganOptions { + asString?: boolean; + sectionTags?: ReadonlyArray; + delimiters?: string; + disableLambda?: boolean; + } + + export interface Token { + tag: string; + otag?: string; + ctag?: string; + i?: number; + n?: string; + text?: string; + } + + export interface Leaf extends Token { + end: number; + nodes: Token[]; + } + + export type Tree = Leaf[]; + + export interface Partials { + [symbol: string]: HoganTemplate; + } + + export interface HoganConstructor { + code: (context: any, partials: object, indent: string) => string; + partials: object; + subs: object; + } + + export class HoganTemplate { + constructor(codeObject: HoganConstructor); + + /** + * Renders the template to a string. + * + * @param context - The data to render the template with. + * @param partials - The partials to render the template with. + * @param indent - The string to indent when rendering the template. + * @returns A rendered template. + */ + render(context: Context, partials?: Partials, indent?: string): string; + } + + export { HoganTemplate as Template, HoganTemplate as template }; + + export function compile(text: string, options?: HoganOptions & { asString: false }): HoganTemplate; + export function compile(text: string, options?: HoganOptions & { asString: true }): string; + /** + * Compiles templates to HoganTemplate objects, which have a render method. + * + * @param text - Raw mustache string to compile. + * @param options - Options to use when compiling. See https://github.com/twitter/hogan.js#compilation-options. + * @returns A HoganTemplate. + */ + export function compile(text: string, options?: HoganOptions): HoganTemplate | string; + /** + * Scans templates returning an array of found tokens. + * + * @param text - Raw mustache string to scan. + * @param delimiters - A string that overrides the default delimiters. Example: "<% %>". + * @returns Found tokens. + */ + export function scan(text: string, delimiters?: string): Token[]; + /** + * Structures tokens into a tree. + * + * @param tokens - An array of scanned tokens. + * @param text - Unused pass undefined. + * @param options - Options to use when parsing. See https://github.com/twitter/hogan.js#compilation-options. + * @returns The tree structure of the given tokens. + */ + export function parse(tokens: ReadonlyArray, text?: undefined, options?: HoganOptions): Tree; +} diff --git a/typings/merge.d.ts b/typings/merge.d.ts new file mode 100644 index 0000000..9881682 --- /dev/null +++ b/typings/merge.d.ts @@ -0,0 +1,3 @@ +declare module "merge" { + export function recursive(clone: boolean, ...items: object[]): object; +} diff --git a/website/templates/pages/demo/demo-scripts.partial.mustache b/website/templates/pages/demo/demo-scripts.partial.mustache index b1cd313..4aca780 100644 --- a/website/templates/pages/demo/demo-scripts.partial.mustache +++ b/website/templates/pages/demo/demo-scripts.partial.mustache @@ -1,8 +1,4 @@ - - - - diff --git a/website/templates/pages/demo/demo.js b/website/templates/pages/demo/demo.js index ff96e78..879822b 100644 --- a/website/templates/pages/demo/demo.js +++ b/website/templates/pages/demo/demo.js @@ -1,4 +1,5 @@ /* global Diff2HtmlUI */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* * Example URLs: @@ -13,227 +14,229 @@ * https://bitbucket.org/atlassian/amps/pull-requests/236 */ -$(document).ready(function() { +const searchParam = "diff"; + +function getUrlFromSearch(search) { + try { + return search + .split("?")[1] + .split(searchParam + "=")[1] + .split("&")[0]; + } catch (_ignore) {} + + return null; +} + +function getParamsFromSearch(search) { + const map = new Map(); + try { + search + .split("?")[1] + .split("&") + .forEach(e => { + const values = e.split("="); + map.set(values[0], values[1]); + }); + } catch (_ignore) {} + + return map; +} + +function prepareUrl(url) { + let fetchUrl; + const headers = new Headers(); + + const githubCommitUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; + const githubPrUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/pull\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; + + const gitlabCommitUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; + const gitlabPrUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/merge_requests\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; + + const bitbucketCommitUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/commits\/(.*?)(?:\/raw)?(?:\/.*)?$/; + const bitbucketPrUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/pull-requests\/(.*?)(?:\/.*)?$/; + + function gitLabUrlGen(userName, projectName, type, value) { + return ( + "https://crossorigin.me/https://gitlab.com/" + userName + "/" + projectName + "/" + type + "/" + value + ".diff" + ); + } + + function gitHubUrlGen(userName, projectName, type, value) { + headers.append("Accept", "application/vnd.github.v3.diff"); + return "https://api.github.com/repos/" + userName + "/" + projectName + "/" + type + "/" + value; + } + + function bitbucketUrlGen(userName, projectName, type, value) { + const baseUrl = "https://bitbucket.org/api/2.0/repositories/"; + if (type === "pullrequests") { + return baseUrl + userName + "/" + projectName + "/pullrequests/" + value + "/diff"; + } + return baseUrl + userName + "/" + projectName + "/diff/" + value; + } + + let values; + if ((values = githubCommitUrl.exec(url))) { + fetchUrl = gitHubUrlGen(values[1], values[2], "commits", values[3]); + } else if ((values = githubPrUrl.exec(url))) { + fetchUrl = gitHubUrlGen(values[1], values[2], "pulls", values[3]); + } else if ((values = gitlabCommitUrl.exec(url))) { + fetchUrl = gitLabUrlGen(values[1], values[2], "commit", values[3]); + } else if ((values = gitlabPrUrl.exec(url))) { + fetchUrl = gitLabUrlGen(values[1], values[2], "merge_requests", values[3]); + } else if ((values = bitbucketCommitUrl.exec(url))) { + fetchUrl = bitbucketUrlGen(values[1], values[2], "commit", values[3]); + } else if ((values = bitbucketPrUrl.exec(url))) { + fetchUrl = bitbucketUrlGen(values[1], values[2], "pullrequests", values[3]); + } else { + console.info("Could not parse url, using the provided url."); + fetchUrl = "https://crossorigin.me/" + url; + } + + return { + originalUrl: url, + url: fetchUrl, + headers: headers + }; +} + +function validateUrl(url) { + return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( + url + ); +} + +function updateUrl(url) { + const params = getParamsFromSearch(window.location.search); + + if (params[searchParam] === url) return; + + params[searchParam] = url; + + const paramString = Object.keys(params) + .map(function(k) { + return k + "=" + params[k]; + }) + .join("&"); + + window.location = "demo.html?" + paramString; +} + +function draw(req, forced, elements) { + if (!validateUrl(req.url)) { + console.error("Invalid url provided!"); + return; + } + + if (validateUrl(req.originalUrl)) updateUrl(req.originalUrl); + + const outputFormat = elements.outputFormat.val(); + const showFiles = elements.showFiles.is(":checked"); + const matching = elements.matching.val(); + const wordsThreshold = elements.wordsThreshold.val(); + const matchingMaxComparisons = elements.matchingMaxComparisons.val(); + + fetch(req.url, { + method: "GET", + headers: req.headers, + mode: "cors", + cache: "default" + }) + .then(function(res) { + return res.text(); + }) + .then(function(data) { + const params = getParamsFromSearch(window.location.search); + delete params[searchParam]; + + if (forced) { + params.outputFormat = outputFormat; + params.showFiles = showFiles; + params.matching = matching; + params.wordsThreshold = wordsThreshold; + params.matchingMaxComparisons = matchingMaxComparisons; + } else { + params.outputFormat = params.outputFormat || outputFormat; + params.showFiles = String(params.showFiles) !== "false" || (params.showFiles === null && showFiles); + params.matching = params.matching || matching; + params.wordsThreshold = params.wordsThreshold || wordsThreshold; + params.matchingMaxComparisons = params.matchingMaxComparisons || matchingMaxComparisons; + + elements.outputFormat.value = params.outputFormat; + elements.showFiles.setAttribute("checked", params.showFiles); + elements.matching.value = params.matching; + elements.wordsThreshold.value = params.wordsThreshold; + elements.matchingMaxComparisons.value = params.matchingMaxComparisons; + } + + params.synchronisedScroll = params.synchronisedScroll || true; + + const diff2htmlUi = new Diff2HtmlUI(data, elements.root); + + if (outputFormat === "side-by-side") { + elements.container.css({ width: "100%" }); + } else { + elements.container.css({ width: "" }); + } + + diff2htmlUi.draw(); + diff2htmlUi.fileListCloseable(params.fileListCloseable || false); + if (params.highlight === undefined || params.highlight) { + diff2htmlUi.highlightCode(); + } + + return undefined; + }) + .catch(() => {}); +} + +function smartDraw(urlOpt, urlElem, forced) { + const url = urlOpt || urlElem.val(); + const req = prepareUrl(url); + draw(req, forced); +} + +function bind(urlElem) { + $("#url-btn").click(e => { + e.preventDefault(); + const url = urlElem.val(); + smartDraw(url, urlElem); + }); + + urlElem.on("paste", e => { + const url = e.originalEvent.clipboardData.getData("Text"); + smartDraw(url, urlElem); + }); +} + +document.addEventListener("DOMContentLoaded", function() { // Improves browser compatibility require("whatwg-fetch"); - const searchParam = "diff"; - - const $container = $(".container"); - const $url = $("#url"); - const $outputFormat = $("#diff-url-options-output-format"); - const $showFiles = $("#diff-url-options-show-files"); - const $matching = $("#diff-url-options-matching"); - const $wordsThreshold = $("#diff-url-options-match-words-threshold"); - const $matchingMaxComparisons = $("#diff-url-options-matching-max-comparisons"); + const elements = { + root: document.getElementById("url-diff-container"), + container: document.getElementsByClassName("container"), + url: document.getElementById("url"), + outputFormat: document.getElementById("diff-url-options-output-format"), + showFiles: document.getElementById("diff-url-options-show-files"), + matching: document.getElementById("diff-url-options-matching"), + wordsThreshold: document.getElementById("diff-url-options-match-words-threshold"), + matchingMaxComparisons: document.getElementById("diff-url-options-matching-max-comparisons") + }; if (window.location.search) { const url = getUrlFromSearch(window.location.search); - $url.val(url); - smartDraw(url); + elements.url.val(url); + smartDraw(url, elements.url); } bind(); - $outputFormat - .add($showFiles) - .add($matching) - .add($wordsThreshold) - .add($matchingMaxComparisons) - .change(function(e) { - console.log(""); - console.log(e); - console.log(""); - smartDraw(null, true); - }); - - function getUrlFromSearch(search) { - try { - return search - .split("?")[1] - .split(searchParam + "=")[1] - .split("&")[0]; - } catch (_ignore) {} - - return null; - } - - function getParamsFromSearch(search) { - const map = {}; - try { - search - .split("?")[1] - .split("&") - .map(function(e) { - const values = e.split("="); - map[values[0]] = values[1]; - }); - } catch (_ignore) {} - - return map; - } - - function bind() { - $("#url-btn").click(function(e) { - e.preventDefault(); - const url = $url.val(); - smartDraw(url); - }); - - $url.on("paste", function(e) { - const url = e.originalEvent.clipboardData.getData("Text"); - smartDraw(url); - }); - } - - function prepareUrl(url) { - let fetchUrl; - const headers = new Headers(); - - const githubCommitUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; - const githubPrUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/pull\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; - - const gitlabCommitUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; - const gitlabPrUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/merge_requests\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; - - const bitbucketCommitUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/commits\/(.*?)(?:\/raw)?(?:\/.*)?$/; - const bitbucketPrUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/pull-requests\/(.*?)(?:\/.*)?$/; - - function gitLabUrlGen(userName, projectName, type, value) { - return ( - "https://crossorigin.me/https://gitlab.com/" + userName + "/" + projectName + "/" + type + "/" + value + ".diff" - ); - } - - function gitHubUrlGen(userName, projectName, type, value) { - headers.append("Accept", "application/vnd.github.v3.diff"); - return "https://api.github.com/repos/" + userName + "/" + projectName + "/" + type + "/" + value; - } - - function bitbucketUrlGen(userName, projectName, type, value) { - const baseUrl = "https://bitbucket.org/api/2.0/repositories/"; - if (type === "pullrequests") { - return baseUrl + userName + "/" + projectName + "/pullrequests/" + value + "/diff"; - } - return baseUrl + userName + "/" + projectName + "/diff/" + value; - } - - let values; - if ((values = githubCommitUrl.exec(url))) { - fetchUrl = gitHubUrlGen(values[1], values[2], "commits", values[3]); - } else if ((values = githubPrUrl.exec(url))) { - fetchUrl = gitHubUrlGen(values[1], values[2], "pulls", values[3]); - } else if ((values = gitlabCommitUrl.exec(url))) { - fetchUrl = gitLabUrlGen(values[1], values[2], "commit", values[3]); - } else if ((values = gitlabPrUrl.exec(url))) { - fetchUrl = gitLabUrlGen(values[1], values[2], "merge_requests", values[3]); - } else if ((values = bitbucketCommitUrl.exec(url))) { - fetchUrl = bitbucketUrlGen(values[1], values[2], "commit", values[3]); - } else if ((values = bitbucketPrUrl.exec(url))) { - fetchUrl = bitbucketUrlGen(values[1], values[2], "pullrequests", values[3]); - } else { - console.info("Could not parse url, using the provided url."); - fetchUrl = "https://crossorigin.me/" + url; - } - - return { - originalUrl: url, - url: fetchUrl, - headers: headers - }; - } - - function smartDraw(urlOpt, forced) { - const url = urlOpt || $url.val(); - const req = prepareUrl(url); - draw(req, forced); - } - - function draw(req, forced) { - if (!validateUrl(req.url)) { - console.error("Invalid url provided!"); - return; - } - - if (validateUrl(req.originalUrl)) updateUrl(req.originalUrl); - - const outputFormat = $outputFormat.val(); - const showFiles = $showFiles.is(":checked"); - const matching = $matching.val(); - const wordsThreshold = $wordsThreshold.val(); - const matchingMaxComparisons = $matchingMaxComparisons.val(); - - fetch(req.url, { - method: "GET", - headers: req.headers, - mode: "cors", - cache: "default" - }) - .then(function(res) { - return res.text(); - }) - .then(function(data) { - const container = "#url-diff-container"; - const diff2htmlUi = new Diff2HtmlUI({ diff: data }); - - if (outputFormat === "side-by-side") { - $container.css({ width: "100%" }); - } else { - $container.css({ width: "" }); - } - - const params = getParamsFromSearch(window.location.search); - delete params[searchParam]; - - if (forced) { - params.outputFormat = outputFormat; - params.showFiles = showFiles; - params.matching = matching; - params.wordsThreshold = wordsThreshold; - params.matchingMaxComparisons = matchingMaxComparisons; - } else { - params.outputFormat = params.outputFormat || outputFormat; - params.showFiles = String(params.showFiles) !== "false" || (params.showFiles === null && showFiles); - params.matching = params.matching || matching; - params.wordsThreshold = params.wordsThreshold || wordsThreshold; - params.matchingMaxComparisons = params.matchingMaxComparisons || matchingMaxComparisons; - - $outputFormat.val(params.outputFormat); - $showFiles.prop("checked", params.showFiles); - $matching.val(params.matching); - $wordsThreshold.val(params.wordsThreshold); - $matchingMaxComparisons.val(params.matchingMaxComparisons); - } - - params.synchronisedScroll = params.synchronisedScroll || true; - - diff2htmlUi.draw(container, params); - diff2htmlUi.fileListCloseable(container, params.fileListCloseable || false); - if (params.highlight === undefined || params.highlight) { - diff2htmlUi.highlightCode(container); - } - }); - } - - function validateUrl(url) { - return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( - url - ); - } - - function updateUrl(url) { - const params = getParamsFromSearch(window.location.search); - - if (params[searchParam] === url) return; - - params[searchParam] = url; - - const paramString = Object.keys(params) - .map(function(k) { - return k + "=" + params[k]; - }) - .join("&"); - - window.location = "demo.html?" + paramString; - } + elements.outputFormat + .add(elements.showFiles) + .add(elements.matching) + .add(elements.wordsThreshold) + .add(elements.matchingMaxComparisons) + .change(() => smartDraw(null, elements.url, true)); }); + +/* eslint-enable @typescript-eslint/explicit-function-return-type */ diff --git a/yarn.lock b/yarn.lock index fb4a42c..3a1c9cb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,17 +10,17 @@ "@babel/highlight" "^7.0.0" "@babel/core@^7.1.0": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.2.tgz#069a776e8d5e9eefff76236bc8845566bd31dd91" - integrity sha512-l8zto/fuoZIbncm+01p8zPSDZu/VuuJhAfA7d/AbzM09WR7iVhavvfNDYCNpo1VvLk6E6xgAoP9P+/EMJHuRkQ== + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.6.4.tgz#6ebd9fe00925f6c3e177bb726a188b5f578088ff" + integrity sha512-Rm0HGw101GY8FTzpWSyRbki/jzq+/PkNQJ+nSulrdY6gFGOsNseCqD6KHRYe2E+EdzuBdr2pxCp6s4Uk6eJ+XQ== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" + "@babel/generator" "^7.6.4" "@babel/helpers" "^7.6.2" - "@babel/parser" "^7.6.2" + "@babel/parser" "^7.6.4" "@babel/template" "^7.6.0" - "@babel/traverse" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/traverse" "^7.6.3" + "@babel/types" "^7.6.3" convert-source-map "^1.1.0" debug "^4.1.0" json5 "^2.1.0" @@ -29,12 +29,12 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.2.tgz#dac8a3c2df118334c2a29ff3446da1636a8f8c03" - integrity sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ== +"@babel/generator@^7.4.0", "@babel/generator@^7.6.3", "@babel/generator@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.6.4.tgz#a4f8437287bf9671b07f483b76e3bb731bc97671" + integrity sha512-jsBuXkFoZxk0yWLyGI9llT9oiQ2FeTASmRFE32U+aaDTfoE92t78eroO7PTpU/OrYq38hlcDM6vbfLDaOLy+7w== dependencies: - "@babel/types" "^7.6.0" + "@babel/types" "^7.6.3" jsesc "^2.5.1" lodash "^4.17.13" source-map "^0.5.0" @@ -85,10 +85,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.2.tgz#205e9c95e16ba3b8b96090677a67c9d6075b70a1" - integrity sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg== +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.6.0", "@babel/parser@^7.6.3", "@babel/parser@^7.6.4": + version "7.6.4" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.6.4.tgz#cb9b36a7482110282d5cb6dd424ec9262b473d81" + integrity sha512-D8RHPW5qd0Vbyo3qb+YjO5nvUVRTXFLQ/FsDxJU2Nqz4uB5EnUN0ZQSEYpvTIbRuttig1XbHWU5oMeQwQSAA+A== "@babel/plugin-syntax-object-rest-spread@^7.0.0": version "7.2.0" @@ -106,25 +106,25 @@ "@babel/parser" "^7.6.0" "@babel/types" "^7.6.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.2": - version "7.6.2" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.2.tgz#b0e2bfd401d339ce0e6c05690206d1e11502ce2c" - integrity sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ== +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.6.2", "@babel/traverse@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.6.3.tgz#66d7dba146b086703c0fb10dd588b7364cec47f9" + integrity sha512-unn7P4LGsijIxaAJo/wpoU11zN+2IaClkQAxcJWBNCMS6cmVh802IyLHNkAjQ0iYnRS3nnxk5O3fuXW28IMxTw== dependencies: "@babel/code-frame" "^7.5.5" - "@babel/generator" "^7.6.2" + "@babel/generator" "^7.6.3" "@babel/helper-function-name" "^7.1.0" "@babel/helper-split-export-declaration" "^7.4.4" - "@babel/parser" "^7.6.2" - "@babel/types" "^7.6.0" + "@babel/parser" "^7.6.3" + "@babel/types" "^7.6.3" debug "^4.1.0" globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0": - version "7.6.1" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.1.tgz#53abf3308add3ac2a2884d539151c57c4b3ac648" - integrity sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g== +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.6.0", "@babel/types@^7.6.3": + version "7.6.3" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.6.3.tgz#3f07d96f854f98e2fbd45c64b0cb942d11e8ba09" + integrity sha512-CqbcpTxMcpuQTMhjI37ZHVgjBkysg5icREQIEZ0eG1yCNwg3oy+5AaLiOKmjsCj6nqOsa6Hf0ObjRVwokb7srA== dependencies: esutils "^2.0.2" lodash "^4.17.13" @@ -332,6 +332,11 @@ dependencies: "@babel/types" "^7.3.0" +"@types/diff@4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/diff/-/diff-4.0.2.tgz#2e9bb89f9acc3ab0108f0f3dc4dbdcf2fff8a99c" + integrity sha512-mIenTfsIe586/yzsyfql69KRnA75S8SVXQbTLpDejRrjH0QSJcpu3AUOi/Vjnt9IOsXKxPhJfGpQUNMueIU1fQ== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -351,10 +356,10 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/hogan.js@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.0.tgz#bf26560f39a38224ab6d0491b06f72c8fbe0953d" - integrity sha512-djkvb/AN43c3lIGCojNQ1FBS9VqqKhcTns5RQnHw4xBT/csy0jAssAsOiJ8NfaaioZaeKYE7XkVRxE5NeSZcaA== +"@types/highlight.js@9.12.3": + version "9.12.3" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.12.3.tgz#b672cfaac25cbbc634a0fd92c515f66faa18dbca" + integrity sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ== "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" @@ -398,19 +403,24 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/mkdirp@^0.5.2": +"@types/mkdirp@0.5.2": version "0.5.2" resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^12.7.2": - version "12.7.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.11.tgz#be879b52031cfb5d295b047f5462d8ef1a716446" - integrity sha512-Otxmr2rrZLKRYIybtdG/sgeO+tHY20GxeDjcGmUnmmlCWyEnv2a2x1ZXBo3BTec4OiTXMQCiazB8NMBf0iRlFw== +"@types/node@*": + version "12.7.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.12.tgz#7c6c571cc2f3f3ac4a59a5f2bd48f5bdbc8653cc" + integrity sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ== -"@types/nopt@^3.0.29": +"@types/node@12.7.2": + version "12.7.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44" + integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg== + +"@types/nopt@3.0.29": version "3.0.29" resolved "https://registry.yarnpkg.com/@types/nopt/-/nopt-3.0.29.tgz#f19df3db4c97ee1459a2740028320a71d70964ce" integrity sha1-8Z3z20yX7hRZonQAKDIKcdcJZM4= @@ -726,18 +736,18 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== -autoprefixer@^9.6.0: - version "9.6.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.4.tgz#e6453be47af316b2923eaeaed87860f52ad4b7eb" - integrity sha512-Koz2cJU9dKOxG8P1f8uVaBntOv9lP4yz9ffWvWaicv9gHBPhpQB22nGijwd8gqW9CNT+UdkbQOQNLVI8jN1ZfQ== +autoprefixer@9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.0.tgz#0111c6bde2ad20c6f17995a33fad7cf6854b4c87" + integrity sha512-kuip9YilBqhirhHEGHaBTZKXL//xxGnzvsD0FtBQa6z+A69qZD6s/BAX9VzDF1i9VKDquTJDQaPLSEhOnL6FvQ== dependencies: - browserslist "^4.7.0" - caniuse-lite "^1.0.30000998" + browserslist "^4.6.1" + caniuse-lite "^1.0.30000971" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.18" - postcss-value-parser "^4.0.2" + postcss "^7.0.16" + postcss-value-parser "^3.3.1" aws-sign2@~0.7.0: version "0.7.0" @@ -949,7 +959,7 @@ browserify-zlib@~0.2.0: dependencies: pako "~1.0.5" -browserify@^16.5.0: +browserify@16.5.0: version "16.5.0" resolved "https://registry.yarnpkg.com/browserify/-/browserify-16.5.0.tgz#a1c2bc0431bec11fd29151941582e3f645ede881" integrity sha512-6bfI3cl76YLAnCZ75AGu/XPOsqUhRyc0F/olGIJeCxtfxF2HvPKEcmjU9M8oAPxl4uBY1U7Nry33Q6koV3f2iw== @@ -1003,7 +1013,7 @@ browserify@^16.5.0: vm-browserify "^1.0.0" xtend "^4.0.0" -browserslist@^4.7.0: +browserslist@^4.6.1: version "4.7.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.7.0.tgz#9ee89225ffc07db03409f2fee524dc8227458a17" integrity sha512-9rGNDtnj+HaahxiVV38Gn8n8Lr8REKsel68v1sPFfIGEK6uSXTY3h9acgiT1dZVtOOUtifo/Dn8daDQ5dUgVsA== @@ -1108,7 +1118,7 @@ camelcase@^5.0.0, camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -caniuse-lite@^1.0.30000989, caniuse-lite@^1.0.30000998: +caniuse-lite@^1.0.30000971, caniuse-lite@^1.0.30000989: version "1.0.30000999" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000999.tgz#427253a69ad7bea4aa8d8345687b8eec51ca0e43" integrity sha512-1CUyKyecPeksKwXZvYw0tEoaMCo/RwBlXmEtN5vVnabvO0KPd9RQLcaAuR9/1F+KDMv6esmOFWlsXuzDk+8rxg== @@ -1186,7 +1196,7 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -clean-css-cli@^4.3.0: +clean-css-cli@4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/clean-css-cli/-/clean-css-cli-4.3.0.tgz#8502aa86d1879e5b111af51b3c2abb799e0684ce" integrity sha512-8GHZfr+mG3zB/Lgqrr27qHBFsPSn0fyEI3f2rIZpxPxUbn2J6A8xyyeBRVTW8duDuXigN0s80vsXiXJOEFIO5Q== @@ -1237,7 +1247,7 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -codacy-coverage@^3.4.0: +codacy-coverage@3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/codacy-coverage/-/codacy-coverage-3.4.0.tgz#196af70844c4e4179718f7a7f9d96b921b4b3a67" integrity sha512-A0ats3/gZtOw76muu++HZ6QrInztWjjLefkLJmmBpjPfyn6nNwNLoApmGmj3F3dfgl2+o6u5GwPnUBkKdfKXTQ== @@ -1299,10 +1309,15 @@ commander@2.15.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== -commander@2.x, commander@^2.20.0, commander@^2.x, commander@~2.20.0: - version "2.20.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.1.tgz#3863ce3ca92d0831dcf2a102f5fb4b5926afd0f9" - integrity sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg== +commander@2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +commander@2.x, commander@^2.20.0, commander@^2.x: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== component-emitter@^1.2.1: version "1.3.0" @@ -1619,7 +1634,7 @@ diff@3.5.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== -diff@^4.0.1: +diff@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== @@ -1683,9 +1698,9 @@ ecc-jsbn@~0.1.1: safer-buffer "^2.1.0" electron-to-chromium@^1.3.247: - version "1.3.275" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.275.tgz#19a38436e34216f51820fa2f4326d5ce141fa36f" - integrity sha512-/YWtW/VapMnuYA1lNOaa1F4GhR1LBf+CUTp60lzDPEEh0XOzyOAyULyYZVF9vziZ3qSbTqCwmKwsyRXp66STbw== + version "1.3.281" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.281.tgz#2dbeb9f0bdffddb1662f9ca00d26c49d31dc0f7e" + integrity sha512-oxXKngPjTWRmXFy4vV9FeAkPl7wU4xMejfOY+HXjGrj4T0z9l96loWWVDLJEtbT/aPKOWKrSz6xoYxd+YJ/gJA== elliptic@^6.0.0: version "6.5.1" @@ -1845,12 +1860,12 @@ eslint-plugin-prettier@3.1.0: dependencies: prettier-linter-helpers "^1.0.0" -eslint-plugin-promise@^4.2.1: +eslint-plugin-promise@4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== -eslint-plugin-standard@^4.0.1: +eslint-plugin-standard@4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.1.tgz#ff0519f7ffaff114f76d1bd7c3996eef0f6e20b4" integrity sha512-v/KBnfyaOMPmZc/dmc6ozOdWqekGp7bBGq4jLAecEfPGmfKiWS4sA8sC0LqiV9w5qmXAtXVn4M3p1jSyhY85SQ== @@ -2105,7 +2120,7 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-html-parser@^1.0.1: +fast-html-parser@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/fast-html-parser/-/fast-html-parser-1.0.1.tgz#4ecc9683b8bb79afe11a50807b7853e79256cea2" integrity sha1-TsyWg7i7ea/hGlCAe3hT55JWzqI= @@ -2379,9 +2394,9 @@ growly@^1.3.0: integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= handlebars@^4.1.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.2.tgz#8810a9821a9d6d52cb2f57d326d6ce7c3dfe741d" - integrity sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg== + version "4.4.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.3.tgz#180bae52c1d0e9ec0c15d7e82a4362d662762f6e" + integrity sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -2476,6 +2491,11 @@ he@1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= +highlight.js@9.15.10: + version "9.15.10" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.15.10.tgz#7b18ed75c90348c045eef9ed08ca1319a2219ad2" + integrity sha512-RoV7OkQm0T3os3Dd2VHLNMoaoDVx77Wygln3n9l5YV172XonWG6rgQD3XnF/BuFFZw9A0TJgmMSO8FEWQgvcXw== + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -2495,7 +2515,7 @@ hoek@6.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== -hogan.js@^3.0.2: +hogan.js@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd" integrity sha1-TNnhq9QpQUbnZ55B14mHMrAse/0= @@ -2504,9 +2524,9 @@ hogan.js@^3.0.2: nopt "1.0.10" hosted-git-info@^2.1.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" - integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== html-encoding-sniffer@^1.0.2: version "1.0.2" @@ -2547,9 +2567,9 @@ ieee754@^1.1.4: integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== ignore-walk@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.2.tgz#99d83a246c196ea5c93ef9315ad7b0819c35069b" - integrity sha512-EXyErtpHbn75ZTsOADsfx6J/FPo6/5cjev46PXrcTpd8z3BoRkXgYu9/JVqrI7tusjmwCZutGeRJeU0Wo1e4Cw== + version "3.0.3" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" + integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== dependencies: minimatch "^3.0.4" @@ -3548,6 +3568,11 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.memoize@~3.0.3: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-3.0.4.tgz#2dcbd2c287cbc0a55cc42328bd0c736150d53e3f" @@ -3654,11 +3679,6 @@ merge2@^1.2.3: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== -merge@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" - integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== - micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -3912,9 +3932,9 @@ node-pre-gyp@^0.12.0: tar "^4" node-releases@^1.1.29: - version "1.1.34" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.34.tgz#ced4655ee1ba9c3a2c5dcbac385e19434155fd40" - integrity sha512-fNn12JTEfniTuCqo0r9jXgl44+KxRH/huV7zM/KAGOKxDKrHr6EbT7SSs4B+DNxyBE2mks28AD+Jw6PkfY5uwA== + version "1.1.35" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.35.tgz#32a74a3cd497aa77f23d509f483475fd160e4c48" + integrity sha512-JGcM/wndCN/2elJlU0IGdVEJQQnJwsLbgPCFd2pY7V0mxf17bZ0Gb/lgOtL29ZQhvEX5shnVhxQyZz3ex94N8w== dependencies: semver "^6.3.0" @@ -3925,7 +3945,7 @@ nopt@1.0.10: dependencies: abbrev "1" -nopt@^4.0.1: +nopt@4.0.1, nopt@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= @@ -3966,9 +3986,9 @@ npm-bundled@^1.0.1: integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== npm-packlist@^1.1.6: - version "1.4.4" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" - integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + version "1.4.6" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.6.tgz#53ba3ed11f8523079f1457376dd379ee4ea42ff4" + integrity sha512-u65uQdb+qwtGvEJh/DgQgW1Xg7sqeNbmxYyrvlNznaVTjV3E5P6F/EFjM+BVHXl7JJlsdG8A64M0XI8FI/IOlg== dependencies: ignore-walk "^3.0.1" npm-bundled "^1.0.1" @@ -4364,7 +4384,7 @@ posix-character-classes@^0.1.0: resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= -postcss-cli@^6.1.3: +postcss-cli@6.1.3: version "6.1.3" resolved "https://registry.yarnpkg.com/postcss-cli/-/postcss-cli-6.1.3.tgz#a9eec3e9cde4aaa90170546baf706f8af6f8ecec" integrity sha512-eieqJU+OR1OFc/lQqMsDmROTJpoMZFvoAQ+82utBQ8/8qGMTfH9bBSPsTdsagYA8uvNzxHw2I2cNSSJkLAGhvw== @@ -4400,12 +4420,12 @@ postcss-reporter@^6.0.0: log-symbols "^2.2.0" postcss "^7.0.7" -postcss-value-parser@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9" - integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== +postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss@^7.0.0, postcss@^7.0.18, postcss@^7.0.7: +postcss@^7.0.0, postcss@^7.0.16, postcss@^7.0.7: version "7.0.18" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.18.tgz#4b9cda95ae6c069c67a4d933029eddd4838ac233" integrity sha512-/7g1QXXgegpF+9GJj4iN7ChGF40sYuGYJ8WZu8DZWnmhQ/G36hfdk3q9LBJmoK+lZ+yzZ5KYpOoxq7LF1BxE8g== @@ -5299,7 +5319,7 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.3" -terser@^4.3.8: +terser@4.3.8: version "4.3.8" resolved "https://registry.yarnpkg.com/terser/-/terser-4.3.8.tgz#707f05f3f4c1c70c840e626addfdb1c158a17136" integrity sha512-otmIRlRVmLChAWsnSFNO0Bfk6YySuBp6G9qrHiJwlLDd4mxe2ta4sjI7TzIR+W1nBMjilzrMcPOz9pSusgx3hQ== @@ -5420,15 +5440,16 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" -ts-jest@24.0.2: - version "24.0.2" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.0.2.tgz#8dde6cece97c31c03e80e474c749753ffd27194d" - integrity sha512-h6ZCZiA1EQgjczxq+uGLXQlNgeg02WWJBbeT8j6nyIBRQdglqbvzDoHahTEIiS6Eor6x8mK6PfZ7brQ9Q6tzHw== +ts-jest@24.1.0: + version "24.1.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.1.0.tgz#2eaa813271a2987b7e6c3fefbda196301c131734" + integrity sha512-HEGfrIEAZKfu1pkaxB9au17b1d9b56YZSqz5eCVE8mX68+5reOvlM93xGOzzCREIov9mdH7JBG+s0UyNAqr0tQ== dependencies: bs-logger "0.x" buffer-from "1.x" fast-json-stable-stringify "2.x" json5 "2.x" + lodash.memoize "4.x" make-error "1.x" mkdirp "0.x" resolve "1.x" @@ -5476,17 +5497,17 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.6.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" - integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== +typescript@3.6.4: + version "3.6.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d" + integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg== uglify-js@^3.1.4: - version "3.6.0" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" - integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + version "3.6.1" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.1.tgz#ae7688c50e1bdcf2f70a0e162410003cf9798311" + integrity sha512-+dSJLJpXBb6oMHP+Yvw8hUgElz4gLTh82XuX68QiJVTXaE5ibl6buzhNkQdYhBlIhozWOC9ge16wyRmjG4TwVQ== dependencies: - commander "~2.20.0" + commander "2.20.0" source-map "~0.6.1" umd@^3.0.0: @@ -5643,7 +5664,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: dependencies: iconv-lite "0.4.24" -whatwg-fetch@^3.0.0: +whatwg-fetch@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==