react-diff-viewer-continued 4.0.6 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,60 +1,201 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
11
+ import cn from "classnames";
12
+ import * as React from "react";
13
+ import memoize from "memoize-one";
14
+ import { computeHiddenBlocks } from "./compute-hidden-blocks.js";
15
+ import { DiffMethod, DiffType, computeLineInformationWorker, computeDiff, } from "./compute-lines.js";
16
+ import { Expand } from "./expand.js";
17
+ import computeStyles from "./styles.js";
18
+ import { Fold } from "./fold.js";
19
+ /**
20
+ * Applies diff styling (ins/del tags) to pre-highlighted HTML by walking through
21
+ * the HTML and wrapping text portions based on character positions in the diff.
22
+ */
23
+ function applyDiffToHighlightedHtml(html, diffArray, styles) {
24
+ const ranges = [];
25
+ let pos = 0;
26
+ for (const diff of diffArray) {
27
+ const value = typeof diff.value === "string" ? diff.value : "";
28
+ if (value.length > 0) {
29
+ ranges.push({ start: pos, end: pos + value.length, type: diff.type });
30
+ pos += value.length;
31
+ }
32
+ }
33
+ const segments = [];
34
+ let i = 0;
35
+ while (i < html.length) {
36
+ if (html[i] === "<") {
37
+ const tagEnd = html.indexOf(">", i);
38
+ if (tagEnd === -1) {
39
+ // Malformed HTML, treat rest as text
40
+ segments.push({ type: "text", content: html.slice(i) });
41
+ break;
42
+ }
43
+ segments.push({ type: "tag", content: html.slice(i, tagEnd + 1) });
44
+ i = tagEnd + 1;
45
+ }
46
+ else {
47
+ // Find the next tag or end of string
48
+ let textEnd = html.indexOf("<", i);
49
+ if (textEnd === -1)
50
+ textEnd = html.length;
51
+ segments.push({ type: "text", content: html.slice(i, textEnd) });
52
+ i = textEnd;
53
+ }
54
+ }
55
+ // Helper to decode HTML entities for character counting
56
+ function decodeEntities(text) {
57
+ return text
58
+ .replace(/&lt;/g, "<")
59
+ .replace(/&gt;/g, ">")
60
+ .replace(/&amp;/g, "&")
61
+ .replace(/&quot;/g, '"')
62
+ .replace(/&#39;/g, "'")
63
+ .replace(/&nbsp;/g, "\u00A0");
7
64
  }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
65
+ // Helper to get the wrapper tag for a diff type
66
+ function getWrapper(type) {
67
+ if (type === DiffType.ADDED) {
68
+ return {
69
+ open: `<ins class="${styles.wordDiff} ${styles.wordAdded}">`,
70
+ close: "</ins>",
71
+ };
72
+ }
73
+ if (type === DiffType.REMOVED) {
74
+ return {
75
+ open: `<del class="${styles.wordDiff} ${styles.wordRemoved}">`,
76
+ close: "</del>",
77
+ };
78
+ }
79
+ return {
80
+ open: `<span class="${styles.wordDiff}">`,
81
+ close: "</span>",
24
82
  };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.DiffMethod = exports.LineNumberPrefix = void 0;
40
- const jsx_runtime_1 = require("react/jsx-runtime");
41
- const classnames_1 = __importDefault(require("classnames"));
42
- const React = __importStar(require("react"));
43
- const memoize_one_1 = __importDefault(require("memoize-one"));
44
- const compute_hidden_blocks_js_1 = require("./compute-hidden-blocks.js");
45
- const compute_lines_js_1 = require("./compute-lines.js");
46
- Object.defineProperty(exports, "DiffMethod", { enumerable: true, get: function () { return compute_lines_js_1.DiffMethod; } });
47
- const expand_js_1 = require("./expand.js");
48
- const styles_js_1 = __importDefault(require("./styles.js"));
49
- const fold_js_1 = require("./fold.js");
50
- var LineNumberPrefix;
83
+ }
84
+ // Process segments, tracking text position
85
+ let textPos = 0;
86
+ let result = "";
87
+ for (const segment of segments) {
88
+ if (segment.type === "tag") {
89
+ result += segment.content;
90
+ }
91
+ else {
92
+ // Text segment - we need to split it according to diff ranges
93
+ const text = segment.content;
94
+ const decodedText = decodeEntities(text);
95
+ // Walk through the text, character by character (in decoded form)
96
+ // but output the original encoded form
97
+ let localDecodedPos = 0;
98
+ let localEncodedPos = 0;
99
+ while (localDecodedPos < decodedText.length) {
100
+ const globalPos = textPos + localDecodedPos;
101
+ // Find the range that covers this position
102
+ const range = ranges.find((r) => globalPos >= r.start && globalPos < r.end);
103
+ if (!range) {
104
+ // No range covers this position (shouldn't happen, but be safe)
105
+ // Just output the character
106
+ const char = text[localEncodedPos];
107
+ result += char;
108
+ localEncodedPos++;
109
+ localDecodedPos++;
110
+ continue;
111
+ }
112
+ // How many decoded characters until the end of this range?
113
+ const charsUntilRangeEnd = range.end - globalPos;
114
+ // How many decoded characters until the end of this text segment?
115
+ const charsUntilTextEnd = decodedText.length - localDecodedPos;
116
+ // Take the minimum
117
+ const charsToTake = Math.min(charsUntilRangeEnd, charsUntilTextEnd);
118
+ // Now we need to find the corresponding encoded substring
119
+ // Walk through encoded text, counting decoded characters
120
+ let encodedChunkEnd = localEncodedPos;
121
+ let decodedCount = 0;
122
+ while (decodedCount < charsToTake && encodedChunkEnd < text.length) {
123
+ if (text[encodedChunkEnd] === "&") {
124
+ // Find entity end
125
+ const entityEnd = text.indexOf(";", encodedChunkEnd);
126
+ if (entityEnd !== -1 && entityEnd - encodedChunkEnd < 10) {
127
+ encodedChunkEnd = entityEnd + 1;
128
+ }
129
+ else {
130
+ encodedChunkEnd++;
131
+ }
132
+ }
133
+ else {
134
+ encodedChunkEnd++;
135
+ }
136
+ decodedCount++;
137
+ }
138
+ const chunk = text.slice(localEncodedPos, encodedChunkEnd);
139
+ const wrapper = getWrapper(range.type);
140
+ if (wrapper) {
141
+ result += wrapper.open + chunk + wrapper.close;
142
+ }
143
+ else {
144
+ result += chunk;
145
+ }
146
+ localEncodedPos = encodedChunkEnd;
147
+ localDecodedPos += charsToTake;
148
+ }
149
+ textPos += decodedText.length;
150
+ }
151
+ }
152
+ return result;
153
+ }
154
+ export var LineNumberPrefix;
51
155
  (function (LineNumberPrefix) {
52
156
  LineNumberPrefix["LEFT"] = "L";
53
157
  LineNumberPrefix["RIGHT"] = "R";
54
- })(LineNumberPrefix || (exports.LineNumberPrefix = LineNumberPrefix = {}));
158
+ })(LineNumberPrefix || (LineNumberPrefix = {}));
55
159
  class DiffViewer extends React.Component {
56
160
  constructor(props) {
57
161
  super(props);
162
+ // Cache for on-demand word diff computation
163
+ this.wordDiffCache = new Map();
164
+ // Refs for measuring content column width and character width
165
+ this.contentColumnRef = React.createRef();
166
+ this.charMeasureRef = React.createRef();
167
+ this.stickyHeaderRef = React.createRef();
168
+ this.resizeObserver = null;
169
+ /**
170
+ * Computes word diff on-demand for a line, with caching.
171
+ * This is used when word diff was deferred during initial computation.
172
+ */
173
+ this.getWordDiffValues = (left, right, lineIndex) => {
174
+ // Handle empty left/right
175
+ if (!left || !right) {
176
+ return { leftValue: left === null || left === void 0 ? void 0 : left.value, rightValue: right === null || right === void 0 ? void 0 : right.value };
177
+ }
178
+ // If no raw values, word diff was already computed or disabled
179
+ // Use explicit undefined check since empty string is a valid raw value
180
+ if (left.rawValue === undefined || right.rawValue === undefined) {
181
+ return { leftValue: left.value, rightValue: right.value };
182
+ }
183
+ // Check cache
184
+ const cacheKey = `${lineIndex}-${left.rawValue}-${right.rawValue}`;
185
+ let cached = this.wordDiffCache.get(cacheKey);
186
+ if (!cached) {
187
+ // Compute word diff on-demand
188
+ // Use CHARS method for on-demand computation since rawValue is always a string
189
+ // (JSON/YAML methods only work with objects, not the string lines we have here)
190
+ const compareMethod = (this.props.compareMethod === DiffMethod.JSON || this.props.compareMethod === DiffMethod.YAML)
191
+ ? DiffMethod.CHARS
192
+ : this.props.compareMethod;
193
+ const computed = computeDiff(left.rawValue, right.rawValue, compareMethod);
194
+ cached = { left: computed.left, right: computed.right };
195
+ this.wordDiffCache.set(cacheKey, cached);
196
+ }
197
+ return { leftValue: cached.left, rightValue: cached.right };
198
+ };
58
199
  /**
59
200
  * Resets code block expand to the initial stage. Will be exposed to the parent component via
60
201
  * refs.
@@ -75,8 +216,28 @@ class DiffViewer extends React.Component {
75
216
  this.onBlockExpand = (id) => {
76
217
  const prevState = this.state.expandedBlocks.slice();
77
218
  prevState.push(id);
78
- this.setState({
79
- expandedBlocks: prevState,
219
+ this.setState({ expandedBlocks: prevState }, () => this.recalculateOffsets());
220
+ };
221
+ /**
222
+ * Recalculates cumulative offsets based on current measurements.
223
+ * Called on resize and when blocks are expanded/collapsed.
224
+ */
225
+ this.recalculateOffsets = () => {
226
+ var _a;
227
+ if (!this.props.infiniteLoading)
228
+ return;
229
+ const columnWidth = this.measureContentColumnWidth();
230
+ const charWidth = this.measureCharWidth();
231
+ if (!columnWidth)
232
+ return;
233
+ const cacheKey = this.getMemoisedKey();
234
+ const { lineInformation, lineBlocks, blocks } = (_a = this.state.computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
235
+ if (!lineInformation)
236
+ return;
237
+ const offsets = this.buildCumulativeOffsets(lineInformation, lineBlocks, blocks, this.state.expandedBlocks, this.props.showDiffOnly, charWidth, columnWidth, this.props.splitView);
238
+ this.setState({ cumulativeOffsets: offsets, contentColumnWidth: columnWidth, charWidth }, () => {
239
+ // Force a scroll position update to recalculate visible rows with new offsets
240
+ this.onScroll();
80
241
  });
81
242
  };
82
243
  /**
@@ -85,7 +246,7 @@ class DiffViewer extends React.Component {
85
246
  *
86
247
  * @param styles User supplied style overrides.
87
248
  */
88
- this.computeStyles = (0, memoize_one_1.default)(styles_js_1.default);
249
+ this.computeStyles = memoize(computeStyles);
89
250
  /**
90
251
  * Returns a function with clicked line number in the closure. Returns an no-op function when no
91
252
  * onLineNumberClick handler is supplied.
@@ -98,6 +259,19 @@ class DiffViewer extends React.Component {
98
259
  }
99
260
  return () => { };
100
261
  };
262
+ /**
263
+ * Checks if the current compare method should show word-level highlighting.
264
+ * Character, word-level, JSON, and YAML diffs benefit from highlighting individual changes.
265
+ * JSON/YAML use CHARS internally for word-level diff, so they should be highlighted.
266
+ */
267
+ this.shouldHighlightWordDiff = () => {
268
+ const { compareMethod } = this.props;
269
+ return (compareMethod === DiffMethod.CHARS ||
270
+ compareMethod === DiffMethod.WORDS ||
271
+ compareMethod === DiffMethod.WORDS_WITH_SPACE ||
272
+ compareMethod === DiffMethod.JSON ||
273
+ compareMethod === DiffMethod.YAML);
274
+ };
101
275
  /**
102
276
  * Maps over the word diff and constructs the required React elements to show word diff.
103
277
  *
@@ -105,18 +279,61 @@ class DiffViewer extends React.Component {
105
279
  * @param renderer Optional renderer to format diff words. Useful for syntax highlighting.
106
280
  */
107
281
  this.renderWordDiff = (diffArray, renderer) => {
282
+ var _a, _b;
283
+ const showHighlight = this.shouldHighlightWordDiff();
284
+ const { compareMethod } = this.props;
285
+ // Don't apply syntax highlighting for JSON/YAML - their word diffs are computed
286
+ // on-demand from raw strings and syntax highlighting creates messy fragmented tokens.
287
+ const skipSyntaxHighlighting = compareMethod === DiffMethod.JSON || compareMethod === DiffMethod.YAML;
288
+ // Reconstruct the full line from diff chunks
289
+ const fullLine = diffArray
290
+ .map((d) => (typeof d.value === "string" ? d.value : ""))
291
+ .join("");
292
+ // For very long lines (>500 chars), skip fancy processing - just render plain text
293
+ // without word-level highlighting to avoid performance issues
294
+ const MAX_LINE_LENGTH = 500;
295
+ if (fullLine.length > MAX_LINE_LENGTH) {
296
+ return [_jsx("span", { children: fullLine }, "long-line")];
297
+ }
298
+ // If we have a renderer and syntax highlighting is enabled, try to highlight
299
+ // the full line first, then apply diff styling to preserve proper tokenization.
300
+ if (renderer && !skipSyntaxHighlighting) {
301
+ // Get the syntax-highlighted content
302
+ const highlighted = renderer(fullLine);
303
+ // Check if the renderer uses dangerouslySetInnerHTML (common with Prism, highlight.js, etc.)
304
+ const htmlContent = (_b = (_a = highlighted === null || highlighted === void 0 ? void 0 : highlighted.props) === null || _a === void 0 ? void 0 : _a.dangerouslySetInnerHTML) === null || _b === void 0 ? void 0 : _b.__html;
305
+ if (typeof htmlContent === "string") {
306
+ // Apply diff styling to the highlighted HTML
307
+ const styledHtml = applyDiffToHighlightedHtml(htmlContent, diffArray, {
308
+ wordDiff: this.styles.wordDiff,
309
+ wordAdded: showHighlight ? this.styles.wordAdded : "",
310
+ wordRemoved: showHighlight ? this.styles.wordRemoved : "",
311
+ });
312
+ // Clone the element with the modified HTML
313
+ return [
314
+ React.cloneElement(highlighted, {
315
+ key: "highlighted-diff",
316
+ dangerouslySetInnerHTML: { __html: styledHtml },
317
+ }),
318
+ ];
319
+ }
320
+ // Renderer doesn't use dangerouslySetInnerHTML - fall through to per-chunk rendering
321
+ }
322
+ // Fallback: render each chunk separately (used for JSON/YAML or non-HTML renderers)
108
323
  return diffArray.map((wordDiff, i) => {
109
- const content = renderer
110
- ? renderer(wordDiff.value)
111
- : (typeof wordDiff.value === 'string'
112
- ? wordDiff.value
113
- // If wordDiff.value is DiffInformation, we don't handle it, unclear why. See c0c99f5712.
114
- : undefined);
115
- return wordDiff.type === compute_lines_js_1.DiffType.ADDED ? ((0, jsx_runtime_1.jsx)("ins", { className: (0, classnames_1.default)(this.styles.wordDiff, {
116
- [this.styles.wordAdded]: wordDiff.type === compute_lines_js_1.DiffType.ADDED,
117
- }), children: content }, i)) : wordDiff.type === compute_lines_js_1.DiffType.REMOVED ? ((0, jsx_runtime_1.jsx)("del", { className: (0, classnames_1.default)(this.styles.wordDiff, {
118
- [this.styles.wordRemoved]: wordDiff.type === compute_lines_js_1.DiffType.REMOVED,
119
- }), children: content }, i)) : ((0, jsx_runtime_1.jsx)("span", { className: (0, classnames_1.default)(this.styles.wordDiff), children: content }, i));
324
+ let content;
325
+ if (typeof wordDiff.value === "string") {
326
+ content = wordDiff.value;
327
+ }
328
+ else {
329
+ // If wordDiff.value is DiffInformation[], we don't handle it. See c0c99f5712.
330
+ content = undefined;
331
+ }
332
+ return wordDiff.type === DiffType.ADDED ? (_jsx("ins", { className: cn(this.styles.wordDiff, {
333
+ [this.styles.wordAdded]: showHighlight,
334
+ }), children: content }, i)) : wordDiff.type === DiffType.REMOVED ? (_jsx("del", { className: cn(this.styles.wordDiff, {
335
+ [this.styles.wordRemoved]: showHighlight,
336
+ }), children: content }, i)) : (_jsx("span", { className: cn(this.styles.wordDiff), children: content }, i));
120
337
  });
121
338
  };
122
339
  /**
@@ -137,9 +354,9 @@ class DiffViewer extends React.Component {
137
354
  const additionalLineNumberTemplate = `${additionalPrefix}-${additionalLineNumber}`;
138
355
  const highlightLine = this.props.highlightLines.includes(lineNumberTemplate) ||
139
356
  this.props.highlightLines.includes(additionalLineNumberTemplate);
140
- const added = type === compute_lines_js_1.DiffType.ADDED;
141
- const removed = type === compute_lines_js_1.DiffType.REMOVED;
142
- const changed = type === compute_lines_js_1.DiffType.CHANGED;
357
+ const added = type === DiffType.ADDED;
358
+ const removed = type === DiffType.REMOVED;
359
+ const changed = type === DiffType.CHANGED;
143
360
  let content;
144
361
  const hasWordDiff = Array.isArray(value);
145
362
  if (hasWordDiff) {
@@ -158,20 +375,20 @@ class DiffViewer extends React.Component {
158
375
  else if (removed && !hasWordDiff) {
159
376
  ElementType = "del";
160
377
  }
161
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!this.props.hideLineNumbers && ((0, jsx_runtime_1.jsx)("td", { onClick: lineNumber && this.onLineNumberClickProxy(lineNumberTemplate), className: (0, classnames_1.default)(this.styles.gutter, {
378
+ return (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && (_jsx("td", { onClick: lineNumber && this.onLineNumberClickProxy(lineNumberTemplate), className: cn(this.styles.gutter, {
162
379
  [this.styles.emptyGutter]: !lineNumber,
163
380
  [this.styles.diffAdded]: added,
164
381
  [this.styles.diffRemoved]: removed,
165
382
  [this.styles.diffChanged]: changed,
166
383
  [this.styles.highlightedGutter]: highlightLine,
167
- }), 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 &&
168
- this.onLineNumberClickProxy(additionalLineNumberTemplate), className: (0, classnames_1.default)(this.styles.gutter, {
384
+ }), children: _jsx("pre", { className: this.styles.lineNumber, children: lineNumber }) })), !this.props.splitView && !this.props.hideLineNumbers && (_jsx("td", { onClick: additionalLineNumber &&
385
+ this.onLineNumberClickProxy(additionalLineNumberTemplate), className: cn(this.styles.gutter, {
169
386
  [this.styles.emptyGutter]: !additionalLineNumber,
170
387
  [this.styles.diffAdded]: added,
171
388
  [this.styles.diffRemoved]: removed,
172
389
  [this.styles.diffChanged]: changed,
173
390
  [this.styles.highlightedGutter]: highlightLine,
174
- }), children: (0, jsx_runtime_1.jsx)("pre", { className: this.styles.lineNumber, children: additionalLineNumber }) })), this.props.renderGutter
391
+ }), children: _jsx("pre", { className: this.styles.lineNumber, children: additionalLineNumber }) })), this.props.renderGutter
175
392
  ? this.props.renderGutter({
176
393
  lineNumber,
177
394
  type,
@@ -181,13 +398,13 @@ class DiffViewer extends React.Component {
181
398
  additionalPrefix,
182
399
  styles: this.styles,
183
400
  })
184
- : null, (0, jsx_runtime_1.jsx)("td", { className: (0, classnames_1.default)(this.styles.marker, {
401
+ : null, _jsx("td", { className: cn(this.styles.marker, {
185
402
  [this.styles.emptyLine]: !content,
186
403
  [this.styles.diffAdded]: added,
187
404
  [this.styles.diffRemoved]: removed,
188
405
  [this.styles.diffChanged]: changed,
189
406
  [this.styles.highlightedLine]: highlightLine,
190
- }), children: (0, jsx_runtime_1.jsxs)("pre", { children: [added && "+", removed && "-"] }) }), (0, jsx_runtime_1.jsx)("td", { className: (0, classnames_1.default)(this.styles.content, {
407
+ }), children: _jsxs("pre", { children: [added && "+", removed && "-"] }) }), _jsx("td", { ref: prefix === LineNumberPrefix.LEFT && !this.state.cumulativeOffsets ? this.contentColumnRef : undefined, className: cn(this.styles.content, {
191
408
  [this.styles.emptyLine]: !content,
192
409
  [this.styles.diffAdded]: added,
193
410
  [this.styles.diffRemoved]: removed,
@@ -205,7 +422,7 @@ class DiffViewer extends React.Component {
205
422
  ? "Added line"
206
423
  : removed && !hasWordDiff
207
424
  ? "Removed line"
208
- : undefined, children: (0, jsx_runtime_1.jsx)(ElementType, { className: this.styles.contentText, children: content }) })] }));
425
+ : undefined, children: _jsx(ElementType, { className: this.styles.contentText, children: content }) })] }));
209
426
  };
210
427
  /**
211
428
  * Generates lines for split view.
@@ -216,7 +433,9 @@ class DiffViewer extends React.Component {
216
433
  * @param index React key for the lines.
217
434
  */
218
435
  this.renderSplitView = ({ left, right }, index) => {
219
- 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));
436
+ // Compute word diff on-demand if deferred
437
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
438
+ return (_jsxs("tr", { className: this.styles.line, children: [this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue), this.renderLine(right.lineNumber, right.type, LineNumberPrefix.RIGHT, rightValue)] }, index));
220
439
  };
221
440
  /**
222
441
  * Generates lines for inline view.
@@ -227,20 +446,22 @@ class DiffViewer extends React.Component {
227
446
  * @param index React key for the lines.
228
447
  */
229
448
  this.renderInlineView = ({ left, right }, index) => {
449
+ // Compute word diff on-demand if deferred
450
+ const { leftValue, rightValue } = this.getWordDiffValues(left, right, index);
230
451
  let content;
231
- if (left.type === compute_lines_js_1.DiffType.REMOVED && right.type === compute_lines_js_1.DiffType.ADDED) {
232
- 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, LineNumberPrefix.RIGHT) })] }, index));
452
+ if (left.type === DiffType.REMOVED && right.type === DiffType.ADDED) {
453
+ return (_jsxs(React.Fragment, { children: [_jsx("tr", { className: this.styles.line, children: this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null) }), _jsx("tr", { className: this.styles.line, children: this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber, LineNumberPrefix.RIGHT) })] }, index));
233
454
  }
234
- if (left.type === compute_lines_js_1.DiffType.REMOVED) {
235
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, null);
455
+ if (left.type === DiffType.REMOVED) {
456
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, null);
236
457
  }
237
- if (left.type === compute_lines_js_1.DiffType.DEFAULT) {
238
- content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, left.value, right.lineNumber, LineNumberPrefix.RIGHT);
458
+ if (left.type === DiffType.DEFAULT) {
459
+ content = this.renderLine(left.lineNumber, left.type, LineNumberPrefix.LEFT, leftValue, right.lineNumber, LineNumberPrefix.RIGHT);
239
460
  }
240
- if (right.type === compute_lines_js_1.DiffType.ADDED) {
241
- content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, right.value, right.lineNumber);
461
+ if (right.type === DiffType.ADDED) {
462
+ content = this.renderLine(null, right.type, LineNumberPrefix.RIGHT, rightValue, right.lineNumber);
242
463
  }
243
- return ((0, jsx_runtime_1.jsx)("tr", { className: this.styles.line, children: content }, index));
464
+ return (_jsx("tr", { className: this.styles.line, children: content }, index));
244
465
  };
245
466
  /**
246
467
  * Returns a function with clicked block number in the closure.
@@ -259,51 +480,257 @@ class DiffViewer extends React.Component {
259
480
  */
260
481
  this.renderSkippedLineIndicator = (num, blockNumber, leftBlockLineNumber, rightBlockLineNumber) => {
261
482
  const { hideLineNumbers, splitView } = this.props;
262
- const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : ((0, jsx_runtime_1.jsxs)("span", { className: this.styles.codeFoldContent, children: ["Expand ", num, " lines ..."] }));
263
- const content = ((0, jsx_runtime_1.jsx)("td", { className: this.styles.codeFoldContentContainer, children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: this.styles.codeFoldExpandButton, onClick: this.onBlockClickProxy(blockNumber), tabIndex: 0, children: message }) }));
483
+ const message = this.props.codeFoldMessageRenderer ? (this.props.codeFoldMessageRenderer(num, leftBlockLineNumber, rightBlockLineNumber)) : (_jsxs("span", { className: this.styles.codeFoldContent, children: ["@@ -", leftBlockLineNumber - num, ",", num, " +", rightBlockLineNumber - num, ",", num, " @@"] }));
484
+ const content = (_jsx("td", { className: this.styles.codeFoldContentContainer, children: _jsx("button", { type: "button", className: this.styles.codeFoldExpandButton, onClick: this.onBlockClickProxy(blockNumber), tabIndex: 0, children: message }) }));
264
485
  const isUnifiedViewWithoutLineNumbers = !splitView && !hideLineNumbers;
265
- 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)({
486
+ const expandGutter = (_jsx("td", { className: this.styles.codeFoldGutter, children: _jsx(Expand, {}) }));
487
+ return (_jsxs("tr", { className: this.styles.codeFold, onClick: this.onBlockClickProxy(blockNumber), role: "button", tabIndex: 0, children: [!hideLineNumbers && expandGutter, this.props.renderGutter ? (_jsx("td", { className: this.styles.codeFoldGutter })) : null, _jsx("td", { className: cn({
266
488
  [this.styles.codeFoldGutter]: isUnifiedViewWithoutLineNumbers,
267
- }) }), 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", {}), !hideLineNumbers ? (0, jsx_runtime_1.jsx)("td", {}) : null] }))] }, `${leftBlockLineNumber}-${rightBlockLineNumber}`));
489
+ }) }), isUnifiedViewWithoutLineNumbers ? (_jsxs(React.Fragment, { children: [_jsx("td", {}), content] })) : (_jsxs(React.Fragment, { children: [content, this.props.renderGutter ? _jsx("td", {}) : null, _jsx("td", {}), _jsx("td", {}), !hideLineNumbers ? _jsx("td", {}) : null] }))] }, `${leftBlockLineNumber}-${rightBlockLineNumber}`));
268
490
  };
269
491
  /**
270
- * Generates the entire diff view.
492
+ *
493
+ * Generates a unique cache key based on the current props used in diff computation.
494
+ *
495
+ * This key is used to memoize results and avoid recomputation for the same inputs.
496
+ * @returns A stringified JSON key representing the current diff settings and input values.
497
+ *
271
498
  */
272
- this.renderDiff = () => {
273
- const { oldValue, newValue, splitView, disableWordDiff, compareMethod, linesOffset, } = this.props;
274
- const { lineInformation, diffLines } = (0, compute_lines_js_1.computeLineInformation)(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines);
499
+ this.getMemoisedKey = () => {
500
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset, alwaysShowLines, extraLinesSurroundingDiff, } = this.props;
501
+ return JSON.stringify({
502
+ oldValue,
503
+ newValue,
504
+ disableWordDiff,
505
+ compareMethod,
506
+ linesOffset,
507
+ alwaysShowLines,
508
+ extraLinesSurroundingDiff,
509
+ });
510
+ };
511
+ /**
512
+ * Computes and memoizes the diff result between `oldValue` and `newValue`.
513
+ *
514
+ * If a memoized result exists for the current input configuration, it uses that.
515
+ * Otherwise, it runs the diff logic in a Web Worker to avoid blocking the UI.
516
+ * It also computes hidden line blocks for collapsing unchanged sections,
517
+ * and stores the result in the local component state.
518
+ */
519
+ this.memoisedCompute = () => __awaiter(this, void 0, void 0, function* () {
520
+ var _a;
521
+ const { oldValue, newValue, disableWordDiff, compareMethod, linesOffset } = this.props;
522
+ const cacheKey = this.getMemoisedKey();
523
+ if (!!this.state.computedDiffResult[cacheKey]) {
524
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: false })));
525
+ return;
526
+ }
527
+ // Defer word diff computation when using infinite loading with reasonable container height
528
+ // This significantly improves initial render time for large diffs
529
+ const containerHeight = (_a = this.props.infiniteLoading) === null || _a === void 0 ? void 0 : _a.containerHeight;
530
+ const containerHeightPx = containerHeight
531
+ ? typeof containerHeight === 'number'
532
+ ? containerHeight
533
+ : parseInt(containerHeight, 10) || 0
534
+ : 0;
535
+ const shouldDeferWordDiff = !disableWordDiff &&
536
+ !!this.props.infiniteLoading &&
537
+ containerHeightPx > 0 &&
538
+ containerHeightPx < 2000;
539
+ const { lineInformation, diffLines } = yield computeLineInformationWorker(oldValue, newValue, disableWordDiff, compareMethod, linesOffset, this.props.alwaysShowLines, shouldDeferWordDiff);
275
540
  const extraLines = this.props.extraLinesSurroundingDiff < 0
276
541
  ? 0
277
542
  : Math.round(this.props.extraLinesSurroundingDiff);
278
- const { lineBlocks, blocks } = (0, compute_hidden_blocks_js_1.computeHiddenBlocks)(lineInformation, diffLines, extraLines);
279
- const diffNodes = lineInformation.map((line, lineIndex) => {
280
- if (this.props.showDiffOnly) {
543
+ const { lineBlocks, blocks } = computeHiddenBlocks(lineInformation, diffLines, extraLines);
544
+ this.state.computedDiffResult[cacheKey] = { lineInformation, lineBlocks, blocks };
545
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { computedDiffResult: this.state.computedDiffResult, isLoading: false })), () => {
546
+ // Trigger offset recalculation after diff is computed and rendered
547
+ // Use requestAnimationFrame to ensure DOM is ready for measurement
548
+ if (this.props.infiniteLoading) {
549
+ requestAnimationFrame(() => this.recalculateOffsets());
550
+ }
551
+ });
552
+ });
553
+ /**
554
+ * Handles scroll events on the scrollable container.
555
+ *
556
+ * Updates the visible start row for virtualization.
557
+ */
558
+ this.onScroll = () => {
559
+ const container = this.state.scrollableContainerRef.current;
560
+ if (!container || !this.props.infiniteLoading)
561
+ return;
562
+ // Account for sticky header height in scroll calculations
563
+ const headerHeight = this.getStickyHeaderHeight();
564
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
565
+ const { cumulativeOffsets } = this.state;
566
+ const newStartRow = cumulativeOffsets
567
+ ? this.findLineAtOffset(contentScrollTop, cumulativeOffsets)
568
+ : Math.floor(contentScrollTop / DiffViewer.ESTIMATED_ROW_HEIGHT);
569
+ // Only update state if the start row changed (avoid unnecessary re-renders)
570
+ if (newStartRow !== this.state.visibleStartRow) {
571
+ this.setState({ visibleStartRow: newStartRow });
572
+ }
573
+ };
574
+ /**
575
+ * Generates the entire diff view with virtualization support.
576
+ */
577
+ this.renderDiff = () => {
578
+ var _a, _b, _c, _d, _e, _f;
579
+ const { splitView, infiniteLoading, showDiffOnly } = this.props;
580
+ const { computedDiffResult, expandedBlocks, visibleStartRow, scrollableContainerRef, cumulativeOffsets } = this.state;
581
+ const cacheKey = this.getMemoisedKey();
582
+ const { lineInformation = [], lineBlocks = [], blocks = [] } = (_a = computedDiffResult[cacheKey]) !== null && _a !== void 0 ? _a : {};
583
+ // Calculate visible range for virtualization
584
+ let visibleRowStart = 0;
585
+ let visibleRowEnd = Infinity;
586
+ const buffer = 5; // render extra rows above/below viewport
587
+ if (infiniteLoading && scrollableContainerRef.current) {
588
+ const container = scrollableContainerRef.current;
589
+ // Account for sticky header height in scroll calculations
590
+ const headerHeight = this.getStickyHeaderHeight();
591
+ const contentScrollTop = Math.max(0, container.scrollTop - headerHeight);
592
+ if (cumulativeOffsets) {
593
+ // Variable height mode: use binary search to find visible range
594
+ const totalHeight = cumulativeOffsets[cumulativeOffsets.length - 1] || 0;
595
+ const lastRowIndex = cumulativeOffsets.length - 2;
596
+ visibleRowStart = Math.max(0, this.findLineAtOffset(contentScrollTop, cumulativeOffsets) - buffer);
597
+ visibleRowEnd = this.findLineAtOffset(contentScrollTop + container.clientHeight, cumulativeOffsets) + buffer;
598
+ // IMPORTANT: The calculated offsets may overestimate row heights (based on char count),
599
+ // but actual CSS rendering might produce shorter rows. To prevent empty space,
600
+ // ensure we render at least enough rows to fill the viewport using ESTIMATED_ROW_HEIGHT
601
+ // as a conservative minimum.
602
+ const minRowsToFillViewport = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
603
+ visibleRowEnd = Math.max(visibleRowEnd, visibleRowStart + minRowsToFillViewport + buffer);
604
+ // Also ensure we render all rows when near the bottom
605
+ if (contentScrollTop + container.clientHeight >= totalHeight - buffer * DiffViewer.ESTIMATED_ROW_HEIGHT) {
606
+ visibleRowEnd = lastRowIndex + buffer;
607
+ }
608
+ }
609
+ else {
610
+ // Fixed height fallback
611
+ const viewportRows = Math.ceil(container.clientHeight / DiffViewer.ESTIMATED_ROW_HEIGHT);
612
+ visibleRowStart = Math.max(0, visibleStartRow - buffer);
613
+ visibleRowEnd = visibleStartRow + viewportRows + buffer;
614
+ }
615
+ }
616
+ // First pass: build a map of lineIndex -> renderedRowIndex
617
+ // This accounts for code folding where some lines don't render or render as fold indicators
618
+ const lineToRowMap = new Map();
619
+ const seenBlocks = new Set();
620
+ let currentRow = 0;
621
+ for (let i = 0; i < lineInformation.length; i++) {
622
+ const blockIndex = lineBlocks[i];
623
+ if (showDiffOnly && blockIndex !== undefined) {
624
+ if (!expandedBlocks.includes(blockIndex)) {
625
+ // Line is in a collapsed block
626
+ const lastLineOfBlock = blocks[blockIndex].endLine === i;
627
+ if (!seenBlocks.has(blockIndex) && lastLineOfBlock) {
628
+ // This line renders as a fold indicator
629
+ seenBlocks.add(blockIndex);
630
+ lineToRowMap.set(i, currentRow);
631
+ currentRow++;
632
+ }
633
+ // Other lines in collapsed block don't render
634
+ }
635
+ else {
636
+ // Block is expanded, line renders normally
637
+ lineToRowMap.set(i, currentRow);
638
+ currentRow++;
639
+ }
640
+ }
641
+ else {
642
+ // Not in a block or showDiffOnly is false, line renders normally
643
+ lineToRowMap.set(i, currentRow);
644
+ currentRow++;
645
+ }
646
+ }
647
+ const totalRenderedRows = currentRow;
648
+ // Second pass: render only lines in the visible range
649
+ const diffNodes = [];
650
+ let topPadding = 0;
651
+ let firstVisibleFound = false;
652
+ let lastRenderedRowIndex = -1;
653
+ seenBlocks.clear();
654
+ for (let lineIndex = 0; lineIndex < lineInformation.length; lineIndex++) {
655
+ const line = lineInformation[lineIndex];
656
+ const rowIndex = lineToRowMap.get(lineIndex);
657
+ // Skip lines that don't render (hidden in collapsed blocks)
658
+ if (rowIndex === undefined)
659
+ continue;
660
+ // Skip lines before visible range
661
+ if (rowIndex < visibleRowStart) {
662
+ continue;
663
+ }
664
+ // Stop after visible range
665
+ if (rowIndex > visibleRowEnd) {
666
+ break;
667
+ }
668
+ // Calculate top padding from the first visible row
669
+ if (!firstVisibleFound) {
670
+ topPadding = cumulativeOffsets
671
+ ? cumulativeOffsets[rowIndex] || 0
672
+ : rowIndex * DiffViewer.ESTIMATED_ROW_HEIGHT;
673
+ firstVisibleFound = true;
674
+ }
675
+ // Track the last rendered row for bottom padding calculation
676
+ lastRenderedRowIndex = rowIndex;
677
+ // Render the line
678
+ if (showDiffOnly) {
281
679
  const blockIndex = lineBlocks[lineIndex];
282
680
  if (blockIndex !== undefined) {
283
681
  const lastLineOfBlock = blocks[blockIndex].endLine === lineIndex;
284
- if (!this.state.expandedBlocks.includes(blockIndex) &&
682
+ if (!expandedBlocks.includes(blockIndex) &&
285
683
  lastLineOfBlock) {
286
- return ((0, jsx_runtime_1.jsx)(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
684
+ diffNodes.push(_jsx(React.Fragment, { children: this.renderSkippedLineIndicator(blocks[blockIndex].lines, blockIndex, line.left.lineNumber, line.right.lineNumber) }, lineIndex));
685
+ continue;
287
686
  }
288
- if (!this.state.expandedBlocks.includes(blockIndex)) {
289
- return null;
687
+ if (!expandedBlocks.includes(blockIndex)) {
688
+ continue;
290
689
  }
291
690
  }
292
691
  }
293
- return splitView
692
+ diffNodes.push(splitView
294
693
  ? this.renderSplitView(line, lineIndex)
295
- : this.renderInlineView(line, lineIndex);
296
- });
694
+ : this.renderInlineView(line, lineIndex));
695
+ }
696
+ // Calculate total content height
697
+ const totalContentHeight = cumulativeOffsets
698
+ ? cumulativeOffsets[cumulativeOffsets.length - 1] || 0
699
+ : totalRenderedRows * DiffViewer.ESTIMATED_ROW_HEIGHT;
700
+ // Calculate bottom padding: space after the last rendered row
701
+ const bottomPadding = cumulativeOffsets && lastRenderedRowIndex >= 0
702
+ ? totalContentHeight - (cumulativeOffsets[lastRenderedRowIndex + 1] || totalContentHeight)
703
+ : 0;
297
704
  return {
298
705
  diffNodes,
299
706
  blocks,
300
707
  lineInformation,
708
+ totalRenderedRows,
709
+ topPadding,
710
+ bottomPadding,
711
+ totalContentHeight,
712
+ renderedCount: diffNodes.length,
713
+ // Debug info
714
+ debug: {
715
+ visibleRowStart,
716
+ visibleRowEnd,
717
+ totalRows: totalRenderedRows,
718
+ offsetsLength: (_b = cumulativeOffsets === null || cumulativeOffsets === void 0 ? void 0 : cumulativeOffsets.length) !== null && _b !== void 0 ? _b : 0,
719
+ renderedCount: diffNodes.length,
720
+ scrollTop: (_d = (_c = scrollableContainerRef.current) === null || _c === void 0 ? void 0 : _c.scrollTop) !== null && _d !== void 0 ? _d : 0,
721
+ headerHeight: this.getStickyHeaderHeight(),
722
+ contentScrollTop: scrollableContainerRef.current
723
+ ? Math.max(0, scrollableContainerRef.current.scrollTop - this.getStickyHeaderHeight())
724
+ : 0,
725
+ clientHeight: (_f = (_e = scrollableContainerRef.current) === null || _e === void 0 ? void 0 : _e.clientHeight) !== null && _f !== void 0 ? _f : 0,
726
+ }
301
727
  };
302
728
  };
303
729
  this.render = () => {
730
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
304
731
  const { oldValue, newValue, useDarkTheme, leftTitle, rightTitle, splitView, compareMethod, hideLineNumbers, nonce, } = this.props;
305
732
  if (typeof compareMethod === "string" &&
306
- compareMethod !== compute_lines_js_1.DiffMethod.JSON) {
733
+ compareMethod !== DiffMethod.JSON) {
307
734
  if (typeof oldValue !== "string" || typeof newValue !== "string") {
308
735
  throw Error('"oldValue" and "newValue" should be strings');
309
736
  }
@@ -323,16 +750,16 @@ class DiffViewer extends React.Component {
323
750
  let deletions = 0;
324
751
  let additions = 0;
325
752
  for (const l of nodes.lineInformation) {
326
- if (l.left.type === compute_lines_js_1.DiffType.ADDED) {
753
+ if (l.left.type === DiffType.ADDED) {
327
754
  additions++;
328
755
  }
329
- if (l.right.type === compute_lines_js_1.DiffType.ADDED) {
756
+ if (l.right.type === DiffType.ADDED) {
330
757
  additions++;
331
758
  }
332
- if (l.left.type === compute_lines_js_1.DiffType.REMOVED) {
759
+ if (l.left.type === DiffType.REMOVED) {
333
760
  deletions++;
334
761
  }
335
- if (l.right.type === compute_lines_js_1.DiffType.REMOVED) {
762
+ if (l.right.type === DiffType.REMOVED) {
336
763
  deletions++;
337
764
  }
338
765
  }
@@ -341,39 +768,216 @@ class DiffViewer extends React.Component {
341
768
  const blocks = [];
342
769
  for (let i = 0; i < 5; i++) {
343
770
  if (percentageAddition > i * 20) {
344
- blocks.push((0, jsx_runtime_1.jsx)("span", { className: (0, classnames_1.default)(this.styles.block, this.styles.blockAddition) }, i));
771
+ blocks.push(_jsx("span", { className: cn(this.styles.block, this.styles.blockAddition) }, i));
345
772
  }
346
773
  else {
347
- blocks.push((0, jsx_runtime_1.jsx)("span", { className: (0, classnames_1.default)(this.styles.block, this.styles.blockDeletion) }, i));
774
+ blocks.push(_jsx("span", { className: cn(this.styles.block, this.styles.blockDeletion) }, i));
348
775
  }
349
776
  }
350
777
  const allExpanded = this.state.expandedBlocks.length === nodes.blocks.length;
351
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: this.styles.summary, role: "banner", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
352
- this.setState({
353
- expandedBlocks: allExpanded
354
- ? []
355
- : nodes.blocks.map((b) => b.index),
356
- });
357
- }, children: allExpanded ? (0, jsx_runtime_1.jsx)(fold_js_1.Fold, {}) : (0, jsx_runtime_1.jsx)(expand_js_1.Expand, {}) }), " ", totalChanges, (0, jsx_runtime_1.jsx)("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? (0, jsx_runtime_1.jsx)("span", { children: this.props.summary }) : null] }), (0, jsx_runtime_1.jsx)("table", { className: (0, classnames_1.default)(this.styles.diffContainer, {
358
- [this.styles.splitView]: splitView,
359
- }), onMouseUp: () => {
360
- const elements = document.getElementsByClassName("right");
361
- for (let i = 0; i < elements.length; i++) {
362
- const element = elements.item(i);
363
- element.classList.remove(this.styles.noSelect);
364
- }
365
- const elementsLeft = document.getElementsByClassName("left");
366
- for (let i = 0; i < elementsLeft.length; i++) {
367
- const element = elementsLeft.item(i);
368
- element.classList.remove(this.styles.noSelect);
369
- }
370
- }, children: (0, jsx_runtime_1.jsxs)("tbody", { children: [(0, jsx_runtime_1.jsxs)("tr", { children: [!this.props.hideLineNumbers ? (0, jsx_runtime_1.jsx)("td", { width: "50px" }) : null, !splitView && !this.props.hideLineNumbers ? ((0, jsx_runtime_1.jsx)("td", { width: "50px" })) : null, this.props.renderGutter ? (0, jsx_runtime_1.jsx)("td", { width: "50px" }) : null, (0, jsx_runtime_1.jsx)("td", { width: "28px" }), (0, jsx_runtime_1.jsx)("td", { width: "100%" }), splitView ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [!this.props.hideLineNumbers ? (0, jsx_runtime_1.jsx)("td", { width: "50px" }) : null, this.props.renderGutter ? (0, jsx_runtime_1.jsx)("td", { width: "50px" }) : null, (0, jsx_runtime_1.jsx)("td", { width: "28px" }), (0, jsx_runtime_1.jsx)("td", { width: "100%" })] })) : null] }), leftTitle || rightTitle ? ((0, jsx_runtime_1.jsxs)("tr", { children: [(0, jsx_runtime_1.jsx)("th", { colSpan: splitView ? colSpanOnSplitView : colSpanOnInlineView, className: (0, classnames_1.default)(this.styles.titleBlock, this.styles.column), children: leftTitle ? ((0, jsx_runtime_1.jsx)("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView ? ((0, jsx_runtime_1.jsx)("th", { colSpan: colSpanOnSplitView, className: (0, classnames_1.default)(this.styles.titleBlock, this.styles.column), children: rightTitle ? ((0, jsx_runtime_1.jsx)("pre", { className: this.styles.contentText, children: rightTitle })) : null })) : null] })) : null, nodes.diffNodes] }) })] }));
778
+ const LoadingElement = this.props.loadingElement;
779
+ const scrollDivStyle = this.props.infiniteLoading ? {
780
+ overflowY: 'scroll',
781
+ overflowX: 'hidden',
782
+ height: this.props.infiniteLoading.containerHeight
783
+ } : {};
784
+ // Only apply noWrap when infiniteLoading is enabled but we don't have cumulative offsets yet
785
+ // Once offsets are calculated, we enable pre-wrap for proper text wrapping
786
+ const shouldNoWrap = !!this.props.infiniteLoading && !this.state.cumulativeOffsets;
787
+ const tableElement = (_jsxs("table", { className: cn(this.styles.diffContainer, {
788
+ [this.styles.splitView]: splitView,
789
+ [this.styles.noWrap]: shouldNoWrap,
790
+ }), onMouseUp: () => {
791
+ const elements = document.getElementsByClassName("right");
792
+ for (let i = 0; i < elements.length; i++) {
793
+ const element = elements.item(i);
794
+ element.classList.remove(this.styles.noSelect);
795
+ }
796
+ const elementsLeft = document.getElementsByClassName("left");
797
+ for (let i = 0; i < elementsLeft.length; i++) {
798
+ const element = elementsLeft.item(i);
799
+ element.classList.remove(this.styles.noSelect);
800
+ }
801
+ }, children: [_jsxs("colgroup", { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), !splitView && !this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" }), splitView && (_jsxs(_Fragment, { children: [!this.props.hideLineNumbers && _jsx("col", { width: "50px" }), this.props.renderGutter && _jsx("col", { width: "50px" }), _jsx("col", { width: "28px" }), _jsx("col", { width: "auto" })] }))] }), _jsx("tbody", { children: nodes.diffNodes })] }));
802
+ return (_jsxs("div", { style: Object.assign(Object.assign({}, scrollDivStyle), { position: 'relative' }), onScroll: this.onScroll, ref: this.state.scrollableContainerRef, children: [(!this.props.hideSummary || leftTitle || rightTitle) && (_jsxs("div", { ref: this.stickyHeaderRef, className: this.styles.stickyHeader, children: [!this.props.hideSummary && (_jsxs("div", { className: this.styles.summary, role: "banner", children: [_jsx("button", { type: "button", className: this.styles.allExpandButton, onClick: () => {
803
+ this.setState({
804
+ expandedBlocks: allExpanded
805
+ ? []
806
+ : nodes.blocks.map((b) => b.index),
807
+ }, () => this.recalculateOffsets());
808
+ }, children: allExpanded ? _jsx(Fold, {}) : _jsx(Expand, {}) }), " ", totalChanges, _jsx("div", { style: { display: "flex", gap: "1px" }, children: blocks }), this.props.summary ? _jsx("span", { children: this.props.summary }) : null] })), (leftTitle || rightTitle) && (_jsxs("div", { className: this.styles.columnHeaders, children: [_jsx("div", { className: this.styles.titleBlock, children: leftTitle ? (_jsx("pre", { className: this.styles.contentText, children: leftTitle })) : null }), splitView && (_jsx("div", { className: this.styles.titleBlock, children: rightTitle ? (_jsx("pre", { className: this.styles.contentText, children: rightTitle })) : null }))] }))] })), this.state.isLoading && LoadingElement && _jsx(LoadingElement, {}), this.props.infiniteLoading ? (_jsx("div", { style: {
809
+ paddingTop: nodes.topPadding,
810
+ paddingBottom: nodes.bottomPadding,
811
+ }, children: tableElement })) : (tableElement), _jsx("span", { ref: this.charMeasureRef, style: {
812
+ position: 'absolute',
813
+ top: 0,
814
+ left: '-9999px',
815
+ visibility: 'hidden',
816
+ whiteSpace: 'pre',
817
+ fontFamily: 'monospace',
818
+ fontSize: 12,
819
+ }, "aria-hidden": "true", children: "M" }), this.props.infiniteLoading && this.props.showDebugInfo && (_jsxs("div", { style: {
820
+ position: 'fixed',
821
+ top: 10,
822
+ right: 10,
823
+ background: 'rgba(0,0,0,0.85)',
824
+ color: '#0f0',
825
+ padding: '10px',
826
+ fontFamily: 'monospace',
827
+ fontSize: '11px',
828
+ zIndex: 9999,
829
+ borderRadius: '4px',
830
+ maxWidth: '300px',
831
+ lineHeight: 1.4,
832
+ }, children: [_jsx("div", { style: { fontWeight: 'bold', marginBottom: '5px', color: '#fff' }, children: "Debug Info" }), _jsxs("div", { children: ["scrollTop: ", nodes.debug.scrollTop] }), _jsxs("div", { children: ["headerHeight: ", nodes.debug.headerHeight] }), _jsxs("div", { children: ["contentScrollTop: ", nodes.debug.contentScrollTop] }), _jsxs("div", { children: ["clientHeight: ", nodes.debug.clientHeight] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["visibleRowStart: ", nodes.debug.visibleRowStart] }), _jsxs("div", { children: ["visibleRowEnd: ", nodes.debug.visibleRowEnd] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["totalRows: ", nodes.debug.totalRows] }), _jsxs("div", { children: ["offsetsLength: ", nodes.debug.offsetsLength] }), _jsxs("div", { children: ["renderedCount: ", nodes.debug.renderedCount] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px' }, children: [_jsxs("div", { children: ["topPadding: ", nodes.topPadding.toFixed(0)] }), _jsxs("div", { children: ["bottomPadding: ", nodes.bottomPadding.toFixed(0)] }), _jsxs("div", { children: ["totalContentHeight: ", nodes.totalContentHeight.toFixed(0)] })] }), _jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#ff0' }, children: [_jsxs("div", { children: ["cumulativeOffsets: ", this.state.cumulativeOffsets ? 'SET' : 'NULL'] }), _jsxs("div", { children: ["columnWidth: ", (_b = (_a = this.state.contentColumnWidth) === null || _a === void 0 ? void 0 : _a.toFixed(0)) !== null && _b !== void 0 ? _b : 'N/A', "px"] }), _jsxs("div", { children: ["charWidth: ", (_d = (_c = this.state.charWidth) === null || _c === void 0 ? void 0 : _c.toFixed(2)) !== null && _d !== void 0 ? _d : 'N/A', "px"] }), _jsxs("div", { children: ["charsPerRow: ", this.state.contentColumnWidth && this.state.charWidth ? Math.floor(this.state.contentColumnWidth / this.state.charWidth) : 'N/A'] })] }), this.state.cumulativeOffsets && (_jsxs("div", { style: { marginTop: '5px', borderTop: '1px solid #444', paddingTop: '5px', color: '#0ff', fontSize: '10px' }, children: [_jsxs("div", { children: ["offsets[", nodes.debug.visibleRowEnd, "]: ", (_f = (_e = this.state.cumulativeOffsets[nodes.debug.visibleRowEnd]) === null || _e === void 0 ? void 0 : _e.toFixed(0)) !== null && _f !== void 0 ? _f : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows - 1, "]: ", (_h = (_g = this.state.cumulativeOffsets[nodes.debug.totalRows - 1]) === null || _g === void 0 ? void 0 : _g.toFixed(0)) !== null && _h !== void 0 ? _h : 'N/A'] }), _jsxs("div", { children: ["offsets[", nodes.debug.totalRows, "]: ", (_k = (_j = this.state.cumulativeOffsets[nodes.debug.totalRows]) === null || _j === void 0 ? void 0 : _j.toFixed(0)) !== null && _k !== void 0 ? _k : 'N/A'] }), _jsxs("div", { style: { marginTop: '3px' }, children: ["viewportEnd: ", (nodes.debug.contentScrollTop + nodes.debug.clientHeight).toFixed(0)] }), _jsxs("div", { style: { marginTop: '3px', color: '#f0f' }, children: ["scrollHeight: ", (_m = (_l = this.state.scrollableContainerRef.current) === null || _l === void 0 ? void 0 : _l.scrollHeight) !== null && _m !== void 0 ? _m : 'N/A'] }), _jsxs("div", { children: ["maxScrollTop: ", ((_p = (_o = this.state.scrollableContainerRef.current) === null || _o === void 0 ? void 0 : _o.scrollHeight) !== null && _p !== void 0 ? _p : 0) - nodes.debug.clientHeight] })] }))] }))] }));
371
833
  };
372
834
  this.state = {
373
835
  expandedBlocks: [],
374
836
  noSelect: undefined,
837
+ scrollableContainerRef: React.createRef(),
838
+ computedDiffResult: {},
839
+ isLoading: false,
840
+ visibleStartRow: 0,
841
+ contentColumnWidth: null,
842
+ charWidth: null,
843
+ cumulativeOffsets: null,
375
844
  };
376
845
  }
846
+ /**
847
+ * Gets the height of the sticky header, if present.
848
+ */
849
+ getStickyHeaderHeight() {
850
+ var _a;
851
+ return ((_a = this.stickyHeaderRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
852
+ }
853
+ /**
854
+ * Measures the width of a single character in the monospace font.
855
+ * Falls back to 7.2px if measurement fails.
856
+ */
857
+ measureCharWidth() {
858
+ const span = this.charMeasureRef.current;
859
+ if (!span)
860
+ return 7.2; // fallback
861
+ return span.getBoundingClientRect().width || 7.2;
862
+ }
863
+ /**
864
+ * Measures the available width for content in a content column.
865
+ * Falls back to estimating from container width if direct measurement fails.
866
+ */
867
+ measureContentColumnWidth() {
868
+ // Try direct measurement first
869
+ const cell = this.contentColumnRef.current;
870
+ if (cell && cell.clientWidth > 0) {
871
+ const style = window.getComputedStyle(cell);
872
+ const padding = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight);
873
+ const width = cell.clientWidth - padding;
874
+ if (width > 0)
875
+ return width;
876
+ }
877
+ // Fallback: estimate from container width
878
+ // In split view: container has 2 content columns + gutters (50px each) + markers (28px each)
879
+ // In unified view: 1 content column + 2 gutters + 1 marker
880
+ const container = this.state.scrollableContainerRef.current;
881
+ if (!container || container.clientWidth <= 0)
882
+ return null;
883
+ const containerWidth = container.clientWidth;
884
+ const gutterWidth = this.props.hideLineNumbers ? 0 : 50;
885
+ const markerWidth = 28;
886
+ const gutterCount = this.props.splitView ? 2 : 2; // left gutter(s)
887
+ const markerCount = this.props.splitView ? 2 : 1;
888
+ const contentColumns = this.props.splitView ? 2 : 1;
889
+ const fixedWidth = gutterCount * gutterWidth + markerCount * markerWidth;
890
+ const availableWidth = containerWidth - fixedWidth;
891
+ return Math.max(100, availableWidth / contentColumns); // minimum 100px
892
+ }
893
+ /**
894
+ * Gets the text length from a value that may be a string or DiffInformation array.
895
+ */
896
+ getTextLength(value) {
897
+ if (!value)
898
+ return 0;
899
+ if (typeof value === 'string')
900
+ return value.length;
901
+ return value.reduce((sum, d) => sum + (typeof d.value === 'string' ? d.value.length : 0), 0);
902
+ }
903
+ /**
904
+ * Builds cumulative vertical offsets for each line based on character count and column width.
905
+ * This allows accurate scroll position calculations with variable row heights.
906
+ */
907
+ buildCumulativeOffsets(lineInformation, lineBlocks, blocks, expandedBlocks, showDiffOnly, charWidth, columnWidth, splitView) {
908
+ var _a, _b;
909
+ const offsets = [0];
910
+ const seenBlocks = new Set();
911
+ for (let i = 0; i < lineInformation.length; i++) {
912
+ const line = lineInformation[i];
913
+ if (showDiffOnly) {
914
+ const blockIndex = lineBlocks[i];
915
+ if (blockIndex !== undefined && !expandedBlocks.includes(blockIndex)) {
916
+ const isLastLine = blocks[blockIndex].endLine === i;
917
+ if (!seenBlocks.has(blockIndex) && isLastLine) {
918
+ seenBlocks.add(blockIndex);
919
+ offsets.push(offsets[offsets.length - 1] + DiffViewer.ESTIMATED_ROW_HEIGHT);
920
+ }
921
+ continue;
922
+ }
923
+ }
924
+ // Calculate visual rows for this line
925
+ const leftLen = ((_a = line.left) === null || _a === void 0 ? void 0 : _a.value) ? this.getTextLength(line.left.value) : 0;
926
+ const rightLen = ((_b = line.right) === null || _b === void 0 ? void 0 : _b.value) ? this.getTextLength(line.right.value) : 0;
927
+ const maxLen = splitView ? Math.max(leftLen, rightLen) : (leftLen || rightLen);
928
+ const charsPerRow = Math.floor(columnWidth / charWidth);
929
+ const visualRows = charsPerRow > 0 ? Math.max(1, Math.ceil(maxLen / charsPerRow)) : 1;
930
+ offsets.push(offsets[offsets.length - 1] + visualRows * DiffViewer.ESTIMATED_ROW_HEIGHT);
931
+ }
932
+ return offsets;
933
+ }
934
+ /**
935
+ * Binary search to find the line index at a given scroll offset.
936
+ */
937
+ findLineAtOffset(scrollTop, offsets) {
938
+ let low = 0;
939
+ let high = offsets.length - 2;
940
+ while (low < high) {
941
+ const mid = Math.floor((low + high + 1) / 2);
942
+ if (offsets[mid] <= scrollTop) {
943
+ low = mid;
944
+ }
945
+ else {
946
+ high = mid - 1;
947
+ }
948
+ }
949
+ return low;
950
+ }
951
+ componentDidUpdate(prevProps) {
952
+ if (prevProps.oldValue !== this.props.oldValue ||
953
+ prevProps.newValue !== this.props.newValue ||
954
+ prevProps.compareMethod !== this.props.compareMethod ||
955
+ prevProps.disableWordDiff !== this.props.disableWordDiff ||
956
+ prevProps.linesOffset !== this.props.linesOffset) {
957
+ // Clear word diff cache when diff changes
958
+ this.wordDiffCache.clear();
959
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true, visibleStartRow: 0, cumulativeOffsets: null })));
960
+ this.memoisedCompute();
961
+ }
962
+ }
963
+ componentDidMount() {
964
+ this.setState((prev) => (Object.assign(Object.assign({}, prev), { isLoading: true })));
965
+ this.memoisedCompute();
966
+ // Set up ResizeObserver for recalculating offsets on container resize
967
+ if (typeof ResizeObserver !== 'undefined' && this.props.infiniteLoading) {
968
+ this.resizeObserver = new ResizeObserver(() => {
969
+ requestAnimationFrame(() => this.recalculateOffsets());
970
+ });
971
+ const container = this.state.scrollableContainerRef.current;
972
+ if (container) {
973
+ this.resizeObserver.observe(container);
974
+ }
975
+ }
976
+ }
977
+ componentWillUnmount() {
978
+ var _a;
979
+ (_a = this.resizeObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
980
+ }
377
981
  }
378
982
  DiffViewer.defaultProps = {
379
983
  oldValue: "",
@@ -381,7 +985,7 @@ DiffViewer.defaultProps = {
381
985
  splitView: true,
382
986
  highlightLines: [],
383
987
  disableWordDiff: false,
384
- compareMethod: compute_lines_js_1.DiffMethod.CHARS,
988
+ compareMethod: DiffMethod.CHARS,
385
989
  styles: {},
386
990
  hideLineNumbers: false,
387
991
  extraLinesSurroundingDiff: 3,
@@ -390,4 +994,7 @@ DiffViewer.defaultProps = {
390
994
  linesOffset: 0,
391
995
  nonce: "",
392
996
  };
393
- exports.default = DiffViewer;
997
+ // Estimated row height based on lineHeight: 1.6em with 12px base font
998
+ DiffViewer.ESTIMATED_ROW_HEIGHT = 19;
999
+ export default DiffViewer;
1000
+ export { DiffMethod };