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.
- package/.claude/settings.local.json +21 -0
- package/.github/workflows/publish.yml +48 -0
- package/.github/workflows/release.yml +23 -8
- package/CHANGELOG.md +20 -0
- package/README.md +147 -29
- package/lib/cjs/src/compute-hidden-blocks.js +1 -4
- package/lib/cjs/src/compute-lines.d.ts +27 -3
- package/lib/cjs/src/compute-lines.js +330 -51
- package/lib/cjs/src/computeWorker.d.ts +1 -0
- package/lib/cjs/src/computeWorker.js +10 -0
- package/lib/cjs/src/expand.js +3 -6
- package/lib/cjs/src/fold.js +3 -6
- package/lib/cjs/src/index.d.ts +111 -2
- package/lib/cjs/src/index.js +737 -130
- package/lib/cjs/src/styles.d.ts +3 -0
- package/lib/cjs/src/styles.js +68 -29
- package/lib/esm/src/compute-lines.js +325 -10
- package/lib/esm/src/computeWorker.js +10 -0
- package/lib/esm/src/index.js +696 -52
- package/lib/esm/src/styles.js +65 -21
- package/package.json +32 -32
- package/playwright-report/index.html +76 -0
- package/publish-examples.sh +0 -0
- package/test-results/.last-run.json +4 -0
- package/tsconfig.json +1 -1
package/lib/cjs/src/styles.d.ts
CHANGED
|
@@ -22,10 +22,13 @@ export interface ReactDiffViewerStyles {
|
|
|
22
22
|
emptyGutter?: string;
|
|
23
23
|
emptyLine?: string;
|
|
24
24
|
codeFold?: string;
|
|
25
|
+
stickyHeader?: string;
|
|
26
|
+
columnHeaders?: string;
|
|
25
27
|
titleBlock?: string;
|
|
26
28
|
content?: string;
|
|
27
29
|
column?: string;
|
|
28
30
|
noSelect?: string;
|
|
31
|
+
noWrap?: string;
|
|
29
32
|
splitView?: string;
|
|
30
33
|
allExpandButton?: string;
|
|
31
34
|
[key: string]: string | undefined;
|
package/lib/cjs/src/styles.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
2
|
var t = {};
|
|
4
3
|
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
@@ -10,12 +9,8 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
9
|
}
|
|
11
10
|
return t;
|
|
12
11
|
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
const create_instance_1 = __importDefault(require("@emotion/css/create-instance"));
|
|
18
|
-
exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
12
|
+
import createEmotion from "@emotion/css/create-instance";
|
|
13
|
+
export default (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
19
14
|
const { variables: overrideVariables = {} } = styleOverride, styles = __rest(styleOverride, ["variables"]);
|
|
20
15
|
const themeVariables = {
|
|
21
16
|
light: Object.assign({
|
|
@@ -48,15 +43,15 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
48
43
|
dark: Object.assign({
|
|
49
44
|
diffViewerBackground: "#2e303c",
|
|
50
45
|
diffViewerColor: "#FFF",
|
|
51
|
-
addedBackground: "#
|
|
46
|
+
addedBackground: "#2ea04326",
|
|
52
47
|
addedColor: "white",
|
|
53
|
-
removedBackground: "#
|
|
48
|
+
removedBackground: "#f851491a",
|
|
54
49
|
removedColor: "white",
|
|
55
50
|
changedBackground: "#3e302c",
|
|
56
|
-
wordAddedBackground: "#
|
|
57
|
-
wordRemovedBackground: "#
|
|
58
|
-
addedGutterBackground: "#
|
|
59
|
-
removedGutterBackground: "#
|
|
51
|
+
wordAddedBackground: "#2ea04366",
|
|
52
|
+
wordRemovedBackground: "#f8514966",
|
|
53
|
+
addedGutterBackground: "#3fb9504d",
|
|
54
|
+
removedGutterBackground: "#f851494d",
|
|
60
55
|
gutterBackground: "#2c2f3a",
|
|
61
56
|
gutterBackgroundDark: "#262933",
|
|
62
57
|
highlightBackground: "#2a3967",
|
|
@@ -64,24 +59,31 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
64
59
|
codeFoldGutterBackground: "#262831",
|
|
65
60
|
codeFoldBackground: "#262831",
|
|
66
61
|
emptyLineBackground: "#363946",
|
|
67
|
-
gutterColor: "#
|
|
68
|
-
addedGutterColor: "#
|
|
69
|
-
removedGutterColor: "#
|
|
70
|
-
codeFoldContentColor: "#
|
|
62
|
+
gutterColor: "#f0f6fc",
|
|
63
|
+
addedGutterColor: "#f0f6fc",
|
|
64
|
+
removedGutterColor: "#f0f6fc",
|
|
65
|
+
codeFoldContentColor: "#9198a1",
|
|
71
66
|
diffViewerTitleBackground: "#2f323e",
|
|
72
|
-
diffViewerTitleColor: "#
|
|
67
|
+
diffViewerTitleColor: "#f0f6fc",
|
|
73
68
|
diffViewerTitleBorderColor: "#353846",
|
|
74
69
|
}, (overrideVariables.dark || {})),
|
|
75
70
|
};
|
|
76
71
|
const variables = useDarkTheme ? themeVariables.dark : themeVariables.light;
|
|
77
|
-
const { css, cx } = (
|
|
72
|
+
const { css, cx } = createEmotion({ key: "react-diff", nonce });
|
|
78
73
|
const content = css({
|
|
79
74
|
width: "auto",
|
|
75
|
+
overflow: "hidden",
|
|
80
76
|
label: "content",
|
|
81
77
|
});
|
|
82
78
|
const splitView = css({
|
|
83
79
|
label: "split-view",
|
|
84
80
|
});
|
|
81
|
+
const stickyHeader = css({
|
|
82
|
+
position: "sticky",
|
|
83
|
+
top: 0,
|
|
84
|
+
zIndex: 2,
|
|
85
|
+
label: "sticky-header",
|
|
86
|
+
});
|
|
85
87
|
const summary = css({
|
|
86
88
|
background: variables.diffViewerTitleBackground,
|
|
87
89
|
color: variables.diffViewerTitleColor,
|
|
@@ -90,13 +92,19 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
90
92
|
alignItems: "center",
|
|
91
93
|
gap: "0.5em",
|
|
92
94
|
fontFamily: "monospace",
|
|
95
|
+
fontSize: 12,
|
|
93
96
|
fill: variables.diffViewerTitleColor,
|
|
94
97
|
});
|
|
98
|
+
const columnHeaders = css({
|
|
99
|
+
display: "flex",
|
|
100
|
+
label: "column-headers",
|
|
101
|
+
});
|
|
95
102
|
const diffContainer = css({
|
|
96
103
|
width: "100%",
|
|
97
104
|
minWidth: "1000px",
|
|
98
105
|
overflowX: "auto",
|
|
99
106
|
tableLayout: "fixed",
|
|
107
|
+
fontSize: 12,
|
|
100
108
|
background: variables.diffViewerBackground,
|
|
101
109
|
pre: {
|
|
102
110
|
margin: 0,
|
|
@@ -106,6 +114,9 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
106
114
|
},
|
|
107
115
|
label: "diff-container",
|
|
108
116
|
borderCollapse: "collapse",
|
|
117
|
+
"@media (max-width: 768px)": {
|
|
118
|
+
minWidth: "unset",
|
|
119
|
+
},
|
|
109
120
|
});
|
|
110
121
|
const lineContent = css({
|
|
111
122
|
overflow: "hidden",
|
|
@@ -123,6 +134,16 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
123
134
|
userSelect: "none",
|
|
124
135
|
label: "unselectable",
|
|
125
136
|
});
|
|
137
|
+
const noWrap = css({
|
|
138
|
+
label: "no-wrap",
|
|
139
|
+
pre: {
|
|
140
|
+
whiteSpace: "pre",
|
|
141
|
+
},
|
|
142
|
+
[`.${contentText}`]: {
|
|
143
|
+
whiteSpace: "pre",
|
|
144
|
+
lineBreak: "auto",
|
|
145
|
+
},
|
|
146
|
+
});
|
|
126
147
|
const allExpandButton = css({
|
|
127
148
|
background: "transparent",
|
|
128
149
|
border: "none",
|
|
@@ -147,8 +168,13 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
147
168
|
overflow: "hidden",
|
|
148
169
|
width: "50%",
|
|
149
170
|
borderBottom: `1px solid ${variables.diffViewerTitleBorderColor}`,
|
|
171
|
+
boxSizing: "border-box",
|
|
172
|
+
fontSize: 12,
|
|
150
173
|
label: "title-block",
|
|
151
|
-
":
|
|
174
|
+
":only-child": {
|
|
175
|
+
width: "100%",
|
|
176
|
+
},
|
|
177
|
+
":last-child:not(:only-child)": {
|
|
152
178
|
borderLeft: `1px solid ${variables.diffViewerTitleBorderColor}`,
|
|
153
179
|
},
|
|
154
180
|
[`.${contentText}`]: {
|
|
@@ -189,20 +215,16 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
189
215
|
label: "diff-changed",
|
|
190
216
|
});
|
|
191
217
|
const wordDiff = css({
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
borderRadius: 4,
|
|
195
|
-
wordBreak: "break-all",
|
|
218
|
+
display: "inline",
|
|
219
|
+
textDecoration: "none",
|
|
196
220
|
label: "word-diff",
|
|
197
221
|
});
|
|
198
222
|
const wordAdded = css({
|
|
199
223
|
background: variables.wordAddedBackground,
|
|
200
|
-
textDecoration: "none",
|
|
201
224
|
label: "word-added",
|
|
202
225
|
});
|
|
203
226
|
const wordRemoved = css({
|
|
204
227
|
background: variables.wordRemovedBackground,
|
|
205
|
-
textDecoration: "none",
|
|
206
228
|
label: "word-removed",
|
|
207
229
|
});
|
|
208
230
|
const codeFoldGutter = css({
|
|
@@ -210,6 +232,8 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
210
232
|
label: "code-fold-gutter",
|
|
211
233
|
minWidth: "50px",
|
|
212
234
|
width: "50px",
|
|
235
|
+
textAlign: "center",
|
|
236
|
+
fill: variables.codeFoldContentColor,
|
|
213
237
|
});
|
|
214
238
|
const codeFoldContentContainer = css({
|
|
215
239
|
padding: "",
|
|
@@ -220,6 +244,7 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
220
244
|
display: "inline",
|
|
221
245
|
margin: 0,
|
|
222
246
|
border: "none",
|
|
247
|
+
fill: variables.codeFoldContentColor,
|
|
223
248
|
label: "code-fold-expand-button",
|
|
224
249
|
});
|
|
225
250
|
const codeFoldContent = css({
|
|
@@ -244,12 +269,20 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
244
269
|
});
|
|
245
270
|
const codeFold = css({
|
|
246
271
|
backgroundColor: variables.codeFoldBackground,
|
|
247
|
-
|
|
248
|
-
fontSize: 14,
|
|
272
|
+
fontSize: 12,
|
|
249
273
|
alignItems: "center",
|
|
250
274
|
userSelect: "none",
|
|
251
275
|
fontWeight: 700,
|
|
276
|
+
cursor: "pointer",
|
|
252
277
|
label: "code-fold",
|
|
278
|
+
"&:hover": {
|
|
279
|
+
color: variables.diffViewerColor,
|
|
280
|
+
fill: variables.diffViewerColor,
|
|
281
|
+
"& *": {
|
|
282
|
+
color: variables.diffViewerColor,
|
|
283
|
+
fill: variables.diffViewerColor,
|
|
284
|
+
},
|
|
285
|
+
},
|
|
253
286
|
a: {
|
|
254
287
|
textDecoration: "underline !important",
|
|
255
288
|
cursor: "pointer",
|
|
@@ -296,7 +329,8 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
296
329
|
padding: "0 10px",
|
|
297
330
|
whiteSpace: "nowrap",
|
|
298
331
|
label: "gutter",
|
|
299
|
-
textAlign: "
|
|
332
|
+
textAlign: "center",
|
|
333
|
+
color: variables.gutterColor,
|
|
300
334
|
background: variables.gutterBackground,
|
|
301
335
|
"&:hover": {
|
|
302
336
|
cursor: "pointer",
|
|
@@ -307,6 +341,8 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
307
341
|
},
|
|
308
342
|
pre: {
|
|
309
343
|
opacity: 0.5,
|
|
344
|
+
textAlign: "center",
|
|
345
|
+
width: "100%",
|
|
310
346
|
},
|
|
311
347
|
[`&.${diffAdded}`]: {
|
|
312
348
|
background: variables.addedGutterBackground,
|
|
@@ -354,6 +390,7 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
354
390
|
blockDeletion,
|
|
355
391
|
wordRemoved,
|
|
356
392
|
noSelect: unselectable,
|
|
393
|
+
noWrap,
|
|
357
394
|
codeFoldGutter,
|
|
358
395
|
codeFoldExpandButton,
|
|
359
396
|
codeFoldContentContainer,
|
|
@@ -365,6 +402,8 @@ exports.default = (styleOverride, useDarkTheme = false, nonce = "") => {
|
|
|
365
402
|
content,
|
|
366
403
|
column,
|
|
367
404
|
codeFoldContent,
|
|
405
|
+
stickyHeader,
|
|
406
|
+
columnHeaders,
|
|
368
407
|
titleBlock,
|
|
369
408
|
allExpandButton,
|
|
370
409
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as diff from "diff";
|
|
2
|
+
import * as yaml from "js-yaml";
|
|
2
3
|
const jsDiff = diff;
|
|
3
4
|
export var DiffType;
|
|
4
5
|
(function (DiffType) {
|
|
@@ -7,6 +8,249 @@ export var DiffType;
|
|
|
7
8
|
DiffType[DiffType["REMOVED"] = 2] = "REMOVED";
|
|
8
9
|
DiffType[DiffType["CHANGED"] = 3] = "CHANGED";
|
|
9
10
|
})(DiffType || (DiffType = {}));
|
|
11
|
+
/**
|
|
12
|
+
* Stringify a value using the specified format.
|
|
13
|
+
*/
|
|
14
|
+
function stringify(val, format) {
|
|
15
|
+
if (format === 'yaml') {
|
|
16
|
+
return yaml.dump(val, { indent: 2, lineWidth: -1, noRefs: true }).trimEnd();
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(val, null, 2);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Performs a fast structural diff on objects.
|
|
22
|
+
*
|
|
23
|
+
* Strategy: Use structural comparison to identify which subtrees changed,
|
|
24
|
+
* then use diffLines only on those changed subtrees. This avoids running
|
|
25
|
+
* the expensive O(ND) Myers diff on the entire content, while still producing
|
|
26
|
+
* proper line-by-line diffs for the parts that changed.
|
|
27
|
+
*/
|
|
28
|
+
function structuralDiff(oldObj, newObj, format = 'json') {
|
|
29
|
+
const oldStr = stringify(oldObj, format);
|
|
30
|
+
const newStr = stringify(newObj, format);
|
|
31
|
+
// Fast path: identical objects
|
|
32
|
+
if (oldStr === newStr) {
|
|
33
|
+
return [{ value: oldStr }];
|
|
34
|
+
}
|
|
35
|
+
// Use recursive structural diff that applies diffLines to changed subtrees
|
|
36
|
+
return diffStructurally(oldObj, newObj, 0, format);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* JSON diff that preserves key order from each object.
|
|
40
|
+
* Uses structural comparison for performance.
|
|
41
|
+
*/
|
|
42
|
+
function structuralJsonDiff(oldObj, newObj) {
|
|
43
|
+
return structuralDiff(oldObj, newObj, 'json');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Optimized diff for JSON strings that preserves original formatting and key order.
|
|
47
|
+
* Uses parsing only to check for structural equality (fast path),
|
|
48
|
+
* then falls back to diffLines on original strings to preserve formatting.
|
|
49
|
+
*/
|
|
50
|
+
function structuralJsonStringDiff(oldJson, newJson) {
|
|
51
|
+
try {
|
|
52
|
+
// Parse JSON to check for structural equality
|
|
53
|
+
const oldObj = JSON.parse(oldJson);
|
|
54
|
+
const newObj = JSON.parse(newJson);
|
|
55
|
+
// Fast path: check if structurally identical by comparing serialized forms
|
|
56
|
+
const oldNormalized = JSON.stringify(oldObj);
|
|
57
|
+
const newNormalized = JSON.stringify(newObj);
|
|
58
|
+
if (oldNormalized === newNormalized) {
|
|
59
|
+
// Structurally identical - return original string to preserve formatting
|
|
60
|
+
return [{ value: oldJson }];
|
|
61
|
+
}
|
|
62
|
+
// Files differ - use diffLines on ORIGINAL strings to preserve key order and formatting
|
|
63
|
+
return diff.diffLines(oldJson, newJson, {
|
|
64
|
+
newlineIsToken: false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
// If JSON parsing fails, fall back to line diff
|
|
69
|
+
return diff.diffLines(oldJson, newJson, {
|
|
70
|
+
newlineIsToken: false,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Optimized diff for YAML that preserves original line numbers.
|
|
76
|
+
* Uses parsing only to check for structural equality (fast path),
|
|
77
|
+
* then falls back to diffLines on original strings to preserve line numbers.
|
|
78
|
+
*/
|
|
79
|
+
function structuralYamlDiff(oldYaml, newYaml) {
|
|
80
|
+
// Parse YAML to check for structural equality
|
|
81
|
+
const oldObj = yaml.load(oldYaml);
|
|
82
|
+
const newObj = yaml.load(newYaml);
|
|
83
|
+
// Fast path: check if structurally identical by comparing serialized forms
|
|
84
|
+
const oldNormalized = yaml.dump(oldObj, { indent: 2, lineWidth: -1, noRefs: true });
|
|
85
|
+
const newNormalized = yaml.dump(newObj, { indent: 2, lineWidth: -1, noRefs: true });
|
|
86
|
+
if (oldNormalized === newNormalized) {
|
|
87
|
+
// Structurally identical - return original string to preserve formatting
|
|
88
|
+
return [{ value: oldYaml }];
|
|
89
|
+
}
|
|
90
|
+
// Files differ - use diffLines on ORIGINAL strings to preserve line numbers
|
|
91
|
+
return diff.diffLines(oldYaml, newYaml, {
|
|
92
|
+
newlineIsToken: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Recursively diff two values structurally.
|
|
97
|
+
* For unchanged parts, output as-is.
|
|
98
|
+
* For changed parts, use diffLines to get proper line-by-line diff.
|
|
99
|
+
*/
|
|
100
|
+
function diffStructurally(oldVal, newVal, indent, format = 'json') {
|
|
101
|
+
const oldStr = stringify(oldVal, format);
|
|
102
|
+
const newStr = stringify(newVal, format);
|
|
103
|
+
// Fast path: identical
|
|
104
|
+
if (oldStr === newStr) {
|
|
105
|
+
return [{ value: reindent(oldStr, indent, format) }];
|
|
106
|
+
}
|
|
107
|
+
// Both are objects - compare key by key
|
|
108
|
+
if (typeof oldVal === 'object' && oldVal !== null &&
|
|
109
|
+
typeof newVal === 'object' && newVal !== null &&
|
|
110
|
+
!Array.isArray(oldVal) && !Array.isArray(newVal)) {
|
|
111
|
+
return diffObjects(oldVal, newVal, indent, format);
|
|
112
|
+
}
|
|
113
|
+
// Both are arrays - compare element by element
|
|
114
|
+
if (Array.isArray(oldVal) && Array.isArray(newVal)) {
|
|
115
|
+
return diffArrays(oldVal, newVal, indent, format);
|
|
116
|
+
}
|
|
117
|
+
// Different types or primitives - use diffLines for proper diff
|
|
118
|
+
return diffWithLines(oldStr, newStr, indent, format);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Diff two objects key by key.
|
|
122
|
+
* Iterates in NEW key order to preserve the new file's structure,
|
|
123
|
+
* then appends any old-only keys as removed.
|
|
124
|
+
*/
|
|
125
|
+
function diffObjects(oldObj, newObj, indent, format = 'json') {
|
|
126
|
+
const changes = [];
|
|
127
|
+
const indentStr = ' '.repeat(indent);
|
|
128
|
+
const innerIndent = ' '.repeat(indent + 1);
|
|
129
|
+
const oldKeySet = new Set(Object.keys(oldObj));
|
|
130
|
+
const newKeys = Object.keys(newObj);
|
|
131
|
+
// Build ordered key list: new keys in their order, then old-only keys
|
|
132
|
+
const oldOnlyKeys = [...oldKeySet].filter(k => !(k in newObj));
|
|
133
|
+
const allKeys = [...newKeys, ...oldOnlyKeys];
|
|
134
|
+
changes.push({ value: '{\n' });
|
|
135
|
+
for (let i = 0; i < allKeys.length; i++) {
|
|
136
|
+
const key = allKeys[i];
|
|
137
|
+
const isLast = i === allKeys.length - 1;
|
|
138
|
+
const comma = isLast ? '' : ',';
|
|
139
|
+
const inOld = key in oldObj;
|
|
140
|
+
const inNew = key in newObj;
|
|
141
|
+
if (inOld && inNew) {
|
|
142
|
+
// Key in both - recursively diff values
|
|
143
|
+
const oldValStr = stringify(oldObj[key], format);
|
|
144
|
+
const newValStr = stringify(newObj[key], format);
|
|
145
|
+
if (oldValStr === newValStr) {
|
|
146
|
+
// Values identical
|
|
147
|
+
const valueStr = reindent(oldValStr, indent + 1);
|
|
148
|
+
changes.push({ value: innerIndent + JSON.stringify(key) + ': ' + valueStr + comma + '\n' });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Values differ - recursively diff them
|
|
152
|
+
const keyPrefix = innerIndent + JSON.stringify(key) + ': ';
|
|
153
|
+
const valueDiff = diffStructurally(oldObj[key], newObj[key], indent + 1, format);
|
|
154
|
+
// Prepend key to first change so they're on the same line
|
|
155
|
+
if (valueDiff.length > 0) {
|
|
156
|
+
valueDiff[0].value = keyPrefix + valueDiff[0].value;
|
|
157
|
+
}
|
|
158
|
+
// Add comma to last change if needed
|
|
159
|
+
if (comma && valueDiff.length > 0) {
|
|
160
|
+
const last = valueDiff[valueDiff.length - 1];
|
|
161
|
+
last.value = last.value.replace(/\n$/, comma + '\n');
|
|
162
|
+
}
|
|
163
|
+
changes.push(...valueDiff);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else if (inOld) {
|
|
167
|
+
// Key only in old - removed
|
|
168
|
+
const valueStr = reindent(stringify(oldObj[key], format), indent + 1);
|
|
169
|
+
changes.push({ removed: true, value: innerIndent + JSON.stringify(key) + ': ' + valueStr + comma + '\n' });
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// Key only in new - added
|
|
173
|
+
const valueStr = reindent(stringify(newObj[key], format), indent + 1);
|
|
174
|
+
changes.push({ added: true, value: innerIndent + JSON.stringify(key) + ': ' + valueStr + comma + '\n' });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
changes.push({ value: indentStr + '}\n' });
|
|
178
|
+
return changes;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Diff two arrays element by element.
|
|
182
|
+
*/
|
|
183
|
+
function diffArrays(oldArr, newArr, indent, format = 'json') {
|
|
184
|
+
const changes = [];
|
|
185
|
+
const indentStr = ' '.repeat(indent);
|
|
186
|
+
const innerIndent = ' '.repeat(indent + 1);
|
|
187
|
+
changes.push({ value: '[\n' });
|
|
188
|
+
const maxLen = Math.max(oldArr.length, newArr.length);
|
|
189
|
+
for (let i = 0; i < maxLen; i++) {
|
|
190
|
+
const isLast = i === maxLen - 1;
|
|
191
|
+
const comma = isLast ? '' : ',';
|
|
192
|
+
if (i >= oldArr.length) {
|
|
193
|
+
// Element only in new - added
|
|
194
|
+
const valueStr = reindent(stringify(newArr[i], format), indent + 1);
|
|
195
|
+
changes.push({ added: true, value: innerIndent + valueStr + comma + '\n' });
|
|
196
|
+
}
|
|
197
|
+
else if (i >= newArr.length) {
|
|
198
|
+
// Element only in old - removed
|
|
199
|
+
const valueStr = reindent(stringify(oldArr[i], format), indent + 1);
|
|
200
|
+
changes.push({ removed: true, value: innerIndent + valueStr + comma + '\n' });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Element in both - recursively diff
|
|
204
|
+
const oldElemStr = stringify(oldArr[i], format);
|
|
205
|
+
const newElemStr = stringify(newArr[i], format);
|
|
206
|
+
if (oldElemStr === newElemStr) {
|
|
207
|
+
// Elements identical
|
|
208
|
+
const valueStr = reindent(oldElemStr, indent + 1);
|
|
209
|
+
changes.push({ value: innerIndent + valueStr + comma + '\n' });
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Elements differ - recursively diff them
|
|
213
|
+
const elemDiff = diffStructurally(oldArr[i], newArr[i], indent + 1, format);
|
|
214
|
+
// Prepend indent to first change so they're on the same line
|
|
215
|
+
if (elemDiff.length > 0) {
|
|
216
|
+
elemDiff[0].value = innerIndent + elemDiff[0].value;
|
|
217
|
+
}
|
|
218
|
+
// Add comma to last change if needed
|
|
219
|
+
if (comma && elemDiff.length > 0) {
|
|
220
|
+
const last = elemDiff[elemDiff.length - 1];
|
|
221
|
+
last.value = last.value.replace(/\n$/, comma + '\n');
|
|
222
|
+
}
|
|
223
|
+
changes.push(...elemDiff);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
changes.push({ value: indentStr + ']\n' });
|
|
228
|
+
return changes;
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Use diffLines for proper line-by-line diff of two strings.
|
|
232
|
+
* This is the fallback for when structural comparison finds different values.
|
|
233
|
+
*/
|
|
234
|
+
function diffWithLines(oldStr, newStr, indent, _format = 'json') {
|
|
235
|
+
const oldIndented = reindent(oldStr, indent);
|
|
236
|
+
const newIndented = reindent(newStr, indent);
|
|
237
|
+
// Use diffLines for proper line-level comparison
|
|
238
|
+
const lineDiff = diff.diffLines(oldIndented, newIndented);
|
|
239
|
+
return lineDiff.map(change => ({
|
|
240
|
+
value: change.value,
|
|
241
|
+
added: change.added,
|
|
242
|
+
removed: change.removed
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Re-indent a string to the specified level.
|
|
247
|
+
*/
|
|
248
|
+
function reindent(str, indent, _format = 'json') {
|
|
249
|
+
if (indent === 0)
|
|
250
|
+
return str;
|
|
251
|
+
const indentStr = ' '.repeat(indent);
|
|
252
|
+
return str.split('\n').map((line, i) => i === 0 ? line : indentStr + line).join('\n');
|
|
253
|
+
}
|
|
10
254
|
// See https://github.com/kpdecker/jsdiff/tree/v4.0.1#api for more info on the below JsDiff methods
|
|
11
255
|
export var DiffMethod;
|
|
12
256
|
(function (DiffMethod) {
|
|
@@ -18,6 +262,7 @@ export var DiffMethod;
|
|
|
18
262
|
DiffMethod["SENTENCES"] = "diffSentences";
|
|
19
263
|
DiffMethod["CSS"] = "diffCss";
|
|
20
264
|
DiffMethod["JSON"] = "diffJson";
|
|
265
|
+
DiffMethod["YAML"] = "diffYaml";
|
|
21
266
|
})(DiffMethod || (DiffMethod = {}));
|
|
22
267
|
/**
|
|
23
268
|
* Splits diff text by new line and computes final list of diff lines based on
|
|
@@ -83,18 +328,37 @@ const computeDiff = (oldValue, newValue, compareMethod = DiffMethod.CHARS) => {
|
|
|
83
328
|
* @param linesOffset line number to start counting from
|
|
84
329
|
* @param showLines lines that are always shown, regardless of diff
|
|
85
330
|
*/
|
|
86
|
-
const computeLineInformation = (oldString, newString, disableWordDiff = false, lineCompareMethod = DiffMethod.CHARS, linesOffset = 0, showLines = []) => {
|
|
331
|
+
const computeLineInformation = (oldString, newString, disableWordDiff = false, lineCompareMethod = DiffMethod.CHARS, linesOffset = 0, showLines = [], deferWordDiff = false) => {
|
|
87
332
|
let diffArray = [];
|
|
88
|
-
//
|
|
333
|
+
// Handle different input types and compare methods
|
|
89
334
|
if (typeof oldString === "string" && typeof newString === "string") {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
335
|
+
// Check if we should use structural diff for JSON or YAML
|
|
336
|
+
if (lineCompareMethod === DiffMethod.JSON) {
|
|
337
|
+
// Use JSON structural diff - preserves original formatting and key order
|
|
338
|
+
diffArray = structuralJsonStringDiff(oldString, newString);
|
|
339
|
+
}
|
|
340
|
+
else if (lineCompareMethod === DiffMethod.YAML) {
|
|
341
|
+
try {
|
|
342
|
+
// Use YAML structural diff - parses, normalizes, and outputs as YAML
|
|
343
|
+
diffArray = structuralYamlDiff(oldString, newString);
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
// If YAML parsing fails, fall back to line diff
|
|
347
|
+
diffArray = diff.diffLines(oldString, newString, {
|
|
348
|
+
newlineIsToken: false,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
diffArray = diff.diffLines(oldString, newString, {
|
|
354
|
+
newlineIsToken: false,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
95
357
|
}
|
|
96
358
|
else {
|
|
97
|
-
|
|
359
|
+
// Use our fast structural JSON diff instead of diff.diffJson
|
|
360
|
+
// This is O(n) for structure comparison vs O(ND) for Myers on large strings
|
|
361
|
+
diffArray = structuralJsonDiff(oldString, newString);
|
|
98
362
|
}
|
|
99
363
|
let rightLineNumber = linesOffset;
|
|
100
364
|
let leftLineNumber = linesOffset;
|
|
@@ -145,7 +409,18 @@ const computeLineInformation = (oldString, newString, disableWordDiff = false, l
|
|
|
145
409
|
right.type = type;
|
|
146
410
|
// Do char level diff and assign the corresponding values to the
|
|
147
411
|
// left and right diff information object.
|
|
148
|
-
|
|
412
|
+
// Skip word diff for very long lines (>500 chars) to avoid performance issues
|
|
413
|
+
const MAX_LINE_LENGTH_FOR_WORD_DIFF = 500;
|
|
414
|
+
const lineIsTooLong = line.length > MAX_LINE_LENGTH_FOR_WORD_DIFF ||
|
|
415
|
+
rightValue.length > MAX_LINE_LENGTH_FOR_WORD_DIFF;
|
|
416
|
+
if (disableWordDiff || lineIsTooLong) {
|
|
417
|
+
right.value = rightValue;
|
|
418
|
+
}
|
|
419
|
+
else if (deferWordDiff) {
|
|
420
|
+
// Store raw values for deferred word diff computation
|
|
421
|
+
left.rawValue = line;
|
|
422
|
+
left.value = line;
|
|
423
|
+
right.rawValue = rightValue;
|
|
149
424
|
right.value = rightValue;
|
|
150
425
|
}
|
|
151
426
|
else {
|
|
@@ -202,4 +477,44 @@ const computeLineInformation = (oldString, newString, disableWordDiff = false, l
|
|
|
202
477
|
diffLines,
|
|
203
478
|
};
|
|
204
479
|
};
|
|
205
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Computes line diff information using a Web Worker to avoid blocking the UI thread.
|
|
482
|
+
* This offloads the expensive `computeLineInformation` logic to a separate thread.
|
|
483
|
+
*
|
|
484
|
+
* @param oldString Old string to compare.
|
|
485
|
+
* @param newString New string to compare with old string.
|
|
486
|
+
* @param disableWordDiff Flag to enable/disable word diff.
|
|
487
|
+
* @param lineCompareMethod JsDiff text diff method from https://github.com/kpdecker/jsdiff/tree/v4.0.1#api
|
|
488
|
+
* @param linesOffset line number to start counting from
|
|
489
|
+
* @param showLines lines that are always shown, regardless of diff
|
|
490
|
+
* @returns Promise<ComputedLineInformation> - Resolves with line-by-line diff data from the worker.
|
|
491
|
+
*/
|
|
492
|
+
const computeLineInformationWorker = (oldString, newString, disableWordDiff = false, lineCompareMethod = DiffMethod.CHARS, linesOffset = 0, showLines = [], deferWordDiff = false) => {
|
|
493
|
+
const fallback = () => computeLineInformation(oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff);
|
|
494
|
+
// Fall back to synchronous computation if Worker is not available (e.g., in Node.js/test environments)
|
|
495
|
+
if (typeof Worker === 'undefined') {
|
|
496
|
+
return Promise.resolve(fallback());
|
|
497
|
+
}
|
|
498
|
+
return new Promise((resolve) => {
|
|
499
|
+
let worker;
|
|
500
|
+
try {
|
|
501
|
+
worker = new Worker(new URL('./computeWorker.js', import.meta.url), { type: 'module' });
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// Worker instantiation failed - fall back to synchronous computation
|
|
505
|
+
resolve(fallback());
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
worker.onmessage = (e) => {
|
|
509
|
+
resolve(e.data);
|
|
510
|
+
worker.terminate();
|
|
511
|
+
};
|
|
512
|
+
worker.onerror = () => {
|
|
513
|
+
// Worker error - fall back to synchronous computation
|
|
514
|
+
worker.terminate();
|
|
515
|
+
resolve(fallback());
|
|
516
|
+
};
|
|
517
|
+
worker.postMessage({ oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff });
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
export { computeLineInformation, computeLineInformationWorker, computeDiff };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { computeLineInformation } from "./compute-lines";
|
|
2
|
+
/**
|
|
3
|
+
* This sets up a message handler inside the Web Worker.
|
|
4
|
+
* When the main thread sends a message to this worker (via postMessage), this function is triggered.
|
|
5
|
+
*/
|
|
6
|
+
self.onmessage = (e) => {
|
|
7
|
+
const { oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff } = e.data;
|
|
8
|
+
const result = computeLineInformation(oldString, newString, disableWordDiff, lineCompareMethod, linesOffset, showLines, deferWordDiff);
|
|
9
|
+
self.postMessage(result);
|
|
10
|
+
};
|