├── .husky ├── .gitignore └── pre-commit ├── .eslintignore ├── babel.config.cjs ├── bin └── diff2html.js ├── .github ├── workflows │ ├── ci.yml │ ├── release.yml │ └── test-and-publish.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── .editorconfig ├── src ├── logger.ts ├── http-utils.ts ├── types.ts ├── __tests__ │ ├── utils-tests.ts │ ├── main-tests.ts │ └── cli-tests.ts ├── utils.ts ├── main.ts ├── configuration.ts ├── cli.ts └── yargs.ts ├── .gitignore ├── .prettierrc.json ├── template.html ├── LICENSE ├── tsconfig.json ├── jest.config.ts ├── SECURITY.md ├── .eslintrc.cjs ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | build/** 3 | node_modules/** 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | }; 4 | -------------------------------------------------------------------------------- /bin/diff2html.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // eslint-disable-next-line import/no-unresolved 4 | import { main } from '../lib/main.js'; 5 | main(); 6 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | test-and-publish: 9 | uses: ./.github/workflows/test-and-publish.yml 10 | with: 11 | environment: dev 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | test-and-publish: 9 | uses: ./.github/workflows/test-and-publish.yml 10 | with: 11 | environment: production 12 | secrets: inherit 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | max_line_length = 120 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-types 2 | export function print(message: string, ...params: (string | number | object)[]): void { 3 | console.log(message, ...params); 4 | } 5 | 6 | // eslint-disable-next-line @typescript-eslint/ban-types 7 | export function error(message: string, ...params: (string | number | object)[]): void { 8 | console.error(message, params); 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | 14 | # Maven 15 | log/ 16 | target/ 17 | 18 | # Node 19 | node_modules/ 20 | npm-debug.log 21 | yarn-error.log 22 | 23 | # Coverage 24 | coverage/ 25 | 26 | # Bower 27 | bower_components/ 28 | 29 | # Terraform 30 | /terraform/.terraform 31 | 32 | /lib/ 33 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "htmlWhitespaceSensitivity": "css", 5 | "insertPragma": false, 6 | "jsxSingleQuote": false, 7 | "printWidth": 120, 8 | "proseWrap": "always", 9 | "quoteProps": "as-needed", 10 | "requirePragma": false, 11 | "semi": true, 12 | "singleQuote": true, 13 | "tabWidth": 2, 14 | "trailingComma": "all", 15 | "useTabs": false 16 | } 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' ---**Is your feature 4 | request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always 5 | frustrated when [...] 6 | 7 | **Describe the solution you'd like** A clear and concise description of what you want to happen. 8 | 9 | **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features 10 | you've considered. 11 | 12 | **Additional context** Add any other context or screenshots about the feature request here. 13 | -------------------------------------------------------------------------------- /src/http-utils.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | 3 | export function put(url: string, payload: object): Promise { 4 | return fetch(url, { 5 | method: 'PUT', 6 | headers: { 7 | Accept: 'application/json', 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify(payload), 11 | }).then(async response => { 12 | const bodyText = await response.text(); 13 | try { 14 | return JSON.parse(bodyText); 15 | } catch (error) { 16 | if (error instanceof SyntaxError) { 17 | throw new Error(`Failed to parse response json.\nBody:\n\n${bodyText}`); 18 | } else { 19 | throw error; 20 | } 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' ---**Describe the bug** A 4 | clear and concise description of what the bug is. 5 | 6 | **To Reproduce** Steps to reproduce the behavior: 7 | 8 | 1. Go to '...' 9 | 2. Click on '....' 10 | 3. Scroll down to '....' 11 | 4. See error 12 | 13 | **Expected behavior** A clear and concise description of what you expected to happen. 14 | 15 | **Screenshots** If applicable, add screenshots to help explain your problem. 16 | 17 | **Desktop (please complete the following information):** 18 | 19 | - OS: [e.g. Windows, Linux, Mac] 20 | - Version [e.g. 22] 21 | 22 | **Additional context** Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type StyleType = 'line' | 'side'; 2 | export type SummaryType = 'closed' | 'open' | 'hidden'; 3 | export type DiffStyleType = 'word' | 'char'; 4 | export type LineMatchingType = 'lines' | 'words' | 'none'; 5 | export type FormatType = 'html' | 'json'; 6 | export type InputType = 'file' | 'command' | 'stdin'; 7 | export type OutputType = 'preview' | 'stdout'; 8 | export type DiffyType = 'browser' | 'pbcopy' | 'print'; 9 | 10 | export type Configuration = { 11 | fileContentToggle: boolean; 12 | synchronisedScroll: boolean; 13 | showFilesOpen: boolean; 14 | highlightCode: boolean; 15 | formatType: FormatType; 16 | outputDestinationType: OutputType; 17 | outputDestinationFile?: string; 18 | inputSource: InputType; 19 | diffyType?: DiffyType; 20 | htmlWrapperTemplate: string; 21 | pageTitle: string; 22 | pageHeader: string; 23 | ignore: string[]; 24 | }; 25 | -------------------------------------------------------------------------------- /src/__tests__/utils-tests.ts: -------------------------------------------------------------------------------- 1 | import * as utils from '../utils'; 2 | 3 | const stringToCompare = 'This is a rand0m preTTy string to write and read a file'; 4 | 5 | describe('utils', () => { 6 | test('should write and read file synchronously', () => { 7 | utils.writeFile('/tmp/file.test', stringToCompare); 8 | 9 | const contentRead = utils.readFile('/tmp/file.test'); 10 | 11 | expect(stringToCompare).toBe(contentRead); 12 | }); 13 | 14 | test('should execute command in shell', () => { 15 | const echoedValue = 'echoed string'; 16 | const result = utils.execute('echo', [echoedValue]); 17 | 18 | expect(result).toBe(`${echoedValue}\n`); 19 | }); 20 | 21 | test('should replace exactly string', () => { 22 | const result = utils.replaceExactly('my long and nice text', 'long', '$&beautiful'); 23 | 24 | expect(result).toBe('my $&beautiful and nice text'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <!--diff2html-title--> 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 |

29 | 30 |
31 | 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014-2016 Rodrigo Fernandes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["*env.d.ts", "src/**/*"], 3 | "exclude": ["node_modules", "src/__tests__/**"], 4 | "compilerOptions": { 5 | "moduleResolution": "NodeNext", 6 | "target": "ESNext", 7 | "module": "NodeNext", 8 | "lib": ["ESNext"], 9 | "jsx": "preserve", 10 | "rootDir": "src", 11 | "outDir": "lib", 12 | "allowJs": true, 13 | "importHelpers": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "skipLibCheck": true, 22 | "resolveJsonModule": true, 23 | "declaration": true, 24 | "declarationMap": true, 25 | "sourceMap": true, 26 | "checkJs": true, 27 | "noEmit": false, 28 | "forceConsistentCasingInFileNames": true, 29 | "isolatedModules": true, 30 | "incremental": false, 31 | "strictNullChecks": true, 32 | "removeComments": true, 33 | "preserveConstEnums": true, 34 | "alwaysStrict": true, 35 | "noImplicitAny": true, 36 | "noImplicitThis": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | import fs from 'fs'; 3 | 4 | export function exists(filePath: string): boolean { 5 | try { 6 | return fs.existsSync(filePath); 7 | } catch (ignore) { 8 | return false; 9 | } 10 | } 11 | 12 | export function readFile(filePath: string): string { 13 | return fs.readFileSync(filePath, 'utf8'); 14 | } 15 | 16 | export function readStdin(): Promise { 17 | return new Promise((resolve): void => { 18 | let content = ''; 19 | process.stdin.resume(); 20 | process.stdin.on('data', buf => { 21 | content += buf.toString('utf8'); 22 | }); 23 | process.stdin.on('end', () => resolve(content)); 24 | }); 25 | } 26 | 27 | export function writeFile(filePath: string, content: string): void { 28 | return fs.writeFileSync(filePath, content); 29 | } 30 | 31 | export function execute(executable: string, args: string[]): string { 32 | return childProcess.spawnSync(executable, args, { encoding: 'utf8' }).stdout; 33 | } 34 | 35 | export function replaceExactly(value: string, searchValue: string, replaceValue: string): string { 36 | return value.replace(searchValue, () => replaceValue); 37 | } 38 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { JestConfigWithTsJest } from 'ts-jest'; 2 | 3 | const jestConfig: JestConfigWithTsJest = { 4 | preset: 'ts-jest/presets/default-esm', 5 | moduleNameMapper: { 6 | '^(\\.{1,2}/.*)\\.js$': '$1', 7 | }, 8 | moduleFileExtensions: ['js', 'json', 'jsx', 'node', 'ts', 'tsx'], 9 | transform: { 10 | // '^.+\\.tsx?$' to process ts with `ts-jest` 11 | // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` 12 | // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` 13 | '^.+\\.m?[tj]sx?$': [ 14 | 'ts-jest', 15 | { 16 | useESM: true, 17 | }, 18 | ], 19 | }, 20 | extensionsToTreatAsEsm: ['.ts'], 21 | verbose: true, 22 | testEnvironment: 'node', 23 | coverageDirectory: './coverage', 24 | coverageProvider: 'v8', 25 | coverageReporters: ['lcov', 'text', 'html', 'json', 'cobertura', 'clover'], 26 | collectCoverageFrom: ['src/**/*.ts'], 27 | coveragePathIgnorePatterns: ['/node_modules/', 'src/__tests__/'], 28 | coverageThreshold: { 29 | global: { 30 | statements: 75, 31 | branches: 53, 32 | functions: 63, 33 | lines: 75, 34 | }, 35 | }, 36 | }; 37 | 38 | export default jestConfig; 39 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as cli from './cli.js'; 2 | import { parseArgv } from './configuration.js'; 3 | import * as log from './logger.js'; 4 | import * as utils from './utils.js'; 5 | import * as yargs from './yargs.js'; 6 | 7 | export async function main(): Promise { 8 | try { 9 | const argv = await yargs.setup(); 10 | const [diff2htmlOptions, configuration] = parseArgv(argv); 11 | 12 | const input = await cli.getInput(configuration.inputSource, argv.extraArguments, configuration.ignore); 13 | 14 | if (!input && !diff2htmlOptions.renderNothingWhenEmpty) { 15 | process.exitCode = 3; 16 | log.error('The input is empty. Try again.'); 17 | return; 18 | } 19 | 20 | if (configuration.diffyType !== undefined) { 21 | await cli.postToDiffy(input, configuration.diffyType); 22 | return; 23 | } 24 | 25 | const output = cli.getOutput(diff2htmlOptions, configuration, input); 26 | 27 | if (configuration.outputDestinationFile) { 28 | utils.writeFile(configuration.outputDestinationFile, output); 29 | } else { 30 | switch (configuration.outputDestinationType) { 31 | case 'preview': 32 | return cli.preview(output, configuration.formatType); 33 | 34 | case 'stdout': 35 | return log.print(output); 36 | } 37 | } 38 | } catch (error) { 39 | if (process.exitCode === undefined || process.exitCode === 0) { 40 | process.exitCode = 1; 41 | } 42 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 43 | const anyError = error as any; 44 | log.error(anyError); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 4.x | :white_check_mark: | 8 | | < 4.x | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | We take all security bugs in `diff2html-cli` seriously. Thank you for the help improving the security of 13 | `diff2html-cli`. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your 14 | contributions. 15 | 16 | Report security bugs by emailing the lead maintainer at `rtfrodrigo [at] gmail [dot] com`. 17 | 18 | The lead maintainer will acknowledge your email within 48 hours, and will send a more detailed response within 48 hours 19 | indicating the next steps in handling your report. After the initial reply to your report, the security team will 20 | endeavor to keep you informed of the progress towards a fix and full announcement, and may ask for additional 21 | information or guidance. 22 | 23 | Report security bugs in third-party modules to the person or team maintaining the module. 24 | 25 | ## Disclosure Policy 26 | 27 | When the security team receives a security bug report, they will assign it to a primary handler. This person will 28 | coordinate the fix and release process, involving the following steps: 29 | 30 | - Confirm the problem and determine the affected versions. 31 | - Audit code to find any potential similar problems. 32 | - Prepare fixes for all releases still under maintenance. These fixes will be released as fast as possible. 33 | 34 | ## Comments on this Policy 35 | 36 | If you have suggestions on how this process could be improved please submit a pull request. 37 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaVersion: 2018, 5 | sourceType: 'module', 6 | }, 7 | env: { 8 | browser: true, 9 | es6: true, 10 | node: true, 11 | }, 12 | globals: { 13 | Atomics: 'readonly', 14 | SharedArrayBuffer: 'readonly', 15 | document: 'readonly', 16 | navigator: 'readonly', 17 | window: 'readonly', 18 | }, 19 | extends: [ 20 | 'eslint:recommended', 21 | 'plugin:@typescript-eslint/eslint-recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:json/recommended', 24 | 'plugin:promise/recommended', 25 | 'plugin:import/errors', 26 | 'plugin:import/warnings', 27 | 'plugin:import/typescript', 28 | 'plugin:node/recommended', 29 | 'plugin:sonarjs/recommended', 30 | 'plugin:jest/recommended', 31 | 'plugin:jest/style', 32 | 'prettier', 33 | ], 34 | plugins: ['@typescript-eslint', 'json', 'promise', 'import', 'node', 'sonarjs', 'jest', 'optimize-regex'], 35 | rules: { 36 | 'optimize-regex/optimize-regex': 'error', 37 | 'import/no-unresolved': 'error', 38 | 'node/no-missing-import': 'off', 39 | // 'node/no-missing-import': [ 40 | // 'error', 41 | // { 42 | // tryExtensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], 43 | // }, 44 | // ], 45 | // We don't need this since we are using transpilation 46 | 'node/no-unsupported-features/es-syntax': 'off', 47 | 'no-process-exit': 'off', 48 | // Too verbose 49 | 'sonarjs/no-duplicate-string': 'off', 50 | // Too verbose 51 | 'sonarjs/cognitive-complexity': 'off', 52 | }, 53 | settings: { 54 | // This loads /tsconfig.json to eslint 55 | 'import/resolver': { 56 | typescript: { alwaysTryTypes: true }, 57 | }, 58 | 'import/parsers': { 59 | '@typescript-eslint/parser': ['.ts', '.tsx'], 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/__tests__/main-tests.ts: -------------------------------------------------------------------------------- 1 | import { jest, expect } from '@jest/globals'; 2 | 3 | import { Configuration, InputType } from '../types'; 4 | import { Diff2HtmlConfig } from 'diff2html'; 5 | 6 | let command: typeof import('../main'); 7 | 8 | const getInputSpy: jest.Mock<(inputType: InputType, inputArgs: string[], ignore: string[]) => Promise> = 9 | jest.fn(); 10 | const getOutputSpy: jest.Mock<(options: Diff2HtmlConfig, config: Configuration, input: string) => string> = jest.fn(); 11 | const previewSpy: jest.Mock<(content: string, format: string) => void> = jest.fn(); 12 | jest.unstable_mockModule('../cli', async () => ({ 13 | getInput: getInputSpy, 14 | getOutput: getOutputSpy, 15 | preview: previewSpy, 16 | })); 17 | 18 | beforeEach(async () => { 19 | command = await import('../main'); 20 | }); 21 | 22 | afterEach(() => { 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | process.argv = ['node', 'diff2html.js', '-i', 'file', '--', 'test']; 27 | 28 | describe('cli', () => { 29 | test('should parse input and run', async () => { 30 | getInputSpy.mockReturnValue(Promise.resolve('input')); 31 | getOutputSpy.mockReturnValue('output'); 32 | previewSpy.mockReturnValue(); 33 | 34 | await command.main(); 35 | 36 | expect(getInputSpy).toHaveBeenCalledTimes(1); 37 | expect(getInputSpy).toHaveBeenCalledWith('file', ['test'], []); 38 | 39 | expect(getOutputSpy).toHaveBeenCalledTimes(1); 40 | expect(getOutputSpy).toHaveBeenCalledWith( 41 | { 42 | diffMaxChanges: undefined, 43 | diffMaxLineLength: undefined, 44 | diffStyle: 'word', 45 | drawFileList: true, 46 | matchWordsThreshold: 0.25, 47 | matching: 'none', 48 | matchingMaxComparisons: 1000, 49 | maxLineLengthHighlight: 10000, 50 | maxLineSizeInBlockForComparison: 200, 51 | outputFormat: 'line-by-line', 52 | renderNothingWhenEmpty: false, 53 | colorScheme: 'auto', 54 | }, 55 | { 56 | diffyType: undefined, 57 | fileContentToggle: true, 58 | formatType: 'html', 59 | highlightCode: true, 60 | htmlWrapperTemplate: expect.stringContaining('diff2html-cli/template.html'), 61 | ignore: [], 62 | inputSource: 'file', 63 | outputDestinationFile: undefined, 64 | outputDestinationType: 'preview', 65 | pageHeader: 'Diff to HTML by rtfpessoa', 66 | pageTitle: 'Diff to HTML by rtfpessoa', 67 | showFilesOpen: false, 68 | synchronisedScroll: true, 69 | }, 70 | 'input', 71 | ); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import { dirname, resolve } from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | const __dirname = dirname(fileURLToPath(import.meta.url)); 5 | 6 | import { Configuration } from './types.js'; 7 | import { Argv } from './yargs.js'; 8 | import { Diff2HtmlConfig } from 'diff2html'; 9 | import { OutputFormatType, DiffStyleType, LineMatchingType } from 'diff2html/lib/types.js'; 10 | 11 | export function parseArgv(argv: Argv): [Diff2HtmlConfig, Configuration] { 12 | const diff2htmlOptions: Diff2HtmlConfig = { 13 | outputFormat: 14 | argv.style === 'side' 15 | ? OutputFormatType.SIDE_BY_SIDE 16 | : argv.style === 'line' 17 | ? OutputFormatType.LINE_BY_LINE 18 | : undefined, 19 | diffStyle: 20 | argv.diffStyle === 'char' ? DiffStyleType.CHAR : argv.diffStyle === 'word' ? DiffStyleType.WORD : undefined, 21 | matching: 22 | argv.matching === 'lines' 23 | ? LineMatchingType.LINES 24 | : argv.matching === 'words' 25 | ? LineMatchingType.WORDS 26 | : argv.matching === 'none' 27 | ? LineMatchingType.NONE 28 | : undefined, 29 | drawFileList: argv.summary !== 'hidden', 30 | matchWordsThreshold: argv.matchWordsThreshold, 31 | matchingMaxComparisons: argv.matchingMaxComparisons, 32 | diffMaxChanges: argv.diffMaxChanges, 33 | diffMaxLineLength: argv.diffMaxLineLength, 34 | renderNothingWhenEmpty: argv.renderNothingWhenEmpty, 35 | maxLineSizeInBlockForComparison: argv.maxLineSizeInBlockForComparison, 36 | maxLineLengthHighlight: argv.maxLineLengthHighlight, 37 | colorScheme: argv.colorScheme, 38 | }; 39 | 40 | const defaultPageTitle = 'Diff to HTML by rtfpessoa'; 41 | const defaultPageHeader = 'Diff to HTML by rtfpessoa'; 42 | const defaultWrapperTemplate = resolve(__dirname, '..', 'template.html'); 43 | const configuration: Configuration = { 44 | showFilesOpen: argv.summary === 'open' || false, 45 | fileContentToggle: argv.fileContentToggle, 46 | synchronisedScroll: argv.synchronisedScroll, 47 | highlightCode: argv.highlightCode, 48 | formatType: argv.format, 49 | outputDestinationType: argv.output, 50 | outputDestinationFile: argv.file, 51 | inputSource: argv.input, 52 | diffyType: argv.diffy, 53 | htmlWrapperTemplate: argv.htmlWrapperTemplate || defaultWrapperTemplate, 54 | pageTitle: argv.title || defaultPageTitle, 55 | pageHeader: argv.title || defaultPageHeader, 56 | ignore: argv.ignore || [], 57 | }; 58 | 59 | return [diff2htmlOptions, configuration]; 60 | } 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to diff2html-cli 2 | 3 | ### Main rules 4 | 5 | - Before you open a ticket or send a pull request, [search](https://github.com/rtfpessoa/diff2html-cli/issues) for 6 | previous discussions about the same feature or issue. Add to the earlier ticket if you find one. 7 | 8 | - If you're proposing a new feature, make sure you create an issue to let other contributors know what you are working 9 | on. 10 | 11 | - Before sending a pull request make sure your code is tested. 12 | 13 | - Before sending a pull request for a feature, be sure to run tests with `npm test`. 14 | 15 | - Use the same coding style as the rest of the codebase, most of the check can be performed with `npm run style`. 16 | 17 | - Use `git rebase` (not `git merge`) to sync your work from time to time with the master branch. 18 | 19 | - After creating your pull request make sure the build is passing on 20 | [CircleCI](https://circleci.com/gh/rtfpessoa/diff2html-cli) and that 21 | [Codacy](https://www.codacy.com/app/Codacy/diff2html-cli) is also confident in the code quality. 22 | 23 | ### Commit Style 24 | 25 | Writing good commit logs is important. A commit log should describe what changed and why. Follow these guidelines when 26 | writing one: 27 | 28 | 1. The first line should be 50 characters or less and contain a short description of the change prefixed with the name 29 | of the changed subsystem (e.g. "net: add localAddress and localPort to Socket"). 30 | 2. Keep the second line blank. 31 | 3. Wrap all other lines at 72 columns. 32 | 33 | A good commit log can look something like this: 34 | 35 | ``` 36 | subsystem: explaining the commit in one line 37 | 38 | Body of commit message is a few lines of text, explaining things 39 | in more detail, possibly giving some background about the issue 40 | being fixed, etc. etc. 41 | 42 | The body of the commit message can be several paragraphs, and 43 | please do proper word-wrap and keep columns shorter than about 44 | 72 characters or so. That way `git log` will show things 45 | nicely even when it is indented. 46 | ``` 47 | 48 | ### Developer's Certificate of Origin 1.0 49 | 50 | By making a contribution to this project, I certify that: 51 | 52 | - (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source 53 | license indicated in the file; or 54 | - (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate 55 | open source license and I have the right under that license to submit that work with modifications, whether created in 56 | whole or in part by me, under the same open source license (unless I am permitted to submit under a different 57 | license), as indicated in the file; or 58 | - (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not 59 | modified it. 60 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, 8 | socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | rtfrodrigo [at] gmail [dot] com. All complaints will be reviewed and investigated and will result in a response that is 48 | deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with 49 | regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 58 | 59 | [homepage]: https://www.contributor-covenant.org 60 | 61 | For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "diff2html-cli", 3 | "version": "4.2.1", 4 | "homepage": "https://diff2html.xyz/index.html#cli", 5 | "description": "Fast Diff to colorized HTML", 6 | "keywords": [ 7 | "git", 8 | "diff", 9 | "pretty", 10 | "parser", 11 | "diffparser", 12 | "gen", 13 | "generator", 14 | "side", 15 | "line", 16 | "side-by-side", 17 | "line-by-line", 18 | "character", 19 | "highlight", 20 | "pretty", 21 | "color", 22 | "html", 23 | "diff2html", 24 | "difftohtml", 25 | "colorized" 26 | ], 27 | "author": { 28 | "name": "Rodrigo Fernandes", 29 | "email": "rtfrodrigo@gmail.com" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git://github.com/rtfpessoa/diff2html-cli.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/rtfpessoa/diff2html-cli/issues" 37 | }, 38 | "engines": { 39 | "node": ">=16" 40 | }, 41 | "preferGlobal": true, 42 | "scripts": { 43 | "eslint": "eslint --ignore-path .gitignore \"**/*.{js,jsx,ts,tsx,json}\"", 44 | "lint:check": "yarn run eslint", 45 | "lint:fix": "yarn run eslint --fix", 46 | "prettier": "prettier --ignore-path .gitignore '**/*.+(js|jsx|ts|tsx|json|css|html|md|mdx)'", 47 | "format:check": "yarn run prettier --check", 48 | "format:fix": "yarn run prettier --write", 49 | "build": "rm -rf lib; tsc -p tsconfig.json --outDir lib", 50 | "gen": "yarn run gen:toc", 51 | "gen:toc-base": "markdown-toc --maxdepth 3 --bullets='-' -i", 52 | "gen:toc": "yarn run gen:toc-base README.md", 53 | "test": "is-ci 'test:coverage' 'test:watch'", 54 | "test:coverage": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --coverage", 55 | "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch", 56 | "test:debug": "node --experimental-vm-modules --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch", 57 | "coverage:open": "yarn run test:coverage && open ./coverage/index.html", 58 | "validate": "yarn run format:check && yarn run lint:check && yarn run build && yarn run test:coverage", 59 | "fix": "yarn run format:fix && yarn run lint:fix", 60 | "preversion": "yarn run validate", 61 | "version": "git add -A package.json", 62 | "prepare": "husky install" 63 | }, 64 | "type": "module", 65 | "bin": { 66 | "diff2html": "./bin/diff2html.js" 67 | }, 68 | "main": "./lib/diff2html.js", 69 | "types": "./lib/diff2html.d.ts", 70 | "lint-staged": { 71 | "**/*.+(js|jsx|ts|tsx|json)": [ 72 | "prettier --write", 73 | "eslint --fix" 74 | ], 75 | "**/*.+(css|html|md|mdx)": [ 76 | "prettier --write" 77 | ], 78 | "README.md": [ 79 | "yarn run gen:toc-base" 80 | ] 81 | }, 82 | "dependencies": { 83 | "clipboardy": "^4.0.0", 84 | "diff2html": "^3.4.47", 85 | "node-fetch": "^3.3.2", 86 | "open": "^10.0.3", 87 | "yargs": "^17.6.0" 88 | }, 89 | "devDependencies": { 90 | "@babel/core": "^7.23.7", 91 | "@babel/preset-env": "^7.23.8", 92 | "@babel/preset-typescript": "^7.23.3", 93 | "@jest/globals": "^29.7.0", 94 | "@types/hogan.js": "^3.0.5", 95 | "@types/jest": "^29.5.11", 96 | "@types/node": "20.11.0", 97 | "@types/node-fetch": "^2.6.10", 98 | "@types/request": "2.48.12", 99 | "@typescript-eslint/eslint-plugin": "6.18.1", 100 | "@typescript-eslint/parser": "6.18.1", 101 | "babel-jest": "^29.7.0", 102 | "eslint": "8.56.0", 103 | "eslint-config-prettier": "9.1.0", 104 | "eslint-import-resolver-typescript": "^3.6.1", 105 | "eslint-plugin-import": "2.29.1", 106 | "eslint-plugin-jest": "27.6.3", 107 | "eslint-plugin-json": "3.1.0", 108 | "eslint-plugin-node": "11.1.0", 109 | "eslint-plugin-optimize-regex": "1.2.1", 110 | "eslint-plugin-promise": "6.1.1", 111 | "eslint-plugin-sonarjs": "0.23.0", 112 | "husky": "8.0.3", 113 | "is-ci-cli": "2.2.0", 114 | "jest": "29.7.0", 115 | "lint-staged": "15.2.0", 116 | "markdown-toc": "^1.2.0", 117 | "prettier": "3.2.1", 118 | "ts-jest": "^29.1.1", 119 | "ts-node": "^10.9.2", 120 | "typescript": "5.3.3" 121 | }, 122 | "license": "MIT", 123 | "files": [ 124 | "bin", 125 | "lib", 126 | "template.html" 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /src/__tests__/cli-tests.ts: -------------------------------------------------------------------------------- 1 | import { jest } from '@jest/globals'; 2 | 3 | let cli: typeof import('../cli'); 4 | 5 | const readFileSpy: jest.Mock<() => string> = jest.fn(); 6 | const readStdinSpy: jest.Mock<() => Promise> = jest.fn(); 7 | const writeFileSpy: jest.Mock<(filePath: string, content: string) => void> = jest.fn(); 8 | const executeSpy: jest.Mock<(executable: string, args: string[]) => string> = jest.fn(); 9 | jest.unstable_mockModule('../utils', async () => ({ 10 | readFile: readFileSpy, 11 | readStdin: readStdinSpy, 12 | execute: executeSpy, 13 | writeFile: writeFileSpy, 14 | })); 15 | 16 | const putSpy: jest.Mock<(url: string, payload: object) => Promise> = jest.fn(); 17 | jest.unstable_mockModule('../http-utils', async () => ({ 18 | put: putSpy, 19 | })); 20 | 21 | beforeEach(async () => { 22 | cli = await import('../cli'); 23 | }); 24 | 25 | afterEach(() => { 26 | jest.clearAllMocks(); 27 | }); 28 | 29 | process.argv = ['node', 'diff2html.js', '-i', 'stdin', '--', 'test']; 30 | 31 | describe('cli', () => { 32 | describe('getInput', () => { 33 | test('should readFile when inputType is `file`', async () => { 34 | readFileSpy.mockReturnValue('contents'); 35 | 36 | await cli.getInput('file', ['lol', 'foo'], []); 37 | 38 | expect(readFileSpy).toHaveBeenCalledTimes(1); 39 | expect(readFileSpy).toHaveBeenCalledWith('lol'); 40 | }); 41 | 42 | test('should readStdin when inputType is `stdin`', async () => { 43 | readStdinSpy.mockReturnValue(Promise.resolve('contents')); 44 | 45 | await cli.getInput('stdin', ['lol'], []); 46 | 47 | expect(readStdinSpy).toHaveBeenCalledTimes(1); 48 | expect(readStdinSpy).toHaveBeenCalledWith(); 49 | }); 50 | 51 | describe('should execute command when inputType is `command`', () => { 52 | test('should pass args to git command and add `--no-color` flag', async () => { 53 | executeSpy.mockReturnValue(''); 54 | 55 | await cli.getInput('command', ['foo'], []); 56 | 57 | expect(executeSpy).toHaveBeenCalledTimes(1); 58 | expect(executeSpy).toHaveBeenCalledWith('git', ['diff', '--no-color', 'foo']); 59 | }); 60 | 61 | test('should not add `--no-color` flag if already in args', async () => { 62 | executeSpy.mockReturnValue(''); 63 | 64 | await cli.getInput('command', ['--no-color'], []); 65 | 66 | expect(executeSpy).toHaveBeenCalledTimes(1); 67 | expect(executeSpy).toHaveBeenCalledWith('git', ['diff', '--no-color']); 68 | }); 69 | 70 | test('should add default flags if no args are provided', async () => { 71 | executeSpy.mockReturnValue(''); 72 | 73 | await cli.getInput('command', [], []); 74 | 75 | expect(executeSpy).toHaveBeenCalledTimes(1); 76 | expect(executeSpy).toHaveBeenCalledWith('git', ['diff', '--no-color', '-M', '-C', 'HEAD']); 77 | }); 78 | 79 | describe('when receiving paths to ignore', () => { 80 | test('should add the `--` separator if it is not already in args', async () => { 81 | executeSpy.mockReturnValue(''); 82 | 83 | await cli.getInput('command', ['foo'], ['some/path']); 84 | 85 | expect(executeSpy).toHaveBeenCalledTimes(1); 86 | expect(executeSpy).toHaveBeenCalledWith('git', ['diff', '--no-color', 'foo', '--', ':(exclude)some/path']); 87 | }); 88 | 89 | test('should not add `--` flag if it is already in args', async () => { 90 | executeSpy.mockReturnValue(''); 91 | 92 | await cli.getInput('command', ['foo', '--', 'other/path'], ['some/path']); 93 | 94 | expect(executeSpy).toHaveBeenCalledTimes(1); 95 | expect(executeSpy).toHaveBeenCalledWith('git', [ 96 | 'diff', 97 | '--no-color', 98 | 'foo', 99 | '--', 100 | 'other/path', 101 | ':(exclude)some/path', 102 | ]); 103 | }); 104 | }); 105 | 106 | test('should not add `--` flag when there are no paths to ignore', async () => { 107 | executeSpy.mockReturnValue(''); 108 | 109 | await cli.getInput('command', ['bar'], []); 110 | 111 | expect(executeSpy).toHaveBeenCalledTimes(1); 112 | expect(executeSpy).toHaveBeenCalledWith('git', ['diff', '--no-color', 'bar']); 113 | }); 114 | }); 115 | }); 116 | 117 | describe('preview', () => { 118 | test('should call `utils.writeFile`', async () => { 119 | writeFileSpy.mockReturnValue(); 120 | 121 | cli.preview('a', 'b'); 122 | 123 | expect(writeFileSpy).toHaveBeenCalledTimes(1); 124 | }); 125 | }); 126 | 127 | describe('postToDiffy', () => { 128 | test('should call `http.put`', async () => { 129 | putSpy.mockReturnValue(Promise.resolve({ id: 'foo' })); 130 | 131 | await cli.postToDiffy('a', 'print'); 132 | 133 | expect(putSpy).toHaveBeenCalledTimes(1); 134 | expect(putSpy).toHaveBeenCalledWith('https://diffy.org/api/diff/', { diff: 'a' }); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /.github/workflows/test-and-publish.yml: -------------------------------------------------------------------------------- 1 | name: test-and-publish 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | environment: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | version: 12 | runs-on: ubuntu-latest 13 | container: 14 | image: codacy/git-version 15 | steps: 16 | - uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | - name: Fix tar access 20 | run: apk add --update --no-progress tar 21 | - name: Fix git access 22 | run: | 23 | git config --global --add safe.directory /__w/diff2html-cli/diff2html-cli 24 | - name: Get next version 25 | run: | 26 | export NEXT_VERSION="$(/bin/git-version --folder=$PWD --release-branch=master)" 27 | echo "Next version is ${NEXT_VERSION}" 28 | echo "${NEXT_VERSION}" > .version 29 | echo "version=${NEXT_VERSION}" >> $GITHUB_ENV 30 | - name: Get next npm tag name 31 | run: | 32 | if [ "${GITHUB_REF_NAME}" = "master" ]; then 33 | export PUBLISH_TAG="latest" 34 | elif [ "${GITHUB_REF_NAME}" = "next" ]; then 35 | export PUBLISH_TAG="next" 36 | else 37 | export PUBLISH_TAG="pr" 38 | fi 39 | echo "Next tag is ${PUBLISH_TAG}" 40 | echo "${PUBLISH_TAG}" > .tag 41 | - name: Upload versions 42 | uses: actions/upload-artifact@v3 43 | with: 44 | name: versions 45 | if-no-files-found: error 46 | path: | 47 | .version 48 | .tag 49 | 50 | build: 51 | runs-on: ubuntu-latest 52 | needs: [version] 53 | strategy: 54 | matrix: 55 | node-version: [16.x, 18.x, 20.x] 56 | steps: 57 | - uses: actions/checkout@v3 58 | with: 59 | fetch-depth: 0 60 | - name: Use Node.js ${{ matrix.node-version }} 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: ${{ matrix.node-version }} 64 | cache: 'yarn' 65 | - name: Log environment setup 66 | run: | 67 | node -v 68 | yarn -v 69 | - name: Install dependencies 70 | run: yarn install --ignore-engines 71 | - name: Build library 72 | run: yarn run validate 73 | 74 | publish: 75 | runs-on: ubuntu-latest 76 | needs: [build] 77 | environment: ${{ inputs.environment }} 78 | steps: 79 | - uses: actions/checkout@v3 80 | with: 81 | fetch-depth: 0 82 | - name: Download versions 83 | uses: actions/download-artifact@v3 84 | with: 85 | name: versions 86 | - name: Store version 87 | run: echo "version=$(cat .version)" >> $GITHUB_ENV 88 | - name: Configure Git 89 | run: | 90 | git config user.email "gh-actions@users.noreply.github.com" 91 | git config user.name "GitHub Actions" 92 | - name: Tag commit 93 | uses: tvdias/github-tagger@v0.0.1 94 | with: 95 | repo-token: '${{ secrets.GITHUB_TOKEN }}' 96 | tag: '${{ env.version }}' 97 | - name: Install dependencies 98 | run: yarn 99 | - uses: actions/setup-node@v3 100 | with: 101 | registry-url: 'https://registry.npmjs.org' 102 | node-version: '18.x' 103 | - name: Configure Yarn version 104 | env: 105 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 106 | run: | 107 | rm -f .npmrc 108 | touch .npmrc 109 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc 110 | echo "registry=https://registry.npmjs.org/" >> .npmrc 111 | echo "access=public" >> .npmrc 112 | echo "save-exact=true" >> .npmrc 113 | yarn config set version-tag-prefix "" 114 | yarn config set version-git-message "Release version %s" 115 | - name: Version package 116 | run: | 117 | # Update version in packages to publish 118 | yarn version --non-interactive --new-version $(cat .version) 119 | - name: Publish to NPM 120 | run: yarn publish --tag $(cat .tag) --non-interactive --new-version $(cat .version) 121 | - uses: actions/setup-node@v3 122 | with: 123 | node-version: '18.x' 124 | registry-url: 'https://npm.pkg.github.com' 125 | - name: Configure Yarn version 126 | env: 127 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 128 | run: | 129 | rm -f .npmrc 130 | touch .npmrc 131 | echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" >> .npmrc 132 | echo "@rtfpessoa:registry=https://npm.pkg.github.com/" >> .npmrc 133 | echo "access=public" >> .npmrc 134 | echo "save-exact=true" >> .npmrc 135 | yarn config set version-tag-prefix "" 136 | yarn config set version-git-message "Release version %s" 137 | - name: Version package 138 | run: | 139 | # Update version in packages to publish 140 | yarn version --non-interactive --new-version $(cat .version) 141 | - name: Publish to GPR 142 | run: | 143 | # HACK: Override npm package name to be able to publish in GitHub 144 | sed -i 's/^ "name":.*/ "name": "@rtfpessoa\/diff2html-cli",/g' package.json 145 | echo "Going to publish version $(cat .version) to GitHub" 146 | yarn publish --tag $(cat .tag) --non-interactive --new-version $(cat .version) 147 | # HACK: Restore npm package name 148 | sed -i 's/^ "name":.*/ "name": "diff2html-cli",/g' package.json 149 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import { createRequire } from 'node:module'; 2 | const require = createRequire(import.meta.url); 3 | 4 | import fs from 'fs'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | 8 | import clipboardy from 'clipboardy'; 9 | import open from 'open'; 10 | import { parse, html, Diff2HtmlConfig } from 'diff2html'; 11 | 12 | import { put } from './http-utils.js'; 13 | import * as log from './logger.js'; 14 | import { Configuration, InputType, DiffyType } from './types.js'; 15 | import * as utils from './utils.js'; 16 | import { ColorSchemeType } from 'diff2html/lib/types.js'; 17 | 18 | const defaultArgs = ['-M', '-C', 'HEAD']; 19 | 20 | const lightGitHubTheme = ``; 21 | const darkGitHubTheme = ``; 22 | const autoGitHubTheme = ` 23 | `; 24 | 25 | const lightBaseStyle = ``; 33 | 34 | const darkBaseStyle = ``; 42 | 43 | const autoBaseStyle = ``; 61 | 62 | function generateGitDiffArgs(gitArgsArr: string[], ignore: string[]): string[] { 63 | const gitDiffArgs: string[] = ['diff']; 64 | 65 | if (!gitArgsArr.includes('--no-color')) gitDiffArgs.push('--no-color'); 66 | 67 | if (gitArgsArr.length === 0) Array.prototype.push.apply(gitDiffArgs, defaultArgs); 68 | 69 | Array.prototype.push.apply(gitDiffArgs, gitArgsArr); 70 | 71 | if (ignore.length > 0) { 72 | if (!gitArgsArr.includes('--')) gitDiffArgs.push('--'); 73 | Array.prototype.push.apply( 74 | gitDiffArgs, 75 | ignore.map(path => `:(exclude)${path}`), 76 | ); 77 | } 78 | 79 | return gitDiffArgs; 80 | } 81 | 82 | function runGitDiff(gitArgsArr: string[], ignore: string[]): string { 83 | const gitDiffArgs = generateGitDiffArgs(gitArgsArr, ignore); 84 | return utils.execute('git', gitDiffArgs); 85 | } 86 | 87 | function prepareHTML(diffHTMLContent: string, config: Configuration, colorScheme?: ColorSchemeType): string { 88 | const template = utils.readFile(config.htmlWrapperTemplate); 89 | 90 | const diff2htmlPath = path.join(path.dirname(require.resolve('diff2html')), '..'); 91 | 92 | const cssFilePath = path.resolve(diff2htmlPath, 'bundles', 'css', 'diff2html.min.css'); 93 | const cssContent = utils.readFile(cssFilePath); 94 | 95 | const jsUiFilePath = path.resolve(diff2htmlPath, 'bundles', 'js', 'diff2html-ui-slim.min.js'); 96 | const jsUiContent = utils.readFile(jsUiFilePath); 97 | 98 | const pageTitle = config.pageTitle; 99 | const pageHeader = config.pageHeader; 100 | 101 | const gitHubTheme = 102 | colorScheme === 'light' ? lightGitHubTheme : colorScheme === 'dark' ? darkGitHubTheme : autoGitHubTheme; 103 | 104 | const baseStyle = colorScheme === 'light' ? lightBaseStyle : colorScheme === 'dark' ? darkBaseStyle : autoBaseStyle; 105 | 106 | /* HACK: 107 | * Replace needs to receive a function as the second argument to perform an exact replacement. 108 | * This will avoid the replacements from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter 109 | */ 110 | return [ 111 | { searchValue: '', replaceValue: pageTitle }, 112 | { 113 | searchValue: '', 114 | replaceValue: `${baseStyle}\n${gitHubTheme}\n`, 115 | }, 116 | { searchValue: '', replaceValue: `` }, 117 | { 118 | searchValue: '//diff2html-fileListToggle', 119 | replaceValue: `diff2htmlUi.fileListToggle(${config.showFilesOpen});`, 120 | }, 121 | { 122 | searchValue: '//diff2html-fileContentToggle', 123 | replaceValue: config.fileContentToggle ? `diff2htmlUi.fileContentToggle();` : '', 124 | }, 125 | { 126 | searchValue: '//diff2html-synchronisedScroll', 127 | replaceValue: config.synchronisedScroll ? `diff2htmlUi.synchronisedScroll();` : '', 128 | }, 129 | { 130 | searchValue: '//diff2html-highlightCode', 131 | replaceValue: config.highlightCode ? `diff2htmlUi.highlightCode();` : '', 132 | }, 133 | { searchValue: '', replaceValue: pageHeader }, 134 | { searchValue: '', replaceValue: diffHTMLContent }, 135 | ].reduce( 136 | (previousValue, replacement) => 137 | utils.replaceExactly(previousValue, replacement.searchValue, replacement.replaceValue), 138 | template, 139 | ); 140 | } 141 | 142 | /** 143 | * Get unified diff input from type 144 | * @param inputType - a string `file`, `stdin`, or `command` 145 | * @param inputArgs - a string array 146 | * @param ignore - a string array 147 | */ 148 | export async function getInput(inputType: InputType, inputArgs: string[], ignore: string[]): Promise { 149 | switch (inputType) { 150 | case 'file': 151 | return utils.readFile(inputArgs[0]); 152 | 153 | case 'stdin': 154 | return utils.readStdin(); 155 | 156 | case 'command': 157 | return runGitDiff(inputArgs, ignore); 158 | } 159 | } 160 | 161 | export function getOutput(options: Diff2HtmlConfig, config: Configuration, input: string): string { 162 | if (config.htmlWrapperTemplate && !fs.existsSync(config.htmlWrapperTemplate)) { 163 | process.exitCode = 4; 164 | throw new Error(`Template ('${config.htmlWrapperTemplate}') not found!`); 165 | } 166 | 167 | const diffJson = parse(input, options); 168 | 169 | switch (config.formatType) { 170 | case 'html': { 171 | const htmlContent = html(diffJson, { ...options }); 172 | return prepareHTML(htmlContent, config, options.colorScheme); 173 | } 174 | case 'json': { 175 | return JSON.stringify(diffJson); 176 | } 177 | } 178 | } 179 | 180 | export function preview(content: string, format: string): void { 181 | const filename = `diff.${format}`; 182 | const filePath: string = path.resolve(os.tmpdir(), filename); 183 | utils.writeFile(filePath, content); 184 | open(filePath, { wait: false }); 185 | } 186 | 187 | type CreateDiffResponse = { id: string }; 188 | 189 | type ApiError = { error: string }; 190 | 191 | function isCreateDiffResponse(obj: unknown): obj is CreateDiffResponse { 192 | return (obj as CreateDiffResponse).id !== undefined; 193 | } 194 | 195 | function isApiError(obj: unknown): obj is ApiError { 196 | return (obj as ApiError).error !== undefined; 197 | } 198 | 199 | export async function postToDiffy(diff: string, diffyOutput: DiffyType): Promise { 200 | const response = await put('https://diffy.org/api/diff/', { diff: diff }); 201 | 202 | if (!isCreateDiffResponse(response)) { 203 | if (isApiError(response)) { 204 | throw new Error(response.error); 205 | } else { 206 | throw new Error( 207 | `Could not find 'id' of created diff in the response json.\nBody:\n\n${JSON.stringify(response, null, 2)}`, 208 | ); 209 | } 210 | } 211 | 212 | const url = `https://diffy.org/diff/${response.id}`; 213 | 214 | log.print('Link powered by https://diffy.org'); 215 | log.print(url); 216 | 217 | if (diffyOutput === 'browser') { 218 | open(url); 219 | } else if (diffyOutput === 'pbcopy') { 220 | clipboardy.writeSync(url); 221 | } 222 | 223 | return url; 224 | } 225 | -------------------------------------------------------------------------------- /src/yargs.ts: -------------------------------------------------------------------------------- 1 | import yargs from 'yargs'; 2 | import { ColorSchemeType } from 'diff2html/lib/types.js'; 3 | 4 | import { 5 | StyleType, 6 | SummaryType, 7 | LineMatchingType, 8 | FormatType, 9 | InputType, 10 | OutputType, 11 | DiffyType, 12 | DiffStyleType, 13 | } from './types.js'; 14 | 15 | export type Argv = { 16 | style: StyleType; 17 | fileContentToggle: boolean; 18 | synchronisedScroll: boolean; 19 | highlightCode: boolean; 20 | diffStyle: DiffStyleType; 21 | summary: SummaryType; 22 | matching: LineMatchingType; 23 | matchWordsThreshold: number; 24 | matchingMaxComparisons: number; 25 | diffMaxChanges?: number; 26 | diffMaxLineLength?: number; 27 | renderNothingWhenEmpty: boolean; 28 | maxLineSizeInBlockForComparison: number; 29 | maxLineLengthHighlight: number; 30 | format: FormatType; 31 | input: InputType; 32 | output: OutputType; 33 | diffy?: DiffyType; 34 | file?: string; 35 | htmlWrapperTemplate?: string; 36 | title?: string; 37 | ignore?: string[]; 38 | extraArguments: string[]; 39 | colorScheme: ColorSchemeType; 40 | }; 41 | 42 | const defaults: Argv = { 43 | style: 'line', 44 | fileContentToggle: true, 45 | synchronisedScroll: true, 46 | highlightCode: true, 47 | summary: 'closed', 48 | diffStyle: 'word', 49 | matching: 'none', 50 | matchWordsThreshold: 0.25, 51 | matchingMaxComparisons: 1000, 52 | renderNothingWhenEmpty: false, 53 | maxLineSizeInBlockForComparison: 200, 54 | maxLineLengthHighlight: 10000, 55 | format: 'html', 56 | input: 'command', 57 | output: 'preview', 58 | ignore: [], 59 | diffy: undefined, 60 | file: undefined, 61 | htmlWrapperTemplate: undefined, 62 | title: undefined, 63 | extraArguments: [], 64 | colorScheme: ColorSchemeType.AUTO, 65 | }; 66 | 67 | type ArgvChoices = { 68 | style: ReadonlyArray; 69 | summary: ReadonlyArray; 70 | diffStyle: ReadonlyArray; 71 | matching: ReadonlyArray; 72 | format: ReadonlyArray; 73 | input: ReadonlyArray; 74 | output: ReadonlyArray; 75 | diffy: ReadonlyArray; 76 | colorScheme: ReadonlyArray; 77 | }; 78 | 79 | const choices: ArgvChoices = { 80 | style: ['line', 'side'], 81 | summary: ['closed', 'open', 'hidden'], 82 | diffStyle: ['word', 'char'], 83 | matching: ['lines', 'words', 'none'], 84 | format: ['html', 'json'], 85 | input: ['file', 'command', 'stdin'], 86 | output: ['preview', 'stdout'], 87 | diffy: ['browser', 'pbcopy', 'print'], 88 | colorScheme: [ColorSchemeType.AUTO, ColorSchemeType.DARK, ColorSchemeType.LIGHT], 89 | }; 90 | 91 | export async function setup(): Promise { 92 | const currentYear = new Date().getFullYear(); 93 | 94 | const argv = await yargs(process.argv.slice(2)) 95 | .usage('Usage: diff2html [options] -- [diff args]') 96 | .option('style', { 97 | alias: 's', 98 | describe: 'Output style', 99 | nargs: 1, 100 | choices: choices.style, 101 | default: defaults.style, 102 | }) 103 | .option('fileContentToggle', { 104 | alias: 'fct', 105 | describe: 'Show viewed checkbox to toggle file content', 106 | type: 'boolean', 107 | default: defaults.fileContentToggle, 108 | }) 109 | .option('synchronisedScroll', { 110 | alias: 'sc', 111 | describe: 'Synchronised horizontal scroll', 112 | type: 'boolean', 113 | default: defaults.synchronisedScroll, 114 | }) 115 | .option('highlightCode', { 116 | alias: 'hc', 117 | describe: 'Highlight Code', 118 | type: 'boolean', 119 | default: defaults.highlightCode, 120 | }) 121 | .option('colorScheme', { 122 | alias: 'cs', 123 | describe: 'Color scheme of HTML output', 124 | choices: choices.colorScheme, 125 | default: defaults.colorScheme, 126 | }) 127 | .option('summary', { 128 | alias: 'su', 129 | describe: 'Show files summary', 130 | choices: choices.summary, 131 | default: defaults.summary, 132 | }) 133 | .option('diffStyle', { 134 | alias: 'd', 135 | describe: 'Diff style', 136 | nargs: 1, 137 | choices: choices.diffStyle, 138 | default: defaults.diffStyle, 139 | }) 140 | .option('matching', { 141 | alias: 'lm', 142 | describe: 'Diff line matching type', 143 | nargs: 1, 144 | choices: choices.matching, 145 | default: defaults.matching, 146 | }) 147 | .option('matchWordsThreshold', { 148 | alias: 'lmt', 149 | describe: 'Diff line matching word threshold', 150 | nargs: 1, 151 | type: 'number', 152 | default: defaults.matchWordsThreshold, 153 | }) 154 | .option('matchingMaxComparisons', { 155 | alias: 'lmm', 156 | describe: 'Diff line matching maximum line comparisons of a block of changes', 157 | nargs: 1, 158 | type: 'number', 159 | default: defaults.matchingMaxComparisons, 160 | }) 161 | .option('diffMaxChanges', { 162 | describe: 'Number of changed lines after which a file diff is deemed as too big and not displayed', 163 | nargs: 1, 164 | type: 'number', 165 | }) 166 | .option('diffMaxLineLength', { 167 | describe: 'Number of characters in a diff line after which a file diff is deemed as too big and not displayed', 168 | nargs: 1, 169 | type: 'number', 170 | }) 171 | .option('renderNothingWhenEmpty', { 172 | describe: 'Render nothing if the diff shows no change in its comparison', 173 | type: 'boolean', 174 | default: defaults.renderNothingWhenEmpty, 175 | }) 176 | .option('maxLineSizeInBlockForComparison', { 177 | describe: 'Maximum number of characters of the bigger line in a block to apply comparison', 178 | nargs: 1, 179 | type: 'number', 180 | default: defaults.maxLineSizeInBlockForComparison, 181 | }) 182 | .option('maxLineLengthHighlight', { 183 | describe: 'Maximum number of characters in a line to apply highlight', 184 | nargs: 1, 185 | type: 'number', 186 | default: defaults.maxLineLengthHighlight, 187 | }) 188 | .option('format', { 189 | alias: 'f', 190 | describe: 'Output format', 191 | nargs: 1, 192 | choices: choices.format, 193 | default: defaults.format, 194 | }) 195 | .option('input', { 196 | alias: 'i', 197 | describe: 'Diff input source', 198 | nargs: 1, 199 | choices: choices.input, 200 | default: defaults.input, 201 | }) 202 | .option('output', { 203 | alias: 'o', 204 | describe: 'Output destination', 205 | nargs: 1, 206 | choices: choices.output, 207 | default: defaults.output, 208 | }) 209 | .option('diffy', { 210 | alias: 'u', 211 | describe: 'Upload to diffy.org', 212 | nargs: 1, 213 | choices: choices.diffy, 214 | default: defaults.diffy, 215 | }) 216 | .option('file', { 217 | alias: 'F', 218 | describe: 'Send output to file (overrides output option)', 219 | nargs: 1, 220 | type: 'string', 221 | default: defaults.file, 222 | }) 223 | .option('htmlWrapperTemplate', { 224 | alias: 'hwt', 225 | describe: 'Use a custom template when generating markup', 226 | nargs: 1, 227 | type: 'string', 228 | default: defaults.htmlWrapperTemplate, 229 | }) 230 | .option('title', { 231 | alias: 't', 232 | describe: 'Page title for HTML output', 233 | nargs: 1, 234 | type: 'string', 235 | default: defaults.title, 236 | }) 237 | .option('ignore', { 238 | alias: 'ig', 239 | describe: 'ignore a file', 240 | type: 'array', 241 | default: defaults.ignore, 242 | }) 243 | .example( 244 | 'diff2html -s line -f html -d word -i command -o preview -- -M HEAD~1', 245 | 'diff last commit, line by line, word comparison between lines,' + 246 | 'previewed in the browser and input from git diff command', 247 | ) 248 | .example('diff2html -i file -- my-file-diff.diff', 'reading the input from a file') 249 | .example('diff2html -f json -o stdout -- -M HEAD~1', 'print json format to stdout') 250 | .example('diff2html -F my-pretty-diff.html -- -M HEAD~1', 'print to file') 251 | .example('diff2html --ig package-lock.json yarn.lock', 'ignore two particular files when generating the diff') 252 | .help() 253 | .alias('help', 'h') 254 | .alias('help', '?') 255 | .version() 256 | .alias('version', 'v') 257 | .epilog( 258 | `© 2014-${currentYear} rtfpessoa 259 | For more information, check out https://diff2html.xyz/ 260 | For support, check out https://github.com/rtfpessoa/diff2html-cli`, 261 | ) 262 | .strict(true) 263 | .recommendCommands().argv; 264 | 265 | return { 266 | ...argv, 267 | ignore: argv.ignore?.map(String) || [], 268 | extraArguments: argv._.map(String), 269 | }; 270 | } 271 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # diff2html-cli 2 | 3 | [![npm](https://img.shields.io/npm/v/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 4 | [![node](https://img.shields.io/node/v/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 5 | [![npm](https://img.shields.io/npm/l/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 6 | [![GitHub Actions](https://github.com/rtfpessoa/diff2html-cli/actions/workflows/release.yml/badge.svg)](https://github.com/rtfpessoa/diff2html-cli/actions/workflows/release.yml) 7 | 8 | [![npm weekly downloads](https://img.shields.io/npm/dw/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 9 | [![npm monthly downloads](https://img.shields.io/npm/dm/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 10 | [![npm yearly downloads](https://img.shields.io/npm/dy/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 11 | [![npm downloads](https://img.shields.io/npm/dt/diff2html-cli)](https://www.npmjs.com/package/diff2html-cli) 12 | 13 | Diff to Html generates pretty HTML diffs from unified and git diff output in your terminal 14 | 15 | ## Table of Contents 16 | 17 | 18 | 19 | - [Features](#features) 20 | - [Online Example](#online-example) 21 | - [Distributions](#distributions) 22 | - [Setup](#setup) 23 | - [Usage](#usage) 24 | - [Exit Status Codes](#exit-status-codes) 25 | - [Custom HTML wrapper template](#custom-html-wrapper-template) 26 | - [Examples](#examples) 27 | - [Contribute](#contribute) 28 | - [Developing](#developing) 29 | - [License](#license) 30 | - [Thanks](#thanks) 31 | 32 | 33 | 34 | ## Features 35 | 36 | - Unified diff and Git diff input 37 | 38 | - `line-by-line` and `side-by-side` diff 39 | 40 | - new and old line numbers 41 | 42 | - inserted and removed lines 43 | 44 | - GitHub like style 45 | 46 | - Code syntax highlight 47 | 48 | - Line similarity matching 49 | 50 | ## Online Example 51 | 52 | > Go to [Diff2HTML](https://diff2html.xyz/) 53 | 54 | ## Distributions 55 | 56 | - [NPM CLI](https://www.npmjs.org/package/diff2html-cli) 57 | - [NPM / Node.js library [ES5 & ES6]](https://github.com/rtfpessoa/diff2html) 58 | - [CDNJS](https://cdnjs.com/libraries/diff2html) 59 | - [WebJar](http://www.webjars.org/) 60 | 61 | ## Setup 62 | 63 | ```sh 64 | npm install -g diff2html-cli 65 | ``` 66 | 67 | ## Usage 68 | 69 | Usage: diff2html [ flags and/or options ] -- [git diff passthrough flags and options] 70 | 71 | | flag | alias | description | choices | default | 72 | | ----- | --------------------------------- | -------------------------------------------------------------------------------------------------- | ---------------------------- | --------- | 73 | | -s | --style | Output style | `line`, `side` | `line` | 74 | | --fct | --fileContentToggle | Adds a viewed checkbox to toggle file content | `true`, `false` | `true` | 75 | | --sc | --synchronisedScroll | Synchronised horizontal scroll | `true`, `false` | `true` | 76 | | --hc | --highlightCode | Highlight code | `true`, `false` | `true` | 77 | | --cs | --colorScheme | Color scheme | `auto`, `dark`, `light` | `auto` | 78 | | --su | --summary | Show files summary | `closed`, `open`, `hidden` | `closed` | 79 | | -d | --diffStyle | Diff style | `word`, `char` | `word` | 80 | | --lm | --matching | Diff line matching type | `lines`, `words`, `none` | `none` | 81 | | --lmt | --matchWordsThreshold | Diff line matching word threshold | | `0.25` | 82 | | --lmm | --matchingMaxComparisons | Diff line matching maximum line comparisons of a block of changes | | `2500` | 83 | | | --diffMaxChanges | Number of changed lines after which a file diff is deemed as too big and not displayed | | | 84 | | | --diffMaxLineLength | Number of characters in a diff line after which a file diff is deemed as too big and not displayed | | | 85 | | | --renderNothingWhenEmpty | Render nothing if the diff shows no change in its comparison | | `false` | 86 | | | --maxLineSizeInBlockForComparison | Maximum number of characters of the bigger line in a block to apply comparison | | `200` | 87 | | | --maxLineLengthHighlight | Maximum number of characters in a line to apply highlight | | `10000` | 88 | | --hwt | --htmlWrapperTemplate | Path to custom template to be rendered when using the `html` output format | `[string]` | 89 | | -t | --title | Page title for `html` output | `[string]` | 90 | | -f | --format | Output format | `html`, `json` | `html` | 91 | | -i | --input | Diff input source | `file`, `command`, `stdin` | `command` | 92 | | -o | --output | Output destination | `preview`, `stdout` | `preview` | 93 | | -u | --diffy | Upload to diffy.org | `browser`, `pbcopy`, `print` | | 94 | | -F | --file | Send output to file (overrides output option) | `[string]` | | 95 | | --ig | --ignore | Ignore particular files from the diff | `[string]` | | 96 | | -v | --version | Show version number | | | 97 | | -h | --help | Show help | | | 98 | 99 | ### Exit Status Codes 100 | 101 | - :tada: 0: Success 102 | - :dizzy_face: 1: Generic Error 103 | - :cold_sweat: 3: Input diff is empty 104 | - :cop: 4: Value of `--hwt | --htmlWrapperTemplate` is not a valid file 105 | 106 | ### Custom HTML wrapper template 107 | 108 | The template is a very based on a simple replace of several placeholders as coded 109 | https://github.com/rtfpessoa/diff2html-cli/blob/master/src/cli.ts#L40 110 | 111 | To provide a custom template you need to make sure you have the following comments and imports in your HTML, exactly as 112 | they are here: 113 | 114 | - Inside the `` tag 115 | 116 | ```html 117 | 118 | 119 | 120 | 129 | ``` 130 | 131 | - Inside the `` tag 132 | 133 | ```html 134 |
135 | 136 |
137 | ``` 138 | 139 | ### Examples 140 | 141 | `diff2html -s line -f html -d word -i command -o preview -- -M HEAD~1` 142 | 143 | - diff last commit, line by line, word comparison between lines, previewed in the browser and input from git diff 144 | command 145 | 146 | `diff2html -i file -- my-file-diff.diff` 147 | 148 | - reading the input from a file 149 | 150 | `diff -u file1.txt file2.txt | diff2html -i stdin` 151 | 152 | - reading diff from stdin 153 | 154 | `diff2html -f json -o stdout -- -M HEAD~1` 155 | 156 | - print json format to stdout 157 | 158 | `diff2html -F my-pretty-diff.html -- -M HEAD~1` 159 | 160 | - print to file 161 | 162 | `diff2html -F my-pretty-diff.html --hwt my-custom-template.html -- -M HEAD~1` 163 | 164 | - print to file using custom markup templates can include the following variables 165 | 166 | `diff2html --ig package-lock.json --ig yarn.lock` 167 | 168 | - Ignore `package-lock.json` and `yarn.lock` from the generated diff 169 | 170 | _NOTE_: notice the `--` in the examples 171 | 172 | ## Contribute 173 | 174 | This is a developer friendly project, all the contributions are welcome. To contribute just send a pull request with 175 | your changes following the guidelines described in `CONTRIBUTING.md`. I will try to review them as soon as possible. 176 | 177 | ## Developing 178 | 179 | Make some changes, `yarn build` and then `./bin/diff2html` 😉 180 | 181 | ## License 182 | 183 | Copyright 2014-present Rodrigo Fernandes. Released under the terms of the MIT license. 184 | 185 | ## Thanks 186 | 187 | This project is inspired in [pretty-diff](https://github.com/scottgonzalez/pretty-diff) by 188 | [Scott González](https://github.com/scottgonzalez). 189 | 190 | --- 191 | --------------------------------------------------------------------------------