Compare commits

..

No commits in common. "master" and "v2.0.0-rc.5" have entirely different histories.

122 changed files with 9403 additions and 31484 deletions

View file

@ -1,342 +0,0 @@
{
"projectName": "diff2html",
"projectOwner": "rtfpessoa",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "none",
"contributors": [
{
"login": "rtfpessoa",
"name": "Rodrigo Fernandes",
"avatar_url": "https://avatars0.githubusercontent.com/u/902384?v=4",
"profile": "https://rtfpessoa.xyz",
"contributions": [
"code"
]
},
{
"login": "stockmind",
"name": "stockmind",
"avatar_url": "https://avatars3.githubusercontent.com/u/5653847?v=4",
"profile": "https://github.com/stockmind",
"contributions": [
"code"
]
},
{
"login": "lantian",
"name": "Ivan Vorontsov",
"avatar_url": "https://avatars3.githubusercontent.com/u/535545?v=4",
"profile": "https://github.com/lantian",
"contributions": [
"code"
]
},
{
"login": "brewern",
"name": "Nick Brewer",
"avatar_url": "https://avatars1.githubusercontent.com/u/129300?v=4",
"profile": "http://www.nick-brewer.com",
"contributions": [
"code"
]
},
{
"login": "romellem",
"name": "Matt Wade",
"avatar_url": "https://avatars0.githubusercontent.com/u/8504000?v=4",
"profile": "http://heyitsmattwade.com",
"contributions": [
"bug",
"code"
]
},
{
"login": "mrfyda",
"name": "Rafael Cortês",
"avatar_url": "https://avatars1.githubusercontent.com/u/593860?v=4",
"profile": "http://mrfyda.github.io",
"contributions": [
"code"
]
},
{
"login": "nmatpt",
"name": "Nuno Teixeira",
"avatar_url": "https://avatars2.githubusercontent.com/u/5034733?v=4",
"profile": "https://github.com/nmatpt",
"contributions": [
"code"
]
},
{
"login": "kaishuu0123",
"name": "Koki Oyatsu",
"avatar_url": "https://avatars0.githubusercontent.com/u/1567423?v=4",
"profile": "https://saino.me/",
"contributions": [
"bug",
"code"
]
},
{
"login": "Jameskmonger",
"name": "James Monger",
"avatar_url": "https://avatars2.githubusercontent.com/u/2037007?v=4",
"profile": "http://www.jamesmonger.com",
"contributions": [
"doc"
]
},
{
"login": "wesssel",
"name": "Wessel van der Pal",
"avatar_url": "https://avatars2.githubusercontent.com/u/7767299?v=4",
"profile": "http://wesssel.github.io/",
"contributions": [
"security",
"code"
]
},
{
"login": "jung-kim",
"name": "jk-kim",
"avatar_url": "https://avatars2.githubusercontent.com/u/5281068?v=4",
"profile": "https://jung-kim.github.io",
"contributions": [
"code"
]
},
{
"login": "sss0791",
"name": "Sergey Semenov",
"avatar_url": "https://avatars1.githubusercontent.com/u/1446970?v=4",
"profile": "https://github.com/sss0791",
"contributions": [
"bug",
"code"
]
},
{
"login": "starpit",
"name": "Nick Mitchell",
"avatar_url": "https://avatars3.githubusercontent.com/u/4741620?v=4",
"profile": "http://researcher.watson.ibm.com/researcher/view.php?person=us-nickm",
"contributions": [
"bug",
"code"
]
},
{
"login": "samiraguiar",
"name": "Samir Aguiar",
"avatar_url": "https://avatars0.githubusercontent.com/u/13439135?v=4",
"profile": "https://github.com/samiraguiar",
"contributions": [
"doc"
]
},
{
"login": "pubkey",
"name": "pubkey",
"avatar_url": "https://avatars3.githubusercontent.com/u/8926560?v=4",
"profile": "https://twitter.com/pubkeypubkey",
"contributions": [
"doc",
"code"
]
},
{
"login": "iliyaZelenko",
"name": "Илья",
"avatar_url": "https://avatars1.githubusercontent.com/u/13103045?v=4",
"profile": "https://github.com/iliyaZelenko",
"contributions": [
"doc"
]
},
{
"login": "mohd-akram",
"name": "Mohamed Akram",
"avatar_url": "https://avatars0.githubusercontent.com/u/1823771?v=4",
"profile": "https://akr.am",
"contributions": [
"bug",
"doc",
"code"
]
},
{
"login": "emarcotte",
"name": "Eugene Marcotte",
"avatar_url": "https://avatars0.githubusercontent.com/u/249390?v=4",
"profile": "https://github.com/emarcotte",
"contributions": [
"code"
]
},
{
"login": "dsabanin",
"name": "Dima Sabanin",
"avatar_url": "https://avatars0.githubusercontent.com/u/8316?v=4",
"profile": "http://twitter.com/dimasabanin",
"contributions": [
"maintenance",
"code"
]
},
{
"login": "benabbottnz",
"name": "Ben Abbott",
"avatar_url": "https://avatars2.githubusercontent.com/u/2616473?v=4",
"profile": "https://github.com/benabbottnz",
"contributions": [
"doc"
]
},
{
"login": "dickeylth",
"name": "弘树@阿里",
"avatar_url": "https://avatars1.githubusercontent.com/u/2196373?v=4",
"profile": "http://webminer.js.org",
"contributions": [
"bug",
"doc"
]
},
{
"login": "Rantanen",
"name": "Mikko Rantanen",
"avatar_url": "https://avatars0.githubusercontent.com/u/385385?v=4",
"profile": "https://github.com/Rantanen",
"contributions": [
"bug"
]
},
{
"login": "extend1994",
"name": "Ann",
"avatar_url": "https://avatars2.githubusercontent.com/u/13430892?v=4",
"profile": "https://github.com/extend1994",
"contributions": [
"doc"
]
},
{
"login": "escitalopram",
"name": "escitalopram",
"avatar_url": "https://avatars0.githubusercontent.com/u/1155220?v=4",
"profile": "https://github.com/escitalopram",
"contributions": [
"code"
]
},
{
"login": "dependabot[bot]",
"name": "dependabot[bot]",
"avatar_url": "https://avatars0.githubusercontent.com/in/29110?v=4",
"profile": "https://github.com/apps/dependabot",
"contributions": [
"security",
"maintenance"
]
},
{
"login": "JoshuaKGoldberg",
"name": "Josh Goldberg",
"avatar_url": "https://avatars1.githubusercontent.com/u/3335181?v=4",
"profile": "http://www.joshuakgoldberg.com",
"contributions": [
"code"
]
},
{
"login": "apeckham",
"name": "Aaron",
"avatar_url": "https://avatars.githubusercontent.com/u/14110?v=4",
"profile": "https://github.com/apeckham",
"contributions": [
"doc"
]
},
{
"login": "pgrimaud",
"name": "Pierre Grimaud",
"avatar_url": "https://avatars.githubusercontent.com/u/1866496?v=4",
"profile": "https://github.com/pgrimaud",
"contributions": [
"doc"
]
},
{
"login": "domdomegg",
"name": "Adam Jones",
"avatar_url": "https://avatars.githubusercontent.com/u/4953590?v=4",
"profile": "https://domdomegg.github.io/",
"contributions": [
"doc"
]
},
{
"login": "charguer",
"name": "Arthur Charguéraud",
"avatar_url": "https://avatars.githubusercontent.com/u/1830652?v=4",
"profile": "https://github.com/charguer",
"contributions": [
"doc"
]
},
{
"login": "Pierrci",
"name": "Pierric Cistac",
"avatar_url": "https://avatars.githubusercontent.com/u/5020707?v=4",
"profile": "https://twitter.com/pierrci",
"contributions": [
"doc",
"code"
]
},
{
"login": "xlith",
"name": "Civan Yavuzşen",
"avatar_url": "https://avatars.githubusercontent.com/u/510560?v=4",
"profile": "https://github.com/xlith",
"contributions": [
"code"
]
},
{
"login": "timgates42",
"name": "Tim Gates",
"avatar_url": "https://avatars.githubusercontent.com/u/47873678?v=4",
"profile": "https://github.com/timgates42",
"contributions": [
"doc"
]
},
{
"login": "campersau",
"name": "campersau",
"avatar_url": "https://avatars.githubusercontent.com/u/4009570?v=4",
"profile": "https://github.com/campersau",
"contributions": [
"code"
]
},
{
"login": "dependabot-preview[bot]",
"name": "dependabot-preview[bot]",
"avatar_url": "https://avatars.githubusercontent.com/in/2141?v=4",
"profile": "https://github.com/apps/dependabot-preview",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7
}

View file

@ -1,44 +0,0 @@
### Step -1: Before filling an issue check out troubleshooting section
- Go to [README.md#Troubleshooting](https://github.com/rtfpessoa/diff2html#troubleshooting)
### Step 0: Describe your environment
- OS: **\_**
- diff2html version: **\_**
- Using diff2html directly or using diff2html-ui helper: **\_**
- Extra flags: **\_**
### Step 1: Describe the problem:
#### Steps to reproduce:
1. ***
2. ***
3. ***
#### diff example:
```diff
diff --git describe.c
index fabadb8,cc95eb0..4866510
--- a/describe.c
+++ b/describe.c
@@@ -98,20 -98,12 +98,20 @@@
return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1;
}
```
#### Observed Results:
- What happened? This could be a description, log output, etc.
#### Expected Results:
- What did you expect to happen?
#### Relevant Code:
```
// TODO(you): code here to reproduce the problem
```

View file

@ -1,23 +0,0 @@
---
name: Bug report about: Create a report to help us improve title: "" labels: "" assignees: "" ---**Describe the bug** A
clear and concise description of what the bug is.
**To Reproduce** Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior** A clear and concise description of what you expected to happen.
**Screenshots** If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. Windows, Linux, Mac]
- Browser [e.g. Firefox, Chrome, Safari]
- Version [e.g. 22]
**Additional context** Add any other context about the problem here.

View file

@ -1,12 +0,0 @@
---
name: Feature request about: Suggest an idea for this project title: "" labels: "" assignees: "" ---**Is your feature
request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always
frustrated when [...]
**Describe the solution you'd like** A clear and concise description of what you want to happen.
**Describe alternatives you've considered** A clear and concise description of any alternative solutions or features
you've considered.
**Additional context** Add any other context or screenshots about the feature request here.

View file

@ -1,12 +0,0 @@
name: ci
on:
pull_request:
branches: [master]
jobs:
test-and-publish:
uses: ./.github/workflows/test-and-publish.yml
with:
environment: dev
secrets: inherit

View file

@ -1,35 +0,0 @@
name: release
on:
push:
branches: [master]
jobs:
test-and-publish:
uses: ./.github/workflows/test-and-publish.yml
with:
environment: production
secrets: inherit
publish-website:
runs-on: ubuntu-latest
container:
image: amazon/aws-cli
needs: [test-and-publish]
environment: 'production'
steps:
- name: Download docs
uses: actions/download-artifact@v4
with:
name: docs
path: docs/
- name: Publish to S3
working-directory: docs
env:
AWS_CF_DISTRIBUTION_ID: ${{ secrets.AWS_CF_DISTRIBUTION_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
run: |
aws s3 sync --region eu-west-1 --delete . s3://diff2html.xyz --metadata-directive REPLACE --cache-control max-age=31557600
aws cloudfront create-invalidation --region eu-west-1 --distribution-id $AWS_CF_DISTRIBUTION_ID --paths /index.html /demo.html /sitemap.xml /robots.txt

View file

@ -1,148 +0,0 @@
name: test-and-publish
on:
workflow_call:
inputs:
environment:
required: true
type: string
jobs:
version:
runs-on: ubuntu-latest
container:
image: codacy/git-version
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Fix tar access
run: apk add --update --no-progress tar
- name: Fix git access
run: |
git config --global --add safe.directory /__w/diff2html/diff2html
- name: Get next version
run: |
export NEXT_VERSION="$(/bin/git-version --folder=$PWD --release-branch=master)"
echo "Next version is ${NEXT_VERSION}"
echo "${NEXT_VERSION}" > next-version.txt
echo "version=${NEXT_VERSION}" >> $GITHUB_ENV
- name: Get next npm tag name
run: |
if [ "${GITHUB_REF_NAME}" = "master" ]; then
export PUBLISH_TAG="latest"
elif [ "${GITHUB_REF_NAME}" = "next" ]; then
export PUBLISH_TAG="next"
else
export PUBLISH_TAG="pr"
fi
echo "Next tag is ${PUBLISH_TAG}"
echo "${PUBLISH_TAG}" > publish-tag.txt
- name: Upload versions
uses: actions/upload-artifact@v4
with:
name: versions
if-no-files-found: error
path: |
next-version.txt
publish-tag.txt
build:
runs-on: ubuntu-latest
needs: [version]
strategy:
matrix:
node-version: [16.x, 18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Log environment setup
run: |
node -v
npm -v
- name: Install dependencies
run: npm install
- name: Build templates
run: npm run build:templates
- name: Build library
run: npm run build
publish:
runs-on: ubuntu-latest
needs: [build]
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Download versions
uses: actions/download-artifact@v4
with:
name: versions
- name: Store version
run: echo "version=$(cat next-version.txt)" >> $GITHUB_ENV
- name: Configure Git
run: |
git config user.email "gh-actions@users.noreply.github.com"
git config user.name "GitHub Actions"
- name: Tag commit
uses: tvdias/github-tagger@v0.0.1
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
tag: '${{ env.version }}'
- name: Install dependencies
run: npm install
- uses: actions/setup-node@v3
with:
registry-url: 'https://registry.npmjs.org'
node-version: '22.x'
- name: Configure NPM version
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
rm -f .npmrc
touch .npmrc
echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc
echo "registry=https://registry.npmjs.org/" >> .npmrc
echo "access=public" >> .npmrc
echo "save-exact=true" >> .npmrc
- name: Version package
run: |
# Update version in packages to publish
npm version $(cat next-version.txt) -m "Release version %s"
- name: Publish to NPM
run: npm publish --tag $(cat publish-tag.txt)
- uses: actions/setup-node@v3
with:
node-version: '22.x'
registry-url: 'https://npm.pkg.github.com'
- name: Configure NPM version
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -f .npmrc
touch .npmrc
echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc
echo "@rtfpessoa:registry=https://npm.pkg.github.com/" >> .npmrc
echo "access=public" >> .npmrc
echo "save-exact=true" >> .npmrc
- name: Publish to GPR
run: |
# HACK: Override npm package name to be able to publish in GitHub
sed -i 's/^ "name":.*/ "name": "@rtfpessoa\/diff2html",/g' package.json
echo "Going to publish version $(cat next-version.txt) to GitHub"
npm publish --tag $(cat publish-tag.txt)
# HACK: Restore npm package name
sed -i 's/^ "name":.*/ "name": "diff2html",/g' package.json
- name: Upload docs
uses: actions/upload-artifact@v4
with:
name: docs
if-no-files-found: error
path: docs/

22
.gitignore vendored
View file

@ -18,29 +18,9 @@ target/
# Node
node_modules/
npm-debug.log
yarn-error.log
# Coverage
# Istanbul
coverage/
# Bower
bower_components/
# Terraform
/terraform/.terraform
# Compiled templates
/src/diff2html-templates.*
# Compiled website
/docs/
# Bundles temporary typescript files compiled by webpack
/bundles-out/
# Web bundles
/bundles/
# CommonJS library
/lib/
# ESNext library
/lib-esm/

1
.husky/.gitignore vendored
View file

@ -1 +0,0 @@
_

View file

@ -1,3 +0,0 @@
#!/bin/sh
npm run lint:staged

49
.jscsrc Normal file
View file

@ -0,0 +1,49 @@
{
"disallowKeywords": [
"with"
],
"disallowKeywordsOnNewLine": [
"else"
],
"disallowMixedSpacesAndTabs": true,
"disallowMultipleVarDecl": {
"allExcept": [
"undefined"
]
},
"disallowNewlineBeforeBlockStatements": true,
"disallowSpaceAfterObjectKeys": true,
"disallowSpaceAfterPrefixUnaryOperators": true,
"disallowSpacesInFunction": {
"beforeOpeningRoundBrace": true
},
"disallowSpacesInsideParentheses": true,
"disallowTrailingWhitespace": true,
"maximumLineLength": 130,
"requireCamelCaseOrUpperCaseIdentifiers": true,
"requireCapitalizedConstructors": true,
"requireSpaceAfterKeywords": [
"if",
"else",
"for",
"while",
"do",
"switch",
"case",
"return",
"try",
"catch",
"typeof"
],
"requireSpaceAfterLineComment": true,
"requireSpaceAfterBinaryOperators": true,
"requireSpaceBeforeBinaryOperators": true,
"requireSpaceBeforeBlockStatements": true,
"requireSpaceBeforeObjectValues": true,
"requireSpacesInFunction": {
"beforeOpeningCurlyBrace": true
},
"requireTrailingComma": null,
"validateIndentation": 2,
"validateLineBreaks": "LF"
}

View file

@ -1,16 +0,0 @@
{
"arrowParens": "avoid",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"jsxSingleQuote": false,
"printWidth": 120,
"proseWrap": "always",
"quoteProps": "as-needed",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

View file

@ -1,3 +0,0 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}

1
CNAME
View file

@ -1 +0,0 @@
diff2html.xyz

View file

@ -1,61 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education,
socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
project or its community. Examples of representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed representative at an online or offline
event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
rtfrodrigo [at] gmail [dot] com. All complaints will be reviewed and investigated and will result in a response that is
deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with
regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq

View file

@ -2,33 +2,31 @@
### Main rules
- 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.
* 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.
- If you're proposing a new feature, make sure you create an issue to let other contributors know what you are working
on.
* If you're proposing a new feature, make sure you create an issue to let other contributors know what you are working on.
- Before sending a pull request make sure your code is tested.
* Before sending a pull request make sure your code is tested.
- Before sending a pull request for a feature, be sure to run tests with `npm run test`.
* Before sending a pull request for a feature, be sure to run tests with `npm test`.
- Use the same coding style as the rest of the codebase, most of the check can be performed with `npm run run lint`.
* Use the same coding style as the rest of the codebase, most of the check can be performed with `npm run style`.
- Use `git rebase` (not `git merge`) to sync your work from time to time with the master branch.
* Use `git rebase` (not `git merge`) to sync your work from time to time with the master branch.
- After creating your pull request make sure the build is passing on
[CircleCI](https://circleci.com/gh/rtfpessoa/diff2html) and that [Codacy](https://www.codacy.com/app/Codacy/diff2html)
is also confident in the code quality.
* After creating your pull request make sure the build is passing on [CircleCI](https://circleci.com/gh/rtfpessoa/diff2html)
and that [Codacy](https://www.codacy.com/app/Codacy/diff2html) is also confident in the code quality.
- In your pull request, do not commit the `dist` or `build` folder if you needed to build the release files.
* In your pull request, do not commit the `dist` or `build` folder if you needed to build the release files.
### Commit Style
Writing good commit logs is important. A commit log should describe what changed and why. Follow these guidelines when
writing one:
Writing good commit logs is important. A commit log should describe what changed and why.
Follow these guidelines when writing one:
1. The first line should be 50 characters or less and contain a short description of the change prefixed with the name
of the changed subsystem (e.g. "net: add localAddress and localPort to Socket").
1. The first line should be 50 characters or less and contain a short
description of the change prefixed with the name of the changed
subsystem (e.g. "net: add localAddress and localPort to Socket").
2. Keep the second line blank.
3. Wrap all other lines at 72 columns.
@ -51,11 +49,14 @@ nicely even when it is indented.
By making a contribution to this project, I certify that:
- (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source
license indicated in the file; or
- (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate
open source license and I have the right under that license to submit that work with modifications, whether created in
whole or in part by me, under the same open source license (unless I am permitted to submit under a different
license), as indicated in the file; or
- (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not
modified it.
* (a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license indicated
in the file; or
* (b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source license
and I have the right under that license to submit that work with
modifications, whether created in whole or in part by me, under the
same open source license (unless I am permitted to submit under a
different license), as indicated in the file; or
* (c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified it.

View file

@ -1,11 +1,12 @@
# Credits
This is the list of all the kind people that have contributed to the diff2html project. This list is ordered by first
contribution.
This is the list of all the kind people that have contributed to the diff2html project.
This list is ordered by first contribution.
Thanks, @rtfpessoa
Thanks,
@rtfpessoa
---
----------
Rodrigo Fernandes, [@rtfpessoa](https://github.com/rtfpessoa)

20
LICENSE Normal file
View file

@ -0,0 +1,20 @@
Copyright 2014-2016 Rodrigo Fernandes https://rtfpessoa.github.io/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,14 +0,0 @@
Copyright 2014-2016 Rodrigo Fernandes https://rtfpessoa.github.io/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

627
README.md
View file

@ -1,613 +1,180 @@
# diff2html
[![npm](https://img.shields.io/npm/v/diff2html)](https://www.npmjs.com/package/diff2html)
[![node](https://img.shields.io/node/v/diff2html)](https://www.npmjs.com/package/diff2html)
[![npm](https://img.shields.io/npm/l/diff2html)](https://www.npmjs.com/package/diff2html)
[![GitHub Actions](https://github.com/rtfpessoa/diff2html/actions/workflows/release.yml/badge.svg)](https://github.com/rtfpessoa/diff2html/actions/workflows/release.yml)
[![Circle CI](https://circleci.com/gh/rtfpessoa/diff2html.svg?style=svg)](https://circleci.com/gh/rtfpessoa/diff2html)
[![Codacy Code Badge](https://api.codacy.com/project/badge/grade/06412dc3f5a14f568778d0db8a1f7dc8)](https://www.codacy.com/app/Codacy/diff2html)
[![Codacy Coverage Badge](https://api.codacy.com/project/badge/coverage/06412dc3f5a14f568778d0db8a1f7dc8)](https://www.codacy.com/app/Codacy/diff2html)
[![npm weekly downloads](https://img.shields.io/npm/dw/diff2html)](https://www.npmjs.com/package/diff2html)
[![npm monthly downloads](https://img.shields.io/npm/dm/diff2html)](https://www.npmjs.com/package/diff2html)
[![npm yearly downloads](https://img.shields.io/npm/dy/diff2html)](https://www.npmjs.com/package/diff2html)
[![npm downloads](https://img.shields.io/npm/dt/diff2html)](https://www.npmjs.com/package/diff2html)
[![npm](https://img.shields.io/npm/v/diff2html.svg)](https://www.npmjs.com/package/diff2html)
[![Dependency Status](https://david-dm.org/rtfpessoa/diff2html.svg)](https://david-dm.org/rtfpessoa/diff2html)
[![devDependency Status](https://david-dm.org/rtfpessoa/diff2html/dev-status.svg)](https://david-dm.org/rtfpessoa/diff2html#info=devDependencies)
[![jsdelivr CDN Downloads](https://data.jsdelivr.com/v1/package/npm/diff2html/badge)](https://www.jsdelivr.com/package/npm/diff2html)
[![All Contributors](https://img.shields.io/badge/all_contributors-22-orange)](#contributors)
[![node](https://img.shields.io/node/v/diff2html.svg)]()
[![npm](https://img.shields.io/npm/l/diff2html.svg)]()
[![npm](https://img.shields.io/npm/dm/diff2html.svg)](https://www.npmjs.com/package/diff2html)
[![Gitter](https://badges.gitter.im/rtfpessoa/diff2html.svg)](https://gitter.im/rtfpessoa/diff2html?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
diff2html generates pretty HTML diffs from git diff or unified diff output.
diff2html generates pretty HTML diffs from git or unified diff output.
## Table of Contents
<!-- toc -->
- [Features](#features)
- [Online Example](#online-example)
- [Distributions](#distributions)
- [Usage](#usage)
- [Diff Text Input](#diff-text-input)
- [Diff2HtmlUI Usage](#diff2htmlui-usage)
- [Diff2HtmlUI API](#diff2htmlui-api)
- [Diff2HtmlUI Configuration](#diff2htmlui-configuration)
- [Diff2HtmlUI Browser](#diff2htmlui-browser)
- [Diff2HtmlUI Examples](#diff2htmlui-examples)
- [Diff2Html Usage](#diff2html-usage)
- [Diff2Html API](#diff2html-api)
- [Diff2Html Configuration](#diff2html-configuration)
- [Diff2Html Browser](#diff2html-browser)
- [Diff2Html NPM / Node.js Library](#diff2html-npm--nodejs-library)
- [Diff2Html Examples](#diff2html-examples)
- [Troubleshooting](#troubleshooting)
- [1. Out of memory or Slow execution](#1-out-of-memory-or-slow-execution)
- [Contribute](#contribute)
- [Contributors](#contributors)
- [License](#license)
- [Thanks](#thanks)
<!-- tocstop -->
[![NPM](https://nodei.co/npm/diff2html.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/diff2html/)
## Features
- Supports git and unified diffs
* Supports git and unified diffs
- Line by line and Side by side diff
* Line by line and Side by side diff
- New and old line numbers
* New and old line numbers
- Inserted and removed lines
* Inserted and removed lines
- GitHub like visual style
* GitHub like style
- Code syntax highlight
* Code syntax highlight
- Line similarity matching
* Line similarity matching
- Easy code selection
* Easy code selection
## Online Example
> Go to [diff2html](https://diff2html.xyz/demo.html)
> Go to [diff2html](http://rtfpessoa.github.io/diff2html/)
## Distributions
- [jsdelivr CDN](https://www.jsdelivr.com/package/npm/diff2html)
- [WebJar](http://www.webjars.org/)
- [Node Library](https://www.npmjs.org/package/diff2html)
- [NPM CLI](https://www.npmjs.org/package/diff2html-cli)
- Manually use from jsdelivr or build the project:
- Browser / Bundle
- Parser and HTML Generator
- [bundles/js/diff2html.min.js](https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js) - includes the
diff parser and html generator
- Wrapper and helper adding syntax highlight, synchronized scroll, and other nice features
- [bundles/js/diff2html-ui.min.js](https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js) -
includes the wrapper of diff2html with highlight for all `highlight.js` supported languages
- [bundles/js/diff2html-ui-slim.min.js](https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui-slim.min.js) -
includes the wrapper of diff2html with "the most common" `highlight.js` supported languages
- [bundles/js/diff2html-ui-base.min.js](https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui-base.min.js) -
includes the wrapper of diff2html without including a `highlight.js` implementation. You can use it without
syntax highlight or by passing your own implementation with the languages you prefer
- NPM / Node.js library
- ES5
- [lib/diff2html.js](https://cdn.jsdelivr.net/npm/diff2html/lib/diff2html.js) - includes the diff parser and html
generator
- [lib/ui/js/diff2html-ui.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui.js) - includes the
wrapper of diff2html with highlight for all `highlight.js` supported languages
- [lib/ui/js/diff2html-ui-slim.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui-slim.js) -
includes the wrapper of diff2html with "the most common" `highlight.js` supported languages
- [lib/ui/js/diff2html-ui-base.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui-base.js)
- ES6
- [lib-esm/diff2html.js](https://cdn.jsdelivr.net/npm/diff2html/lib-esm/diff2html.js) - includes the diff parser
and html generator
- [lib/ui/js/diff2html-ui.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui.js) - includes the
wrapper of diff2html with highlight for all `highlight.js` supported languages
- [lib/ui/js/diff2html-ui-slim.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui-slim.js) -
includes the wrapper of diff2html with "the most common" `highlight.js` supported languages
- [lib/ui/js/diff2html-ui-base.js](https://cdn.jsdelivr.net/npm/diff2html/lib/ui/js/diff2html-ui-base.js) -
includes the wrapper of diff2html without including a `highlight.js` implementation. You can use it without
syntax highlight or by passing your own implementation with the languages you prefer
* [WebJar](http://www.webjars.org/)
## Usage
* [Node Module](https://www.npmjs.org/package/diff2html)
Diff2Html can be used in various ways as listed in the [distributions](#distributions) section. The two main ways are:
* [Bower Package](http://bower.io/search/?q=diff2html)
- [Diff2HtmlUI](#diff2htmlui-usage): using this wrapper makes it easy to inject the html in the DOM and adds some nice
features to the diff, like syntax highlight.
- [Diff2Html](#diff2html-usage): using the parser and html generator directly from the library gives you complete
control about what you can do with the json or html generated.
* [Node CLI](https://www.npmjs.org/package/diff2html-cli)
Below you can find more details and examples about each option.
* Manually download and import `dist/diff2html.min.js` into your page
## Diff Text Input
## How to use
diff2html accepts the text contents of a
[unified diff](https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html) or the superset format git
diff (https://git-scm.com/docs/git-diff) (not combined or word diff). To provide multiples files as input just
concatenate the diffs (just like the output of git diff).
> Pretty HTML diff
## Diff2HtmlUI Usage
Diff2Html.getPrettyHtml(exInput, configuration)
> Intermediate Json From Git Word Diff Output
Diff2Html.getJsonFromDiff(exInput)
> Check out the `index.html` for a complete example.
## Configuration
The HTML output accepts a Javascript object with configuration. Possible options:
- `inputFormat`: the format of the input data: `'diff'` or `'json'`, default is `'diff'`
- `outputFormat`: the format of the output data: `'line-by-line'` or `'side-by-side'`, default is `'line-by-line'`
- `showFiles`: show a file list before the diff: `true` or `false`, default is `false`
- `matching`: matching level: `'lines'` for matching lines, `'words'` for matching lines and words or `'none'`, default is `none`
- `matchWordsThreshold`: similarity threshold for word matching, default is 0.25
- `matchingMaxComparisons`: perform at most this much comparisons for line matching a block of changes, default is `2500`
## Diff2HtmlUI Helper
> Simple wrapper to ease simple tasks in the browser such as: code highlight and js effects
- Invoke Diff2html
- Inject output in DOM element
- Enable collapsible file summary list
- Enable syntax highlight of the code in the diffs
### How to use
### Diff2HtmlUI API
> Create a Diff2HtmlUI instance
```ts
constructor(target: HTMLElement, diffInput?: string | DiffFile[]) // diff2html-ui, diff2html-ui-slim
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}, hljs?: HighlightJS) // diff2html-ui-base
```
> Generate and inject in the document the Pretty HTML representation of the diff
```ts
draw(): void
```
> Enable extra features
```ts
synchronisedScroll(): void
fileListToggle(startVisible: boolean): void
highlightCode(): void
stickyFileHeaders(): void
```
### Diff2HtmlUI Configuration
- `synchronisedScroll`: scroll both panes in side-by-side mode: `true` or `false`, default is `true`
- `highlight`: syntax highlight the code on the diff: `true` or `false`, default is `true`
- `fileListToggle`: allow the file summary list to be toggled: `true` or `false`, default is `true`
- `fileListStartVisible`: choose if the file summary list starts visible: `true` or `false`, default is `false`
- `fileContentToggle`: allow each file contents to be toggled: `true` or `false`, default is `true`
- `stickyFileHeaders`: make file headers sticky: `true` or `false`, default is `true`
- [All the options](#diff2html-configuration) from Diff2Html are also valid configurations in Diff2HtmlUI
### Diff2HtmlUI Browser
#### Mandatory HTML resource imports
> HTML resource imports
```html
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<link rel="stylesheet" type="text/css" href="dist/diff2html.css">
<!-- Javascripts -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script type="text/javascript" src="dist/diff2html.js"></script>
<script type="text/javascript" src="dist/diff2html-ui.js"></script>
```
#### Init
> Init
```js
const targetElement = document.getElementById('destination-elem-id');
const configuration = { drawFileList: true, matching: 'lines' };
const diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
var diff2htmlUi = new Diff2HtmlUI({diff: diffString});
// or
const diff2htmlUi = new Diff2HtmlUI(targetElement, diffJson, configuration);
var diff2htmlUi = new Diff2HtmlUI({json: diffJson});
```
#### Draw
> Draw
```js
diff2htmlUi.draw();
diff2htmlUi.draw('html-target-elem', {inputFormat: 'json', showFiles: true, matching: 'lines'});
```
#### Syntax Highlight
> Highlight Code
**NOTE:** The highlight.js css should come before the diff2html css
```js
diff2htmlUi.highlightCode('html-target-elem');
```
> Collapse File Summary List
```js
diff2htmlUi.fileListCloseable('html-target-elem', false);
```
## Syntax Highlight
> Add the dependencies.
Choose one color scheme, and add the main highlight code.
If your favourite language is not included in the default package also add its javascript highlight file.
```html
<!-- Stylesheet -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css" />
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/styles/github.min.css">
<!-- Javascripts -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.3.0/languages/scala.min.js"></script>
<script type="text/javascript" src="dist/diff2html-ui.js"></script>
```
> Pass the option `highlight` with value true or invoke `diff2htmlUi.highlightCode()` after `diff2htmlUi.draw()`.
> Invoke the Diff2HtmlUI helper
```js
document.addEventListener('DOMContentLoaded', () => {
const diffString = `diff --git a/sample.js b/sample.js
index 0000001..0ddf2ba
--- a/sample.js
+++ b/sample.js
@@ -1 +1 @@
-console.log("Hello World!")
+console.log("Hello from Diff2Html!")`;
const targetElement = document.getElementById('myDiffElement');
const configuration = { drawFileList: true, matching: 'lines', highlight: true };
const diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
$(document).ready(function() {
var diff2htmlUi = new Diff2HtmlUI({diff: lineDiffExample});
diff2htmlUi.draw('#line-by-line', {inputFormat: 'json', showFiles: true, matching: 'lines'});
diff2htmlUi.highlightCode('#line-by-line');
});
```
When using the `auto` color scheme, you will need to specify both the light and dark themes for highlight.js to use.
```html
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github.min.css"
media="screen and (prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/styles/github-dark.min.css"
media="screen and (prefers-color-scheme: dark)"
/>
```
#### Collapsable File Summary List
## Collapsable File Summary List
> Add the dependencies.
```html
<!-- Javascripts -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.js"></script>
<script type="text/javascript" src="dist/diff2html-ui.js"></script>
```
> Invoke the Diff2HtmlUI helper Pass the option `fileListToggle` with value true or invoke
> `diff2htmlUi.fileListToggle()` after `diff2htmlUi.draw()`.
> Invoke the Diff2HtmlUI helper
```js
document.addEventListener('DOMContentLoaded', () => {
const targetElement = document.getElementById('myDiffElement');
var diff2htmlUi = new Diff2HtmlUI(targetElement, lineDiffExample, { drawFileList: true, matching: 'lines' });
diff2htmlUi.draw();
diff2htmlUi.fileListToggle(false);
$(document).ready(function() {
var diff2htmlUi = new Diff2HtmlUI({diff: lineDiffExample});
diff2htmlUi.draw('#line-by-line', {inputFormat: 'json', showFiles: true, matching: 'lines'});
diff2htmlUi.fileListCloseable('#line-by-line', false);
});
```
### Diff2HtmlUI Examples
## Contributions
#### Example with plain HTML+CSS+JS
```html
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8" />
<!-- Make sure to load the highlight.js CSS file before the Diff2Html CSS file -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css" />
<link
rel="stylesheet"
type="text/css"
href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"
/>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
</head>
<script>
const diffString = `diff --git a/sample.js b/sample.js
index 0000001..0ddf2ba
--- a/sample.js
+++ b/sample.js
@@ -1 +1 @@
-console.log("Hello World!")
+console.log("Hello from Diff2Html!")`;
document.addEventListener('DOMContentLoaded', function () {
var targetElement = document.getElementById('myDiffElement');
var configuration = {
drawFileList: true,
fileListToggle: false,
fileListStartVisible: false,
fileContentToggle: false,
matching: 'lines',
outputFormat: 'side-by-side',
synchronisedScroll: true,
highlight: true,
renderNothingWhenEmpty: false,
};
var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
});
</script>
<body>
<div id="myDiffElement"></div>
</body>
</html>
```
#### StimulusJS with TypeScript
```ts
import { Controller } from '@hotwired/stimulus';
import { Diff2HtmlUI, Diff2HtmlUIConfig } from 'diff2html/lib/ui/js/diff2html-ui-slim.js';
// Requires `npm install highlight.js`
import 'highlight.js/styles/github.css';
import 'diff2html/bundles/css/diff2html.min.css';
export default class extends Controller {
connect(): void {
const diff2htmlUi = new Diff2HtmlUI(this.diffElement, this.unifiedDiff, this.diffConfiguration);
diff2htmlUi.draw();
}
get unifiedDiff(): string {
return this.data.get('unifiedDiff') || '';
}
get diffElement(): HTMLElement {
return this.element as HTMLElement;
}
get diffConfiguration(): Diff2HtmlUIConfig {
return {
drawFileList: true,
matching: 'lines',
};
}
}
```
## Diff2Html Usage
### Diff2Html API
> JSON representation of the diff
```ts
function parse(diffInput: string, configuration: Diff2HtmlConfig = {}): DiffFile[];
```
> Pretty HTML representation of the diff
```ts
function html(diffInput: string | DiffFile[], configuration: Diff2HtmlConfig = {}): string;
```
### Diff2Html Configuration
The HTML output accepts a Javascript object with configuration. Possible options:
- `outputFormat`: the format of the output data: `'line-by-line'` or `'side-by-side'`, default is `'line-by-line'`
- `drawFileList`: show a file list before the diff: `true` or `false`, default is `true`
- `srcPrefix`: add a prefix to all source (before changes) filepaths, default is `''`. Should match the prefix used when
[generating the diff](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---src-prefixltprefixgt).
- `dstPrefix`: add a prefix to all destination (after changes) filepaths, default is `''`. Should match the prefix used
when [generating the diff](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---dst-prefixltprefixgt)
- `diffMaxChanges`: number of changed lines after which a file diff is deemed as too big and not displayed, default is
`undefined`
- `diffMaxLineLength`: number of characters in a diff line after which a file diff is deemed as too big and not
displayed, default is `undefined`
- `diffTooBigMessage`: function allowing to customize the message in case of file diff too big (if `diffMaxChanges` or
`diffMaxLineLength` is set). Will be given a file index as a number and should return a string.
- `matching`: matching level: `'lines'` for matching lines, `'words'` for matching lines and words or `'none'`, default
is `none`
- `matchWordsThreshold`: similarity threshold for word matching, default is `0.25`
- `maxLineLengthHighlight`: only perform diff changes highlight if lines are smaller than this, default is `10000`
- `diffStyle`: show differences level in each line: `'word'` or `'char'`, default is `'word'`
- `renderNothingWhenEmpty`: render nothing if the diff shows no change in its comparison: `true` or `false`, default is
`false`
- `matchingMaxComparisons`: perform at most this much comparisons for line matching a block of changes, default is
`2500`
- `maxLineSizeInBlockForComparison`: maximum number of characters of the bigger line in a block to apply comparison,
default is `200`
- `compiledTemplates`: object ([Hogan.js](https://github.com/twitter/hogan.js/) template values) with previously
compiled templates to replace parts of the html, default is `{}`. For example:
`{ "tag-file-changed": Hogan.compile("<span class="d2h-tag d2h-changed d2h-changed-tag">MODIFIED</span>") }`
- `rawTemplates`: object (string values) with raw not compiled templates to replace parts of the html, default is `{}`.
For example: `{ "tag-file-changed": "<span class="d2h-tag d2h-changed d2h-changed-tag">MODIFIED</span>" }`
> For more information regarding the possible templates look into
> [src/templates](https://github.com/rtfpessoa/diff2html/tree/master/src/templates)
- `highlightLanguages`: Map of extension to language name, used for highlighting. This overrides the default language
detection based on file extensions.
- `colorScheme`: color scheme to use for the diff, default is `light`. Possible values are `light`, `dark`, and `auto`
which will use the browser's preferred color scheme.
### Diff2Html Browser
Import the stylesheet and the library code.
To load correctly in the Browser you need to include the stylesheet in the final HTML.
```html
<!-- CSS -->
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<!-- Javascripts -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script>
```
It will now be available as a global variable named `Diff2Html`.
```js
document.addEventListener('DOMContentLoaded', () => {
var diffHtml = Diff2Html.html('<Unified Diff String>', {
drawFileList: true,
matching: 'lines',
outputFormat: 'side-by-side',
});
document.getElementById('destination-elem-id').innerHTML = diffHtml;
});
```
### Diff2Html NPM / Node.js Library
```js
const Diff2html = require('diff2html');
const diffJson = Diff2html.parse('<Unified Diff String>');
const diffHtml = Diff2html.html(diffJson, { drawFileList: true });
console.log(diffHtml);
```
### Diff2Html Examples
#### Example with Angular
- Typescript
```typescript
import * as Diff2Html from 'diff2html';
import { Component, OnInit } from '@angular/core';
export class AppDiffComponent implements OnInit {
outputHtml: string;
constructor() {
this.init();
}
ngOnInit() {}
init() {
let strInput =
'--- a/server/vendor/golang.org/x/sys/unix/zsyscall_linux_mipsle.go\n+++ b/server/vendor/golang.org/x/sys/unix/zsyscall_linux_mipsle.go\n@@ -1035,6 +1035,17 @@ func Prctl(option int, arg2 uintptr, arg3 uintptr, arg4 uintptr, arg5 uintptr) (\n \n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n+func Pselect(nfd int, r *FdSet, w *FdSet, e *FdSet, timeout *Timespec, sigmask *Sigset_t) (n int, err error) {\n+\tr0, _, e1 := Syscall6(SYS_PSELECT6, uintptr(nfd), uintptr(unsafe.Pointer(r)), uintptr(unsafe.Pointer(w)), uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)))\n+\tn = int(r0)\n+\tif e1 != 0 {\n+\t\terr = errnoErr(e1)\n+\t}\n+\treturn\n+}\n+\n+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n+\n func read(fd int, p []byte) (n int, err error) {\n \tvar _p0 unsafe.Pointer\n \tif len(p) > 0 {\n';
let outputHtml = Diff2Html.html(strInput, { drawFileList: true, matching: 'lines' });
this.outputHtml = outputHtml;
}
}
```
- HTML
```html
<!doctype html>
<html>
<head>
<title>diff2html</title>
</head>
<body>
<div [innerHtml]="outputHtml"></div>
</body>
</html>
```
- `.angular-cli.json` - Add styles
```json
"styles": [
"diff2html.min.css"
]
```
#### Example with Vue.js
```vue
<template>
<div v-html="prettyHtml" />
</template>
<script>
import * as Diff2Html from 'diff2html';
import 'diff2html/bundles/css/diff2html.min.css';
export default {
data() {
return {
diffs:
'--- a/server/vendor/golang.org/x/sys/unix/zsyscall_linux_mipsle.go\n+++ b/server/vendor/golang.org/x/sys/unix/zsyscall_linux_mipsle.go\n@@ -1035,6 +1035,17 @@ func Prctl(option int, arg2 uintptr, arg3 uintptr, arg4 uintptr, arg5 uintptr) (\n \n // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n \n+func Pselect(nfd int, r *FdSet, w *FdSet, e *FdSet, timeout *Timespec, sigmask *Sigset_t) (n int, err error) {\n+\tr0, _, e1 := Syscall6(SYS_PSELECT6, uintptr(nfd), uintptr(unsafe.Pointer(r)), uintptr(unsafe.Pointer(w)), uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(timeout)), uintptr(unsafe.Pointer(sigmask)))\n+\tn = int(r0)\n+\tif e1 != 0 {\n+\t\terr = errnoErr(e1)\n+\t}\n+\treturn\n+}\n+\n+// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT\n+\n func read(fd int, p []byte) (n int, err error) {\n \tvar _p0 unsafe.Pointer\n \tif len(p) > 0 {\n',
};
},
computed: {
prettyHtml() {
return Diff2Html.html(this.diffs, {
drawFileList: true,
matching: 'lines',
outputFormat: 'side-by-side',
});
},
},
};
</script>
```
## Troubleshooting
### 1. Out of memory or Slow execution
#### Causes:
- Big files
- Big lines
#### Fix:
- Disable the line matching algorithm, by setting the option `{"matching": "none"}` when invoking diff2html
## Contribute
This is a developer friendly project, all the contributions are welcome. To contribute just send a pull request with
your changes following the guidelines described in `CONTRIBUTING.md`. I will try to review them as soon as possible.
## Contributors
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://rtfpessoa.xyz"><img src="https://avatars0.githubusercontent.com/u/902384?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rodrigo Fernandes</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=rtfpessoa" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/stockmind"><img src="https://avatars3.githubusercontent.com/u/5653847?v=4?s=100" width="100px;" alt=""/><br /><sub><b>stockmind</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=stockmind" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/lantian"><img src="https://avatars3.githubusercontent.com/u/535545?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ivan Vorontsov</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=lantian" title="Code">💻</a></td>
<td align="center"><a href="http://www.nick-brewer.com"><img src="https://avatars1.githubusercontent.com/u/129300?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nick Brewer</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=brewern" title="Code">💻</a></td>
<td align="center"><a href="http://heyitsmattwade.com"><img src="https://avatars0.githubusercontent.com/u/8504000?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Matt Wade</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Aromellem" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=romellem" title="Code">💻</a></td>
<td align="center"><a href="http://mrfyda.github.io"><img src="https://avatars1.githubusercontent.com/u/593860?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Rafael Cortês</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=mrfyda" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/nmatpt"><img src="https://avatars2.githubusercontent.com/u/5034733?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nuno Teixeira</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=nmatpt" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://saino.me/"><img src="https://avatars0.githubusercontent.com/u/1567423?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Koki Oyatsu</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Akaishuu0123" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=kaishuu0123" title="Code">💻</a></td>
<td align="center"><a href="http://www.jamesmonger.com"><img src="https://avatars2.githubusercontent.com/u/2037007?v=4?s=100" width="100px;" alt=""/><br /><sub><b>James Monger</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=Jameskmonger" title="Documentation">📖</a></td>
<td align="center"><a href="http://wesssel.github.io/"><img src="https://avatars2.githubusercontent.com/u/7767299?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Wessel van der Pal</b></sub></a><br /><a href="#security-wesssel" title="Security">🛡️</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=wesssel" title="Code">💻</a></td>
<td align="center"><a href="https://jung-kim.github.io"><img src="https://avatars2.githubusercontent.com/u/5281068?v=4?s=100" width="100px;" alt=""/><br /><sub><b>jk-kim</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=jung-kim" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/sss0791"><img src="https://avatars1.githubusercontent.com/u/1446970?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Sergey Semenov</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Asss0791" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=sss0791" title="Code">💻</a></td>
<td align="center"><a href="http://researcher.watson.ibm.com/researcher/view.php?person=us-nickm"><img src="https://avatars3.githubusercontent.com/u/4741620?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Nick Mitchell</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Astarpit" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=starpit" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/samiraguiar"><img src="https://avatars0.githubusercontent.com/u/13439135?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Samir Aguiar</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=samiraguiar" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://twitter.com/pubkeypubkey"><img src="https://avatars3.githubusercontent.com/u/8926560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>pubkey</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=pubkey" title="Documentation">📖</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=pubkey" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/iliyaZelenko"><img src="https://avatars1.githubusercontent.com/u/13103045?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Илья</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=iliyaZelenko" title="Documentation">📖</a></td>
<td align="center"><a href="https://akr.am"><img src="https://avatars0.githubusercontent.com/u/1823771?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mohamed Akram</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Amohd-akram" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=mohd-akram" title="Documentation">📖</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=mohd-akram" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/emarcotte"><img src="https://avatars0.githubusercontent.com/u/249390?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Eugene Marcotte</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=emarcotte" title="Code">💻</a></td>
<td align="center"><a href="http://twitter.com/dimasabanin"><img src="https://avatars0.githubusercontent.com/u/8316?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Dima Sabanin</b></sub></a><br /><a href="#maintenance-dsabanin" title="Maintenance">🚧</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=dsabanin" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/benabbottnz"><img src="https://avatars2.githubusercontent.com/u/2616473?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ben Abbott</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=benabbottnz" title="Documentation">📖</a></td>
<td align="center"><a href="http://webminer.js.org"><img src="https://avatars1.githubusercontent.com/u/2196373?v=4?s=100" width="100px;" alt=""/><br /><sub><b>弘树@阿里</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3Adickeylth" title="Bug reports">🐛</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=dickeylth" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/Rantanen"><img src="https://avatars0.githubusercontent.com/u/385385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Mikko Rantanen</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/issues?q=author%3ARantanen" title="Bug reports">🐛</a></td>
<td align="center"><a href="https://github.com/extend1994"><img src="https://avatars2.githubusercontent.com/u/13430892?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ann</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=extend1994" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/escitalopram"><img src="https://avatars0.githubusercontent.com/u/1155220?v=4?s=100" width="100px;" alt=""/><br /><sub><b>escitalopram</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=escitalopram" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/apps/dependabot"><img src="https://avatars0.githubusercontent.com/in/29110?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dependabot[bot]</b></sub></a><br /><a href="#security-dependabot[bot]" title="Security">🛡️</a> <a href="#maintenance-dependabot[bot]" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://www.joshuakgoldberg.com"><img src="https://avatars1.githubusercontent.com/u/3335181?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Josh Goldberg</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=JoshuaKGoldberg" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/apeckham"><img src="https://avatars.githubusercontent.com/u/14110?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Aaron</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=apeckham" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/pgrimaud"><img src="https://avatars.githubusercontent.com/u/1866496?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pierre Grimaud</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=pgrimaud" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://domdomegg.github.io/"><img src="https://avatars.githubusercontent.com/u/4953590?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Adam Jones</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=domdomegg" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/charguer"><img src="https://avatars.githubusercontent.com/u/1830652?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Arthur Charguéraud</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=charguer" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/pierrci"><img src="https://avatars.githubusercontent.com/u/5020707?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Pierric Cistac</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=Pierrci" title="Documentation">📖</a> <a href="https://github.com/rtfpessoa/diff2html/commits?author=Pierrci" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/xlith"><img src="https://avatars.githubusercontent.com/u/510560?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Civan Yavuzşen</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=xlith" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/timgates42"><img src="https://avatars.githubusercontent.com/u/47873678?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Tim Gates</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=timgates42" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/campersau"><img src="https://avatars.githubusercontent.com/u/4009570?v=4?s=100" width="100px;" alt=""/><br /><sub><b>campersau</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=campersau" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/apps/dependabot-preview"><img src="https://avatars.githubusercontent.com/in/2141?v=4?s=100" width="100px;" alt=""/><br /><sub><b>dependabot-preview[bot]</b></sub></a><br /><a href="https://github.com/rtfpessoa/diff2html/commits?author=dependabot-preview[bot]" title="Code">💻</a></td>
</tr>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification.
Contributions of any kind welcome!
This is a developer friendly project, all the contributions are welcome.
To contribute just send a pull request with your changes following the guidelines described in `CONTRIBUTING.md`.
I will try to review them as soon as possible.
## License
Copyright 2014-present Rodrigo Fernandes. Released under the terms of the MIT license.
Copyright 2014-2016 Rodrigo Fernandes. Released under the terms of the MIT license.
## Thanks
This project is inspired in [pretty-diff](https://github.com/scottgonzalez/pretty-diff) by
[Scott González](https://github.com/scottgonzalez).
This project is inspired in [pretty-diff](https://github.com/scottgonzalez/pretty-diff) by [Scott González](https://github.com/scottgonzalez).
---

View file

@ -1,35 +0,0 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 2.9.x | :white_check_mark: |
| < 2.9 | :x: |
## Reporting a Vulnerability
We take all security bugs in `diff2html` seriously. Thank you for the help improving the security of `diff2html`. We
appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions.
Report security bugs by emailing the lead maintainer at `rtfrodrigo [at] gmail [dot] com`.
The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours
indicating the next steps in handling your report. After the initial reply to your report, the security team will
endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional
information or guidance.
Report security bugs in third-party modules to the person or team maintaining the module.
## Disclosure Policy
When the security team receives a security bug report, they will assign it to a primary handler. This person will
coordinate the fix and release process, involving the following steps:
- Confirm the problem and determine the affected versions.
- Audit code to find any potential similar problems.
- Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible.
## Comments on this Policy
If you have suggestions on how this process could be improved please submit a pull request.

51
bower.json Normal file
View file

@ -0,0 +1,51 @@
{
"name": "diff2html",
"version": "2.0.0-rc.5",
"homepage": "http://rtfpessoa.github.io/diff2html/",
"description": "Fast Diff to colorized HTML",
"keywords": [
"git",
"diff",
"pretty",
"side",
"line",
"side-by-side",
"line-by-line",
"character",
"highlight",
"pretty",
"color",
"html",
"diff2html",
"difftohtml",
"colorized"
],
"authors": [
"Rodrigo Fernandes <rtfrodrigo@gmail.com>"
],
"repository": {
"type": "git",
"url": "git://github.com/rtfpessoa/diff2html.git"
},
"main": [
"./dist/diff2html-templates.js",
"./dist/diff2html.js",
"./dist/diff2html-ui.js",
"./dist/diff2html.css"
],
"license": "MIT",
"moduleType": [
"globals",
"node"
],
"ignore": [
"**/.*",
"webpack.config.js",
"package.json",
"release.sh",
"circle.yml",
"css",
"src",
"test"
]
}

9
circle.yml Normal file
View file

@ -0,0 +1,9 @@
test:
override:
- nvm install 0.12 && npm test
- nvm install 4 && npm test
- nvm install 5 && npm test
post:
- npm install
- npm test
- cat ./coverage/lcov.info | ./node_modules/.bin/codacy-coverage

354
dist/diff2html-ui.js vendored Normal file
View file

@ -0,0 +1,354 @@
(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<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (global){
/*
*
* Diff to HTML (diff2html-ui.js)
* Author: rtfpessoa
*
* Depends on: [ jQuery ]
* Optional dependencies on: [ highlight.js ]
*
*/
/*global $, hljs, Diff2Html*/
(function() {
var highlightJS = require('./highlight.js-internals.js').HighlightJS;
var diffJson = null;
var defaultTarget = "body";
var currentSelectionColumnId = -1;
function Diff2HtmlUI(config) {
var cfg = config || {};
if (cfg.diff) {
diffJson = Diff2Html.getJsonFromDiff(cfg.diff);
} else if (cfg.json) {
diffJson = cfg.json;
}
this._initSelection();
}
Diff2HtmlUI.prototype.draw = function(targetId, config) {
var cfg = config || {};
var $target = this._getTarget(targetId);
$target.html(Diff2Html.getPrettyHtml(diffJson, cfg));
};
Diff2HtmlUI.prototype.fileListCloseable = function(targetId, startVisible) {
var $target = this._getTarget(targetId);
var hashTag = this._getHashTag();
var $showBtn = $target.find(".d2h-show");
var $hideBtn = $target.find(".d2h-hide");
var $fileList = $target.find(".d2h-file-list");
if (hashTag === 'files-summary-show') show();
else if (hashTag === 'files-summary-hide') hide();
else if (startVisible) show();
else hide();
$showBtn.click(show);
$hideBtn.click(hide);
function show() {
$showBtn.hide();
$hideBtn.show();
$fileList.show();
}
function hide() {
$hideBtn.hide();
$showBtn.show();
$fileList.hide();
}
};
Diff2HtmlUI.prototype.highlightCode = function(targetId) {
var that = this;
var $target = that._getTarget(targetId);
// collect all the diff files and execute the highlight on their lines
var $files = $target.find(".d2h-file-wrapper");
$files.map(function(_i, file) {
var oldLinesState;
var newLinesState;
var $file = $(file);
var language = $file.data("lang");
// collect all the code lines and execute the highlight on them
var $codeLines = $file.find(".d2h-code-line-ctn");
$codeLines.map(function(_j, line) {
var $line = $(line);
var text = line.textContent;
var lineParent = line.parentNode;
var lineState;
if (lineParent.className.indexOf("d2h-del") !== -1) {
lineState = oldLinesState;
} else {
lineState = newLinesState;
}
var result = hljs.getLanguage(language) ? hljs.highlight(language, text, true, lineState) : hljs.highlightAuto(text);
if (lineParent.className.indexOf("d2h-del") !== -1) {
oldLinesState = result.top;
} else if (lineParent.className.indexOf("d2h-ins") !== -1) {
newLinesState = result.top;
} else {
oldLinesState = result.top;
newLinesState = result.top;
}
var originalStream = highlightJS.nodeStream(line);
if (originalStream.length) {
var resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = highlightJS.mergeStreams(originalStream, highlightJS.nodeStream(resultNode), text);
}
$line.addClass("hljs");
$line.addClass(result.language);
$line.html(result.value);
});
});
};
Diff2HtmlUI.prototype._getTarget = function(targetId) {
var $target;
if (typeof targetId === 'object' && targetId instanceof jQuery) {
$target = targetId;
} else if (typeof targetId === 'string') {
$target = $(targetId);
} else {
console.error("Wrong target provided! Falling back to default value 'body'.");
console.log("Please provide a jQuery object or a valid DOM query string.");
$target = $(defaultTarget);
}
return $target;
};
Diff2HtmlUI.prototype._getHashTag = function() {
var docUrl = document.URL;
var hashTagIndex = docUrl.indexOf('#');
var hashTag = null;
if (hashTagIndex !== -1) {
hashTag = docUrl.substr(hashTagIndex + 1);
}
return hashTag;
};
Diff2HtmlUI.prototype._distinct = function(collection) {
return collection.filter(function(v, i) {
return collection.indexOf(v) === i;
});
};
Diff2HtmlUI.prototype._initSelection = function() {
var body = $('body');
var that = this;
body.on('mousedown', '.d2h-diff-table', function(event) {
var target = $(event.target);
var table = target.closest('.d2h-diff-table');
if (target.closest('.d2h-code-line,.d2h-code-side-line').length) {
table.removeClass('selecting-left');
table.addClass('selecting-right');
currentSelectionColumnId = 1;
} else if (target.closest('.d2h-code-linenumber,.d2h-code-side-linenumber').length) {
table.removeClass('selecting-right');
table.addClass('selecting-left');
currentSelectionColumnId = 0;
}
});
body.on('copy', '.d2h-diff-table', function(event) {
var clipboardData = event.originalEvent.clipboardData;
var text = that._getSelectedText();
clipboardData.setData('text', text);
event.preventDefault();
});
};
Diff2HtmlUI.prototype._getSelectedText = function() {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var doc = range.cloneContents();
var nodes = doc.querySelectorAll('tr');
var text = '';
var idx = currentSelectionColumnId;
if (nodes.length === 0) {
text = doc.textContent;
} else {
[].forEach.call(nodes, function(tr, i) {
var td = tr.cells[tr.cells.length === 1 ? 0 : idx];
text += (i ? '\n' : '') + td.textContent.replace(/(?:\r\n|\r|\n)/g, '');
});
}
return text;
};
module.exports.Diff2HtmlUI = Diff2HtmlUI;
// Expose diff2html in the browser
global.Diff2HtmlUI = Diff2HtmlUI;
})();
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./highlight.js-internals.js":2}],2:[function(require,module,exports){
/*
*
* highlight.js
* Author: isagalaev
*
*/
(function() {
function HighlightJS() {
}
/*
* Copied from Highlight.js Private API
* Will be removed when this part of the API is exposed
*/
/* Utility functions */
function escape(value) {
return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}
function tag(node) {
return node.nodeName.toLowerCase();
}
/* Stream merging */
HighlightJS.prototype.nodeStream = function(node) {
var result = [];
(function _nodeStream(node, offset) {
for (var child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 3)
offset += child.nodeValue.length;
else if (child.nodeType == 1) {
result.push({
event: 'start',
offset: offset,
node: child
});
offset = _nodeStream(child, offset);
// Prevent void elements from having an end tag that would actually
// double them in the output. There are more void elements in HTML
// but we list only those realistically expected in code display.
if (!tag(child).match(/br|hr|img|input/)) {
result.push({
event: 'stop',
offset: offset,
node: child
});
}
}
}
return offset;
})(node, 0);
return result;
};
HighlightJS.prototype.mergeStreams = function(original, highlighted, value) {
var processed = 0;
var result = '';
var nodeStack = [];
function selectStream() {
if (!original.length || !highlighted.length) {
return original.length ? original : highlighted;
}
if (original[0].offset != highlighted[0].offset) {
return (original[0].offset < highlighted[0].offset) ? original : highlighted;
}
/*
To avoid starting the stream just before it should stop the order is
ensured that original always starts first and closes last:
if (event1 == 'start' && event2 == 'start')
return original;
if (event1 == 'start' && event2 == 'stop')
return highlighted;
if (event1 == 'stop' && event2 == 'start')
return original;
if (event1 == 'stop' && event2 == 'stop')
return highlighted;
... which is collapsed to:
*/
return highlighted[0].event == 'start' ? original : highlighted;
}
function open(node) {
function attrStr(a) {
return ' ' + a.nodeName + '="' + escape(a.value) + '"';
}
result += '<' + tag(node) + Array.prototype.map.call(node.attributes, attrStr).join('') + '>';
}
function close(node) {
result += '</' + tag(node) + '>';
}
function render(event) {
(event.event == 'start' ? open : close)(event.node);
}
while (original.length || highlighted.length) {
var stream = selectStream();
result += escape(value.substr(processed, stream[0].offset - processed));
processed = stream[0].offset;
if (stream == original) {
/*
On any opening or closing tag of the original markup we first close
the entire highlighted node stack, then render the original tag along
with all the following original tags at the same offset and then
reopen all the tags on the highlighted stack.
*/
nodeStack.reverse().forEach(close);
do {
render(stream.splice(0, 1)[0]);
stream = selectStream();
} while (stream == original && stream.length && stream[0].offset == processed);
nodeStack.reverse().forEach(open);
} else {
if (stream[0].event == 'start') {
nodeStack.push(stream[0].node);
} else {
nodeStack.pop();
}
render(stream.splice(0, 1)[0]);
}
}
return result + escape(value.substr(processed));
};
/* **** Highlight.js Private API **** */
module.exports.HighlightJS = new HighlightJS();
})();
},{}]},{},[1]);

1
dist/diff2html-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

354
dist/diff2html.css vendored Normal file
View file

@ -0,0 +1,354 @@
/*
*
* Diff to HTML (diff2html.css)
* Author: rtfpessoa
*
*/
.d2h-wrapper {
text-align: left;
}
.d2h-file-header {
padding: 5px 10px;
border-bottom: 1px solid #d8d8d8;
background-color: #f7f7f7;
}
.d2h-file-stats {
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: flex;
align-items: center;
width: 100%;
font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 15px;
line-height: 15px;
}
.d2h-file-name {
line-height: 33px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.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 > div {
height: 16px;
line-height: 16px;
}
.d2h-files-diff {
width: 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;
}
.d2h-code-line {
display: block;
white-space: nowrap;
padding: 0 10px;
margin-left: 80px;
}
.d2h-code-side-line {
display: block;
white-space: nowrap;
padding: 0 10px;
height: 18px;
line-height: 18px;
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: 0.2em;
}
.d2h-code-line ins,
.d2h-code-side-line ins {
display: inline-block;
margin-top: -1px;
text-decoration: none;
background-color: #97f295;
border-radius: 0.2em;
}
.d2h-code-line-prefix {
float: left;
background: none;
padding: 0;
}
.d2h-code-line-ctn {
background: none;
padding: 0;
}
.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;
height: 18px;
padding-left: 2px;
padding-right: 2px;
line-height: 18px;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid #eeeeee;
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;
height: 18px;
line-height: 18px;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid #eeeeee;
border-width: 0 1px 0 1px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
}
/*
* Changes Highlight
*/
.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, 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;
}
/*
* File Summary List
*/
.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: bold;
}
.d2h-file-list-line {
display: flex;
text-align: left;
}
.d2h-file-list-line .d2h-file-name {
line-height: 21px;
}
.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: 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;
}
/*
* Selection util.
*/
.selecting-left .d2h-code-line,
.selecting-left .d2h-code-line *,
.selecting-right td.d2h-code-linenumber,
.selecting-right td.d2h-code-linenumber *,
.selecting-left .d2h-code-side-line,
.selecting-left .d2h-code-side-line *,
.selecting-right td.d2h-code-side-linenumber,
.selecting-right td.d2h-code-side-linenumber * {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.selecting-left .d2h-code-line::selection,
.selecting-left .d2h-code-line *::selection
.selecting-right td.d2h-code-linenumber::selection,
.selecting-left .d2h-code-side-line::selection,
.selecting-left .d2h-code-side-line *::selection,
.selecting-right td.d2h-code-side-linenumber::selection,
.selecting-right td.d2h-code-side-linenumber *::selection {
background: transparent;
}

3837
dist/diff2html.js vendored Normal file

File diff suppressed because it is too large Load diff

1
dist/diff2html.min.css vendored Normal file
View file

@ -0,0 +1 @@
.d2h-code-line-prefix,.line-num1{float:left}.d2h-wrapper{text-align:left}.d2h-file-header{padding:5px 10px;border-bottom:1px solid #d8d8d8;background-color:#f7f7f7}.d2h-file-stats{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:flex;align-items:center;width:100%;font-family:"Source Sans Pro","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:15px}.d2h-file-name{line-height:33px;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.d2h-file-diff,.d2h-file-side-diff{overflow-x:scroll;overflow-y:hidden}.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>div{height:16px;line-height:16px}.d2h-files-diff{width:100%}.d2h-file-side-diff{display:inline-block;width:50%;margin-right:-4px}.d2h-code-line,.d2h-code-side-line{display:block;white-space:nowrap;padding:0 10px}.d2h-code-line{margin-left:80px}.d2h-code-side-line{height:18px;line-height:18px;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}.d2h-code-line-ctn,.d2h-code-line-prefix{background:0 0;padding:0}.d2h-code-linenumber,.d2h-code-side-linenumber{position:absolute;height:18px;line-height:18px;background-color:#fff;text-align:right;color:rgba(0,0,0,.3);cursor:pointer}.line-num1,.line-num2{width:40px;padding-left:3px;box-sizing:border-box;overflow:hidden;text-overflow:ellipsis}.line-num2{float:right}.d2h-code-linenumber{box-sizing:border-box;width:86px;padding-left:2px;padding-right:2px;border:solid #eee;border-width:0 1px}.d2h-code-side-linenumber{box-sizing:border-box;width:56px;padding-left:5px;padding-right:5px;border:solid #eee;border-width:0 1px;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:flex;text-align:left}.d2h-file-list-line .d2h-file-name{line-height:21px}.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:flex;font-size:10px;margin-left:5px;padding:0 2px;background-color:#fff}.d2h-deleted-tag{border:1px solid #c33}.d2h-added-tag{border:1px solid #399839}.d2h-changed-tag{border:1px solid #d0b44c}.d2h-moved-tag{border:1px solid #3572b0}.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;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.selecting-left .d2h-code-line ::selection .selecting-right td.d2h-code-linenumber::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-side-linenumber ::selection,.selecting-right td.d2h-code-side-linenumber::selection{background:0 0}

4
dist/diff2html.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,77 +0,0 @@
import globals from 'globals';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import pluginJest from 'eslint-plugin-jest';
import json from '@eslint/json';
import pluginPromise from 'eslint-plugin-promise';
export default [
{ ...eslint.configs.recommended, files: ['src/**/*.{js,mjs,cjs,ts}'] },
...tseslint.configs.recommended,
// ...tseslint.configs.recommendedTypeChecked,
// ...tseslint.configs.strict,
// ...tseslint.configs.stylistic,
// ...tseslint.configs.strictTypeChecked,
// ...tseslint.configs.stylisticTypeChecked,
{
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
},
pluginPromise.configs['flat/recommended'],
{
plugins: {
json,
},
files: ['**/*.json'],
language: 'json/json',
rules: {
'json/no-duplicate-keys': 'error',
},
},
{
...pluginJest.configs['flat/recommended'],
...pluginJest.configs['flat/style'],
files: ['src/__tests__/**/*tests.ts'],
plugins: { jest: pluginJest },
languageOptions: {
globals: pluginJest.environments.globals.globals,
},
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
...globals.es2025,
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
document: 'readonly',
navigator: 'readonly',
window: 'readonly',
},
parserOptions: {
project: './tsconfig.eslint.json',
tsconfigRootDir: './',
},
},
},
{
ignores: ['src/diff2html-templates.*', 'coverage/', 'docs/', 'bundles-out/', 'bundles/', 'lib/', 'lib-esm/'],
},
{
...tseslint.configs.disableTypeChecked,
files: ['**/*.{js,mjs,cjs}'],
},
];

View file

@ -1,23 +0,0 @@
module.exports = {
verbose: true,
preset: 'ts-jest',
testEnvironment: 'node',
coverageDirectory: './coverage',
coverageReporters: ['lcov', 'text', 'html', 'json', 'cobertura', 'clover'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/ui/**',
'!src/diff2html-templates.ts',
'!src/__tests__/**',
'!node_modules/**',
],
coverageThreshold: {
global: {
statements: 93,
branches: 86,
functions: 98,
lines: 93,
},
},
prettierPath: require.resolve('prettier-2'),
};

18121
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "diff2html",
"version": "3.0.0-beta.1",
"homepage": "https://diff2html.xyz",
"version": "2.0.0-rc.5",
"homepage": "http://rtfpessoa.github.io/diff2html/",
"description": "Fast Diff to colorized HTML",
"keywords": [
"git",
@ -26,135 +26,51 @@
},
"repository": {
"type": "git",
"url": "git://github.com/rtfpessoa/diff2html.git"
"url": "https://www.github.com/rtfpessoa/diff2html.git"
},
"bugs": {
"url": "https://www.github.com/rtfpessoa/diff2html/issues"
},
"engines": {
"node": ">=12"
"node": ">=0.12"
},
"preferGlobal": true,
"scripts": {
"lint:staged": "lint-staged",
"lint:check": "eslint",
"lint:fix": "eslint --fix",
"prettier": "prettier --ignore-path .gitignore '**/*.+(js|jsx|ts|tsx|json|css|html|md|mdx)'",
"format:check": "npm run prettier --check",
"format:fix": "npm run prettier --write",
"build": "npm run build:css && npm run build:templates && npm run build:commonjs && npm run build:esm && npm run build:bundles && npm run build:website",
"build:commonjs": "rm -rf lib; tsc -p tsconfig.json -m CommonJS --outDir lib",
"build:esm": "rm -rf lib-esm; tsc -p tsconfig.json -m ESNext --outDir lib-esm",
"build:bundles": "rm -rf ./bundles/js; webpack --mode production --config webpack.bundles.ts",
"build:css": "rm -rf ./bundles/css; postcss --config ./postcss.config.js --no-map -o ./bundles/css/diff2html.min.css ./src/ui/css/diff2html.css",
"build:templates": "ts-node ./scripts/hulk.ts --wrapper ts --variable 'defaultTemplates' ./src/templates/*.mustache > ./src/diff2html-templates.ts",
"build:website": "rm -rf docs; webpack --mode production --config webpack.website.ts",
"gen": "npm run gen:toc",
"gen:toc-base": "markdown-toc --maxdepth 3 --bullets='-' -i",
"gen:toc": "npm run gen:toc-base README.md",
"test": "is-ci 'test:coverage' 'test:watch'",
"test:coverage": "jest --coverage",
"test:watch": "jest --watch",
"test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch",
"coverage:open": "npm run test:coverage && open ./coverage/index.html",
"coverage:push": "curl -Ls https://coverage.codacy.com/get.sh | bash",
"validate": "npm run build:templates && npm run format:check && npm run lint:check && npm run build && npm run test:coverage",
"fix": "npm run format:fix && npm run lint:fix",
"start": "npm run start:website",
"start:website": "webpack serve --mode development --config webpack.website.ts",
"preversion": "npm run validate",
"version": "git add -A package.json",
"prepare": "husky"
"release": "./scripts/release.sh",
"release-bower": "./scripts/update-bower-version.sh",
"templates": "./scripts/hulk.js --wrapper node --variable 'browserTemplates' ./src/templates/*.mustache > ./src/templates/diff2html-templates.js",
"style": "jscs src/*.js src/ui/js/*.js",
"coverage": "istanbul cover _mocha -- -u exports -R spec ./test/**/*",
"check-coverage": "istanbul check-coverage --statements 90 --functions 90 --branches 85 --lines 90 ./coverage/coverage.json",
"test": "npm run style && npm run coverage && npm run check-coverage",
"codacy": "npm run coverage && cat ./coverage/lcov.info | codacy-coverage",
"preversion": "npm run release && npm test",
"version": "npm run release-bower && git add -A src dist package.json bower.json",
"postversion": "git push && git push --tags"
},
"main": "./lib/diff2html.js",
"module": "./lib-esm/diff2html.js",
"types": "./lib/diff2html.d.ts",
"lint-staged": {
"**/*.+(js|jsx|ts|tsx|json)": [
"prettier --write",
"eslint --fix"
],
"**/*.+(css|html|md|mdx)": [
"prettier --write"
],
"README.md": [
"npm run gen:toc-base"
]
"main": "./src/diff2html.js",
"browser": {
"fs": false
},
"dependencies": {
"diff": "^7.0.0",
"hogan.js": "3.0.2"
},
"optionalDependencies": {
"highlight.js": "11.9.0"
"diff": "^2.2.2",
"hogan.js": "^3.0.2"
},
"devDependencies": {
"prettier-2": "npm:prettier@^2",
"@eslint/js": "^9.17.0",
"@eslint/json": "^0.9.0",
"@types/diff": "^6.0.0",
"@types/hogan.js": "3.0.5",
"@types/jest": "^29.5.14",
"@types/node": "^22.10.2",
"@types/nopt": "3.0.32",
"all-contributors-cli": "^6.24.0",
"autoprefixer": "^10.4.20",
"bulma": "^1.0.2",
"clipboard": "2.0.11",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^7.1.2",
"cssnano": "^7.0.6",
"eslint": "^9.17.0",
"eslint-plugin-jest": "28.10.0",
"eslint-plugin-promise": "^7.2.1",
"file-loader": "6.2.0",
"globals": "^15.14.0",
"handlebars": "4.7.8",
"handlebars-loader": "1.7.3",
"html-webpack-plugin": "^5.6.3",
"husky": "^9.1.7",
"image-webpack-loader": "8.1.0",
"is-ci-cli": "2.2.0",
"jest": "29.7.0",
"lint-staged": "^15.2.11",
"markdown-toc": "^1.2.0",
"mini-css-extract-plugin": "^2.9.2",
"mkdirp": "3.0.1",
"nopt": "^8.0.0",
"postcss": "^8.4.49",
"postcss-cli": "11.0.0",
"postcss-import": "^16.1.0",
"postcss-loader": "^8.1.1",
"postcss-preset-env": "^10.1.2",
"prettier": "^3.4.2",
"ts-jest": "^29.2.5",
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.2",
"url-loader": "4.1.1",
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0",
"whatwg-fetch": "3.6.20"
},
"resolutions": {
"lodash": ">=4.17.20",
"minimist": ">=1.2.5",
"acorn": ">=7.4.0",
"autolinker": ">=3.14.1",
"bl": ">=2.2.1",
"decompress": ">=4.2.1",
"node-forge": ">=0.10.0",
"trim-newlines": ">=3.0.1",
"async": ">=2.6.4",
"terser": ">=5.14.2",
"semver-regex": ">=4.0.5",
"http-cache-semantics": ">=4.1.1"
"browserify": "^13.0.0",
"clean-css": "^3.4.10",
"codacy-coverage": "^1.1.3",
"fast-html-parser": "^1.0.1",
"istanbul": "^0.4.2",
"jscs": "^3.0.3",
"mkdirp": "^0.5.1",
"mocha": "^2.4.5",
"nopt": "^3.0.6",
"uglifyjs": "^2.4.10"
},
"license": "MIT",
"files": [
"bundles",
"lib",
"lib-esm"
"src",
"dist"
]
}

View file

@ -1,10 +0,0 @@
module.exports = {
sourceMap: false,
plugins: {
'postcss-import': {},
'postcss-preset-env': {
browsers: 'last 2 versions',
},
cssnano: {},
},
};

291
sample/index.html Normal file
View file

@ -0,0 +1,291 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Diff to HTML by rtfpessoa</title>
<!--
Diff to HTML (index.html)
Author: rtfpessoa
-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/highlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.4.0/languages/scala.min.js"></script>
<!-- diff2html -->
<link rel="stylesheet" type="text/css" href="../dist/diff2html.css">
<script type="text/javascript" src="../dist/diff2html.js"></script>
<script type="text/javascript" src="../dist/diff2html-ui.js"></script>
<!-- -->
<script>
var lineDiffExample =
"--- a/src/my/really/big/path/sample.js\n" +
"+++ b/src/my/small/path/sample.js\n" +
"@@ -1 +1,2 @@\n" +
"-test\n" +
"+test1r\n" +
"+test2r\n" +
'diff --git a/src/core/init.java b/src/core/init.java\n' +
'index e49196a..50f310c 100644\n' +
'--- a/src/core/init.java\n' +
'+++ b/src/core/init.java\n' +
'@@ -101,7 +101,7 @@\n' +
' /**\n' +
' * Setter for property filesize.\n' +
' *\n' +
" * @param filesize value of property 'filesize'.\n" +
' */\n' +
' public void setFilesize(int filesize) {\n' +
'- this.filesizeOld = filesizeOld;\n' +
'+ this.filesizeNew = filesizeNew;\n' +
' }\n' +
'diff --git a/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/coverage.init b/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/coverage.init\n' +
'index fc56817..e8e7e49 100644\n' +
'--- a/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/coverage.init\n' +
'+++ b/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/src/very/long/file/path/coverage.init\n' +
'@@ -19,7 +19,7 @@\n' +
' -opt "\-nostart"\n' +
' \n' +
' # skip stopenv\n' +
'--do "runbvt,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata,stopenv,getlogs,pullcoveragedata"\n' +
'+-do "runbvt,getlogs,pullcoveragedata"\n' +
' \n' +
' ##########################################\n' +
' # logs files to bring back to base\n' +
'diff --git a/src/attributes/attr.js b/src/attributes/attr.js\n' +
'index facdd41..b627fe8 100644\n' +
'--- a/src/attributes/attr.js\n' +
'+++ b/src/attributes/attr.js\n' +
'@@ -1,11 +1,10 @@\n' +
' define([\n' +
' "../core",\n' +
' "../var/rnotwhite",\n' +
'- "../var/strundefined",\n' +
' "../core/access",\n' +
' "./support",\n' +
' "../selector"\n' +
'-], function( jQuery, rnotwhite, strundefined, access, support ) {\n' +
'+], function( jQuery, rnotwhite, access, support ) {\n' +
' \n' +
' var nodeHook, boolHook,\n' +
' attrHandle = jQuery.expr.attrHandle;\n' +
'@@ -33,7 +32,7 @@ jQuery.extend({\n' +
' }\n' +
' \n' +
' // Fallback to prop when attributes are not supported\n' +
'- if ( typeof elem.getAttribute === strundefined ) {\n' +
'+ if ( !elem.getAttribute ) {\n' +
' return jQuery.prop( elem, name, value );\n' +
' }\n' +
' \n' +
'diff --git a/src/attributes/classes.js b/src/attributes/classes.js\n' +
'index c617824..c8d1393 100644\n' +
'--- a/src/attributes/classes.js\n' +
'+++ b/src/attributes/classes.js\n' +
'@@ -1,10 +1,9 @@\n' +
' define([\n' +
' "../core",\n' +
' "../var/rnotwhite",\n' +
'- "../var/strundefined",\n' +
' "../data/var/dataPriv",\n' +
' "../core/init"\n' +
'-], function( jQuery, rnotwhite, strundefined, dataPriv ) {\n' +
'+], function( jQuery, rnotwhite, dataPriv ) {\n' +
' \n' +
' var rclass = /[\\t\\r\\n\\f]/g;\n' +
' \n' +
'@@ -128,7 +127,7 @@ jQuery.fn.extend({\n' +
' }\n' +
' \n' +
' // Toggle whole class name\n' +
'- } else if ( type === strundefined || type === "boolean" ) {\n' +
'+ } else if ( value === undefined || type === "boolean" ) {\n' +
' if ( this.className ) {\n' +
' // store className if set\n' +
' dataPriv.set( this, "__className__", this.className );\n' +
'diff --git a/src/core/init.js b/src/core/init.js\n' +
'index e49196a..50f310c 100644\n' +
'--- a/src/core/init.js\n' +
'+++ b/src/core/init.js\n' +
'@@ -101,7 +101,7 @@ var rootjQuery,\n' +
' // HANDLE: $(function)\n' +
' // Shortcut for document ready\n' +
' } else if ( jQuery.isFunction( selector ) ) {\n' +
'- return typeof rootjQuery.ready !== "undefined" ?\n' +
'+ return rootjQuery.ready !== undefined ?\n' +
' rootjQuery.ready( selector ) :\n' +
' // Execute immediately if ready is not present\n' +
' selector( jQuery );\n' +
'diff --git a/src/event.js b/src/event.js\n' +
'index 7336f4d..6183f70 100644\n' +
'--- a/src/event.js\n' +
'+++ b/src/event.js\n' +
'@@ -1,6 +1,5 @@\n' +
' define([\n' +
' "./core",\n' +
'- "./var/strundefined",\n' +
' "./var/rnotwhite",\n' +
' "./var/hasOwn",\n' +
' "./var/slice",\n' +
'@@ -10,7 +9,7 @@ define([\n' +
' "./core/init",\n' +
' "./data/accepts",\n' +
' "./selector"\n' +
'-], function( jQuery, strundefined, rnotwhite, hasOwn, slice, support, dataPriv ) {\n' +
'+], function( jQuery, rnotwhite, hasOwn, slice, support, dataPriv ) {\n' +
' \n' +
' var\n' +
' rkeyEvent = /^key/,\n' +
'@@ -72,7 +71,7 @@ jQuery.event = {\n' +
' eventHandle = elemData.handle = function( e ) {\n' +
' // Discard the second event of a jQuery.event.trigger() and\n' +
' // when an event is called after a page has unloaded\n' +
'- return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ?\n' +
'+ return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?\n' +
' jQuery.event.dispatch.apply( elem, arguments ) : undefined;\n' +
' };\n' +
' }\n' +
'diff --git a/src/exports/global.js b/src/exports/global.js\n' +
'index 6513287..1db4144 100644\n' +
'--- a/src/exports/global.js\n' +
'+++ b/src/exports/global.js\n' +
'@@ -1,7 +1,9 @@\n' +
' define([\n' +
'- "../core",\n' +
'- "../var/strundefined"\n' +
'-], function( jQuery, strundefined ) {\n' +
'+ "../core"\n' +
'+], function( jQuery ) {\n' +
'+\n' +
'+/* exported noGlobal */\n' +
'+/* global noGlobal: false */\n' +
' \n' +
' var\n' +
' // Map over jQuery in case of overwrite\n' +
'@@ -25,7 +27,7 @@ jQuery.noConflict = function( deep ) {\n' +
' // Expose jQuery and $ identifiers, even in AMD\n' +
' // (#7102#comment:10, https://github.com/jquery/jquery/pull/557)\n' +
' // and CommonJS for browser emulators (#13566)\n' +
'-if ( typeof noGlobal === strundefined ) {\n' +
'+if ( !noGlobal ) {\n' +
' window.jQuery = window.$ = jQuery;\n' +
' }\n' +
' \n' +
'diff --git a/src/offset.js b/src/offset.js\n' +
'index cc6ffb4..fa51f18 100644\n' +
'--- a/src/offset.js\n' +
'+++ b/src/offset.js\n' +
'@@ -1,6 +1,5 @@\n' +
' define([\n' +
' "./core",\n' +
'- "./var/strundefined",\n' +
' "./core/access",\n' +
' "./css/var/rnumnonpx",\n' +
' "./css/curCSS",\n' +
'@@ -10,7 +9,7 @@ define([\n' +
' "./core/init",\n' +
' "./css",\n' +
' "./selector" // contains\n' +
'-], function( jQuery, strundefined, access, rnumnonpx, curCSS, addGetHookIf, support ) {\n' +
'+], function( jQuery, access, rnumnonpx, curCSS, addGetHookIf, support ) {\n' +
' \n' +
' var docElem = window.document.documentElement;\n' +
' \n' +
'@@ -99,7 +98,7 @@ jQuery.fn.extend({\n' +
' \n' +
' // Support: BlackBerry 5, iOS 3 (original iPhone)\n' +
' // If we dont have gBCR, just use 0,0 rather than error\n' +
'- if ( typeof elem.getBoundingClientRect !== strundefined ) {\n' +
'+ if ( elem.getBoundingClientRect !== undefined ) {\n' +
' box = elem.getBoundingClientRect();\n' +
' }\n' +
' win = getWindow( doc );\n' +
'diff --git a/src/var/strundefined.js b/src/var/strundefined.js\n' +
'deleted file mode 100644\n' +
'index 04e16b0..0000000\n' +
'--- a/src/var/strundefined.js\n' +
'+++ /dev/null\n' +
'@@ -1,3 +0,0 @@\n' +
'-define(function() {\n' +
'- return typeof undefined;\n' +
'-});\n' +
'diff --git a/components/textdiff/textdiff.html b/components/textdiff/textdiff.html\n' +
'index a3484bf..82209af 100644\n' +
'--- a/components/textdiff/textdiff.html\n' +
'+++ b/components/textdiff/textdiff.html\n' +
'@@ -1,6 +1,8 @@\n' +
' <!-- ko if: isShowingDiffs -->\n' +
' <div>\n' +
'- <div data-bind="event: { load: setDom($element) }"></div>\n' +
'+ <!-- ko if: isParsed -->\n' +
'+ <div data-bind="template: {nodes: ko.utils.parseHtmlFragment(htmlSrc())}"></div>\n' +
'+ <!-- /ko -->\n' +
' <div class="btn-load-more" data-bind="visible: loadMoreCount() > 0">\n' +
' </div>\n' +
'diff --git a/test.js b/test.js\n' +
'new file mode 100644\n' +
'index 0000000..e1e22ec\n' +
'--- /dev/null\n' +
'+++ b/test.js\n' +
'@@ -0,0 +1,6 @@\n' +
"+var parser = require('./source/git-parser');\n" +
'+\n' +
"+var text = 'diff --git a/components/app/app.html b/components/app/app.html\\nindex ecb7a95..027bd9b 100644\\n--- a/components/app/app.html\\n+++ b/components/app/app.html\\n@@ -52,0 +53,3 @@\\n+\\n+\\n+\\n@@ -56,0 +60,3 @@\\n+\\n+\\n+\\n'\n" +
'+var patchLineList = [ false, false, false, false ];\n' +
'+\n' +
'+console.log(parser.parsePatchDiffResult(text, patchLineList));\n' +
"diff --git a/a.xml b/b.xml\n" +
"index e54317e..82a9a56 100644\n" +
"--- a/a.xml\n" +
"+++ b/b.xml\n" +
"@@ -242,4 +242,6 @@ need to create a new job for native server java api and move these tests to a ne\n" +
" </packages>\n" +
" </test>\n" +
" -->\n" +
"+\n" +
"+\n" +
"--- a/sample.js\n" +
"+++ b/sample.js\n" +
"@@ -1 +1,2 @@\n" +
"-test\n" +
"+test1r\n" +
"+test2r\n";
$(document).ready(function() {
var diff2htmlUi = new Diff2HtmlUI({diff: lineDiffExample});
diff2htmlUi.draw('#line-by-line', {inputFormat: 'json', showFiles: true, matching: 'lines'});
diff2htmlUi.fileListCloseable('#line-by-line', false);
diff2htmlUi.highlightCode('#line-by-line');
diff2htmlUi.draw('#side-by-side', {
inputFormat: 'json',
showFiles: true,
matching: 'lines',
outputFormat: 'side-by-side'
});
diff2htmlUi.fileListCloseable('#side-by-side', false);
diff2htmlUi.highlightCode('#side-by-side');
});
</script>
</head>
<body style="text-align: center; font-family: 'Source Sans Pro',sans-serif;">
<h1>Diff to HTML by <a href="https://github.com/rtfpessoa">rtfpessoa</a></h1>
<h2>Line by Line</h2>
<div id="line-by-line" style="margin: 0 auto; max-width: 900px;">
</div>
<h2>Side by Side</h2>
<div id="side-by-side" style="margin: 0 auto;">
</div>
</body>
</html>

0
scripts/.ignore Normal file
View file

207
scripts/hulk.js Executable file
View file

@ -0,0 +1,207 @@
#!/usr/bin/env node
/*
* Copyright 2011 Twitter, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// dependencies
var hogan = require('hogan.js');
var path = require('path');
var nopt = require('nopt');
var mkderp = require('mkdirp');
var fs = require('fs');
// locals
var specials = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
var specialsRegExp = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
var options = {
'namespace': String,
'outputdir': path,
'variable': String,
'wrapper': String,
'version': true,
'help': true
};
var shortHand = {
'n': ['--namespace'],
'o': ['--outputdir'],
'vn': ['--variable'],
'w': ['--wrapper'],
'h': ['--help'],
'v': ['--version']
};
var templates;
// options
options = nopt(options, shortHand);
// escape special regexp characters
function esc(text) {
return text.replace(specialsRegExp, '\\$1');
}
// cyan function for rob
function cyan(text) {
return '\033[36m' + text + '\033[39m';
}
// check for dirs and correct ext (<3 for windows)
function extractFiles(args) {
var usage = '\n' +
cyan('USAGE:') + ' hulk [--wrapper wrapper] [--outputdir outputdir] ' +
'[--namespace namespace] [--variable variable] FILES\n\n' +
cyan('OPTIONS:') + ' [-w, --wrapper] :: wraps the template (i.e. amd)\n' +
' [-o, --outputdir] :: outputs the templates as individual files to a directory\n\n' +
' [-n, --namespace] :: prepend string to template names\n\n' +
' [-vn, --variable] :: variable name for non-amd wrapper\n\n' +
cyan('EXAMPLE:') + ' hulk --wrapper amd ./templates/*.mustache\n\n' +
cyan('NOTE:') + ' hulk supports the "*" wildcard and allows you to target specific extensions too\n';
var files = [];
if (options.version) {
console.log(require('../package.json').version);
process.exit(0);
}
if (!args.length || options.help) {
console.log(usage);
process.exit(0);
}
args.forEach(function(arg) {
if (/\*/.test(arg)) {
arg = arg.split('*');
return files = files.concat(
fs.readdirSync(arg[0] || '.')
.map(function(f) {
var file = path.join(arg[0], f);
return new RegExp(esc(arg[1]) + '$').test(f) && fs.statSync(file).isFile() && file;
})
.filter(function(f) {
return f;
})
);
}
if (fs.statSync(arg).isFile()) files.push(arg);
});
return files;
}
// remove utf-8 byte order mark, http://en.wikipedia.org/wiki/Byte_order_mark
function removeByteOrderMark(text) {
if (text.charCodeAt(0) === 0xfeff) {
return text.substring(1);
}
return text;
}
// wrap templates
function wrap(file, name, openedFile) {
switch (options.wrapper) {
case "amd":
return 'define(' + (!options.outputdir ? '"' + path.join(path.dirname(file), name) + '", ' : '') +
'[ "hogan.js" ], function(Hogan){ return new Hogan.Template(' +
hogan.compile(openedFile, {asString: 1}) +
');});';
case "node":
var globalObj = 'global.' + (options.variable || 'templates') + '["' + name + '"]';
var globalStmt = globalObj + ' = new Hogan.Template(' + hogan.compile(openedFile, {asString: 1}) + ');';
var nodeOutput = globalStmt;
// if we have a template per file the export will expose the template directly
if (options.outputdir) {
nodeOutput = nodeOutput + '\n' + 'module.exports = ' + globalObj + ';';
}
return nodeOutput;
default:
return (options.variable || 'templates') +
'["' + name + '"] = new Hogan.Template(' +
hogan.compile(openedFile, {asString: 1}) +
');';
}
}
function prepareOutput(content) {
var variableName = options.variable || 'templates';
switch (options.wrapper) {
case "amd":
return content;
case "node":
var nodeExport = '';
// if we have aggregated templates the export will expose the template map
if (!options.outputdir) {
nodeExport = 'module.exports = global.' + variableName + ';\n';
}
return '(function() {\n' +
'if (!!!global.' + variableName + ') global.' + variableName + ' = {};\n' +
'var Hogan = require("hogan.js");' +
content + '\n' +
nodeExport +
'})();';
default:
return 'if (!!!' + variableName + ') var ' + variableName + ' = {};\n' + content;
}
}
// write the directory
if (options.outputdir) {
mkderp.sync(options.outputdir);
}
// Prepend namespace to template name
function namespace(name) {
return (options.namespace || '') + name;
}
// write a template foreach file that matches template extension
templates = extractFiles(options.argv.remain)
.map(function(file) {
var openedFile = fs.readFileSync(file, 'utf-8');
var name;
if (!openedFile) return;
name = namespace(path.basename(file).replace(/\..*$/, ''));
openedFile = removeByteOrderMark(openedFile.trim());
openedFile = wrap(file, name, openedFile);
if (!options.outputdir) return openedFile;
fs.writeFileSync(path.join(options.outputdir, name + '.js')
, prepareOutput(openedFile));
})
.filter(function(t) {
return t;
});
// output templates
if (!templates.length || options.outputdir) process.exit(0);
console.log(prepareOutput(templates.join('\n')));

View file

@ -1,188 +0,0 @@
/*
* Copyright 2011 Twitter, Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as path from 'path';
import * as fs from 'fs';
import * as hogan from 'hogan.js';
import nopt from 'nopt';
import * as mkderp from 'mkdirp';
const options = nopt(
{
namespace: String,
outputdir: path,
variable: String,
wrapper: String,
version: true,
help: true,
},
{
n: ['--namespace'],
o: ['--outputdir'],
vn: ['--variable'],
w: ['--wrapper'],
h: ['--help'],
v: ['--version'],
},
);
const specials = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
const specialsRegExp = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
function escape(text: string): string {
return text.replace(specialsRegExp, '\\$1');
}
function cyan(text: string): string {
return '\x1B[36m' + text + '\x1B[39m';
}
function extractFiles(files: string[]): string[] {
const usage = `${cyan(
'USAGE:',
)} hulk [--wrapper wrapper] [--outputdir outputdir] [--namespace namespace] [--variable variable] FILES
${cyan('OPTIONS:')} [-w, --wrapper] :: wraps the template (i.e. amd)
[-o, --outputdir] :: outputs the templates as individual files to a directory
[-n, --namespace] :: prepend string to template names
[-vn, --variable] :: variable name for non-amd wrapper
${cyan('EXAMPLE:')} hulk --wrapper amd ./templates/*.mustache
${cyan('NOTE:')} hulk supports the "*" wildcard and allows you to target specific extensions too
`;
if (options.version) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
console.log(require('../package.json').version);
process.exit(0);
}
if (!files.length || options.help) {
console.log(usage);
process.exit(0);
}
return files
.map((fileGlob: string) => {
if (/\*/.test(fileGlob)) {
const [fileGlobPrefix, fileGlobSuffix] = fileGlob.split('*');
return fs.readdirSync(fileGlobPrefix || '.').reduce<string[]>((previousFiles, relativeFilePath) => {
const file = path.join(fileGlobPrefix, relativeFilePath);
if (new RegExp(`${escape(fileGlobSuffix)}$`).test(relativeFilePath) && fs.statSync(file).isFile()) {
previousFiles.push(file);
}
return previousFiles;
}, []);
} else if (fs.statSync(fileGlob).isFile()) {
return [fileGlob];
} else {
return [];
}
})
.reduce((previous, current) => previous.concat(current), []);
}
// Remove utf-8 byte order mark, http://en.wikipedia.org/wiki/Byte_order_mark
function removeByteOrderMark(text: string): string {
if (text.charCodeAt(0) === 0xfeff) {
return text.substring(1);
}
return text;
}
// Wrap templates
function wrap(file: string, name: string, openedFile: string): string {
const hoganTemplateString = `new Hogan.Template(${hogan.compile(openedFile, { asString: true })})`;
const objectName = options.variable || 'templates';
const objectAccessor = `${objectName}["${name}"]`;
const objectStmt = `${objectAccessor} = ${hoganTemplateString};`;
switch (options.wrapper) {
case 'amd':
return `define(${
!options.outputdir ? `"${path.join(path.dirname(file), name)}", ` : ''
}["hogan.js"], function(Hogan) { return ${hoganTemplateString}; });`;
case 'node':
// If we have a template per file the export will expose the template directly
return options.outputdir ? `global.${objectStmt};\nmodule.exports = ${objectAccessor};` : `global.${objectStmt}`;
case 'ts':
return `// @ts-ignore\n${objectStmt}`;
default:
return objectStmt;
}
}
function prepareOutput(content: string): string {
const variableName = options.variable || 'templates';
switch (options.wrapper) {
case 'amd':
return content;
case 'node':
return `(function() {
if (!!!global.${variableName}) global.${variableName} = {};
var Hogan = require("hogan.js");
${content}
${!options.outputdir ? `module.exports = global.${variableName};\n` : ''})();`;
case 'ts':
return `import * as Hogan from "hogan.js";
type CompiledTemplates = { [name: string]: Hogan.Template };
export const ${variableName}: CompiledTemplates = {};
${content}`;
default:
return 'if (!!!' + variableName + ') var ' + variableName + ' = {};\n' + content;
}
}
// Write the directory
if (options.outputdir) {
mkderp.sync(options.outputdir);
}
// Prepend namespace to template name
function namespace(name: string): string {
return (options.namespace || '') + name;
}
// Write a template foreach file that matches template extension
const templates = extractFiles(options.argv.remain)
.map(file => {
const timmedFileContents = fs.readFileSync(file, 'utf8').trim();
if (!timmedFileContents) return;
const name = namespace(path.basename(file).replace(/\..*$/, ''));
const cleanFileContents = wrap(file, name, removeByteOrderMark(timmedFileContents));
if (!options.outputdir) return cleanFileContents;
const fileExtension = options.wrapper === 'ts' ? 'ts' : 'js';
return fs.writeFileSync(path.join(options.outputdir, `${name}.${fileExtension}`), prepareOutput(cleanFileContents));
})
.filter(templateContents => typeof templateContents !== 'undefined');
// Output templates
if (!templates.length || options.outputdir) process.exit(0);
console.log(prepareOutput(templates.join('\n')));

52
scripts/release.sh Executable file
View file

@ -0,0 +1,52 @@
#!/bin/bash
#
# diff2html release script
# by rtfpessoa
#
set -e
INPUT_DIR=src
INTPUT_TEMPLATES_DIR=${INPUT_DIR}/templates
INPUT_UI_DIR=${INPUT_DIR}/ui
INPUT_JS_FILE=${INPUT_DIR}/diff2html.js
INPUT_JS_UI_FILE=${INPUT_UI_DIR}/js/diff2html-ui.js
INPUT_CSS_FILE=${INPUT_UI_DIR}/css/diff2html.css
GENERATED_TEMPLATES_FILE=${INTPUT_TEMPLATES_DIR}/diff2html-templates.js
OUTPUT_DIR=dist
OUTPUT_JS_FILE=${OUTPUT_DIR}/diff2html.js
OUTPUT_MIN_JS_FILE=${OUTPUT_DIR}/diff2html.min.js
OUTPUT_JS_UI_FILE=${OUTPUT_DIR}/diff2html-ui.js
OUTPUT_MIN_JS_UI_FILE=${OUTPUT_DIR}/diff2html-ui.min.js
OUTPUT_CSS_FILE=${OUTPUT_DIR}/diff2html.css
OUTPUT_MIN_CSS_FILE=${OUTPUT_DIR}/diff2html.min.css
echo "Creating diff2html release ..."
echo "Cleaning previous versions ..."
rm -rf ${OUTPUT_DIR}
mkdir -p ${OUTPUT_DIR}
echo "Minifying ${OUTPUT_CSS_FILE} to ${OUTPUT_MIN_CSS_FILE}"
cp -f ${INPUT_CSS_FILE} ${OUTPUT_CSS_FILE}
cleancss --advanced --compatibility=ie8 -o ${OUTPUT_MIN_CSS_FILE} ${OUTPUT_CSS_FILE}
echo "Pre-compile hogan.js templates"
npm run templates
echo "Generating js aggregation file in ${OUTPUT_JS_FILE}"
browserify -e ${INPUT_JS_FILE} -o ${OUTPUT_JS_FILE}
echo "Minifying ${OUTPUT_JS_FILE} to ${OUTPUT_MIN_JS_FILE}"
uglifyjs ${OUTPUT_JS_FILE} -c -o ${OUTPUT_MIN_JS_FILE}
echo "Generating js ui aggregation file in ${OUTPUT_JS_UI_FILE}"
browserify -e ${INPUT_JS_UI_FILE} -o ${OUTPUT_JS_UI_FILE}
echo "Minifying ${OUTPUT_JS_UI_FILE} to ${OUTPUT_MIN_JS_UI_FILE}"
uglifyjs ${OUTPUT_JS_UI_FILE} -c -o ${OUTPUT_MIN_JS_UI_FILE}
echo "diff2html release created successfully!"

16
scripts/update-bower-version.sh Executable file
View file

@ -0,0 +1,16 @@
#!/bin/bash
#
# diff2html update bower version
# by rtfpessoa
#
set -e
echo "diff2html updating bower version..."
RELEASE_VERSION=$(cat package.json | grep "version" | head -1 | gsed -e 's/ "version": "\(.*\)",/\1/')
gsed -i 's/.*"version".*/ "version": "'${RELEASE_VERSION}'",/' bower.json
echo "diff2html updated bower version successfully!"

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,272 +0,0 @@
import { FileListRenderer } from '../file-list-renderer';
import HoganJsUtils from '../hoganjs-utils';
import { ColorSchemeType } from '../types';
describe('FileListRenderer', () => {
describe('render', () => {
it('should expose old and new files to templates', () => {
const hoganUtils = new HoganJsUtils({
rawTemplates: {
'file-summary-wrapper': '{{{files}}}',
'file-summary-line': '{{oldName}}, {{newName}}, {{fileName}}',
},
});
const fileListRenderer = new FileListRenderer(hoganUtils);
const files = [
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js',
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 0,
language: 'js',
oldName: 'dev/null',
newName: 'my/file/name.js',
isNew: true,
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 0,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'dev/null',
isDeleted: true,
},
];
const fileHtml = fileListRenderer.render(files);
expect(fileHtml).toMatchInlineSnapshot(`
"my/file/name.js, my/file/name.js, my/file/name.js
my/file/name1.js, my/file/name2.js, my/file/{name1.js name2.js}
dev/null, my/file/name.js, my/file/name.js
my/file/name.js, dev/null, my/file/name.js"
`);
});
it('should work for all kinds of files', () => {
const hoganUtils = new HoganJsUtils({});
const fileListRenderer = new FileListRenderer(hoganUtils);
const files = [
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js',
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 0,
language: 'js',
oldName: 'dev/null',
newName: 'my/file/name.js',
isNew: true,
},
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 0,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'dev/null',
isDeleted: true,
},
];
const fileHtml = fileListRenderer.render(files);
expect(fileHtml).toMatchInlineSnapshot(`
"<div class="d2h-file-list-wrapper d2h-light-color-scheme">
<div class="d2h-file-list-header">
<span class="d2h-file-list-title">Files changed (4)</span>
<a class="d2h-file-switch d2h-hide">hide</a>
<a class="d2h-file-switch d2h-show">show</a>
</div>
<ol class="d2h-file-list">
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-changed" height="16" title="modified" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z"></path>
</svg> <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+12</span>
<span class="d2h-lines-deleted">-41</span>
</span>
</span>
</li>
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-moved" height="16" title="renamed" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z"></path>
</svg> <a href="#d2h-662683" class="d2h-file-name">my/file/{name1.js name2.js}</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+12</span>
<span class="d2h-lines-deleted">-41</span>
</span>
</span>
</li>
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-added" height="16" title="added" version="1.1" viewBox="0 0 14 16"
width="14">
<path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM6 9H3V7h3V4h2v3h3v2H8v3H6V9z"></path>
</svg> <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+12</span>
<span class="d2h-lines-deleted">-0</span>
</span>
</span>
</li>
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-deleted" height="16" title="removed" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM11 9H3V7h8v2z"></path>
</svg> <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+0</span>
<span class="d2h-lines-deleted">-41</span>
</span>
</span>
</li>
</ol>
</div>"
`);
});
describe('with dark colorScheme', () => {
it('should include dark colorScheme', () => {
const hoganUtils = new HoganJsUtils({});
const fileListRenderer = new FileListRenderer(hoganUtils, {
colorScheme: ColorSchemeType.DARK,
});
const files = [
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
},
];
const fileHtml = fileListRenderer.render(files);
expect(fileHtml).toMatchInlineSnapshot(`
"<div class="d2h-file-list-wrapper d2h-dark-color-scheme">
<div class="d2h-file-list-header">
<span class="d2h-file-list-title">Files changed (1)</span>
<a class="d2h-file-switch d2h-hide">hide</a>
<a class="d2h-file-switch d2h-show">show</a>
</div>
<ol class="d2h-file-list">
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-changed" height="16" title="modified" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z"></path>
</svg> <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+12</span>
<span class="d2h-lines-deleted">-41</span>
</span>
</span>
</li>
</ol>
</div>"
`);
});
});
describe('with auto colorScheme', () => {
it('should include auto colorScheme', () => {
const hoganUtils = new HoganJsUtils({});
const fileListRenderer = new FileListRenderer(hoganUtils, {
colorScheme: ColorSchemeType.AUTO,
});
const files = [
{
isCombined: false,
isGitDiff: false,
blocks: [],
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
},
];
const fileHtml = fileListRenderer.render(files);
expect(fileHtml).toMatchInlineSnapshot(`
"<div class="d2h-file-list-wrapper d2h-auto-color-scheme">
<div class="d2h-file-list-header">
<span class="d2h-file-list-title">Files changed (1)</span>
<a class="d2h-file-switch d2h-hide">hide</a>
<a class="d2h-file-switch d2h-show">show</a>
</div>
<ol class="d2h-file-list">
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon d2h-changed" height="16" title="modified" version="1.1"
viewBox="0 0 14 16" width="14">
<path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z"></path>
</svg> <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">+12</span>
<span class="d2h-lines-deleted">-41</span>
</span>
</span>
</li>
</ol>
</div>"
`);
});
});
});
});

View file

@ -1,56 +0,0 @@
import HoganJsUtils from '../hoganjs-utils';
import { CSSLineClass } from '../render-utils';
describe('HoganJsUtils', () => {
describe('render', () => {
it('should render view', () => {
const hoganJsUtils = new HoganJsUtils({});
const result = hoganJsUtils.render('generic', 'empty-diff', {
contentClass: 'd2h-code-line',
CSSLineClass: CSSLineClass,
});
expect(result).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-info">
<div class="d2h-code-line">
File without changes
</div>
</td>
</tr>"
`);
});
it('should throw exception if template is missing', () => {
const hoganJsUtils = new HoganJsUtils({});
expect(() => hoganJsUtils.render('generic', 'missing-template', {})).toThrow(Error);
});
it('should allow templates to be overridden with compiled templates', () => {
const emptyDiffTemplate = HoganJsUtils.compile('<p>{{myName}}</p>');
const hoganJsUtils = new HoganJsUtils({ compiledTemplates: { 'generic-empty-diff': emptyDiffTemplate } });
const result = hoganJsUtils.render('generic', 'empty-diff', { myName: 'Rodrigo Fernandes' });
expect(result).toMatchInlineSnapshot(`"<p>Rodrigo Fernandes</p>"`);
});
it('should allow templates to be overridden with uncompiled templates', () => {
const emptyDiffTemplate = '<p>{{myName}}</p>';
const hoganJsUtils = new HoganJsUtils({ rawTemplates: { 'generic-empty-diff': emptyDiffTemplate } });
const result = hoganJsUtils.render('generic', 'empty-diff', { myName: 'Rodrigo Fernandes' });
expect(result).toMatchInlineSnapshot(`"<p>Rodrigo Fernandes</p>"`);
});
it('should allow templates to be overridden giving priority to raw templates', () => {
const emptyDiffTemplate = HoganJsUtils.compile('<p>Not used!</p>');
const emptyDiffTemplateUncompiled = '<p>{{myName}}</p>';
const hoganJsUtils = new HoganJsUtils({
compiledTemplates: { 'generic-empty-diff': emptyDiffTemplate },
rawTemplates: { 'generic-empty-diff': emptyDiffTemplateUncompiled },
});
const result = hoganJsUtils.render('generic', 'empty-diff', { myName: 'Rodrigo Fernandes' });
expect(result).toMatchInlineSnapshot(`"<p>Rodrigo Fernandes</p>"`);
});
});
});

View file

@ -1,725 +0,0 @@
import LineByLineRenderer from '../line-by-line-renderer';
import HoganJsUtils from '../hoganjs-utils';
import { LineType, DiffFile, LineMatchingType } from '../types';
import { CSSLineClass } from '../render-utils';
describe('LineByLineRenderer', () => {
describe('_generateEmptyDiff', () => {
it('should return an empty diff', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateEmptyDiff();
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-info">
<div class="d2h-code-line">
File without changes
</div>
</td>
</tr>"
`);
});
});
describe('makeLineHtml', () => {
it('should work for insertions', () => {
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateSingleLineHtml(file, {
type: CSSLineClass.INSERTS,
prefix: '+',
content: 'test',
oldNumber: undefined,
newNumber: 30,
});
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-ins">
<div class="line-num1"></div>
<div class="line-num2">30</div>
</td>
<td class="d2h-ins">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">test</span>
</div>
</td>
</tr>"
`);
});
it('should work for deletions', () => {
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateSingleLineHtml(file, {
type: CSSLineClass.DELETES,
prefix: '-',
content: 'test',
oldNumber: 30,
newNumber: undefined,
});
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-del">
<div class="line-num1">30</div>
<div class="line-num2"></div>
</td>
<td class="d2h-del">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn">test</span>
</div>
</td>
</tr>"
`);
});
it('should convert indents into non breakin spaces (2 white spaces)', () => {
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateSingleLineHtml(file, {
type: CSSLineClass.INSERTS,
prefix: '+',
content: ' test',
oldNumber: undefined,
newNumber: 30,
});
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-ins">
<div class="line-num1"></div>
<div class="line-num2">30</div>
</td>
<td class="d2h-ins">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> test</span>
</div>
</td>
</tr>"
`);
});
it('should convert indents into non breakin spaces (4 white spaces)', () => {
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateSingleLineHtml(file, {
type: CSSLineClass.INSERTS,
prefix: '+',
content: ' test',
oldNumber: undefined,
newNumber: 30,
});
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-ins">
<div class="line-num1"></div>
<div class="line-num2">30</div>
</td>
<td class="d2h-ins">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> test</span>
</div>
</td>
</tr>"
`);
});
it('should preserve tabs', () => {
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const fileHtml = lineByLineRenderer.generateSingleLineHtml(file, {
type: CSSLineClass.INSERTS,
prefix: '+',
content: '\ttest',
oldNumber: undefined,
newNumber: 30,
});
expect(fileHtml).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-ins">
<div class="line-num1"></div>
<div class="line-num2">30</div>
</td>
<td class="d2h-ins">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"> test</span>
</div>
</td>
</tr>"
`);
});
});
describe('makeFileDiffHtml', () => {
it('should work for simple file', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js',
isCombined: false,
isGitDiff: false,
blocks: [],
};
const diffs = '<span>Random Html</span>';
const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs);
expect(fileHtml).toMatchInlineSnapshot(`
"<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">my/file/name.js</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<span>Random Html</span>
</tbody>
</table>
</div>
</div>
</div>"
`);
});
it('should work for simple added file', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const file = {
addedLines: 12,
deletedLines: 0,
language: 'js',
oldName: 'dev/null',
newName: 'my/file/name.js',
isNew: true,
isCombined: false,
isGitDiff: false,
blocks: [],
};
const diffs = '<span>Random Html</span>';
const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs);
expect(fileHtml).toMatchInlineSnapshot(`
"<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">my/file/name.js</span>
<span class="d2h-tag d2h-added d2h-added-tag">ADDED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<span>Random Html</span>
</tbody>
</table>
</div>
</div>
</div>"
`);
});
it('should work for simple deleted file', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const file = {
addedLines: 0,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'dev/null',
isDeleted: true,
isCombined: false,
isGitDiff: false,
blocks: [],
};
const diffs = '<span>Random Html</span>';
const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs);
expect(fileHtml).toMatchInlineSnapshot(`
"<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">my/file/name.js</span>
<span class="d2h-tag d2h-deleted d2h-deleted-tag">DELETED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<span>Random Html</span>
</tbody>
</table>
</div>
</div>
</div>"
`);
});
it('should work for simple renamed file', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js',
isRename: true,
isCombined: false,
isGitDiff: false,
blocks: [],
};
const diffs = '<span>Random Html</span>';
const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs);
expect(fileHtml).toMatchInlineSnapshot(`
"<div id="d2h-662683" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">my/file/{name1.js name2.js}</span>
<span class="d2h-tag d2h-moved d2h-moved-tag">RENAMED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<span>Random Html</span>
</tbody>
</table>
</div>
</div>
</div>"
`);
});
it('should return empty when option renderNothingWhenEmpty is true and file blocks not present', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {
renderNothingWhenEmpty: true,
});
const file = {
addedLines: 0,
deletedLines: 0,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js',
isRename: true,
isCombined: false,
isGitDiff: false,
blocks: [],
};
const diffs = '<span>Random Html</span>';
const fileHtml = lineByLineRenderer.makeFileDiffHtml(file, diffs);
expect(fileHtml).toMatchInlineSnapshot(`""`);
});
});
describe('generateLineByLineJsonHtml', () => {
it('should work for list of files', () => {
const exampleJson: DiffFile[] = [
{
blocks: [
{
lines: [
{
content: '-test',
type: LineType.DELETE,
oldNumber: 1,
newNumber: undefined,
},
{
content: '+test1r',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 1,
},
],
oldStartLine: 1,
oldStartLine2: undefined,
newStartLine: 1,
header: '@@ -1 +1 @@',
},
],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
newName: 'sample',
language: 'txt',
isCombined: false,
isGitDiff: true,
},
];
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {
matching: LineMatchingType.LINES,
});
const html = lineByLineRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="txt">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-line">@@ -1 +1 @@</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-del d2h-change">
<div class="line-num1">1</div>
<div class="line-num2"></div>
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>test</del></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-ins d2h-change">
<div class="line-num1"></div>
<div class="line-num2">1</div>
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>test1r</ins></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>"
`);
});
it('should work for empty blocks', () => {
const exampleJson = [
{
blocks: [],
deletedLines: 0,
addedLines: 0,
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false,
isGitDiff: false,
},
];
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {
renderNothingWhenEmpty: false,
});
const html = lineByLineRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-info">
<div class="d2h-code-line">
File without changes
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>"
`);
});
it('should work for too big file diff', () => {
const exampleJson = [
{
blocks: [
{
header: '<a href="http://example.com">Custom link to render</a>',
lines: [],
newStartLine: 0,
oldStartLine: 0,
oldStartLine2: undefined,
},
],
deletedLines: 0,
addedLines: 0,
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false,
isGitDiff: false,
isTooBig: true,
},
];
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils);
const html = lineByLineRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-file-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-line"><a href="http://example.com">Custom link to render</a></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>"
`);
});
});
describe('_generateFileHtml', () => {
it('should work for simple file', () => {
const hoganUtils = new HoganJsUtils({});
const lineByLineRenderer = new LineByLineRenderer(hoganUtils, {});
const file: DiffFile = {
blocks: [
{
lines: [
{
content: ' one context line',
type: LineType.CONTEXT,
oldNumber: 1,
newNumber: 1,
},
{
content: '-test',
type: LineType.DELETE,
oldNumber: 2,
newNumber: undefined,
},
{
content: '+test1r',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 2,
},
{
content: '+test2r',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 3,
},
],
oldStartLine: 1,
oldStartLine2: undefined,
newStartLine: 1,
header: '@@ -1 +1 @@',
},
],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: 'txt',
newName: 'sample',
isCombined: false,
isGitDiff: true,
};
const html = lineByLineRenderer.generateFileHtml(file);
expect(html).toMatchInlineSnapshot(`
"<tr>
<td class="d2h-code-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-line">@@ -1 +1 @@</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-cntx">
<div class="line-num1">1</div>
<div class="line-num2">1</div>
</td>
<td class="d2h-cntx">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn">one context line</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-del d2h-change">
<div class="line-num1">2</div>
<div class="line-num2"></div>
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>test</del></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-ins d2h-change">
<div class="line-num1"></div>
<div class="line-num2">2</div>
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>test1r</ins></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-linenumber d2h-ins">
<div class="line-num1"></div>
<div class="line-num2">3</div>
</td>
<td class="d2h-ins">
<div class="d2h-code-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">test2r</span>
</div>
</td>
</tr>"
`);
});
});
});

View file

@ -1,152 +0,0 @@
import { escapeForHtml, getHtmlId, filenameDiff, diffHighlight } from '../render-utils';
import { DiffStyleType, LineMatchingType } from '../types';
describe('Utils', () => {
describe('escapeForHtml', () => {
it('should escape & with &amp;', () => {
const result = escapeForHtml('&');
expect(result).toBe('&amp;');
});
it('should escape < with &lt;', () => {
const result = escapeForHtml('<');
expect(result).toBe('&lt;');
});
it('should escape > with &gt;', () => {
const result = escapeForHtml('>');
expect(result).toBe('&gt;');
});
it('should escape " with &quot;', () => {
const result = escapeForHtml('"');
expect(result).toBe('&quot;');
});
it("should escape ' with &#x27;", () => {
const result = escapeForHtml("'");
expect(result).toBe('&#x27;');
});
it('should escape / with &#x2F;', () => {
const result = escapeForHtml('/');
expect(result).toBe('&#x2F;');
});
it('should escape a string containing HTML code', () => {
const result = escapeForHtml(`<a href="/search?q=diff2html">Search 'Diff2Html'</a>`);
expect(result).toBe(
'&lt;a href=&quot;&#x2F;search?q=diff2html&quot;&gt;Search &#x27;Diff2Html&#x27;&lt;&#x2F;a&gt;',
);
});
});
describe('getHtmlId', () => {
it('should generate file unique id', () => {
const result = getHtmlId({
oldName: 'sample.js',
newName: 'sample.js',
});
expect(result).toBe('d2h-960013');
});
});
describe('getDiffName', () => {
it('should generate the file name for a changed file', () => {
const result = filenameDiff({
oldName: 'sample.js',
newName: 'sample.js',
});
expect(result).toBe('sample.js');
});
it('should generate the file name for a changed file and full rename', () => {
const result = filenameDiff({
oldName: 'sample1.js',
newName: 'sample2.js',
});
expect(result).toBe('sample1.js → sample2.js');
});
it('should generate the file name for a changed file and prefix rename', () => {
const result = filenameDiff({
oldName: 'src/path/sample.js',
newName: 'source/path/sample.js',
});
expect(result).toBe('{src → source}/path/sample.js');
});
it('should generate the file name for a changed file and suffix rename', () => {
const result = filenameDiff({
oldName: 'src/path/sample1.js',
newName: 'src/path/sample2.js',
});
expect(result).toBe('src/path/{sample1.js → sample2.js}');
});
it('should generate the file name for a changed file and middle rename', () => {
const result = filenameDiff({
oldName: 'src/really/big/path/sample.js',
newName: 'src/small/path/sample.js',
});
expect(result).toBe('src/{really/big → small}/path/sample.js');
});
it('should generate the file name for a deleted file', () => {
const result = filenameDiff({
oldName: 'src/my/file.js',
newName: '/dev/null',
});
expect(result).toBe('src/my/file.js');
});
it('should generate the file name for a new file', () => {
const result = filenameDiff({
oldName: '/dev/null',
newName: 'src/my/file.js',
});
expect(result).toBe('src/my/file.js');
});
});
describe('diffHighlight', () => {
it('should highlight two lines', () => {
const result = diffHighlight('-var myVar = 2;', '+var myVariable = 3;', false, {
matching: LineMatchingType.WORDS,
});
expect(result).toEqual({
oldLine: {
prefix: '-',
content: 'var <del>myVar</del> = <del>2</del>;',
},
newLine: {
prefix: '+',
content: 'var <ins>myVariable</ins> = <ins>3</ins>;',
},
});
});
it('should highlight two lines char by char', () => {
const result = diffHighlight('-var myVar = 2;', '+var myVariable = 3;', false, {
diffStyle: DiffStyleType.CHAR,
});
expect(result).toEqual({
oldLine: {
prefix: '-',
content: 'var myVar = <del>2</del>;',
},
newLine: {
prefix: '+',
content: 'var myVar<ins>iable</ins> = <ins>3</ins>;',
},
});
});
it('should highlight combined diff lines', () => {
const result = diffHighlight(' -var myVar = 2;', ' +var myVariable = 3;', true, {
diffStyle: DiffStyleType.WORD,
matching: LineMatchingType.WORDS,
matchWordsThreshold: 1.0,
});
expect(result).toEqual({
oldLine: {
prefix: ' -',
content: 'var <del class="d2h-change">myVar</del> = <del class="d2h-change">2</del>;',
},
newLine: {
prefix: ' +',
content: 'var <ins class="d2h-change">myVariable</ins> = <ins class="d2h-change">3</ins>;',
},
});
});
});
});

View file

@ -1,538 +0,0 @@
import SideBySideRenderer from '../side-by-side-renderer';
import HoganJsUtils from '../hoganjs-utils';
import { LineType, DiffLine, DiffFile, LineMatchingType } from '../types';
import { CSSLineClass } from '../render-utils';
describe('SideBySideRenderer', () => {
describe('generateEmptyDiff', () => {
it('should return an empty diff', () => {
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const fileHtml = sideBySideRenderer.generateEmptyDiff();
expect(fileHtml).toMatchInlineSnapshot(`
{
"left": "<tr>
<td class="d2h-info">
<div class="d2h-code-side-line">
File without changes
</div>
</td>
</tr>",
"right": "",
}
`);
});
});
describe('generateSideBySideFileHtml', () => {
it('should generate lines with the right prefixes', () => {
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const file: DiffFile = {
isGitDiff: true,
blocks: [
{
lines: [
{
content: ' context',
type: LineType.CONTEXT,
oldNumber: 19,
newNumber: 19,
},
{
content: '-removed',
type: LineType.DELETE,
oldNumber: 20,
newNumber: undefined,
},
{
content: '+added',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 20,
},
{
content: '+another added',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 21,
},
],
oldStartLine: 19,
newStartLine: 19,
header: '@@ -19,7 +19,7 @@',
},
],
deletedLines: 1,
addedLines: 2,
checksumBefore: 'fc56817',
checksumAfter: 'e8e7e49',
mode: '100644',
oldName: 'coverage.init',
language: 'init',
newName: 'coverage.init',
isCombined: false,
};
const fileHtml = sideBySideRenderer.generateFileHtml(file);
expect(fileHtml).toMatchInlineSnapshot(`
{
"left": "<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -19,7 +19,7 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
19
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn">context</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-del d2h-change">
20
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>removed</del></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr>",
"right": "<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-cntx">
19
</td>
<td class="d2h-cntx">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn">context</span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins d2h-change">
20
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>added</ins></span>
</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins">
21
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">another added</span>
</div>
</td>
</tr>",
}
`);
});
});
describe('generateSingleLineHtml', () => {
it('should work for insertions', () => {
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const fileHtml = sideBySideRenderer.generateLineHtml(undefined, {
type: CSSLineClass.INSERTS,
prefix: '+',
content: 'test',
number: 30,
});
expect(fileHtml).toMatchInlineSnapshot(`
{
"left": "<tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr>",
"right": "<tr>
<td class="d2h-code-side-linenumber d2h-ins">
30
</td>
<td class="d2h-ins">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn">test</span>
</div>
</td>
</tr>",
}
`);
});
it('should work for deletions', () => {
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const fileHtml = sideBySideRenderer.generateLineHtml(
{
type: CSSLineClass.DELETES,
prefix: '-',
content: 'test',
number: 30,
},
undefined,
);
expect(fileHtml).toMatchInlineSnapshot(`
{
"left": "<tr>
<td class="d2h-code-side-linenumber d2h-del">
30
</td>
<td class="d2h-del">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn">test</span>
</div>
</td>
</tr>",
"right": "<tr>
<td class="d2h-code-side-linenumber d2h-code-side-emptyplaceholder d2h-cntx d2h-emptyplaceholder">
</td>
<td class="d2h-cntx d2h-emptyplaceholder">
<div class="d2h-code-side-line d2h-code-side-emptyplaceholder">
<span class="d2h-code-line-prefix">&nbsp;</span>
<span class="d2h-code-line-ctn"><br></span>
</div>
</td>
</tr>",
}
`);
});
});
describe('generateSideBySideJsonHtml', () => {
it('should work for list of files', () => {
const exampleJson: DiffFile[] = [
{
blocks: [
{
lines: [
{
content: '-test',
type: LineType.DELETE,
oldNumber: 1,
newNumber: undefined,
},
{
content: '+test1r',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 1,
},
],
oldStartLine: 1,
oldStartLine2: undefined,
newStartLine: 1,
header: '@@ -1 +1 @@',
},
],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: 'txt',
newName: 'sample',
isCombined: false,
isGitDiff: true,
},
];
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, { matching: LineMatchingType.LINES });
const html = sideBySideRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="txt">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">@@ -1 +1 @@</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-del d2h-change">
1
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>test</del></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr><tr>
<td class="d2h-code-side-linenumber d2h-ins d2h-change">
1
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>test1r</ins></span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>"
`);
});
it('should work for files without blocks', () => {
const exampleJson: DiffFile[] = [
{
blocks: [],
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false,
addedLines: 0,
deletedLines: 0,
isGitDiff: false,
},
];
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, {});
const html = sideBySideRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-info">
<div class="d2h-code-side-line">
File without changes
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>"
`);
});
it('should work for too big file diff', () => {
const exampleJson = [
{
blocks: [
{
header: '<a href="http://example.com">Custom link to render</a>',
lines: [],
newStartLine: 0,
oldStartLine: 0,
oldStartLine2: undefined,
},
],
deletedLines: 0,
addedLines: 0,
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false,
isGitDiff: false,
isTooBig: true,
},
];
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils);
const html = sideBySideRenderer.render(exampleJson);
expect(html).toMatchInlineSnapshot(`
"<div class="d2h-wrapper d2h-light-color-scheme">
<div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">
<div class="d2h-file-header">
<span class="d2h-file-name-wrapper">
<svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">
<path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>
</svg> <span class="d2h-file-name">sample</span>
<span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>
</div>
<div class="d2h-files-diff">
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line"><a href="http://example.com">Custom link to render</a></div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="d2h-file-side-diff">
<div class="d2h-code-wrapper">
<table class="d2h-diff-table">
<tbody class="d2h-diff-tbody">
<tr>
<td class="d2h-code-side-linenumber d2h-info"></td>
<td class="d2h-info">
<div class="d2h-code-side-line">&nbsp;</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>"
`);
});
});
describe('processLines', () => {
it('should process file lines', () => {
const oldLines: DiffLine[] = [
{
content: '-test',
type: LineType.DELETE,
oldNumber: 1,
newNumber: undefined,
},
];
const newLines: DiffLine[] = [
{
content: '+test1r',
type: LineType.INSERT,
oldNumber: undefined,
newNumber: 1,
},
];
const hoganUtils = new HoganJsUtils({});
const sideBySideRenderer = new SideBySideRenderer(hoganUtils, { matching: LineMatchingType.LINES });
const html = sideBySideRenderer.processChangedLines(false, oldLines, newLines);
expect(html).toMatchInlineSnapshot(`
{
"left": "<tr>
<td class="d2h-code-side-linenumber d2h-del d2h-change">
1
</td>
<td class="d2h-del d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">-</span>
<span class="d2h-code-line-ctn"><del>test</del></span>
</div>
</td>
</tr>",
"right": "<tr>
<td class="d2h-code-side-linenumber d2h-ins d2h-change">
1
</td>
<td class="d2h-ins d2h-change">
<div class="d2h-code-side-line">
<span class="d2h-code-line-prefix">+</span>
<span class="d2h-code-line-ctn"><ins>test1r</ins></span>
</div>
</td>
</tr>",
}
`);
});
});
});

View file

@ -1,31 +0,0 @@
import { escapeForRegExp, unifyPath, hashCode } from '../utils';
describe('Utils', () => {
describe('escapeForRegExp', () => {
it('should escape markdown link text', () => {
const result = escapeForRegExp('[Link](https://diff2html.xyz)');
expect(result).toBe('\\[Link\\]\\(https:\\/\\/diff2html\\.xyz\\)');
});
it('should escape all dangerous characters', () => {
const result = escapeForRegExp('-[]/{}()*+?.\\^$|');
expect(result).toBe('\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|');
});
});
describe('unifyPath', () => {
it('should unify windows style path', () => {
const result = unifyPath('\\Users\\Downloads\\diff.html');
expect(result).toBe('/Users/Downloads/diff.html');
});
});
describe('hashCode', () => {
it('should create consistent hash for a text piece', () => {
const string = '/home/diff2html/diff.html';
expect(hashCode(string)).toEqual(hashCode(string));
});
it('should create different hash for different text pieces', () => {
expect(hashCode('/home/diff2html/diff1.html')).not.toEqual(hashCode('/home/diff2html/diff2.html'));
});
});
});

347
src/diff-parser.js Normal file
View file

@ -0,0 +1,347 @@
/*
*
* Diff Parser (diff-parser.js)
* Author: rtfpessoa
*
*/
(function() {
var utils = require('./utils.js').Utils;
var LINE_TYPE = {
INSERTS: 'd2h-ins',
DELETES: 'd2h-del',
INSERT_CHANGES: 'd2h-ins d2h-change',
DELETE_CHANGES: 'd2h-del d2h-change',
CONTEXT: 'd2h-cntx',
INFO: 'd2h-info'
};
function DiffParser() {
}
DiffParser.prototype.LINE_TYPE = LINE_TYPE;
DiffParser.prototype.generateDiffJson = function(diffInput, configuration) {
var config = configuration || {};
var files = [];
var currentFile = null;
var currentBlock = null;
var oldLine = null;
var oldLine2 = null; // Used for combined diff
var newLine = null;
/* Add previous block(if exists) before start a new file */
var saveBlock = function() {
if (currentBlock) {
currentFile.blocks.push(currentBlock);
currentBlock = null;
}
};
/*
* Add previous file(if exists) before start a new one
* if it has name (to avoid binary files errors)
*/
var saveFile = function() {
if (currentFile && currentFile.newName) {
files.push(currentFile);
currentFile = null;
}
};
/* Create file structure */
var startFile = function() {
saveBlock();
saveFile();
currentFile = {};
currentFile.blocks = [];
currentFile.deletedLines = 0;
currentFile.addedLines = 0;
};
var startBlock = function(line) {
saveBlock();
var values;
/**
* From Range:
* -<start line>[,<number of lines>]
*
* To Range:
* +<start line>[,<number of lines>]
*
* @@ from-file-range to-file-range @@
*
* @@@ from-file-range from-file-range to-file-range @@@
*
* number of lines is optional, if omited consider 0
*/
if (values = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@.*/.exec(line)) {
currentFile.isCombined = false;
oldLine = values[1];
newLine = values[2];
} else if (values = /^@@@ -(\d+)(?:,\d+)? -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@@.*/.exec(line)) {
currentFile.isCombined = true;
oldLine = values[1];
oldLine2 = values[2];
newLine = values[3];
} else {
console.error("Failed to parse lines, starting in 0!");
oldLine = 0;
newLine = 0;
currentFile.isCombined = false;
}
/* Create block metadata */
currentBlock = {};
currentBlock.lines = [];
currentBlock.oldStartLine = oldLine;
currentBlock.oldStartLine2 = oldLine2;
currentBlock.newStartLine = newLine;
currentBlock.header = line;
};
var createLine = function(line) {
var currentLine = {};
currentLine.content = line;
var newLinePrefixes = !currentFile.isCombined ? ['+'] : ['+', ' +'];
var delLinePrefixes = !currentFile.isCombined ? ['-'] : ['-', ' -'];
/* Fill the line data */
if (utils.startsWith(line, newLinePrefixes)) {
currentFile.addedLines++;
currentLine.type = LINE_TYPE.INSERTS;
currentLine.oldNumber = null;
currentLine.newNumber = newLine++;
currentBlock.lines.push(currentLine);
} else if (utils.startsWith(line, delLinePrefixes)) {
currentFile.deletedLines++;
currentLine.type = LINE_TYPE.DELETES;
currentLine.oldNumber = oldLine++;
currentLine.newNumber = null;
currentBlock.lines.push(currentLine);
} else {
currentLine.type = LINE_TYPE.CONTEXT;
currentLine.oldNumber = oldLine++;
currentLine.newNumber = newLine++;
currentBlock.lines.push(currentLine);
}
};
var diffLines =
diffInput.replace(/\\ No newline at end of file/g, '')
.replace(/\r\n?/g, '\n')
.split('\n');
/* Diff */
var oldMode = /^old mode (\d{6})/;
var newMode = /^new mode (\d{6})/;
var deletedFileMode = /^deleted file mode (\d{6})/;
var newFileMode = /^new file mode (\d{6})/;
var copyFrom = /^copy from "?(.+)"?/;
var copyTo = /^copy to "?(.+)"?/;
var renameFrom = /^rename from "?(.+)"?/;
var renameTo = /^rename to "?(.+)"?/;
var similarityIndex = /^similarity index (\d+)%/;
var dissimilarityIndex = /^dissimilarity index (\d+)%/;
var index = /^index ([0-9a-z]+)\.\.([0-9a-z]+)\s*(\d{6})?/;
/* Combined Diff */
var combinedIndex = /^index ([0-9a-z]+),([0-9a-z]+)\.\.([0-9a-z]+)/;
var combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/;
var combinedNewFile = /^new file mode (\d{6})/;
var combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/;
diffLines.forEach(function(line) {
// Unmerged paths, and possibly other non-diffable files
// https://github.com/scottgonzalez/pretty-diff/issues/11
// Also, remove some useless lines
if (!line || utils.startsWith(line, '*')) {
return;
}
if (
utils.startsWith(line, 'diff') || // Git diffs always start with diff
!currentFile || // If we do not have a file yet, we should crete one
(
currentFile && // If we already have some file in progress and
(
currentFile.oldName && utils.startsWith(line, '---') || // Either we reached a old file identification line
currentFile.newName && utils.startsWith(line, '+++') // Or we reached a new file identification line
)
)
) {
startFile();
}
var values;
/*
* --- Date Timestamp[FractionalSeconds] TimeZone
* --- 2002-02-21 23:30:39.942229878 -0800
*/
if (currentFile && !currentFile.oldName &&
utils.startsWith(line, '---') && (values = getSrcFilename(line, config))) {
currentFile.oldName = values;
currentFile.language = getExtension(currentFile.oldName, currentFile.language);
return;
}
/*
* +++ Date Timestamp[FractionalSeconds] TimeZone
* +++ 2002-02-21 23:30:39.942229878 -0800
*/
if (currentFile && !currentFile.newName &&
utils.startsWith(line, '+++') && (values = getDstFilename(line, config))) {
currentFile.newName = values;
currentFile.language = getExtension(currentFile.newName, currentFile.language);
return;
}
if (currentFile && utils.startsWith(line, '@')) {
startBlock(line);
return;
}
/*
* There are three types of diff lines. These lines are defined by the way they start.
* 1. New line starts with: +
* 2. Old line starts with: -
* 3. Context line starts with: <SPACE>
*/
if (currentBlock && (utils.startsWith(line, '+') || utils.startsWith(line, '-') || utils.startsWith(line, ' '))) {
createLine(line);
return;
}
if (
(currentFile && currentFile.blocks.length) ||
(currentBlock && currentBlock.lines.length)
) {
startFile();
}
/*
* Git diffs provide more information regarding files modes, renames, copies,
* commits between changes and similarity indexes
*/
if ((values = oldMode.exec(line))) {
currentFile.oldMode = values[1];
} else if ((values = newMode.exec(line))) {
currentFile.newMode = values[1];
} else if ((values = deletedFileMode.exec(line))) {
currentFile.deletedFileMode = values[1];
currentFile.isDeleted = true;
} else if ((values = newFileMode.exec(line))) {
currentFile.newFileMode = values[1];
currentFile.isNew = true;
} else if ((values = copyFrom.exec(line))) {
currentFile.oldName = values[1];
currentFile.isCopy = true;
} else if ((values = copyTo.exec(line))) {
currentFile.newName = values[1];
currentFile.isCopy = true;
} else if ((values = renameFrom.exec(line))) {
currentFile.oldName = values[1];
currentFile.isRename = true;
} else if ((values = renameTo.exec(line))) {
currentFile.newName = values[1];
currentFile.isRename = true;
} else if ((values = similarityIndex.exec(line))) {
currentFile.unchangedPercentage = values[1];
} else if ((values = dissimilarityIndex.exec(line))) {
currentFile.changedPercentage = values[1];
} else if ((values = index.exec(line))) {
currentFile.checksumBefore = values[1];
currentFile.checksumAfter = values[2];
values[3] && (currentFile.mode = values[3]);
} else if ((values = combinedIndex.exec(line))) {
currentFile.checksumBefore = [values[2], values[3]];
currentFile.checksumAfter = values[1];
} else if ((values = combinedMode.exec(line))) {
currentFile.oldMode = [values[2], values[3]];
currentFile.newMode = values[1];
} else if ((values = combinedNewFile.exec(line))) {
currentFile.newFileMode = values[1];
currentFile.isNew = true;
} else if ((values = combinedDeletedFile.exec(line))) {
currentFile.deletedFileMode = values[1];
currentFile.isDeleted = true;
}
});
saveBlock();
saveFile();
return files;
};
function getExtension(filename, language) {
var nameSplit = filename.split('.');
if (nameSplit.length > 1) {
return nameSplit[nameSplit.length - 1];
}
return language;
}
function getSrcFilename(line, cfg) {
var prefixes = ["a/", "i/", "w/", "c/", "o/"];
if (cfg.srcPrefix) {
prefixes.push(cfg.srcPrefix);
}
return _getFilename('---', line, prefixes);
}
function getDstFilename(line, cfg) {
var prefixes = ["b/", "i/", "w/", "c/", "o/"];
if (cfg.dstPrefix) {
prefixes.push(cfg.dstPrefix);
}
return _getFilename('\\+\\+\\+', line, prefixes);
}
function _getFilename(linePrefix, line, prefixes) {
var FilenameRegExp = new RegExp('^' + linePrefix + ' "?(.+?)"?$');
var filename;
var values = FilenameRegExp.exec(line);
if (values && values[1]) {
filename = values[1];
var matchingPrefixes = prefixes.filter(function(p) {
return filename.indexOf(p) === 0;
});
if (matchingPrefixes[0]) {
// Remove prefix if exists
filename = filename.slice(matchingPrefixes[0].length);
}
}
return filename;
}
module.exports.DiffParser = new DiffParser();
})();

View file

@ -1,481 +0,0 @@
import { DiffFile, DiffBlock, DiffLine, LineType } from './types';
import { escapeForRegExp } from './utils';
export interface DiffParserConfig {
srcPrefix?: string;
dstPrefix?: string;
diffMaxChanges?: number;
diffMaxLineLength?: number;
diffTooBigMessage?: (fileIndex: number) => string;
}
function getExtension(filename: string, language: string): string {
const filenameParts = filename.split('.');
return filenameParts.length > 1 ? filenameParts[filenameParts.length - 1] : language;
}
function startsWithAny(str: string, prefixes: string[]): boolean {
return prefixes.reduce<boolean>((startsWith, prefix) => startsWith || str.startsWith(prefix), false);
}
const baseDiffFilenamePrefixes = ['a/', 'b/', 'i/', 'w/', 'c/', 'o/'];
function getFilename(line: string, linePrefix?: string, extraPrefix?: string): string {
const prefixes = extraPrefix !== undefined ? [...baseDiffFilenamePrefixes, extraPrefix] : baseDiffFilenamePrefixes;
const FilenameRegExp = linePrefix
? new RegExp(`^${escapeForRegExp(linePrefix)} "?(.+?)"?$`)
: new RegExp('^"?(.+?)"?$');
const [, filename = ''] = FilenameRegExp.exec(line) || [];
const matchingPrefix = prefixes.find(p => filename.indexOf(p) === 0);
const fnameWithoutPrefix = matchingPrefix ? filename.slice(matchingPrefix.length) : filename;
// Cleanup timestamps generated by the unified diff (diff command) as specified in
// https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Unified.html
// Ie: 2016-10-25 11:37:14.000000000 +0200
return fnameWithoutPrefix.replace(/\s+\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d+)? [+-]\d{4}.*$/, '');
}
function getSrcFilename(line: string, srcPrefix?: string): string | undefined {
return getFilename(line, '---', srcPrefix);
}
function getDstFilename(line: string, dstPrefix?: string): string | undefined {
return getFilename(line, '+++', dstPrefix);
}
/**
*
* Docs:
* - Unified: https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html
* - Git Diff: https://git-scm.com/docs/git-diff-tree#_raw_output_format
* - Git Combined Diff: https://git-scm.com/docs/git-diff-tree#_combined_diff_format
*
*/
export function parse(diffInput: string, config: DiffParserConfig = {}): DiffFile[] {
const files: DiffFile[] = [];
let currentFile: DiffFile | null = null;
let currentBlock: DiffBlock | null = null;
let oldLine: number | null = null;
let oldLine2: number | null = null; // Used for combined diff
let newLine: number | null = null;
let possibleOldName: string | null = null;
let possibleNewName: string | null = null;
/* Diff Header */
const oldFileNameHeader = '--- ';
const newFileNameHeader = '+++ ';
const hunkHeaderPrefix = '@@';
/* Diff */
const oldMode = /^old mode (\d{6})/;
const newMode = /^new mode (\d{6})/;
const deletedFileMode = /^deleted file mode (\d{6})/;
const newFileMode = /^new file mode (\d{6})/;
const copyFrom = /^copy from "?(.+)"?/;
const copyTo = /^copy to "?(.+)"?/;
const renameFrom = /^rename from "?(.+)"?/;
const renameTo = /^rename to "?(.+)"?/;
const similarityIndex = /^similarity index (\d+)%/;
const dissimilarityIndex = /^dissimilarity index (\d+)%/;
const index = /^index ([\da-z]+)\.\.([\da-z]+)\s*(\d{6})?/;
const binaryFiles = /^Binary files (.*) and (.*) differ/;
const binaryDiff = /^GIT binary patch/;
/* Combined Diff */
const combinedIndex = /^index ([\da-z]+),([\da-z]+)\.\.([\da-z]+)/;
const combinedMode = /^mode (\d{6}),(\d{6})\.\.(\d{6})/;
const combinedNewFile = /^new file mode (\d{6})/;
const combinedDeletedFile = /^deleted file mode (\d{6}),(\d{6})/;
const diffLines = diffInput
.replace(/\\ No newline at end of file/g, '')
.replace(/\r\n?/g, '\n')
.split('\n');
/* Add previous block(if exists) before start a new file */
function saveBlock(): void {
if (currentBlock !== null && currentFile !== null) {
currentFile.blocks.push(currentBlock);
currentBlock = null;
}
}
/*
* Add previous file(if exists) before start a new one
* if it has name (to avoid binary files errors)
*/
function saveFile(): void {
if (currentFile !== null) {
if (!currentFile.oldName && possibleOldName !== null) {
currentFile.oldName = possibleOldName;
}
if (!currentFile.newName && possibleNewName !== null) {
currentFile.newName = possibleNewName;
}
if (currentFile.newName) {
files.push(currentFile);
currentFile = null;
}
}
possibleOldName = null;
possibleNewName = null;
}
/* Create file structure */
function startFile(): void {
saveBlock();
saveFile();
// eslint-disable-next-line
// @ts-ignore
currentFile = {
blocks: [],
deletedLines: 0,
addedLines: 0,
};
}
function startBlock(line: string): void {
saveBlock();
let values;
/**
* From Range:
* -<start line>[,<number of lines>]
*
* To Range:
* +<start line>[,<number of lines>]
*
* @@ from-file-range to-file-range @@
*
* @@@ from-file-range from-file-range to-file-range @@@
*
* number of lines is optional, if omited consider 0
*/
if (currentFile !== null) {
if ((values = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@.*/.exec(line))) {
currentFile.isCombined = false;
oldLine = parseInt(values[1], 10);
newLine = parseInt(values[2], 10);
} else if ((values = /^@@@ -(\d+)(?:,\d+)? -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@@.*/.exec(line))) {
currentFile.isCombined = true;
oldLine = parseInt(values[1], 10);
oldLine2 = parseInt(values[2], 10);
newLine = parseInt(values[3], 10);
} else {
if (line.startsWith(hunkHeaderPrefix)) {
console.error('Failed to parse lines, starting in 0!');
}
oldLine = 0;
newLine = 0;
currentFile.isCombined = false;
}
}
/* Create block metadata */
currentBlock = {
lines: [],
// eslint-disable-next-line
// @ts-ignore
oldStartLine: oldLine,
// eslint-disable-next-line
// @ts-ignore
oldStartLine2: oldLine2,
// eslint-disable-next-line
// @ts-ignore
newStartLine: newLine,
header: line,
};
}
function createLine(line: string): void {
if (currentFile === null || currentBlock === null || oldLine === null || newLine === null) return;
// eslint-disable-next-line
// @ts-ignore
const currentLine: DiffLine = {
content: line,
};
const addedPrefixes = currentFile.isCombined ? ['+ ', ' +', '++'] : ['+'];
const deletedPrefixes = currentFile.isCombined ? ['- ', ' -', '--'] : ['-'];
if (startsWithAny(line, addedPrefixes)) {
currentFile.addedLines++;
currentLine.type = LineType.INSERT;
currentLine.oldNumber = undefined;
currentLine.newNumber = newLine++;
} else if (startsWithAny(line, deletedPrefixes)) {
currentFile.deletedLines++;
currentLine.type = LineType.DELETE;
currentLine.oldNumber = oldLine++;
currentLine.newNumber = undefined;
} else {
currentLine.type = LineType.CONTEXT;
currentLine.oldNumber = oldLine++;
currentLine.newNumber = newLine++;
}
currentBlock.lines.push(currentLine);
}
/*
* Checks if there is a hunk header coming before a new file starts
*
* Hunk header is a group of three lines started by ( `--- ` , `+++ ` , `@@` )
*/
function existHunkHeader(line: string, lineIdx: number): boolean {
let idx = lineIdx;
while (idx < diffLines.length - 3) {
if (line.startsWith('diff')) {
return false;
}
if (
diffLines[idx].startsWith(oldFileNameHeader) &&
diffLines[idx + 1].startsWith(newFileNameHeader) &&
diffLines[idx + 2].startsWith(hunkHeaderPrefix)
) {
return true;
}
idx++;
}
return false;
}
diffLines.forEach((line, lineIndex) => {
// Unmerged paths, and possibly other non-diffable files
// https://github.com/scottgonzalez/pretty-diff/issues/11
// Also, remove some useless lines
if (!line || line.startsWith('*')) {
return;
}
// Used to store regex capture groups
let values;
const prevLine = diffLines[lineIndex - 1];
const nxtLine = diffLines[lineIndex + 1];
const afterNxtLine = diffLines[lineIndex + 2];
if (line.startsWith('diff --git') || line.startsWith('diff --combined')) {
startFile();
// diff --git a/blocked_delta_results.png b/blocked_delta_results.png
const gitDiffStart = /^diff --git "?([a-ciow]\/.+)"? "?([a-ciow]\/.+)"?/;
if ((values = gitDiffStart.exec(line))) {
possibleOldName = getFilename(values[1], undefined, config.dstPrefix);
possibleNewName = getFilename(values[2], undefined, config.srcPrefix);
}
if (currentFile === null) {
throw new Error('Where is my file !!!');
}
currentFile.isGitDiff = true;
return;
}
if (line.startsWith('Binary files') && !currentFile?.isGitDiff) {
startFile();
const unixDiffBinaryStart = /^Binary files "?([a-ciow]\/.+)"? and "?([a-ciow]\/.+)"? differ/;
if ((values = unixDiffBinaryStart.exec(line))) {
possibleOldName = getFilename(values[1], undefined, config.dstPrefix);
possibleNewName = getFilename(values[2], undefined, config.srcPrefix);
}
if (currentFile === null) {
throw new Error('Where is my file !!!');
}
currentFile.isBinary = true;
return;
}
if (
!currentFile || // If we do not have a file yet, we should crete one
(!currentFile.isGitDiff &&
currentFile && // If we already have some file in progress and
line.startsWith(oldFileNameHeader) && // If we get to an old file path header line
// And is followed by the new file path header line and the hunk header line
nxtLine.startsWith(newFileNameHeader) &&
afterNxtLine.startsWith(hunkHeaderPrefix))
) {
startFile();
}
// Ignore remaining diff for current file if marked as too big
if (currentFile?.isTooBig) {
return;
}
if (
currentFile &&
((typeof config.diffMaxChanges === 'number' &&
currentFile.addedLines + currentFile.deletedLines > config.diffMaxChanges) ||
(typeof config.diffMaxLineLength === 'number' && line.length > config.diffMaxLineLength))
) {
currentFile.isTooBig = true;
currentFile.addedLines = 0;
currentFile.deletedLines = 0;
currentFile.blocks = [];
currentBlock = null;
const message =
typeof config.diffTooBigMessage === 'function'
? config.diffTooBigMessage(files.length)
: 'Diff too big to be displayed';
startBlock(message);
return;
}
/*
* We need to make sure that we have the three lines of the header.
* This avoids cases like the ones described in:
* - https://github.com/rtfpessoa/diff2html/issues/87
*/
if (
(line.startsWith(oldFileNameHeader) && nxtLine.startsWith(newFileNameHeader)) ||
(line.startsWith(newFileNameHeader) && prevLine.startsWith(oldFileNameHeader))
) {
/*
* --- Date Timestamp[FractionalSeconds] TimeZone
* --- 2002-02-21 23:30:39.942229878 -0800
*/
if (
currentFile &&
!currentFile.oldName &&
line.startsWith('--- ') &&
(values = getSrcFilename(line, config.srcPrefix))
) {
currentFile.oldName = values;
currentFile.language = getExtension(currentFile.oldName, currentFile.language);
return;
}
/*
* +++ Date Timestamp[FractionalSeconds] TimeZone
* +++ 2002-02-21 23:30:39.942229878 -0800
*/
if (
currentFile &&
!currentFile.newName &&
line.startsWith('+++ ') &&
(values = getDstFilename(line, config.dstPrefix))
) {
currentFile.newName = values;
currentFile.language = getExtension(currentFile.newName, currentFile.language);
return;
}
}
if (
currentFile &&
(line.startsWith(hunkHeaderPrefix) ||
(currentFile.isGitDiff && currentFile.oldName && currentFile.newName && !currentBlock))
) {
startBlock(line);
return;
}
/*
* There are three types of diff lines. These lines are defined by the way they start.
* 1. New line starts with: +
* 2. Old line starts with: -
* 3. Context line starts with: <SPACE>
*/
if (currentBlock && (line.startsWith('+') || line.startsWith('-') || line.startsWith(' '))) {
createLine(line);
return;
}
const doesNotExistHunkHeader = !existHunkHeader(line, lineIndex);
if (currentFile === null) {
throw new Error('Where is my file !!!');
}
/*
* Git diffs provide more information regarding files modes, renames, copies,
* commits between changes and similarity indexes
*/
if ((values = oldMode.exec(line))) {
currentFile.oldMode = values[1];
} else if ((values = newMode.exec(line))) {
currentFile.newMode = values[1];
} else if ((values = deletedFileMode.exec(line))) {
currentFile.deletedFileMode = values[1];
currentFile.isDeleted = true;
} else if ((values = newFileMode.exec(line))) {
currentFile.newFileMode = values[1];
currentFile.isNew = true;
} else if ((values = copyFrom.exec(line))) {
if (doesNotExistHunkHeader) {
currentFile.oldName = values[1];
}
currentFile.isCopy = true;
} else if ((values = copyTo.exec(line))) {
if (doesNotExistHunkHeader) {
currentFile.newName = values[1];
}
currentFile.isCopy = true;
} else if ((values = renameFrom.exec(line))) {
if (doesNotExistHunkHeader) {
currentFile.oldName = values[1];
}
currentFile.isRename = true;
} else if ((values = renameTo.exec(line))) {
if (doesNotExistHunkHeader) {
currentFile.newName = values[1];
}
currentFile.isRename = true;
} else if ((values = binaryFiles.exec(line))) {
currentFile.isBinary = true;
currentFile.oldName = getFilename(values[1], undefined, config.srcPrefix);
currentFile.newName = getFilename(values[2], undefined, config.dstPrefix);
startBlock('Binary file');
} else if (binaryDiff.test(line)) {
currentFile.isBinary = true;
startBlock(line);
} else if ((values = similarityIndex.exec(line))) {
currentFile.unchangedPercentage = parseInt(values[1], 10);
} else if ((values = dissimilarityIndex.exec(line))) {
currentFile.changedPercentage = parseInt(values[1], 10);
} else if ((values = index.exec(line))) {
currentFile.checksumBefore = values[1];
currentFile.checksumAfter = values[2];
if (values[3]) currentFile.mode = values[3];
} else if ((values = combinedIndex.exec(line))) {
currentFile.checksumBefore = [values[2], values[3]];
currentFile.checksumAfter = values[1];
} else if ((values = combinedMode.exec(line))) {
currentFile.oldMode = [values[2], values[3]];
currentFile.newMode = values[1];
} else if ((values = combinedNewFile.exec(line))) {
currentFile.newFileMode = values[1];
currentFile.isNew = true;
} else if ((values = combinedDeletedFile.exec(line))) {
currentFile.deletedFileMode = values[1];
currentFile.isDeleted = true;
}
});
saveBlock();
saveFile();
return files;
}

111
src/diff2html.js Normal file
View file

@ -0,0 +1,111 @@
/*
*
* Diff to HTML (diff2html.js)
* Author: rtfpessoa
*
*/
(function() {
var diffParser = require('./diff-parser.js').DiffParser;
var fileLister = require('./file-list-printer.js').FileListPrinter;
var htmlPrinter = require('./html-printer.js').HtmlPrinter;
function Diff2Html() {
}
/*
* Line diff type configuration
var config = {
'wordByWord': true, // (default)
// OR
'charByChar': true
};
*/
/*
* Generates json object from string diff input
*/
Diff2Html.prototype.getJsonFromDiff = function(diffInput, config) {
var configOrEmpty = config || {};
return diffParser.generateDiffJson(diffInput, configOrEmpty);
};
/*
* Generates the html diff. The config parameter configures the output/input formats and other options
*/
Diff2Html.prototype.getPrettyHtml = function(diffInput, config) {
var configOrEmpty = config || {};
var diffJson = diffInput;
if (!configOrEmpty.inputFormat || configOrEmpty.inputFormat === 'diff') {
diffJson = diffParser.generateDiffJson(diffInput, configOrEmpty);
}
var fileList = '';
if (configOrEmpty.showFiles === true) {
fileList = fileLister.generateFileList(diffJson, configOrEmpty);
}
var diffOutput = '';
if (configOrEmpty.outputFormat === 'side-by-side') {
diffOutput = htmlPrinter.generateSideBySideJsonHtml(diffJson, configOrEmpty);
} else {
diffOutput = htmlPrinter.generateLineByLineJsonHtml(diffJson, configOrEmpty);
}
return fileList + diffOutput;
};
/*
* Deprecated methods - The following methods exist only to maintain compatibility with previous versions
*/
/*
* Generates pretty html from string diff input
*/
Diff2Html.prototype.getPrettyHtmlFromDiff = function(diffInput, config) {
var configOrEmpty = config || {};
configOrEmpty.inputFormat = 'diff';
configOrEmpty.outputFormat = 'line-by-line';
return this.getPrettyHtml(diffInput, configOrEmpty);
};
/*
* Generates pretty html from a json object
*/
Diff2Html.prototype.getPrettyHtmlFromJson = function(diffJson, config) {
var configOrEmpty = config || {};
configOrEmpty.inputFormat = 'json';
configOrEmpty.outputFormat = 'line-by-line';
return this.getPrettyHtml(diffJson, configOrEmpty);
};
/*
* Generates pretty side by side html from string diff input
*/
Diff2Html.prototype.getPrettySideBySideHtmlFromDiff = function(diffInput, config) {
var configOrEmpty = config || {};
configOrEmpty.inputFormat = 'diff';
configOrEmpty.outputFormat = 'side-by-side';
return this.getPrettyHtml(diffInput, configOrEmpty);
};
/*
* Generates pretty side by side html from a json object
*/
Diff2Html.prototype.getPrettySideBySideHtmlFromJson = function(diffJson, config) {
var configOrEmpty = config || {};
configOrEmpty.inputFormat = 'json';
configOrEmpty.outputFormat = 'side-by-side';
return this.getPrettyHtml(diffJson, configOrEmpty);
};
var diffObject = new Diff2Html();
module.exports.Diff2Html = diffObject;
// Expose diff2html in the browser
global.Diff2Html = diffObject;
})();

View file

@ -1,46 +0,0 @@
import * as DiffParser from './diff-parser';
import { FileListRenderer } from './file-list-renderer';
import LineByLineRenderer, { LineByLineRendererConfig, defaultLineByLineRendererConfig } from './line-by-line-renderer';
import SideBySideRenderer, { SideBySideRendererConfig, defaultSideBySideRendererConfig } from './side-by-side-renderer';
import { DiffFile, OutputFormatType } from './types';
import HoganJsUtils, { HoganJsUtilsConfig } from './hoganjs-utils';
export interface Diff2HtmlConfig
extends DiffParser.DiffParserConfig,
LineByLineRendererConfig,
SideBySideRendererConfig,
HoganJsUtilsConfig {
outputFormat?: OutputFormatType;
drawFileList?: boolean;
}
export const defaultDiff2HtmlConfig = {
...defaultLineByLineRendererConfig,
...defaultSideBySideRendererConfig,
outputFormat: OutputFormatType.LINE_BY_LINE,
drawFileList: true,
};
export function parse(diffInput: string, configuration: Diff2HtmlConfig = {}): DiffFile[] {
return DiffParser.parse(diffInput, { ...defaultDiff2HtmlConfig, ...configuration });
}
export function html(diffInput: string | DiffFile[], configuration: Diff2HtmlConfig = {}): string {
const config = { ...defaultDiff2HtmlConfig, ...configuration };
const diffJson = typeof diffInput === 'string' ? DiffParser.parse(diffInput, config) : diffInput;
const hoganUtils = new HoganJsUtils(config);
const { colorScheme } = config;
const fileListRendererConfig = { colorScheme };
const fileList = config.drawFileList ? new FileListRenderer(hoganUtils, fileListRendererConfig).render(diffJson) : '';
const diffOutput =
config.outputFormat === 'side-by-side'
? new SideBySideRenderer(hoganUtils, config).render(diffJson)
: new LineByLineRenderer(hoganUtils, config).render(diffJson);
return fileList + diffOutput;
}

44
src/file-list-printer.js Normal file
View file

@ -0,0 +1,44 @@
/*
*
* FileListPrinter (file-list-printer.js)
* Author: nmatpt
*
*/
(function() {
var printerUtils = require('./printer-utils.js').PrinterUtils;
var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
var baseTemplatesPath = 'file-summary';
var iconsBaseTemplatesPath = 'icon';
function FileListPrinter() {
}
FileListPrinter.prototype.generateFileList = function(diffFiles) {
var lineTemplate = hoganUtils.template(baseTemplatesPath, 'line');
var files = diffFiles.map(function(file) {
var fileTypeName = printerUtils.getFileTypeIcon(file);
var iconTemplate = hoganUtils.template(iconsBaseTemplatesPath, fileTypeName);
return lineTemplate.render({
fileHtmlId: printerUtils.getHtmlId(file),
fileName: printerUtils.getDiffName(file),
deletedLines: '-' + file.deletedLines,
addedLines: '+' + file.addedLines
}, {
fileIcon: iconTemplate
});
}).join('\n');
return hoganUtils.render(baseTemplatesPath, 'wrapper', {
filesNumber: diffFiles.length,
files: files
});
};
module.exports.FileListPrinter = new FileListPrinter();
})();

View file

@ -1,52 +0,0 @@
import * as renderUtils from './render-utils';
import HoganJsUtils from './hoganjs-utils';
import { ColorSchemeType, DiffFile } from './types';
const baseTemplatesPath = 'file-summary';
const iconsBaseTemplatesPath = 'icon';
export interface FileListRendererConfig {
colorScheme?: ColorSchemeType;
}
export const defaultFileListRendererConfig = {
colorScheme: renderUtils.defaultRenderConfig.colorScheme,
};
export class FileListRenderer {
private readonly hoganUtils: HoganJsUtils;
private readonly config: typeof defaultFileListRendererConfig;
constructor(hoganUtils: HoganJsUtils, config: FileListRendererConfig = {}) {
this.hoganUtils = hoganUtils;
this.config = { ...defaultFileListRendererConfig, ...config };
}
render(diffFiles: DiffFile[]): string {
const files = diffFiles
.map(file =>
this.hoganUtils.render(
baseTemplatesPath,
'line',
{
fileHtmlId: renderUtils.getHtmlId(file),
oldName: file.oldName,
newName: file.newName,
fileName: renderUtils.filenameDiff(file),
deletedLines: '-' + file.deletedLines,
addedLines: '+' + file.addedLines,
},
{
fileIcon: this.hoganUtils.template(iconsBaseTemplatesPath, renderUtils.getFileIcon(file)),
},
),
)
.join('\n');
return this.hoganUtils.render(baseTemplatesPath, 'wrapper', {
colorScheme: renderUtils.colorSchemeToCss(this.config.colorScheme),
filesNumber: diffFiles.length,
files: files,
});
}
}

79
src/hoganjs-utils.js Normal file
View file

@ -0,0 +1,79 @@
/*
*
* Utils (hoganjs-utils.js)
* Author: rtfpessoa
*
*/
(function() {
var fs = require('fs');
var path = require('path');
var hogan = require('hogan.js');
var hoganTemplates = require('./templates/diff2html-templates.js');
var templatesPath = path.resolve(__dirname, 'templates');
function HoganJsUtils() {
}
HoganJsUtils.prototype.render = function(namespace, view, params, configuration) {
var template = this.template(namespace, view, configuration);
if (template) {
return template.render(params);
}
return null;
};
HoganJsUtils.prototype.template = function(namespace, view, configuration) {
var config = configuration || {};
var templateKey = this._templateKey(namespace, view);
return this._getTemplate(templateKey, config);
};
HoganJsUtils.prototype._getTemplate = function(templateKey, config) {
var template;
if (!config.noCache) {
template = this._readFromCache(templateKey);
}
if (!template) {
template = this._loadTemplate(templateKey);
}
return template;
};
HoganJsUtils.prototype._loadTemplate = function(templateKey) {
var template;
try {
if (fs.readFileSync) {
var templatePath = path.join(templatesPath, templateKey);
var templateContent = fs.readFileSync(templatePath + '.mustache', 'utf8');
template = hogan.compile(templateContent);
hoganTemplates[templateKey] = template;
}
} catch (e) {
console.error('Failed to read (template: ' + templateKey + ') from fs: ' + e.message);
}
return template;
};
HoganJsUtils.prototype._readFromCache = function(templateKey) {
return hoganTemplates[templateKey];
};
HoganJsUtils.prototype._templateKey = function(namespace, view) {
return namespace + '-' + view;
};
module.exports.HoganJsUtils = new HoganJsUtils();
})();

View file

@ -1,54 +0,0 @@
import * as Hogan from 'hogan.js';
import { defaultTemplates } from './diff2html-templates';
export interface RawTemplates {
[name: string]: string;
}
export interface CompiledTemplates {
[name: string]: Hogan.Template;
}
export interface HoganJsUtilsConfig {
compiledTemplates?: CompiledTemplates;
rawTemplates?: RawTemplates;
}
export default class HoganJsUtils {
private preCompiledTemplates: CompiledTemplates;
constructor({ compiledTemplates = {}, rawTemplates = {} }: HoganJsUtilsConfig) {
const compiledRawTemplates = Object.entries(rawTemplates).reduce<CompiledTemplates>(
(previousTemplates, [name, templateString]) => {
const compiledTemplate: Hogan.Template = Hogan.compile(templateString, { asString: false });
return { ...previousTemplates, [name]: compiledTemplate };
},
{},
);
this.preCompiledTemplates = { ...defaultTemplates, ...compiledTemplates, ...compiledRawTemplates };
}
static compile(templateString: string): Hogan.Template {
return Hogan.compile(templateString, { asString: false });
}
render(namespace: string, view: string, params: Hogan.Context, partials?: Hogan.Partials, indent?: string): string {
const templateKey = this.templateKey(namespace, view);
try {
const template = this.preCompiledTemplates[templateKey];
return template.render(params, partials, indent);
} catch (_e) {
throw new Error(`Could not find template to render '${templateKey}'`);
}
}
template(namespace: string, view: string): Hogan.Template {
return this.preCompiledTemplates[this.templateKey(namespace, view)];
}
private templateKey(namespace: string, view: string): string {
return `${namespace}-${view}`;
}
}

28
src/html-printer.js Normal file
View file

@ -0,0 +1,28 @@
/*
*
* HtmlPrinter (html-printer.js)
* Author: rtfpessoa
*
*/
(function() {
var LineByLinePrinter = require('./line-by-line-printer.js').LineByLinePrinter;
var SideBySidePrinter = require('./side-by-side-printer.js').SideBySidePrinter;
function HtmlPrinter() {
}
HtmlPrinter.prototype.generateLineByLineJsonHtml = function(diffFiles, config) {
var lineByLinePrinter = new LineByLinePrinter(config);
return lineByLinePrinter.generateLineByLineJsonHtml(diffFiles);
};
HtmlPrinter.prototype.generateSideBySideJsonHtml = function(diffFiles, config) {
var sideBySidePrinter = new SideBySidePrinter(config);
return sideBySidePrinter.generateSideBySideJsonHtml(diffFiles);
};
module.exports.HtmlPrinter = new HtmlPrinter();
})();

213
src/line-by-line-printer.js Normal file
View file

@ -0,0 +1,213 @@
/*
*
* LineByLinePrinter (line-by-line-printer.js)
* Author: rtfpessoa
*
*/
(function() {
var diffParser = require('./diff-parser.js').DiffParser;
var printerUtils = require('./printer-utils.js').PrinterUtils;
var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
var genericTemplatesPath = 'generic';
var baseTemplatesPath = 'line-by-line';
var iconsBaseTemplatesPath = 'icon';
var tagsBaseTemplatesPath = 'tag';
function LineByLinePrinter(config) {
this.config = config;
}
LineByLinePrinter.prototype.makeFileDiffHtml = function(file, diffs) {
var fileDiffTemplate = hoganUtils.template(baseTemplatesPath, 'file-diff');
var filePathTemplate = hoganUtils.template(genericTemplatesPath, 'file-path');
var fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, 'file');
var fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file));
return fileDiffTemplate.render({
file: file,
fileHtmlId: printerUtils.getHtmlId(file),
diffs: diffs,
filePath: filePathTemplate.render({
fileDiffName: printerUtils.getDiffName(file)
}, {
fileIcon: fileIconTemplate,
fileTag: fileTagTemplate
})
});
};
LineByLinePrinter.prototype.makeLineByLineHtmlWrapper = function(content) {
return hoganUtils.render(genericTemplatesPath, 'wrapper', {'content': content});
};
LineByLinePrinter.prototype.generateLineByLineJsonHtml = function(diffFiles) {
var that = this;
var htmlDiffs = diffFiles.map(function(file) {
var diffs;
if (file.blocks.length) {
diffs = that._generateFileHtml(file);
} else {
diffs = that._generateEmptyDiff();
}
return that.makeFileDiffHtml(file, diffs);
});
return this.makeLineByLineHtmlWrapper(htmlDiffs.join('\n'));
};
var matcher = Rematch.rematch(function(a, b) {
var amod = a.content.substr(1);
var bmod = b.content.substr(1);
return Rematch.distance(amod, bmod);
});
LineByLinePrinter.prototype.makeColumnLineNumberHtml = function(block) {
return hoganUtils.render(genericTemplatesPath, 'column-line-number', {
diffParser: diffParser,
blockHeader: block.header,
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line'
});
};
LineByLinePrinter.prototype._generateFileHtml = function(file) {
var that = this;
return file.blocks.map(function(block) {
var lines = that.makeColumnLineNumberHtml(block);
var oldLines = [];
var newLines = [];
function processChangeBlock() {
var matches;
var insertType;
var deleteType;
var comparisons = oldLines.length * newLines.length;
var maxComparisons = that.config.matchingMaxComparisons || 2500;
var doMatching = comparisons < maxComparisons && (that.config.matching === 'lines' ||
that.config.matching === 'words');
if (doMatching) {
matches = matcher(oldLines, newLines);
insertType = diffParser.LINE_TYPE.INSERT_CHANGES;
deleteType = diffParser.LINE_TYPE.DELETE_CHANGES;
} else {
matches = [[oldLines, newLines]];
insertType = diffParser.LINE_TYPE.INSERTS;
deleteType = diffParser.LINE_TYPE.DELETES;
}
matches.forEach(function(match) {
oldLines = match[0];
newLines = match[1];
var processedOldLines = [];
var processedNewLines = [];
var common = Math.min(oldLines.length, newLines.length);
var oldLine, newLine;
for (var j = 0; j < common; j++) {
oldLine = oldLines[j];
newLine = newLines[j];
that.config.isCombined = file.isCombined;
var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config);
processedOldLines +=
that.makeLineHtml(deleteType, oldLine.oldNumber, oldLine.newNumber,
diff.first.line, diff.first.prefix);
processedNewLines +=
that.makeLineHtml(insertType, newLine.oldNumber, newLine.newNumber,
diff.second.line, diff.second.prefix);
}
lines += processedOldLines + processedNewLines;
lines += that._processLines(oldLines.slice(common), newLines.slice(common));
});
oldLines = [];
newLines = [];
}
for (var i = 0; i < block.lines.length; i++) {
var line = block.lines[i];
var escapedLine = utils.escape(line.content);
if (line.type !== diffParser.LINE_TYPE.INSERTS &&
(newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0))) {
processChangeBlock();
}
if (line.type === diffParser.LINE_TYPE.CONTEXT) {
lines += that.makeLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) {
lines += that.makeLineHtml(line.type, line.oldNumber, line.newNumber, escapedLine);
} else if (line.type === diffParser.LINE_TYPE.DELETES) {
oldLines.push(line);
} else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) {
newLines.push(line);
} else {
console.error('Unknown state in html line-by-line generator');
processChangeBlock();
}
}
processChangeBlock();
return lines;
}).join('\n');
};
LineByLinePrinter.prototype._processLines = function(oldLines, newLines) {
var lines = '';
for (var i = 0; i < oldLines.length; i++) {
var oldLine = oldLines[i];
var oldEscapedLine = utils.escape(oldLine.content);
lines += this.makeLineHtml(oldLine.type, oldLine.oldNumber, oldLine.newNumber, oldEscapedLine);
}
for (var j = 0; j < newLines.length; j++) {
var newLine = newLines[j];
var newEscapedLine = utils.escape(newLine.content);
lines += this.makeLineHtml(newLine.type, newLine.oldNumber, newLine.newNumber, newEscapedLine);
}
return lines;
};
LineByLinePrinter.prototype.makeLineHtml = function(type, oldNumber, newNumber, content, prefix) {
var lineNumberTemplate = hoganUtils.render(baseTemplatesPath, 'numbers', {
oldNumber: utils.valueOrEmpty(oldNumber),
newNumber: utils.valueOrEmpty(newNumber)
});
return hoganUtils.render(genericTemplatesPath, 'line',
{
type: type,
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
prefix: prefix && utils.convertWhiteSpaceToNonBreakingSpace(prefix),
content: content && utils.convertWhiteSpaceToNonBreakingSpace(content),
lineNumber: lineNumberTemplate
});
};
LineByLinePrinter.prototype._generateEmptyDiff = function() {
return hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-line',
diffParser: diffParser
});
};
module.exports.LineByLinePrinter = LineByLinePrinter;
})();

View file

@ -1,299 +0,0 @@
import HoganJsUtils from './hoganjs-utils';
import * as Rematch from './rematch';
import * as renderUtils from './render-utils';
import {
DiffFile,
DiffLine,
LineType,
DiffBlock,
DiffLineDeleted,
DiffLineContent,
DiffLineContext,
DiffLineInserted,
} from './types';
export interface LineByLineRendererConfig extends renderUtils.RenderConfig {
renderNothingWhenEmpty?: boolean;
matchingMaxComparisons?: number;
maxLineSizeInBlockForComparison?: number;
}
export const defaultLineByLineRendererConfig = {
...renderUtils.defaultRenderConfig,
renderNothingWhenEmpty: false,
matchingMaxComparisons: 2500,
maxLineSizeInBlockForComparison: 200,
};
const genericTemplatesPath = 'generic';
const baseTemplatesPath = 'line-by-line';
const iconsBaseTemplatesPath = 'icon';
const tagsBaseTemplatesPath = 'tag';
export default class LineByLineRenderer {
private readonly hoganUtils: HoganJsUtils;
private readonly config: typeof defaultLineByLineRendererConfig;
constructor(hoganUtils: HoganJsUtils, config: LineByLineRendererConfig = {}) {
this.hoganUtils = hoganUtils;
this.config = { ...defaultLineByLineRendererConfig, ...config };
}
render(diffFiles: DiffFile[]): string {
const diffsHtml = diffFiles
.map(file => {
let diffs;
if (file.blocks.length) {
diffs = this.generateFileHtml(file);
} else {
diffs = this.generateEmptyDiff();
}
return this.makeFileDiffHtml(file, diffs);
})
.join('\n');
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', {
colorScheme: renderUtils.colorSchemeToCss(this.config.colorScheme),
content: diffsHtml,
});
}
makeFileDiffHtml(file: DiffFile, diffs: string): string {
if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return '';
const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'file-diff');
const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, 'file-path');
const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, 'file');
const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file));
return fileDiffTemplate.render({
file: file,
fileHtmlId: renderUtils.getHtmlId(file),
diffs: diffs,
filePath: filePathTemplate.render(
{
fileDiffName: renderUtils.filenameDiff(file),
},
{
fileIcon: fileIconTemplate,
fileTag: fileTagTemplate,
},
),
});
}
generateEmptyDiff(): string {
return this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-line',
CSSLineClass: renderUtils.CSSLineClass,
});
}
generateFileHtml(file: DiffFile): string {
const matcher = Rematch.newMatcherFn(
Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content),
);
return file.blocks
.map(block => {
let lines = this.hoganUtils.render(genericTemplatesPath, 'block-header', {
CSSLineClass: renderUtils.CSSLineClass,
blockHeader: file.isTooBig ? block.header : renderUtils.escapeForHtml(block.header),
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
});
this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
if (oldLines.length && newLines.length && !contextLines.length) {
this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
const { left, right } = this.processChangedLines(file, file.isCombined, oldLines, newLines);
lines += left;
lines += right;
});
} else if (contextLines.length) {
contextLines.forEach(line => {
const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
lines += this.generateSingleLineHtml(file, {
type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
content: content,
oldNumber: line.oldNumber,
newNumber: line.newNumber,
});
});
} else if (oldLines.length || newLines.length) {
const { left, right } = this.processChangedLines(file, file.isCombined, oldLines, newLines);
lines += left;
lines += right;
} else {
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
}
});
return lines;
})
.join('\n');
}
applyLineGroupping(block: DiffBlock): DiffLineGroups {
const blockLinesGroups: DiffLineGroups = [];
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
let newLines: (DiffLineInserted & DiffLineContent)[] = [];
for (let i = 0; i < block.lines.length; i++) {
const diffLine = block.lines[i];
if (
(diffLine.type !== LineType.INSERT && newLines.length) ||
(diffLine.type === LineType.CONTEXT && oldLines.length > 0)
) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = [];
newLines = [];
}
if (diffLine.type === LineType.CONTEXT) {
blockLinesGroups.push([[diffLine], [], []]);
} else if (diffLine.type === LineType.INSERT && oldLines.length === 0) {
blockLinesGroups.push([[], [], [diffLine]]);
} else if (diffLine.type === LineType.INSERT && oldLines.length > 0) {
newLines.push(diffLine);
} else if (diffLine.type === LineType.DELETE) {
oldLines.push(diffLine);
}
}
if (oldLines.length || newLines.length) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = [];
newLines = [];
}
return blockLinesGroups;
}
applyRematchMatching(
oldLines: DiffLine[],
newLines: DiffLine[],
matcher: Rematch.MatcherFn<DiffLine>,
): DiffLine[][][] {
const comparisons = oldLines.length * newLines.length;
const maxLineSizeInBlock = Math.max.apply(
null,
[0].concat(oldLines.concat(newLines).map(elem => elem.content.length)),
);
const doMatching =
comparisons < this.config.matchingMaxComparisons &&
maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison &&
(this.config.matching === 'lines' || this.config.matching === 'words');
return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
}
processChangedLines(file: DiffFile, isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
const fileHtml = {
right: '',
left: '',
};
const maxLinesNumber = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLinesNumber; i++) {
const oldLine = oldLines[i];
const newLine = newLines[i];
const diff =
oldLine !== undefined && newLine !== undefined
? renderUtils.diffHighlight(oldLine.content, newLine.content, isCombined, this.config)
: undefined;
const preparedOldLine =
oldLine !== undefined && oldLine.oldNumber !== undefined
? {
...(diff !== undefined
? {
prefix: diff.oldLine.prefix,
content: diff.oldLine.content,
type: renderUtils.CSSLineClass.DELETE_CHANGES,
}
: {
...renderUtils.deconstructLine(oldLine.content, isCombined),
type: renderUtils.toCSSClass(oldLine.type),
}),
oldNumber: oldLine.oldNumber,
newNumber: oldLine.newNumber,
}
: undefined;
const preparedNewLine =
newLine !== undefined && newLine.newNumber !== undefined
? {
...(diff !== undefined
? {
prefix: diff.newLine.prefix,
content: diff.newLine.content,
type: renderUtils.CSSLineClass.INSERT_CHANGES,
}
: {
...renderUtils.deconstructLine(newLine.content, isCombined),
type: renderUtils.toCSSClass(newLine.type),
}),
oldNumber: newLine.oldNumber,
newNumber: newLine.newNumber,
}
: undefined;
const { left, right } = this.generateLineHtml(file, preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
}
return fileHtml;
}
generateLineHtml(file: DiffFile, oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
return {
left: this.generateSingleLineHtml(file, oldLine),
right: this.generateSingleLineHtml(file, newLine),
};
}
generateSingleLineHtml(file: DiffFile, line?: DiffPreparedLine): string {
if (line === undefined) return '';
const lineNumberHtml = this.hoganUtils.render(baseTemplatesPath, 'numbers', {
oldNumber: line.oldNumber || '',
newNumber: line.newNumber || '',
});
return this.hoganUtils.render(genericTemplatesPath, 'line', {
type: line.type,
lineClass: 'd2h-code-linenumber',
contentClass: 'd2h-code-line',
prefix: line.prefix === ' ' ? '&nbsp;' : line.prefix,
content: line.content,
lineNumber: lineNumberHtml,
line,
file,
});
}
}
type DiffLineGroups = [
(DiffLineContext & DiffLineContent)[],
(DiffLineDeleted & DiffLineContent)[],
(DiffLineInserted & DiffLineContent)[],
][];
type DiffPreparedLine = {
type: renderUtils.CSSLineClass;
prefix: string;
content: string;
oldNumber?: number;
newNumber?: number;
};
type FileHtml = {
left: string;
right: string;
};

221
src/printer-utils.js Normal file
View file

@ -0,0 +1,221 @@
/*
*
* PrinterUtils (printer-utils.js)
* Author: rtfpessoa
*
*/
(function() {
var jsDiff = require('diff');
var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
var separator = '/';
function PrinterUtils() {
}
PrinterUtils.prototype.getHtmlId = function(file) {
var hashCode = function(text) {
var i, chr, len;
var hash = 0;
for (i = 0, len = text.length; i < len; i++) {
chr = text.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
return 'd2h-' + hashCode(this.getDiffName(file)).toString().slice(-6);
};
PrinterUtils.prototype.getDiffName = function(file) {
var oldFilename = unifyPath(file.oldName);
var newFilename = unifyPath(file.newName);
if (oldFilename && newFilename && oldFilename !== newFilename && !isDevNullName(oldFilename) && !isDevNullName(newFilename)) {
var prefixPaths = [];
var suffixPaths = [];
var oldFilenameParts = oldFilename.split(separator);
var newFilenameParts = newFilename.split(separator);
var oldFilenamePartsSize = oldFilenameParts.length;
var newFilenamePartsSize = newFilenameParts.length;
var i = 0;
var j = oldFilenamePartsSize - 1;
var k = newFilenamePartsSize - 1;
while (i < j && i < k) {
if (oldFilenameParts[i] === newFilenameParts[i]) {
prefixPaths.push(newFilenameParts[i]);
i += 1;
} else {
break;
}
}
while (j > i && k > i) {
if (oldFilenameParts[j] === newFilenameParts[k]) {
suffixPaths.unshift(newFilenameParts[k]);
j -= 1;
k -= 1;
} else {
break;
}
}
var finalPrefix = prefixPaths.join(separator);
var finalSuffix = suffixPaths.join(separator);
var oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator);
var newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator);
if (finalPrefix.length && finalSuffix.length) {
return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix;
} else if (finalPrefix.length) {
return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}';
} else if (finalSuffix.length) {
return '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix;
}
return oldFilename + ' → ' + newFilename;
} else if (newFilename && !isDevNullName(newFilename)) {
return newFilename;
} else if (oldFilename) {
return oldFilename;
}
return 'unknown/file/path';
};
PrinterUtils.prototype.getFileTypeIcon = function(file) {
var templateName = 'file-changed';
if (file.isRename) {
templateName = 'file-renamed';
} else if (file.isCopy) {
templateName = 'file-renamed';
} else if (file.isNew) {
templateName = 'file-added';
} else if (file.isDeleted) {
templateName = 'file-deleted';
} else if (file.newName !== file.oldName) {
// If file is not Added, not Deleted and the names changed it must be a rename :)
templateName = 'file-renamed';
}
return templateName;
};
PrinterUtils.prototype.diffHighlight = function(diffLine1, diffLine2, config) {
var linePrefix1, linePrefix2, unprefixedLine1, unprefixedLine2;
var prefixSize = 1;
if (config.isCombined) {
prefixSize = 2;
}
linePrefix1 = diffLine1.substr(0, prefixSize);
linePrefix2 = diffLine2.substr(0, prefixSize);
unprefixedLine1 = diffLine1.substr(prefixSize);
unprefixedLine2 = diffLine2.substr(prefixSize);
var diff;
if (config.charByChar) {
diff = jsDiff.diffChars(unprefixedLine1, unprefixedLine2);
} else {
diff = jsDiff.diffWordsWithSpace(unprefixedLine1, unprefixedLine2);
}
var highlightedLine = '';
var changedWords = [];
if (!config.charByChar && config.matching === 'words') {
var treshold = 0.25;
if (typeof (config.matchWordsThreshold) !== 'undefined') {
treshold = config.matchWordsThreshold;
}
var matcher = Rematch.rematch(function(a, b) {
var amod = a.value;
var bmod = b.value;
return Rematch.distance(amod, bmod);
});
var removed = diff.filter(function isRemoved(element) {
return element.removed;
});
var added = diff.filter(function isAdded(element) {
return element.added;
});
var chunks = matcher(added, removed);
chunks.forEach(function(chunk) {
if (chunk[0].length === 1 && chunk[1].length === 1) {
var dist = Rematch.distance(chunk[0][0].value, chunk[1][0].value);
if (dist < treshold) {
changedWords.push(chunk[0][0]);
changedWords.push(chunk[1][0]);
}
}
});
}
diff.forEach(function(part) {
var addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : '';
var elemType = part.added ? 'ins' : part.removed ? 'del' : null;
var escapedValue = utils.escape(part.value);
if (elemType !== null) {
highlightedLine += '<' + elemType + addClass + '>' + escapedValue + '</' + elemType + '>';
} else {
highlightedLine += escapedValue;
}
});
return {
first: {
prefix: linePrefix1,
line: removeIns(highlightedLine)
},
second: {
prefix: linePrefix2,
line: removeDel(highlightedLine)
}
};
};
function unifyPath(path) {
if (path) {
return path.replace('\\', '/');
}
return path;
}
function isDevNullName(name) {
return name.indexOf('dev/null') !== -1;
}
function removeIns(line) {
return line.replace(/(<ins[^>]*>((.|\n)*?)<\/ins>)/g, '');
}
function removeDel(line) {
return line.replace(/(<del[^>]*>((.|\n)*?)<\/del>)/g, '');
}
module.exports.PrinterUtils = new PrinterUtils();
})();

149
src/rematch.js Normal file
View file

@ -0,0 +1,149 @@
/*
*
* Rematch (rematch.js)
* Matching two sequences of objects by similarity
* Author: W. Illmeyer, Nexxar GmbH
*
*/
(function() {
var Rematch = {};
/*
Copyright (c) 2011 Andrei Mackenzie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
function levenshtein(a, b) {
if (a.length == 0) {
return b.length;
}
if (b.length == 0) {
return a.length;
}
var matrix = [];
// Increment along the first column of each row
var i;
for (i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
// Increment each column in the first row
var j;
for (j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for (i = 1; i <= b.length; i++) {
for (j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) == a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution
Math.min(matrix[i][j - 1] + 1, // Insertion
matrix[i - 1][j] + 1)); // Deletion
}
}
}
return matrix[b.length][a.length];
}
Rematch.levenshtein = levenshtein;
Rematch.distance = function distance(x, y) {
x = x.trim();
y = y.trim();
var lev = levenshtein(x, y);
var score = lev / (x.length + y.length);
return score;
};
Rematch.rematch = function rematch(distanceFunction) {
function findBestMatch(a, b, cache) {
var cachecount = 0;
for (var key in cache) {
cachecount++;
}
var bestMatchDist = Infinity;
var bestMatch;
for (var i = 0; i < a.length; ++i) {
for (var j = 0; j < b.length; ++j) {
var cacheKey = JSON.stringify([a[i], b[j]]);
var md;
if (cache.hasOwnProperty(cacheKey)) {
md = cache[cacheKey];
} else {
md = distanceFunction(a[i], b[j]);
cache[cacheKey] = md;
}
if (md < bestMatchDist) {
bestMatchDist = md;
bestMatch = {indexA: i, indexB: j, score: bestMatchDist};
}
}
}
return bestMatch;
}
function group(a, b, level, cache) {
if (typeof (cache) === "undefined") {
cache = {};
}
var bm = findBestMatch(a, b, cache);
if (!level) {
level = 0;
}
if (!bm || (a.length + b.length < 3)) {
return [[a, b]];
}
var a1 = a.slice(0, bm.indexA);
var b1 = b.slice(0, bm.indexB);
var aMatch = [a[bm.indexA]];
var bMatch = [b[bm.indexB]];
var tailA = bm.indexA + 1;
var tailB = bm.indexB + 1;
var a2 = a.slice(tailA);
var b2 = b.slice(tailB);
var group1 = group(a1, b1, level + 1, cache);
var groupMatch = group(aMatch, bMatch, level + 1, cache);
var group2 = group(a2, b2, level + 1, cache);
var result = groupMatch;
if (bm.indexA > 0 || bm.indexB > 0) {
result = group1.concat(result);
}
if (a.length > tailA || b.length > tailB) {
result = result.concat(group2);
}
return result;
}
return group;
};
module.exports.Rematch = Rematch;
})();

View file

@ -1,135 +0,0 @@
/*
* Matching two sequences of objects by similarity
* Author: W. Illmeyer, Nexxar GmbH
*/
export type BestMatch = {
indexA: number;
indexB: number;
score: number;
};
/*
Copyright (c) 2011 Andrei Mackenzie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export function levenshtein(a: string, b: string): number {
if (a.length === 0) {
return b.length;
}
if (b.length === 0) {
return a.length;
}
const matrix = [];
// Increment along the first column of each row
let i;
for (i = 0; i <= b.length; i++) {
matrix[i] = [i];
}
// Increment each column in the first row
let j;
for (j = 0; j <= a.length; j++) {
matrix[0][j] = j;
}
// Fill in the rest of the matrix
for (i = 1; i <= b.length; i++) {
for (j = 1; j <= a.length; j++) {
if (b.charAt(i - 1) === a.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1, // Substitution
Math.min(
matrix[i][j - 1] + 1, // Insertion
matrix[i - 1][j] + 1,
),
); // Deletion
}
}
}
return matrix[b.length][a.length];
}
export type DistanceFn<T> = (x: T, y: T) => number;
export function newDistanceFn<T>(str: (value: T) => string): DistanceFn<T> {
return (x: T, y: T): number => {
const xValue = str(x).trim();
const yValue = str(y).trim();
const lev = levenshtein(xValue, yValue);
return lev / (xValue.length + yValue.length);
};
}
export type MatcherFn<T> = (a: T[], b: T[], level?: number, cache?: Map<string, number>) => T[][][];
export function newMatcherFn<T>(distance: (x: T, y: T) => number): MatcherFn<T> {
function findBestMatch(a: T[], b: T[], cache: Map<string, number> = new Map()): BestMatch | undefined {
let bestMatchDist = Infinity;
let bestMatch;
for (let i = 0; i < a.length; ++i) {
for (let j = 0; j < b.length; ++j) {
const cacheKey = JSON.stringify([a[i], b[j]]);
let md;
if (!(cache.has(cacheKey) && (md = cache.get(cacheKey)))) {
md = distance(a[i], b[j]);
cache.set(cacheKey, md);
}
if (md < bestMatchDist) {
bestMatchDist = md;
bestMatch = { indexA: i, indexB: j, score: bestMatchDist };
}
}
}
return bestMatch;
}
function group(a: T[], b: T[], level = 0, cache: Map<string, number> = new Map()): T[][][] {
const bm = findBestMatch(a, b, cache);
if (!bm || a.length + b.length < 3) {
return [[a, b]];
}
const a1 = a.slice(0, bm.indexA);
const b1 = b.slice(0, bm.indexB);
const aMatch = [a[bm.indexA]];
const bMatch = [b[bm.indexB]];
const tailA = bm.indexA + 1;
const tailB = bm.indexB + 1;
const a2 = a.slice(tailA);
const b2 = b.slice(tailB);
const group1 = group(a1, b1, level + 1, cache);
const groupMatch = group(aMatch, bMatch, level + 1, cache);
const group2 = group(a2, b2, level + 1, cache);
let result = groupMatch;
if (bm.indexA > 0 || bm.indexB > 0) {
result = group1.concat(result);
}
if (a.length > tailA || b.length > tailB) {
result = result.concat(group2);
}
return result;
}
return group;
}

View file

@ -1,304 +0,0 @@
import * as jsDiff from 'diff';
import { unifyPath, hashCode } from './utils';
import * as rematch from './rematch';
import {
ColorSchemeType,
DiffFile,
DiffFileName,
DiffLineParts,
DiffStyleType,
LineMatchingType,
LineType,
} from './types';
export type CSSLineClass =
| 'd2h-ins'
| 'd2h-del'
| 'd2h-cntx'
| 'd2h-info'
| 'd2h-ins d2h-change'
| 'd2h-del d2h-change';
export const CSSLineClass: { [_: string]: CSSLineClass } = {
INSERTS: 'd2h-ins',
DELETES: 'd2h-del',
CONTEXT: 'd2h-cntx',
INFO: 'd2h-info',
INSERT_CHANGES: 'd2h-ins d2h-change',
DELETE_CHANGES: 'd2h-del d2h-change',
};
export type HighlightedLines = {
oldLine: {
prefix: string;
content: string;
};
newLine: {
prefix: string;
content: string;
};
};
export interface RenderConfig {
matching?: LineMatchingType;
matchWordsThreshold?: number;
maxLineLengthHighlight?: number;
diffStyle?: DiffStyleType;
colorScheme?: ColorSchemeType;
}
export const defaultRenderConfig = {
matching: LineMatchingType.NONE,
matchWordsThreshold: 0.25,
maxLineLengthHighlight: 10000,
diffStyle: DiffStyleType.WORD,
colorScheme: ColorSchemeType.LIGHT,
};
const separator = '/';
const distance = rematch.newDistanceFn((change: jsDiff.Change) => change.value);
const matcher = rematch.newMatcherFn(distance);
function isDevNullName(name: string): boolean {
return name.indexOf('dev/null') !== -1;
}
function removeInsElements(line: string): string {
return line.replace(/(<ins[^>]*>((.|\n)*?)<\/ins>)/g, '');
}
function removeDelElements(line: string): string {
return line.replace(/(<del[^>]*>((.|\n)*?)<\/del>)/g, '');
}
/**
* Convert from LineType to CSSLineClass
*/
export function toCSSClass(lineType: LineType): CSSLineClass {
switch (lineType) {
case LineType.CONTEXT:
return CSSLineClass.CONTEXT;
case LineType.INSERT:
return CSSLineClass.INSERTS;
case LineType.DELETE:
return CSSLineClass.DELETES;
}
}
export function colorSchemeToCss(colorScheme: ColorSchemeType): string {
switch (colorScheme) {
case ColorSchemeType.DARK:
return 'd2h-dark-color-scheme';
case ColorSchemeType.AUTO:
return 'd2h-auto-color-scheme';
case ColorSchemeType.LIGHT:
default:
return 'd2h-light-color-scheme';
}
}
/**
* Prefix length of the hunk lines in the diff
*/
function prefixLength(isCombined: boolean): number {
return isCombined ? 2 : 1;
}
/**
* Escapes all required characters for safe HTML rendering
*/
// TODO: Test this method inside deconstructLine since it should not be used anywhere else
export function escapeForHtml(str: string): string {
return str
.slice(0)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/\//g, '&#x2F;');
}
/**
* Deconstructs diff @line by separating the content from the prefix type
*/
export function deconstructLine(line: string, isCombined: boolean, escape = true): DiffLineParts {
const indexToSplit = prefixLength(isCombined);
return {
prefix: line.substring(0, indexToSplit),
content: escape ? escapeForHtml(line.substring(indexToSplit)) : line.substring(indexToSplit),
};
}
/**
* Generates pretty filename diffs
*
* e.g.:
* 1. file = { oldName: "my/path/to/file.js", newName: "my/path/to/new-file.js" }
* returns "my/path/to/{file.js → new-file.js}"
* 2. file = { oldName: "my/path/to/file.js", newName: "very/new/path/to/new-file.js" }
* returns "my/path/to/file.js → very/new/path/to/new-file.js"
* 3. file = { oldName: "my/path/to/file.js", newName: "my/path/for/file.js" }
* returns "my/path/{to → for}/file.js"
*/
export function filenameDiff(file: DiffFileName): string {
// TODO: Move unify path to parsing
const oldFilename = unifyPath(file.oldName);
const newFilename = unifyPath(file.newName);
if (oldFilename !== newFilename && !isDevNullName(oldFilename) && !isDevNullName(newFilename)) {
const prefixPaths = [];
const suffixPaths = [];
const oldFilenameParts = oldFilename.split(separator);
const newFilenameParts = newFilename.split(separator);
const oldFilenamePartsSize = oldFilenameParts.length;
const newFilenamePartsSize = newFilenameParts.length;
let i = 0;
let j = oldFilenamePartsSize - 1;
let k = newFilenamePartsSize - 1;
while (i < j && i < k) {
if (oldFilenameParts[i] === newFilenameParts[i]) {
prefixPaths.push(newFilenameParts[i]);
i += 1;
} else {
break;
}
}
while (j > i && k > i) {
if (oldFilenameParts[j] === newFilenameParts[k]) {
suffixPaths.unshift(newFilenameParts[k]);
j -= 1;
k -= 1;
} else {
break;
}
}
const finalPrefix = prefixPaths.join(separator);
const finalSuffix = suffixPaths.join(separator);
const oldRemainingPath = oldFilenameParts.slice(i, j + 1).join(separator);
const newRemainingPath = newFilenameParts.slice(i, k + 1).join(separator);
if (finalPrefix.length && finalSuffix.length) {
return (
finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix
);
} else if (finalPrefix.length) {
return finalPrefix + separator + '{' + oldRemainingPath + ' → ' + newRemainingPath + '}';
} else if (finalSuffix.length) {
return '{' + oldRemainingPath + ' → ' + newRemainingPath + '}' + separator + finalSuffix;
}
return oldFilename + ' → ' + newFilename;
} else if (!isDevNullName(newFilename)) {
return newFilename;
} else {
return oldFilename;
}
}
/**
* Generates a unique string numerical identifier based on the names of the file diff
*/
export function getHtmlId(file: DiffFileName): string {
return `d2h-${hashCode(filenameDiff(file)).toString().slice(-6)}`;
}
/**
* Selects the correct icon name for the file
*/
export function getFileIcon(file: DiffFile): string {
let templateName = 'file-changed';
if (file.isRename) {
templateName = 'file-renamed';
} else if (file.isCopy) {
templateName = 'file-renamed';
} else if (file.isNew) {
templateName = 'file-added';
} else if (file.isDeleted) {
templateName = 'file-deleted';
} else if (file.newName !== file.oldName) {
// If file is not Added, not Deleted and the names changed it must be a rename :)
templateName = 'file-renamed';
}
return templateName;
}
/**
* Highlight differences between @diffLine1 and @diffLine2 using <ins> and <del> tags
*/
export function diffHighlight(
diffLine1: string,
diffLine2: string,
isCombined: boolean,
config: RenderConfig = {},
): HighlightedLines {
const { matching, maxLineLengthHighlight, matchWordsThreshold, diffStyle } = { ...defaultRenderConfig, ...config };
const line1 = deconstructLine(diffLine1, isCombined, false);
const line2 = deconstructLine(diffLine2, isCombined, false);
if (line1.content.length > maxLineLengthHighlight || line2.content.length > maxLineLengthHighlight) {
return {
oldLine: {
prefix: line1.prefix,
content: escapeForHtml(line1.content),
},
newLine: {
prefix: line2.prefix,
content: escapeForHtml(line2.content),
},
};
}
const diff =
diffStyle === 'char'
? jsDiff.diffChars(line1.content, line2.content)
: jsDiff.diffWordsWithSpace(line1.content, line2.content);
const changedWords: jsDiff.Change[] = [];
if (diffStyle === 'word' && matching === 'words') {
const removed = diff.filter(element => element.removed);
const added = diff.filter(element => element.added);
const chunks = matcher(added, removed);
chunks.forEach(chunk => {
if (chunk[0].length === 1 && chunk[1].length === 1) {
const dist = distance(chunk[0][0], chunk[1][0]);
if (dist < matchWordsThreshold) {
changedWords.push(chunk[0][0]);
changedWords.push(chunk[1][0]);
}
}
});
}
const highlightedLine = diff.reduce((highlightedLine, part) => {
const elemType = part.added ? 'ins' : part.removed ? 'del' : null;
const addClass = changedWords.indexOf(part) > -1 ? ' class="d2h-change"' : '';
const escapedValue = escapeForHtml(part.value);
return elemType !== null
? `${highlightedLine}<${elemType}${addClass}>${escapedValue}</${elemType}>`
: `${highlightedLine}${escapedValue}`;
}, '');
return {
oldLine: {
prefix: line1.prefix,
content: removeInsElements(highlightedLine),
},
newLine: {
prefix: line2.prefix,
content: removeDelElements(highlightedLine),
},
};
}

248
src/side-by-side-printer.js Normal file
View file

@ -0,0 +1,248 @@
/*
*
* HtmlPrinter (html-printer.js)
* Author: rtfpessoa
*
*/
(function() {
var diffParser = require('./diff-parser.js').DiffParser;
var printerUtils = require('./printer-utils.js').PrinterUtils;
var utils = require('./utils.js').Utils;
var Rematch = require('./rematch.js').Rematch;
var hoganUtils = require('./hoganjs-utils.js').HoganJsUtils;
var genericTemplatesPath = 'generic';
var baseTemplatesPath = 'side-by-side';
var iconsBaseTemplatesPath = 'icon';
var tagsBaseTemplatesPath = 'tag';
var matcher = Rematch.rematch(function(a, b) {
var amod = a.content.substr(1);
var bmod = b.content.substr(1);
return Rematch.distance(amod, bmod);
});
function SideBySidePrinter(config) {
this.config = config;
}
SideBySidePrinter.prototype.makeDiffHtml = function(file, diffs) {
var fileDiffTemplate = hoganUtils.template(baseTemplatesPath, 'file-diff');
var filePathTemplate = hoganUtils.template(genericTemplatesPath, 'file-path');
var fileIconTemplate = hoganUtils.template(iconsBaseTemplatesPath, 'file');
var fileTagTemplate = hoganUtils.template(tagsBaseTemplatesPath, printerUtils.getFileTypeIcon(file));
return fileDiffTemplate.render({
file: file,
fileHtmlId: printerUtils.getHtmlId(file),
diffs: diffs,
filePath: filePathTemplate.render({
fileDiffName: printerUtils.getDiffName(file)
}, {
fileIcon: fileIconTemplate,
fileTag: fileTagTemplate
})
});
};
SideBySidePrinter.prototype.generateSideBySideJsonHtml = function(diffFiles) {
var that = this;
var content = diffFiles.map(function(file) {
var diffs;
if (file.blocks.length) {
diffs = that.generateSideBySideFileHtml(file);
} else {
diffs = that.generateEmptyDiff();
}
return that.makeDiffHtml(file, diffs);
}).join('\n');
return hoganUtils.render(genericTemplatesPath, 'wrapper', {'content': content});
};
SideBySidePrinter.prototype.makeSideHtml = function(blockHeader) {
return hoganUtils.render(genericTemplatesPath, 'column-line-number', {
diffParser: diffParser,
blockHeader: blockHeader,
lineClass: 'd2h-code-side-linenumber',
contentClass: 'd2h-code-side-line'
});
};
SideBySidePrinter.prototype.generateSideBySideFileHtml = function(file) {
var that = this;
var fileHtml = {};
fileHtml.left = '';
fileHtml.right = '';
file.blocks.forEach(function(block) {
fileHtml.left += that.makeSideHtml(block.header);
fileHtml.right += that.makeSideHtml('');
var oldLines = [];
var newLines = [];
function processChangeBlock() {
var matches;
var insertType;
var deleteType;
var comparisons = oldLines.length * newLines.length;
var maxComparisons = that.config.matchingMaxComparisons || 2500;
var doMatching = comparisons < maxComparisons && (that.config.matching === 'lines' ||
that.config.matching === 'words');
if (doMatching) {
matches = matcher(oldLines, newLines);
insertType = diffParser.LINE_TYPE.INSERT_CHANGES;
deleteType = diffParser.LINE_TYPE.DELETE_CHANGES;
} else {
matches = [[oldLines, newLines]];
insertType = diffParser.LINE_TYPE.INSERTS;
deleteType = diffParser.LINE_TYPE.DELETES;
}
matches.forEach(function(match) {
oldLines = match[0];
newLines = match[1];
var common = Math.min(oldLines.length, newLines.length);
var max = Math.max(oldLines.length, newLines.length);
for (var j = 0; j < common; j++) {
var oldLine = oldLines[j];
var newLine = newLines[j];
that.config.isCombined = file.isCombined;
var diff = printerUtils.diffHighlight(oldLine.content, newLine.content, that.config);
fileHtml.left +=
that.generateSingleLineHtml(deleteType, oldLine.oldNumber,
diff.first.line, diff.first.prefix);
fileHtml.right +=
that.generateSingleLineHtml(insertType, newLine.newNumber,
diff.second.line, diff.second.prefix);
}
if (max > common) {
var oldSlice = oldLines.slice(common);
var newSlice = newLines.slice(common);
var tmpHtml = that.processLines(oldSlice, newSlice);
fileHtml.left += tmpHtml.left;
fileHtml.right += tmpHtml.right;
}
});
oldLines = [];
newLines = [];
}
for (var i = 0; i < block.lines.length; i++) {
var line = block.lines[i];
var prefix = line.content[0];
var escapedLine = utils.escape(line.content.substr(1));
if (line.type !== diffParser.LINE_TYPE.INSERTS &&
(newLines.length > 0 || (line.type !== diffParser.LINE_TYPE.DELETES && oldLines.length > 0))) {
processChangeBlock();
}
if (line.type === diffParser.LINE_TYPE.CONTEXT) {
fileHtml.left += that.generateSingleLineHtml(line.type, line.oldNumber, escapedLine, prefix);
fileHtml.right += that.generateSingleLineHtml(line.type, line.newNumber, escapedLine, prefix);
} else if (line.type === diffParser.LINE_TYPE.INSERTS && !oldLines.length) {
fileHtml.left += that.generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
fileHtml.right += that.generateSingleLineHtml(line.type, line.newNumber, escapedLine, prefix);
} else if (line.type === diffParser.LINE_TYPE.DELETES) {
oldLines.push(line);
} else if (line.type === diffParser.LINE_TYPE.INSERTS && Boolean(oldLines.length)) {
newLines.push(line);
} else {
console.error('unknown state in html side-by-side generator');
processChangeBlock();
}
}
processChangeBlock();
});
return fileHtml;
};
SideBySidePrinter.prototype.processLines = function(oldLines, newLines) {
var that = this;
var fileHtml = {};
fileHtml.left = '';
fileHtml.right = '';
var maxLinesNumber = Math.max(oldLines.length, newLines.length);
for (var i = 0; i < maxLinesNumber; i++) {
var oldLine = oldLines[i];
var newLine = newLines[i];
var oldContent;
var newContent;
var oldPrefix;
var newPrefix;
if (oldLine) {
oldContent = utils.escape(oldLine.content.substr(1));
oldPrefix = oldLine.content[0];
}
if (newLine) {
newContent = utils.escape(newLine.content.substr(1));
newPrefix = newLine.content[0];
}
if (oldLine && newLine) {
fileHtml.left += that.generateSingleLineHtml(oldLine.type, oldLine.oldNumber, oldContent, oldPrefix);
fileHtml.right += that.generateSingleLineHtml(newLine.type, newLine.newNumber, newContent, newPrefix);
} else if (oldLine) {
fileHtml.left += that.generateSingleLineHtml(oldLine.type, oldLine.oldNumber, oldContent, oldPrefix);
fileHtml.right += that.generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
} else if (newLine) {
fileHtml.left += that.generateSingleLineHtml(diffParser.LINE_TYPE.CONTEXT, '', '', '');
fileHtml.right += that.generateSingleLineHtml(newLine.type, newLine.newNumber, newContent, newPrefix);
} else {
console.error('How did it get here?');
}
}
return fileHtml;
};
SideBySidePrinter.prototype.generateSingleLineHtml = function(type, number, content, prefix) {
return hoganUtils.render(genericTemplatesPath, 'line',
{
type: type,
lineClass: 'd2h-code-side-linenumber',
contentClass: 'd2h-code-side-line',
prefix: prefix && utils.convertWhiteSpaceToNonBreakingSpace(prefix),
content: content && utils.convertWhiteSpaceToNonBreakingSpace(content),
lineNumber: number
});
};
SideBySidePrinter.prototype.generateEmptyDiff = function() {
var fileHtml = {};
fileHtml.right = '';
fileHtml.left = hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-side-line',
diffParser: diffParser
});
return fileHtml;
};
module.exports.SideBySidePrinter = SideBySidePrinter;
})();

View file

@ -1,314 +0,0 @@
import HoganJsUtils from './hoganjs-utils';
import * as Rematch from './rematch';
import * as renderUtils from './render-utils';
import {
DiffLine,
LineType,
DiffFile,
DiffBlock,
DiffLineContext,
DiffLineDeleted,
DiffLineInserted,
DiffLineContent,
} from './types';
export interface SideBySideRendererConfig extends renderUtils.RenderConfig {
renderNothingWhenEmpty?: boolean;
matchingMaxComparisons?: number;
maxLineSizeInBlockForComparison?: number;
}
export const defaultSideBySideRendererConfig = {
...renderUtils.defaultRenderConfig,
renderNothingWhenEmpty: false,
matchingMaxComparisons: 2500,
maxLineSizeInBlockForComparison: 200,
};
const genericTemplatesPath = 'generic';
const baseTemplatesPath = 'side-by-side';
const iconsBaseTemplatesPath = 'icon';
const tagsBaseTemplatesPath = 'tag';
export default class SideBySideRenderer {
private readonly hoganUtils: HoganJsUtils;
private readonly config: typeof defaultSideBySideRendererConfig;
constructor(hoganUtils: HoganJsUtils, config: SideBySideRendererConfig = {}) {
this.hoganUtils = hoganUtils;
this.config = { ...defaultSideBySideRendererConfig, ...config };
}
render(diffFiles: DiffFile[]): string {
const diffsHtml = diffFiles
.map(file => {
let diffs;
if (file.blocks.length) {
diffs = this.generateFileHtml(file);
} else {
diffs = this.generateEmptyDiff();
}
return this.makeFileDiffHtml(file, diffs);
})
.join('\n');
return this.hoganUtils.render(genericTemplatesPath, 'wrapper', {
colorScheme: renderUtils.colorSchemeToCss(this.config.colorScheme),
content: diffsHtml,
});
}
makeFileDiffHtml(file: DiffFile, diffs: FileHtml): string {
if (this.config.renderNothingWhenEmpty && Array.isArray(file.blocks) && file.blocks.length === 0) return '';
const fileDiffTemplate = this.hoganUtils.template(baseTemplatesPath, 'file-diff');
const filePathTemplate = this.hoganUtils.template(genericTemplatesPath, 'file-path');
const fileIconTemplate = this.hoganUtils.template(iconsBaseTemplatesPath, 'file');
const fileTagTemplate = this.hoganUtils.template(tagsBaseTemplatesPath, renderUtils.getFileIcon(file));
return fileDiffTemplate.render({
file: file,
fileHtmlId: renderUtils.getHtmlId(file),
diffs: diffs,
filePath: filePathTemplate.render(
{
fileDiffName: renderUtils.filenameDiff(file),
},
{
fileIcon: fileIconTemplate,
fileTag: fileTagTemplate,
},
),
});
}
generateEmptyDiff(): FileHtml {
return {
right: '',
left: this.hoganUtils.render(genericTemplatesPath, 'empty-diff', {
contentClass: 'd2h-code-side-line',
CSSLineClass: renderUtils.CSSLineClass,
}),
};
}
generateFileHtml(file: DiffFile): FileHtml {
const matcher = Rematch.newMatcherFn(
Rematch.newDistanceFn((e: DiffLine) => renderUtils.deconstructLine(e.content, file.isCombined).content),
);
return file.blocks
.map(block => {
const fileHtml = {
left: this.makeHeaderHtml(block.header, file),
right: this.makeHeaderHtml(''),
};
this.applyLineGroupping(block).forEach(([contextLines, oldLines, newLines]) => {
if (oldLines.length && newLines.length && !contextLines.length) {
this.applyRematchMatching(oldLines, newLines, matcher).map(([oldLines, newLines]) => {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
fileHtml.left += left;
fileHtml.right += right;
});
} else if (contextLines.length) {
contextLines.forEach(line => {
const { prefix, content } = renderUtils.deconstructLine(line.content, file.isCombined);
const { left, right } = this.generateLineHtml(
{
type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
content: content,
number: line.oldNumber,
},
{
type: renderUtils.CSSLineClass.CONTEXT,
prefix: prefix,
content: content,
number: line.newNumber,
},
);
fileHtml.left += left;
fileHtml.right += right;
});
} else if (oldLines.length || newLines.length) {
const { left, right } = this.processChangedLines(file.isCombined, oldLines, newLines);
fileHtml.left += left;
fileHtml.right += right;
} else {
console.error('Unknown state reached while processing groups of lines', contextLines, oldLines, newLines);
}
});
return fileHtml;
})
.reduce(
(accomulated, html) => {
return { left: accomulated.left + html.left, right: accomulated.right + html.right };
},
{ left: '', right: '' },
);
}
applyLineGroupping(block: DiffBlock): DiffLineGroups {
const blockLinesGroups: DiffLineGroups = [];
let oldLines: (DiffLineDeleted & DiffLineContent)[] = [];
let newLines: (DiffLineInserted & DiffLineContent)[] = [];
for (let i = 0; i < block.lines.length; i++) {
const diffLine = block.lines[i];
if (
(diffLine.type !== LineType.INSERT && newLines.length) ||
(diffLine.type === LineType.CONTEXT && oldLines.length > 0)
) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = [];
newLines = [];
}
if (diffLine.type === LineType.CONTEXT) {
blockLinesGroups.push([[diffLine], [], []]);
} else if (diffLine.type === LineType.INSERT && oldLines.length === 0) {
blockLinesGroups.push([[], [], [diffLine]]);
} else if (diffLine.type === LineType.INSERT && oldLines.length > 0) {
newLines.push(diffLine);
} else if (diffLine.type === LineType.DELETE) {
oldLines.push(diffLine);
}
}
if (oldLines.length || newLines.length) {
blockLinesGroups.push([[], oldLines, newLines]);
oldLines = [];
newLines = [];
}
return blockLinesGroups;
}
applyRematchMatching(
oldLines: DiffLine[],
newLines: DiffLine[],
matcher: Rematch.MatcherFn<DiffLine>,
): DiffLine[][][] {
const comparisons = oldLines.length * newLines.length;
const maxLineSizeInBlock = Math.max.apply(
null,
[0].concat(oldLines.concat(newLines).map(elem => elem.content.length)),
);
const doMatching =
comparisons < this.config.matchingMaxComparisons &&
maxLineSizeInBlock < this.config.maxLineSizeInBlockForComparison &&
(this.config.matching === 'lines' || this.config.matching === 'words');
return doMatching ? matcher(oldLines, newLines) : [[oldLines, newLines]];
}
makeHeaderHtml(blockHeader: string, file?: DiffFile): string {
return this.hoganUtils.render(genericTemplatesPath, 'block-header', {
CSSLineClass: renderUtils.CSSLineClass,
blockHeader: file?.isTooBig ? blockHeader : renderUtils.escapeForHtml(blockHeader),
lineClass: 'd2h-code-side-linenumber',
contentClass: 'd2h-code-side-line',
});
}
processChangedLines(isCombined: boolean, oldLines: DiffLine[], newLines: DiffLine[]): FileHtml {
const fileHtml = {
right: '',
left: '',
};
const maxLinesNumber = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLinesNumber; i++) {
const oldLine = oldLines[i];
const newLine = newLines[i];
const diff =
oldLine !== undefined && newLine !== undefined
? renderUtils.diffHighlight(oldLine.content, newLine.content, isCombined, this.config)
: undefined;
const preparedOldLine =
oldLine !== undefined && oldLine.oldNumber !== undefined
? {
...(diff !== undefined
? {
prefix: diff.oldLine.prefix,
content: diff.oldLine.content,
type: renderUtils.CSSLineClass.DELETE_CHANGES,
}
: {
...renderUtils.deconstructLine(oldLine.content, isCombined),
type: renderUtils.toCSSClass(oldLine.type),
}),
number: oldLine.oldNumber,
}
: undefined;
const preparedNewLine =
newLine !== undefined && newLine.newNumber !== undefined
? {
...(diff !== undefined
? {
prefix: diff.newLine.prefix,
content: diff.newLine.content,
type: renderUtils.CSSLineClass.INSERT_CHANGES,
}
: {
...renderUtils.deconstructLine(newLine.content, isCombined),
type: renderUtils.toCSSClass(newLine.type),
}),
number: newLine.newNumber,
}
: undefined;
const { left, right } = this.generateLineHtml(preparedOldLine, preparedNewLine);
fileHtml.left += left;
fileHtml.right += right;
}
return fileHtml;
}
generateLineHtml(oldLine?: DiffPreparedLine, newLine?: DiffPreparedLine): FileHtml {
return {
left: this.generateSingleHtml(oldLine),
right: this.generateSingleHtml(newLine),
};
}
generateSingleHtml(line?: DiffPreparedLine): string {
const lineClass = 'd2h-code-side-linenumber';
const contentClass = 'd2h-code-side-line';
return this.hoganUtils.render(genericTemplatesPath, 'line', {
type: line?.type || `${renderUtils.CSSLineClass.CONTEXT} d2h-emptyplaceholder`,
lineClass: line !== undefined ? lineClass : `${lineClass} d2h-code-side-emptyplaceholder`,
contentClass: line !== undefined ? contentClass : `${contentClass} d2h-code-side-emptyplaceholder`,
prefix: line?.prefix === ' ' ? '&nbsp;' : line?.prefix,
content: line?.content,
lineNumber: line?.number,
});
}
}
type DiffLineGroups = [
(DiffLineContext & DiffLineContent)[],
(DiffLineDeleted & DiffLineContent)[],
(DiffLineInserted & DiffLineContent)[],
][];
type DiffPreparedLine = {
type: renderUtils.CSSLineClass;
prefix: string;
content: string;
number: number;
};
type FileHtml = {
left: string;
right: string;
};

View file

@ -0,0 +1,23 @@
(function() {
if (!!!global.browserTemplates) global.browserTemplates = {};
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("<li class=\"d2h-file-list-line\">");t.b("\n" + i);t.b(" <span class=\"d2h-file-name-wrapper\">");t.b("\n" + i);t.b(" <span>");t.b(t.rp("<fileIcon0",c,p,""));t.b("</span>");t.b("\n" + i);t.b(" <a href=\"#");t.b(t.v(t.f("fileHtmlId",c,p,0)));t.b("\" class=\"d2h-file-name\">");t.b(t.v(t.f("fileName",c,p,0)));t.b("</a>");t.b("\n" + i);t.b(" <span class=\"d2h-file-stats\">");t.b("\n" + i);t.b(" <span class=\"d2h-lines-added\">");t.b(t.v(t.f("addedLines",c,p,0)));t.b("</span>");t.b("\n" + i);t.b(" <span class=\"d2h-lines-deleted\">");t.b(t.v(t.f("deletedLines",c,p,0)));t.b("</span>");t.b("\n" + i);t.b(" </span>");t.b("\n" + i);t.b(" </span>");t.b("\n" + i);t.b("</li>");return t.fl(); },partials: {"<fileIcon0":{name:"fileIcon", partials: {}, subs: { }}}, subs: { }});
global.browserTemplates["file-summary-wrapper"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<div class=\"d2h-file-list-wrapper\">");t.b("\n" + i);t.b(" <div class=\"d2h-file-list-header\">");t.b("\n" + i);t.b(" <span class=\"d2h-file-list-title\">Files changed (");t.b(t.v(t.f("filesNumber",c,p,0)));t.b(")</span>");t.b("\n" + i);t.b(" <a class=\"d2h-file-switch d2h-hide\">hide</a>");t.b("\n" + i);t.b(" <a class=\"d2h-file-switch d2h-show\">show</a>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" <ol class=\"d2h-file-list\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("files",c,p,0)));t.b("\n" + i);t.b(" </ol>");t.b("\n" + i);t.b("</div>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["generic-column-line-number"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<tr>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.f("lineClass",c,p,0)));t.b(" ");t.b(t.v(t.d("diffParser.LINE_TYPE.INFO",c,p,0)));t.b("\"></td>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.d("diffParser.LINE_TYPE.INFO",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"");t.b(t.v(t.f("contentClass",c,p,0)));t.b(" ");t.b(t.v(t.d("diffParser.LINE_TYPE.INFO",c,p,0)));t.b("\">");t.b(t.t(t.f("blockHeader",c,p,0)));t.b("</div>");t.b("\n" + i);t.b(" </td>");t.b("\n" + i);t.b("</tr>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["generic-empty-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<tr>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.d("diffParser.LINE_TYPE.INFO",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"");t.b(t.v(t.f("contentClass",c,p,0)));t.b(" ");t.b(t.v(t.d("diffParser.LINE_TYPE.INFO",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" File without changes");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </td>");t.b("\n" + i);t.b("</tr>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["generic-file-path"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-file-name-wrapper\">");t.b("\n" + i);t.b(" <span class=\"d2h-icon-wrapper\">");t.b(t.rp("<fileIcon0",c,p,""));t.b("</span>");t.b("\n" + i);t.b(" <span class=\"d2h-file-name\">");t.b(t.v(t.f("fileDiffName",c,p,0)));t.b("</span>");t.b("\n" + i);t.b(t.rp("<fileTag1",c,p," "));t.b("</span>");return t.fl(); },partials: {"<fileIcon0":{name:"fileIcon", partials: {}, subs: { }},"<fileTag1":{name:"fileTag", partials: {}, subs: { }}}, subs: { }});
global.browserTemplates["generic-line"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<tr>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.f("lineClass",c,p,0)));t.b(" ");t.b(t.v(t.f("type",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("lineNumber",c,p,0)));t.b("\n" + i);t.b(" </td>");t.b("\n" + i);t.b(" <td class=\"");t.b(t.v(t.f("type",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"");t.b(t.v(t.f("contentClass",c,p,0)));t.b(" ");t.b(t.v(t.f("type",c,p,0)));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(" <span class=\"d2h-code-line-prefix\">");t.b(t.t(t.f("prefix",c,p,0)));t.b("</span>");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(" <span class=\"d2h-code-line-ctn\">");t.b(t.t(t.f("content",c,p,0)));t.b("</span>");t.b("\n" + i);});c.pop();}t.b(" </div>");t.b("\n" + i);t.b(" </td>");t.b("\n" + i);t.b("</tr>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["generic-wrapper"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<div class=\"d2h-wrapper\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("content",c,p,0)));t.b("\n" + i);t.b("</div>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["icon-file-added"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<svg aria-hidden=\"true\" class=\"d2h-icon d2h-added\" height=\"16\" title=\"added\" version=\"1.1\" viewBox=\"0 0 14 16\"");t.b("\n" + i);t.b(" width=\"14\">");t.b("\n" + i);t.b(" <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM6 9H3V7h3V4h2v3h3v2H8v3H6V9z\"></path>");t.b("\n" + i);t.b("</svg>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["icon-file-changed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<svg aria-hidden=\"true\" class=\"d2h-icon d2h-changed\" height=\"16\" title=\"modified\" version=\"1.1\"");t.b("\n" + i);t.b(" viewBox=\"0 0 14 16\" width=\"14\">");t.b("\n" + i);t.b(" <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z\"></path>");t.b("\n" + i);t.b("</svg>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["icon-file-deleted"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<svg aria-hidden=\"true\" class=\"d2h-icon d2h-deleted\" height=\"16\" title=\"removed\" version=\"1.1\"");t.b("\n" + i);t.b(" viewBox=\"0 0 14 16\" width=\"14\">");t.b("\n" + i);t.b(" <path d=\"M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM11 9H3V7h8v2z\"></path>");t.b("\n" + i);t.b("</svg>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["icon-file-renamed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<svg aria-hidden=\"true\" class=\"d2h-icon d2h-moved\" height=\"16\" title=\"renamed\" version=\"1.1\"");t.b("\n" + i);t.b(" viewBox=\"0 0 14 16\" width=\"14\">");t.b("\n" + i);t.b(" <path d=\"M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z\"></path>");t.b("\n" + i);t.b("</svg>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["icon-file"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<svg aria-hidden=\"true\" class=\"d2h-icon\" height=\"16\" version=\"1.1\" viewBox=\"0 0 12 16\" width=\"12\">");t.b("\n" + i);t.b(" <path d=\"M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z\"></path>");t.b("\n" + i);t.b("</svg>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["line-by-line-file-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<div id=\"");t.b(t.v(t.f("fileHtmlId",c,p,0)));t.b("\" class=\"d2h-file-wrapper\" data-lang=\"");t.b(t.v(t.d("file.language",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"d2h-file-header\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" <div class=\"d2h-file-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("diffs",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b("</div>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["line-by-line-numbers"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<div class=\"line-num1\">");t.b(t.v(t.f("oldNumber",c,p,0)));t.b("</div>");t.b("\n" + i);t.b("<div class=\"line-num2\">");t.b(t.v(t.f("newNumber",c,p,0)));t.b("</div>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["side-by-side-file-diff"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<div id=\"");t.b(t.v(t.f("fileHtmlId",c,p,0)));t.b("\" class=\"d2h-file-wrapper\" data-lang=\"");t.b(t.v(t.d("file.language",c,p,0)));t.b("\">");t.b("\n" + i);t.b(" <div class=\"d2h-file-header\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.f("filePath",c,p,0)));t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" <div class=\"d2h-files-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-file-side-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.left",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" <div class=\"d2h-file-side-diff\">");t.b("\n" + i);t.b(" <div class=\"d2h-code-wrapper\">");t.b("\n" + i);t.b(" <table class=\"d2h-diff-table\">");t.b("\n" + i);t.b(" <tbody class=\"d2h-diff-tbody\">");t.b("\n" + i);t.b(" ");t.b(t.t(t.d("diffs.right",c,p,0)));t.b("\n" + i);t.b(" </tbody>");t.b("\n" + i);t.b(" </table>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b(" </div>");t.b("\n" + i);t.b("</div>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["tag-file-added"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-tag d2h-added d2h-added-tag\">ADDED</span>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["tag-file-changed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-tag d2h-changed d2h-changed-tag\">CHANGED</span>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["tag-file-deleted"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-tag d2h-deleted d2h-deleted-tag\">DELETED</span>");return t.fl(); },partials: {}, subs: { }});
global.browserTemplates["tag-file-renamed"] = new Hogan.Template({code: function (c,p,i) { var t=this;t.b(i=i||"");t.b("<span class=\"d2h-tag d2h-moved d2h-moved-tag\">RENAMED</span>");return t.fl(); },partials: {}, subs: { }});
module.exports = global.browserTemplates;
})();

View file

@ -1,6 +1,6 @@
<li class="d2h-file-list-line">
<span class="d2h-file-name-wrapper">
{{>fileIcon}}
<span>{{>fileIcon}}</span>
<a href="#{{fileHtmlId}}" class="d2h-file-name">{{fileName}}</a>
<span class="d2h-file-stats">
<span class="d2h-lines-added">{{addedLines}}</span>

View file

@ -1,4 +1,4 @@
<div class="d2h-file-list-wrapper {{colorScheme}}">
<div class="d2h-file-list-wrapper">
<div class="d2h-file-list-header">
<span class="d2h-file-list-title">Files changed ({{filesNumber}})</span>
<a class="d2h-file-switch d2h-hide">hide</a>

View file

@ -1,6 +0,0 @@
<tr>
<td class="{{lineClass}} {{CSSLineClass.INFO}}"></td>
<td class="{{CSSLineClass.INFO}}">
<div class="{{contentClass}}">{{#blockHeader}}{{{blockHeader}}}{{/blockHeader}}{{^blockHeader}}&nbsp;{{/blockHeader}}</div>
</td>
</tr>

View file

@ -0,0 +1,6 @@
<tr>
<td class="{{lineClass}} {{diffParser.LINE_TYPE.INFO}}"></td>
<td class="{{diffParser.LINE_TYPE.INFO}}">
<div class="{{contentClass}} {{diffParser.LINE_TYPE.INFO}}">{{{blockHeader}}}</div>
</td>
</tr>

View file

@ -1,6 +1,6 @@
<tr>
<td class="{{CSSLineClass.INFO}}">
<div class="{{contentClass}}">
<td class="{{diffParser.LINE_TYPE.INFO}}">
<div class="{{contentClass}} {{diffParser.LINE_TYPE.INFO}}">
File without changes
</div>
</td>

View file

@ -1,9 +1,5 @@
<span class="d2h-file-name-wrapper">
{{>fileIcon}}
<span class="d2h-icon-wrapper">{{>fileIcon}}</span>
<span class="d2h-file-name">{{fileDiffName}}</span>
{{>fileTag}}
</span>
<label class="d2h-file-collapse">
<input class="d2h-file-collapse-input" type="checkbox" name="viewed" value="viewed">
Viewed
</label>

View file

@ -3,19 +3,13 @@
{{{lineNumber}}}
</td>
<td class="{{type}}">
<div class="{{contentClass}}">
<div class="{{contentClass}} {{type}}">
{{#prefix}}
<span class="d2h-code-line-prefix">{{{prefix}}}</span>
{{/prefix}}
{{^prefix}}
<span class="d2h-code-line-prefix">&nbsp;</span>
{{/prefix}}
{{#content}}
<span class="d2h-code-line-ctn">{{{content}}}</span>
{{/content}}
{{^content}}
<span class="d2h-code-line-ctn"><br></span>
{{/content}}
</div>
</td>
</tr>

View file

@ -1,3 +1,3 @@
<div class="d2h-wrapper {{colorScheme}}">
<div class="d2h-wrapper">
{{{content}}}
</div>

View file

@ -1,99 +0,0 @@
export type DiffLineParts = {
prefix: string;
content: string;
};
export enum LineType {
INSERT = 'insert',
DELETE = 'delete',
CONTEXT = 'context',
}
export interface DiffLineDeleted {
type: LineType.DELETE;
oldNumber: number;
newNumber: undefined;
}
export interface DiffLineInserted {
type: LineType.INSERT;
oldNumber: undefined;
newNumber: number;
}
export interface DiffLineContext {
type: LineType.CONTEXT;
oldNumber: number;
newNumber: number;
}
export type DiffLineContent = {
content: string;
};
export type DiffLine = (DiffLineDeleted | DiffLineInserted | DiffLineContext) & DiffLineContent;
export interface DiffBlock {
oldStartLine: number;
oldStartLine2?: number;
newStartLine: number;
header: string;
lines: DiffLine[];
}
export interface DiffFileName {
oldName: string;
newName: string;
}
export interface DiffFile extends DiffFileName {
addedLines: number;
deletedLines: number;
isCombined: boolean;
isGitDiff: boolean;
language: string;
blocks: DiffBlock[];
oldMode?: string | string[];
newMode?: string;
deletedFileMode?: string;
newFileMode?: string;
isDeleted?: boolean;
isNew?: boolean;
isCopy?: boolean;
isRename?: boolean;
isBinary?: boolean;
isTooBig?: boolean;
unchangedPercentage?: number;
changedPercentage?: number;
checksumBefore?: string | string[];
checksumAfter?: string;
mode?: string;
}
export type OutputFormatType = 'line-by-line' | 'side-by-side';
export const OutputFormatType: { [_: string]: OutputFormatType } = {
LINE_BY_LINE: 'line-by-line',
SIDE_BY_SIDE: 'side-by-side',
};
export type LineMatchingType = 'lines' | 'words' | 'none';
export const LineMatchingType: { [_: string]: LineMatchingType } = {
LINES: 'lines',
WORDS: 'words',
NONE: 'none',
};
export type DiffStyleType = 'word' | 'char';
export const DiffStyleType: { [_: string]: DiffStyleType } = {
WORD: 'word',
CHAR: 'char',
};
export enum ColorSchemeType {
AUTO = 'auto',
DARK = 'dark',
LIGHT = 'light',
}

View file

@ -5,104 +5,17 @@
*
*/
:root,
:host {
--d2h-bg-color: #fff;
--d2h-border-color: #ddd;
--d2h-dim-color: rgba(0, 0, 0, 0.3);
--d2h-line-border-color: #eeeeee;
--d2h-file-header-bg-color: #f7f7f7;
--d2h-file-header-border-color: #d8d8d8;
--d2h-empty-placeholder-bg-color: #f1f1f1;
--d2h-empty-placeholder-border-color: #e1e1e1;
--d2h-selected-color: #c8e1ff;
--d2h-ins-bg-color: #dfd;
--d2h-ins-border-color: #b4e2b4;
--d2h-ins-highlight-bg-color: #97f295;
--d2h-ins-label-color: #399839;
--d2h-del-bg-color: #fee8e9;
--d2h-del-border-color: #e9aeae;
--d2h-del-highlight-bg-color: #ffb6ba;
--d2h-del-label-color: #c33;
--d2h-change-del-color: #fdf2d0;
--d2h-change-ins-color: #ded;
--d2h-info-bg-color: #f8fafd;
--d2h-info-border-color: #d5e4f2;
--d2h-change-label-color: #d0b44c;
--d2h-moved-label-color: #3572b0;
/**
* Dark Color Scheme
*/
--d2h-dark-color: rgb(230, 237, 243);
--d2h-dark-bg-color: rgb(13, 17, 23);
--d2h-dark-border-color: rgb(48, 54, 61);
--d2h-dark-dim-color: rgb(110, 118, 129);
--d2h-dark-line-border-color: rgb(33, 38, 45);
--d2h-dark-file-header-bg-color: rgb(22, 27, 34);
--d2h-dark-file-header-border-color: rgb(48, 54, 61);
--d2h-dark-empty-placeholder-bg-color: rgba(110, 118, 129, 0.1);
--d2h-dark-empty-placeholder-border-color: rgb(48, 54, 61);
--d2h-dark-selected-color: rgba(56, 139, 253, 0.1);
--d2h-dark-ins-bg-color: rgba(46, 160, 67, 0.15);
--d2h-dark-ins-border-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-highlight-bg-color: rgba(46, 160, 67, 0.4);
--d2h-dark-ins-label-color: rgb(63, 185, 80);
--d2h-dark-del-bg-color: rgba(248, 81, 73, 0.1);
--d2h-dark-del-border-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-highlight-bg-color: rgba(248, 81, 73, 0.4);
--d2h-dark-del-label-color: rgb(248, 81, 73);
--d2h-dark-change-del-color: rgba(210, 153, 34, 0.2);
--d2h-dark-change-ins-color: rgba(46, 160, 67, 0.25);
--d2h-dark-info-bg-color: rgba(56, 139, 253, 0.1);
--d2h-dark-info-border-color: rgba(56, 139, 253, 0.4);
--d2h-dark-change-label-color: rgb(210, 153, 34);
--d2h-dark-moved-label-color: #3572b0;
}
.d2h-wrapper {
text-align: left;
}
.d2h-file-header {
display: flex;
height: 35px;
padding: 5px 10px;
border-bottom: 1px solid var(--d2h-file-header-border-color);
background-color: var(--d2h-file-header-bg-color);
font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
.d2h-file-header.d2h-sticky-header {
position: sticky;
top: 0;
z-index: 1;
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;
@ -110,120 +23,88 @@
.d2h-lines-added {
text-align: right;
border: 1px solid var(--d2h-ins-border-color);
border: 1px solid #b4e2b4;
border-radius: 5px 0 0 5px;
color: var(--d2h-ins-label-color);
color: #399839;
padding: 2px;
vertical-align: middle;
}
.d2h-lines-deleted {
text-align: left;
border: 1px solid var(--d2h-del-border-color);
border: 1px solid #e9aeae;
border-radius: 0 5px 5px 0;
color: var(--d2h-del-label-color);
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;
line-height: 15px;
}
.d2h-file-name {
line-height: 33px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
overflow: hidden;
}
.d2h-file-wrapper {
border: 1px solid var(--d2h-border-color);
border: 1px solid #ddd;
border-radius: 3px;
margin-bottom: 1em;
}
.d2h-file-collapse {
justify-content: flex-end;
display: none;
cursor: pointer;
font-size: 12px;
align-items: center;
border-radius: 3px;
border: 1px solid var(--d2h-border-color);
padding: 4px 8px;
}
.d2h-file-collapse.d2h-selected {
background-color: var(--d2h-selected-color);
}
.d2h-file-collapse-input {
margin: 0 4px 0 0;
}
.d2h-diff-table {
width: 100%;
border-collapse: collapse;
font-family: 'Menlo', 'Consolas', monospace;
font-family: "Menlo", "Consolas", monospace;
font-size: 13px;
}
.d2h-diff-tbody > tr > td > div {
height: 16px;
line-height: 16px;
}
.d2h-files-diff {
display: flex;
width: 100%;
}
.d2h-file-diff {
overflow-x: scroll;
overflow-y: hidden;
}
.d2h-files-diff.d2h-d-none,
.d2h-file-diff.d2h-d-none {
display: none;
}
.d2h-file-side-diff {
display: inline-block;
overflow-x: scroll;
overflow-y: hidden;
width: 50%;
margin-right: -4px;
}
.d2h-code-line {
display: inline-block;
display: block;
white-space: nowrap;
user-select: none;
width: calc(100% - 16em);
/* Compensate for the absolute positioning of the line numbers */
padding: 0 8em;
padding: 0 10px;
margin-left: 80px;
}
.d2h-code-side-line {
display: inline-block;
display: block;
white-space: nowrap;
user-select: none;
width: calc(100% - 9em);
/* Compensate for the absolute positioning of the line numbers */
padding: 0 4.5em;
}
.d2h-code-line-ctn {
display: inline-block;
background: none;
padding: 0;
word-wrap: normal;
white-space: pre;
user-select: text;
width: 100%;
vertical-align: middle;
padding: 0 10px;
height: 18px;
line-height: 18px;
margin-left: 50px;
}
.d2h-code-line del,
@ -231,7 +112,7 @@
display: inline-block;
margin-top: -1px;
text-decoration: none;
background-color: var(--d2h-del-highlight-bg-color);
background-color: #ffb6ba;
border-radius: 0.2em;
}
@ -240,92 +121,71 @@
display: inline-block;
margin-top: -1px;
text-decoration: none;
background-color: var(--d2h-ins-highlight-bg-color);
background-color: #97f295;
border-radius: 0.2em;
text-align: left;
}
.d2h-code-line-prefix {
display: inline;
float: left;
background: none;
padding: 0;
}
.d2h-code-line-ctn {
background: none;
padding: 0;
word-wrap: normal;
white-space: pre;
}
.line-num1 {
box-sizing: border-box;
float: left;
width: 3.5em;
width: 40px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
padding-left: 3px;
}
.line-num2 {
box-sizing: border-box;
float: right;
width: 3.5em;
width: 40px;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
padding-left: 3px;
}
.d2h-code-linenumber {
box-sizing: border-box;
width: 7.5em;
/* Keep the numbers fixed on line contents scroll */
position: absolute;
display: inline-block;
background-color: var(--d2h-bg-color);
color: var(--d2h-dim-color);
width: 86px;
height: 18px;
padding-left: 2px;
padding-right: 2px;
line-height: 18px;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid var(--d2h-line-border-color);
border: solid #eeeeee;
border-width: 0 1px 0 1px;
cursor: pointer;
}
.d2h-code-linenumber:after {
content: '\200b';
}
.d2h-code-side-linenumber {
/* Keep the numbers fixed on line contents scroll */
position: absolute;
display: inline-block;
box-sizing: border-box;
width: 4em;
background-color: var(--d2h-bg-color);
color: var(--d2h-dim-color);
position: absolute;
width: 56px;
padding-left: 5px;
padding-right: 5px;
height: 18px;
line-height: 18px;
background-color: #fff;
color: rgba(0, 0, 0, 0.3);
text-align: right;
border: solid var(--d2h-line-border-color);
border: solid #eeeeee;
border-width: 0 1px 0 1px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 0.5em 0 0.5em;
}
.d2h-code-side-linenumber:after {
content: '\200b';
}
.d2h-code-side-emptyplaceholder,
.d2h-emptyplaceholder {
background-color: var(--d2h-empty-placeholder-bg-color);
border-color: var(--d2h-empty-placeholder-border-color);
}
.d2h-code-linenumber,
.d2h-code-side-linenumber,
.d2h-code-line-prefix,
.d2h-emptyplaceholder {
user-select: none;
}
.d2h-code-linenumber,
.d2h-code-side-linenumber {
direction: rtl;
}
/*
@ -333,27 +193,27 @@
*/
.d2h-del {
background-color: var(--d2h-del-bg-color);
border-color: var(--d2h-del-border-color);
background-color: #fee8e9;
border-color: #e9aeae;
}
.d2h-ins {
background-color: var(--d2h-ins-bg-color);
border-color: var(--d2h-ins-border-color);
background-color: #dfd;
border-color: #b4e2b4;
}
.d2h-info {
background-color: var(--d2h-info-bg-color);
color: var(--d2h-dim-color);
border-color: var(--d2h-info-border-color);
background-color: #f8fafd;
color: rgba(0, 0, 0, 0.3);
border-color: #d5e4f2;
}
.d2h-file-diff .d2h-del.d2h-change {
background-color: var(--d2h-change-del-color);
background-color: #fdf2d0;
}
.d2h-file-diff .d2h-ins.d2h-change {
background-color: var(--d2h-change-ins-color);
background-color: #ded;
}
/*
@ -366,11 +226,11 @@
.d2h-file-list-wrapper a {
text-decoration: none;
color: var(--d2h-moved-label-color);
color: #3572b0;
}
.d2h-file-list-wrapper a:visited {
color: var(--d2h-moved-label-color);
color: #3572b0;
}
.d2h-file-list-header {
@ -382,12 +242,14 @@
}
.d2h-file-list-line {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
text-align: left;
}
.d2h-file-list-line .d2h-file-name {
line-height: 21px;
}
.d2h-file-list {
display: block;
list-style: none;
@ -396,7 +258,7 @@
}
.d2h-file-list > li {
border-bottom: var(--d2h-border-color) solid 1px;
border-bottom: #ddd solid 1px;
padding: 5px 10px;
margin: 0;
}
@ -411,6 +273,10 @@
cursor: pointer;
}
.d2h-icon-wrapper {
line-height: 31px;
}
.d2h-icon {
vertical-align: middle;
margin-right: 10px;
@ -418,324 +284,71 @@
}
.d2h-deleted {
color: var(--d2h-del-label-color);
color: #c33;
}
.d2h-added {
color: var(--d2h-ins-label-color);
color: #399839;
}
.d2h-changed {
color: var(--d2h-change-label-color);
color: #d0b44c;
}
.d2h-moved {
color: var(--d2h-moved-label-color);
color: #3572b0;
}
.d2h-tag {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
font-size: 10px;
margin-left: 5px;
padding: 0 2px;
background-color: var(--d2h-bg-color);
background-color: #fff;
}
.d2h-deleted-tag {
border: var(--d2h-del-label-color) 1px solid;
border: #c33 1px solid;
}
.d2h-added-tag {
border: var(--d2h-ins-label-color) 1px solid;
border: #399839 1px solid;
}
.d2h-changed-tag {
border: var(--d2h-change-label-color) 1px solid;
border: #d0b44c 1px solid;
}
.d2h-moved-tag {
border: var(--d2h-moved-label-color) 1px solid;
border: #3572b0 1px solid;
}
/**
* Dark Mode Colors
/*
* Selection util.
*/
.d2h-dark-color-scheme {
color: var(--d2h-dark-color);
background-color: var(--d2h-dark-bg-color);
.selecting-left .d2h-code-line,
.selecting-left .d2h-code-line *,
.selecting-right td.d2h-code-linenumber,
.selecting-right td.d2h-code-linenumber *,
.selecting-left .d2h-code-side-line,
.selecting-left .d2h-code-side-line *,
.selecting-right td.d2h-code-side-linenumber,
.selecting-right td.d2h-code-side-linenumber * {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.d2h-dark-color-scheme .d2h-file-header {
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-dark-color-scheme .d2h-lines-added {
border: 1px solid var(--d2h-dark-ins-border-color);
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-lines-deleted {
border: 1px solid var(--d2h-dark-del-border-color);
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-code-line del,
.d2h-dark-color-scheme .d2h-code-side-line del {
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-code-line ins,
.d2h-dark-color-scheme .d2h-code-side-line ins {
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-dark-color-scheme .d2h-diff-tbody {
border-color: var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-code-side-linenumber {
background-color: var(--d2h-dark-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-line-border-color);
}
.d2h-dark-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-dark-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-dark-color-scheme .d2h-code-linenumber {
background-color: var(--d2h-dark-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-line-border-color);
}
.d2h-dark-color-scheme .d2h-del {
background-color: var(--d2h-dark-del-bg-color);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-dark-color-scheme .d2h-ins {
background-color: var(--d2h-dark-ins-bg-color);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-dark-color-scheme .d2h-info {
background-color: var(--d2h-dark-info-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-info-border-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: var(--d2h-dark-change-del-color);
}
.d2h-dark-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-dark-color-scheme .d2h-file-wrapper {
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-dark-color-scheme .d2h-file-collapse {
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-file-collapse.d2h-selected {
background-color: var(--d2h-dark-selected-color);
}
.d2h-dark-color-scheme .d2h-file-list-wrapper a {
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-file-list-wrapper a:visited {
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-file-list > li {
border-bottom: var(--d2h-dark-bg-color) solid 1px;
}
.d2h-dark-color-scheme .d2h-deleted {
color: var(--d2h-dark-del-label-color);
}
.d2h-dark-color-scheme .d2h-added {
color: var(--d2h-dark-ins-label-color);
}
.d2h-dark-color-scheme .d2h-changed {
color: var(--d2h-dark-change-label-color);
}
.d2h-dark-color-scheme .d2h-moved {
color: var(--d2h-dark-moved-label-color);
}
.d2h-dark-color-scheme .d2h-tag {
background-color: var(--d2h-dark-bg-color);
}
.d2h-dark-color-scheme .d2h-deleted-tag {
border: var(--d2h-dark-del-label-color) 1px solid;
}
.d2h-dark-color-scheme .d2h-added-tag {
border: var(--d2h-dark-ins-label-color) 1px solid;
}
.d2h-dark-color-scheme .d2h-changed-tag {
border: var(--d2h-dark-change-label-color) 1px solid;
}
.d2h-dark-color-scheme .d2h-moved-tag {
border: var(--d2h-dark-moved-label-color) 1px solid;
}
/**
* Auto Mode Colors
*/
@media (prefers-color-scheme: dark) {
.d2h-auto-color-scheme {
background-color: var(--d2h-dark-bg-color);
color: var(--d2h-dark-color);
}
.d2h-auto-color-scheme .d2h-file-header {
background-color: var(--d2h-dark-file-header-bg-color);
border-bottom: var(--d2h-dark-file-header-border-color);
}
.d2h-auto-color-scheme .d2h-lines-added {
border: 1px solid var(--d2h-dark-ins-border-color);
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-lines-deleted {
border: 1px solid var(--d2h-dark-del-border-color);
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-code-line del,
.d2h-auto-color-scheme .d2h-code-side-line del {
background-color: var(--d2h-dark-del-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-code-line ins,
.d2h-auto-color-scheme .d2h-code-side-line ins {
background-color: var(--d2h-dark-ins-highlight-bg-color);
}
.d2h-auto-color-scheme .d2h-diff-tbody {
border-color: var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-code-side-linenumber {
background-color: var(--d2h-dark-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-line-border-color);
}
.d2h-auto-color-scheme .d2h-files-diff .d2h-code-side-emptyplaceholder,
.d2h-auto-color-scheme .d2h-files-diff .d2h-emptyplaceholder {
background-color: var(--d2h-dark-empty-placeholder-bg-color);
border-color: var(--d2h-dark-empty-placeholder-border-color);
}
.d2h-auto-color-scheme .d2h-code-linenumber {
background-color: var(--d2h-dark-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-line-border-color);
}
.d2h-auto-color-scheme .d2h-del {
background-color: var(--d2h-dark-del-bg-color);
border-color: var(--d2h-dark-del-border-color);
}
.d2h-auto-color-scheme .d2h-ins {
background-color: var(--d2h-dark-ins-bg-color);
border-color: var(--d2h-dark-ins-border-color);
}
.d2h-auto-color-scheme .d2h-info {
background-color: var(--d2h-dark-info-bg-color);
color: var(--d2h-dark-dim-color);
border-color: var(--d2h-dark-info-border-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-del.d2h-change {
background-color: var(--d2h-dark-change-del-color);
}
.d2h-auto-color-scheme .d2h-file-diff .d2h-ins.d2h-change {
background-color: var(--d2h-dark-change-ins-color);
}
.d2h-auto-color-scheme .d2h-file-wrapper {
border: 1px solid var(--d2h-dark-border-color);
}
.d2h-auto-color-scheme .d2h-file-collapse {
border: 1px solid var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-file-collapse.d2h-selected {
background-color: var(--d2h-dark-selected-color);
}
.d2h-auto-color-scheme .d2h-file-list-wrapper a {
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-file-list-wrapper a:visited {
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-file-list > li {
border-bottom: var(--d2h-dark-bg-color) solid 1px;
}
.d2h-dark-color-scheme .d2h-deleted {
color: var(--d2h-dark-del-label-color);
}
.d2h-auto-color-scheme .d2h-added {
color: var(--d2h-dark-ins-label-color);
}
.d2h-auto-color-scheme .d2h-changed {
color: var(--d2h-dark-change-label-color);
}
.d2h-auto-color-scheme .d2h-moved {
color: var(--d2h-dark-moved-label-color);
}
.d2h-auto-color-scheme .d2h-tag {
background-color: var(--d2h-dark-bg-color);
}
.d2h-auto-color-scheme .d2h-deleted-tag {
border: var(--d2h-dark-del-label-color) 1px solid;
}
.d2h-auto-color-scheme .d2h-added-tag {
border: var(--d2h-dark-ins-label-color) 1px solid;
}
.d2h-auto-color-scheme .d2h-changed-tag {
border: var(--d2h-dark-change-label-color) 1px solid;
}
.d2h-auto-color-scheme .d2h-moved-tag {
border: var(--d2h-dark-moved-label-color) 1px solid;
}
.selecting-left .d2h-code-line::selection,
.selecting-left .d2h-code-line *::selection
.selecting-right td.d2h-code-linenumber::selection,
.selecting-left .d2h-code-side-line::selection,
.selecting-left .d2h-code-side-line *::selection,
.selecting-right td.d2h-code-side-linenumber::selection,
.selecting-right td.d2h-code-side-linenumber *::selection {
background: transparent;
}

View file

@ -1,225 +0,0 @@
import { closeTags, nodeStream, mergeStreams, getLanguage } from './highlight.js-helpers';
import { html, Diff2HtmlConfig, defaultDiff2HtmlConfig } from '../../diff2html';
import { DiffFile } from '../../types';
import { HighlightResult, HLJSApi } from 'highlight.js';
export interface Diff2HtmlUIConfig extends Diff2HtmlConfig {
synchronisedScroll?: boolean;
highlight?: boolean;
fileListToggle?: boolean;
fileListStartVisible?: boolean;
highlightLanguages?: Map<string, string>;
/**
* @deprecated since version 3.1.0
* Smart selection is now enabled by default with vanilla CSS
*/
smartSelection?: boolean;
fileContentToggle?: boolean;
stickyFileHeaders?: boolean;
}
export const defaultDiff2HtmlUIConfig = {
...defaultDiff2HtmlConfig,
synchronisedScroll: true,
highlight: true,
fileListToggle: true,
fileListStartVisible: false,
highlightLanguages: new Map<string, string>(),
/**
* @deprecated since version 3.1.0
* Smart selection is now enabled by default with vanilla CSS
*/
smartSelection: true,
fileContentToggle: true,
stickyFileHeaders: true,
};
export class Diff2HtmlUI {
readonly config: typeof defaultDiff2HtmlUIConfig;
readonly diffHtml: string;
readonly targetElement: HTMLElement;
readonly hljs: HLJSApi | null = null;
currentSelectionColumnId = -1;
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}, hljs?: HLJSApi) {
this.config = { ...defaultDiff2HtmlUIConfig, ...config };
this.diffHtml = diffInput !== undefined ? html(diffInput, this.config) : target.innerHTML;
this.targetElement = target;
if (hljs !== undefined) this.hljs = hljs;
}
draw(): void {
this.targetElement.innerHTML = this.diffHtml;
if (this.config.synchronisedScroll) this.synchronisedScroll();
if (this.config.highlight) this.highlightCode();
if (this.config.fileListToggle) this.fileListToggle(this.config.fileListStartVisible);
if (this.config.fileContentToggle) this.fileContentToggle();
if (this.config.stickyFileHeaders) this.stickyFileHeaders();
}
synchronisedScroll(): void {
this.targetElement.querySelectorAll('.d2h-file-wrapper').forEach(wrapper => {
const [left, right] = Array<Element>().slice.call(wrapper.querySelectorAll('.d2h-file-side-diff'));
if (left === undefined || right === undefined) return;
const onScroll = (event: Event): void => {
if (event === null || event.target === null) return;
if (event.target === left) {
right.scrollTop = left.scrollTop;
right.scrollLeft = left.scrollLeft;
} else {
left.scrollTop = right.scrollTop;
left.scrollLeft = right.scrollLeft;
}
};
left.addEventListener('scroll', onScroll);
right.addEventListener('scroll', onScroll);
});
}
fileListToggle(startVisible: boolean): void {
const showBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-show');
const hideBtn: HTMLElement | null = this.targetElement.querySelector('.d2h-hide');
const fileList: HTMLElement | null = this.targetElement.querySelector('.d2h-file-list');
if (showBtn === null || hideBtn === null || fileList === null) return;
const show: () => void = () => {
showBtn.style.display = 'none';
hideBtn.style.display = 'inline';
fileList.style.display = 'block';
};
const hide: () => void = () => {
showBtn.style.display = 'inline';
hideBtn.style.display = 'none';
fileList.style.display = 'none';
};
showBtn.addEventListener('click', () => show());
hideBtn.addEventListener('click', () => hide());
const hashTag = this.getHashTag();
if (hashTag === 'files-summary-show') show();
else if (hashTag === 'files-summary-hide') hide();
else if (startVisible) show();
else hide();
}
fileContentToggle(): void {
this.targetElement.querySelectorAll<HTMLElement>('.d2h-file-collapse').forEach(fileContentToggleBtn => {
fileContentToggleBtn.style.display = 'flex';
const toggleFileContents: (selector: string) => void = selector => {
const fileContents: HTMLElement | null | undefined = fileContentToggleBtn
.closest('.d2h-file-wrapper')
?.querySelector(selector);
if (fileContents !== null && fileContents !== undefined) {
fileContentToggleBtn.classList.toggle('d2h-selected');
fileContents.classList.toggle('d2h-d-none');
}
};
const toggleHandler: (e: Event) => void = e => {
if (fileContentToggleBtn === e.target) return;
toggleFileContents('.d2h-file-diff');
toggleFileContents('.d2h-files-diff');
};
fileContentToggleBtn.addEventListener('click', e => toggleHandler(e));
});
}
highlightCode(): void {
const hljs = this.hljs;
if (hljs === null) {
throw new Error('Missing a `highlight.js` implementation. Please provide one when instantiating Diff2HtmlUI.');
}
// Collect all the diff files and execute the highlight on their lines
const files = this.targetElement.querySelectorAll('.d2h-file-wrapper');
files.forEach(file => {
const language = file.getAttribute('data-lang');
if (!(this.config.highlightLanguages instanceof Map)) {
this.config.highlightLanguages = new Map(Object.entries(this.config.highlightLanguages));
}
let hljsLanguage =
language && this.config.highlightLanguages.has(language)
? this.config.highlightLanguages.get(language)!
: language
? getLanguage(language)
: 'plaintext';
// Fallback to plaintext in case language is not loaded
if (hljs.getLanguage(hljsLanguage) === undefined) {
hljsLanguage = 'plaintext';
}
// Collect all the code lines and execute the highlight on them
const codeLines = file.querySelectorAll('.d2h-code-line-ctn');
codeLines.forEach(line => {
const text = line.textContent;
const lineParent = line.parentNode;
if (text === null || lineParent === null || !this.isElement(lineParent)) return;
const result: HighlightResult = closeTags(
hljs.highlight(text, {
language: hljsLanguage,
ignoreIllegals: true,
}),
);
const originalStream = nodeStream(line);
if (originalStream.length) {
const resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = mergeStreams(originalStream, nodeStream(resultNode), text);
}
line.classList.add('hljs');
if (result.language) {
line.classList.add(result.language);
}
line.innerHTML = result.value;
});
});
}
stickyFileHeaders(): void {
this.targetElement.querySelectorAll('.d2h-file-header').forEach(header => {
header.classList.add('d2h-sticky-header');
});
}
/**
* @deprecated since version 3.1.0
*/
smartSelection(): void {
console.warn('Smart selection is now enabled by default with CSS. No need to call this method anymore.');
}
private getHashTag(): string | null {
const docUrl = document.URL;
const hashTagIndex = docUrl.indexOf('#');
let hashTag = null;
if (hashTagIndex !== -1) {
hashTag = docUrl.substr(hashTagIndex + 1);
}
return hashTag;
}
private isElement(arg?: unknown): arg is Element {
return arg !== null && (arg as Element)?.classList !== undefined;
}
}

View file

@ -1,13 +0,0 @@
import { hljs } from './highlight.js-slim';
import { DiffFile } from '../../types';
import { Diff2HtmlUI as Diff2HtmlUIBase, Diff2HtmlUIConfig, defaultDiff2HtmlUIConfig } from './diff2html-ui-base';
export class Diff2HtmlUI extends Diff2HtmlUIBase {
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}) {
super(target, diffInput, config, hljs);
}
}
export { defaultDiff2HtmlUIConfig };
export type { Diff2HtmlUIConfig };

208
src/ui/js/diff2html-ui.js Normal file
View file

@ -0,0 +1,208 @@
/*
*
* Diff to HTML (diff2html-ui.js)
* Author: rtfpessoa
*
* Depends on: [ jQuery ]
* Optional dependencies on: [ highlight.js ]
*
*/
/*global $, hljs, Diff2Html*/
(function() {
var highlightJS = require('./highlight.js-internals.js').HighlightJS;
var diffJson = null;
var defaultTarget = "body";
var currentSelectionColumnId = -1;
function Diff2HtmlUI(config) {
var cfg = config || {};
if (cfg.diff) {
diffJson = Diff2Html.getJsonFromDiff(cfg.diff);
} else if (cfg.json) {
diffJson = cfg.json;
}
this._initSelection();
}
Diff2HtmlUI.prototype.draw = function(targetId, config) {
var cfg = config || {};
var $target = this._getTarget(targetId);
$target.html(Diff2Html.getPrettyHtml(diffJson, cfg));
};
Diff2HtmlUI.prototype.fileListCloseable = function(targetId, startVisible) {
var $target = this._getTarget(targetId);
var hashTag = this._getHashTag();
var $showBtn = $target.find(".d2h-show");
var $hideBtn = $target.find(".d2h-hide");
var $fileList = $target.find(".d2h-file-list");
if (hashTag === 'files-summary-show') show();
else if (hashTag === 'files-summary-hide') hide();
else if (startVisible) show();
else hide();
$showBtn.click(show);
$hideBtn.click(hide);
function show() {
$showBtn.hide();
$hideBtn.show();
$fileList.show();
}
function hide() {
$hideBtn.hide();
$showBtn.show();
$fileList.hide();
}
};
Diff2HtmlUI.prototype.highlightCode = function(targetId) {
var that = this;
var $target = that._getTarget(targetId);
// collect all the diff files and execute the highlight on their lines
var $files = $target.find(".d2h-file-wrapper");
$files.map(function(_i, file) {
var oldLinesState;
var newLinesState;
var $file = $(file);
var language = $file.data("lang");
// collect all the code lines and execute the highlight on them
var $codeLines = $file.find(".d2h-code-line-ctn");
$codeLines.map(function(_j, line) {
var $line = $(line);
var text = line.textContent;
var lineParent = line.parentNode;
var lineState;
if (lineParent.className.indexOf("d2h-del") !== -1) {
lineState = oldLinesState;
} else {
lineState = newLinesState;
}
var result = hljs.getLanguage(language) ? hljs.highlight(language, text, true, lineState) : hljs.highlightAuto(text);
if (lineParent.className.indexOf("d2h-del") !== -1) {
oldLinesState = result.top;
} else if (lineParent.className.indexOf("d2h-ins") !== -1) {
newLinesState = result.top;
} else {
oldLinesState = result.top;
newLinesState = result.top;
}
var originalStream = highlightJS.nodeStream(line);
if (originalStream.length) {
var resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div');
resultNode.innerHTML = result.value;
result.value = highlightJS.mergeStreams(originalStream, highlightJS.nodeStream(resultNode), text);
}
$line.addClass("hljs");
$line.addClass(result.language);
$line.html(result.value);
});
});
};
Diff2HtmlUI.prototype._getTarget = function(targetId) {
var $target;
if (typeof targetId === 'object' && targetId instanceof jQuery) {
$target = targetId;
} else if (typeof targetId === 'string') {
$target = $(targetId);
} else {
console.error("Wrong target provided! Falling back to default value 'body'.");
console.log("Please provide a jQuery object or a valid DOM query string.");
$target = $(defaultTarget);
}
return $target;
};
Diff2HtmlUI.prototype._getHashTag = function() {
var docUrl = document.URL;
var hashTagIndex = docUrl.indexOf('#');
var hashTag = null;
if (hashTagIndex !== -1) {
hashTag = docUrl.substr(hashTagIndex + 1);
}
return hashTag;
};
Diff2HtmlUI.prototype._distinct = function(collection) {
return collection.filter(function(v, i) {
return collection.indexOf(v) === i;
});
};
Diff2HtmlUI.prototype._initSelection = function() {
var body = $('body');
var that = this;
body.on('mousedown', '.d2h-diff-table', function(event) {
var target = $(event.target);
var table = target.closest('.d2h-diff-table');
if (target.closest('.d2h-code-line,.d2h-code-side-line').length) {
table.removeClass('selecting-left');
table.addClass('selecting-right');
currentSelectionColumnId = 1;
} else if (target.closest('.d2h-code-linenumber,.d2h-code-side-linenumber').length) {
table.removeClass('selecting-right');
table.addClass('selecting-left');
currentSelectionColumnId = 0;
}
});
body.on('copy', '.d2h-diff-table', function(event) {
var clipboardData = event.originalEvent.clipboardData;
var text = that._getSelectedText();
clipboardData.setData('text', text);
event.preventDefault();
});
};
Diff2HtmlUI.prototype._getSelectedText = function() {
var sel = window.getSelection();
var range = sel.getRangeAt(0);
var doc = range.cloneContents();
var nodes = doc.querySelectorAll('tr');
var text = '';
var idx = currentSelectionColumnId;
if (nodes.length === 0) {
text = doc.textContent;
} else {
[].forEach.call(nodes, function(tr, i) {
var td = tr.cells[tr.cells.length === 1 ? 0 : idx];
text += (i ? '\n' : '') + td.textContent.replace(/(?:\r\n|\r|\n)/g, '');
});
}
return text;
};
module.exports.Diff2HtmlUI = Diff2HtmlUI;
// Expose diff2html in the browser
global.Diff2HtmlUI = Diff2HtmlUI;
})();

View file

@ -1,13 +0,0 @@
import hljs from 'highlight.js';
import { DiffFile } from '../../types';
import { Diff2HtmlUI as Diff2HtmlUIBase, Diff2HtmlUIConfig, defaultDiff2HtmlUIConfig } from './diff2html-ui-base';
export class Diff2HtmlUI extends Diff2HtmlUIBase {
constructor(target: HTMLElement, diffInput?: string | DiffFile[], config: Diff2HtmlUIConfig = {}) {
super(target, diffInput, config, hljs);
}
}
export { defaultDiff2HtmlUIConfig };
export type { Diff2HtmlUIConfig };

View file

@ -1,655 +0,0 @@
/*
* Adapted Highlight.js Internal APIs
* Used to highlight selected html elements using context
*/
import { HighlightResult } from 'highlight.js';
/* Utility functions */
function escapeHTML(value: string): string {
return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}
function tag(node: Node): string {
return node.nodeName.toLowerCase();
}
/* Stream merging */
type NodeEvent = {
event: 'start' | 'stop';
offset: number;
node: Node;
};
export function nodeStream(node: Node): NodeEvent[] {
const result: NodeEvent[] = [];
const nodeStream = (node: Node, offset: number): number => {
for (let child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType === 3 && child.nodeValue !== null) {
offset += child.nodeValue.length;
} else if (child.nodeType === 1) {
result.push({
event: 'start',
offset: offset,
node: child,
});
offset = nodeStream(child, offset);
// Prevent void elements from having an end tag that would actually
// double them in the output. There are more void elements in HTML
// but we list only those realistically expected in code display.
if (!tag(child).match(/br|hr|img|input/)) {
result.push({
event: 'stop',
offset: offset,
node: child,
});
}
}
}
return offset;
};
nodeStream(node, 0);
return result;
}
export function mergeStreams(original: NodeEvent[], highlighted: NodeEvent[], value: string): string {
let processed = 0;
let result = '';
const nodeStack = [];
function isElement(arg?: unknown): arg is Element {
return arg !== null && (arg as Element)?.attributes !== undefined;
}
function selectStream(): NodeEvent[] {
if (!original.length || !highlighted.length) {
return original.length ? original : highlighted;
}
if (original[0].offset !== highlighted[0].offset) {
return original[0].offset < highlighted[0].offset ? original : highlighted;
}
/*
To avoid starting the stream just before it should stop the order is
ensured that original always starts first and closes last:
if (event1 == 'start' && event2 == 'start')
return original;
if (event1 == 'start' && event2 == 'stop')
return highlighted;
if (event1 == 'stop' && event2 == 'start')
return original;
if (event1 == 'stop' && event2 == 'stop')
return highlighted;
... which is collapsed to:
*/
return highlighted[0].event === 'start' ? original : highlighted;
}
function open(node: Node): void {
if (!isElement(node)) {
throw new Error('Node is not an Element');
}
result += `<${tag(node)} ${Array<Attr>()
.map.call(node.attributes, attr => `${attr.nodeName}="${escapeHTML(attr.value).replace(/"/g, '&quot;')}"`)
.join(' ')}>`;
}
function close(node: Node): void {
result += '</' + tag(node) + '>';
}
function render(event: NodeEvent): void {
(event.event === 'start' ? open : close)(event.node);
}
while (original.length || highlighted.length) {
let stream = selectStream();
result += escapeHTML(value.substring(processed, stream[0].offset));
processed = stream[0].offset;
if (stream === original) {
/*
On any opening or closing tag of the original markup we first close
the entire highlighted node stack, then render the original tag along
with all the following original tags at the same offset and then
reopen all the tags on the highlighted stack.
*/
nodeStack.reverse().forEach(close);
do {
render(stream.splice(0, 1)[0]);
stream = selectStream();
} while (stream === original && stream.length && stream[0].offset === processed);
nodeStack.reverse().forEach(open);
} else {
if (stream[0].event === 'start') {
nodeStack.push(stream[0].node);
} else {
nodeStack.pop();
}
render(stream.splice(0, 1)[0]);
}
}
return result + escapeHTML(value.substr(processed));
}
// https://github.com/hexojs/hexo-util/blob/979873b63a725377c2bd6ad834d790023496130d/lib/highlight.js#L123
export function closeTags(res: HighlightResult): HighlightResult {
const tokenStack = new Array<string>();
res.value = res.value
.split('\n')
.map(line => {
const prepend = tokenStack.map(token => `<span class="${token}">`).join('');
const matches = line.matchAll(/(<span class="(.*?)">|<\/span>)/g);
Array.from(matches).forEach(match => {
if (match[0] === '</span>') tokenStack.shift();
else tokenStack.unshift(match[2]);
});
const append = '</span>'.repeat(tokenStack.length);
return prepend + line + append;
})
.join('\n');
return res;
}
// Sourced from https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md and
// https://github.com/exercism/v2-website/blob/main/config/initializers/prism.rb#L187-L315
const languagesToExt: { [_: string]: string } = {
'1c': '1c',
abnf: 'abnf',
accesslog: 'accesslog',
as: 'actionscript',
adb: 'ada',
ada: 'ada',
ads: 'ada',
angelscript: 'angelscript',
// asc: 'angelscript',
apache: 'apache',
applescript: 'applescript',
scpt: 'applescript',
arcade: 'arcade',
cpp: 'cpp',
hpp: 'cpp',
arduino: 'arduino',
ino: 'arduino',
armasm: 'armasm',
arm: 'armasm',
xml: 'xml',
html: 'xml',
xhtml: 'xml',
rss: 'xml',
atom: 'xml',
xjb: 'xml',
xsd: 'xml',
xsl: 'xml',
plist: 'xml',
svg: 'xml',
asciidoc: 'asciidoc',
adoc: 'asciidoc',
asc: 'asciidoc',
aspectj: 'aspectj',
ahk: 'autohotkey',
ahkl: 'autohotkey',
au3: 'autoit',
avrasm: 'avrasm',
awk: 'awk',
axapta: 'axapta',
'x++': 'axapta',
bash: 'bash',
sh: 'bash',
zsh: 'bash',
b: 'basic',
bnf: 'bnf',
bf: 'brainfuck',
c: 'c',
h: 'c',
cats: 'c',
idc: 'c',
cal: 'cal',
capnproto: 'capnproto',
capnp: 'capnproto',
ceylon: 'ceylon',
clean: 'clean',
clj: 'clojure',
boot: 'clojure',
cl2: 'clojure',
cljc: 'clojure',
cljs: 'clojure',
'cljs.hl': 'clojure',
cljscm: 'clojure',
cljx: 'clojure',
hic: 'clojure',
'clojure-repl': 'clojure-repl',
cmake: 'cmake',
'cmake.in': 'cmake',
coffee: 'coffeescript',
_coffee: 'coffeescript',
cake: 'coffeescript',
cjsx: 'coffeescript',
iced: 'coffeescript',
cson: 'coffeescript',
coq: 'coq',
cos: 'cos',
cls: 'cos',
crmsh: 'crmsh',
crm: 'crmsh',
pcmk: 'crmsh',
cr: 'crystal',
cs: 'csharp',
csx: 'csharp',
csp: 'csp',
css: 'css',
d: 'd',
di: 'd',
md: 'markdown',
markdown: 'markdown',
mdown: 'markdown',
mdwn: 'markdown',
mkd: 'markdown',
mkdn: 'markdown',
mkdown: 'markdown',
ronn: 'markdown',
workbook: 'markdown',
dart: 'dart',
dpr: 'delphi',
dfm: 'delphi',
pas: 'delphi',
pascal: 'delphi',
diff: 'diff',
patch: 'diff',
django: 'django',
jinja: 'django',
dns: 'dns',
zone: 'dns',
bind: 'dns',
dockerfile: 'dockerfile',
docker: 'dockerfile',
dos: 'dos',
bat: 'dos',
cmd: 'dos',
dsconfig: 'dsconfig',
dts: 'dts',
dust: 'dust',
dst: 'dust',
ebnf: 'ebnf',
ex: 'elixir',
exs: 'elixir',
elm: 'elm',
rb: 'ruby',
builder: 'ruby',
eye: 'ruby',
gemspec: 'ruby',
god: 'ruby',
jbuilder: 'ruby',
mspec: 'ruby',
pluginspec: 'ruby',
podspec: 'ruby',
rabl: 'ruby',
rake: 'ruby',
rbuild: 'ruby',
rbw: 'ruby',
rbx: 'ruby',
ru: 'ruby',
ruby: 'ruby',
spec: 'ruby',
thor: 'ruby',
watchr: 'ruby',
erb: 'erb',
'erlang-repl': 'erlang-repl',
erl: 'erlang',
'app.src': 'erlang',
escript: 'erlang',
hrl: 'erlang',
xrl: 'erlang',
yrl: 'erlang',
excel: 'excel',
xls: 'excel',
xlsx: 'excel',
fix: 'fix',
flix: 'flix',
f90: 'fortran',
f: 'fortran',
f03: 'fortran',
f08: 'fortran',
f77: 'fortran',
f95: 'fortran',
for: 'fortran',
fpp: 'fortran',
fs: 'fsharp',
fsx: 'fsharp',
gams: 'gams',
gms: 'gams',
gauss: 'gauss',
gss: 'gauss',
gcode: 'gcode',
nc: 'gcode',
gherkin: 'gherkin',
glsl: 'glsl',
fp: 'glsl',
frag: 'glsl',
frg: 'glsl',
fsh: 'glsl',
fshader: 'glsl',
geo: 'glsl',
geom: 'glsl',
glslv: 'glsl',
gshader: 'glsl',
shader: 'glsl',
tesc: 'glsl',
tese: 'glsl',
vert: 'glsl',
vrx: 'glsl',
vsh: 'glsl',
vshader: 'glsl',
gml: 'gml',
go: 'go',
bal: 'go',
golo: 'golo',
gololang: 'golo',
gradle: 'gradle',
groovy: 'groovy',
grt: 'groovy',
gtpl: 'groovy',
gvy: 'groovy',
haml: 'haml',
'haml.deface': 'haml',
handlebars: 'handlebars',
hbs: 'handlebars',
'html.hbs': 'handlebars',
'html.handlebars': 'handlebars',
hs: 'haskell',
hsc: 'haskell',
idr: 'haskell',
purs: 'haskell',
hx: 'haxe',
hxsl: 'haxe',
hsp: 'hsp',
htmlbars: 'htmlbars',
http: 'http',
https: 'http',
hy: 'hy',
inform7: 'inform7',
i7: 'inform7',
ini: 'ini',
toml: 'ini',
cfg: 'ini',
prefs: 'ini',
// properties: 'ini',
irpf90: 'irpf90',
isbl: 'isbl',
java: 'java',
jsp: 'java',
js: 'javascript',
jsx: 'javascript',
_js: 'javascript',
bones: 'javascript',
es: 'javascript',
es6: 'javascript',
gs: 'javascript',
jake: 'javascript',
jsb: 'javascript',
jscad: 'javascript',
jsfl: 'javascript',
jsm: 'javascript',
jss: 'javascript',
mjs: 'javascript',
njs: 'javascript',
pac: 'javascript',
sjs: 'javascript',
ssjs: 'javascript',
xsjs: 'javascript',
xsjslib: 'javascript',
cfc: 'javascript',
'jboss-cli': 'jboss-cli',
json: 'json',
avsc: 'json',
geojson: 'json',
gltf: 'json',
'JSON-tmLanguage': 'json',
jsonl: 'json',
tfstate: 'json',
'tfstate.backup': 'json',
topojson: 'json',
webapp: 'json',
webmanifest: 'json',
jl: 'julia',
'julia-repl': 'julia-repl',
kt: 'kotlin',
ktm: 'kotlin',
kts: 'kotlin',
lasso: 'lasso',
// ls: 'lasso',
lassoscript: 'lasso',
tex: 'latex',
ldif: 'ldif',
leaf: 'leaf',
less: 'less',
lisp: 'lisp',
factor: 'lisp',
livecodeserver: 'livecodeserver',
ls: 'livescript',
_ls: 'livescript',
llvm: 'llvm',
lsl: 'lsl',
lua: 'lua',
nse: 'lua',
p8: 'lua',
pd_lua: 'lua',
rbxs: 'lua',
wlua: 'lua',
mak: 'makefile',
make: 'makefile',
mk: 'makefile',
mkfile: 'makefile',
mathematica: 'mathematica',
mma: 'mathematica',
wl: 'mathematica',
matlab: 'matlab',
maxima: 'maxima',
mel: 'mel',
mercury: 'mercury',
mipsasm: 'mipsasm',
miz: 'mizar',
voc: 'mizar',
al: 'perl',
cgi: 'perl',
fcgi: 'perl',
perl: 'perl',
ph: 'perl',
plx: 'perl',
pl: 'perl',
pm: 'perl',
psgi: 'perl',
t: 'perl',
mojolicious: 'mojolicious',
monkey: 'monkey',
monkey2: 'monkey',
moonscript: 'moonscript',
moon: 'moonscript',
n1ql: 'n1ql',
nginxconf: 'nginx',
nim: 'nim',
nimrod: 'nim',
nix: 'nix',
nsi: 'nsis',
nsh: 'nsis',
m: 'objectivec',
objc: 'objectivec',
mm: 'objectivec',
'obj-c': 'objectivec',
'obj-c++': 'objectivec',
'objective-c++': 'objectivec',
fun: 'ocaml',
sig: 'ocaml',
// sml: 'ocaml',
ml: 'ocaml',
mli: 'ocaml',
eliom: 'ocaml',
eliomi: 'ocaml',
ml4: 'ocaml',
mll: 'ocaml',
mly: 'ocaml',
openscad: 'openscad',
oxygene: 'oxygene',
parser3: 'parser3',
pf: 'pf',
'pf.conf': 'pf',
pgsql: 'pgsql',
postgres: 'pgsql',
postgresql: 'pgsql',
php: 'php',
aw: 'php',
ctp: 'php',
inc: 'php',
php3: 'php',
php4: 'php',
php5: 'php',
phps: 'php',
phpt: 'php',
'php-template': 'php-template',
plaintext: 'plaintext',
txt: 'plaintext',
text: 'plaintext',
pony: 'pony',
ps: 'powershell',
ps1: 'powershell',
psd1: 'powershell',
psm1: 'powershell',
pde: 'processing',
profile: 'profile',
pro: 'prolog',
prolog: 'prolog',
yap: 'prolog',
properties: 'properties',
proto: 'protobuf',
puppet: 'puppet',
pp: 'puppet',
purebasic: 'purebasic',
py: 'python',
bzl: 'python',
gyp: 'python',
gypi: 'python',
lmi: 'python',
py3: 'python',
pyde: 'python',
pyi: 'python',
pyp: 'python',
pyt: 'python',
pyw: 'python',
rpy: 'python',
tac: 'python',
wsgi: 'python',
xpy: 'python',
'python-repl': 'python-repl',
pycon: 'python-repl',
q: 'q',
k: 'q',
kdb: 'q',
qml: 'qml',
r: 'r',
rd: 'r',
rsx: 'r',
reasonml: 'reasonml',
re: 'reasonml',
rib: 'rib',
roboconf: 'roboconf',
graph: 'roboconf',
instances: 'roboconf',
routeros: 'routeros',
rsl: 'rsl',
ruleslanguage: 'ruleslanguage',
rs: 'rust',
'rs.in': 'rust',
sas: 'sas',
// pony: 'scala',
scala: 'scala',
kojo: 'scala',
sbt: 'scala',
sc: 'scala',
scm: 'scheme',
sch: 'scheme',
sld: 'scheme',
sls: 'scheme',
sps: 'scheme',
ss: 'scheme',
rkt: 'scheme',
scilab: 'scilab',
scss: 'scss',
shell: 'shell',
smali: 'smali',
st: 'smalltalk',
sml: 'sml',
sqf: 'sqf',
sql: 'sql',
cql: 'sql',
ddl: 'sql',
mysql: 'sql',
prc: 'sql',
tab: 'sql',
udf: 'sql',
viw: 'sql',
stan: 'stan',
stanfuncs: 'stan',
stata: 'stata',
step21: 'step21',
step: 'step21',
stp: 'step21',
styl: 'stylus',
subunit: 'subunit',
swift: 'swift',
taggerscript: 'taggerscript',
yml: 'yaml',
mir: 'yaml',
reek: 'yaml',
rviz: 'yaml',
'sublime-syntax': 'yaml',
syntax: 'yaml',
yaml: 'yaml',
'yaml-tmlanguage': 'yaml',
'yml.mysql': 'yaml',
tap: 'tap',
tcl: 'tcl',
adp: 'tcl',
tm: 'tcl',
thrift: 'thrift',
tp: 'tp',
twig: 'twig',
craftcms: 'twig',
ts: 'typescript',
tsx: 'typescript',
vala: 'vala',
vbnet: 'vbnet',
vb: 'vbnet',
vbscript: 'vbscript',
vbs: 'vbscript',
'vbscript-html': 'vbscript-html',
v: 'verilog',
veo: 'verilog',
vhdl: 'vhdl',
vhd: 'vhdl',
vhf: 'vhdl',
vhi: 'vhdl',
vho: 'vhdl',
vhs: 'vhdl',
vht: 'vhdl',
vhw: 'vhdl',
vim: 'vim',
x86asm: 'x86asm',
xl: 'xl',
xquery: 'xquery',
xpath: 'xquery',
xq: 'xquery',
zephir: 'zephir',
zep: 'zephir',
};
export function getLanguage(fileExtension: string): string {
return languagesToExt[fileExtension] ?? 'plaintext';
}

View file

@ -0,0 +1,139 @@
/*
*
* highlight.js
* Author: isagalaev
*
*/
(function() {
function HighlightJS() {
}
/*
* Copied from Highlight.js Private API
* Will be removed when this part of the API is exposed
*/
/* Utility functions */
function escape(value) {
return value.replace(/&/gm, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}
function tag(node) {
return node.nodeName.toLowerCase();
}
/* Stream merging */
HighlightJS.prototype.nodeStream = function(node) {
var result = [];
(function _nodeStream(node, offset) {
for (var child = node.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 3)
offset += child.nodeValue.length;
else if (child.nodeType == 1) {
result.push({
event: 'start',
offset: offset,
node: child
});
offset = _nodeStream(child, offset);
// Prevent void elements from having an end tag that would actually
// double them in the output. There are more void elements in HTML
// but we list only those realistically expected in code display.
if (!tag(child).match(/br|hr|img|input/)) {
result.push({
event: 'stop',
offset: offset,
node: child
});
}
}
}
return offset;
})(node, 0);
return result;
};
HighlightJS.prototype.mergeStreams = function(original, highlighted, value) {
var processed = 0;
var result = '';
var nodeStack = [];
function selectStream() {
if (!original.length || !highlighted.length) {
return original.length ? original : highlighted;
}
if (original[0].offset != highlighted[0].offset) {
return (original[0].offset < highlighted[0].offset) ? original : highlighted;
}
/*
To avoid starting the stream just before it should stop the order is
ensured that original always starts first and closes last:
if (event1 == 'start' && event2 == 'start')
return original;
if (event1 == 'start' && event2 == 'stop')
return highlighted;
if (event1 == 'stop' && event2 == 'start')
return original;
if (event1 == 'stop' && event2 == 'stop')
return highlighted;
... which is collapsed to:
*/
return highlighted[0].event == 'start' ? original : highlighted;
}
function open(node) {
function attrStr(a) {
return ' ' + a.nodeName + '="' + escape(a.value) + '"';
}
result += '<' + tag(node) + Array.prototype.map.call(node.attributes, attrStr).join('') + '>';
}
function close(node) {
result += '</' + tag(node) + '>';
}
function render(event) {
(event.event == 'start' ? open : close)(event.node);
}
while (original.length || highlighted.length) {
var stream = selectStream();
result += escape(value.substr(processed, stream[0].offset - processed));
processed = stream[0].offset;
if (stream == original) {
/*
On any opening or closing tag of the original markup we first close
the entire highlighted node stack, then render the original tag along
with all the following original tags at the same offset and then
reopen all the tags on the highlighted stack.
*/
nodeStack.reverse().forEach(close);
do {
render(stream.splice(0, 1)[0]);
stream = selectStream();
} while (stream == original && stream.length && stream[0].offset == processed);
nodeStack.reverse().forEach(open);
} else {
if (stream[0].event == 'start') {
nodeStack.push(stream[0].node);
} else {
nodeStack.pop();
}
render(stream.splice(0, 1)[0]);
}
}
return result + escape(value.substr(processed));
};
/* **** Highlight.js Private API **** */
module.exports.HighlightJS = new HighlightJS();
})();

View file

@ -1,391 +0,0 @@
// Require the highlight.js library without languages
import highlightJS from 'highlight.js/lib/core';
// Convert to imports
// ^hljs\.registerLanguage\('(.+)', require\('\./languages\/(.+)'\)\);$
// import $1 from 'highlight.js/lib/languages/$2';
// import _1c from 'highlight.js/lib/languages/1c';
// import abnf from 'highlight.js/lib/languages/abnf';
// import accesslog from 'highlight.js/lib/languages/accesslog';
// import actionscript from 'highlight.js/lib/languages/actionscript';
// import ada from 'highlight.js/lib/languages/ada';
// import angelscript from 'highlight.js/lib/languages/angelscript';
// import apache from 'highlight.js/lib/languages/apache';
// import applescript from 'highlight.js/lib/languages/applescript';
// import arcade from 'highlight.js/lib/languages/arcade';
import cpp from 'highlight.js/lib/languages/cpp';
// import arduino from 'highlight.js/lib/languages/arduino';
// import armasm from 'highlight.js/lib/languages/armasm';
import xml from 'highlight.js/lib/languages/xml';
// import asciidoc from 'highlight.js/lib/languages/asciidoc';
// import aspectj from 'highlight.js/lib/languages/aspectj';
// import autohotkey from 'highlight.js/lib/languages/autohotkey';
// import autoit from 'highlight.js/lib/languages/autoit';
// import avrasm from 'highlight.js/lib/languages/avrasm';
import awk from 'highlight.js/lib/languages/awk';
// import axapta from 'highlight.js/lib/languages/axapta';
import bash from 'highlight.js/lib/languages/bash';
// import basic from 'highlight.js/lib/languages/basic';
// import bnf from 'highlight.js/lib/languages/bnf';
// import brainfuck from 'highlight.js/lib/languages/brainfuck';
import c from 'highlight.js/lib/languages/c';
// import cal from 'highlight.js/lib/languages/cal';
// import capnproto from 'highlight.js/lib/languages/capnproto';
// import ceylon from 'highlight.js/lib/languages/ceylon';
// import clean from 'highlight.js/lib/languages/clean';
import clojure from 'highlight.js/lib/languages/clojure';
// import clojureRepl from 'highlight.js/lib/languages/clojure-repl';
// import cmake from 'highlight.js/lib/languages/cmake';
// import coffeescript from 'highlight.js/lib/languages/coffeescript';
// import coq from 'highlight.js/lib/languages/coq';
// import cos from 'highlight.js/lib/languages/cos';
// import crmsh from 'highlight.js/lib/languages/crmsh';
import crystal from 'highlight.js/lib/languages/crystal';
import csharp from 'highlight.js/lib/languages/csharp';
import csp from 'highlight.js/lib/languages/csp';
import css from 'highlight.js/lib/languages/css';
// import d from 'highlight.js/lib/languages/d';
import markdown from 'highlight.js/lib/languages/markdown';
import dart from 'highlight.js/lib/languages/dart';
// import delphi from 'highlight.js/lib/languages/delphi';
import diff from 'highlight.js/lib/languages/diff';
// import django from 'highlight.js/lib/languages/django';
// import dns from 'highlight.js/lib/languages/dns';
import dockerfile from 'highlight.js/lib/languages/dockerfile';
// import dos from 'highlight.js/lib/languages/dos';
// import dsconfig from 'highlight.js/lib/languages/dsconfig';
// import dts from 'highlight.js/lib/languages/dts';
// import dust from 'highlight.js/lib/languages/dust';
// import ebnf from 'highlight.js/lib/languages/ebnf';
import elixir from 'highlight.js/lib/languages/elixir';
import elm from 'highlight.js/lib/languages/elm';
import ruby from 'highlight.js/lib/languages/ruby';
// import erb from 'highlight.js/lib/languages/erb';
// import erlangRepl from 'highlight.js/lib/languages/erlang-repl';
import erlang from 'highlight.js/lib/languages/erlang';
// import excel from 'highlight.js/lib/languages/excel';
// import fix from 'highlight.js/lib/languages/fix';
// import flix from 'highlight.js/lib/languages/flix';
// import fortran from 'highlight.js/lib/languages/fortran';
import fsharp from 'highlight.js/lib/languages/fsharp';
// import gams from 'highlight.js/lib/languages/gams';
// import gauss from 'highlight.js/lib/languages/gauss';
// import gcode from 'highlight.js/lib/languages/gcode';
// import gherkin from 'highlight.js/lib/languages/gherkin';
// import glsl from 'highlight.js/lib/languages/glsl';
// import gml from 'highlight.js/lib/languages/gml';
import go from 'highlight.js/lib/languages/go';
// import golo from 'highlight.js/lib/languages/golo';
import gradle from 'highlight.js/lib/languages/gradle';
import groovy from 'highlight.js/lib/languages/groovy';
// import haml from 'highlight.js/lib/languages/haml';
import handlebars from 'highlight.js/lib/languages/handlebars';
import haskell from 'highlight.js/lib/languages/haskell';
// import haxe from 'highlight.js/lib/languages/haxe';
// import hsp from 'highlight.js/lib/languages/hsp';
// import htmlbars from 'highlight.js/lib/languages/htmlbars';
// import http from 'highlight.js/lib/languages/http';
// import hy from 'highlight.js/lib/languages/hy';
// import inform7 from 'highlight.js/lib/languages/inform7';
import ini from 'highlight.js/lib/languages/ini';
// import irpf90 from 'highlight.js/lib/languages/irpf90';
// import isbl from 'highlight.js/lib/languages/isbl';
import java from 'highlight.js/lib/languages/java';
import javascript from 'highlight.js/lib/languages/javascript';
// import jbossCli from 'highlight.js/lib/languages/jboss-cli';
import json from 'highlight.js/lib/languages/json';
// import julia from 'highlight.js/lib/languages/julia';
// import juliaRepl from 'highlight.js/lib/languages/julia-repl';
import kotlin from 'highlight.js/lib/languages/kotlin';
// import lasso from 'highlight.js/lib/languages/lasso';
// import latex from 'highlight.js/lib/languages/latex';
// import ldif from 'highlight.js/lib/languages/ldif';
// import leaf from 'highlight.js/lib/languages/leaf';
import less from 'highlight.js/lib/languages/less';
import lisp from 'highlight.js/lib/languages/lisp';
// import livecodeserver from 'highlight.js/lib/languages/livecodeserver';
// import livescript from 'highlight.js/lib/languages/livescript';
// import llvm from 'highlight.js/lib/languages/llvm';
// import lsl from 'highlight.js/lib/languages/lsl';
import lua from 'highlight.js/lib/languages/lua';
import makefile from 'highlight.js/lib/languages/makefile';
// import mathematica from 'highlight.js/lib/languages/mathematica';
// import matlab from 'highlight.js/lib/languages/matlab';
// import maxima from 'highlight.js/lib/languages/maxima';
// import mel from 'highlight.js/lib/languages/mel';
// import mercury from 'highlight.js/lib/languages/mercury';
// import mipsasm from 'highlight.js/lib/languages/mipsasm';
// import mizar from 'highlight.js/lib/languages/mizar';
import perl from 'highlight.js/lib/languages/perl';
// import mojolicious from 'highlight.js/lib/languages/mojolicious';
// import monkey from 'highlight.js/lib/languages/monkey';
// import moonscript from 'highlight.js/lib/languages/moonscript';
// import n1ql from 'highlight.js/lib/languages/n1ql';
import nginx from 'highlight.js/lib/languages/nginx';
// import nim from 'highlight.js/lib/languages/nim';
// import nix from 'highlight.js/lib/languages/nix';
// import nsis from 'highlight.js/lib/languages/nsis';
import objectivec from 'highlight.js/lib/languages/objectivec';
// import ocaml from 'highlight.js/lib/languages/ocaml';
// import openscad from 'highlight.js/lib/languages/openscad';
// import oxygene from 'highlight.js/lib/languages/oxygene';
// import parser3 from 'highlight.js/lib/languages/parser3';
// import pf from 'highlight.js/lib/languages/pf';
import pgsql from 'highlight.js/lib/languages/pgsql';
import php from 'highlight.js/lib/languages/php';
// import phpTemplate from 'highlight.js/lib/languages/php-template';
import plaintext from 'highlight.js/lib/languages/plaintext';
// import pony from 'highlight.js/lib/languages/pony';
import powershell from 'highlight.js/lib/languages/powershell';
// import processing from 'highlight.js/lib/languages/processing';
// import profile from 'highlight.js/lib/languages/profile';
// import prolog from 'highlight.js/lib/languages/prolog';
import properties from 'highlight.js/lib/languages/properties';
import protobuf from 'highlight.js/lib/languages/protobuf';
// import puppet from 'highlight.js/lib/languages/puppet';
// import purebasic from 'highlight.js/lib/languages/purebasic';
import python from 'highlight.js/lib/languages/python';
// import pythonRepl from 'highlight.js/lib/languages/python-repl';
// import q from 'highlight.js/lib/languages/q';
// import qml from 'highlight.js/lib/languages/qml';
// import r from 'highlight.js/lib/languages/r';
// import reasonml from 'highlight.js/lib/languages/reasonml';
// import rib from 'highlight.js/lib/languages/rib';
// import roboconf from 'highlight.js/lib/languages/roboconf';
// import routeros from 'highlight.js/lib/languages/routeros';
// import rsl from 'highlight.js/lib/languages/rsl';
// import ruleslanguage from 'highlight.js/lib/languages/ruleslanguage';
import rust from 'highlight.js/lib/languages/rust';
// import sas from 'highlight.js/lib/languages/sas';
import scala from 'highlight.js/lib/languages/scala';
// import scheme from 'highlight.js/lib/languages/scheme';
// import scilab from 'highlight.js/lib/languages/scilab';
import scss from 'highlight.js/lib/languages/scss';
import shell from 'highlight.js/lib/languages/shell';
// import smali from 'highlight.js/lib/languages/smali';
// import smalltalk from 'highlight.js/lib/languages/smalltalk';
// import sml from 'highlight.js/lib/languages/sml';
// import sqf from 'highlight.js/lib/languages/sqf';
import sql from 'highlight.js/lib/languages/sql';
// import stan from 'highlight.js/lib/languages/stan';
// import stata from 'highlight.js/lib/languages/stata';
// import step21 from 'highlight.js/lib/languages/step21';
// import stylus from 'highlight.js/lib/languages/stylus';
// import subunit from 'highlight.js/lib/languages/subunit';
import swift from 'highlight.js/lib/languages/swift';
// import taggerscript from 'highlight.js/lib/languages/taggerscript';
import yaml from 'highlight.js/lib/languages/yaml';
// import tap from 'highlight.js/lib/languages/tap';
// import tcl from 'highlight.js/lib/languages/tcl';
// import thrift from 'highlight.js/lib/languages/thrift';
// import tp from 'highlight.js/lib/languages/tp';
// import twig from 'highlight.js/lib/languages/twig';
import typescript from 'highlight.js/lib/languages/typescript';
// import vala from 'highlight.js/lib/languages/vala';
// import vbnet from 'highlight.js/lib/languages/vbnet';
// import vbscript from 'highlight.js/lib/languages/vbscript';
// import vbscriptHtml from 'highlight.js/lib/languages/vbscript-html';
// import verilog from 'highlight.js/lib/languages/verilog';
// import vhdl from 'highlight.js/lib/languages/vhdl';
// import vim from 'highlight.js/lib/languages/vim';
// import x86asm from 'highlight.js/lib/languages/x86asm';
// import xl from 'highlight.js/lib/languages/xl';
// import xquery from 'highlight.js/lib/languages/xquery';
// import zephir from 'highlight.js/lib/languages/zephir';
// Convert to registerLanguage
// ^hljs\.registerLanguage\('(.+)', require\('\./languages\/(.+)'\)\);$
// highlightJS.registerLanguage('$1', $1);
// Separately require languages
// highlightJS.registerLanguage('1c', _1c);
// highlightJS.registerLanguage('abnf', abnf);
// highlightJS.registerLanguage('accesslog', accesslog);
// highlightJS.registerLanguage('actionscript', actionscript);
// highlightJS.registerLanguage('ada', ada);
// highlightJS.registerLanguage('angelscript', angelscript);
// highlightJS.registerLanguage('apache', apache);
// highlightJS.registerLanguage('applescript', applescript);
// highlightJS.registerLanguage('arcade', arcade);
highlightJS.registerLanguage('cpp', cpp);
// highlightJS.registerLanguage('arduino', arduino);
// highlightJS.registerLanguage('armasm', armasm);
highlightJS.registerLanguage('xml', xml);
// highlightJS.registerLanguage('asciidoc', asciidoc);
// highlightJS.registerLanguage('aspectj', aspectj);
// highlightJS.registerLanguage('autohotkey', autohotkey);
// highlightJS.registerLanguage('autoit', autoit);
// highlightJS.registerLanguage('avrasm', avrasm);
highlightJS.registerLanguage('awk', awk);
// highlightJS.registerLanguage('axapta', axapta);
highlightJS.registerLanguage('bash', bash);
// highlightJS.registerLanguage('basic', basic);
// highlightJS.registerLanguage('bnf', bnf);
// highlightJS.registerLanguage('brainfuck', brainfuck);
highlightJS.registerLanguage('c', c);
// highlightJS.registerLanguage('cal', cal);
// highlightJS.registerLanguage('capnproto', capnproto);
// highlightJS.registerLanguage('ceylon', ceylon);
// highlightJS.registerLanguage('clean', clean);
highlightJS.registerLanguage('clojure', clojure);
// highlightJS.registerLanguage('clojure-repl', clojureRepl);
// highlightJS.registerLanguage('cmake', cmake);
// highlightJS.registerLanguage('coffeescript', coffeescript);
// highlightJS.registerLanguage('coq', coq);
// highlightJS.registerLanguage('cos', cos);
// highlightJS.registerLanguage('crmsh', crmsh);
highlightJS.registerLanguage('crystal', crystal);
highlightJS.registerLanguage('csharp', csharp);
highlightJS.registerLanguage('csp', csp);
highlightJS.registerLanguage('css', css);
// highlightJS.registerLanguage('d', d);
highlightJS.registerLanguage('markdown', markdown);
highlightJS.registerLanguage('dart', dart);
// highlightJS.registerLanguage('delphi', delphi);
highlightJS.registerLanguage('diff', diff);
// highlightJS.registerLanguage('django', django);
// highlightJS.registerLanguage('dns', dns);
highlightJS.registerLanguage('dockerfile', dockerfile);
// highlightJS.registerLanguage('dos', dos);
// highlightJS.registerLanguage('dsconfig', dsconfig);
// highlightJS.registerLanguage('dts', dts);
// highlightJS.registerLanguage('dust', dust);
// highlightJS.registerLanguage('ebnf', ebnf);
highlightJS.registerLanguage('elixir', elixir);
highlightJS.registerLanguage('elm', elm);
highlightJS.registerLanguage('ruby', ruby);
// highlightJS.registerLanguage('erb', erb);
// highlightJS.registerLanguage('erlang-repl', erlangRepl);
highlightJS.registerLanguage('erlang', erlang);
// highlightJS.registerLanguage('excel', excel);
// highlightJS.registerLanguage('fix', fix);
// highlightJS.registerLanguage('flix', flix);
// highlightJS.registerLanguage('fortran', fortran);
highlightJS.registerLanguage('fsharp', fsharp);
// highlightJS.registerLanguage('gams', gams);
// highlightJS.registerLanguage('gauss', gauss);
// highlightJS.registerLanguage('gcode', gcode);
// highlightJS.registerLanguage('gherkin', gherkin);
// highlightJS.registerLanguage('glsl', glsl);
// highlightJS.registerLanguage('gml', gml);
highlightJS.registerLanguage('go', go);
// highlightJS.registerLanguage('golo', golo);
highlightJS.registerLanguage('gradle', gradle);
highlightJS.registerLanguage('groovy', groovy);
// highlightJS.registerLanguage('haml', haml);
highlightJS.registerLanguage('handlebars', handlebars);
highlightJS.registerLanguage('haskell', haskell);
// highlightJS.registerLanguage('haxe', haxe);
// highlightJS.registerLanguage('hsp', hsp);
// highlightJS.registerLanguage('htmlbars', htmlbars);
// highlightJS.registerLanguage('http', http);
// highlightJS.registerLanguage('hy', hy);
// highlightJS.registerLanguage('inform7', inform7);
highlightJS.registerLanguage('ini', ini);
// highlightJS.registerLanguage('irpf90', irpf90);
// highlightJS.registerLanguage('isbl', isbl);
highlightJS.registerLanguage('java', java);
highlightJS.registerLanguage('javascript', javascript);
// highlightJS.registerLanguage('jboss-cli', jbossCli);
highlightJS.registerLanguage('json', json);
// highlightJS.registerLanguage('julia', julia);
// highlightJS.registerLanguage('julia-repl', juliaRepl);
highlightJS.registerLanguage('kotlin', kotlin);
// highlightJS.registerLanguage('lasso', lasso);
// highlightJS.registerLanguage('latex', latex);
// highlightJS.registerLanguage('ldif', ldif);
// highlightJS.registerLanguage('leaf', leaf);
highlightJS.registerLanguage('less', less);
highlightJS.registerLanguage('lisp', lisp);
// highlightJS.registerLanguage('livecodeserver', livecodeserver);
// highlightJS.registerLanguage('livescript', livescript);
// highlightJS.registerLanguage('llvm', llvm);
// highlightJS.registerLanguage('lsl', lsl);
highlightJS.registerLanguage('lua', lua);
highlightJS.registerLanguage('makefile', makefile);
// highlightJS.registerLanguage('mathematica', mathematica);
// highlightJS.registerLanguage('matlab', matlab);
// highlightJS.registerLanguage('maxima', maxima);
// highlightJS.registerLanguage('mel', mel);
// highlightJS.registerLanguage('mercury', mercury);
// highlightJS.registerLanguage('mipsasm', mipsasm);
// highlightJS.registerLanguage('mizar', mizar);
highlightJS.registerLanguage('perl', perl);
// highlightJS.registerLanguage('mojolicious', mojolicious);
// highlightJS.registerLanguage('monkey', monkey);
// highlightJS.registerLanguage('moonscript', moonscript);
// highlightJS.registerLanguage('n1ql', n1ql);
highlightJS.registerLanguage('nginx', nginx);
// highlightJS.registerLanguage('nim', nim);
// highlightJS.registerLanguage('nix', nix);
// highlightJS.registerLanguage('nsis', nsis);
highlightJS.registerLanguage('objectivec', objectivec);
// highlightJS.registerLanguage('ocaml', ocaml);
// highlightJS.registerLanguage('openscad', openscad);
// highlightJS.registerLanguage('oxygene', oxygene);
// highlightJS.registerLanguage('parser3', parser3);
// highlightJS.registerLanguage('pf', pf);
highlightJS.registerLanguage('pgsql', pgsql);
highlightJS.registerLanguage('php', php);
// highlightJS.registerLanguage('php-template', phpTemplate);
highlightJS.registerLanguage('plaintext', plaintext);
// highlightJS.registerLanguage('pony', pony);
highlightJS.registerLanguage('powershell', powershell);
// highlightJS.registerLanguage('processing', processing);
// highlightJS.registerLanguage('profile', profile);
// highlightJS.registerLanguage('prolog', prolog);
highlightJS.registerLanguage('properties', properties);
highlightJS.registerLanguage('protobuf', protobuf);
// highlightJS.registerLanguage('puppet', puppet);
// highlightJS.registerLanguage('purebasic', purebasic);
highlightJS.registerLanguage('python', python);
// highlightJS.registerLanguage('python-repl', pythonRepl);
// highlightJS.registerLanguage('q', q);
// highlightJS.registerLanguage('qml', qml);
// highlightJS.registerLanguage('r', r);
// highlightJS.registerLanguage('reasonml', reasonml);
// highlightJS.registerLanguage('rib', rib);
// highlightJS.registerLanguage('roboconf', roboconf);
// highlightJS.registerLanguage('routeros', routeros);
// highlightJS.registerLanguage('rsl', rsl);
// highlightJS.registerLanguage('ruleslanguage', ruleslanguage);
highlightJS.registerLanguage('rust', rust);
// highlightJS.registerLanguage('sas', sas);
highlightJS.registerLanguage('scala', scala);
// highlightJS.registerLanguage('scheme', scheme);
// highlightJS.registerLanguage('scilab', scilab);
highlightJS.registerLanguage('scss', scss);
highlightJS.registerLanguage('shell', shell);
// highlightJS.registerLanguage('smali', smali);
// highlightJS.registerLanguage('smalltalk', smalltalk);
// highlightJS.registerLanguage('sml', sml);
// highlightJS.registerLanguage('sqf', sqf);
highlightJS.registerLanguage('sql', sql);
// highlightJS.registerLanguage('stan', stan);
// highlightJS.registerLanguage('stata', stata);
// highlightJS.registerLanguage('step21', step21);
// highlightJS.registerLanguage('stylus', stylus);
// highlightJS.registerLanguage('subunit', subunit);
highlightJS.registerLanguage('swift', swift);
// highlightJS.registerLanguage('taggerscript', taggerscript);
highlightJS.registerLanguage('yaml', yaml);
// highlightJS.registerLanguage('tap', tap);
// highlightJS.registerLanguage('tcl', tcl);
// highlightJS.registerLanguage('thrift', thrift);
// highlightJS.registerLanguage('tp', tp);
// highlightJS.registerLanguage('twig', twig);
highlightJS.registerLanguage('typescript', typescript);
// highlightJS.registerLanguage('vala', vala);
// highlightJS.registerLanguage('vbnet', vbnet);
// highlightJS.registerLanguage('vbscript', vbscript);
// highlightJS.registerLanguage('vbscript-html', vbscriptHtml);
// highlightJS.registerLanguage('verilog', verilog);
// highlightJS.registerLanguage('vhdl', vhdl);
// highlightJS.registerLanguage('vim', vim);
// highlightJS.registerLanguage('x86asm', x86asm);
// highlightJS.registerLanguage('xl', xl);
// highlightJS.registerLanguage('xquery', xquery);
// highlightJS.registerLanguage('zephir', zephir);
export const hljs = highlightJS;

46
src/utils.js Normal file
View file

@ -0,0 +1,46 @@
/*
*
* Utils (utils.js)
* Author: rtfpessoa
*
*/
(function() {
function Utils() {
}
Utils.prototype.convertWhiteSpaceToNonBreakingSpace = function(str) {
return str.slice(0).replace(/ /g, '&nbsp;');
};
Utils.prototype.escape = function(str) {
return str.slice(0)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\t/g, ' ');
};
Utils.prototype.startsWith = function(str, start) {
if (typeof start === 'object') {
var result = false;
start.forEach(function(s) {
if (str.indexOf(s) === 0) {
result = true;
}
});
return result;
}
return str.indexOf(start) === 0;
};
Utils.prototype.valueOrEmpty = function(value) {
return value ? value : '';
};
module.exports.Utils = new Utils();
})();

View file

@ -1,54 +0,0 @@
const specials = [
// Order matters for these
'-',
'[',
']',
// Order doesn't matter for any of these
'/',
'{',
'}',
'(',
')',
'*',
'+',
'?',
'.',
'\\',
'^',
'$',
'|',
];
// All characters will be escaped with '\'
// even though only some strictly require it when inside of []
const regex = RegExp('[' + specials.join('\\') + ']', 'g');
/**
* Escapes all required characters for safe usage inside a RegExp
*/
export function escapeForRegExp(str: string): string {
return str.replace(regex, '\\$&');
}
/**
* Converts all '\' in @path to unix style '/'
*/
export function unifyPath(path: string): string {
return path ? path.replace(/\\/g, '/') : path;
}
/**
* Create unique number identifier for @text
*/
export function hashCode(text: string): number {
let i, chr, len;
let hash = 0;
for (i = 0, len = text.length; i < len; i++) {
chr = text.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}

View file

@ -1,217 +0,0 @@
# Inspired by https://gist.github.com/danihodovic/a51eb0d9d4b29649c2d094f4251827dd
provider "aws" {
profile = "${var.aws_profile}"
region = "${var.aws_region}"
}
provider "aws" {
alias = "nvirginia"
profile = "${var.aws_profile}"
region = "us-east-1"
}
terraform {
backend "s3" {
region = "us-east-1"
encrypt = true
bucket = "terraform-state-bucket.rtfpessoa.xyz"
dynamodb_table = "terraform-state-table"
key = "diff2html.xyz"
}
}
resource "aws_acm_certificate" "cert" {
provider = "aws.nvirginia"
domain_name = "${var.domain}"
subject_alternative_names = ["*.${var.domain}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "root_domain" {
zone_id = "${var.hosted_zone_id}"
name = "${var.domain}"
type = "A"
alias {
name = "${aws_cloudfront_distribution.cdn.domain_name}"
zone_id = "${aws_cloudfront_distribution.cdn.hosted_zone_id}"
evaluate_target_health = false
}
}
resource "aws_route53_record" "www_domain" {
zone_id = "${var.hosted_zone_id}"
name = "${local.www_domain}"
type = "A"
alias {
name = "${aws_cloudfront_distribution.www_cdn.domain_name}"
zone_id = "${aws_cloudfront_distribution.www_cdn.hosted_zone_id}"
evaluate_target_health = false
}
}
resource "aws_route53_record" "cert_validation" {
zone_id = "${var.hosted_zone_id}"
name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}"
type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}"
records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"]
ttl = 60
}
resource "aws_acm_certificate_validation" "cert" {
provider = "aws.nvirginia"
certificate_arn = "${aws_acm_certificate.cert.arn}"
validation_record_fqdns = ["${aws_route53_record.cert_validation.fqdn}"]
}
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
comment = "${var.domain} origin access identity"
}
locals {
s3_origin_id = "S3-${var.domain}"
s3_www_origin_id = "S3-www-${var.domain}"
www_domain = "www.${var.domain}"
}
resource "aws_s3_bucket" "site" {
bucket = "${var.domain}"
acl = "private"
policy = <<EOF
{
"Version": "2008-10-17",
"Statement": [{
"Sid": "AllowCloudFrontRead",
"Effect": "Allow",
"Principal": { "AWS": "${aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn}" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::${var.domain}/*"
}]
}
EOF
}
resource "aws_cloudfront_distribution" "cdn" {
origin {
domain_name = "${aws_s3_bucket.site.bucket_regional_domain_name}"
origin_id = "${local.s3_origin_id}"
s3_origin_config {
origin_access_identity = "${aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path}"
}
}
# If using route53 aliases for DNS we need to declare it here too, otherwise we'll get 403s.
aliases = ["${var.domain}"]
enabled = true
is_ipv6_enabled = true
default_root_object = "index.html"
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "${local.s3_origin_id}"
forwarded_values {
query_string = true
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
compress = true
viewer_protocol_policy = "redirect-to-https"
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
viewer_certificate {
acm_certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}"
minimum_protocol_version = "TLSv1.1_2016"
ssl_support_method = "sni-only"
}
}
resource "aws_s3_bucket" "www_site" {
bucket = "${local.www_domain}"
acl = "public-read"
website {
redirect_all_requests_to = "https://${var.domain}"
}
}
resource "aws_cloudfront_distribution" "www_cdn" {
origin {
origin_id = "${local.s3_www_origin_id}"
domain_name = "${aws_s3_bucket.www_site.website_endpoint}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "http-only"
origin_ssl_protocols = ["TLSv1.1", "TLSv1.2"]
}
}
# If using route53 aliases for DNS we need to declare it here too, otherwise we'll get 403s.
aliases = ["${local.www_domain}"]
enabled = true
is_ipv6_enabled = true
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "${local.s3_www_origin_id}"
forwarded_values {
query_string = true
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
compress = true
viewer_protocol_policy = "redirect-to-https"
}
price_class = "PriceClass_All"
restrictions {
geo_restriction {
restriction_type = "none"
locations = []
}
}
viewer_certificate {
acm_certificate_arn = "${aws_acm_certificate_validation.cert.certificate_arn}"
minimum_protocol_version = "TLSv1.1_2016"
ssl_support_method = "sni-only"
}
}

View file

@ -1,7 +0,0 @@
output "route53_domain" {
value = "${aws_route53_record.root_domain.fqdn}"
}
output "cdn_domain" {
value = "${aws_cloudfront_distribution.cdn.domain_name}"
}

View file

@ -1,19 +0,0 @@
variable "aws_region" {
description = "The aws region to deploy"
default = "eu-west-1"
}
variable "aws_profile" {
description = "The aws profile to use"
default = "personal"
}
variable "domain" {
description = "The domain to deploy this page"
default = "diff2html.xyz"
}
variable "hosted_zone_id" {
description = "The hosted zone id where the domain will be created"
default = "Z2T76N7UKY0XQI"
}

428
test/diff-parser-tests.js Normal file
View file

@ -0,0 +1,428 @@
var assert = require('assert');
var DiffParser = require('../src/diff-parser.js').DiffParser;
describe('DiffParser', function() {
describe('generateDiffJson', function() {
it('should parse unix with \n diff', function() {
var diff =
'diff --git a/sample b/sample\n' +
'index 0000001..0ddf2ba\n' +
'--- a/sample\n' +
'+++ b/sample\n' +
'@@ -1 +1 @@\n' +
'-test\n' +
'+test1r\n';
checkDiffSample(diff)
});
it('should parse windows with \r\n diff', function() {
var diff =
'diff --git a/sample b/sample\r\n' +
'index 0000001..0ddf2ba\r\n' +
'--- a/sample\r\n' +
'+++ b/sample\r\n' +
'@@ -1 +1 @@\r\n' +
'-test\r\n' +
'+test1r\r\n';
checkDiffSample(diff)
});
it('should parse old os x with \r diff', function() {
var diff =
'diff --git a/sample b/sample\r' +
'index 0000001..0ddf2ba\r' +
'--- a/sample\r' +
'+++ b/sample\r' +
'@@ -1 +1 @@\r' +
'-test\r' +
'+test1r\r';
checkDiffSample(diff)
});
it('should parse mixed eols diff', function() {
var diff =
'diff --git a/sample b/sample\n' +
'index 0000001..0ddf2ba\r\n' +
'--- a/sample\r' +
'+++ b/sample\r\n' +
'@@ -1 +1 @@\n' +
'-test\r' +
'+test1r\n';
checkDiffSample(diff)
});
function checkDiffSample(diff) {
var result = DiffParser.generateDiffJson(diff);
var file1 = result[0];
assert.equal(1, result.length);
assert.equal(1, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('sample', file1.oldName);
assert.equal('sample', file1.newName);
assert.equal(1, file1.blocks.length);
}
it('should parse diff with special characters', function() {
var diff =
'diff --git "a/bla with \ttab.scala" "b/bla with \ttab.scala"\n' +
'index 4c679d7..e9bd385 100644\n' +
'--- "a/bla with \ttab.scala"\n' +
'+++ "b/bla with \ttab.scala"\n' +
'@@ -1 +1,2 @@\n' +
'-cenas\n' +
'+cenas com ananas\n' +
'+bananas';
var result = DiffParser.generateDiffJson(diff);
var file1 = result[0];
assert.equal(1, result.length);
assert.equal(2, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('bla with \ttab.scala', file1.oldName);
assert.equal('bla with \ttab.scala', file1.newName);
assert.equal(1, file1.blocks.length);
});
it('should parse diff with prefix', function() {
var diff =
'diff --git "\tbla with \ttab.scala" "\tbla with \ttab.scala"\n' +
'index 4c679d7..e9bd385 100644\n' +
'--- "\tbla with \ttab.scala"\n' +
'+++ "\tbla with \ttab.scala"\n' +
'@@ -1 +1,2 @@\n' +
'-cenas\n' +
'+cenas com ananas\n' +
'+bananas';
var result = DiffParser.generateDiffJson(diff, {"srcPrefix": "\t", "dstPrefix": "\t"});
var file1 = result[0];
assert.equal(1, result.length);
assert.equal(2, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('bla with \ttab.scala', file1.oldName);
assert.equal('bla with \ttab.scala', file1.newName);
assert.equal(1, file1.blocks.length);
});
it('should parse diff with deleted file', function() {
var diff =
'diff --git a/src/var/strundefined.js b/src/var/strundefined.js\n' +
'deleted file mode 100644\n' +
'index 04e16b0..0000000\n' +
'--- a/src/var/strundefined.js\n' +
'+++ /dev/null\n' +
'@@ -1,3 +0,0 @@\n' +
'-define(function() {\n' +
'- return typeof undefined;\n' +
'-});\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(false, file1.isCombined);
assert.equal(0, file1.addedLines);
assert.equal(3, file1.deletedLines);
assert.equal('src/var/strundefined.js', file1.oldName);
assert.equal('/dev/null', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(true, file1.isDeleted);
assert.equal('04e16b0', file1.checksumBefore);
assert.equal('0000000', file1.checksumAfter);
});
it('should parse diff with new file', function() {
var diff =
'diff --git a/test.js b/test.js\n' +
'new file mode 100644\n' +
'index 0000000..e1e22ec\n' +
'--- /dev/null\n' +
'+++ b/test.js\n' +
'@@ -0,0 +1,5 @@\n' +
"+var parser = require('./source/git-parser');\n" +
'+\n' +
'+var patchLineList = [ false, false, false, false ];\n' +
'+\n' +
'+console.log(parser.parsePatchDiffResult(text, patchLineList));\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(false, file1.isCombined);
assert.equal(5, file1.addedLines);
assert.equal(0, file1.deletedLines);
assert.equal('/dev/null', file1.oldName);
assert.equal('test.js', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(true, file1.isNew);
assert.equal(100644, file1.newFileMode);
assert.equal('0000000', file1.checksumBefore);
assert.equal('e1e22ec', file1.checksumAfter);
});
it('should parse diff with nested diff', function() {
var diff =
'diff --git a/src/offset.js b/src/offset.js\n' +
'index cc6ffb4..fa51f18 100644\n' +
'--- a/src/offset.js\n' +
'+++ b/src/offset.js\n' +
'@@ -1,6 +1,5 @@\n' +
"+var parser = require('./source/git-parser');\n" +
'+\n' +
"+var text = 'diff --git a/components/app/app.html b/components/app/app.html\\nindex ecb7a95..027bd9b 100644\\n--- a/components/app/app.html\\n+++ b/components/app/app.html\\n@@ -52,0 +53,3 @@\\n+\\n+\\n+\\n@@ -56,0 +60,3 @@\\n+\\n+\\n+\\n'\n" +
'+var patchLineList = [ false, false, false, false ];\n' +
'+\n' +
'+console.log(parser.parsePatchDiffResult(text, patchLineList));\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(false, file1.isCombined);
assert.equal(6, file1.addedLines);
assert.equal(0, file1.deletedLines);
assert.equal('src/offset.js', file1.oldName);
assert.equal('src/offset.js', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(6, file1.blocks[0].lines.length);
assert.equal('cc6ffb4', file1.checksumBefore);
assert.equal('fa51f18', file1.checksumAfter);
});
it('should parse diff with multiple blocks', function() {
var diff =
'diff --git a/src/attributes/classes.js b/src/attributes/classes.js\n' +
'index c617824..c8d1393 100644\n' +
'--- a/src/attributes/classes.js\n' +
'+++ b/src/attributes/classes.js\n' +
'@@ -1,10 +1,9 @@\n' +
' define([\n' +
' "../core",\n' +
' "../var/rnotwhite",\n' +
'- "../var/strundefined",\n' +
' "../data/var/dataPriv",\n' +
' "../core/init"\n' +
'-], function( jQuery, rnotwhite, strundefined, dataPriv ) {\n' +
'+], function( jQuery, rnotwhite, dataPriv ) {\n' +
' \n' +
' var rclass = /[\\t\\r\\n\\f]/g;\n' +
' \n' +
'@@ -128,7 +127,7 @@ jQuery.fn.extend({\n' +
' }\n' +
' \n' +
' // Toggle whole class name\n' +
'- } else if ( type === strundefined || type === "boolean" ) {\n' +
'+ } else if ( value === undefined || type === "boolean" ) {\n' +
' if ( this.className ) {\n' +
' // store className if set\n' +
' dataPriv.set( this, "__className__", this.className );\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(false, file1.isCombined);
assert.equal(2, file1.addedLines);
assert.equal(3, file1.deletedLines);
assert.equal('src/attributes/classes.js', file1.oldName);
assert.equal('src/attributes/classes.js', file1.newName);
assert.equal(2, file1.blocks.length);
assert.equal(11, file1.blocks[0].lines.length);
assert.equal(8, file1.blocks[1].lines.length);
assert.equal('c617824', file1.checksumBefore);
assert.equal('c8d1393', file1.checksumAfter);
});
it('should parse diff with multiple files', function() {
var diff =
'diff --git a/src/core/init.js b/src/core/init.js\n' +
'index e49196a..50f310c 100644\n' +
'--- a/src/core/init.js\n' +
'+++ b/src/core/init.js\n' +
'@@ -101,7 +101,7 @@ var rootjQuery,\n' +
' // HANDLE: $(function)\n' +
' // Shortcut for document ready\n' +
' } else if ( jQuery.isFunction( selector ) ) {\n' +
'- return typeof rootjQuery.ready !== "undefined" ?\n' +
'+ return rootjQuery.ready !== undefined ?\n' +
' rootjQuery.ready( selector ) :\n' +
' // Execute immediately if ready is not present\n' +
' selector( jQuery );\n' +
'diff --git a/src/event.js b/src/event.js\n' +
'index 7336f4d..6183f70 100644\n' +
'--- a/src/event.js\n' +
'+++ b/src/event.js\n' +
'@@ -1,6 +1,5 @@\n' +
' define([\n' +
' "./core",\n' +
'- "./var/strundefined",\n' +
' "./var/rnotwhite",\n' +
' "./var/hasOwn",\n' +
' "./var/slice",\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(2, result.length);
var file1 = result[0];
assert.equal(false, file1.isCombined);
assert.equal(1, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('src/core/init.js', file1.oldName);
assert.equal('src/core/init.js', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(8, file1.blocks[0].lines.length);
assert.equal('e49196a', file1.checksumBefore);
assert.equal('50f310c', file1.checksumAfter);
var file2 = result[1];
assert.equal(false, file2.isCombined);
assert.equal(0, file2.addedLines);
assert.equal(1, file2.deletedLines);
assert.equal('src/event.js', file2.oldName);
assert.equal('src/event.js', file2.newName);
assert.equal(1, file2.blocks.length);
assert.equal(6, file2.blocks[0].lines.length);
assert.equal('7336f4d', file2.checksumBefore);
assert.equal('6183f70', file2.checksumAfter);
});
it('should parse combined diff', function() {
var diff =
'diff --combined describe.c\n' +
'index fabadb8,cc95eb0..4866510\n' +
'--- a/describe.c\n' +
'+++ b/describe.c\n' +
'@@@ -98,20 -98,12 +98,20 @@@\n' +
' return (a_date > b_date) ? -1 : (a_date == b_date) ? 0 : 1;\n' +
' }\n' +
' \n' +
'- static void describe(char *arg)\n' +
' -static void describe(struct commit *cmit, int last_one)\n' +
'++static void describe(char *arg, int last_one)\n' +
' {\n' +
' + unsigned char sha1[20];\n' +
' + struct commit *cmit;\n' +
' struct commit_list *list;\n' +
' static int initialized = 0;\n' +
' struct commit_name *n;\n' +
' \n' +
' + if (get_sha1(arg, sha1) < 0)\n' +
' + usage(describe_usage);\n' +
' + cmit = lookup_commit_reference(sha1);\n' +
' + if (!cmit)\n' +
' + usage(describe_usage);\n' +
' +\n' +
' if (!initialized) {\n' +
' initialized = 1;\n' +
' for_each_ref(get_name);\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(true, file1.isCombined);
assert.equal(9, file1.addedLines);
assert.equal(2, file1.deletedLines);
assert.equal('describe.c', file1.oldName);
assert.equal('describe.c', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(22, file1.blocks[0].lines.length);
assert.deepEqual(['4866510', 'cc95eb0'].sort(), file1.checksumBefore.sort());
assert.equal('fabadb8', file1.checksumAfter);
});
it('should parse diffs with copied files', function() {
var diff =
'diff --git a/index.js b/more-index.js\n' +
'dissimilarity index 5%\n' +
'copy from index.js\n' +
'copy to more-index.js\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(0, file1.addedLines);
assert.equal(0, file1.deletedLines);
assert.equal('index.js', file1.oldName);
assert.equal('more-index.js', file1.newName);
assert.equal(0, file1.blocks.length);
assert.equal(true, file1.isCopy);
assert.equal(5, file1.changedPercentage);
});
it('should parse diffs with moved files', function() {
var diff =
'diff --git a/more-index.js b/other-index.js\n' +
'similarity index 86%\n' +
'rename from more-index.js\n' +
'rename to other-index.js\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(0, file1.addedLines);
assert.equal(0, file1.deletedLines);
assert.equal('more-index.js', file1.oldName);
assert.equal('other-index.js', file1.newName);
assert.equal(0, file1.blocks.length);
assert.equal(true, file1.isRename);
assert.equal(86, file1.unchangedPercentage);
});
it('should parse diffs correct line numbers', function() {
var diff =
'diff --git a/sample b/sample\n' +
'index 0000001..0ddf2ba\n' +
'--- a/sample\n' +
'+++ b/sample\n' +
'@@ -1 +1,2 @@\n' +
'-test\n' +
'+test1r\n';
var result = DiffParser.generateDiffJson(diff);
assert.equal(1, result.length);
var file1 = result[0];
assert.equal(1, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('sample', file1.oldName);
assert.equal('sample', file1.newName);
assert.equal(1, file1.blocks.length);
assert.equal(2, file1.blocks[0].lines.length);
assert.equal(1, file1.blocks[0].lines[0].oldNumber);
assert.equal(null, file1.blocks[0].lines[0].newNumber);
assert.equal(null, file1.blocks[0].lines[1].oldNumber);
assert.equal(1, file1.blocks[0].lines[1].newNumber);
});
it('should parse unified non git diff', function() {
var diff =
'--- a/sample.js\n' +
'+++ b/sample.js\n' +
'@@ -1 +1,2 @@\n' +
'-test\n' +
'+test1r\n' +
'+test2r\n';
var result = DiffParser.generateDiffJson(diff);
var file1 = result[0];
assert.equal(1, result.length);
assert.equal(2, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('sample.js', file1.oldName);
assert.equal('sample.js', file1.newName);
assert.equal(1, file1.blocks.length);
var linesContent = file1.blocks[0].lines.map(function(line) {
return line.content;
});
assert.deepEqual(linesContent, ['-test', '+test1r', '+test2r']);
});
});
});

288
test/diff2html-tests.js Normal file
View file

@ -0,0 +1,288 @@
var assert = require('assert');
var Diff2Html = require('../src/diff2html.js').Diff2Html;
var diffExample1 =
'diff --git a/sample b/sample\n' +
'index 0000001..0ddf2ba\n' +
'--- a/sample\n' +
'+++ b/sample\n' +
'@@ -1 +1 @@\n' +
'-test\n' +
'+test1\n';
var jsonExample1 =
[{
blocks: [{
lines: [{
content: '-test',
type: 'd2h-del',
oldNumber: 1,
newNumber: null
},
{
content: '+test1',
type: 'd2h-ins',
oldNumber: null,
newNumber: 1
}],
oldStartLine: '1',
oldStartLine2: null,
newStartLine: '1',
header: '@@ -1 +1 @@'
}],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: undefined,
newName: 'sample',
isCombined: false
}];
var filesExample1 =
'<div class="d2h-file-list-wrapper">\n' +
' <div class="d2h-file-list-header">\n' +
' <span class="d2h-file-list-title">Files changed (1)</span>\n' +
' <a class="d2h-file-switch d2h-hide">hide</a>\n' +
' <a class="d2h-file-switch d2h-show">show</a>\n' +
' </div>\n' +
' <ol class="d2h-file-list">\n' +
' <li class="d2h-file-list-line">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span><svg aria-hidden="true" class="d2h-icon d2h-changed" height="16" title="modified" version="1.1"\n' +
' viewBox="0 0 14 16" width="14">\n' +
' <path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z"></path>\n' +
'</svg></span>\n' +
' <a href="#d2h-675094" class="d2h-file-name">sample</a>\n' +
' <span class="d2h-file-stats">\n' +
' <span class="d2h-lines-added">+1</span>\n' +
' <span class="d2h-lines-deleted">-1</span>\n' +
' </span>\n' +
' </span>\n' +
'</li>\n' +
' </ol>\n' +
'</div>';
var htmlLineExample1 =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">@@ -1 +1 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-del">\n' +
' <div class="line-num1">1</div>\n' +
'<div class="line-num2"></div>\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">1</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
var htmlLineExample1WithFilesSummary = filesExample1 + htmlLineExample1;
var htmlSideExample1 =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-files-diff">\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info">@@ -1 +1 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-del">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-side-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info"></div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
var htmlSideExample1WithFilesSummary = filesExample1 + htmlSideExample1;
describe('Diff2Html', function() {
describe('getJsonFromDiff', function() {
it('should parse simple diff to json', function() {
var diff =
'diff --git a/sample b/sample\n' +
'index 0000001..0ddf2ba\n' +
'--- a/sample\n' +
'+++ b/sample\n' +
'@@ -1 +1 @@\n' +
'-test\n' +
'+test1\n';
var result = Diff2Html.getJsonFromDiff(diff);
var file1 = result[0];
assert.equal(1, result.length);
assert.equal(1, file1.addedLines);
assert.equal(1, file1.deletedLines);
assert.equal('sample', file1.oldName);
assert.equal('sample', file1.newName);
assert.equal(1, file1.blocks.length);
});
// Test case for issue #49
it('should parse diff with added EOF', function() {
var diff =
'diff --git a/sample.scala b/sample.scala\n' +
'index b583263..8b2fc3e 100644\n' +
'--- a/b583263..8b2fc3e\n' +
'+++ b/8b2fc3e\n' +
'@@ -50,5 +50,7 @@ case class Response[+A](value: Option[A],\n' +
' object ResponseErrorCode extends JsonEnumeration {\n' +
' val NoError, ServiceError, JsonError,\n' +
' InvalidPermissions, MissingPermissions, GenericError,\n' +
'- TokenRevoked, MissingToken = Value\n' +
'-}\n' +
'\\ No newline at end of file\n' +
'+ TokenRevoked, MissingToken,\n' +
'+ IndexLock, RepositoryError, NotValidRepo, PullRequestNotMergeable, BranchError,\n' +
'+ PluginError, CodeParserError, EngineError = Value\n' +
'+}\n';
var result = Diff2Html.getJsonFromDiff(diff);
assert.equal(50, result[0].blocks[0].lines[0].oldNumber);
assert.equal(50, result[0].blocks[0].lines[0].newNumber);
assert.equal(51, result[0].blocks[0].lines[1].oldNumber);
assert.equal(51, result[0].blocks[0].lines[1].newNumber);
assert.equal(52, result[0].blocks[0].lines[2].oldNumber);
assert.equal(52, result[0].blocks[0].lines[2].newNumber);
assert.equal(53, result[0].blocks[0].lines[3].oldNumber);
assert.equal(null, result[0].blocks[0].lines[3].newNumber);
assert.equal(54, result[0].blocks[0].lines[4].oldNumber);
assert.equal(null, result[0].blocks[0].lines[4].newNumber);
assert.equal(null, result[0].blocks[0].lines[5].oldNumber);
assert.equal(53, result[0].blocks[0].lines[5].newNumber);
assert.equal(null, result[0].blocks[0].lines[6].oldNumber);
assert.equal(54, result[0].blocks[0].lines[6].newNumber);
assert.equal(null, result[0].blocks[0].lines[7].oldNumber);
assert.equal(55, result[0].blocks[0].lines[7].newNumber);
assert.equal(null, result[0].blocks[0].lines[8].oldNumber);
assert.equal(56, result[0].blocks[0].lines[8].newNumber);
});
it('should generate pretty line by line html from diff', function() {
var result = Diff2Html.getPrettyHtmlFromDiff(diffExample1);
assert.equal(htmlLineExample1, result);
});
it('should generate pretty line by line html from json', function() {
var result = Diff2Html.getPrettyHtmlFromJson(jsonExample1);
assert.equal(htmlLineExample1, result);
});
it('should generate pretty diff with files summary', function() {
var result = Diff2Html.getPrettyHtmlFromDiff(diffExample1, {showFiles: true});
assert.equal(htmlLineExample1WithFilesSummary, result);
});
it('should generate pretty side by side html from diff', function() {
var result = Diff2Html.getPrettySideBySideHtmlFromDiff(diffExample1);
assert.equal(htmlSideExample1, result);
});
it('should generate pretty side by side html from json', function() {
var result = Diff2Html.getPrettySideBySideHtmlFromJson(jsonExample1);
assert.equal(htmlSideExample1, result);
});
it('should generate pretty side by side html from diff', function() {
var result = Diff2Html.getPrettySideBySideHtmlFromDiff(diffExample1, {showFiles: true});
assert.equal(htmlSideExample1WithFilesSummary, result);
});
});
});

View file

@ -0,0 +1,105 @@
var assert = require('assert');
var fileListPrinter = require('../src/file-list-printer.js').FileListPrinter;
describe('FileListPrinter', function() {
describe('generateFileList', function() {
it('should work for all kinds of files', function() {
var files = [{
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js'
}, {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js'
}, {
addedLines: 12,
deletedLines: 0,
language: 'js',
oldName: 'dev/null',
newName: 'my/file/name.js',
isNew: true
}, {
addedLines: 0,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'dev/null',
isDeleted: true
}];
var fileHtml = fileListPrinter.generateFileList(files);
var expected =
'<div class="d2h-file-list-wrapper">\n' +
' <div class="d2h-file-list-header">\n' +
' <span class="d2h-file-list-title">Files changed (4)</span>\n' +
' <a class="d2h-file-switch d2h-hide">hide</a>\n' +
' <a class="d2h-file-switch d2h-show">show</a>\n' +
' </div>\n' +
' <ol class="d2h-file-list">\n' +
' <li class="d2h-file-list-line">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span><svg aria-hidden="true" class="d2h-icon d2h-changed" height="16" title="modified" version="1.1"\n' +
' viewBox="0 0 14 16" width="14">\n' +
' <path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM4 8c0-1.66 1.34-3 3-3s3 1.34 3 3-1.34 3-3 3-3-1.34-3-3z"></path>\n' +
'</svg></span>\n' +
' <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>\n' +
' <span class="d2h-file-stats">\n' +
' <span class="d2h-lines-added">+12</span>\n' +
' <span class="d2h-lines-deleted">-41</span>\n' +
' </span>\n' +
' </span>\n' +
'</li>\n' +
'<li class="d2h-file-list-line">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span><svg aria-hidden="true" class="d2h-icon d2h-moved" height="16" title="renamed" version="1.1"\n' +
' viewBox="0 0 14 16" width="14">\n' +
' <path d="M6 9H3V7h3V4l5 4-5 4V9z m8-7v12c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h12c0.55 0 1 0.45 1 1z m-1 0H1v12h12V2z"></path>\n' +
'</svg></span>\n' +
' <a href="#d2h-662683" class="d2h-file-name">my/file/{name1.js → name2.js}</a>\n' +
' <span class="d2h-file-stats">\n' +
' <span class="d2h-lines-added">+12</span>\n' +
' <span class="d2h-lines-deleted">-41</span>\n' +
' </span>\n' +
' </span>\n' +
'</li>\n' +
'<li class="d2h-file-list-line">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span><svg aria-hidden="true" class="d2h-icon d2h-added" height="16" title="added" version="1.1" viewBox="0 0 14 16"\n' +
' width="14">\n' +
' <path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM6 9H3V7h3V4h2v3h3v2H8v3H6V9z"></path>\n' +
'</svg></span>\n' +
' <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>\n' +
' <span class="d2h-file-stats">\n' +
' <span class="d2h-lines-added">+12</span>\n' +
' <span class="d2h-lines-deleted">-0</span>\n' +
' </span>\n' +
' </span>\n' +
'</li>\n' +
'<li class="d2h-file-list-line">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span><svg aria-hidden="true" class="d2h-icon d2h-deleted" height="16" title="removed" version="1.1"\n' +
' viewBox="0 0 14 16" width="14">\n' +
' <path d="M13 1H1C0.45 1 0 1.45 0 2v12c0 0.55 0.45 1 1 1h12c0.55 0 1-0.45 1-1V2c0-0.55-0.45-1-1-1z m0 13H1V2h12v12zM11 9H3V7h8v2z"></path>\n' +
'</svg></span>\n' +
' <a href="#d2h-781444" class="d2h-file-name">my/file/name.js</a>\n' +
' <span class="d2h-file-stats">\n' +
' <span class="d2h-lines-added">+0</span>\n' +
' <span class="d2h-lines-deleted">-41</span>\n' +
' </span>\n' +
' </span>\n' +
'</li>\n' +
' </ol>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
});
});

36
test/hogan-cache-tests.js Normal file
View file

@ -0,0 +1,36 @@
var assert = require('assert');
var HoganJsUtils = require('../src/hoganjs-utils.js').HoganJsUtils;
var diffParser = require('../src/diff-parser.js').DiffParser;
describe('HoganJsUtils', function() {
describe('render', function() {
var emptyDiffHtml =
'<tr>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">\n' +
' File without changes\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
it('should render view', function() {
var result = HoganJsUtils.render('generic', 'empty-diff', {
contentClass: 'd2h-code-line',
diffParser: diffParser
});
assert.equal(emptyDiffHtml, result);
});
it('should render view without cache', function() {
var result = HoganJsUtils.render('generic', 'empty-diff', {
contentClass: 'd2h-code-line',
diffParser: diffParser
}, {noCache: true});
assert.equal(emptyDiffHtml + '\n', result);
});
it('should return null if template is missing', function() {
var result = HoganJsUtils.render('generic', 'missing-template', {}, {noCache: true});
assert.equal(null, result);
});
});
});

583
test/line-by-line-tests.js Normal file
View file

@ -0,0 +1,583 @@
var assert = require('assert');
var Utils = require('../src/utils.js').Utils;
var LineByLinePrinter = require('../src/line-by-line-printer.js').LineByLinePrinter;
describe('LineByLinePrinter', function() {
describe('_generateEmptyDiff', function() {
it('should return an empty diff', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter._generateEmptyDiff();
var expected = '<tr>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">\n' +
' File without changes\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n';
assert.equal(expected, fileHtml);
});
});
describe('makeLineHtml', function() {
it('should work for insertions', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineHtml(
diffParser.LINE_TYPE.INSERTS, '', 30, '+', 'test');
fileHtml = fileHtml.replace(/\n\n+/g, '\n');
var expected = '<tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">30</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">test</span>\n' +
' <span class="d2h-code-line-ctn">+</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
it('should work for deletions', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineHtml(
diffParser.LINE_TYPE.DELETES, 30, '', '-', 'test');
fileHtml = fileHtml.replace(/\n\n+/g, '\n');
var expected = '<tr>\n' +
' <td class="d2h-code-linenumber d2h-del">\n' +
' <div class="line-num1">30</div>\n' +
'<div class="line-num2"></div>\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">test</span>\n' +
' <span class="d2h-code-line-ctn">-</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
it('should convert indents into non breakin spaces (2 white spaces)', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineHtml(
diffParser.LINE_TYPE.INSERTS, '', 30, '+', ' test');
fileHtml = fileHtml.replace(/\n\n+/g, '\n');
var expected = '<tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">30</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">&nbsp;&nbsp;test</span>\n' +
' <span class="d2h-code-line-ctn">+</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
it('should convert indents into non breakin spaces (4 white spaces)', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineHtml(
diffParser.LINE_TYPE.INSERTS, '', 30, '+', ' test');
fileHtml = fileHtml.replace(/\n\n+/g, '\n');
var expected = '<tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">30</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">&nbsp;&nbsp;&nbsp;&nbsp;test</span>\n' +
' <span class="d2h-code-line-ctn">+</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
it('should convert indents into non breakin spaces (one tab)', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineHtml(
diffParser.LINE_TYPE.INSERTS, '', 30, '+', Utils.escape('\ttest'));
fileHtml = fileHtml.replace(/\n\n+/g, '\n');
var expected = '<tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'' +
'<div class="line-num2">30</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">&nbsp;&nbsp;&nbsp;&nbsp;test</span>\n' +
' <span class="d2h-code-line-ctn">+</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
});
describe('makeFileDiffHtml', function() {
it('should work for simple file', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'my/file/name.js'
};
var diffs = '<span>Random Html</span>';
var fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs);
var expected =
'<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">my/file/name.js</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <span>Random Html</span>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
it('should work for simple added file', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var file = {
addedLines: 12,
deletedLines: 0,
language: 'js',
oldName: 'dev/null',
newName: 'my/file/name.js',
isNew: true
};
var diffs = '<span>Random Html</span>';
var fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs);
var expected =
'<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">my/file/name.js</span>\n' +
' <span class="d2h-tag d2h-added d2h-added-tag">ADDED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <span>Random Html</span>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
it('should work for simple deleted file', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var file = {
addedLines: 0,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name.js',
newName: 'dev/null',
isDeleted: true
};
var diffs = '<span>Random Html</span>';
var fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs);
var expected =
'<div id="d2h-781444" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">my/file/name.js</span>\n' +
' <span class="d2h-tag d2h-deleted d2h-deleted-tag">DELETED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <span>Random Html</span>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
it('should work for simple renamed file', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var file = {
addedLines: 12,
deletedLines: 41,
language: 'js',
oldName: 'my/file/name1.js',
newName: 'my/file/name2.js',
isRename: true
};
var diffs = '<span>Random Html</span>';
var fileHtml = lineByLinePrinter.makeFileDiffHtml(file, diffs);
var expected =
'<div id="d2h-662683" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">my/file/{name1.js → name2.js}</span>\n' +
' <span class="d2h-tag d2h-moved d2h-moved-tag">RENAMED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <span>Random Html</span>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
});
describe('makeLineByLineHtmlWrapper', function() {
it('should work for simple content', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var fileHtml = lineByLinePrinter.makeLineByLineHtmlWrapper('<span>Random Html</span>');
var expected =
'<div class="d2h-wrapper">\n' +
' <span>Random Html</span>\n' +
'</div>';
assert.equal(expected, fileHtml);
});
});
describe('generateLineByLineJsonHtml', function() {
it('should work for list of files', function() {
var exampleJson = [{
blocks: [{
lines: [{
content: '-test',
type: 'd2h-del',
oldNumber: 1,
newNumber: null
},
{
content: '+test1r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 1
}],
oldStartLine: '1',
oldStartLine2: null,
newStartLine: '1',
header: '@@ -1 +1 @@'
}],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: undefined,
newName: 'sample',
isCombined: false
}];
var lineByLinePrinter = new LineByLinePrinter({matching: 'lines'});
var html = lineByLinePrinter.generateLineByLineJsonHtml(exampleJson);
var expected =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">@@ -1 +1 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-del d2h-change">\n' +
' <div class="line-num1">1</div>\n' +
'<div class="line-num2"></div>\n' +
' </td>\n' +
' <td class="d2h-del d2h-change">\n' +
' <div class="d2h-code-line d2h-del d2h-change">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-ins d2h-change">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">1</div>\n' +
' </td>\n' +
' <td class="d2h-ins d2h-change">\n' +
' <div class="d2h-code-line d2h-ins d2h-change">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1r</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
assert.equal(expected, html);
});
it('should work for empty blocks', function() {
var exampleJson = [{
blocks: [],
deletedLines: 0,
addedLines: 0,
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false
}];
var lineByLinePrinter = new LineByLinePrinter();
var html = lineByLinePrinter.generateLineByLineJsonHtml(exampleJson);
var expected =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-file-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">\n' +
' File without changes\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
'\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
assert.equal(expected, html);
});
});
describe('_processLines', function() {
it('should work for simple block header', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var oldLines = [{
content: '-test',
type: 'd2h-del',
oldNumber: 1,
newNumber: null
}];
var newLines = [{
content: '+test1r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 1
}];
var html = lineByLinePrinter._processLines(oldLines, newLines);
var expected =
'<tr>\n' +
' <td class="d2h-code-linenumber d2h-del">\n' +
' <div class="line-num1">1</div>\n' +
'<div class="line-num2"></div>\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-line d2h-del">\n' +
' <span class="d2h-code-line-ctn">-test</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">1</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-ctn">+test1r</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, html);
});
});
describe('_generateFileHtml', function() {
it('should work for simple file', function() {
var lineByLinePrinter = new LineByLinePrinter({});
var file = {
blocks: [{
lines: [{
content: ' one context line',
type: 'd2h-cntx',
oldNumber: 1,
newNumber: 1
},
{
content: '-test',
type: 'd2h-del',
oldNumber: 2,
newNumber: null
},
{
content: '+test1r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 2
},
{
content: '+test2r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 3
}],
oldStartLine: '1',
oldStartLine2: null,
newStartLine: '1',
header: '@@ -1 +1 @@'
}],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: undefined,
newName: 'sample',
isCombined: false
};
var html = lineByLinePrinter._generateFileHtml(file);
var expected =
'<tr>\n' +
' <td class="d2h-code-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-line d2h-info">@@ -1 +1 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-cntx">\n' +
' <div class="line-num1">1</div>\n' +
'<div class="line-num2">1</div>\n' +
' </td>\n' +
' <td class="d2h-cntx">\n' +
' <div class="d2h-code-line d2h-cntx">\n' +
' <span class="d2h-code-line-ctn">&nbsp;one&nbsp;context&nbsp;line</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-del">\n' +
' <div class="line-num1">2</div>\n' +
'<div class="line-num2"></div>\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">2</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1r</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-linenumber d2h-ins">\n' +
' <div class="line-num1"></div>\n' +
'<div class="line-num2">3</div>\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-line d2h-ins">\n' +
' <span class="d2h-code-line-ctn">+test2r</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, html);
});
});
});

139
test/printer-utils-tests.js Normal file
View file

@ -0,0 +1,139 @@
var assert = require('assert');
var PrinterUtils = require('../src/printer-utils.js').PrinterUtils;
describe('Utils', function() {
describe('getHtmlId', function() {
it('should generate file unique id', function() {
var result = PrinterUtils.getHtmlId({
oldName: 'sample.js',
newName: 'sample.js'
});
assert.equal('d2h-960013', result);
});
it('should generate file unique id for empty hashes', function() {
var result = PrinterUtils.getHtmlId({
oldName: 'sample.js',
newName: 'sample.js'
});
assert.equal('d2h-960013', result);
});
});
describe('getDiffName', function() {
it('should generate the file name for a changed file', function() {
var result = PrinterUtils.getDiffName({
oldName: 'sample.js',
newName: 'sample.js'
});
assert.equal('sample.js', result);
});
it('should generate the file name for a changed file and full rename', function() {
var result = PrinterUtils.getDiffName({
oldName: 'sample1.js',
newName: 'sample2.js'
});
assert.equal('sample1.js → sample2.js', result);
});
it('should generate the file name for a changed file and prefix rename', function() {
var result = PrinterUtils.getDiffName({
oldName: 'src/path/sample.js',
newName: 'source/path/sample.js'
});
assert.equal('{src → source}/path/sample.js', result);
});
it('should generate the file name for a changed file and suffix rename', function() {
var result = PrinterUtils.getDiffName({
oldName: 'src/path/sample1.js',
newName: 'src/path/sample2.js'
});
assert.equal('src/path/{sample1.js → sample2.js}', result);
});
it('should generate the file name for a changed file and middle rename', function() {
var result = PrinterUtils.getDiffName({
oldName: 'src/really/big/path/sample.js',
newName: 'src/small/path/sample.js'
});
assert.equal('src/{really/big → small}/path/sample.js', result);
});
it('should generate the file name for a deleted file', function() {
var result = PrinterUtils.getDiffName({
oldName: 'src/my/file.js',
newName: '/dev/null'
});
assert.equal('src/my/file.js', result);
});
it('should generate the file name for a new file', function() {
var result = PrinterUtils.getDiffName({
oldName: '/dev/null',
newName: 'src/my/file.js'
});
assert.equal('src/my/file.js', result);
});
it('should generate handle undefined filename', function() {
var result = PrinterUtils.getDiffName({});
assert.equal('unknown/file/path', result);
});
});
describe('diffHighlight', function() {
it('should highlight two lines', function() {
var result = PrinterUtils.diffHighlight(
'-var myVar = 2;',
'+var myVariable = 3;',
{matching: 'words'}
);
assert.deepEqual({
first: {
prefix: '-',
line: 'var <del>myVar</del> = <del>2</del>;'
},
second: {
prefix: '+',
line: 'var <ins>myVariable</ins> = <ins>3</ins>;'
}
}, result);
});
it('should highlight two lines char by char', function() {
var result = PrinterUtils.diffHighlight(
'-var myVar = 2;',
'+var myVariable = 3;',
{charByChar: true}
);
assert.deepEqual({
first: {
prefix: '-',
line: 'var myVar = <del>2</del>;'
},
second: {
prefix: '+',
line: 'var myVar<ins>iable</ins> = <ins>3</ins>;'
}
}, result);
});
it('should highlight combined diff lines', function() {
var result = PrinterUtils.diffHighlight(
' -var myVar = 2;',
' +var myVariable = 3;',
{
isCombined: true,
matching: 'words',
matchWordsThreshold: 1.00
}
);
assert.deepEqual({
first: {
prefix: ' -',
line: 'var <del class="d2h-change">myVar</del> = <del class="d2h-change">2</del>;'
},
second: {
prefix: ' +',
line: 'var <ins class="d2h-change">myVariable</ins> = <ins class="d2h-change">3</ins>;'
}
}, result);
});
});
});

View file

@ -0,0 +1,400 @@
var assert = require('assert');
var SideBySidePrinter = require('../src/side-by-side-printer.js').SideBySidePrinter;
describe('SideBySidePrinter', function() {
describe('generateEmptyDiff', function() {
it('should return an empty diff', function() {
var sideBySidePrinter = new SideBySidePrinter({});
var fileHtml = sideBySidePrinter.generateEmptyDiff();
var expectedRight = '';
var expectedLeft = '<tr>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info">\n' +
' File without changes\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n';
assert.equal(expectedRight, fileHtml.right);
assert.equal(expectedLeft, fileHtml.left);
});
});
describe('generateSideBySideFileHtml', function() {
it('should generate lines with the right prefixes', function() {
var sideBySidePrinter = new SideBySidePrinter({});
var file = {
"blocks": [{
"lines": [
{
"content": " context",
"type": "d2h-cntx",
"oldNumber": 19,
"newNumber": 19
},
{
"content": "-removed",
"type": "d2h-del",
"oldNumber": 20,
"newNumber": null
},
{
"content": "+added",
"type": "d2h-ins",
"oldNumber": null,
"newNumber": 20
},
{
"content": "+another added",
"type": "d2h-ins",
"oldNumber": null,
"newNumber": 21
}
],
"oldStartLine": "19",
"newStartLine": "19",
"header": "@@ -19,7 +19,7 @@"
}],
"deletedLines": 1,
"addedLines": 1,
"checksumBefore": "fc56817",
"checksumAfter": "e8e7e49",
"mode": "100644",
"oldName": "coverage.init",
"language": "init",
"newName": "coverage.init",
"isCombined": false
};
var fileHtml = sideBySidePrinter.generateSideBySideFileHtml(file);
var expectedLeft =
'<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info">@@ -19,7 +19,7 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-cntx">\n' +
' 19\n' +
' </td>\n' +
' <td class="d2h-cntx">\n' +
' <div class="d2h-code-side-line d2h-cntx">\n' +
' <span class="d2h-code-line-prefix">&nbsp;</span>\n' +
' <span class="d2h-code-line-ctn">context</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-del">\n' +
' 20\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-side-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>removed</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-cntx">\n' +
' ' +
'\n' +
' </td>\n' +
' <td class="d2h-cntx">\n' +
' <div class="d2h-code-side-line d2h-cntx">\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
var expectedRight =
'<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info"></div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-cntx">\n' +
' 19\n' +
' </td>\n' +
' <td class="d2h-cntx">\n' +
' <div class="d2h-code-side-line d2h-cntx">\n' +
' <span class="d2h-code-line-prefix">&nbsp;</span>\n' +
' <span class="d2h-code-line-ctn">context</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
' 20\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>added</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
' 21\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn">another&nbsp;added</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expectedLeft, fileHtml.left);
assert.equal(expectedRight, fileHtml.right);
});
});
describe('generateSingleLineHtml', function() {
it('should work for insertions', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var sideBySidePrinter = new SideBySidePrinter({});
var fileHtml = sideBySidePrinter.generateSingleLineHtml(
diffParser.LINE_TYPE.INSERTS, 30, 'test', '+');
var expected = '<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
' 30\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn">test</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
it('should work for deletions', function() {
var diffParser = require('../src/diff-parser.js').DiffParser;
var sideBySidePrinter = new SideBySidePrinter({});
var fileHtml = sideBySidePrinter.generateSingleLineHtml(
diffParser.LINE_TYPE.DELETES, 30, 'test', '-');
var expected = '<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-del">\n' +
' 30\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-side-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn">test</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expected, fileHtml);
});
});
describe('generateSideBySideJsonHtml', function() {
it('should work for list of files', function() {
var exampleJson = [{
blocks: [{
lines: [{
content: '-test',
type: 'd2h-del',
oldNumber: 1,
newNumber: null
},
{
content: '+test1r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 1
}],
oldStartLine: '1',
oldStartLine2: null,
newStartLine: '1',
header: '@@ -1 +1 @@'
}],
deletedLines: 1,
addedLines: 1,
checksumBefore: '0000001',
checksumAfter: '0ddf2ba',
oldName: 'sample',
language: undefined,
newName: 'sample',
isCombined: false
}];
var sideBySidePrinter = new SideBySidePrinter({matching: 'lines'});
var html = sideBySidePrinter.generateSideBySideJsonHtml(exampleJson);
var expected =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-files-diff">\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info">@@ -1 +1 @@</div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-del d2h-change">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-del d2h-change">\n' +
' <div class="d2h-code-side-line d2h-del d2h-change">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn"><del>test</del></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-code-side-linenumber d2h-info"></td>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info"></div>\n' +
' </td>\n' +
'</tr><tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins d2h-change">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-ins d2h-change">\n' +
' <div class="d2h-code-side-line d2h-ins d2h-change">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn"><ins>test1r</ins></span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
assert.equal(expected, html);
});
it('should work for files without blocks', function() {
var exampleJson = [{
blocks: [],
oldName: 'sample',
language: 'js',
newName: 'sample',
isCombined: false
}];
var sideBySidePrinter = new SideBySidePrinter();
var html = sideBySidePrinter.generateSideBySideJsonHtml(exampleJson);
var expected =
'<div class="d2h-wrapper">\n' +
' <div id="d2h-675094" class="d2h-file-wrapper" data-lang="js">\n' +
' <div class="d2h-file-header">\n' +
' <span class="d2h-file-name-wrapper">\n' +
' <span class="d2h-icon-wrapper"><svg aria-hidden="true" class="d2h-icon" height="16" version="1.1" viewBox="0 0 12 16" width="12">\n' +
' <path d="M6 5H2v-1h4v1zM2 8h7v-1H2v1z m0 2h7v-1H2v1z m0 2h7v-1H2v1z m10-7.5v9.5c0 0.55-0.45 1-1 1H1c-0.55 0-1-0.45-1-1V2c0-0.55 0.45-1 1-1h7.5l3.5 3.5z m-1 0.5L8 2H1v12h10V5z"></path>\n' +
'</svg></span>\n' +
' <span class="d2h-file-name">sample</span>\n' +
' <span class="d2h-tag d2h-changed d2h-changed-tag">CHANGED</span></span>\n' +
' </div>\n' +
' <div class="d2h-files-diff">\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' <tr>\n' +
' <td class="d2h-info">\n' +
' <div class="d2h-code-side-line d2h-info">\n' +
' File without changes\n' +
' </div>\n' +
' </td>\n' +
'</tr>\n' +
'\n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' <div class="d2h-file-side-diff">\n' +
' <div class="d2h-code-wrapper">\n' +
' <table class="d2h-diff-table">\n' +
' <tbody class="d2h-diff-tbody">\n' +
' \n' +
' </tbody>\n' +
' </table>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
'</div>\n' +
'</div>';
assert.equal(expected, html);
});
});
describe('processLines', function() {
it('should process file lines', function() {
var oldLines = [{
content: '-test',
type: 'd2h-del',
oldNumber: 1,
newNumber: null
}];
var newLines = [{
content: '+test1r',
type: 'd2h-ins',
oldNumber: null,
newNumber: 1
}];
var sideBySidePrinter = new SideBySidePrinter({matching: 'lines'});
var html = sideBySidePrinter.processLines(oldLines, newLines);
var expectedLeft =
'<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-del">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-del">\n' +
' <div class="d2h-code-side-line d2h-del">\n' +
' <span class="d2h-code-line-prefix">-</span>\n' +
' <span class="d2h-code-line-ctn">test</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
var expectedRight =
'<tr>\n' +
' <td class="d2h-code-side-linenumber d2h-ins">\n' +
' 1\n' +
' </td>\n' +
' <td class="d2h-ins">\n' +
' <div class="d2h-code-side-line d2h-ins">\n' +
' <span class="d2h-code-line-prefix">+</span>\n' +
' <span class="d2h-code-line-ctn">test1r</span>\n' +
' </div>\n' +
' </td>\n' +
'</tr>';
assert.equal(expectedLeft, html.left);
assert.equal(expectedRight, html.right);
});
});
});

39
test/utils-tests.js Normal file
View file

@ -0,0 +1,39 @@
var assert = require('assert');
var Utils = require('../src/utils.js').Utils;
describe('Utils', function() {
describe('escape', function() {
it('should escape & with &amp;', function() {
var result = Utils.escape('&');
assert.equal('&amp;', result);
});
it('should escape < with &lt;', function() {
var result = Utils.escape('<');
assert.equal('&lt;', result);
});
it('should escape > with &gt;', function() {
var result = Utils.escape('>');
assert.equal('&gt;', result);
});
it('should escape a string with multiple problematic characters', function() {
var result = Utils.escape('<a href="#">\tlink text</a>');
var expected = '&lt;a href="#"&gt; link text&lt;/a&gt;';
assert.equal(expected, result);
});
});
describe('convertWhiteSpaceToNonBreakingSpace', function() {
it('should escape 1 whitespaces with &nbsp;', function() {
var result = Utils.convertWhiteSpaceToNonBreakingSpace(' ');
assert.equal('&nbsp;', result);
});
it('should escape 2 whitespaces with &nbsp;', function() {
var result = Utils.convertWhiteSpaceToNonBreakingSpace(' ');
assert.equal('&nbsp;&nbsp;', result);
});
it('should escape 4 whitespaces with &nbsp;', function() {
var result = Utils.convertWhiteSpaceToNonBreakingSpace(' ');
assert.equal('&nbsp;&nbsp;&nbsp;&nbsp;', result);
});
});
});

View file

@ -1,5 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules"],
"include": ["./**/*"]
}

View file

@ -1,37 +0,0 @@
{
"include": ["src/**/*", "typings/**/*"],
"exclude": ["node_modules", "src/__tests__/**"],
"compilerOptions": {
"outDir": "bundles-out",
"module": "CommonJS",
"moduleResolution": "Node",
"target": "ES6",
"lib": ["ES6", "DOM"],
"jsx": "preserve",
"allowJs": false,
"checkJs": false,
"importHelpers": false,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"noEmit": false,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"incremental": false,
"strictNullChecks": true,
"removeComments": true,
"preserveConstEnums": true,
"alwaysStrict": true,
"noImplicitAny": true,
"noImplicitThis": true
}
}

3
typings/merge.d.ts vendored
View file

@ -1,3 +0,0 @@
declare module 'merge' {
export function recursive(clone: boolean, ...items: object[]): object;
}

Some files were not shown because too many files have changed in this diff Show more