UNPKG

react-diff-viewer

Version:

A simple and beautiful text diff viewer component made with diff and React

340 lines (339 loc) 18.5 kB
"use strict"; var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); Object.defineProperty(exports, "__esModule", { value: true }); var React = require("react"); var PropTypes = require("prop-types"); var classnames_1 = require("classnames"); var compute_lines_1 = require("./compute-lines"); exports.DiffMethod = compute_lines_1.DiffMethod; var styles_1 = require("./styles"); // eslint-disable-next-line @typescript-eslint/no-var-requires var m = require('memoize-one'); var memoize = m.default || m; var LineNumberPrefix; (function (LineNumberPrefix) { LineNumberPrefix["LEFT"] = "L"; LineNumberPrefix["RIGHT"] = "R"; })(LineNumberPrefix = exports.LineNumberPrefix || (exports.LineNumberPrefix = {})); var DiffViewer = /** @class */ (function (_super) { __extends(DiffViewer, _super); function DiffViewer(props) { var _this = _super.call(this, props) || this; /** * Resets code block expand to the initial stage. Will be exposed to the parent component via * refs. */ _this.resetCodeBlocks = function () { 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 = function (id) { var 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 = function (id) { if (_this.props.onLineNumberClick) { return function (e) { return _this.props.onLineNumberClick(id, e); }; } return function () { }; }; /** * 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 = function (diffArray, renderer) { return diffArray.map(function (wordDiff, i) { var _a; return (React.createElement("span", { key: i, className: classnames_1.default(_this.styles.wordDiff, (_a = {}, _a[_this.styles.wordAdded] = wordDiff.type === compute_lines_1.DiffType.ADDED, _a[_this.styles.wordRemoved] = wordDiff.type === compute_lines_1.DiffType.REMOVED, _a)) }, renderer ? renderer(wordDiff.value) : wordDiff.value)); }); }; /** * 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 = function (lineNumber, type, prefix, value, additionalLineNumber, additionalPrefix) { var _a, _b, _c, _d; var lineNumberTemplate = prefix + "-" + lineNumber; var additionalLineNumberTemplate = additionalPrefix + "-" + additionalLineNumber; var highlightLine = _this.props.highlightLines.includes(lineNumberTemplate) || _this.props.highlightLines.includes(additionalLineNumberTemplate); var added = type === compute_lines_1.DiffType.ADDED; var removed = type === compute_lines_1.DiffType.REMOVED; var 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 (React.createElement(React.Fragment, null, !_this.props.hideLineNumbers && (React.createElement("td", { onClick: lineNumber && _this.onLineNumberClickProxy(lineNumberTemplate), className: classnames_1.default(_this.styles.gutter, (_a = {}, _a[_this.styles.emptyGutter] = !lineNumber, _a[_this.styles.diffAdded] = added, _a[_this.styles.diffRemoved] = removed, _a[_this.styles.highlightedGutter] = highlightLine, _a)) }, React.createElement("pre", { className: _this.styles.lineNumber }, lineNumber))), !_this.props.splitView && !_this.props.hideLineNumbers && (React.createElement("td", { onClick: additionalLineNumber && _this.onLineNumberClickProxy(additionalLineNumberTemplate), className: classnames_1.default(_this.styles.gutter, (_b = {}, _b[_this.styles.emptyGutter] = !additionalLineNumber, _b[_this.styles.diffAdded] = added, _b[_this.styles.diffRemoved] = removed, _b[_this.styles.highlightedGutter] = highlightLine, _b)) }, React.createElement("pre", { className: _this.styles.lineNumber }, additionalLineNumber))), React.createElement("td", { className: classnames_1.default(_this.styles.marker, (_c = {}, _c[_this.styles.emptyLine] = !content, _c[_this.styles.diffAdded] = added, _c[_this.styles.diffRemoved] = removed, _c[_this.styles.highlightedLine] = highlightLine, _c)) }, React.createElement("pre", null, added && '+', removed && '-')), React.createElement("td", { className: classnames_1.default(_this.styles.content, (_d = {}, _d[_this.styles.emptyLine] = !content, _d[_this.styles.diffAdded] = added, _d[_this.styles.diffRemoved] = removed, _d[_this.styles.highlightedLine] = highlightLine, _d)) }, React.createElement("pre", { className: _this.styles.contentText }, 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 = function (_a, index) { var left = _a.left, right = _a.right; return (React.createElement("tr", { key: index, className: _this.styles.line }, _this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value), _this.renderLine(right.lineNumber, right.type, LineNumberPrefix.RIGHT, right.value))); }; /** * 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 = function (_a, index) { var left = _a.left, right = _a.right; var content; if (left.type === compute_lines_1.DiffType.REMOVED && right.type === compute_lines_1.DiffType.ADDED) { return (React.createElement(React.Fragment, { key: index }, React.createElement("tr", { className: _this.styles.line }, _this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null)), React.createElement("tr", { className: _this.styles.line }, _this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber)))); } 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 (React.createElement("tr", { key: index, className: _this.styles.line }, content)); }; /** * Returns a function with clicked block number in the closure. * * @param id Cold fold block id. */ _this.onBlockClickProxy = function (id) { return function () { return _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 = function (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) { var _a; var _b = _this.props, hideLineNumbers = _b.hideLineNumbers, splitView = _b.splitView; var message = _this.props.codeFoldMessageRenderer ? (_this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : (React.createElement("pre", { className: _this.styles.codeFoldContent }, "Expand ", num, " lines ...")); var content = (React.createElement("td", null, React.createElement("a", { onClick: _this.onBlockClickProxy(blockNumber), tabIndex: 0 }, message))); var isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers; return (React.createElement("tr", { key: leftBlockLineNumber + "-" + rightBlockLineNumber, className: _this.styles.codeFold }, !hideLineNumbers && React.createElement("td", { className: _this.styles.codeFoldGutter }), React.createElement("td", { className: classnames_1.default((_a = {}, _a[_this.styles.codeFoldGutter] = isUnifiedViewWithoutLineNumbers, _a)) }), isUnifiedViewWithoutLineNumbers ? (React.createElement(React.Fragment, null, React.createElement("td", null), content)) : (React.createElement(React.Fragment, null, content, React.createElement("td", null))), React.createElement("td", null), React.createElement("td", null))); }; /** * Generates the entire diff view. */ _this.renderDiff = function () { var _a = _this.props, oldValue = _a.oldValue, newValue = _a.newValue, splitView = _a.splitView, disableWordDiff = _a.disableWordDiff, compareMethod = _a.compareMethod, linesOffset = _a.linesOffset; var _b = compute_lines_1.computeLineInformation(oldValue, newValue, disableWordDiff, compareMethod, linesOffset), lineInformation = _b.lineInformation, diffLines = _b.diffLines; var extraLines = _this.props.extraLinesSurroundingDiff < 0 ? 0 : _this.props.extraLinesSurroundingDiff; var skippedLines = []; return lineInformation.map(function (line, i) { var diffBlockStart = diffLines[0]; var currentPosition = diffBlockStart - i; if (_this.props.showDiffOnly) { if (currentPosition === -extraLines) { skippedLines = []; diffLines.shift(); } if (line.left.type === compute_lines_1.DiffType.DEFAULT && (currentPosition > extraLines || typeof diffBlockStart === 'undefined') && !_this.state.expandedBlocks.includes(diffBlockStart)) { skippedLines.push(i + 1); if (i === lineInformation.length - 1 && skippedLines.length > 1) { return _this.renderSkippedLineIndicator(skippedLines.length, diffBlockStart, line.left.lineNumber, line.right.lineNumber); } return null; } } var diffNodes = splitView ? _this.renderSplitView(line, i) : _this.renderInlineView(line, i); if (currentPosition === extraLines && skippedLines.length > 0) { var length_1 = skippedLines.length; skippedLines = []; return (React.createElement(React.Fragment, { key: i }, _this.renderSkippedLineIndicator(length_1, diffBlockStart, line.left.lineNumber, line.right.lineNumber), diffNodes)); } return diffNodes; }); }; _this.render = function () { var _a; var _b = _this.props, oldValue = _b.oldValue, newValue = _b.newValue, useDarkTheme = _b.useDarkTheme, leftTitle = _b.leftTitle, rightTitle = _b.rightTitle, splitView = _b.splitView, hideLineNumbers = _b.hideLineNumbers; if (typeof oldValue !== 'string' || typeof newValue !== 'string') { throw Error('"oldValue" and "newValue" should be strings'); } _this.styles = _this.computeStyles(_this.props.styles, useDarkTheme); var nodes = _this.renderDiff(); var colSpanOnSplitView = hideLineNumbers ? 2 : 3; var colSpanOnInlineView = hideLineNumbers ? 2 : 4; var title = (leftTitle || rightTitle) && (React.createElement("tr", null, React.createElement("td", { colSpan: splitView ? colSpanOnSplitView : colSpanOnInlineView, className: _this.styles.titleBlock }, React.createElement("pre", { className: _this.styles.contentText }, leftTitle)), splitView && (React.createElement("td", { colSpan: colSpanOnSplitView, className: _this.styles.titleBlock }, React.createElement("pre", { className: _this.styles.contentText }, rightTitle))))); return (React.createElement("table", { className: classnames_1.default(_this.styles.diffContainer, (_a = {}, _a[_this.styles.splitView] = splitView, _a)) }, React.createElement("tbody", null, title, nodes))); }; _this.state = { expandedBlocks: [], }; return _this; } DiffViewer.defaultProps = { oldValue: '', newValue: '', splitView: true, highlightLines: [], disableWordDiff: false, compareMethod: compute_lines_1.DiffMethod.CHARS, styles: {}, hideLineNumbers: false, extraLinesSurroundingDiff: 3, showDiffOnly: true, useDarkTheme: false, linesOffset: 0, }; DiffViewer.propTypes = { oldValue: PropTypes.string.isRequired, newValue: PropTypes.string.isRequired, splitView: PropTypes.bool, disableWordDiff: PropTypes.bool, compareMethod: PropTypes.oneOf(Object.values(compute_lines_1.DiffMethod)), renderContent: PropTypes.func, onLineNumberClick: PropTypes.func, extraLinesSurroundingDiff: PropTypes.number, styles: PropTypes.object, hideLineNumbers: PropTypes.bool, showDiffOnly: PropTypes.bool, highlightLines: PropTypes.arrayOf(PropTypes.string), leftTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), rightTitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), linesOffset: PropTypes.number, }; return DiffViewer; }(React.Component)); exports.default = DiffViewer;