UNPKG

react-diff-viewer-continued

Version:

Continuation of a simple and beautiful text diff viewer component made with diff and React

304 lines (303 loc) 18.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DiffMethod = exports.LineNumberPrefix = void 0; const jsx_runtime_1 = require("react/jsx-runtime"); const React = __importStar(require("react")); const classnames_1 = __importDefault(require("classnames")); const compute_lines_1 = require("./compute-lines"); Object.defineProperty(exports, "DiffMethod", { enumerable: true, get: function () { return compute_lines_1.DiffMethod; } }); const styles_1 = __importDefault(require("./styles")); const compute_hidden_blocks_1 = require("./compute-hidden-blocks"); const m = require('memoize-one'); const memoize = m.default || m; var LineNumberPrefix; (function (LineNumberPrefix) { LineNumberPrefix["LEFT"] = "L"; LineNumberPrefix["RIGHT"] = "R"; })(LineNumberPrefix || (exports.LineNumberPrefix = LineNumberPrefix = {})); class DiffViewer extends React.Component { constructor(props) { super(props); /** * Resets code block expand to the initial stage. Will be exposed to the parent component via * refs. */ this.resetCodeBlocks = () => { if (this.state.expandedBlocks.length > 0) { this.setState({ expandedBlocks: [], }); return true; } return false; }; /** * Pushes the target expanded code block to the state. During the re-render, * this value is used to expand/fold unmodified code. */ this.onBlockExpand = (id) => { const prevState = this.state.expandedBlocks.slice(); prevState.push(id); this.setState({ expandedBlocks: prevState, }); }; /** * Computes final styles for the diff viewer. It combines the default styles with the user * supplied overrides. The computed styles are cached with performance in mind. * * @param styles User supplied style overrides. */ this.computeStyles = memoize(styles_1.default); /** * Returns a function with clicked line number in the closure. Returns an no-op function when no * onLineNumberClick handler is supplied. * * @param id Line id of a line. */ this.onLineNumberClickProxy = (id) => { if (this.props.onLineNumberClick) { return (e) => this.props.onLineNumberClick(id, e); } return () => { }; }; /** * Maps over the word diff and constructs the required React elements to show word diff. * * @param diffArray Word diff information derived from line information. * @param renderer Optional renderer to format diff words. Useful for syntax highlighting. */ this.renderWordDiff = (diffArray, renderer) => { return diffArray.map((wordDiff, i) => { return ((0, jsx_runtime_1.jsx)("span", { className: (0, classnames_1.default)(this.styles.wordDiff, { [this.styles.wordAdded]: wordDiff.type === compute_lines_1.DiffType.ADDED, [this.styles.wordRemoved]: wordDiff.type === compute_lines_1.DiffType.REMOVED, }), children: renderer ? renderer(wordDiff.value) : wordDiff.value }, i)); }); }; /** * Maps over the line diff and constructs the required react elements to show line diff. It calls * renderWordDiff when encountering word diff. This takes care of both inline and split view line * renders. * * @param lineNumber Line number of the current line. * @param type Type of diff of the current line. * @param prefix Unique id to prefix with the line numbers. * @param value Content of the line. It can be a string or a word diff array. * @param additionalLineNumber Additional line number to be shown. Useful for rendering inline * diff view. Right line number will be passed as additionalLineNumber. * @param additionalPrefix Similar to prefix but for additional line number. */ this.renderLine = (lineNumber, type, prefix, value, additionalLineNumber, additionalPrefix) => { const lineNumberTemplate = `${prefix}-${lineNumber}`; const additionalLineNumberTemplate = `${additionalPrefix}-${additionalLineNumber}`; const highlightLine = this.props.highlightLines.includes(lineNumberTemplate) || this.props.highlightLines.includes(additionalLineNumberTemplate); const added = type === compute_lines_1.DiffType.ADDED; const removed = type === compute_lines_1.DiffType.REMOVED; const changed = type === compute_lines_1.DiffType.CHANGED; let content; if (Array.isArray(value)) { content = this.renderWordDiff(value, this.props.renderContent); } else if (this.props.renderContent) { content = this.props.renderContent(value); } else { content = value; } return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [!this.props.hideLineNumbers && ((0, jsx_runtime_1.jsx)("td", { onClick: lineNumber && this.onLineNumberClickProxy(lineNumberTemplate), className: (0, classnames_1.default)(this.styles.gutter, { [this.styles.emptyGutter]: !lineNumber, [this.styles.diffAdded]: added, [this.styles.diffRemoved]: removed, [this.styles.diffChanged]: changed, [this.styles.highlightedGutter]: highlightLine, }), children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.lineNumber, children: lineNumber }) })), !this.props.splitView && !this.props.hideLineNumbers && ((0, jsx_runtime_1.jsx)("td", { onClick: additionalLineNumber && this.onLineNumberClickProxy(additionalLineNumberTemplate), className: (0, classnames_1.default)(this.styles.gutter, { [this.styles.emptyGutter]: !additionalLineNumber, [this.styles.diffAdded]: added, [this.styles.diffRemoved]: removed, [this.styles.diffChanged]: changed, [this.styles.highlightedGutter]: highlightLine, }), children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.lineNumber, children: additionalLineNumber }) })), this.props.renderGutter ? this.props.renderGutter({ lineNumber, type, prefix, value, additionalLineNumber, additionalPrefix, styles: this.styles, }) : null, !this.props.hideMarkers && ((0, jsx_runtime_1.jsx)("td", { className: (0, classnames_1.default)(this.styles.marker, { [this.styles.emptyLine]: !content, [this.styles.diffAdded]: added, [this.styles.diffRemoved]: removed, [this.styles.diffChanged]: changed, [this.styles.highlightedLine]: highlightLine, }), children: (0, jsx_runtime_1.jsxs)("pre", { children: [added && '+', removed && '-'] }) })), (0, jsx_runtime_1.jsx)("td", { className: (0, classnames_1.default)(this.styles.content, { [this.styles.emptyLine]: !content, [this.styles.diffAdded]: added, [this.styles.diffRemoved]: removed, [this.styles.diffChanged]: changed, [this.styles.highlightedLine]: highlightLine, }), children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.contentText, children: content }) })] })); }; /** * Generates lines for split view. * * @param obj Line diff information. * @param obj.left Life diff information for the left pane of the split view. * @param obj.right Life diff information for the right pane of the split view. * @param index React key for the lines. */ this.renderSplitView = ({ left, right }, index) => { return ((0, jsx_runtime_1.jsxs)("tr", { className: this.styles.line, children: [this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value), this.renderLine(right.lineNumber, right.type, LineNumberPrefix.RIGHT, right.value)] }, index)); }; /** * Generates lines for inline view. * * @param obj Line diff information. * @param obj.left Life diff information for the added section of the inline view. * @param obj.right Life diff information for the removed section of the inline view. * @param index React key for the lines. */ this.renderInlineView = ({ left, right }, index) => { let content; if (left.type === compute_lines_1.DiffType.REMOVED && right.type === compute_lines_1.DiffType.ADDED) { return ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)("tr", { className: this.styles.line, children: this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null) }), (0, jsx_runtime_1.jsx)("tr", { className: this.styles.line, children: this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber) })] }, index)); } if (left.type === compute_lines_1.DiffType.REMOVED) { content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null); } if (left.type === compute_lines_1.DiffType.DEFAULT) { content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, right.lineNumber, LineNumberPrefix.RIGHT); } if (right.type === compute_lines_1.DiffType.ADDED) { content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber); } return ((0, jsx_runtime_1.jsx)("tr", { className: this.styles.line, children: content }, index)); }; /** * Returns a function with clicked block number in the closure. * * @param id Cold fold block id. */ this.onBlockClickProxy = (id) => () => this.onBlockExpand(id); /** * Generates cold fold block. It also uses the custom message renderer when available to show * cold fold messages. * * @param num Number of skipped lines between two blocks. * @param blockNumber Code fold block id. * @param leftBlockLineNumber First left line number after the current code fold block. * @param rightBlockLineNumber First right line number after the current code fold block. */ this.renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => { const { hideLineNumbers, splitView } = this.props; const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : ((0, jsx_runtime_1.jsxs)("pre", { className: this.styles.codeFoldContent, children: ["Expand ", num, " lines ..."] })); const content = ((0, jsx_runtime_1.jsx)("td", { children: (0, jsx_runtime_1.jsx)("a", { onClick: this.onBlockClickProxy(blockNumber), tabIndex: 0, children: message }) })); const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers; return ((0, jsx_runtime_1.jsxs)("tr", { className: this.styles.codeFold, children: [!hideLineNumbers && (0, jsx_runtime_1.jsx)("td", { className: this.styles.codeFoldGutter }), this.props.renderGutter ? ((0, jsx_runtime_1.jsx)("td", { className: this.styles.codeFoldGutter })) : null, (0, jsx_runtime_1.jsx)("td", { className: (0, classnames_1.default)({ [this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers, }) }), isUnifiedViewWithoutLineNumbers ? ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [(0, jsx_runtime_1.jsx)("td", {}), content] })) : ((0, jsx_runtime_1.jsxs)(React.Fragment, { children: [content, this.props.renderGutter ? (0, jsx_runtime_1.jsx)("td", {}) : null, (0, jsx_runtime_1.jsx)("td", {})] })), (0, jsx_runtime_1.jsx)("td", {}), (0, jsx_runtime_1.jsx)("td", {})] }, `${leftBlockLineNumber}-${rightBlockLineNumber}`)); }; /** * Generates the entire diff view. */ this.renderDiff = () => { const { oldValue, newValue, splitView, disableWordDiff, compareMethod, linesOffset, } = this.props; const { lineInformation, diffLines } = (0, compute_lines_1.computeLineInformation)(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines); const extraLines = this.props.extraLinesSurroundingDiff < 0 ? 0 : Math.round(this.props.extraLinesSurroundingDiff); const { lineBlocks, blocks } = (0, compute_hidden_blocks_1.computeHiddenBlocks)(lineInformation, diffLines, extraLines); return lineInformation.map((line, lineIndex) => { if (this.props.showDiffOnly) { const blockIndex = lineBlocks[lineIndex]; if (blockIndex !== undefined) { const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex; if (!this.state.expandedBlocks.includes(blockIndex) && lastLineOfBlock) { return ((0, jsx_runtime_1.jsx)(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex)); } else if (!this.state.expandedBlocks.includes(blockIndex)) { return null; } } } const diffNodes = splitView ? this.renderSplitView(line, lineIndex) : this.renderInlineView(line, lineIndex); return diffNodes; }); }; this.render = () => { const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, hideLineNumbers, hideMarkers, nonce, } = this.props; if (this.props.compareMethod !== compute_lines_1.DiffMethod.JSON) { if (typeof oldValue !== 'string' || typeof newValue !== 'string') { throw Error('"oldValue" and "newValue" should be strings'); } } this.styles = this.computeStyles(this.props.styles, useDarkTheme, nonce); const nodes = this.renderDiff(); let colSpanOnSplitView = hideLineNumbers ? 2 : 3; let colSpanOnInlineView = hideLineNumbers ? 2 : 4; if (hideMarkers) { colSpanOnSplitView -= 1; colSpanOnInlineView -= 1; } const columnExtension = this.props.renderGutter ? 1 : 0; const title = (leftTitle || rightTitle) && ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("td", { colSpan: (splitView ? colSpanOnSplitView : colSpanOnInlineView) + columnExtension, className: this.styles.titleBlock, children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.contentText, children: leftTitle }) }), splitView && ((0, jsx_runtime_1.jsx)("td", { colSpan: colSpanOnSplitView + columnExtension, className: this.styles.titleBlock, children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.contentText, children: rightTitle }) }))] })); return ((0, jsx_runtime_1.jsx)("table", { className: (0, classnames_1.default)(this.styles.diffContainer, { [this.styles.splitView]: splitView, }), children: (0, jsx_runtime_1.jsxs)("tbody", { children: [title, nodes] }) })); }; this.state = { expandedBlocks: [], }; } } DiffViewer.defaultProps = { oldValue: '', newValue: '', splitView: true, highlightLines: [], disableWordDiff: false, compareMethod: compute_lines_1.DiffMethod.CHARS, styles: {}, hideLineNumbers: false, hideMarkers: false, extraLinesSurroundingDiff: 3, showDiffOnly: true, useDarkTheme: false, linesOffset: 0, nonce: '', }; exports.default = DiffViewer;