├── docs ├── assets ├── CNAME ├── robots.txt ├── favicon.ico ├── img │ ├── snapshot-1.png │ ├── snapshot-2.png │ └── snapshot-3.png ├── sitemap.xml ├── main.min.css └── demo.html ├── website ├── CNAME ├── robots.txt ├── favicon.ico ├── img │ ├── snapshot-1.png │ ├── snapshot-2.png │ └── snapshot-3.png ├── templates │ ├── pages │ │ ├── index │ │ │ └── index-scripts.partial.mustache │ │ └── demo │ │ │ ├── demo-assets.partial.mustache │ │ │ ├── demo-scripts.partial.mustache │ │ │ ├── demo.partial.mustache │ │ │ └── demo.js │ └── template.mustache ├── sitemap.xml └── main.css ├── src ├── templates │ ├── generic-wrapper.mustache │ ├── tag-file-added.mustache │ ├── tag-file-changed.mustache │ ├── tag-file-deleted.mustache │ ├── tag-file-renamed.mustache │ ├── line-by-line-numbers.mustache │ ├── generic-file-path.mustache │ ├── generic-empty-diff.mustache │ ├── generic-column-line-number.mustache │ ├── icon-file-deleted.mustache │ ├── icon-file.mustache │ ├── icon-file-added.mustache │ ├── icon-file-renamed.mustache │ ├── icon-file-changed.mustache │ ├── file-summary-wrapper.mustache │ ├── file-summary-line.mustache │ ├── generic-line.mustache │ ├── line-by-line-file-diff.mustache │ ├── side-by-side-file-diff.mustache │ └── diff2html-templates.js ├── utils.js ├── html-printer.js ├── file-list-printer.js ├── hoganjs-utils.js ├── diff2html.js ├── ui │ ├── js │ │ ├── highlight.js-internals.js │ │ └── diff2html-ui.js │ └── css │ │ └── diff2html.css ├── rematch.js ├── printer-utils.js ├── line-by-line-printer.js └── side-by-side-printer.js ├── .eslintignore ├── .gitignore ├── .editorconfig ├── scripts ├── update-bower-version.sh ├── release-website.sh ├── release.sh ├── release-website.js └── hulk.js ├── typescript ├── diff2html-tests.ts └── diff2html.d.ts ├── CREDITS.md ├── test ├── utils-tests.js ├── hogan-cache-tests.js ├── printer-utils-tests.js └── file-list-printer-tests.js ├── .github └── ISSUE_TEMPLATE.md ├── bower.json ├── circle.yml ├── LICENSE.md ├── package.json ├── CONTRIBUTING.md ├── dist ├── diff2html.min.css ├── diff2html-ui.min.js └── diff2html-ui.js ├── .eslintrc.json └── README.md /docs/assets: -------------------------------------------------------------------------------- 1 | ../dist -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | diff2html.xyz -------------------------------------------------------------------------------- /docs/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /website/CNAME: -------------------------------------------------------------------------------- 1 | diff2html.xyz -------------------------------------------------------------------------------- /website/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/docs/favicon.ico -------------------------------------------------------------------------------- /website/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/website/favicon.ico -------------------------------------------------------------------------------- /docs/img/snapshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/docs/img/snapshot-1.png -------------------------------------------------------------------------------- /docs/img/snapshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/docs/img/snapshot-2.png -------------------------------------------------------------------------------- /docs/img/snapshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/docs/img/snapshot-3.png -------------------------------------------------------------------------------- /src/templates/generic-wrapper.mustache: -------------------------------------------------------------------------------- 1 |
2 | {{{content}}} 3 |
4 | -------------------------------------------------------------------------------- /src/templates/tag-file-added.mustache: -------------------------------------------------------------------------------- 1 | ADDED 2 | -------------------------------------------------------------------------------- /src/templates/tag-file-changed.mustache: -------------------------------------------------------------------------------- 1 | CHANGED 2 | -------------------------------------------------------------------------------- /src/templates/tag-file-deleted.mustache: -------------------------------------------------------------------------------- 1 | DELETED 2 | -------------------------------------------------------------------------------- /src/templates/tag-file-renamed.mustache: -------------------------------------------------------------------------------- 1 | RENAMED 2 | -------------------------------------------------------------------------------- /website/img/snapshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/website/img/snapshot-1.png -------------------------------------------------------------------------------- /website/img/snapshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/website/img/snapshot-2.png -------------------------------------------------------------------------------- /website/img/snapshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hirokith/diff2html/master/website/img/snapshot-3.png -------------------------------------------------------------------------------- /src/templates/line-by-line-numbers.mustache: -------------------------------------------------------------------------------- 1 |
{{oldNumber}}
2 |
{{newNumber}}
3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Skip coverage and build folders 2 | coverage/** 3 | dist/** 4 | 5 | # Ignore symlink to build folder 6 | docs/** 7 | 8 | # Ignore HTML templates generated code 9 | src/** 10 | !src/*.js 11 | !src/ui/js/*.js 12 | -------------------------------------------------------------------------------- /src/templates/generic-file-path.mustache: -------------------------------------------------------------------------------- 1 | 2 | {{>fileIcon}} 3 | {{fileDiffName}} 4 | {{>fileTag}} 5 | 6 | -------------------------------------------------------------------------------- /src/templates/generic-empty-diff.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | File without changes 5 |
6 | 7 | 8 | -------------------------------------------------------------------------------- /website/templates/pages/index/index-scripts.partial.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/templates/generic-column-line-number.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
{{{blockHeader}}}
5 | 6 | 7 | -------------------------------------------------------------------------------- /website/templates/pages/demo/demo-assets.partial.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/templates/icon-file-deleted.mustache: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/templates/icon-file.mustache: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/templates/icon-file-added.mustache: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /src/templates/icon-file-renamed.mustache: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | 14 | # Maven 15 | log/ 16 | target/ 17 | 18 | # Node 19 | node_modules/ 20 | npm-debug.log 21 | yarn-error.log 22 | 23 | # Istanbul 24 | coverage/ 25 | 26 | # Bower 27 | bower_components/ 28 | -------------------------------------------------------------------------------- /src/templates/icon-file-changed.mustache: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | max_line_length = 120 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/templates/file-summary-wrapper.mustache: -------------------------------------------------------------------------------- 1 |
2 |
3 | Files changed ({{filesNumber}}) 4 | hide 5 | show 6 |
7 |
    8 | {{{files}}} 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/templates/file-summary-line.mustache: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | {{>fileIcon}} 4 | {{fileName}} 5 | 6 | {{addedLines}} 7 | {{deletedLines}} 8 | 9 | 10 |
  • 11 | -------------------------------------------------------------------------------- /src/templates/generic-line.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{{lineNumber}}} 4 | 5 | 6 |
    7 | {{#prefix}} 8 | {{{prefix}}} 9 | {{/prefix}} 10 | {{#content}} 11 | {{{content}}} 12 | {{/content}} 13 |
    14 | 15 | 16 | -------------------------------------------------------------------------------- /src/templates/line-by-line-file-diff.mustache: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{{filePath}}} 4 |
    5 |
    6 |
    7 | 8 | 9 | {{{diffs}}} 10 | 11 |
    12 |
    13 |
    14 |
    15 | -------------------------------------------------------------------------------- /website/templates/pages/demo/demo-scripts.partial.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /docs/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | https://diff2html.xyz/ 9 | 10 | 11 | https://diff2html.xyz/index.html 12 | 13 | 14 | https://diff2html.xyz/demo.html 15 | 16 | 17 | -------------------------------------------------------------------------------- /website/sitemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | https://diff2html.xyz/ 9 | 10 | 11 | https://diff2html.xyz/index.html 12 | 13 | 14 | https://diff2html.xyz/demo.html 15 | 16 | 17 | -------------------------------------------------------------------------------- /scripts/update-bower-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # diff2html update bower version 5 | # by rtfpessoa 6 | # 7 | 8 | set -e 9 | 10 | echo "diff2html updating bower version..." 11 | 12 | if [[ "$OSTYPE" == "linux-gnu" ]]; then 13 | SED_BIN=sed 14 | elif [[ "$OSTYPE" == "darwin"* ]]; then 15 | SED_BIN=gsed 16 | else 17 | echo "Cannot run this script in ${OSTYPE}" 18 | exit 1 19 | fi 20 | 21 | RELEASE_VERSION=$(cat package.json | grep "version" | head -1 | $SED_BIN -e 's/ "version": "\(.*\)",/\1/') 22 | 23 | $SED_BIN -i 's/.*"version".*/ "version": "'${RELEASE_VERSION}'",/' bower.json 24 | 25 | echo "diff2html updated bower version successfully!" 26 | -------------------------------------------------------------------------------- /typescript/diff2html-tests.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import Diff2Html = require('diff2html'); 4 | 5 | let d2h = Diff2Html.Diff2Html; 6 | 7 | class Diff2HtmlOptionsImpl implements Diff2Html.Options { 8 | constructor(public inputFormat: string) { 9 | } 10 | } 11 | 12 | let strInput = 13 | 'diff --git a/sample b/sample\n' + 14 | 'index 0000001..0ddf2ba\n' + 15 | '--- a/sample\n' + 16 | '+++ b/sample\n' + 17 | '@@ -1 +1 @@\n' + 18 | '-test\n' + 19 | '+test1r\n'; 20 | 21 | let strConfiguration = new Diff2HtmlOptionsImpl('diff'); 22 | let diffInput = d2h.getJsonFromDiff(strInput, strConfiguration); 23 | 24 | let diffConfiguration = new Diff2HtmlOptionsImpl('json'); 25 | let htmlString = d2h.getPrettyHtml(diffInput, diffConfiguration); 26 | console.log(htmlString); 27 | -------------------------------------------------------------------------------- /CREDITS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | This is the list of all the kind people that have contributed to the diff2html project. 4 | This list is ordered by first contribution. 5 | 6 | Thanks, 7 | @rtfpessoa 8 | 9 | ---------- 10 | 11 | Rodrigo Fernandes, [@rtfpessoa](https://github.com/rtfpessoa) 12 | 13 | JK Kim, [@codingtwinky](https://github.com/codingtwinky) 14 | 15 | Paulo Bu, [@pbu88](https://github.com/pbu88) 16 | 17 | Nuno Teixeira, [@nmatpt](https://github.com/nmatpt) 18 | 19 | Mikko Rantanen, [@Rantanen](https://github.com/Rantanen) 20 | 21 | Wolfgang Illmeyer, [@escitalopram](https://github.com/escitalopram) 22 | 23 | Jameskmonger, [@Jameskmonger](https://github.com/Jameskmonger) 24 | 25 | Rafael Cortês, [@mrfyda](https://github.com/mrfyda) 26 | 27 | Ivan Vorontsov, [@lantian](https://github.com/lantian) 28 | -------------------------------------------------------------------------------- /test/utils-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var Utils = require('../src/utils.js').Utils; 4 | 5 | describe('Utils', function() { 6 | describe('escape', function() { 7 | it('should escape & with &', function() { 8 | var result = Utils.escape('&'); 9 | assert.equal('&', result); 10 | }); 11 | it('should escape < with <', function() { 12 | var result = Utils.escape('<'); 13 | assert.equal('<', result); 14 | }); 15 | it('should escape > with >', function() { 16 | var result = Utils.escape('>'); 17 | assert.equal('>', result); 18 | }); 19 | it('should escape a string with multiple problematic characters', function() { 20 | var result = Utils.escape('\tlink text'); 21 | var expected = '<a href="#"> link text</a>'; 22 | assert.equal(expected, result); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/templates/side-by-side-file-diff.mustache: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | {{{filePath}}} 4 |
    5 |
    6 |
    7 |
    8 | 9 | 10 | {{{diffs.left}}} 11 | 12 |
    13 |
    14 |
    15 |
    16 |
    17 | 18 | 19 | {{{diffs.right}}} 20 | 21 |
    22 |
    23 |
    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Utils (utils.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | function Utils() { 10 | } 11 | 12 | Utils.prototype.escape = function(str) { 13 | return str.slice(0) 14 | .replace(/&/g, '&') 15 | .replace(//g, '>') 17 | .replace(/"/g, '"') 18 | .replace(/'/g, ''') 19 | .replace(/\//g, '/') 20 | .replace(/\t/g, ' '); 21 | }; 22 | 23 | Utils.prototype.startsWith = function(str, start) { 24 | if (typeof start === 'object') { 25 | var result = false; 26 | start.forEach(function(s) { 27 | if (str.indexOf(s) === 0) { 28 | result = true; 29 | } 30 | }); 31 | 32 | return result; 33 | } 34 | 35 | return str && str.indexOf(start) === 0; 36 | }; 37 | 38 | Utils.prototype.valueOrEmpty = function(value) { 39 | return value || ''; 40 | }; 41 | 42 | module.exports.Utils = new Utils(); 43 | })(); 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Step -1: Before filling an issue check out troubleshooting section 2 | 3 | * Go to [README.md#Troubleshooting](https://github.com/rtfpessoa/diff2html#troubleshooting) 4 | 5 | ### Step 0: Describe your environment 6 | 7 | * OS: _____ 8 | * diff2html version: _____ 9 | * Using diff2html directly or using diff2html-ui helper: _____ 10 | * Extra flags: _____ 11 | 12 | ### Step 1: Describe the problem: 13 | 14 | #### Steps to reproduce: 15 | 16 | 1. _____ 17 | 2. _____ 18 | 3. _____ 19 | 20 | #### diff example: 21 | 22 | ```diff 23 | diff --git describe.c 24 | index fabadb8,cc95eb0..4866510 25 | --- a/describe.c 26 | +++ b/describe.c 27 | @@@ -98,20 -98,12 +98,20 @@@ 28 | return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1; 29 | } 30 | ``` 31 | 32 | #### Observed Results: 33 | 34 | * What happened? This could be a description, log output, etc. 35 | 36 | #### Expected Results: 37 | 38 | * What did you expect to happen? 39 | 40 | #### Relevant Code: 41 | 42 | ``` 43 | // TODO(you): code here to reproduce the problem 44 | ``` 45 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff2html", 3 | "version": "2.3.0", 4 | "homepage": "https://diff2html.xyz", 5 | "description": "Fast Diff to colorized HTML", 6 | "keywords": [ 7 | "git", 8 | "diff", 9 | "pretty", 10 | "side", 11 | "line", 12 | "side-by-side", 13 | "line-by-line", 14 | "character", 15 | "highlight", 16 | "pretty", 17 | "color", 18 | "html", 19 | "diff2html", 20 | "difftohtml", 21 | "colorized" 22 | ], 23 | "authors": [ 24 | "Rodrigo Fernandes " 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/rtfpessoa/diff2html.git" 29 | }, 30 | "main": [ 31 | "./dist/diff2html.js", 32 | "./dist/diff2html.css" 33 | ], 34 | "license": "MIT", 35 | "moduleType": [ 36 | "globals", 37 | "node" 38 | ], 39 | "ignore": [ 40 | "**/.*", 41 | "docs", 42 | "scripts", 43 | "src", 44 | "test", 45 | "typescript", 46 | "circle.yml", 47 | "CONTRIBUTING.md", 48 | "CREDITS.md", 49 | "npm-shrinkwrap.json", 50 | "package.json" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 7 4 | dependencies: 5 | cache_directories: 6 | - ~/nvm 7 | - ~/.npm 8 | override: 9 | - git clean -dfx 10 | test: 11 | override: 12 | - echo "Running tests with node 7.x" 13 | - git clean -dfx 14 | - nvm install 7 && nvm use 7 && nvm alias default 7 15 | - npm install 16 | - npm run lint 17 | - npm run test 18 | - npm run codacy 19 | - echo "Running tests with node 6.x" 20 | - git clean -dfx 21 | - nvm install 6 && nvm use 6 && nvm alias default 6 22 | - npm install 23 | - npm run test 24 | - echo "Running tests with node 5.x" 25 | - git clean -dfx 26 | - nvm install 5 && nvm use 5 && nvm alias default 5 27 | - npm install 28 | - npm run test 29 | - echo "Running tests with node 4.x" 30 | - git clean -dfx 31 | - nvm install 4 && nvm use 4 && nvm alias default 4 32 | - npm install 33 | - npm run test 34 | - echo "Running tests with node 0.12.x" 35 | - git clean -dfx 36 | - nvm install 0.12 && nvm use 0.12 && nvm alias default 0.12 37 | - npm install 38 | - npm run test 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2014-2016 Rodrigo Fernandes https://rtfpessoa.github.io/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/html-printer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HtmlPrinter (html-printer.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var LineByLinePrinter = require('./line-by-line-printer.js').LineByLinePrinter; 10 | var SideBySidePrinter = require('./side-by-side-printer.js').SideBySidePrinter; 11 | var FileListPrinter = require('./file-list-printer.js').FileListPrinter; 12 | 13 | function HtmlPrinter() { 14 | } 15 | 16 | HtmlPrinter.prototype.generateLineByLineJsonHtml = function(diffFiles, config) { 17 | var lineByLinePrinter = new LineByLinePrinter(config); 18 | return lineByLinePrinter.generateLineByLineJsonHtml(diffFiles); 19 | }; 20 | 21 | HtmlPrinter.prototype.generateSideBySideJsonHtml = function(diffFiles, config) { 22 | var sideBySidePrinter = new SideBySidePrinter(config); 23 | return sideBySidePrinter.generateSideBySideJsonHtml(diffFiles); 24 | }; 25 | 26 | HtmlPrinter.prototype.generateFileListSummary = function(diffJson, config) { 27 | var fileListPrinter = new FileListPrinter(config); 28 | return fileListPrinter.generateFileList(diffJson); 29 | }; 30 | 31 | module.exports.HtmlPrinter = new HtmlPrinter(); 32 | })(); 33 | -------------------------------------------------------------------------------- /src/file-list-printer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * FileListPrinter (file-list-printer.js) 4 | * Author: nmatpt 5 | * 6 | */ 7 | 8 | (function() { 9 | var printerUtils = require('./printer-utils.js').PrinterUtils; 10 | 11 | var hoganUtils; 12 | 13 | var baseTemplatesPath = 'file-summary'; 14 | var iconsBaseTemplatesPath = 'icon'; 15 | 16 | function FileListPrinter(config) { 17 | this.config = config; 18 | 19 | var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils; 20 | hoganUtils = new HoganJsUtils(config); 21 | } 22 | 23 | FileListPrinter.prototype.generateFileList = function(diffFiles) { 24 | var lineTemplate = hoganUtils.template(baseTemplatesPath, 'line'); 25 | 26 | var files = diffFiles.map(function(file) { 27 | var fileTypeName = printerUtils.getFileTypeIcon(file); 28 | var iconTemplate = hoganUtils.template(iconsBaseTemplatesPath, fileTypeName); 29 | 30 | return lineTemplate.render({ 31 | fileHtmlId: printerUtils.getHtmlId(file), 32 | fileName: printerUtils.getDiffName(file), 33 | deletedLines: '-' + file.deletedLines, 34 | addedLines: '+' + file.addedLines 35 | }, { 36 | fileIcon: iconTemplate 37 | }); 38 | }).join('\n'); 39 | 40 | return hoganUtils.render(baseTemplatesPath, 'wrapper', { 41 | filesNumber: diffFiles.length, 42 | files: files 43 | }); 44 | }; 45 | 46 | module.exports.FileListPrinter = FileListPrinter; 47 | })(); 48 | -------------------------------------------------------------------------------- /scripts/release-website.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # diff2html website release script 5 | # by rtfpessoa 6 | # 7 | 8 | set -e 9 | 10 | INPUT_DIR=website 11 | INPUT_DEMO_JS=${INPUT_DIR}/templates/pages/demo/demo.js 12 | INPUT_CSS_FILE=${INPUT_DIR}/main.css 13 | 14 | OUTPUT_DIR=docs 15 | OUTPUT_DEMO_JS=${OUTPUT_DIR}/demo.js 16 | OUTPUT_DEMO_MIN_JS=${OUTPUT_DIR}/demo.min.js 17 | OUTPUT_CSS_FILE=${OUTPUT_DIR}/main.css 18 | OUTPUT_MIN_CSS_FILE=${OUTPUT_DIR}/main.min.css 19 | 20 | echo "Creating diff2html website release ..." 21 | 22 | echo "Cleaning previous versions ..." 23 | rm -rf ${OUTPUT_DIR} 24 | mkdir -p ${OUTPUT_DIR} 25 | 26 | echo "Minifying ${OUTPUT_CSS_FILE} to ${OUTPUT_MIN_CSS_FILE}" 27 | postcss --use autoprefixer -o ${OUTPUT_CSS_FILE} ${INPUT_CSS_FILE} 28 | cleancss --advanced --compatibility=ie8 -o ${OUTPUT_MIN_CSS_FILE} ${OUTPUT_CSS_FILE} 29 | 30 | echo "Generating website js aggregation file in ${OUTPUT_DEMO_JS}" 31 | browserify -e ${INPUT_DEMO_JS} -o ${OUTPUT_DEMO_JS} 32 | 33 | echo "Minifying ${OUTPUT_DEMO_JS} to ${OUTPUT_DEMO_MIN_JS}" 34 | uglifyjs ${OUTPUT_DEMO_JS} -c -o ${OUTPUT_DEMO_MIN_JS} 35 | 36 | echo "Generating HTMLs from templates ..." 37 | node ./scripts/release-website.js 38 | 39 | echo "Copying static files ..." 40 | cp -rf ${INPUT_DIR}/img ${OUTPUT_DIR}/ 41 | cp -f ${INPUT_DIR}/CNAME ${OUTPUT_DIR}/ 42 | cp -f ${INPUT_DIR}/favicon.ico ${OUTPUT_DIR}/ 43 | cp -f ${INPUT_DIR}/robots.txt ${OUTPUT_DIR}/ 44 | cp -f ${INPUT_DIR}/sitemap.xml ${OUTPUT_DIR}/ 45 | 46 | echo "Creating diff2html assets symlink ..." 47 | ln -s ../dist docs/assets 48 | 49 | echo "diff2html website release created successfully!" 50 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 4 | # diff2html release script 5 | # by rtfpessoa 6 | # 7 | 8 | set -e 9 | 10 | INPUT_DIR=src 11 | INTPUT_TEMPLATES_DIR=${INPUT_DIR}/templates 12 | INPUT_UI_DIR=${INPUT_DIR}/ui 13 | INPUT_JS_FILE=${INPUT_DIR}/diff2html.js 14 | INPUT_JS_UI_FILE=${INPUT_UI_DIR}/js/diff2html-ui.js 15 | INPUT_CSS_FILE=${INPUT_UI_DIR}/css/diff2html.css 16 | 17 | GENERATED_TEMPLATES_FILE=${INTPUT_TEMPLATES_DIR}/diff2html-templates.js 18 | 19 | OUTPUT_DIR=dist 20 | OUTPUT_JS_FILE=${OUTPUT_DIR}/diff2html.js 21 | OUTPUT_MIN_JS_FILE=${OUTPUT_DIR}/diff2html.min.js 22 | OUTPUT_JS_UI_FILE=${OUTPUT_DIR}/diff2html-ui.js 23 | OUTPUT_MIN_JS_UI_FILE=${OUTPUT_DIR}/diff2html-ui.min.js 24 | OUTPUT_CSS_FILE=${OUTPUT_DIR}/diff2html.css 25 | OUTPUT_MIN_CSS_FILE=${OUTPUT_DIR}/diff2html.min.css 26 | 27 | echo "Creating diff2html release ..." 28 | 29 | echo "Cleaning previous versions ..." 30 | rm -rf ${OUTPUT_DIR} 31 | mkdir -p ${OUTPUT_DIR} 32 | 33 | echo "Minifying ${OUTPUT_CSS_FILE} to ${OUTPUT_MIN_CSS_FILE}" 34 | postcss --use autoprefixer -o ${OUTPUT_CSS_FILE} ${INPUT_CSS_FILE} 35 | cleancss --advanced --compatibility=ie8 -o ${OUTPUT_MIN_CSS_FILE} ${OUTPUT_CSS_FILE} 36 | 37 | echo "Pre-compile hogan.js templates" 38 | npm run templates 39 | 40 | echo "Generating js aggregation file in ${OUTPUT_JS_FILE}" 41 | browserify -e ${INPUT_JS_FILE} -o ${OUTPUT_JS_FILE} 42 | 43 | echo "Minifying ${OUTPUT_JS_FILE} to ${OUTPUT_MIN_JS_FILE}" 44 | uglifyjs ${OUTPUT_JS_FILE} -c -o ${OUTPUT_MIN_JS_FILE} 45 | 46 | echo "Generating js ui aggregation file in ${OUTPUT_JS_UI_FILE}" 47 | browserify -e ${INPUT_JS_UI_FILE} -o ${OUTPUT_JS_UI_FILE} 48 | 49 | echo "Minifying ${OUTPUT_JS_UI_FILE} to ${OUTPUT_MIN_JS_UI_FILE}" 50 | uglifyjs ${OUTPUT_JS_UI_FILE} -c -o ${OUTPUT_MIN_JS_UI_FILE} 51 | 52 | echo "diff2html release created successfully!" 53 | -------------------------------------------------------------------------------- /typescript/diff2html.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for diff2html 2 | // Project: https://github.com/rtfpessoa/diff2html 3 | // Definitions by: rtfpessoa 4 | // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped 5 | 6 | declare namespace Diff2Html { 7 | 8 | export interface Options { 9 | inputFormat?: string; 10 | outputFormat?: string; 11 | showFiles?: boolean; 12 | matching?: string; 13 | synchronisedScroll?: boolean; 14 | matchWordsThreshold?: number; 15 | matchingMaxComparisons?: number; 16 | } 17 | 18 | export interface Line { 19 | content: string; 20 | type: string; 21 | oldNumber: number; 22 | newNumber: number; 23 | } 24 | 25 | export interface Block { 26 | oldStartLine: number; 27 | oldStartLine2?: number; 28 | newStartLine: number; 29 | header: string; 30 | lines: Line[]; 31 | } 32 | 33 | export interface Result { 34 | addedLines: number; 35 | deletedLines: number; 36 | isCombined: boolean; 37 | isGitDiff: boolean; 38 | oldName: string; 39 | newName: string; 40 | language: string; 41 | blocks: Block[]; 42 | oldMode?: string; 43 | newMode?: string; 44 | deletedFileMode?: string; 45 | newFileMode?: string; 46 | isDeleted?: boolean; 47 | isNew?: boolean; 48 | isCopy?: boolean; 49 | isRename?: boolean; 50 | unchangedPercentage?: number; 51 | changedPercentage?: number; 52 | checksumBefore?: string; 53 | checksumAfter?: string; 54 | mode?: string; 55 | } 56 | 57 | export interface Diff2Html { 58 | getJsonFromDiff(input: string, configuration?: Options): Result; 59 | getPrettyHtml(input: any, configuration?: Options): string; 60 | } 61 | } 62 | 63 | declare module "diff2html" { 64 | var d2h: { "Diff2Html": Diff2Html.Diff2Html }; 65 | export = d2h; 66 | } 67 | -------------------------------------------------------------------------------- /scripts/release-website.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var hogan = require('hogan.js'); 4 | 5 | var root = 'website/templates'; 6 | var pagesRoot = root + '/pages'; 7 | 8 | var websitePages = fs.readdirSync(root + '/pages'); 9 | 10 | var template = hogan.compile(readFile(root + '/template.mustache')); 11 | 12 | var options = { 13 | 'all': { 14 | 'demoUrl': 'demo.html?diff=https://github.com/rtfpessoa/diff2html/pull/106' 15 | }, 16 | 'demo': { 17 | 'extraClass': 'template-index-min' 18 | } 19 | }; 20 | 21 | websitePages.map(function(page) { 22 | var pagePartialTemplate = hogan.compile(readFile(pagesRoot + '/' + page + '/' + page + '.partial.mustache')); 23 | var pageAssetsTemplate = hogan.compile(readFile(pagesRoot + '/' + page + '/' + page + '-assets.partial.mustache')); 24 | var pageScriptsTemplate = hogan.compile(readFile(pagesRoot + '/' + page + '/' + page + '-scripts.partial.mustache')); 25 | 26 | var templateOptions = {}; 27 | 28 | var key; 29 | 30 | // Allow the pages to share common options 31 | var genericOptions = options['all'] || {}; 32 | for (key in genericOptions) { 33 | if (genericOptions.hasOwnProperty(key)) { 34 | templateOptions[key] = genericOptions[key]; 35 | } 36 | } 37 | 38 | // Allow each page to have custom options 39 | var pageOptions = options[page] || {}; 40 | for (key in pageOptions) { 41 | if (pageOptions.hasOwnProperty(key)) { 42 | templateOptions[key] = pageOptions[key]; 43 | } 44 | } 45 | 46 | var pagePartial = pagePartialTemplate.render(templateOptions); 47 | var pageAssets = pageAssetsTemplate.render(templateOptions); 48 | var pageScripts = pageScriptsTemplate.render(templateOptions); 49 | 50 | templateOptions.assets = pageAssets; 51 | templateOptions.scripts = pageScripts; 52 | templateOptions.content = pagePartial; 53 | 54 | var pageHtml = template.render(templateOptions); 55 | writeFile('docs/' + page + '.html', pageHtml); 56 | }); 57 | 58 | function readFile(filePath) { 59 | try { 60 | return fs.readFileSync(filePath, 'utf8'); 61 | } catch (_ignore) { 62 | } 63 | 64 | return ''; 65 | } 66 | 67 | function writeFile(filePath, content) { 68 | return fs.writeFileSync(filePath, content); 69 | } 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff2html", 3 | "version": "2.3.0", 4 | "homepage": "https://diff2html.xyz", 5 | "description": "Fast Diff to colorized HTML", 6 | "keywords": [ 7 | "git", 8 | "diff", 9 | "pretty", 10 | "side", 11 | "line", 12 | "side-by-side", 13 | "line-by-line", 14 | "character", 15 | "highlight", 16 | "pretty", 17 | "color", 18 | "html", 19 | "diff2html", 20 | "difftohtml", 21 | "colorized" 22 | ], 23 | "author": { 24 | "name": "Rodrigo Fernandes", 25 | "email": "rtfrodrigo@gmail.com" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://www.github.com/rtfpessoa/diff2html.git" 30 | }, 31 | "bugs": { 32 | "url": "https://www.github.com/rtfpessoa/diff2html/issues" 33 | }, 34 | "engines": { 35 | "node": ">=0.12" 36 | }, 37 | "preferGlobal": true, 38 | "scripts": { 39 | "release": "./scripts/release.sh", 40 | "release-website": "./scripts/release-website.sh", 41 | "release-bower": "./scripts/update-bower-version.sh", 42 | "templates": "./scripts/hulk.js --wrapper node --variable 'browserTemplates' ./src/templates/*.mustache > ./src/templates/diff2html-templates.js", 43 | "style": "npm run lint", 44 | "lint": "eslint .", 45 | "coverage": "istanbul cover _mocha -- -u exports -R spec ./test/**/*", 46 | "check-coverage": "istanbul check-coverage --statements 90 --functions 90 --branches 85 --lines 90 ./coverage/coverage.json", 47 | "test": "npm run coverage && npm run check-coverage", 48 | "codacy": "cat ./coverage/lcov.info | codacy-coverage", 49 | "preversion": "npm run release && npm run release-website && npm run lint && npm test", 50 | "version": "npm run release-bower && git add -A src dist docs package.json bower.json", 51 | "postversion": "git push && git push --tags" 52 | }, 53 | "main": "./src/diff2html.js", 54 | "browser": { 55 | "fs": false 56 | }, 57 | "dependencies": { 58 | "diff": "^3.2.0", 59 | "hogan.js": "^3.0.2", 60 | "whatwg-fetch": "^2.0.3" 61 | }, 62 | "devDependencies": { 63 | "autoprefixer": "^6.7.7", 64 | "browserify": "^14.1.0", 65 | "clean-css-cli": "^4.0.9", 66 | "codacy-coverage": "^2.0.1", 67 | "eslint": "^3.18.0", 68 | "eslint-plugin-promise": "^3.5.0", 69 | "eslint-plugin-standard": "^2.1.1", 70 | "fast-html-parser": "^1.0.1", 71 | "istanbul": "^0.4.5", 72 | "mkdirp": "^0.5.1", 73 | "mocha": "^3.2.0", 74 | "nopt": "^4.0.1", 75 | "postcss-cli": "^3.0.0", 76 | "uglifyjs": "^2.4.10" 77 | }, 78 | "license": "MIT", 79 | "files": [ 80 | "src", 81 | "dist", 82 | "typescript" 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /src/hoganjs-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Utils (hoganjs-utils.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var hogan = require('hogan.js'); 12 | 13 | var hoganTemplates = require('./templates/diff2html-templates.js'); 14 | 15 | var extraTemplates; 16 | 17 | function HoganJsUtils(configuration) { 18 | this.config = configuration || {}; 19 | extraTemplates = this.config.templates || {}; 20 | 21 | var rawTemplates = this.config.rawTemplates || {}; 22 | for (var templateName in rawTemplates) { 23 | if (rawTemplates.hasOwnProperty(templateName)) { 24 | if (!extraTemplates[templateName]) extraTemplates[templateName] = this.compile(rawTemplates[templateName]); 25 | } 26 | } 27 | } 28 | 29 | HoganJsUtils.prototype.render = function(namespace, view, params) { 30 | var template = this.template(namespace, view); 31 | if (template) { 32 | return template.render(params); 33 | } 34 | 35 | return null; 36 | }; 37 | 38 | HoganJsUtils.prototype.template = function(namespace, view) { 39 | var templateKey = this._templateKey(namespace, view); 40 | 41 | return this._getTemplate(templateKey); 42 | }; 43 | 44 | HoganJsUtils.prototype._getTemplate = function(templateKey) { 45 | var template; 46 | 47 | if (!this.config.noCache) { 48 | template = this._readFromCache(templateKey); 49 | } 50 | 51 | if (!template) { 52 | template = this._loadTemplate(templateKey); 53 | } 54 | 55 | return template; 56 | }; 57 | 58 | HoganJsUtils.prototype._loadTemplate = function(templateKey) { 59 | var template; 60 | 61 | try { 62 | if (fs.readFileSync) { 63 | var templatesPath = path.resolve(__dirname, 'templates'); 64 | var templatePath = path.join(templatesPath, templateKey); 65 | var templateContent = fs.readFileSync(templatePath + '.mustache', 'utf8'); 66 | template = hogan.compile(templateContent); 67 | hoganTemplates[templateKey] = template; 68 | } 69 | } catch (e) { 70 | console.error('Failed to read (template: ' + templateKey + ') from fs: ' + e.message); 71 | } 72 | 73 | return template; 74 | }; 75 | 76 | HoganJsUtils.prototype._readFromCache = function(templateKey) { 77 | return extraTemplates[templateKey] || hoganTemplates[templateKey]; 78 | }; 79 | 80 | HoganJsUtils.prototype._templateKey = function(namespace, view) { 81 | return namespace + '-' + view; 82 | }; 83 | 84 | HoganJsUtils.prototype.compile = function(templateStr) { 85 | return hogan.compile(templateStr); 86 | }; 87 | 88 | module.exports.HoganJsUtils = HoganJsUtils; 89 | })(); 90 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to diff2html 2 | 3 | ### Main rules 4 | 5 | * Before you open a ticket or send a pull request, [search](https://github.com/rtfpessoa/diff2html/issues) for previous discussions about the same feature or issue. Add to the earlier ticket if you find one. 6 | 7 | * If you're proposing a new feature, make sure you create an issue to let other contributors know what you are working on. 8 | 9 | * Before sending a pull request make sure your code is tested. 10 | 11 | * Before sending a pull request for a feature, be sure to run tests with `npm test`. 12 | 13 | * Use the same coding style as the rest of the codebase, most of the check can be performed with `npm run style`. 14 | 15 | * Use `git rebase` (not `git merge`) to sync your work from time to time with the master branch. 16 | 17 | * After creating your pull request make sure the build is passing on [CircleCI](https://circleci.com/gh/rtfpessoa/diff2html) 18 | and that [Codacy](https://www.codacy.com/app/Codacy/diff2html) is also confident in the code quality. 19 | 20 | * In your pull request, do not commit the `dist` or `build` folder if you needed to build the release files. 21 | 22 | ### Commit Style 23 | 24 | Writing good commit logs is important. A commit log should describe what changed and why. 25 | Follow these guidelines when writing one: 26 | 27 | 1. The first line should be 50 characters or less and contain a short 28 | description of the change prefixed with the name of the changed 29 | subsystem (e.g. "net: add localAddress and localPort to Socket"). 30 | 2. Keep the second line blank. 31 | 3. Wrap all other lines at 72 columns. 32 | 33 | A good commit log can look something like this: 34 | 35 | ``` 36 | subsystem: explaining the commit in one line 37 | 38 | Body of commit message is a few lines of text, explaining things 39 | in more detail, possibly giving some background about the issue 40 | being fixed, etc. etc. 41 | 42 | The body of the commit message can be several paragraphs, and 43 | please do proper word-wrap and keep columns shorter than about 44 | 72 characters or so. That way `git log` will show things 45 | nicely even when it is indented. 46 | ``` 47 | 48 | ### Developer's Certificate of Origin 1.0 49 | 50 | By making a contribution to this project, I certify that: 51 | 52 | * (a) The contribution was created in whole or in part by me and I 53 | have the right to submit it under the open source license indicated 54 | in the file; or 55 | * (b) The contribution is based upon previous work that, to the best 56 | of my knowledge, is covered under an appropriate open source license 57 | and I have the right under that license to submit that work with 58 | modifications, whether created in whole or in part by me, under the 59 | same open source license (unless I am permitted to submit under a 60 | different license), as indicated in the file; or 61 | * (c) The contribution was provided directly to me by some other 62 | person who certified (a), (b) or (c) and I have not modified it. 63 | -------------------------------------------------------------------------------- /test/hogan-cache-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var HoganJsUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(); 4 | var diffParser = require('../src/diff-parser.js').DiffParser; 5 | 6 | describe('HoganJsUtils', function() { 7 | describe('render', function() { 8 | var emptyDiffHtml = 9 | '\n' + 10 | ' \n' + 11 | '
    \n' + 12 | ' File without changes\n' + 13 | '
    \n' + 14 | ' \n' + 15 | ''; 16 | 17 | it('should render view', function() { 18 | var result = HoganJsUtils.render('generic', 'empty-diff', { 19 | contentClass: 'd2h-code-line', 20 | diffParser: diffParser 21 | }); 22 | assert.equal(emptyDiffHtml, result); 23 | }); 24 | 25 | it('should render view without cache', function() { 26 | var result = HoganJsUtils.render('generic', 'empty-diff', { 27 | contentClass: 'd2h-code-line', 28 | diffParser: diffParser 29 | }, {noCache: true}); 30 | assert.equal(emptyDiffHtml, result); 31 | }); 32 | 33 | it('should return null if template is missing', function() { 34 | var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)({noCache: true}); 35 | var result = hoganUtils.render('generic', 'missing-template', {}); 36 | assert.equal(null, result); 37 | }); 38 | 39 | it('should allow templates to be overridden with compiled templates', function() { 40 | var emptyDiffTemplate = HoganJsUtils.compile('

    {{myName}}

    '); 41 | 42 | var config = {templates: {'generic-empty-diff': emptyDiffTemplate}}; 43 | var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config); 44 | var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'}); 45 | assert.equal('

    Rodrigo Fernandes

    ', result); 46 | }); 47 | 48 | it('should allow templates to be overridden with uncompiled templates', function() { 49 | var emptyDiffTemplate = '

    {{myName}}

    '; 50 | 51 | var config = {rawTemplates: {'generic-empty-diff': emptyDiffTemplate}}; 52 | var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config); 53 | var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'}); 54 | assert.equal('

    Rodrigo Fernandes

    ', result); 55 | }); 56 | 57 | it('should allow templates to be overridden giving priority to compiled templates', function() { 58 | var emptyDiffTemplate = HoganJsUtils.compile('

    {{myName}}

    '); 59 | var emptyDiffTemplateUncompiled = '

    Not used!

    '; 60 | 61 | var config = { 62 | templates: {'generic-empty-diff': emptyDiffTemplate}, 63 | rawTemplates: {'generic-empty-diff': emptyDiffTemplateUncompiled} 64 | }; 65 | var hoganUtils = new (require('../src/hoganjs-utils.js').HoganJsUtils)(config); 66 | var result = hoganUtils.render('generic', 'empty-diff', {myName: 'Rodrigo Fernandes'}); 67 | assert.equal('

    Rodrigo Fernandes

    ', result); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/diff2html.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Diff to HTML (diff2html.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var diffParser = require('./diff-parser.js').DiffParser; 10 | var htmlPrinter = require('./html-printer.js').HtmlPrinter; 11 | 12 | function Diff2Html() { 13 | } 14 | 15 | /* 16 | * Line diff type configuration 17 | var config = { 18 | 'wordByWord': true, // (default) 19 | // OR 20 | 'charByChar': true 21 | }; 22 | */ 23 | 24 | /* 25 | * Generates json object from string diff input 26 | */ 27 | Diff2Html.prototype.getJsonFromDiff = function(diffInput, config) { 28 | var configOrEmpty = config || {}; 29 | return diffParser.generateDiffJson(diffInput, configOrEmpty); 30 | }; 31 | 32 | /* 33 | * Generates the html diff. The config parameter configures the output/input formats and other options 34 | */ 35 | Diff2Html.prototype.getPrettyHtml = function(diffInput, config) { 36 | var configOrEmpty = config || {}; 37 | 38 | var diffJson = diffInput; 39 | if (!configOrEmpty.inputFormat || configOrEmpty.inputFormat === 'diff') { 40 | diffJson = diffParser.generateDiffJson(diffInput, configOrEmpty); 41 | } 42 | 43 | var fileList = ''; 44 | if (configOrEmpty.showFiles === true) { 45 | fileList = htmlPrinter.generateFileListSummary(diffJson, configOrEmpty); 46 | } 47 | 48 | var diffOutput = ''; 49 | if (configOrEmpty.outputFormat === 'side-by-side') { 50 | diffOutput = htmlPrinter.generateSideBySideJsonHtml(diffJson, configOrEmpty); 51 | } else { 52 | diffOutput = htmlPrinter.generateLineByLineJsonHtml(diffJson, configOrEmpty); 53 | } 54 | 55 | return fileList + diffOutput; 56 | }; 57 | 58 | /* 59 | * Deprecated methods - The following methods exist only to maintain compatibility with previous versions 60 | */ 61 | 62 | /* 63 | * Generates pretty html from string diff input 64 | */ 65 | Diff2Html.prototype.getPrettyHtmlFromDiff = function(diffInput, config) { 66 | var configOrEmpty = config || {}; 67 | configOrEmpty.inputFormat = 'diff'; 68 | configOrEmpty.outputFormat = 'line-by-line'; 69 | return this.getPrettyHtml(diffInput, configOrEmpty); 70 | }; 71 | 72 | /* 73 | * Generates pretty html from a json object 74 | */ 75 | Diff2Html.prototype.getPrettyHtmlFromJson = function(diffJson, config) { 76 | var configOrEmpty = config || {}; 77 | configOrEmpty.inputFormat = 'json'; 78 | configOrEmpty.outputFormat = 'line-by-line'; 79 | return this.getPrettyHtml(diffJson, configOrEmpty); 80 | }; 81 | 82 | /* 83 | * Generates pretty side by side html from string diff input 84 | */ 85 | Diff2Html.prototype.getPrettySideBySideHtmlFromDiff = function(diffInput, config) { 86 | var configOrEmpty = config || {}; 87 | configOrEmpty.inputFormat = 'diff'; 88 | configOrEmpty.outputFormat = 'side-by-side'; 89 | return this.getPrettyHtml(diffInput, configOrEmpty); 90 | }; 91 | 92 | /* 93 | * Generates pretty side by side html from a json object 94 | */ 95 | Diff2Html.prototype.getPrettySideBySideHtmlFromJson = function(diffJson, config) { 96 | var configOrEmpty = config || {}; 97 | configOrEmpty.inputFormat = 'json'; 98 | configOrEmpty.outputFormat = 'side-by-side'; 99 | return this.getPrettyHtml(diffJson, configOrEmpty); 100 | }; 101 | 102 | var diffObject = new Diff2Html(); 103 | module.exports.Diff2Html = diffObject; 104 | 105 | // Expose diff2html in the browser 106 | global.Diff2Html = diffObject; 107 | })(); 108 | -------------------------------------------------------------------------------- /src/ui/js/highlight.js-internals.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * highlight.js 4 | * Author: isagalaev 5 | * 6 | */ 7 | 8 | (function() { 9 | function HighlightJS() { 10 | } 11 | 12 | /* 13 | * Copied from Highlight.js Private API 14 | * Will be removed when this part of the API is exposed 15 | */ 16 | 17 | /* Utility vars */ 18 | 19 | var ArrayProto = []; 20 | 21 | /* Utility functions */ 22 | 23 | function escape(value) { 24 | return value.replace(/&/gm, '&').replace(//gm, '>'); 25 | } 26 | 27 | function tag(node) { 28 | return node.nodeName.toLowerCase(); 29 | } 30 | 31 | /* Stream merging */ 32 | 33 | HighlightJS.prototype.nodeStream = function(node) { 34 | var result = []; 35 | (function _nodeStream(node, offset) { 36 | for (var child = node.firstChild; child; child = child.nextSibling) { 37 | if (child.nodeType === 3) { 38 | offset += child.nodeValue.length; 39 | } else if (child.nodeType === 1) { 40 | result.push({ 41 | event: 'start', 42 | offset: offset, 43 | node: child 44 | }); 45 | offset = _nodeStream(child, offset); 46 | // Prevent void elements from having an end tag that would actually 47 | // double them in the output. There are more void elements in HTML 48 | // but we list only those realistically expected in code display. 49 | if (!tag(child).match(/br|hr|img|input/)) { 50 | result.push({ 51 | event: 'stop', 52 | offset: offset, 53 | node: child 54 | }); 55 | } 56 | } 57 | } 58 | return offset; 59 | })(node, 0); 60 | return result; 61 | }; 62 | 63 | HighlightJS.prototype.mergeStreams = function(original, highlighted, value) { 64 | var processed = 0; 65 | var result = ''; 66 | var nodeStack = []; 67 | 68 | function selectStream() { 69 | if (!original.length || !highlighted.length) { 70 | return original.length ? original : highlighted; 71 | } 72 | if (original[0].offset !== highlighted[0].offset) { 73 | return (original[0].offset < highlighted[0].offset) ? original : highlighted; 74 | } 75 | 76 | /* 77 | To avoid starting the stream just before it should stop the order is 78 | ensured that original always starts first and closes last: 79 | if (event1 == 'start' && event2 == 'start') 80 | return original; 81 | if (event1 == 'start' && event2 == 'stop') 82 | return highlighted; 83 | if (event1 == 'stop' && event2 == 'start') 84 | return original; 85 | if (event1 == 'stop' && event2 == 'stop') 86 | return highlighted; 87 | ... which is collapsed to: 88 | */ 89 | return highlighted[0].event === 'start' ? original : highlighted; 90 | } 91 | 92 | function open(node) { 93 | function attr_str(a) { 94 | return ' ' + a.nodeName + '="' + escape(a.value) + '"'; 95 | } 96 | 97 | result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>'; 98 | } 99 | 100 | function close(node) { 101 | result += ''; 102 | } 103 | 104 | function render(event) { 105 | (event.event === 'start' ? open : close)(event.node); 106 | } 107 | 108 | while (original.length || highlighted.length) { 109 | var stream = selectStream(); 110 | result += escape(value.substring(processed, stream[0].offset)); 111 | processed = stream[0].offset; 112 | if (stream === original) { 113 | /* 114 | On any opening or closing tag of the original markup we first close 115 | the entire highlighted node stack, then render the original tag along 116 | with all the following original tags at the same offset and then 117 | reopen all the tags on the highlighted stack. 118 | */ 119 | nodeStack.reverse().forEach(close); 120 | do { 121 | render(stream.splice(0, 1)[0]); 122 | stream = selectStream(); 123 | } while (stream === original && stream.length && stream[0].offset === processed); 124 | nodeStack.reverse().forEach(open); 125 | } else { 126 | if (stream[0].event === 'start') { 127 | nodeStack.push(stream[0].node); 128 | } else { 129 | nodeStack.pop(); 130 | } 131 | render(stream.splice(0, 1)[0]); 132 | } 133 | } 134 | return result + escape(value.substr(processed)); 135 | }; 136 | 137 | /* **** Highlight.js Private API **** */ 138 | 139 | module.exports.HighlightJS = new HighlightJS(); 140 | })(); 141 | -------------------------------------------------------------------------------- /src/rematch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Rematch (rematch.js) 4 | * Matching two sequences of objects by similarity 5 | * Author: W. Illmeyer, Nexxar GmbH 6 | * 7 | */ 8 | 9 | (function() { 10 | var Rematch = {}; 11 | 12 | /* 13 | Copyright (c) 2011 Andrei Mackenzie 14 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 15 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 16 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 17 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 18 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 20 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | function levenshtein(a, b) { 25 | if (a.length === 0) { 26 | return b.length; 27 | } 28 | if (b.length === 0) { 29 | return a.length; 30 | } 31 | 32 | var matrix = []; 33 | 34 | // Increment along the first column of each row 35 | var i; 36 | for (i = 0; i <= b.length; i++) { 37 | matrix[i] = [i]; 38 | } 39 | 40 | // Increment each column in the first row 41 | var j; 42 | for (j = 0; j <= a.length; j++) { 43 | matrix[0][j] = j; 44 | } 45 | 46 | // Fill in the rest of the matrix 47 | for (i = 1; i <= b.length; i++) { 48 | for (j = 1; j <= a.length; j++) { 49 | if (b.charAt(i - 1) === a.charAt(j - 1)) { 50 | matrix[i][j] = matrix[i - 1][j - 1]; 51 | } else { 52 | matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution 53 | Math.min(matrix[i][j - 1] + 1, // Insertion 54 | matrix[i - 1][j] + 1)); // Deletion 55 | } 56 | } 57 | } 58 | 59 | return matrix[b.length][a.length]; 60 | } 61 | 62 | Rematch.levenshtein = levenshtein; 63 | 64 | Rematch.distance = function distance(x, y) { 65 | x = x.trim(); 66 | y = y.trim(); 67 | var lev = levenshtein(x, y); 68 | var score = lev / (x.length + y.length); 69 | 70 | return score; 71 | }; 72 | 73 | Rematch.rematch = function rematch(distanceFunction) { 74 | function findBestMatch(a, b, cache) { 75 | var bestMatchDist = Infinity; 76 | var bestMatch; 77 | for (var i = 0; i < a.length; ++i) { 78 | for (var j = 0; j < b.length; ++j) { 79 | var cacheKey = JSON.stringify([a[i], b[j]]); 80 | var md; 81 | if (cache.hasOwnProperty(cacheKey)) { 82 | md = cache[cacheKey]; 83 | } else { 84 | md = distanceFunction(a[i], b[j]); 85 | cache[cacheKey] = md; 86 | } 87 | if (md < bestMatchDist) { 88 | bestMatchDist = md; 89 | bestMatch = {indexA: i, indexB: j, score: bestMatchDist}; 90 | } 91 | } 92 | } 93 | 94 | return bestMatch; 95 | } 96 | 97 | function group(a, b, level, cache) { 98 | if (typeof (cache) === 'undefined') { 99 | cache = {}; 100 | } 101 | 102 | var bm = findBestMatch(a, b, cache); 103 | 104 | if (!level) { 105 | level = 0; 106 | } 107 | 108 | if (!bm || (a.length + b.length < 3)) { 109 | return [[a, b]]; 110 | } 111 | 112 | var a1 = a.slice(0, bm.indexA); 113 | var b1 = b.slice(0, bm.indexB); 114 | var aMatch = [a[bm.indexA]]; 115 | var bMatch = [b[bm.indexB]]; 116 | var tailA = bm.indexA + 1; 117 | var tailB = bm.indexB + 1; 118 | var a2 = a.slice(tailA); 119 | var b2 = b.slice(tailB); 120 | 121 | var group1 = group(a1, b1, level + 1, cache); 122 | var groupMatch = group(aMatch, bMatch, level + 1, cache); 123 | var group2 = group(a2, b2, level + 1, cache); 124 | var result = groupMatch; 125 | 126 | if (bm.indexA > 0 || bm.indexB > 0) { 127 | result = group1.concat(result); 128 | } 129 | 130 | if (a.length > tailA || b.length > tailB) { 131 | result = result.concat(group2); 132 | } 133 | 134 | return result; 135 | } 136 | 137 | return group; 138 | }; 139 | 140 | module.exports.Rematch = Rematch; 141 | })(); 142 | -------------------------------------------------------------------------------- /test/printer-utils-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var PrinterUtils = require('../src/printer-utils.js').PrinterUtils; 4 | 5 | describe('Utils', function() { 6 | describe('getHtmlId', function() { 7 | it('should generate file unique id', function() { 8 | var result = PrinterUtils.getHtmlId({ 9 | oldName: 'sample.js', 10 | newName: 'sample.js' 11 | }); 12 | assert.equal('d2h-960013', result); 13 | }); 14 | it('should generate file unique id for empty hashes', function() { 15 | var result = PrinterUtils.getHtmlId({ 16 | oldName: 'sample.js', 17 | newName: 'sample.js' 18 | }); 19 | assert.equal('d2h-960013', result); 20 | }); 21 | }); 22 | 23 | describe('getDiffName', function() { 24 | it('should generate the file name for a changed file', function() { 25 | var result = PrinterUtils.getDiffName({ 26 | oldName: 'sample.js', 27 | newName: 'sample.js' 28 | }); 29 | assert.equal('sample.js', result); 30 | }); 31 | it('should generate the file name for a changed file and full rename', function() { 32 | var result = PrinterUtils.getDiffName({ 33 | oldName: 'sample1.js', 34 | newName: 'sample2.js' 35 | }); 36 | assert.equal('sample1.js → sample2.js', result); 37 | }); 38 | it('should generate the file name for a changed file and prefix rename', function() { 39 | var result = PrinterUtils.getDiffName({ 40 | oldName: 'src/path/sample.js', 41 | newName: 'source/path/sample.js' 42 | }); 43 | assert.equal('{src → source}/path/sample.js', result); 44 | }); 45 | it('should generate the file name for a changed file and suffix rename', function() { 46 | var result = PrinterUtils.getDiffName({ 47 | oldName: 'src/path/sample1.js', 48 | newName: 'src/path/sample2.js' 49 | }); 50 | assert.equal('src/path/{sample1.js → sample2.js}', result); 51 | }); 52 | it('should generate the file name for a changed file and middle rename', function() { 53 | var result = PrinterUtils.getDiffName({ 54 | oldName: 'src/really/big/path/sample.js', 55 | newName: 'src/small/path/sample.js' 56 | }); 57 | assert.equal('src/{really/big → small}/path/sample.js', result); 58 | }); 59 | it('should generate the file name for a deleted file', function() { 60 | var result = PrinterUtils.getDiffName({ 61 | oldName: 'src/my/file.js', 62 | newName: '/dev/null' 63 | }); 64 | assert.equal('src/my/file.js', result); 65 | }); 66 | it('should generate the file name for a new file', function() { 67 | var result = PrinterUtils.getDiffName({ 68 | oldName: '/dev/null', 69 | newName: 'src/my/file.js' 70 | }); 71 | assert.equal('src/my/file.js', result); 72 | }); 73 | it('should generate handle undefined filename', function() { 74 | var result = PrinterUtils.getDiffName({}); 75 | assert.equal('unknown/file/path', result); 76 | }); 77 | }); 78 | 79 | describe('diffHighlight', function() { 80 | it('should highlight two lines', function() { 81 | var result = PrinterUtils.diffHighlight( 82 | '-var myVar = 2;', 83 | '+var myVariable = 3;', 84 | {matching: 'words'} 85 | ); 86 | 87 | assert.deepEqual({ 88 | first: { 89 | prefix: '-', 90 | line: 'var myVar = 2;' 91 | }, 92 | second: { 93 | prefix: '+', 94 | line: 'var myVariable = 3;' 95 | } 96 | }, result); 97 | }); 98 | it('should highlight two lines char by char', function() { 99 | var result = PrinterUtils.diffHighlight( 100 | '-var myVar = 2;', 101 | '+var myVariable = 3;', 102 | {charByChar: true} 103 | ); 104 | 105 | assert.deepEqual({ 106 | first: { 107 | prefix: '-', 108 | line: 'var myVar = 2;' 109 | }, 110 | second: { 111 | prefix: '+', 112 | line: 'var myVariable = 3;' 113 | } 114 | }, result); 115 | }); 116 | it('should highlight combined diff lines', function() { 117 | var result = PrinterUtils.diffHighlight( 118 | ' -var myVar = 2;', 119 | ' +var myVariable = 3;', 120 | { 121 | isCombined: true, 122 | matching: 'words', 123 | matchWordsThreshold: 1.00 124 | } 125 | ); 126 | 127 | assert.deepEqual({ 128 | first: { 129 | prefix: ' -', 130 | line: 'var myVar = 2;' 131 | }, 132 | second: { 133 | prefix: ' +', 134 | line: 'var myVariable = 3;' 135 | } 136 | }, result); 137 | }); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /dist/diff2html.min.css: -------------------------------------------------------------------------------- 1 | .d2h-wrapper{text-align:left}.d2h-file-header{padding:5px 10px;border-bottom:1px solid #d8d8d8;background-color:#f7f7f7}.d2h-file-stats{display:-webkit-box;display:-ms-flexbox;display:flex;margin-left:auto;font-size:14px}.d2h-lines-added{text-align:right;border:1px solid #b4e2b4;border-radius:5px 0 0 5px;color:#399839;padding:2px;vertical-align:middle}.d2h-lines-deleted{text-align:left;border:1px solid #e9aeae;border-radius:0 5px 5px 0;color:#c33;padding:2px;vertical-align:middle;margin-left:1px}.d2h-file-name-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;width:100%;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px}.d2h-file-name{white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden;line-height:21px}.d2h-file-wrapper{border:1px solid #ddd;border-radius:3px;margin-bottom:1em}.d2h-diff-table{width:100%;border-collapse:collapse;font-family:Menlo,Consolas,monospace;font-size:13px}.d2h-diff-tbody>tr>td{height:20px;line-height:20px}.d2h-files-diff{display:block;width:100%;height:100%}.d2h-file-diff{overflow-x:scroll;overflow-y:hidden}.d2h-file-side-diff{display:inline-block;overflow-x:scroll;overflow-y:hidden;width:50%;margin-right:-4px;margin-bottom:-8px}.d2h-code-line{display:inline-block;white-space:nowrap;padding:0 10px;margin-left:80px}.d2h-code-side-line{display:inline-block;white-space:nowrap;padding:0 10px;margin-left:50px}.d2h-code-line del,.d2h-code-side-line del{display:inline-block;margin-top:-1px;text-decoration:none;background-color:#ffb6ba;border-radius:.2em}.d2h-code-line ins,.d2h-code-side-line ins{display:inline-block;margin-top:-1px;text-decoration:none;background-color:#97f295;border-radius:.2em;text-align:left}.d2h-code-line-prefix{display:inline;background:0 0;padding:0;word-wrap:normal;white-space:pre}.d2h-code-line-ctn{display:inline;background:0 0;padding:0;word-wrap:normal;white-space:pre}.line-num1{box-sizing:border-box;float:left;width:40px;overflow:hidden;text-overflow:ellipsis;padding-left:3px}.line-num2{box-sizing:border-box;float:right;width:40px;overflow:hidden;text-overflow:ellipsis;padding-left:3px}.d2h-code-linenumber{box-sizing:border-box;position:absolute;width:86px;padding-left:2px;padding-right:2px;background-color:#fff;color:rgba(0,0,0,.3);text-align:right;border:solid #eee;border-width:0 1px 0 1px;cursor:pointer}.d2h-code-side-linenumber{box-sizing:border-box;position:absolute;width:56px;padding-left:5px;padding-right:5px;background-color:#fff;color:rgba(0,0,0,.3);text-align:right;border:solid #eee;border-width:0 1px 0 1px;cursor:pointer;overflow:hidden;text-overflow:ellipsis}.d2h-del{background-color:#fee8e9;border-color:#e9aeae}.d2h-ins{background-color:#dfd;border-color:#b4e2b4}.d2h-info{background-color:#f8fafd;color:rgba(0,0,0,.3);border-color:#d5e4f2}.d2h-file-diff .d2h-del.d2h-change{background-color:#fdf2d0}.d2h-file-diff .d2h-ins.d2h-change{background-color:#ded}.d2h-file-list-wrapper{margin-bottom:10px}.d2h-file-list-wrapper a{text-decoration:none;color:#3572b0}.d2h-file-list-wrapper a:visited{color:#3572b0}.d2h-file-list-header{text-align:left}.d2h-file-list-title{font-weight:700}.d2h-file-list-line{display:-webkit-box;display:-ms-flexbox;display:flex;text-align:left}.d2h-file-list{display:block;list-style:none;padding:0;margin:0}.d2h-file-list>li{border-bottom:#ddd solid 1px;padding:5px 10px;margin:0}.d2h-file-list>li:last-child{border-bottom:none}.d2h-file-switch{display:none;font-size:10px;cursor:pointer}.d2h-icon-wrapper{line-height:31px}.d2h-icon{vertical-align:middle;margin-right:10px;fill:currentColor}.d2h-deleted{color:#c33}.d2h-added{color:#399839}.d2h-changed{color:#d0b44c}.d2h-moved{color:#3572b0}.d2h-tag{display:-webkit-box;display:-ms-flexbox;display:flex;font-size:10px;margin-left:5px;padding:0 2px;background-color:#fff}.d2h-deleted-tag{border:#c33 1px solid}.d2h-added-tag{border:#399839 1px solid}.d2h-changed-tag{border:#d0b44c 1px solid}.d2h-moved-tag{border:#3572b0 1px solid}.selecting-left .d2h-code-line,.selecting-left .d2h-code-line *,.selecting-left .d2h-code-side-line,.selecting-left .d2h-code-side-line *,.selecting-right td.d2h-code-linenumber,.selecting-right td.d2h-code-linenumber *,.selecting-right td.d2h-code-side-linenumber,.selecting-right td.d2h-code-side-linenumber *{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.selecting-left .d2h-code-line ::-moz-selection,.selecting-left .d2h-code-line::-moz-selection,.selecting-left .d2h-code-side-line ::-moz-selection,.selecting-left .d2h-code-side-line::-moz-selection,.selecting-right td.d2h-code-linenumber::-moz-selection,.selecting-right td.d2h-code-side-linenumber ::-moz-selection,.selecting-right td.d2h-code-side-linenumber::-moz-selection{background:0 0}.selecting-left .d2h-code-line ::selection,.selecting-left .d2h-code-line::selection,.selecting-left .d2h-code-side-line ::selection,.selecting-left .d2h-code-side-line::selection,.selecting-right td.d2h-code-linenumber::selection,.selecting-right td.d2h-code-side-linenumber ::selection,.selecting-right td.d2h-code-side-linenumber::selection{background:0 0} -------------------------------------------------------------------------------- /test/file-list-printer-tests.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var fileListPrinter = new (require('../src/file-list-printer.js').FileListPrinter)(); 4 | 5 | describe('FileListPrinter', function() { 6 | describe('generateFileList', function() { 7 | it('should work for all kinds of files', function() { 8 | var files = [{ 9 | addedLines: 12, 10 | deletedLines: 41, 11 | language: 'js', 12 | oldName: 'my/file/name.js', 13 | newName: 'my/file/name.js' 14 | }, { 15 | addedLines: 12, 16 | deletedLines: 41, 17 | language: 'js', 18 | oldName: 'my/file/name1.js', 19 | newName: 'my/file/name2.js' 20 | }, { 21 | addedLines: 12, 22 | deletedLines: 0, 23 | language: 'js', 24 | oldName: 'dev/null', 25 | newName: 'my/file/name.js', 26 | isNew: true 27 | }, { 28 | addedLines: 0, 29 | deletedLines: 41, 30 | language: 'js', 31 | oldName: 'my/file/name.js', 32 | newName: 'dev/null', 33 | isDeleted: true 34 | }]; 35 | 36 | var fileHtml = fileListPrinter.generateFileList(files); 37 | 38 | var expected = 39 | '
    \n' + 40 | '
    \n' + 41 | ' Files changed (4)\n' + 42 | ' hide\n' + 43 | ' show\n' + 44 | '
    \n' + 45 | '
      \n' + 46 | '
    1. \n' + 47 | ' \n' + 48 | ' \n' + 52 | ' my/file/name.js\n' + 53 | ' \n' + 54 | ' +12\n' + 55 | ' -41\n' + 56 | ' \n' + 57 | ' \n' + 58 | '
    2. \n' + 59 | '
    3. \n' + 60 | ' \n' + 61 | ' \n' + 65 | ' my/file/{name1.js → name2.js}\n' + 66 | ' \n' + 67 | ' +12\n' + 68 | ' -41\n' + 69 | ' \n' + 70 | ' \n' + 71 | '
    4. \n' + 72 | '
    5. \n' + 73 | ' \n' + 74 | ' \n' + 78 | ' my/file/name.js\n' + 79 | ' \n' + 80 | ' +12\n' + 81 | ' -0\n' + 82 | ' \n' + 83 | ' \n' + 84 | '
    6. \n' + 85 | '
    7. \n' + 86 | ' \n' + 87 | ' \n' + 91 | ' my/file/name.js\n' + 92 | ' \n' + 93 | ' +0\n' + 94 | ' -41\n' + 95 | ' \n' + 96 | ' \n' + 97 | '
    8. \n' + 98 | '
    \n' + 99 | '
    '; 100 | 101 | assert.equal(expected, fileHtml); 102 | }); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /website/templates/template.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | diff2html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | {{#assets}} 37 | {{{assets}}} 38 | 39 | {{/assets}} 40 | 50 | 51 | 52 |
    53 | 54 |
    55 | 93 | 94 | {{{content}}} 95 | 96 |
    97 | 98 |
    99 |

    100 | Website originally designed and built by 101 | @mdo, 102 | @fat, and 103 | @dhg, 104 | adapted with by 105 | @rtfpessoa. 106 |

    107 | 119 |
    120 | 121 |
    122 | 123 | 124 | 125 | 128 | 129 | 143 | 144 | {{#scripts}} 145 | {{{scripts}}} 146 | 147 | {{/scripts}} 148 | 149 | 150 | -------------------------------------------------------------------------------- /website/templates/pages/demo/demo.partial.mustache: -------------------------------------------------------------------------------- 1 |

    Diff Prettifier 2 | 6 | 7 |

    8 |

    GitHub, Bitbucket and GitLab commit and pull request compatible

    9 |

    Just paste the GitHub, Bitbucket or GitLab commit, pull request or merge request url 10 | or any other git or unified compatible diff and we will render a pretty html representation of it 11 | with code syntax highlight and line similarity matching for better code reviews. 12 |

    13 |

    Options:

    14 |
    15 |
    16 | 22 |
    23 |
    24 | 27 |
    28 |
    29 | 36 |
    37 |
    38 | 43 |
    44 |
    45 | 51 |
    52 |
    53 |
    54 |
    55 | 56 | Load 57 |
    58 |
    59 |
    60 |
    61 |
    62 |

    Help:

    63 |
      64 |
    • 65 | Why should I use this instead of GitHub, Bitbucket or GitLab? 66 |

      Code Syntax Highlight

      67 |

      Line similarity match (similar lines are together)

      68 |

      Line by Line and Side by Side diffs

      69 |

      Supports any git and unified compatible diffs

      70 |

      Easy code selection

      71 |
    • 72 |
    • 73 | What urls are supported? 74 |

      Any GitHub, Bitbucket or GitLab Commit, Pull Request or Merge Request urls.

      75 |

      Any Git or Unified Raw Diff or Patch urls.

      76 |
    • 77 |
    • 78 | Can I send a custom url for a friend, colleague or co-worker? 79 |

      Just add a url parameter called diff to current url using as value your Commit, Pull Request, Merge Request, Diff 80 | or Patch url.

      81 |

      ex: https://diff2html.xyz/{{ demoUrl }} 82 |

      83 |
    • 84 |
    • 85 | Why can't I paste a diff? 86 |

      diffy.org is an amazing tool created by pbu88 88 | to share your diffs and uses diff2html under the hood.

      89 |

      Also, diff2html cli can directly publish diffs to diffy.org

      90 |
    • 91 |
    92 |
    93 |

    Thank you

    94 |

    I want to thank kevinsimper for this great idea, 95 | providing better diff support for existing online services. 96 |

    97 | -------------------------------------------------------------------------------- /docs/main.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Colossal 2015 3 | * Adapted by @rtfpessoa 4 | */.template-index{width:100%}.template-index-min{min-width:700px}.container{width:100%;padding:0 8%}.m-b-md{margin-bottom:23px!important}.p-t{padding-top:15px!important}@media (min-width:768px){p.m-b{height:75px;overflow-y:hidden}}.btn{display:inline-block;color:#fff;background:#26a65b;font-weight:400}.btn:hover{color:#fff;background:#5dbe5d}.btn-clipboard{position:absolute;top:0;right:0;z-index:10;display:block;padding:5px 8px;font-size:12px;color:#fff;background-color:#767676;border-radius:0 4px 0 4px;cursor:pointer}.btn-clipboard:hover{color:#000;background-color:#dcdfe4}.footer{position:relative;padding:40px 0;text-align:center;font-size:14px;border-top:1px solid #dcdfe4}.footer p{margin-bottom:5px}.footer a{color:#26a65b}.container a{color:#26a65b}.container a.btn{color:#fff}.footer-list-item{display:inline-block}.footer-list-item:not(:last-child):after{content:"\b7"}.footer>ul{padding:0}@media (min-width:768px){.footer{padding:60px 0}}@media (min-width:768px){.row-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}}.row-bordered{position:relative}.row-bordered:before{content:'';display:block;width:80%;position:absolute;bottom:0;left:50%;margin-left:-40%;height:1px;background:-webkit-radial-gradient(ellipse at center,rgba(0,0,0,.2) 0,rgba(255,255,255,0) 75%);background:-webkit-radial-gradient(center ellipse,rgba(0,0,0,.2) 0,rgba(255,255,255,0) 75%);background:radial-gradient(ellipse at center,rgba(0,0,0,.2) 0,rgba(255,255,255,0) 75%)}.hero{position:relative;text-align:center;padding:80px 0;border-bottom:1px solid #dcdfe4}.hero-booticon{font-family:Helvetica Neue,Helvetica,Arial,sans-serif;margin:0 auto 30px;width:100%;font-size:8vw;display:block;font-weight:500;text-align:center;cursor:default}.hero-homepage.hero{padding-top:0;padding-bottom:40px;overflow:hidden;border-bottom:0;border-bottom:1px solid #dcdfe4}.hero-homepage>.btn{margin-top:20px}.swag-line:before{content:'';position:fixed;display:block;top:0;left:0;right:0;height:5px;z-index:2;background-color:#26a65b;background:-webkit-linear-gradient(45deg,#28a142,#26a65b);background:linear-gradient(45deg,#28a142,#26a65b)}.navbar{background-color:#fff;border:0 #fff}.navbar-header{text-align:center}.navbar-brand{height:auto;padding:19px 25px;font-size:16px;display:inline-block;float:none;text-align:center;margin:5px 0 0}.navbar-nav{margin-right:-15px}.navbar-nav>li>a{font-size:14px}.navbar-default .navbar-brand,.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover,.navbar-default .navbar-nav>li>a,.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{background:0 0;color:#293a46;font-weight:300}.navbar-default .navbar-toggle{position:absolute;left:0;top:7px;border-color:#fff;color:#293a46;margin-right:0}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background:#f9f9f9;border-color:#f9f9f9}@media (min-width:768px){.navbar-full .navbar-brand{margin-left:-25px}.navbar-tall{height:125px}.navbar-tall .navbar-header,.navbar-tall .navbar-nav{line-height:125px;text-align:left}.navbar-brand{float:none;display:inline-block;text-align:left;margin:0}.navbar-nav>li>a{display:inline-block;margin-left:13px}.navbar-nav>li:first-child>a{margin-left:0}}.screenshot{display:block;overflow:hidden}.screenshot>img{width:100%}.screenshots-fan{margin-top:50px}.screenshots-fan .screenshot{position:relative;width:auto;display:inline-block;text-align:center}.screenshots-fan .screenshot:first-child,.screenshots-fan .screenshot:last-child{z-index:2}.screenshots-fan .screenshot{z-index:3}@media (min-width:768px){.screenshots-fan{position:relative;overflow:hidden;margin-top:60px;height:200px}.screenshots-fan .screenshot{height:auto;top:10px;width:350px}.screenshots-fan .screenshot:first-child,.screenshots-fan .screenshot:last-child{width:250px;position:absolute;top:65px}.screenshots-fan .screenshot:first-child{left:10px}.screenshots-fan .screenshot:last-child{left:auto;right:10px}}@media (min-width:992px){.screenshots-fan{margin-top:60px;height:240px}.screenshots-fan .screenshot{width:400px}.screenshots-fan .screenshot:first-child,.screenshots-fan .screenshot:last-child{width:300px}}@media (min-width:1200px){.screenshots-fan{margin-top:80px;height:380px}.screenshots-fan .screenshot{width:550px}.screenshots-fan .screenshot:first-child,.screenshots-fan .screenshot:last-child{width:450px}}body{font-size:16px;font-family:Roboto,sans-serif;font-weight:300;line-height:1.6}h1{font-size:26px;font-weight:300}h2{font-size:18px;font-weight:300}h3{font-size:26px;font-weight:300}h4{font-size:16px;font-weight:300}h5{font-size:16px;font-weight:400}h1,h2,h3,h4,h5{line-height:1.4}h1,h2{margin:10px 0}h5{margin:6px 0}@media (min-width:768px){body{font-size:16px;font-family:Roboto,sans-serif;font-weight:300;line-height:1.6}h1{font-size:38px;font-weight:300}h2{font-size:26px;font-weight:300;line-height:1.4}h3{font-size:26px;font-weight:300}h4{font-size:18px;font-weight:300}h5{font-size:16px;font-weight:400}}body{color:#293a46}a{text-decoration:none;color:inherit}a:focus,a:hover{text-decoration:underline}.nav li a{text-decoration:none;color:inherit}.nav li a:hover{text-decoration:underline}.text-muted{color:#697176}.template-index h3{font-size:21px;margin-bottom:12px}.template-index h4{color:#697176;line-height:1.6}.template-index h4 a,.template-index p a{color:#26a65b}.template-index h5{font-size:17px;margin-bottom:8px}.homepage-code-example,.homepage-terminal-example{position:relative;font-family:monospace;background:#272b38;color:#48d8a0;border-radius:8px;padding:30px}.homepage-code-example .text-muted,.homepage-terminal-example .text-muted{color:#6a7490}@media (min-width:768px){.homepage-terminal-example{padding:50px}.homepage-code-example{padding:10px}.homepage-code-example>p{margin:0}}.hero-green{color:#26a65b}.hero-black{color:#353535}.hero-red{color:#cb2c37}.svg-icon-large{width:50px;display:block;margin:0 auto}.svg-icon-large>svg{width:100%;height:auto}.row-padded-small{padding:40px 0}.unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.url-diff-container{width:980px}.diff-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;width:100%}.diff-url-input{display:inline-block;margin-right:10px;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;height:31px}.diff-url-btn{display:inline-block;float:right;width:48px}.options-label-value{font-weight:400}.diff-url-options-container label input,.diff-url-options-container label select{display:block}.col-md- .col-md-15{width:20%} -------------------------------------------------------------------------------- /dist/diff2html-ui.min.js: -------------------------------------------------------------------------------- 1 | !function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a="function"==typeof require&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}for(var i="function"==typeof require&&require,o=0;o/gm,">")}function tag(node){return node.nodeName.toLowerCase()}var ArrayProto=[];HighlightJS.prototype.nodeStream=function(node){var result=[];return function _nodeStream(node,offset){for(var child=node.firstChild;child;child=child.nextSibling)3===child.nodeType?offset+=child.nodeValue.length:1===child.nodeType&&(result.push({event:"start",offset:offset,node:child}),offset=_nodeStream(child,offset),tag(child).match(/br|hr|img|input/)||result.push({event:"stop",offset:offset,node:child}));return offset}(node,0),result},HighlightJS.prototype.mergeStreams=function(original,highlighted,value){function selectStream(){return original.length&&highlighted.length?original[0].offset!==highlighted[0].offset?original[0].offset"}function close(node){result+=""}function render(event){("start"===event.event?open:close)(event.node)}for(var processed=0,result="",nodeStack=[];original.length||highlighted.length;){var stream=selectStream();if(result+=escape(value.substring(processed,stream[0].offset)),processed=stream[0].offset,stream===original){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"start"===stream[0].event?nodeStack.push(stream[0].node):nodeStack.pop(),render(stream.splice(0,1)[0])}return result+escape(value.substr(processed))},module.exports.HighlightJS=new HighlightJS}()},{}]},{},[1]); -------------------------------------------------------------------------------- /scripts/hulk.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Copyright 2011 Twitter, Inc. 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // dependencies 19 | var hogan = require('hogan.js'); 20 | var path = require('path'); 21 | var nopt = require('nopt'); 22 | var mkderp = require('mkdirp'); 23 | var fs = require('fs'); 24 | 25 | // locals 26 | var specials = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\']; 27 | var specialsRegExp = new RegExp('(\\' + specials.join('|\\') + ')', 'g'); 28 | var options = { 29 | 'namespace': String, 30 | 'outputdir': path, 31 | 'variable': String, 32 | 'wrapper': String, 33 | 'version': true, 34 | 'help': true 35 | }; 36 | var shortHand = { 37 | 'n': ['--namespace'], 38 | 'o': ['--outputdir'], 39 | 'vn': ['--variable'], 40 | 'w': ['--wrapper'], 41 | 'h': ['--help'], 42 | 'v': ['--version'] 43 | }; 44 | var templates; 45 | 46 | // options 47 | options = nopt(options, shortHand); 48 | 49 | // escape special regexp characters 50 | function esc(text) { 51 | return text.replace(specialsRegExp, '\\$1'); 52 | } 53 | 54 | // cyan function for rob 55 | function cyan(text) { 56 | return '\x1B[36m' + text + '\x1B[39m'; 57 | } 58 | 59 | // check for dirs and correct ext (<3 for windows) 60 | function extractFiles(args) { 61 | var usage = '\n' + 62 | cyan('USAGE:') + ' hulk [--wrapper wrapper] [--outputdir outputdir] ' + 63 | '[--namespace namespace] [--variable variable] FILES\n\n' + 64 | cyan('OPTIONS:') + ' [-w, --wrapper] :: wraps the template (i.e. amd)\n' + 65 | ' [-o, --outputdir] :: outputs the templates as individual files to a directory\n\n' + 66 | ' [-n, --namespace] :: prepend string to template names\n\n' + 67 | ' [-vn, --variable] :: variable name for non-amd wrapper\n\n' + 68 | cyan('EXAMPLE:') + ' hulk --wrapper amd ./templates/*.mustache\n\n' + 69 | cyan('NOTE:') + ' hulk supports the "*" wildcard and allows you to target specific extensions too\n'; 70 | var files = []; 71 | 72 | if (options.version) { 73 | console.log(require('../package.json').version); 74 | process.exit(0); 75 | } 76 | 77 | if (!args.length || options.help) { 78 | console.log(usage); 79 | process.exit(0); 80 | } 81 | 82 | args.forEach(function(arg) { 83 | if (/\*/.test(arg)) { 84 | arg = arg.split('*'); 85 | files = files.concat( 86 | fs.readdirSync(arg[0] || '.') 87 | .map(function(f) { 88 | var file = path.join(arg[0], f); 89 | return new RegExp(esc(arg[1]) + '$').test(f) && fs.statSync(file).isFile() && file; 90 | }) 91 | .filter(function(f) { 92 | return f; 93 | }) 94 | ); 95 | return files; 96 | } 97 | 98 | if (fs.statSync(arg).isFile()) files.push(arg); 99 | }); 100 | 101 | return files; 102 | } 103 | 104 | // remove utf-8 byte order mark, http://en.wikipedia.org/wiki/Byte_order_mark 105 | function removeByteOrderMark(text) { 106 | if (text.charCodeAt(0) === 0xfeff) { 107 | return text.substring(1); 108 | } 109 | return text; 110 | } 111 | 112 | // wrap templates 113 | function wrap(file, name, openedFile) { 114 | switch (options.wrapper) { 115 | case 'amd': 116 | return 'define(' + (!options.outputdir ? '"' + path.join(path.dirname(file), name) + '", ' : '') + 117 | '[ "hogan.js" ], function(Hogan){ return new Hogan.Template(' + 118 | hogan.compile(openedFile, {asString: 1}) + 119 | ');});'; 120 | case 'node': 121 | var globalObj = 'global.' + (options.variable || 'templates') + '["' + name + '"]'; 122 | var globalStmt = globalObj + ' = new Hogan.Template(' + hogan.compile(openedFile, {asString: 1}) + ');'; 123 | var nodeOutput = globalStmt; 124 | 125 | // if we have a template per file the export will expose the template directly 126 | if (options.outputdir) { 127 | nodeOutput = nodeOutput + '\n' + 'module.exports = ' + globalObj + ';'; 128 | } 129 | 130 | return nodeOutput; 131 | default: 132 | return (options.variable || 'templates') + 133 | '["' + name + '"] = new Hogan.Template(' + 134 | hogan.compile(openedFile, {asString: 1}) + 135 | ');'; 136 | } 137 | } 138 | 139 | function prepareOutput(content) { 140 | var variableName = options.variable || 'templates'; 141 | switch (options.wrapper) { 142 | case 'amd': 143 | return content; 144 | case 'node': 145 | var nodeExport = ''; 146 | 147 | // if we have aggregated templates the export will expose the template map 148 | if (!options.outputdir) { 149 | nodeExport = 'module.exports = global.' + variableName + ';\n'; 150 | } 151 | 152 | return '(function() {\n' + 153 | 'if (!!!global.' + variableName + ') global.' + variableName + ' = {};\n' + 154 | 'var Hogan = require("hogan.js");' + 155 | content + '\n' + 156 | nodeExport + 157 | '})();'; 158 | default: 159 | return 'if (!!!' + variableName + ') var ' + variableName + ' = {};\n' + content; 160 | } 161 | } 162 | 163 | // write the directory 164 | if (options.outputdir) { 165 | mkderp.sync(options.outputdir); 166 | } 167 | 168 | // Prepend namespace to template name 169 | function namespace(name) { 170 | return (options.namespace || '') + name; 171 | } 172 | 173 | // write a template foreach file that matches template extension 174 | templates = extractFiles(options.argv.remain) 175 | .map(function(file) { 176 | var openedFile = fs.readFileSync(file, 'utf-8').trim(); 177 | var name; 178 | if (!openedFile) return; 179 | name = namespace(path.basename(file).replace(/\..*$/, '')); 180 | openedFile = removeByteOrderMark(openedFile); 181 | openedFile = wrap(file, name, openedFile); 182 | if (!options.outputdir) return openedFile; 183 | fs.writeFileSync(path.join(options.outputdir, name + '.js') 184 | , prepareOutput(openedFile)); 185 | }) 186 | .filter(function(t) { 187 | return t; 188 | }); 189 | 190 | // output templates 191 | if (!templates.length || options.outputdir) process.exit(0); 192 | 193 | console.log(prepareOutput(templates.join('\n'))); 194 | -------------------------------------------------------------------------------- /src/ui/js/diff2html-ui.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Diff to HTML (diff2html-ui.js) 4 | * Author: rtfpessoa 5 | * 6 | * Depends on: [ jQuery ] 7 | * Optional dependencies on: [ highlight.js ] 8 | * 9 | */ 10 | 11 | /*global $, hljs, Diff2Html*/ 12 | 13 | (function() { 14 | var highlightJS = require('./highlight.js-internals.js').HighlightJS; 15 | 16 | var diffJson = null; 17 | var defaultTarget = 'body'; 18 | var currentSelectionColumnId = -1; 19 | 20 | function Diff2HtmlUI(config) { 21 | var cfg = config || {}; 22 | 23 | if (cfg.diff) { 24 | diffJson = Diff2Html.getJsonFromDiff(cfg.diff); 25 | } else if (cfg.json) { 26 | diffJson = cfg.json; 27 | } 28 | 29 | this._initSelection(); 30 | } 31 | 32 | Diff2HtmlUI.prototype.draw = function(targetId, config) { 33 | var cfg = config || {}; 34 | cfg.inputFormat = 'json'; 35 | var $target = this._getTarget(targetId); 36 | $target.html(Diff2Html.getPrettyHtml(diffJson, cfg)); 37 | 38 | if (cfg.synchronisedScroll) { 39 | this.synchronisedScroll($target, cfg); 40 | } 41 | }; 42 | 43 | Diff2HtmlUI.prototype.synchronisedScroll = function(targetId) { 44 | var $target = this._getTarget(targetId); 45 | $target.find('.d2h-file-side-diff').scroll(function() { 46 | var $this = $(this); 47 | $this.closest('.d2h-file-wrapper').find('.d2h-file-side-diff') 48 | .scrollLeft($this.scrollLeft()); 49 | }); 50 | }; 51 | 52 | Diff2HtmlUI.prototype.fileListCloseable = function(targetId, startVisible) { 53 | var $target = this._getTarget(targetId); 54 | 55 | var hashTag = this._getHashTag(); 56 | 57 | var $showBtn = $target.find('.d2h-show'); 58 | var $hideBtn = $target.find('.d2h-hide'); 59 | var $fileList = $target.find('.d2h-file-list'); 60 | 61 | if (hashTag === 'files-summary-show') show(); 62 | else if (hashTag === 'files-summary-hide') hide(); 63 | else if (startVisible) show(); 64 | else hide(); 65 | 66 | $showBtn.click(show); 67 | $hideBtn.click(hide); 68 | 69 | function show() { 70 | $showBtn.hide(); 71 | $hideBtn.show(); 72 | $fileList.show(); 73 | } 74 | 75 | function hide() { 76 | $hideBtn.hide(); 77 | $showBtn.show(); 78 | $fileList.hide(); 79 | } 80 | }; 81 | 82 | Diff2HtmlUI.prototype.highlightCode = function(targetId) { 83 | var that = this; 84 | 85 | var $target = that._getTarget(targetId); 86 | 87 | // collect all the diff files and execute the highlight on their lines 88 | var $files = $target.find('.d2h-file-wrapper'); 89 | $files.map(function(_i, file) { 90 | var oldLinesState; 91 | var newLinesState; 92 | var $file = $(file); 93 | var language = $file.data('lang'); 94 | 95 | // collect all the code lines and execute the highlight on them 96 | var $codeLines = $file.find('.d2h-code-line-ctn'); 97 | $codeLines.map(function(_j, line) { 98 | var $line = $(line); 99 | var text = line.textContent; 100 | var lineParent = line.parentNode; 101 | 102 | var lineState; 103 | if (lineParent.className.indexOf('d2h-del') !== -1) { 104 | lineState = oldLinesState; 105 | } else { 106 | lineState = newLinesState; 107 | } 108 | 109 | var result = hljs.getLanguage(language) ? hljs.highlight(language, text, true, lineState) : hljs.highlightAuto(text); 110 | 111 | if (lineParent.className.indexOf('d2h-del') !== -1) { 112 | oldLinesState = result.top; 113 | } else if (lineParent.className.indexOf('d2h-ins') !== -1) { 114 | newLinesState = result.top; 115 | } else { 116 | oldLinesState = result.top; 117 | newLinesState = result.top; 118 | } 119 | 120 | var originalStream = highlightJS.nodeStream(line); 121 | if (originalStream.length) { 122 | var resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); 123 | resultNode.innerHTML = result.value; 124 | result.value = highlightJS.mergeStreams(originalStream, highlightJS.nodeStream(resultNode), text); 125 | } 126 | 127 | $line.addClass('hljs'); 128 | $line.addClass(result.language); 129 | $line.html(result.value); 130 | }); 131 | }); 132 | }; 133 | 134 | Diff2HtmlUI.prototype._getTarget = function(targetId) { 135 | var $target; 136 | 137 | if (typeof targetId === 'object' && targetId instanceof jQuery) { 138 | $target = targetId; 139 | } else if (typeof targetId === 'string') { 140 | $target = $(targetId); 141 | } else { 142 | console.error("Wrong target provided! Falling back to default value 'body'."); 143 | console.log('Please provide a jQuery object or a valid DOM query string.'); 144 | $target = $(defaultTarget); 145 | } 146 | 147 | return $target; 148 | }; 149 | 150 | Diff2HtmlUI.prototype._getHashTag = function() { 151 | var docUrl = document.URL; 152 | var hashTagIndex = docUrl.indexOf('#'); 153 | 154 | var hashTag = null; 155 | if (hashTagIndex !== -1) { 156 | hashTag = docUrl.substr(hashTagIndex + 1); 157 | } 158 | 159 | return hashTag; 160 | }; 161 | 162 | Diff2HtmlUI.prototype._distinct = function(collection) { 163 | return collection.filter(function(v, i) { 164 | return collection.indexOf(v) === i; 165 | }); 166 | }; 167 | 168 | Diff2HtmlUI.prototype._initSelection = function() { 169 | var body = $('body'); 170 | var that = this; 171 | 172 | body.on('mousedown', '.d2h-diff-table', function(event) { 173 | var target = $(event.target); 174 | var table = target.closest('.d2h-diff-table'); 175 | 176 | if (target.closest('.d2h-code-line,.d2h-code-side-line').length) { 177 | table.removeClass('selecting-left'); 178 | table.addClass('selecting-right'); 179 | currentSelectionColumnId = 1; 180 | } else if (target.closest('.d2h-code-linenumber,.d2h-code-side-linenumber').length) { 181 | table.removeClass('selecting-right'); 182 | table.addClass('selecting-left'); 183 | currentSelectionColumnId = 0; 184 | } 185 | }); 186 | 187 | body.on('copy', '.d2h-diff-table', function(event) { 188 | var clipboardData = event.originalEvent.clipboardData; 189 | var text = that._getSelectedText(); 190 | clipboardData.setData('text', text); 191 | event.preventDefault(); 192 | }); 193 | }; 194 | 195 | Diff2HtmlUI.prototype._getSelectedText = function() { 196 | var sel = window.getSelection(); 197 | var range = sel.getRangeAt(0); 198 | var doc = range.cloneContents(); 199 | var nodes = doc.querySelectorAll('tr'); 200 | var text = ''; 201 | var idx = currentSelectionColumnId; 202 | 203 | if (nodes.length === 0) { 204 | text = doc.textContent; 205 | } else { 206 | [].forEach.call(nodes, function(tr, i) { 207 | var td = tr.cells[tr.cells.length === 1 ? 0 : idx]; 208 | text += (i ? '\n' : '') + td.textContent.replace(/(?:\r\n|\r|\n)/g, ''); 209 | }); 210 | } 211 | 212 | return text; 213 | }; 214 | 215 | module.exports.Diff2HtmlUI = Diff2HtmlUI; 216 | 217 | // Expose diff2html in the browser 218 | global.Diff2HtmlUI = Diff2HtmlUI; 219 | })(); 220 | -------------------------------------------------------------------------------- /website/templates/pages/demo/demo.js: -------------------------------------------------------------------------------- 1 | /* global Diff2HtmlUI */ 2 | 3 | /* 4 | * Example URLs: 5 | * 6 | * https://github.com/rtfpessoa/diff2html/commit/7d02e67f3b3386ac5d804f974d025cd7a1165839 7 | * https://github.com/rtfpessoa/diff2html/pull/106 8 | * 9 | * https://gitlab.com/gitlab-org/gitlab-ce/commit/4e963fed42ad518caa7353d361a38a1250c99c41 10 | * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6763 11 | * 12 | * https://bitbucket.org/atlassian/amps/commits/52c38116f12475f75af4a147b7a7685478b83eca 13 | * https://bitbucket.org/atlassian/amps/pull-requests/236 14 | */ 15 | 16 | $(document).ready(function() { 17 | // Improves browser compatibility 18 | require('whatwg-fetch'); 19 | 20 | var searchParam = 'diff'; 21 | 22 | var $container = $('.container'); 23 | var $url = $('#url'); 24 | var $outputFormat = $('#diff-url-options-output-format'); 25 | var $showFiles = $('#diff-url-options-show-files'); 26 | var $matching = $('#diff-url-options-matching'); 27 | var $wordThreshold = $('#diff-url-options-match-words-threshold'); 28 | var $matchingMaxComparisons = $('#diff-url-options-matching-max-comparisons'); 29 | 30 | if (window.location.search) { 31 | var url = getUrlFromSearch(window.location.search); 32 | $url.val(url); 33 | smartDraw(url); 34 | } 35 | 36 | bind(); 37 | 38 | $outputFormat 39 | .add($showFiles) 40 | .add($matching) 41 | .add($wordThreshold) 42 | .add($matchingMaxComparisons) 43 | .change(function() { 44 | smartDraw(); 45 | }); 46 | 47 | function getUrlFromSearch(search) { 48 | try { 49 | return search 50 | .split('?')[1] 51 | .split(searchParam + '=')[1] 52 | .split('&')[0]; 53 | } catch (_ignore) { 54 | } 55 | 56 | return null; 57 | } 58 | 59 | function bind() { 60 | $('#url-btn').click(function(e) { 61 | e.preventDefault(); 62 | var url = $url.val(); 63 | smartDraw(url); 64 | }); 65 | 66 | $url.on('paste', function(e) { 67 | var url = e.originalEvent.clipboardData.getData('Text'); 68 | smartDraw(url); 69 | }); 70 | } 71 | 72 | function prepareUrl(url) { 73 | var fetchUrl; 74 | var headers = new Headers(); 75 | 76 | var githubCommitUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; 77 | var githubPrUrl = /^https?:\/\/(?:www\.)?github\.com\/(.*?)\/(.*?)\/pull\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; 78 | 79 | var gitlabCommitUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/commit\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; 80 | var gitlabPrUrl = /^https?:\/\/(?:www\.)?gitlab\.com\/(.*?)\/(.*?)\/merge_requests\/(.*?)(?:\.diff)?(?:\.patch)?(?:\/.*)?$/; 81 | 82 | var bitbucketCommitUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/commits\/(.*?)(?:\/raw)?(?:\/.*)?$/; 83 | var bitbucketPrUrl = /^https?:\/\/(?:www\.)?bitbucket\.org\/(.*?)\/(.*?)\/pull-requests\/(.*?)(?:\/.*)?$/; 84 | 85 | function gitLabUrlGen(userName, projectName, type, value) { 86 | return 'https://crossorigin.me/https://gitlab.com/' + userName + '/' + projectName + '/' + type + '/' + value + '.diff'; 87 | } 88 | 89 | function gitHubUrlGen(userName, projectName, type, value) { 90 | headers.append('Accept', 'application/vnd.github.v3.diff'); 91 | return 'https://api.github.com/repos/' + userName + '/' + projectName + '/' + type + '/' + value; 92 | } 93 | 94 | function bitbucketUrlGen(userName, projectName, type, value) { 95 | var baseUrl = 'https://bitbucket.org/api/2.0/repositories/'; 96 | if (type === 'pullrequests') { 97 | return baseUrl + userName + '/' + projectName + '/pullrequests/' + value + '/diff'; 98 | } 99 | return baseUrl + userName + '/' + projectName + '/diff/' + value; 100 | } 101 | 102 | var values; 103 | if ((values = githubCommitUrl.exec(url))) { 104 | fetchUrl = gitHubUrlGen(values[1], values[2], 'commits', values[3]); 105 | } else if ((values = githubPrUrl.exec(url))) { 106 | fetchUrl = gitHubUrlGen(values[1], values[2], 'pulls', values[3]); 107 | } else if ((values = gitlabCommitUrl.exec(url))) { 108 | fetchUrl = gitLabUrlGen(values[1], values[2], 'commit', values[3]); 109 | } else if ((values = gitlabPrUrl.exec(url))) { 110 | fetchUrl = gitLabUrlGen(values[1], values[2], 'merge_requests', values[3]); 111 | } else if ((values = bitbucketCommitUrl.exec(url))) { 112 | fetchUrl = bitbucketUrlGen(values[1], values[2], 'commit', values[3]); 113 | } else if ((values = bitbucketPrUrl.exec(url))) { 114 | fetchUrl = bitbucketUrlGen(values[1], values[2], 'pullrequests', values[3]); 115 | } else { 116 | console.info('Could not parse url, using the provided url.'); 117 | fetchUrl = 'https://crossorigin.me/' + url; 118 | } 119 | 120 | return { 121 | originalUrl: url, 122 | url: fetchUrl, 123 | headers: headers 124 | }; 125 | } 126 | 127 | function smartDraw(urlOpt) { 128 | var url = urlOpt || $url.val(); 129 | var req = prepareUrl(url); 130 | draw(req); 131 | } 132 | 133 | function draw(req) { 134 | if (!validateUrl(req.url)) { 135 | console.error('Invalid url provided!'); 136 | return; 137 | } 138 | 139 | if (validateUrl(req.originalUrl)) updateUrl(req.originalUrl); 140 | 141 | var outputFormat = $outputFormat.val(); 142 | var showFiles = $showFiles.is(':checked'); 143 | var matching = $matching.val(); 144 | var wordThreshold = $wordThreshold.val(); 145 | var matchingMaxComparisons = $matchingMaxComparisons.val(); 146 | 147 | fetch(req.url, { 148 | method: 'GET', 149 | headers: req.headers, 150 | mode: 'cors', 151 | cache: 'default' 152 | }) 153 | .then(function(res) { 154 | return res.text(); 155 | }) 156 | .then(function(data) { 157 | var container = '#url-diff-container'; 158 | var diff2htmlUi = new Diff2HtmlUI({diff: data}); 159 | 160 | if (outputFormat === 'side-by-side') { 161 | $container.css({'width': '100%'}); 162 | } else { 163 | $container.css({'width': ''}); 164 | } 165 | 166 | diff2htmlUi.draw(container, { 167 | outputFormat: outputFormat, 168 | showFiles: showFiles, 169 | matching: matching, 170 | matchWordsThreshold: wordThreshold, 171 | matchingMaxComparisons: matchingMaxComparisons, 172 | synchronisedScroll: true 173 | }); 174 | diff2htmlUi.fileListCloseable(container, false); 175 | diff2htmlUi.highlightCode(container); 176 | }); 177 | } 178 | 179 | function validateUrl(url) { 180 | 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); 181 | } 182 | 183 | function updateUrl(url) { 184 | var currentUrl = getUrlFromSearch(window.location.search); 185 | 186 | if (currentUrl === url) return; 187 | 188 | window.location = 'demo.html?' + searchParam + '=' + url; 189 | } 190 | }); 191 | -------------------------------------------------------------------------------- /src/printer-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * PrinterUtils (printer-utils.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var jsDiff = require('diff'); 10 | var utils = require('./utils.js').Utils; 11 | var Rematch = require('./rematch.js').Rematch; 12 | 13 | var separator = '/'; 14 | 15 | function PrinterUtils() { 16 | } 17 | 18 | PrinterUtils.prototype.separatePrefix = function(isCombined, line) { 19 | var prefix; 20 | var lineWithoutPrefix; 21 | 22 | if (isCombined) { 23 | prefix = line.substring(0, 2); 24 | lineWithoutPrefix = line.substring(2); 25 | } else { 26 | prefix = line.substring(0, 1); 27 | lineWithoutPrefix = line.substring(1); 28 | } 29 | 30 | return { 31 | 'prefix': prefix, 32 | 'line': lineWithoutPrefix 33 | }; 34 | }; 35 | 36 | PrinterUtils.prototype.getHtmlId = function(file) { 37 | var hashCode = function(text) { 38 | var i, chr, len; 39 | var hash = 0; 40 | 41 | for (i = 0, len = text.length; i < len; i++) { 42 | chr = text.charCodeAt(i); 43 | hash = ((hash << 5) - hash) + chr; 44 | hash |= 0; // Convert to 32bit integer 45 | } 46 | 47 | return hash; 48 | }; 49 | 50 | return 'd2h-' + hashCode(this.getDiffName(file)).toString().slice(-6); 51 | }; 52 | 53 | PrinterUtils.prototype.getDiffName = function(file) { 54 | var oldFilename = unifyPath(file.oldName); 55 | var newFilename = unifyPath(file.newName); 56 | 57 | if (oldFilename && newFilename && oldFilename !== newFilename && !isDevNullName(oldFilename) && !isDevNullName(newFilename)) { 58 | var prefixPaths = []; 59 | var suffixPaths = []; 60 | 61 | var oldFilenameParts = oldFilename.split(separator); 62 | var newFilenameParts = newFilename.split(separator); 63 | 64 | var oldFilenamePartsSize = oldFilenameParts.length; 65 | var newFilenamePartsSize = newFilenameParts.length; 66 | 67 | var i = 0; 68 | var j = oldFilenamePartsSize - 1; 69 | var k = newFilenamePartsSize - 1; 70 | 71 | while (i < j && i < k) { 72 | if (oldFilenameParts[i] === newFilenameParts[i]) { 73 | prefixPaths.push(newFilenameParts[i]); 74 | i += 1; 75 | } else { 76 | break; 77 | } 78 | } 79 | 80 | while (j > i && k > i) { 81 | if (oldFilenameParts[j] === newFilenameParts[k]) { 82 | suffixPaths.unshift(newFilenameParts[k]); 83 | j -= 1; 84 | k -= 1; 85 | } else { 86 | break; 87 | } 88 | } 89 | 90 | var finalPrefix = prefixPaths.join(separator); 91 | var finalSuffix = suffixPaths.join(separator); 92 | 93 | var oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator); 94 | var newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator); 95 | 96 | if (finalPrefix.length && finalSuffix.length) { 97 | return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix; 98 | } else if (finalPrefix.length) { 99 | return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}'; 100 | } else if (finalSuffix.length) { 101 | return '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix; 102 | } 103 | 104 | return oldFilename + ' → ' + newFilename; 105 | } else if (newFilename && !isDevNullName(newFilename)) { 106 | return newFilename; 107 | } else if (oldFilename) { 108 | return oldFilename; 109 | } 110 | 111 | return 'unknown/file/path'; 112 | }; 113 | 114 | PrinterUtils.prototype.getFileTypeIcon = function(file) { 115 | var templateName = 'file-changed'; 116 | 117 | if (file.isRename) { 118 | templateName = 'file-renamed'; 119 | } else if (file.isCopy) { 120 | templateName = 'file-renamed'; 121 | } else if (file.isNew) { 122 | templateName = 'file-added'; 123 | } else if (file.isDeleted) { 124 | templateName = 'file-deleted'; 125 | } else if (file.newName !== file.oldName) { 126 | // If file is not Added, not Deleted and the names changed it must be a rename :) 127 | templateName = 'file-renamed'; 128 | } 129 | 130 | return templateName; 131 | }; 132 | 133 | PrinterUtils.prototype.diffHighlight = function(diffLine1, diffLine2, config) { 134 | var linePrefix1, linePrefix2, unprefixedLine1, unprefixedLine2; 135 | 136 | var prefixSize = 1; 137 | 138 | if (config.isCombined) { 139 | prefixSize = 2; 140 | } 141 | 142 | linePrefix1 = diffLine1.substr(0, prefixSize); 143 | linePrefix2 = diffLine2.substr(0, prefixSize); 144 | unprefixedLine1 = diffLine1.substr(prefixSize); 145 | unprefixedLine2 = diffLine2.substr(prefixSize); 146 | 147 | var diff; 148 | if (config.charByChar) { 149 | diff = jsDiff.diffChars(unprefixedLine1, unprefixedLine2); 150 | } else { 151 | diff = jsDiff.diffWordsWithSpace(unprefixedLine1, unprefixedLine2); 152 | } 153 | 154 | var highlightedLine = ''; 155 | 156 | var changedWords = []; 157 | if (!config.charByChar && config.matching === 'words') { 158 | var treshold = 0.25; 159 | 160 | if (typeof (config.matchWordsThreshold) !== 'undefined') { 161 | treshold = config.matchWordsThreshold; 162 | } 163 | 164 | var matcher = Rematch.rematch(function(a, b) { 165 | var amod = a.value; 166 | var bmod = b.value; 167 | 168 | return Rematch.distance(amod, bmod); 169 | }); 170 | 171 | var removed = diff.filter(function isRemoved(element) { 172 | return element.removed; 173 | }); 174 | 175 | var added = diff.filter(function isAdded(element) { 176 | return element.added; 177 | }); 178 | 179 | var chunks = matcher(added, removed); 180 | chunks.forEach(function(chunk) { 181 | if (chunk[0].length === 1 && chunk[1].length === 1) { 182 | var dist = Rematch.distance(chunk[0][0].value, chunk[1][0].value); 183 | if (dist < treshold) { 184 | changedWords.push(chunk[0][0]); 185 | changedWords.push(chunk[1][0]); 186 | } 187 | } 188 | }); 189 | } 190 | 191 | diff.forEach(function(part) { 192 | var addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : ''; 193 | var elemType = part.added ? 'ins' : part.removed ? 'del' : null; 194 | var escapedValue = utils.escape(part.value); 195 | 196 | if (elemType !== null) { 197 | highlightedLine += '<' + elemType + addClass + '>' + escapedValue + ''; 198 | } else { 199 | highlightedLine += escapedValue; 200 | } 201 | }); 202 | 203 | return { 204 | first: { 205 | prefix: linePrefix1, 206 | line: removeIns(highlightedLine) 207 | }, 208 | second: { 209 | prefix: linePrefix2, 210 | line: removeDel(highlightedLine) 211 | } 212 | }; 213 | }; 214 | 215 | function unifyPath(path) { 216 | if (path) { 217 | return path.replace('\\', '/'); 218 | } 219 | 220 | return path; 221 | } 222 | 223 | function isDevNullName(name) { 224 | return name.indexOf('dev/null') !== -1; 225 | } 226 | 227 | function removeIns(line) { 228 | return line.replace(/(]*>((.|\n)*?)<\/ins>)/g, ''); 229 | } 230 | 231 | function removeDel(line) { 232 | return line.replace(/(]*>((.|\n)*?)<\/del>)/g, ''); 233 | } 234 | 235 | module.exports.PrinterUtils = new PrinterUtils(); 236 | })(); 237 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true, 6 | "jsx": true 7 | }, 8 | "sourceType": "module" 9 | }, 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "browser": true, 14 | "commonjs": true, 15 | "jquery": true, 16 | "phantomjs": true, 17 | "jasmine": true, 18 | "mocha": true, 19 | "amd": true, 20 | "worker": true, 21 | "qunit": true 22 | }, 23 | "plugins": [ 24 | "standard", 25 | "promise" 26 | ], 27 | "globals": { 28 | "document": false, 29 | "navigator": false, 30 | "window": false 31 | }, 32 | "rules": { 33 | "accessor-pairs": 2, 34 | "arrow-spacing": [ 35 | 2, 36 | { 37 | "before": true, 38 | "after": true 39 | } 40 | ], 41 | "block-spacing": [ 42 | 2, 43 | "always" 44 | ], 45 | "brace-style": [ 46 | 2, 47 | "1tbs", 48 | { 49 | "allowSingleLine": true 50 | } 51 | ], 52 | "camelcase": [ 53 | 2, 54 | { 55 | "properties": "never" 56 | } 57 | ], 58 | "comma-dangle": [ 59 | 2, 60 | "never" 61 | ], 62 | "comma-spacing": [ 63 | 2, 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "comma-style": [ 70 | 2, 71 | "last" 72 | ], 73 | "constructor-super": 2, 74 | "curly": [ 75 | 2, 76 | "multi-line" 77 | ], 78 | "dot-location": [ 79 | 2, 80 | "property" 81 | ], 82 | "eol-last": 2, 83 | "eqeqeq": [ 84 | 2, 85 | "allow-null" 86 | ], 87 | "generator-star-spacing": [ 88 | 2, 89 | { 90 | "before": true, 91 | "after": true 92 | } 93 | ], 94 | "handle-callback-err": [ 95 | 2, 96 | "^(err|error)$" 97 | ], 98 | "indent": [ 99 | 2, 100 | 2, 101 | { 102 | "SwitchCase": 1 103 | } 104 | ], 105 | "jsx-quotes": [ 106 | 2, 107 | "prefer-single" 108 | ], 109 | "key-spacing": [ 110 | 2, 111 | { 112 | "beforeColon": false, 113 | "afterColon": true 114 | } 115 | ], 116 | "keyword-spacing": [ 117 | 2, 118 | { 119 | "before": true, 120 | "after": true 121 | } 122 | ], 123 | "new-cap": [ 124 | 2, 125 | { 126 | "newIsCap": true, 127 | "capIsNew": false 128 | } 129 | ], 130 | "new-parens": 2, 131 | "no-array-constructor": 2, 132 | "no-caller": 2, 133 | "no-class-assign": 2, 134 | "no-cond-assign": 2, 135 | "no-const-assign": 2, 136 | "no-control-regex": 2, 137 | "no-debugger": 2, 138 | "no-delete-var": 2, 139 | "no-dupe-args": 2, 140 | "no-dupe-class-members": 2, 141 | "no-dupe-keys": 2, 142 | "no-duplicate-case": 2, 143 | "no-duplicate-imports": 2, 144 | "no-empty-character-class": 2, 145 | "no-empty-pattern": 2, 146 | "no-eval": 2, 147 | "no-ex-assign": 2, 148 | "no-extend-native": 2, 149 | "no-extra-bind": 2, 150 | "no-extra-boolean-cast": 2, 151 | "no-extra-parens": [ 152 | 2, 153 | "functions" 154 | ], 155 | "no-fallthrough": 2, 156 | "no-floating-decimal": 2, 157 | "no-func-assign": 2, 158 | "no-implied-eval": 2, 159 | "no-inner-declarations": [ 160 | 2, 161 | "functions" 162 | ], 163 | "no-invalid-regexp": 2, 164 | "no-irregular-whitespace": 2, 165 | "no-iterator": 2, 166 | "no-label-var": 2, 167 | "no-labels": [ 168 | 2, 169 | { 170 | "allowLoop": false, 171 | "allowSwitch": false 172 | } 173 | ], 174 | "no-lone-blocks": 2, 175 | "no-mixed-spaces-and-tabs": 2, 176 | "no-multi-spaces": 2, 177 | "no-multi-str": 2, 178 | "no-multiple-empty-lines": [ 179 | 2, 180 | { 181 | "max": 1 182 | } 183 | ], 184 | "no-native-reassign": 2, 185 | "no-negated-in-lhs": 2, 186 | "no-new": 2, 187 | "no-new-func": 2, 188 | "no-new-object": 2, 189 | "no-new-require": 2, 190 | "no-new-symbol": 2, 191 | "no-new-wrappers": 2, 192 | "no-obj-calls": 2, 193 | "no-octal": 2, 194 | "no-octal-escape": 2, 195 | "no-path-concat": 2, 196 | "no-proto": 2, 197 | "no-redeclare": 2, 198 | "no-regex-spaces": 2, 199 | "no-return-assign": [ 200 | 2, 201 | "except-parens" 202 | ], 203 | "no-self-assign": 2, 204 | "no-self-compare": 2, 205 | "no-sequences": 2, 206 | "no-shadow-restricted-names": 2, 207 | "no-spaced-func": 2, 208 | "no-sparse-arrays": 2, 209 | "no-this-before-super": 2, 210 | "no-throw-literal": 2, 211 | "no-trailing-spaces": 2, 212 | "no-undef": 2, 213 | "no-undef-init": 2, 214 | "no-unexpected-multiline": 2, 215 | "no-unmodified-loop-condition": 2, 216 | "no-unneeded-ternary": [ 217 | 2, 218 | { 219 | "defaultAssignment": false 220 | } 221 | ], 222 | "no-unreachable": 2, 223 | "no-unsafe-finally": 2, 224 | "no-unused-vars": [ 225 | 2, 226 | { 227 | "vars": "all", 228 | "args": "none" 229 | } 230 | ], 231 | "no-useless-call": 2, 232 | "no-useless-computed-key": 2, 233 | "no-useless-constructor": 2, 234 | "no-useless-escape": 2, 235 | "no-whitespace-before-property": 2, 236 | "no-with": 2, 237 | "one-var": [ 238 | 2, 239 | { 240 | "initialized": "never" 241 | } 242 | ], 243 | "operator-linebreak": [ 244 | 2, 245 | "after", 246 | { 247 | "overrides": { 248 | "?": "before", 249 | ":": "before" 250 | } 251 | } 252 | ], 253 | "padded-blocks": [ 254 | 2, 255 | "never" 256 | ], 257 | "quotes": [ 258 | 2, 259 | "single", 260 | "avoid-escape" 261 | ], 262 | "semi": [ 263 | 2, 264 | "always" 265 | ], 266 | "semi-spacing": [ 267 | 2, 268 | { 269 | "before": false, 270 | "after": true 271 | } 272 | ], 273 | "space-before-blocks": [ 274 | 2, 275 | "always" 276 | ], 277 | "space-before-function-paren": [ 278 | 2, 279 | "never" 280 | ], 281 | "space-in-parens": [ 282 | 2, 283 | "never" 284 | ], 285 | "space-infix-ops": 2, 286 | "space-unary-ops": [ 287 | 2, 288 | { 289 | "words": true, 290 | "nonwords": false 291 | } 292 | ], 293 | "spaced-comment": [ 294 | 2, 295 | "always", 296 | { 297 | "markers": [ 298 | "global", 299 | "globals", 300 | "eslint", 301 | "eslint-disable", 302 | "*package", 303 | "!", 304 | "," 305 | ] 306 | } 307 | ], 308 | "template-curly-spacing": [ 309 | 2, 310 | "never" 311 | ], 312 | "use-isnan": 2, 313 | "valid-typeof": 2, 314 | "wrap-iife": [ 315 | 2, 316 | "any" 317 | ], 318 | "yield-star-spacing": [ 319 | 2, 320 | "both" 321 | ], 322 | "yoda": [ 323 | 2, 324 | "never" 325 | ], 326 | "standard/object-curly-even-spacing": [ 327 | 2, 328 | "either" 329 | ], 330 | "standard/array-bracket-even-spacing": [ 331 | 2, 332 | "either" 333 | ], 334 | "standard/computed-property-even-spacing": [ 335 | 2, 336 | "even" 337 | ], 338 | "promise/param-names": 2, 339 | "linebreak-style": [ 340 | 2, 341 | "unix" 342 | ] 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff2html 2 | 3 | [![Codacy Code Badge](https://api.codacy.com/project/badge/grade/06412dc3f5a14f568778d0db8a1f7dc8)](https://www.codacy.com/app/Codacy/diff2html) 4 | [![Codacy Coverage Badge](https://api.codacy.com/project/badge/coverage/06412dc3f5a14f568778d0db8a1f7dc8)](https://www.codacy.com/app/Codacy/diff2html) 5 | [![Circle CI](https://circleci.com/gh/rtfpessoa/diff2html.svg?style=svg)](https://circleci.com/gh/rtfpessoa/diff2html) 6 | [![Dependency Status](https://dependencyci.com/github/rtfpessoa/diff2html/badge)](https://dependencyci.com/github/rtfpessoa/diff2html) 7 | 8 | [![npm](https://img.shields.io/npm/v/diff2html.svg)](https://www.npmjs.com/package/diff2html) 9 | [![Dependency Status](https://david-dm.org/rtfpessoa/diff2html.svg)](https://david-dm.org/rtfpessoa/diff2html) 10 | [![devDependency Status](https://david-dm.org/rtfpessoa/diff2html/dev-status.svg)](https://david-dm.org/rtfpessoa/diff2html#info=devDependencies) 11 | 12 | [![node](https://img.shields.io/node/v/diff2html.svg)]() 13 | [![npm](https://img.shields.io/npm/l/diff2html.svg)]() 14 | [![npm](https://img.shields.io/npm/dm/diff2html.svg)](https://www.npmjs.com/package/diff2html) 15 | [![Gitter](https://badges.gitter.im/rtfpessoa/diff2html.svg)](https://gitter.im/rtfpessoa/diff2html?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 16 | 17 | diff2html generates pretty HTML diffs from git or unified diff output. 18 | 19 | [![NPM](https://nodei.co/npm/diff2html.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/diff2html/) 20 | 21 | ## Features 22 | 23 | * Supports git and unified diffs 24 | 25 | * Line by line and Side by side diff 26 | 27 | * New and old line numbers 28 | 29 | * Inserted and removed lines 30 | 31 | * GitHub like style 32 | 33 | * Code syntax highlight 34 | 35 | * Line similarity matching 36 | 37 | * Easy code selection 38 | 39 | ## Online Example 40 | 41 | > Go to [diff2html](https://diff2html.xyz/) 42 | 43 | ## Distributions 44 | 45 | * [WebJar](http://www.webjars.org/) 46 | 47 | * [Node Module](https://www.npmjs.org/package/diff2html) 48 | 49 | * [Bower Package](http://bower.io/search/?q=diff2html) 50 | 51 | * [Node CLI](https://www.npmjs.org/package/diff2html-cli) 52 | 53 | * Manually download and import `dist/diff2html.min.js` into your page 54 | 55 | ## How to use 56 | 57 | ### Browser Library 58 | 59 | Import the stylesheet and the library code 60 | 61 | ```html 62 | 63 | 64 | 65 | 66 | 67 | ``` 68 | 69 | It will now be available as a global variable named `Diff2Html`. 70 | 71 | ### Node Module 72 | 73 | ```js 74 | let dif2html = require("diff2html").Diff2Html 75 | ``` 76 | 77 | ## API 78 | 79 | > Pretty HTML diff 80 | 81 | getJsonFromDiff(input: string, configuration?: Options): Result 82 | 83 | > Intermediate Json From Git Word Diff Output 84 | 85 | getPrettyHtml(input: any, configuration?: Options): string 86 | 87 | > Check out the `typescript/diff2html.d.ts` for a complete API definition in TypeScript. 88 | 89 | > Check out the `docs/demo.html` for a demo example. 90 | 91 | ## Configuration 92 | The HTML output accepts a Javascript object with configuration. Possible options: 93 | 94 | - `inputFormat`: the format of the input data: `'diff'` or `'json'`, default is `'diff'` 95 | - `outputFormat`: the format of the output data: `'line-by-line'` or `'side-by-side'`, default is `'line-by-line'` 96 | - `showFiles`: show a file list before the diff: `true` or `false`, default is `false` 97 | - `matching`: matching level: `'lines'` for matching lines, `'words'` for matching lines and words or `'none'`, default is `none` 98 | - `synchronisedScroll`: scroll both panes in side-by-side mode: `true` or `false`, default is `false` 99 | - `matchWordsThreshold`: similarity threshold for word matching, default is 0.25 100 | - `matchingMaxComparisons`: perform at most this much comparisons for line matching a block of changes, default is `2500` 101 | - `templates`: object with previously compiled templates to replace parts of the html 102 | - `rawTemplates`: object with raw not compiled templates to replace parts of the html 103 | > For more information regarding the possible templates look into [src/templates](https://github.com/rtfpessoa/diff2html/tree/master/src/templates) 104 | 105 | ## Diff2HtmlUI Helper 106 | 107 | > Simple wrapper to ease simple tasks in the browser such as: code highlight and js effects 108 | 109 | * Invoke Diff2html 110 | * Inject output in DOM element 111 | * Enable collapsible file summary list 112 | * Enable syntax highlight of the code in the diffs 113 | 114 | ### How to use 115 | 116 | #### Mandatory HTML resource imports 117 | 118 | ```html 119 | 120 | 121 | 122 | 123 | 124 | 125 | ``` 126 | 127 | #### Init 128 | 129 | ```js 130 | var diff2htmlUi = new Diff2HtmlUI({diff: diffString}); 131 | // or 132 | var diff2htmlUi = new Diff2HtmlUI({json: diffJson}); 133 | ``` 134 | 135 | #### Draw 136 | 137 | ```js 138 | diff2htmlUi.draw('html-target-elem', {inputFormat: 'json', showFiles: true, matching: 'lines'}); 139 | ``` 140 | 141 | #### Syntax Highlight 142 | 143 | > Add the dependencies. 144 | Choose one color scheme, and add the main highlight code. Note that the stylesheet for the color scheme must come **before** the main diff2html stylesheet. 145 | If your favourite language is not included in the default package also add its javascript highlight file. 146 | 147 | ```html 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | ``` 158 | 159 | > Invoke the Diff2HtmlUI helper 160 | 161 | ```js 162 | $(document).ready(function() { 163 | var diff2htmlUi = new Diff2HtmlUI({diff: lineDiffExample}); 164 | diff2htmlUi.draw('#line-by-line', {inputFormat: 'json', showFiles: true, matching: 'lines'}); 165 | diff2htmlUi.highlightCode('#line-by-line'); 166 | }); 167 | ``` 168 | 169 | #### Collapsable File Summary List 170 | 171 | > Add the dependencies. 172 | 173 | ```html 174 | 175 | 176 | 177 | ``` 178 | 179 | > Invoke the Diff2HtmlUI helper 180 | 181 | ```js 182 | $(document).ready(function() { 183 | var diff2htmlUi = new Diff2HtmlUI({diff: lineDiffExample}); 184 | diff2htmlUi.draw('#line-by-line', {inputFormat: 'json', showFiles: true, matching: 'lines'}); 185 | diff2htmlUi.fileListCloseable('#line-by-line', false); 186 | }); 187 | ``` 188 | 189 | # Troubleshooting 190 | 191 | ### 1. Out of memory or Slow execution 192 | 193 | #### Causes: 194 | * Big files 195 | * Big lines 196 | 197 | #### Fix: 198 | * Disable the line matching algorithm, by setting the option `{"matching": "none"}` when invoking diff2html 199 | 200 | ## Contributions 201 | 202 | This is a developer friendly project, all the contributions are welcome. 203 | To contribute just send a pull request with your changes following the guidelines described in `CONTRIBUTING.md`. 204 | I will try to review them as soon as possible. 205 | 206 | ## License 207 | 208 | Copyright 2014-2016 Rodrigo Fernandes. Released under the terms of the MIT license. 209 | 210 | ## Thanks 211 | 212 | This project is inspired in [pretty-diff](https://github.com/scottgonzalez/pretty-diff) by [Scott González](https://github.com/scottgonzalez). 213 | 214 | --- 215 | -------------------------------------------------------------------------------- /src/ui/css/diff2html.css: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Diff to HTML (diff2html.css) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | .d2h-wrapper { 9 | text-align: left; 10 | } 11 | 12 | .d2h-file-header { 13 | padding: 5px 10px; 14 | border-bottom: 1px solid #d8d8d8; 15 | background-color: #f7f7f7; 16 | } 17 | 18 | .d2h-file-stats { 19 | display: -webkit-box; 20 | display: -ms-flexbox; 21 | display: flex; 22 | margin-left: auto; 23 | font-size: 14px; 24 | } 25 | 26 | .d2h-lines-added { 27 | text-align: right; 28 | border: 1px solid #b4e2b4; 29 | border-radius: 5px 0 0 5px; 30 | color: #399839; 31 | padding: 2px; 32 | vertical-align: middle; 33 | } 34 | 35 | .d2h-lines-deleted { 36 | text-align: left; 37 | border: 1px solid #e9aeae; 38 | border-radius: 0 5px 5px 0; 39 | color: #c33; 40 | padding: 2px; 41 | vertical-align: middle; 42 | margin-left: 1px; 43 | } 44 | 45 | .d2h-file-name-wrapper { 46 | display: -webkit-box; 47 | display: -ms-flexbox; 48 | display: flex; 49 | -webkit-box-align: center; 50 | -ms-flex-align: center; 51 | align-items: center; 52 | width: 100%; 53 | font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif; 54 | font-size: 15px; 55 | } 56 | 57 | .d2h-file-name { 58 | white-space: nowrap; 59 | text-overflow: ellipsis; 60 | overflow-x: hidden; 61 | line-height: 21px; 62 | } 63 | 64 | .d2h-file-wrapper { 65 | border: 1px solid #ddd; 66 | border-radius: 3px; 67 | margin-bottom: 1em; 68 | } 69 | 70 | .d2h-diff-table { 71 | width: 100%; 72 | border-collapse: collapse; 73 | font-family: "Menlo", "Consolas", monospace; 74 | font-size: 13px; 75 | } 76 | 77 | .d2h-diff-tbody > tr > td { 78 | height: 20px; 79 | line-height: 20px; 80 | } 81 | 82 | .d2h-files-diff { 83 | display: block; 84 | width: 100%; 85 | height: 100%; 86 | } 87 | 88 | .d2h-file-diff { 89 | overflow-x: scroll; 90 | overflow-y: hidden; 91 | } 92 | 93 | .d2h-file-side-diff { 94 | display: inline-block; 95 | overflow-x: scroll; 96 | overflow-y: hidden; 97 | width: 50%; 98 | margin-right: -4px; 99 | margin-bottom: -8px; 100 | } 101 | 102 | .d2h-code-line { 103 | display: inline-block; 104 | white-space: nowrap; 105 | padding: 0 10px; 106 | margin-left: 80px; 107 | } 108 | 109 | .d2h-code-side-line { 110 | display: inline-block; 111 | white-space: nowrap; 112 | padding: 0 10px; 113 | margin-left: 50px; 114 | } 115 | 116 | .d2h-code-line del, 117 | .d2h-code-side-line del { 118 | display: inline-block; 119 | margin-top: -1px; 120 | text-decoration: none; 121 | background-color: #ffb6ba; 122 | border-radius: 0.2em; 123 | } 124 | 125 | .d2h-code-line ins, 126 | .d2h-code-side-line ins { 127 | display: inline-block; 128 | margin-top: -1px; 129 | text-decoration: none; 130 | background-color: #97f295; 131 | border-radius: 0.2em; 132 | text-align: left; 133 | } 134 | 135 | .d2h-code-line-prefix { 136 | display: inline; 137 | background: none; 138 | padding: 0; 139 | word-wrap: normal; 140 | white-space: pre; 141 | } 142 | 143 | .d2h-code-line-ctn { 144 | display: inline; 145 | background: none; 146 | padding: 0; 147 | word-wrap: normal; 148 | white-space: pre; 149 | } 150 | 151 | .line-num1 { 152 | box-sizing: border-box; 153 | float: left; 154 | width: 40px; 155 | overflow: hidden; 156 | text-overflow: ellipsis; 157 | padding-left: 3px; 158 | } 159 | 160 | .line-num2 { 161 | box-sizing: border-box; 162 | float: right; 163 | width: 40px; 164 | overflow: hidden; 165 | text-overflow: ellipsis; 166 | padding-left: 3px; 167 | } 168 | 169 | .d2h-code-linenumber { 170 | box-sizing: border-box; 171 | position: absolute; 172 | width: 86px; 173 | padding-left: 2px; 174 | padding-right: 2px; 175 | background-color: #fff; 176 | color: rgba(0, 0, 0, 0.3); 177 | text-align: right; 178 | border: solid #eeeeee; 179 | border-width: 0 1px 0 1px; 180 | cursor: pointer; 181 | } 182 | 183 | .d2h-code-side-linenumber { 184 | box-sizing: border-box; 185 | position: absolute; 186 | width: 56px; 187 | padding-left: 5px; 188 | padding-right: 5px; 189 | background-color: #fff; 190 | color: rgba(0, 0, 0, 0.3); 191 | text-align: right; 192 | border: solid #eeeeee; 193 | border-width: 0 1px 0 1px; 194 | cursor: pointer; 195 | overflow: hidden; 196 | text-overflow: ellipsis; 197 | } 198 | 199 | /* 200 | * Changes Highlight 201 | */ 202 | 203 | .d2h-del { 204 | background-color: #fee8e9; 205 | border-color: #e9aeae; 206 | } 207 | 208 | .d2h-ins { 209 | background-color: #dfd; 210 | border-color: #b4e2b4; 211 | } 212 | 213 | .d2h-info { 214 | background-color: #f8fafd; 215 | color: rgba(0, 0, 0, 0.3); 216 | border-color: #d5e4f2; 217 | } 218 | 219 | .d2h-file-diff .d2h-del.d2h-change { 220 | background-color: #fdf2d0; 221 | } 222 | 223 | .d2h-file-diff .d2h-ins.d2h-change { 224 | background-color: #ded; 225 | } 226 | 227 | /* 228 | * File Summary List 229 | */ 230 | 231 | .d2h-file-list-wrapper { 232 | margin-bottom: 10px; 233 | } 234 | 235 | .d2h-file-list-wrapper a { 236 | text-decoration: none; 237 | color: #3572b0; 238 | } 239 | 240 | .d2h-file-list-wrapper a:visited { 241 | color: #3572b0; 242 | } 243 | 244 | .d2h-file-list-header { 245 | text-align: left; 246 | } 247 | 248 | .d2h-file-list-title { 249 | font-weight: bold; 250 | } 251 | 252 | .d2h-file-list-line { 253 | display: -webkit-box; 254 | display: -ms-flexbox; 255 | display: flex; 256 | text-align: left; 257 | } 258 | 259 | .d2h-file-list { 260 | display: block; 261 | list-style: none; 262 | padding: 0; 263 | margin: 0; 264 | } 265 | 266 | .d2h-file-list > li { 267 | border-bottom: #ddd solid 1px; 268 | padding: 5px 10px; 269 | margin: 0; 270 | } 271 | 272 | .d2h-file-list > li:last-child { 273 | border-bottom: none; 274 | } 275 | 276 | .d2h-file-switch { 277 | display: none; 278 | font-size: 10px; 279 | cursor: pointer; 280 | } 281 | 282 | .d2h-icon-wrapper { 283 | line-height: 31px; 284 | } 285 | 286 | .d2h-icon { 287 | vertical-align: middle; 288 | margin-right: 10px; 289 | fill: currentColor; 290 | } 291 | 292 | .d2h-deleted { 293 | color: #c33; 294 | } 295 | 296 | .d2h-added { 297 | color: #399839; 298 | } 299 | 300 | .d2h-changed { 301 | color: #d0b44c; 302 | } 303 | 304 | .d2h-moved { 305 | color: #3572b0; 306 | } 307 | 308 | .d2h-tag { 309 | display: -webkit-box; 310 | display: -ms-flexbox; 311 | display: flex; 312 | font-size: 10px; 313 | margin-left: 5px; 314 | padding: 0 2px; 315 | background-color: #fff; 316 | } 317 | 318 | .d2h-deleted-tag { 319 | border: #c33 1px solid; 320 | } 321 | 322 | .d2h-added-tag { 323 | border: #399839 1px solid; 324 | } 325 | 326 | .d2h-changed-tag { 327 | border: #d0b44c 1px solid; 328 | } 329 | 330 | .d2h-moved-tag { 331 | border: #3572b0 1px solid; 332 | } 333 | 334 | /* 335 | * Selection util. 336 | */ 337 | 338 | .selecting-left .d2h-code-line, 339 | .selecting-left .d2h-code-line *, 340 | .selecting-right td.d2h-code-linenumber, 341 | .selecting-right td.d2h-code-linenumber *, 342 | .selecting-left .d2h-code-side-line, 343 | .selecting-left .d2h-code-side-line *, 344 | .selecting-right td.d2h-code-side-linenumber, 345 | .selecting-right td.d2h-code-side-linenumber * { 346 | -webkit-touch-callout: none; 347 | -webkit-user-select: none; 348 | -moz-user-select: none; 349 | -ms-user-select: none; 350 | user-select: none; 351 | } 352 | 353 | .selecting-left .d2h-code-line::-moz-selection, 354 | .selecting-left .d2h-code-line *::-moz-selection, 355 | .selecting-right td.d2h-code-linenumber::-moz-selection, 356 | .selecting-left .d2h-code-side-line::-moz-selection, 357 | .selecting-left .d2h-code-side-line *::-moz-selection, 358 | .selecting-right td.d2h-code-side-linenumber::-moz-selection, 359 | .selecting-right td.d2h-code-side-linenumber *::-moz-selection { 360 | background: transparent; 361 | } 362 | 363 | .selecting-left .d2h-code-line::selection, 364 | .selecting-left .d2h-code-line *::selection, 365 | .selecting-right td.d2h-code-linenumber::selection, 366 | .selecting-left .d2h-code-side-line::selection, 367 | .selecting-left .d2h-code-side-line *::selection, 368 | .selecting-right td.d2h-code-side-linenumber::selection, 369 | .selecting-right td.d2h-code-side-linenumber *::selection { 370 | background: transparent; 371 | } 372 | -------------------------------------------------------------------------------- /src/line-by-line-printer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LineByLinePrinter (line-by-line-printer.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var diffParser = require('./diff-parser.js').DiffParser; 10 | var printerUtils = require('./printer-utils.js').PrinterUtils; 11 | var utils = require('./utils.js').Utils; 12 | var Rematch = require('./rematch.js').Rematch; 13 | 14 | var hoganUtils; 15 | 16 | var genericTemplatesPath = 'generic'; 17 | var baseTemplatesPath = 'line-by-line'; 18 | var iconsBaseTemplatesPath = 'icon'; 19 | var tagsBaseTemplatesPath = 'tag'; 20 | 21 | function LineByLinePrinter(config) { 22 | this.config = config; 23 | 24 | var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils; 25 | hoganUtils = new HoganJsUtils(config); 26 | } 27 | 28 | LineByLinePrinter.prototype.makeFileDiffHtml = function(file, diffs) { 29 | var fileDiffTemplate = hoganUtils.template(baseTemplatesPath, 'file-diff'); 30 | var filePathTemplate = hoganUtils.template(genericTemplatesPath, 'file-path'); 31 | var fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, 'file'); 32 | var fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file)); 33 | 34 | return fileDiffTemplate.render({ 35 | file: file, 36 | fileHtmlId: printerUtils.getHtmlId(file), 37 | diffs: diffs, 38 | filePath: filePathTemplate.render({ 39 | fileDiffName: printerUtils.getDiffName(file) 40 | }, { 41 | fileIcon: fileIconTemplate, 42 | fileTag: fileTagTemplate 43 | }) 44 | }); 45 | }; 46 | 47 | LineByLinePrinter.prototype.makeLineByLineHtmlWrapper = function(content) { 48 | return hoganUtils.render(genericTemplatesPath, 'wrapper', {'content': content}); 49 | }; 50 | 51 | LineByLinePrinter.prototype.generateLineByLineJsonHtml = function(diffFiles) { 52 | var that = this; 53 | var htmlDiffs = diffFiles.map(function(file) { 54 | var diffs; 55 | if (file.blocks.length) { 56 | diffs = that._generateFileHtml(file); 57 | } else { 58 | diffs = that._generateEmptyDiff(); 59 | } 60 | return that.makeFileDiffHtml(file, diffs); 61 | }); 62 | 63 | return this.makeLineByLineHtmlWrapper(htmlDiffs.join('\n')); 64 | }; 65 | 66 | var matcher = Rematch.rematch(function(a, b) { 67 | var amod = a.content.substr(1); 68 | var bmod = b.content.substr(1); 69 | 70 | return Rematch.distance(amod, bmod); 71 | }); 72 | 73 | LineByLinePrinter.prototype.makeColumnLineNumberHtml = function(block) { 74 | return hoganUtils.render(genericTemplatesPath, 'column-line-number', { 75 | diffParser: diffParser, 76 | blockHeader: utils.escape(block.header), 77 | lineClass: 'd2h-code-linenumber', 78 | contentClass: 'd2h-code-line' 79 | }); 80 | }; 81 | 82 | LineByLinePrinter.prototype._generateFileHtml = function(file) { 83 | var that = this; 84 | return file.blocks.map(function(block) { 85 | var lines = that.makeColumnLineNumberHtml(block); 86 | var oldLines = []; 87 | var newLines = []; 88 | 89 | function processChangeBlock() { 90 | var matches; 91 | var insertType; 92 | var deleteType; 93 | 94 | var comparisons = oldLines.length * newLines.length; 95 | var maxComparisons = that.config.matchingMaxComparisons || 2500; 96 | var doMatching = comparisons < maxComparisons && (that.config.matching === 'lines' || 97 | that.config.matching === 'words'); 98 | 99 | if (doMatching) { 100 | matches = matcher(oldLines, newLines); 101 | insertType = diffParser.LINE_TYPE.INSERT_CHANGES; 102 | deleteType = diffParser.LINE_TYPE.DELETE_CHANGES; 103 | } else { 104 | matches = [[oldLines, newLines]]; 105 | insertType = diffParser.LINE_TYPE.INSERTS; 106 | deleteType = diffParser.LINE_TYPE.DELETES; 107 | } 108 | 109 | matches.forEach(function(match) { 110 | oldLines = match[0]; 111 | newLines = match[1]; 112 | 113 | var processedOldLines = []; 114 | var processedNewLines = []; 115 | 116 | var common = Math.min(oldLines.length, newLines.length); 117 | 118 | var oldLine, newLine; 119 | for (var j = 0; j < common; j++) { 120 | oldLine = oldLines[j]; 121 | newLine = newLines[j]; 122 | 123 | that.config.isCombined = file.isCombined; 124 | var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config); 125 | 126 | processedOldLines += 127 | that.makeLineHtml(file.isCombined, deleteType, oldLine.oldNumber, oldLine.newNumber, 128 | diff.first.line, diff.first.prefix); 129 | processedNewLines += 130 | that.makeLineHtml(file.isCombined, insertType, newLine.oldNumber, newLine.newNumber, 131 | diff.second.line, diff.second.prefix); 132 | } 133 | 134 | lines += processedOldLines + processedNewLines; 135 | lines += that._processLines(file.isCombined, oldLines.slice(common), newLines.slice(common)); 136 | }); 137 | 138 | oldLines = []; 139 | newLines = []; 140 | } 141 | 142 | for (var i = 0; i < block.lines.length; i++) { 143 | var line = block.lines[i]; 144 | var escapedLine = utils.escape(line.content); 145 | 146 | if (line.type !== diffParser.LINE_TYPE.INSERTS && 147 | (newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0))) { 148 | processChangeBlock(); 149 | } 150 | 151 | if (line.type === diffParser.LINE_TYPE.CONTEXT) { 152 | lines += that.makeLineHtml(file.isCombined, line.type, line.oldNumber, line.newNumber, escapedLine); 153 | } else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) { 154 | lines += that.makeLineHtml(file.isCombined, line.type, line.oldNumber, line.newNumber, escapedLine); 155 | } else if (line.type === diffParser.LINE_TYPE.DELETES) { 156 | oldLines.push(line); 157 | } else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) { 158 | newLines.push(line); 159 | } else { 160 | console.error('Unknown state in html line-by-line generator'); 161 | processChangeBlock(); 162 | } 163 | } 164 | 165 | processChangeBlock(); 166 | 167 | return lines; 168 | }).join('\n'); 169 | }; 170 | 171 | LineByLinePrinter.prototype._processLines = function(isCombined, oldLines, newLines) { 172 | var lines = ''; 173 | 174 | for (var i = 0; i < oldLines.length; i++) { 175 | var oldLine = oldLines[i]; 176 | var oldEscapedLine = utils.escape(oldLine.content); 177 | lines += this.makeLineHtml(isCombined, oldLine.type, oldLine.oldNumber, oldLine.newNumber, oldEscapedLine); 178 | } 179 | 180 | for (var j = 0; j < newLines.length; j++) { 181 | var newLine = newLines[j]; 182 | var newEscapedLine = utils.escape(newLine.content); 183 | lines += this.makeLineHtml(isCombined, newLine.type, newLine.oldNumber, newLine.newNumber, newEscapedLine); 184 | } 185 | 186 | return lines; 187 | }; 188 | 189 | LineByLinePrinter.prototype.makeLineHtml = function(isCombined, type, oldNumber, newNumber, content, possiblePrefix) { 190 | var lineNumberTemplate = hoganUtils.render(baseTemplatesPath, 'numbers', { 191 | oldNumber: utils.valueOrEmpty(oldNumber), 192 | newNumber: utils.valueOrEmpty(newNumber) 193 | }); 194 | 195 | var lineWithoutPrefix = content; 196 | var prefix = possiblePrefix; 197 | 198 | if (!prefix) { 199 | var lineWithPrefix = printerUtils.separatePrefix(isCombined, content); 200 | prefix = lineWithPrefix.prefix; 201 | lineWithoutPrefix = lineWithPrefix.line; 202 | } 203 | 204 | return hoganUtils.render(genericTemplatesPath, 'line', 205 | { 206 | type: type, 207 | lineClass: 'd2h-code-linenumber', 208 | contentClass: 'd2h-code-line', 209 | prefix: prefix, 210 | content: lineWithoutPrefix, 211 | lineNumber: lineNumberTemplate 212 | }); 213 | }; 214 | 215 | LineByLinePrinter.prototype._generateEmptyDiff = function() { 216 | return hoganUtils.render(genericTemplatesPath, 'empty-diff', { 217 | contentClass: 'd2h-code-line', 218 | diffParser: diffParser 219 | }); 220 | }; 221 | 222 | module.exports.LineByLinePrinter = LineByLinePrinter; 223 | })(); 224 | -------------------------------------------------------------------------------- /src/side-by-side-printer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HtmlPrinter (html-printer.js) 4 | * Author: rtfpessoa 5 | * 6 | */ 7 | 8 | (function() { 9 | var diffParser = require('./diff-parser.js').DiffParser; 10 | var printerUtils = require('./printer-utils.js').PrinterUtils; 11 | var utils = require('./utils.js').Utils; 12 | var Rematch = require('./rematch.js').Rematch; 13 | 14 | var hoganUtils; 15 | 16 | var genericTemplatesPath = 'generic'; 17 | var baseTemplatesPath = 'side-by-side'; 18 | var iconsBaseTemplatesPath = 'icon'; 19 | var tagsBaseTemplatesPath = 'tag'; 20 | 21 | var matcher = Rematch.rematch(function(a, b) { 22 | var amod = a.content.substr(1); 23 | var bmod = b.content.substr(1); 24 | 25 | return Rematch.distance(amod, bmod); 26 | }); 27 | 28 | function SideBySidePrinter(config) { 29 | this.config = config; 30 | 31 | var HoganJsUtils = require('./hoganjs-utils.js').HoganJsUtils; 32 | hoganUtils = new HoganJsUtils(config); 33 | } 34 | 35 | SideBySidePrinter.prototype.makeDiffHtml = function(file, diffs) { 36 | var fileDiffTemplate = hoganUtils.template(baseTemplatesPath, 'file-diff'); 37 | var filePathTemplate = hoganUtils.template(genericTemplatesPath, 'file-path'); 38 | var fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, 'file'); 39 | var fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file)); 40 | 41 | return fileDiffTemplate.render({ 42 | file: file, 43 | fileHtmlId: printerUtils.getHtmlId(file), 44 | diffs: diffs, 45 | filePath: filePathTemplate.render({ 46 | fileDiffName: printerUtils.getDiffName(file) 47 | }, { 48 | fileIcon: fileIconTemplate, 49 | fileTag: fileTagTemplate 50 | }) 51 | }); 52 | }; 53 | 54 | SideBySidePrinter.prototype.generateSideBySideJsonHtml = function(diffFiles) { 55 | var that = this; 56 | 57 | var content = diffFiles.map(function(file) { 58 | var diffs; 59 | if (file.blocks.length) { 60 | diffs = that.generateSideBySideFileHtml(file); 61 | } else { 62 | diffs = that.generateEmptyDiff(); 63 | } 64 | 65 | return that.makeDiffHtml(file, diffs); 66 | }).join('\n'); 67 | 68 | return hoganUtils.render(genericTemplatesPath, 'wrapper', {'content': content}); 69 | }; 70 | 71 | SideBySidePrinter.prototype.makeSideHtml = function(blockHeader) { 72 | return hoganUtils.render(genericTemplatesPath, 'column-line-number', { 73 | diffParser: diffParser, 74 | blockHeader: utils.escape(blockHeader), 75 | lineClass: 'd2h-code-side-linenumber', 76 | contentClass: 'd2h-code-side-line' 77 | }); 78 | }; 79 | 80 | SideBySidePrinter.prototype.generateSideBySideFileHtml = function(file) { 81 | var that = this; 82 | var fileHtml = {}; 83 | fileHtml.left = ''; 84 | fileHtml.right = ''; 85 | 86 | file.blocks.forEach(function(block) { 87 | fileHtml.left += that.makeSideHtml(block.header); 88 | fileHtml.right += that.makeSideHtml(''); 89 | 90 | var oldLines = []; 91 | var newLines = []; 92 | 93 | function processChangeBlock() { 94 | var matches; 95 | var insertType; 96 | var deleteType; 97 | 98 | var comparisons = oldLines.length * newLines.length; 99 | var maxComparisons = that.config.matchingMaxComparisons || 2500; 100 | var doMatching = comparisons < maxComparisons && (that.config.matching === 'lines' || 101 | that.config.matching === 'words'); 102 | 103 | if (doMatching) { 104 | matches = matcher(oldLines, newLines); 105 | insertType = diffParser.LINE_TYPE.INSERT_CHANGES; 106 | deleteType = diffParser.LINE_TYPE.DELETE_CHANGES; 107 | } else { 108 | matches = [[oldLines, newLines]]; 109 | insertType = diffParser.LINE_TYPE.INSERTS; 110 | deleteType = diffParser.LINE_TYPE.DELETES; 111 | } 112 | 113 | matches.forEach(function(match) { 114 | oldLines = match[0]; 115 | newLines = match[1]; 116 | 117 | var common = Math.min(oldLines.length, newLines.length); 118 | var max = Math.max(oldLines.length, newLines.length); 119 | 120 | for (var j = 0; j < common; j++) { 121 | var oldLine = oldLines[j]; 122 | var newLine = newLines[j]; 123 | 124 | that.config.isCombined = file.isCombined; 125 | 126 | var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config); 127 | 128 | fileHtml.left += 129 | that.generateSingleLineHtml(file.isCombined, deleteType, oldLine.oldNumber, 130 | diff.first.line, diff.first.prefix); 131 | fileHtml.right += 132 | that.generateSingleLineHtml(file.isCombined, insertType, newLine.newNumber, 133 | diff.second.line, diff.second.prefix); 134 | } 135 | 136 | if (max > common) { 137 | var oldSlice = oldLines.slice(common); 138 | var newSlice = newLines.slice(common); 139 | 140 | var tmpHtml = that.processLines(file.isCombined, oldSlice, newSlice); 141 | fileHtml.left += tmpHtml.left; 142 | fileHtml.right += tmpHtml.right; 143 | } 144 | }); 145 | 146 | oldLines = []; 147 | newLines = []; 148 | } 149 | 150 | for (var i = 0; i < block.lines.length; i++) { 151 | var line = block.lines[i]; 152 | var prefix = line.content[0]; 153 | var escapedLine = utils.escape(line.content.substr(1)); 154 | 155 | if (line.type !== diffParser.LINE_TYPE.INSERTS && 156 | (newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0))) { 157 | processChangeBlock(); 158 | } 159 | 160 | if (line.type === diffParser.LINE_TYPE.CONTEXT) { 161 | fileHtml.left += that.generateSingleLineHtml(file.isCombined, line.type, line.oldNumber, escapedLine, prefix); 162 | fileHtml.right += that.generateSingleLineHtml(file.isCombined, line.type, line.newNumber, escapedLine, prefix); 163 | } else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) { 164 | fileHtml.left += that.generateSingleLineHtml(file.isCombined, diffParser.LINE_TYPE.CONTEXT, '', '', ''); 165 | fileHtml.right += that.generateSingleLineHtml(file.isCombined, line.type, line.newNumber, escapedLine, prefix); 166 | } else if (line.type === diffParser.LINE_TYPE.DELETES) { 167 | oldLines.push(line); 168 | } else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) { 169 | newLines.push(line); 170 | } else { 171 | console.error('unknown state in html side-by-side generator'); 172 | processChangeBlock(); 173 | } 174 | } 175 | 176 | processChangeBlock(); 177 | }); 178 | 179 | return fileHtml; 180 | }; 181 | 182 | SideBySidePrinter.prototype.processLines = function(isCombined, oldLines, newLines) { 183 | var that = this; 184 | var fileHtml = {}; 185 | fileHtml.left = ''; 186 | fileHtml.right = ''; 187 | 188 | var maxLinesNumber = Math.max(oldLines.length, newLines.length); 189 | for (var i = 0; i < maxLinesNumber; i++) { 190 | var oldLine = oldLines[i]; 191 | var newLine = newLines[i]; 192 | var oldContent; 193 | var newContent; 194 | var oldPrefix; 195 | var newPrefix; 196 | 197 | if (oldLine) { 198 | oldContent = utils.escape(oldLine.content.substr(1)); 199 | oldPrefix = oldLine.content[0]; 200 | } 201 | 202 | if (newLine) { 203 | newContent = utils.escape(newLine.content.substr(1)); 204 | newPrefix = newLine.content[0]; 205 | } 206 | 207 | if (oldLine && newLine) { 208 | fileHtml.left += that.generateSingleLineHtml(isCombined, oldLine.type, oldLine.oldNumber, oldContent, oldPrefix); 209 | fileHtml.right += that.generateSingleLineHtml(isCombined, newLine.type, newLine.newNumber, newContent, newPrefix); 210 | } else if (oldLine) { 211 | fileHtml.left += that.generateSingleLineHtml(isCombined, oldLine.type, oldLine.oldNumber, oldContent, oldPrefix); 212 | fileHtml.right += that.generateSingleLineHtml(isCombined, diffParser.LINE_TYPE.CONTEXT, '', '', ''); 213 | } else if (newLine) { 214 | fileHtml.left += that.generateSingleLineHtml(isCombined, diffParser.LINE_TYPE.CONTEXT, '', '', ''); 215 | fileHtml.right += that.generateSingleLineHtml(isCombined, newLine.type, newLine.newNumber, newContent, newPrefix); 216 | } else { 217 | console.error('How did it get here?'); 218 | } 219 | } 220 | 221 | return fileHtml; 222 | }; 223 | 224 | SideBySidePrinter.prototype.generateSingleLineHtml = function(isCombined, type, number, content, possiblePrefix) { 225 | var lineWithoutPrefix = content; 226 | var prefix = possiblePrefix; 227 | 228 | if (!prefix) { 229 | var lineWithPrefix = printerUtils.separatePrefix(isCombined, content); 230 | prefix = lineWithPrefix.prefix; 231 | lineWithoutPrefix = lineWithPrefix.line; 232 | } 233 | 234 | return hoganUtils.render(genericTemplatesPath, 'line', 235 | { 236 | type: type, 237 | lineClass: 'd2h-code-side-linenumber', 238 | contentClass: 'd2h-code-side-line', 239 | prefix: prefix, 240 | content: lineWithoutPrefix, 241 | lineNumber: number 242 | }); 243 | }; 244 | 245 | SideBySidePrinter.prototype.generateEmptyDiff = function() { 246 | var fileHtml = {}; 247 | fileHtml.right = ''; 248 | 249 | fileHtml.left = hoganUtils.render(genericTemplatesPath, 'empty-diff', { 250 | contentClass: 'd2h-code-side-line', 251 | diffParser: diffParser 252 | }); 253 | 254 | return fileHtml; 255 | }; 256 | 257 | module.exports.SideBySidePrinter = SideBySidePrinter; 258 | })(); 259 | -------------------------------------------------------------------------------- /website/main.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright Colossal 2015 3 | * Adapted by @rtfpessoa 4 | */ 5 | 6 | .template-index { 7 | width: 100%; 8 | } 9 | 10 | .template-index-min { 11 | min-width: 700px; 12 | } 13 | 14 | .container { 15 | width: 100%; 16 | padding: 0 8%; 17 | } 18 | 19 | .m-b-md { 20 | margin-bottom: 23px !important 21 | } 22 | 23 | .p-t { 24 | padding-top: 15px !important 25 | } 26 | 27 | @media (min-width: 768px) { 28 | p.m-b { 29 | height: 75px; 30 | overflow-y: hidden; 31 | } 32 | } 33 | 34 | .btn { 35 | display: inline-block; 36 | color: #fff; 37 | background: #26A65B; 38 | font-weight: 400 39 | } 40 | 41 | .btn:hover { 42 | color: #fff; 43 | background: #5dbe5d; 44 | } 45 | 46 | .btn-clipboard { 47 | position: absolute; 48 | top: 0; 49 | right: 0; 50 | z-index: 10; 51 | display: block; 52 | padding: 5px 8px; 53 | font-size: 12px; 54 | color: #fff; 55 | background-color: #767676; 56 | border-radius: 0 4px 0 4px; 57 | cursor: pointer; 58 | } 59 | 60 | .btn-clipboard:hover { 61 | color: #000; 62 | background-color: #dcdfe4; 63 | } 64 | 65 | .footer { 66 | position: relative; 67 | padding: 40px 0; 68 | text-align: center; 69 | font-size: 14px; 70 | border-top: 1px solid #dcdfe4 71 | } 72 | 73 | .footer p { 74 | margin-bottom: 5px 75 | } 76 | 77 | .footer a { 78 | color: #26A65B; 79 | } 80 | 81 | .container a { 82 | color: #26A65B; 83 | } 84 | 85 | .container a.btn { 86 | color: #fff; 87 | } 88 | 89 | .footer-list-item { 90 | display: inline-block 91 | } 92 | 93 | .footer-list-item:not(:last-child):after { 94 | content: "\b7" 95 | } 96 | 97 | .footer > ul { 98 | padding: 0; 99 | } 100 | 101 | @media (min-width: 768px) { 102 | .footer { 103 | padding: 60px 0; 104 | } 105 | } 106 | 107 | @media (min-width: 768px) { 108 | .row-centered { 109 | display: -webkit-box; 110 | display: -ms-flexbox; 111 | display: flex; 112 | -webkit-box-align: center; 113 | -ms-flex-align: center; 114 | align-items: center; 115 | } 116 | } 117 | 118 | .row-bordered { 119 | position: relative 120 | } 121 | 122 | .row-bordered:before { 123 | content: ''; 124 | display: block; 125 | width: 80%; 126 | position: absolute; 127 | bottom: 0; 128 | left: 50%; 129 | margin-left: -40%; 130 | height: 1px; 131 | background: -webkit-radial-gradient(ellipse at center, rgba(0, 0, 0, 0.2) 0, rgba(255, 255, 255, 0) 75%); 132 | background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.2) 0, rgba(255, 255, 255, 0) 75%) 133 | } 134 | 135 | .hero { 136 | position: relative; 137 | text-align: center; 138 | padding: 80px 0; 139 | border-bottom: 1px solid #dcdfe4 140 | } 141 | 142 | .hero-booticon { 143 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif; 144 | margin: 0 auto 30px; 145 | width: 100%; 146 | font-size: 8vw; 147 | display: block; 148 | font-weight: 500; 149 | text-align: center; 150 | cursor: default; 151 | } 152 | 153 | .hero-homepage.hero { 154 | padding-top: 0; 155 | padding-bottom: 40px; 156 | overflow: hidden; 157 | border-bottom: 0; 158 | border-bottom: 1px solid #dcdfe4; 159 | } 160 | 161 | .hero-homepage > .btn { 162 | margin-top: 20px 163 | } 164 | 165 | .swag-line:before { 166 | content: ''; 167 | position: fixed; 168 | display: block; 169 | top: 0; 170 | left: 0; 171 | right: 0; 172 | height: 5px; 173 | z-index: 2; 174 | background-color: #26A65B; 175 | background: -webkit-linear-gradient(45deg, #28a142, #26A65B); 176 | background: linear-gradient(45deg, #28a142, #26A65B) 177 | } 178 | 179 | .navbar { 180 | background-color: #fff; 181 | border: 0 #fff; 182 | } 183 | 184 | .navbar-header { 185 | text-align: center 186 | } 187 | 188 | .navbar-brand { 189 | height: auto; 190 | padding: 19px 25px; 191 | font-size: 16px; 192 | display: inline-block; 193 | float: none; 194 | text-align: center; 195 | margin: 5px 0 0 196 | } 197 | 198 | .navbar-nav { 199 | margin-right: -15px 200 | } 201 | 202 | .navbar-nav > li > a { 203 | font-size: 14px 204 | } 205 | 206 | .navbar-default .navbar-brand, .navbar-default .navbar-brand:focus, .navbar-default .navbar-brand:hover, .navbar-default .navbar-nav > li > a, .navbar-default .navbar-nav > li > a:focus, .navbar-default .navbar-nav > li > a:hover { 207 | background: transparent; 208 | color: #293a46; 209 | font-weight: 300 210 | } 211 | 212 | .navbar-default .navbar-toggle { 213 | position: absolute; 214 | left: 0; 215 | top: 7px; 216 | border-color: #fff; 217 | color: #293a46; 218 | margin-right: 0 219 | } 220 | 221 | .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { 222 | background: #f9f9f9; 223 | border-color: #f9f9f9 224 | } 225 | 226 | @media (min-width: 768px) { 227 | .navbar-full .navbar-brand { 228 | margin-left: -25px 229 | } 230 | 231 | .navbar-tall { 232 | height: 125px 233 | } 234 | 235 | .navbar-tall .navbar-header, .navbar-tall .navbar-nav { 236 | line-height: 125px; 237 | text-align: left 238 | } 239 | 240 | .navbar-brand { 241 | float: none; 242 | display: inline-block; 243 | text-align: left; 244 | margin: 0 245 | } 246 | 247 | .navbar-nav > li > a { 248 | display: inline-block; 249 | margin-left: 13px 250 | } 251 | 252 | .navbar-nav > li:first-child > a { 253 | margin-left: 0 254 | } 255 | 256 | } 257 | 258 | .screenshot { 259 | display: block; 260 | overflow: hidden; 261 | } 262 | 263 | .screenshot > img { 264 | width: 100% 265 | } 266 | 267 | .screenshots-fan { 268 | margin-top: 50px 269 | } 270 | 271 | .screenshots-fan .screenshot { 272 | position: relative; 273 | width: auto; 274 | display: inline-block; 275 | text-align: center; 276 | } 277 | 278 | .screenshots-fan .screenshot:last-child, .screenshots-fan .screenshot:first-child { 279 | z-index: 2 280 | } 281 | 282 | .screenshots-fan .screenshot { 283 | z-index: 3 284 | } 285 | 286 | @media (min-width: 768px) { 287 | .screenshots-fan { 288 | position: relative; 289 | overflow: hidden; 290 | margin-top: 60px; 291 | height: 200px 292 | } 293 | 294 | .screenshots-fan .screenshot { 295 | height: auto; 296 | top: 10px; 297 | width: 350px 298 | } 299 | 300 | .screenshots-fan .screenshot:first-child, .screenshots-fan .screenshot:last-child { 301 | width: 250px; 302 | position: absolute; 303 | top: 65px 304 | } 305 | 306 | .screenshots-fan .screenshot:first-child { 307 | left: 10px 308 | } 309 | 310 | .screenshots-fan .screenshot:last-child { 311 | left: auto; 312 | right: 10px 313 | } 314 | } 315 | 316 | @media (min-width: 992px) { 317 | .screenshots-fan { 318 | margin-top: 60px; 319 | height: 240px 320 | } 321 | 322 | .screenshots-fan .screenshot { 323 | width: 400px 324 | } 325 | 326 | .screenshots-fan .screenshot:first-child, .screenshots-fan .screenshot:last-child { 327 | width: 300px 328 | } 329 | } 330 | 331 | @media (min-width: 1200px) { 332 | .screenshots-fan { 333 | margin-top: 80px; 334 | height: 380px 335 | } 336 | 337 | .screenshots-fan .screenshot { 338 | width: 550px 339 | } 340 | 341 | .screenshots-fan .screenshot:first-child, .screenshots-fan .screenshot:last-child { 342 | width: 450px 343 | } 344 | } 345 | 346 | body { 347 | font-size: 16px; 348 | font-family: Roboto, sans-serif; 349 | font-weight: 300; 350 | line-height: 1.6 351 | } 352 | 353 | h1 { 354 | font-size: 26px; 355 | font-weight: 300 356 | } 357 | 358 | h2 { 359 | font-size: 18px; 360 | font-weight: 300 361 | } 362 | 363 | h3 { 364 | font-size: 26px; 365 | font-weight: 300 366 | } 367 | 368 | h4 { 369 | font-size: 16px; 370 | font-weight: 300 371 | } 372 | 373 | h5 { 374 | font-size: 16px; 375 | font-weight: 400 376 | } 377 | 378 | h1, h2, h3, h4, h5 { 379 | line-height: 1.4 380 | } 381 | 382 | h1, h2 { 383 | margin: 10px 0 384 | } 385 | 386 | h5 { 387 | margin: 6px 0 388 | } 389 | 390 | @media (min-width: 768px) { 391 | body { 392 | font-size: 16px; 393 | font-family: Roboto, sans-serif; 394 | font-weight: 300; 395 | line-height: 1.6 396 | } 397 | 398 | h1 { 399 | font-size: 38px; 400 | font-weight: 300 401 | } 402 | 403 | h2 { 404 | font-size: 26px; 405 | font-weight: 300; 406 | line-height: 1.4 407 | } 408 | 409 | h3 { 410 | font-size: 26px; 411 | font-weight: 300 412 | } 413 | 414 | h4 { 415 | font-size: 18px; 416 | font-weight: 300 417 | } 418 | 419 | h5 { 420 | font-size: 16px; 421 | font-weight: 400 422 | } 423 | } 424 | 425 | body { 426 | color: #293a46; 427 | } 428 | 429 | a { 430 | text-decoration: none; 431 | color: inherit; 432 | } 433 | 434 | a:hover, a:focus { 435 | text-decoration: underline; 436 | } 437 | 438 | .nav li a { 439 | text-decoration: none; 440 | color: inherit; 441 | } 442 | 443 | .nav li a:hover { 444 | text-decoration: underline; 445 | } 446 | 447 | .text-muted { 448 | color: #697176 449 | } 450 | 451 | .template-index h3 { 452 | font-size: 21px; 453 | margin-bottom: 12px 454 | } 455 | 456 | .template-index h4 { 457 | color: #697176; 458 | line-height: 1.6 459 | } 460 | 461 | .template-index h4 a, .template-index p a { 462 | color: #26A65B; 463 | } 464 | 465 | .template-index h5 { 466 | font-size: 17px; 467 | margin-bottom: 8px 468 | } 469 | 470 | .homepage-terminal-example, .homepage-code-example { 471 | position: relative; 472 | font-family: monospace; 473 | background: #272b38; 474 | color: #48d8a0; 475 | border-radius: 8px; 476 | padding: 30px 477 | } 478 | 479 | .homepage-terminal-example .text-muted, 480 | .homepage-code-example .text-muted { 481 | color: #6a7490 482 | } 483 | 484 | @media (min-width: 768px) { 485 | .homepage-terminal-example { 486 | padding: 50px; 487 | } 488 | 489 | .homepage-code-example { 490 | padding: 10px; 491 | } 492 | 493 | .homepage-code-example > p { 494 | margin: 0; 495 | } 496 | } 497 | 498 | .hero-green { 499 | color: #26A65B; 500 | } 501 | 502 | .hero-black { 503 | color: #353535; 504 | } 505 | 506 | .hero-red { 507 | color: #CB2C37; 508 | } 509 | 510 | .svg-icon-large { 511 | width: 50px; 512 | display: block; 513 | margin: 0 auto; 514 | } 515 | 516 | .svg-icon-large > svg { 517 | width: 100%; 518 | height: auto; 519 | } 520 | 521 | .row-padded-small { 522 | padding: 40px 0; 523 | } 524 | 525 | *.unselectable { 526 | -webkit-touch-callout: none; 527 | -webkit-user-select: none; 528 | -moz-user-select: none; 529 | -ms-user-select: none; 530 | user-select: none; 531 | } 532 | 533 | .url-diff-container { 534 | width: 980px; 535 | } 536 | 537 | .diff-url-wrapper { 538 | display: flex; 539 | width: 100%; 540 | } 541 | 542 | .diff-url-input { 543 | display: inline-block; 544 | margin-right: 10px; 545 | flex-grow: 1; 546 | height: 31px; 547 | } 548 | 549 | .diff-url-btn { 550 | display: inline-block; 551 | float: right; 552 | width: 48px; 553 | } 554 | 555 | .options-label-value { 556 | font-weight: normal; 557 | } 558 | 559 | .diff-url-options-container label select, 560 | .diff-url-options-container label input { 561 | display: block; 562 | } 563 | 564 | /* 15 columns */ 565 | 566 | .col-md- *.col-md-15 { 567 | width: 20%; 568 | } 569 | -------------------------------------------------------------------------------- /src/templates/diff2html-templates.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (!!!global.browserTemplates) global.browserTemplates = {}; 3 | var Hogan = require("hogan.js");global.browserTemplates["file-summary-line"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("
  • ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.rp("");t.b("\n" + i);t.b(" ");t.b(t.v(t.f("fileName",c,p,0)));t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.v(t.f("addedLines",c,p,0)));t.b("");t.b("\n" + i);t.b(" ");t.b(t.v(t.f("deletedLines",c,p,0)));t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
  • ");return t.fl(); },partials: {"");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" Files changed (");t.b(t.v(t.f("filesNumber",c,p,0)));t.b(")");t.b("\n" + i);t.b(" hide");t.b("\n" + i);t.b(" show");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
      ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("files",c,p,0)));t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 5 | global.browserTemplates["generic-column-line-number"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b(t.t(t.f("blockHeader",c,p,0)));t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 6 | global.browserTemplates["generic-empty-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" File without changes");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 7 | global.browserTemplates["generic-file-path"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b(t.rp("");t.b("\n" + i);t.b(" ");t.b(t.v(t.f("fileDiffName",c,p,0)));t.b("");t.b("\n" + i);t.b(t.rp("");return t.fl(); },partials: {"");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("lineNumber",c,p,0)));t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);if(t.s(t.f("prefix",c,p,1),c,p,0,171,247,"{{ }}")){t.rs(c,p,function(c,p,t){t.b(" ");t.b(t.t(t.f("prefix",c,p,0)));t.b("");t.b("\n" + i);});c.pop();}if(t.s(t.f("content",c,p,1),c,p,0,279,353,"{{ }}")){t.rs(c,p,function(c,p,t){t.b(" ");t.b(t.t(t.f("content",c,p,0)));t.b("");t.b("\n" + i);});c.pop();}t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 9 | global.browserTemplates["generic-wrapper"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("
    ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("content",c,p,0)));t.b("\n" + i);t.b("
    ");return t.fl(); },partials: {}, subs: { }}); 10 | global.browserTemplates["icon-file-added"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 11 | global.browserTemplates["icon-file-changed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 12 | global.browserTemplates["icon-file-deleted"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 13 | global.browserTemplates["icon-file-renamed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 14 | global.browserTemplates["icon-file"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("");return t.fl(); },partials: {}, subs: { }}); 15 | global.browserTemplates["line-by-line-file-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("diffs",c,p,0)));t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");return t.fl(); },partials: {}, subs: { }}); 16 | global.browserTemplates["line-by-line-numbers"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("
    ");t.b(t.v(t.f("oldNumber",c,p,0)));t.b("
    ");t.b("\n" + i);t.b("
    ");t.b(t.v(t.f("newNumber",c,p,0)));t.b("
    ");return t.fl(); },partials: {}, subs: { }}); 17 | global.browserTemplates["side-by-side-file-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.left",c,p,0)));t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.right",c,p,0)));t.b("\n" + i);t.b(" ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");t.b("\n" + i);t.b("
    ");return t.fl(); },partials: {}, subs: { }}); 18 | global.browserTemplates["tag-file-added"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("ADDED");return t.fl(); },partials: {}, subs: { }}); 19 | global.browserTemplates["tag-file-changed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("CHANGED");return t.fl(); },partials: {}, subs: { }}); 20 | global.browserTemplates["tag-file-deleted"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("DELETED");return t.fl(); },partials: {}, subs: { }}); 21 | global.browserTemplates["tag-file-renamed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("RENAMED");return t.fl(); },partials: {}, subs: { }}); 22 | module.exports = global.browserTemplates; 23 | })(); 24 | -------------------------------------------------------------------------------- /dist/diff2html-ui.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o/gm, '>'); 249 | } 250 | 251 | function tag(node) { 252 | return node.nodeName.toLowerCase(); 253 | } 254 | 255 | /* Stream merging */ 256 | 257 | HighlightJS.prototype.nodeStream = function(node) { 258 | var result = []; 259 | (function _nodeStream(node, offset) { 260 | for (var child = node.firstChild; child; child = child.nextSibling) { 261 | if (child.nodeType === 3) { 262 | offset += child.nodeValue.length; 263 | } else if (child.nodeType === 1) { 264 | result.push({ 265 | event: 'start', 266 | offset: offset, 267 | node: child 268 | }); 269 | offset = _nodeStream(child, offset); 270 | // Prevent void elements from having an end tag that would actually 271 | // double them in the output. There are more void elements in HTML 272 | // but we list only those realistically expected in code display. 273 | if (!tag(child).match(/br|hr|img|input/)) { 274 | result.push({ 275 | event: 'stop', 276 | offset: offset, 277 | node: child 278 | }); 279 | } 280 | } 281 | } 282 | return offset; 283 | })(node, 0); 284 | return result; 285 | }; 286 | 287 | HighlightJS.prototype.mergeStreams = function(original, highlighted, value) { 288 | var processed = 0; 289 | var result = ''; 290 | var nodeStack = []; 291 | 292 | function selectStream() { 293 | if (!original.length || !highlighted.length) { 294 | return original.length ? original : highlighted; 295 | } 296 | if (original[0].offset !== highlighted[0].offset) { 297 | return (original[0].offset < highlighted[0].offset) ? original : highlighted; 298 | } 299 | 300 | /* 301 | To avoid starting the stream just before it should stop the order is 302 | ensured that original always starts first and closes last: 303 | if (event1 == 'start' && event2 == 'start') 304 | return original; 305 | if (event1 == 'start' && event2 == 'stop') 306 | return highlighted; 307 | if (event1 == 'stop' && event2 == 'start') 308 | return original; 309 | if (event1 == 'stop' && event2 == 'stop') 310 | return highlighted; 311 | ... which is collapsed to: 312 | */ 313 | return highlighted[0].event === 'start' ? original : highlighted; 314 | } 315 | 316 | function open(node) { 317 | function attr_str(a) { 318 | return ' ' + a.nodeName + '="' + escape(a.value) + '"'; 319 | } 320 | 321 | result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>'; 322 | } 323 | 324 | function close(node) { 325 | result += ''; 326 | } 327 | 328 | function render(event) { 329 | (event.event === 'start' ? open : close)(event.node); 330 | } 331 | 332 | while (original.length || highlighted.length) { 333 | var stream = selectStream(); 334 | result += escape(value.substring(processed, stream[0].offset)); 335 | processed = stream[0].offset; 336 | if (stream === original) { 337 | /* 338 | On any opening or closing tag of the original markup we first close 339 | the entire highlighted node stack, then render the original tag along 340 | with all the following original tags at the same offset and then 341 | reopen all the tags on the highlighted stack. 342 | */ 343 | nodeStack.reverse().forEach(close); 344 | do { 345 | render(stream.splice(0, 1)[0]); 346 | stream = selectStream(); 347 | } while (stream === original && stream.length && stream[0].offset === processed); 348 | nodeStack.reverse().forEach(open); 349 | } else { 350 | if (stream[0].event === 'start') { 351 | nodeStack.push(stream[0].node); 352 | } else { 353 | nodeStack.pop(); 354 | } 355 | render(stream.splice(0, 1)[0]); 356 | } 357 | } 358 | return result + escape(value.substr(processed)); 359 | }; 360 | 361 | /* **** Highlight.js Private API **** */ 362 | 363 | module.exports.HighlightJS = new HighlightJS(); 364 | })(); 365 | 366 | },{}]},{},[1]); 367 | -------------------------------------------------------------------------------- /docs/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | diff2html 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 55 |
    56 | 57 |
    58 | 96 | 97 |

    Diff Prettifier 98 | 102 | 103 |

    104 |

    GitHub, Bitbucket and GitLab commit and pull request compatible

    105 |

    Just paste the GitHub, Bitbucket or GitLab commit, pull request or merge request url 106 | or any other git or unified compatible diff and we will render a pretty html representation of it 107 | with code syntax highlight and line similarity matching for better code reviews. 108 |

    109 |

    Options:

    110 |
    111 |
    112 | 118 |
    119 |
    120 | 123 |
    124 |
    125 | 132 |
    133 |
    134 | 139 |
    140 |
    141 | 147 |
    148 |
    149 |
    150 |
    151 | 152 | Load 153 |
    154 |
    155 |
    156 |
    157 |
    158 |

    Help:

    159 |
      160 |
    • 161 | Why should I use this instead of GitHub, Bitbucket or GitLab? 162 |

      Code Syntax Highlight

      163 |

      Line similarity match (similar lines are together)

      164 |

      Line by Line and Side by Side diffs

      165 |

      Supports any git and unified compatible diffs

      166 |

      Easy code selection

      167 |
    • 168 |
    • 169 | What urls are supported? 170 |

      Any GitHub, Bitbucket or GitLab Commit, Pull Request or Merge Request urls.

      171 |

      Any Git or Unified Raw Diff or Patch urls.

      172 |
    • 173 |
    • 174 | Can I send a custom url for a friend, colleague or co-worker? 175 |

      Just add a url parameter called diff to current url using as value your Commit, Pull Request, Merge Request, Diff 176 | or Patch url.

      177 |

      ex: https://diff2html.xyz/demo.html?diff=https://github.com/rtfpessoa/diff2html/pull/106 178 |

      179 |
    • 180 |
    • 181 | Why can't I paste a diff? 182 |

      diffy.org is an amazing tool created by pbu88 184 | to share your diffs and uses diff2html under the hood.

      185 |

      Also, diff2html cli can directly publish diffs to diffy.org

      186 |
    • 187 |
    188 |
    189 |

    Thank you

    190 |

    I want to thank kevinsimper for this great idea, 191 | providing better diff support for existing online services. 192 |

    193 | 194 | 195 |
    196 | 197 |
    198 |

    199 | Website originally designed and built by 200 | @mdo, 201 | @fat, and 202 | @dhg, 203 | adapted with by 204 | @rtfpessoa. 205 |

    206 | 218 |
    219 | 220 |
    221 | 222 | 223 | 224 | 227 | 228 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | --------------------------------------------------------------------------------