@script-it/json-views-core 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +174 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/annotation-capabilities.d.ts +17 -0
- package/dist/annotation-capabilities.js +21 -0
- package/dist/atomic-value.d.ts +3 -0
- package/dist/atomic-value.js +6 -0
- package/dist/date-type.d.ts +48 -0
- package/dist/date-type.js +244 -0
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +252 -0
- package/dist/document-session.d.ts +52 -0
- package/dist/document-session.js +164 -0
- package/dist/formats/conversion.d.ts +17 -0
- package/dist/formats/conversion.js +75 -0
- package/dist/formats/csv-format.d.ts +31 -0
- package/dist/formats/csv-format.js +269 -0
- package/dist/formats/document-format.d.ts +35 -0
- package/dist/formats/document-format.js +1 -0
- package/dist/formats/json-format.d.ts +12 -0
- package/dist/formats/json-format.js +109 -0
- package/dist/html-view-bindings.d.ts +4 -0
- package/dist/html-view-bindings.js +32 -0
- package/dist/html-view-limits.d.ts +1 -0
- package/dist/html-view-limits.js +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/inference-fields.d.ts +5 -0
- package/dist/inference-fields.js +23 -0
- package/dist/inference.d.ts +26 -0
- package/dist/inference.js +304 -0
- package/dist/json-limits.d.ts +8 -0
- package/dist/json-limits.js +25 -0
- package/dist/json-path.d.ts +14 -0
- package/dist/json-path.js +84 -0
- package/dist/json-source-patcher.d.ts +97 -0
- package/dist/json-source-patcher.js +778 -0
- package/dist/metadata.d.ts +137 -0
- package/dist/metadata.js +952 -0
- package/dist/safe-pattern.d.ts +5 -0
- package/dist/safe-pattern.js +20 -0
- package/dist/schema-validation.d.ts +3 -0
- package/dist/schema-validation.js +4 -0
- package/dist/source-default.d.ts +2 -0
- package/dist/source-default.js +39 -0
- package/dist/table-suitability.d.ts +5 -0
- package/dist/table-suitability.js +55 -0
- package/dist/type-registry.d.ts +57 -0
- package/dist/type-registry.js +238 -0
- package/dist/value-comparison.d.ts +18 -0
- package/dist/value-comparison.js +133 -0
- package/dist/view-model.d.ts +18 -0
- package/dist/view-model.js +134 -0
- package/package.json +52 -0
- package/schema/jsonviews.schema.json +102 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { MAX_JSON_DEPTH, JsonDepthLimitError } from './json-limits.js';
|
|
2
|
+
import { isEditableAtomicValue } from './atomic-value.js';
|
|
3
|
+
import { describeValuePath, requireValueAtPath } from './json-path.js';
|
|
4
|
+
import { sourceDiagnosticHelp } from './diagnostics.js';
|
|
5
|
+
function assertSameEditableAtomicType(current, next, path) {
|
|
6
|
+
if (!isEditableAtomicValue(current)) {
|
|
7
|
+
throw new TypeError(`JSON path ${describeValuePath(path)} does not point to an editable atomic value`);
|
|
8
|
+
}
|
|
9
|
+
if (!isEditableAtomicValue(next)) {
|
|
10
|
+
throw new TypeError(`Replacement at ${describeValuePath(path)} must be a finite editable JSON atomic value`);
|
|
11
|
+
}
|
|
12
|
+
const currentType = typeof current;
|
|
13
|
+
const nextType = typeof next;
|
|
14
|
+
if (currentType !== nextType) {
|
|
15
|
+
throw new TypeError(`Replacement at ${describeValuePath(path)} must keep type ${currentType}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class JsonSourceScanner {
|
|
19
|
+
index = 0;
|
|
20
|
+
source;
|
|
21
|
+
constructor(source) {
|
|
22
|
+
this.source = source;
|
|
23
|
+
}
|
|
24
|
+
parse() {
|
|
25
|
+
// Callers validate once with JSON.parse before scanning source spans.
|
|
26
|
+
const root = this.parseValue();
|
|
27
|
+
const frames = [];
|
|
28
|
+
const push = (node, property) => {
|
|
29
|
+
if (node.kind === 'atomic')
|
|
30
|
+
return;
|
|
31
|
+
if (frames.length >= MAX_JSON_DEPTH)
|
|
32
|
+
throw new JsonDepthLimitError();
|
|
33
|
+
frames.push({ node, afterValue: false, property });
|
|
34
|
+
};
|
|
35
|
+
push(root);
|
|
36
|
+
while (frames.length) {
|
|
37
|
+
const frame = frames[frames.length - 1];
|
|
38
|
+
const node = frame.node;
|
|
39
|
+
this.skipWhitespace();
|
|
40
|
+
const close = node.kind === 'array' ? ']' : '}';
|
|
41
|
+
if (this.source[this.index] === close) {
|
|
42
|
+
node.end = ++this.index;
|
|
43
|
+
if (frame.property)
|
|
44
|
+
frame.property.end = node.end;
|
|
45
|
+
frames.pop();
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (frame.afterValue) {
|
|
49
|
+
if (this.source[this.index++] !== ',')
|
|
50
|
+
throw new SyntaxError('Expected comma');
|
|
51
|
+
this.skipWhitespace();
|
|
52
|
+
}
|
|
53
|
+
frame.afterValue = true;
|
|
54
|
+
if (node.kind === 'array') {
|
|
55
|
+
const value = this.parseValue();
|
|
56
|
+
node.items.push(value);
|
|
57
|
+
push(value);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const start = this.index;
|
|
61
|
+
this.scanString();
|
|
62
|
+
const key = JSON.parse(this.source.slice(start, this.index));
|
|
63
|
+
this.skipWhitespace();
|
|
64
|
+
if (this.source[this.index++] !== ':')
|
|
65
|
+
throw new SyntaxError('Expected colon');
|
|
66
|
+
const value = this.parseValue();
|
|
67
|
+
const property = { key, start, end: value.end, value };
|
|
68
|
+
node.properties.push(property);
|
|
69
|
+
node.propertyIndex.set(key, value);
|
|
70
|
+
push(value, property);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
this.skipWhitespace();
|
|
74
|
+
if (this.index !== this.source.length)
|
|
75
|
+
throw new SyntaxError('Unexpected content after JSON value');
|
|
76
|
+
return root;
|
|
77
|
+
}
|
|
78
|
+
skipWhitespace() {
|
|
79
|
+
while (this.index < this.source.length && /[\t\n\r ]/.test(this.source[this.index]))
|
|
80
|
+
this.index++;
|
|
81
|
+
}
|
|
82
|
+
parseValue() {
|
|
83
|
+
this.skipWhitespace();
|
|
84
|
+
const start = this.index;
|
|
85
|
+
const token = this.source[this.index];
|
|
86
|
+
if (token === '{') {
|
|
87
|
+
this.index++;
|
|
88
|
+
return { start, end: this.index, kind: 'object', properties: [], propertyIndex: new Map() };
|
|
89
|
+
}
|
|
90
|
+
if (token === '[') {
|
|
91
|
+
this.index++;
|
|
92
|
+
return { start, end: this.index, kind: 'array', items: [] };
|
|
93
|
+
}
|
|
94
|
+
if (token === '"') {
|
|
95
|
+
this.scanString();
|
|
96
|
+
return { start, end: this.index, kind: 'atomic' };
|
|
97
|
+
}
|
|
98
|
+
if (token === 't')
|
|
99
|
+
return this.scanLiteral(start, 'true');
|
|
100
|
+
if (token === 'f')
|
|
101
|
+
return this.scanLiteral(start, 'false');
|
|
102
|
+
if (token === 'n')
|
|
103
|
+
return this.scanLiteral(start, 'null');
|
|
104
|
+
return this.scanNumber(start);
|
|
105
|
+
}
|
|
106
|
+
scanString() {
|
|
107
|
+
if (this.source[this.index] !== '"')
|
|
108
|
+
throw new SyntaxError('Expected JSON string');
|
|
109
|
+
this.index++;
|
|
110
|
+
while (this.index < this.source.length) {
|
|
111
|
+
const token = this.source[this.index];
|
|
112
|
+
this.index++;
|
|
113
|
+
if (token === '"')
|
|
114
|
+
return;
|
|
115
|
+
if (token === '\\')
|
|
116
|
+
this.index++;
|
|
117
|
+
}
|
|
118
|
+
throw new SyntaxError('Unterminated JSON string');
|
|
119
|
+
}
|
|
120
|
+
scanLiteral(start, literal) {
|
|
121
|
+
this.index += literal.length;
|
|
122
|
+
return { start, end: this.index, kind: 'atomic' };
|
|
123
|
+
}
|
|
124
|
+
scanNumber(start) {
|
|
125
|
+
while (this.index < this.source.length && /[0-9eE+.-]/.test(this.source[this.index]))
|
|
126
|
+
this.index++;
|
|
127
|
+
if (this.index === start)
|
|
128
|
+
throw new SyntaxError('Expected JSON value');
|
|
129
|
+
return { start, end: this.index, kind: 'atomic' };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function sourceNodeAtPath(root, path) {
|
|
133
|
+
let current = root;
|
|
134
|
+
for (let index = 0; index < path.length; index++) {
|
|
135
|
+
const segment = path[index];
|
|
136
|
+
if (current.kind === 'array' && typeof segment === 'number') {
|
|
137
|
+
const child = current.items?.[segment];
|
|
138
|
+
if (!child)
|
|
139
|
+
throw new RangeError(`JSON path ${describeValuePath(path)} does not exist`);
|
|
140
|
+
current = child;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (current.kind === 'object' && typeof segment === 'string') {
|
|
144
|
+
const child = current.propertyIndex?.get(segment);
|
|
145
|
+
if (!child)
|
|
146
|
+
throw new RangeError(`JSON path ${describeValuePath(path)} does not exist`);
|
|
147
|
+
current = child;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
throw new TypeError(`JSON path ${describeValuePath(path.slice(0, index))} is not the requested container type`);
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
/** Locates the exact source span represented by a parsed JSON value path. */
|
|
155
|
+
export function jsonSourceRangeAtPath(source, path) {
|
|
156
|
+
return jsonSourceRangesAtPaths(source, [path])[0];
|
|
157
|
+
}
|
|
158
|
+
/** Locates several value paths with one source parse, preserving path order. */
|
|
159
|
+
export function jsonSourceRangesAtPaths(source, paths) {
|
|
160
|
+
JSON.parse(source);
|
|
161
|
+
const root = new JsonSourceScanner(source).parse();
|
|
162
|
+
return paths.map((path) => {
|
|
163
|
+
const target = sourceNodeAtPath(root, path);
|
|
164
|
+
return { start: target.start, end: target.end };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Serializes only JSON data: finite numbers, strings, booleans, null, dense arrays,
|
|
168
|
+
* and plain data objects. Accessors, toJSON methods, cycles and lossy coercions are rejected.
|
|
169
|
+
*/
|
|
170
|
+
export function stringifyJsonValue(value, space = 0) {
|
|
171
|
+
const indent = ' '.repeat(Math.max(0, Math.min(10, Math.trunc(space))));
|
|
172
|
+
const ancestors = new Set();
|
|
173
|
+
const invalid = () => { throw new TypeError('Replacement must be valid JSON data without coercion'); };
|
|
174
|
+
const visit = (item, depth) => {
|
|
175
|
+
if (item === null)
|
|
176
|
+
return 'null';
|
|
177
|
+
if (typeof item === 'string' || typeof item === 'boolean')
|
|
178
|
+
return JSON.stringify(item);
|
|
179
|
+
if (typeof item === 'number') {
|
|
180
|
+
if (!Number.isFinite(item))
|
|
181
|
+
return invalid();
|
|
182
|
+
return Object.is(item, -0) ? '-0' : JSON.stringify(item);
|
|
183
|
+
}
|
|
184
|
+
if (typeof item !== 'object' || ancestors.has(item))
|
|
185
|
+
return invalid();
|
|
186
|
+
if (depth >= MAX_JSON_DEPTH)
|
|
187
|
+
throw new JsonDepthLimitError();
|
|
188
|
+
const array = Array.isArray(item);
|
|
189
|
+
const prototype = Object.getPrototypeOf(item);
|
|
190
|
+
if (!array && prototype !== Object.prototype && prototype !== null)
|
|
191
|
+
return invalid();
|
|
192
|
+
ancestors.add(item);
|
|
193
|
+
const keys = Reflect.ownKeys(item);
|
|
194
|
+
const fields = [];
|
|
195
|
+
if (array) {
|
|
196
|
+
if (keys.length !== item.length + 1)
|
|
197
|
+
return invalid();
|
|
198
|
+
for (let index = 0; index < item.length; index += 1) {
|
|
199
|
+
const property = Object.getOwnPropertyDescriptor(item, String(index));
|
|
200
|
+
if (!property || !('value' in property) || !property.enumerable)
|
|
201
|
+
return invalid();
|
|
202
|
+
fields.push(visit(property.value, depth + 1));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
for (const key of keys) {
|
|
207
|
+
const property = Object.getOwnPropertyDescriptor(item, key);
|
|
208
|
+
if (typeof key !== 'string' || !property || !('value' in property) || !property.enumerable)
|
|
209
|
+
return invalid();
|
|
210
|
+
fields.push(`${JSON.stringify(key)}:${indent ? ' ' : ''}${visit(property.value, depth + 1)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
ancestors.delete(item);
|
|
214
|
+
const open = array ? '[' : '{';
|
|
215
|
+
const close = array ? ']' : '}';
|
|
216
|
+
if (fields.length === 0)
|
|
217
|
+
return open + close;
|
|
218
|
+
return indent
|
|
219
|
+
? `${open}\n${indent.repeat(depth + 1)}${fields.join(`,\n${indent.repeat(depth + 1)}`)}\n${indent.repeat(depth)}${close}`
|
|
220
|
+
: `${open}${fields.join(',')}${close}`;
|
|
221
|
+
};
|
|
222
|
+
return visit(value, 0);
|
|
223
|
+
}
|
|
224
|
+
function serializeReplacement(next, path, space = 0) {
|
|
225
|
+
try {
|
|
226
|
+
return stringifyJsonValue(next, space);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new TypeError(`Replacement at ${describeValuePath(path)} must be valid JSON data`, { cause: error });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Reads a source-preserving inspection value and identifies information JSON.parse cannot retain. */
|
|
233
|
+
export function inspectJsonSource(source) {
|
|
234
|
+
const value = JSON.parse(source);
|
|
235
|
+
const tree = new JsonSourceScanner(source).parse();
|
|
236
|
+
const diagnostics = [];
|
|
237
|
+
const decimalKey = (token) => {
|
|
238
|
+
const [coefficient, exponent = '0'] = token.toLowerCase().split('e');
|
|
239
|
+
const negative = coefficient.startsWith('-');
|
|
240
|
+
const unsigned = negative ? coefficient.slice(1) : coefficient;
|
|
241
|
+
const [whole, fraction = ''] = unsigned.split('.');
|
|
242
|
+
const digits = (whole + fraction).replace(/^0+/, '');
|
|
243
|
+
if (!digits)
|
|
244
|
+
return negative ? '-0' : '0';
|
|
245
|
+
const significant = digits.replace(/0+$/, '');
|
|
246
|
+
return `${negative ? '-' : ''}${significant}e${Number(exponent) - fraction.length + digits.length - significant.length}`;
|
|
247
|
+
};
|
|
248
|
+
const pending = [{ node: tree, path: [], shadowed: false }];
|
|
249
|
+
while (pending.length) {
|
|
250
|
+
const { node, path, shadowed } = pending.pop();
|
|
251
|
+
if (node.kind === 'object') {
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
for (const property of node.properties ?? []) {
|
|
254
|
+
const childPath = [...path, property.key];
|
|
255
|
+
const childShadowed = shadowed || node.propertyIndex?.get(property.key) !== property.value;
|
|
256
|
+
if (seen.has(property.key))
|
|
257
|
+
diagnostics.push({
|
|
258
|
+
code: 'duplicate-key', sourcePath: childPath, token: JSON.stringify(property.key),
|
|
259
|
+
...(childShadowed ? { shadowed: true } : {}),
|
|
260
|
+
start: property.start, end: property.end,
|
|
261
|
+
message: 'Duplicate object key; structured inspection shows the last occurrence. Source retains every occurrence.',
|
|
262
|
+
});
|
|
263
|
+
seen.add(property.key);
|
|
264
|
+
pending.push({ node: property.value, path: childPath, shadowed: childShadowed });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else if (node.kind === 'array') {
|
|
268
|
+
node.items?.forEach((child, index) => pending.push({ node: child, path: [...path, index], shadowed }));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const token = source.slice(node.start, node.end);
|
|
272
|
+
if (!/^-?[0-9]/.test(token))
|
|
273
|
+
continue;
|
|
274
|
+
const number = Number(token);
|
|
275
|
+
if (!Number.isFinite(number) || Object.is(number, -0)
|
|
276
|
+
|| (Number.isInteger(number) && !Number.isSafeInteger(number))
|
|
277
|
+
|| decimalKey(token) !== decimalKey(String(number))) {
|
|
278
|
+
diagnostics.push({
|
|
279
|
+
code: 'unsafe-number', sourcePath: path, token, start: node.start, end: node.end,
|
|
280
|
+
...(shadowed ? { shadowed: true } : {}),
|
|
281
|
+
message: 'This numeric token cannot be safely edited through JavaScript numbers. Edit its exact value in Source.',
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
diagnostics.sort((a, b) => a.start - b.start);
|
|
287
|
+
return { value, diagnostics: diagnostics.map((item) => ({ ...item, help: sourceDiagnosticHelp(item.code) })) };
|
|
288
|
+
}
|
|
289
|
+
/** Replaces one existing JSON value while preserving every unrelated source byte. */
|
|
290
|
+
export function replaceJsonValueInSource(source, path, next) {
|
|
291
|
+
const parsed = JSON.parse(source);
|
|
292
|
+
requireValueAtPath(parsed, path);
|
|
293
|
+
const serialized = serializeReplacement(next, path);
|
|
294
|
+
const target = sourceNodeAtPath(new JsonSourceScanner(source).parse(), path);
|
|
295
|
+
return `${source.slice(0, target.start)}${serialized}${source.slice(target.end)}`;
|
|
296
|
+
}
|
|
297
|
+
/** Adds or replaces one property while preserving the surrounding JSON source. */
|
|
298
|
+
export function upsertJsonObjectPropertyInSource(source, objectPath, key, next) {
|
|
299
|
+
const parsed = JSON.parse(source);
|
|
300
|
+
const object = requireValueAtPath(parsed, objectPath);
|
|
301
|
+
if (object === null || typeof object !== 'object' || Array.isArray(object)) {
|
|
302
|
+
throw new TypeError(`JSON path ${describeValuePath(objectPath)} does not point to an object`);
|
|
303
|
+
}
|
|
304
|
+
const replacement = serializeReplacement(next, [...objectPath, key]);
|
|
305
|
+
const tree = new JsonSourceScanner(source).parse();
|
|
306
|
+
if (Object.prototype.hasOwnProperty.call(object, key)) {
|
|
307
|
+
const target = sourceNodeAtPath(tree, [...objectPath, key]);
|
|
308
|
+
return `${source.slice(0, target.start)}${replacement}${source.slice(target.end)}`;
|
|
309
|
+
}
|
|
310
|
+
const target = sourceNodeAtPath(tree, objectPath);
|
|
311
|
+
if (target.kind !== 'object') {
|
|
312
|
+
throw new TypeError(`JSON path ${describeValuePath(objectPath)} does not point to an object`);
|
|
313
|
+
}
|
|
314
|
+
const properties = target.properties ?? [];
|
|
315
|
+
const serialized = `${JSON.stringify(key)}:${replacement}`;
|
|
316
|
+
if (properties.length === 0) {
|
|
317
|
+
return `${source.slice(0, target.start + 1)}${serialized}${source.slice(target.end - 1)}`;
|
|
318
|
+
}
|
|
319
|
+
const first = properties[0];
|
|
320
|
+
const leadingWhitespace = source.slice(target.start + 1, first.start);
|
|
321
|
+
const lineBreak = leadingWhitespace.lastIndexOf('\n');
|
|
322
|
+
const separator = lineBreak >= 0
|
|
323
|
+
? `,\n${leadingWhitespace.slice(lineBreak + 1)}`
|
|
324
|
+
: ', ';
|
|
325
|
+
const insertionPoint = properties[properties.length - 1].end;
|
|
326
|
+
return `${source.slice(0, insertionPoint)}${separator}${serialized}${source.slice(insertionPoint)}`;
|
|
327
|
+
}
|
|
328
|
+
/** Appends one item while preserving every byte outside the target array. */
|
|
329
|
+
export function appendJsonArrayItemInSource(source, arrayPath, next) {
|
|
330
|
+
const parsed = JSON.parse(source);
|
|
331
|
+
const array = requireValueAtPath(parsed, arrayPath);
|
|
332
|
+
if (!Array.isArray(array)) {
|
|
333
|
+
throw new TypeError(`JSON path ${describeValuePath(arrayPath)} does not point to an array`);
|
|
334
|
+
}
|
|
335
|
+
const target = sourceNodeAtPath(new JsonSourceScanner(source).parse(), arrayPath);
|
|
336
|
+
if (target.kind !== 'array') {
|
|
337
|
+
throw new TypeError(`JSON path ${describeValuePath(arrayPath)} does not point to an array`);
|
|
338
|
+
}
|
|
339
|
+
const items = target.items ?? [];
|
|
340
|
+
if (items.length === 0) {
|
|
341
|
+
const innerWhitespace = source.slice(target.start + 1, target.end - 1);
|
|
342
|
+
if (!innerWhitespace.includes('\n')) {
|
|
343
|
+
const serialized = serializeReplacement(next, [...arrayPath, array.length]);
|
|
344
|
+
return `${source.slice(0, target.start + 1)}${serialized}${source.slice(target.end - 1)}`;
|
|
345
|
+
}
|
|
346
|
+
const closingIndent = innerWhitespace.slice(innerWhitespace.lastIndexOf('\n') + 1);
|
|
347
|
+
const itemIndent = `${closingIndent} `;
|
|
348
|
+
const serialized = serializeReplacement(next, [...arrayPath, array.length], 2).replace(/\n/g, `\n${itemIndent}`);
|
|
349
|
+
return `${source.slice(0, target.start + 1)}\n${itemIndent}${serialized}\n${closingIndent}${source.slice(target.end - 1)}`;
|
|
350
|
+
}
|
|
351
|
+
const first = items[0];
|
|
352
|
+
const leadingWhitespace = source.slice(target.start + 1, first.start);
|
|
353
|
+
const lineBreak = leadingWhitespace.lastIndexOf('\n');
|
|
354
|
+
const itemIndent = lineBreak >= 0 ? leadingWhitespace.slice(lineBreak + 1) : '';
|
|
355
|
+
const separator = lineBreak >= 0 ? `,\n${itemIndent}` : ', ';
|
|
356
|
+
const serialized = lineBreak >= 0
|
|
357
|
+
? serializeReplacement(next, [...arrayPath, array.length], 2).replace(/\n/g, `\n${itemIndent}`)
|
|
358
|
+
: serializeReplacement(next, [...arrayPath, array.length]);
|
|
359
|
+
const insertionPoint = items[items.length - 1].end;
|
|
360
|
+
return `${source.slice(0, insertionPoint)}${separator}${serialized}${source.slice(insertionPoint)}`;
|
|
361
|
+
}
|
|
362
|
+
/** Adds/replaces properties against one source scan, preserving unrelated tokens. */
|
|
363
|
+
export function upsertJsonObjectPropertiesInSource(source, changes) {
|
|
364
|
+
if (changes.length === 0)
|
|
365
|
+
return source;
|
|
366
|
+
const parsed = JSON.parse(source);
|
|
367
|
+
const tree = new JsonSourceScanner(source).parse();
|
|
368
|
+
const groups = new Map();
|
|
369
|
+
for (const change of changes) {
|
|
370
|
+
const object = requireValueAtPath(parsed, change.objectPath);
|
|
371
|
+
if (object === null || typeof object !== 'object' || Array.isArray(object))
|
|
372
|
+
throw new TypeError('Property writes require an object');
|
|
373
|
+
const parent = sourceNodeAtPath(tree, change.objectPath);
|
|
374
|
+
const properties = groups.get(parent) ?? new Map();
|
|
375
|
+
if (properties.has(change.key))
|
|
376
|
+
throw new TypeError('JSON property writes overlap');
|
|
377
|
+
properties.set(change.key, serializeReplacement(change.value, [...change.objectPath, change.key]));
|
|
378
|
+
groups.set(parent, properties);
|
|
379
|
+
}
|
|
380
|
+
const edits = [];
|
|
381
|
+
for (const [parent, changes] of groups) {
|
|
382
|
+
const added = [];
|
|
383
|
+
for (const [key, text] of changes) {
|
|
384
|
+
const existing = parent.propertyIndex.get(key);
|
|
385
|
+
if (existing)
|
|
386
|
+
edits.push({ start: existing.start, end: existing.end, text });
|
|
387
|
+
else
|
|
388
|
+
added.push(`${JSON.stringify(key)}:${text}`);
|
|
389
|
+
}
|
|
390
|
+
if (added.length === 0)
|
|
391
|
+
continue;
|
|
392
|
+
const properties = parent.properties;
|
|
393
|
+
const first = properties[0];
|
|
394
|
+
const whitespace = first ? source.slice(parent.start + 1, first.start) : '';
|
|
395
|
+
const lineBreak = whitespace.lastIndexOf('\n');
|
|
396
|
+
const separator = lineBreak >= 0 ? `,\n${whitespace.slice(lineBreak + 1)}` : ', ';
|
|
397
|
+
const start = properties.at(-1)?.end ?? parent.start + 1;
|
|
398
|
+
edits.push({ start, end: start, text: (first ? separator : '') + added.join(separator) });
|
|
399
|
+
}
|
|
400
|
+
edits.sort((a, b) => a.start - b.start || b.end - a.end);
|
|
401
|
+
let cursor = 0;
|
|
402
|
+
const parts = [];
|
|
403
|
+
for (const edit of edits) {
|
|
404
|
+
if (edit.start < cursor)
|
|
405
|
+
throw new TypeError('JSON property writes overlap');
|
|
406
|
+
parts.push(source.slice(cursor, edit.start), edit.text);
|
|
407
|
+
cursor = edit.end;
|
|
408
|
+
}
|
|
409
|
+
parts.push(source.slice(cursor));
|
|
410
|
+
return parts.join('');
|
|
411
|
+
}
|
|
412
|
+
/** Replaces independent existing values as one source-preserving document update. */
|
|
413
|
+
export function replaceJsonValuesInSource(source, replacements) {
|
|
414
|
+
if (replacements.length === 0)
|
|
415
|
+
return source;
|
|
416
|
+
const parsed = JSON.parse(source);
|
|
417
|
+
const tree = new JsonSourceScanner(source).parse();
|
|
418
|
+
const edits = replacements.map((replacement) => {
|
|
419
|
+
requireValueAtPath(parsed, replacement.path);
|
|
420
|
+
const target = sourceNodeAtPath(tree, replacement.path);
|
|
421
|
+
return { ...target, path: replacement.path, serialized: serializeReplacement(replacement.value, replacement.path) };
|
|
422
|
+
}).sort((left, right) => left.start - right.start || right.end - left.end);
|
|
423
|
+
for (let index = 1; index < edits.length; index += 1) {
|
|
424
|
+
if (edits[index].start < edits[index - 1].end) {
|
|
425
|
+
throw new TypeError(`JSON path ${describeValuePath(edits[index].path)} overlaps ${describeValuePath(edits[index - 1].path)}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const parts = [];
|
|
429
|
+
let cursor = 0;
|
|
430
|
+
for (const edit of edits) {
|
|
431
|
+
parts.push(source.slice(cursor, edit.start), edit.serialized);
|
|
432
|
+
cursor = edit.end;
|
|
433
|
+
}
|
|
434
|
+
parts.push(source.slice(cursor));
|
|
435
|
+
return parts.join('');
|
|
436
|
+
}
|
|
437
|
+
/** Parses, validates, and replaces only the selected atomic token in the source. */
|
|
438
|
+
export function replaceJsonAtomicValueInSource(source, path, next) {
|
|
439
|
+
const root = JSON.parse(source);
|
|
440
|
+
assertSameEditableAtomicType(requireValueAtPath(root, path), next, path);
|
|
441
|
+
const target = sourceNodeAtPath(new JsonSourceScanner(source).parse(), path);
|
|
442
|
+
if (target.kind !== 'atomic') {
|
|
443
|
+
throw new TypeError(`JSON path ${describeValuePath(path)} does not point to an atomic value`);
|
|
444
|
+
}
|
|
445
|
+
return `${source.slice(0, target.start)}${serializeReplacement(next, path)}${source.slice(target.end)}`;
|
|
446
|
+
}
|
|
447
|
+
function removalRange(nodes, index) {
|
|
448
|
+
const target = nodes[index];
|
|
449
|
+
if (!target)
|
|
450
|
+
throw new RangeError(`JSON child ${index} does not exist`);
|
|
451
|
+
const next = nodes[index + 1];
|
|
452
|
+
if (next)
|
|
453
|
+
return { start: target.start, end: next.start };
|
|
454
|
+
const previous = nodes[index - 1];
|
|
455
|
+
if (previous)
|
|
456
|
+
return { start: previous.end, end: target.end };
|
|
457
|
+
return { start: target.start, end: target.end };
|
|
458
|
+
}
|
|
459
|
+
/** Removes one array item or object property while preserving unrelated JSON source bytes. */
|
|
460
|
+
export function removeJsonValueInSource(source, path) {
|
|
461
|
+
if (path.length === 0)
|
|
462
|
+
throw new TypeError('The JSON document root cannot be removed');
|
|
463
|
+
const parsed = JSON.parse(source);
|
|
464
|
+
requireValueAtPath(parsed, path);
|
|
465
|
+
const parentPath = path.slice(0, -1);
|
|
466
|
+
const segment = path[path.length - 1];
|
|
467
|
+
const parent = sourceNodeAtPath(new JsonSourceScanner(source).parse(), parentPath);
|
|
468
|
+
let range;
|
|
469
|
+
if (parent.kind === 'array' && typeof segment === 'number') {
|
|
470
|
+
range = removalRange(parent.items ?? [], segment);
|
|
471
|
+
}
|
|
472
|
+
else if (parent.kind === 'object' && typeof segment === 'string') {
|
|
473
|
+
const properties = parent.properties ?? [];
|
|
474
|
+
let propertyIndex = -1;
|
|
475
|
+
for (let index = 0; index < properties.length; index++) {
|
|
476
|
+
if (properties[index].key === segment)
|
|
477
|
+
propertyIndex = index;
|
|
478
|
+
}
|
|
479
|
+
if (propertyIndex < 0)
|
|
480
|
+
throw new RangeError(`JSON path ${describeValuePath(path)} does not exist`);
|
|
481
|
+
range = removalRange(properties, propertyIndex);
|
|
482
|
+
}
|
|
483
|
+
else {
|
|
484
|
+
throw new TypeError(`JSON path ${describeValuePath(parentPath)} is not the requested container type`);
|
|
485
|
+
}
|
|
486
|
+
return source.slice(0, range.start) + source.slice(range.end);
|
|
487
|
+
}
|
|
488
|
+
/** Removes locations in the original source, independent of caller order.
|
|
489
|
+
* Duplicate locations are idempotent; ancestor/descendant plans are rejected.
|
|
490
|
+
* Adjacent siblings share a removal span so commas remain valid.
|
|
491
|
+
*/
|
|
492
|
+
export function removeJsonValuesInSource(source, paths) {
|
|
493
|
+
if (paths.length === 0)
|
|
494
|
+
return source;
|
|
495
|
+
const parsed = JSON.parse(source);
|
|
496
|
+
const tree = new JsonSourceScanner(source).parse();
|
|
497
|
+
const groups = new Map();
|
|
498
|
+
const targets = [];
|
|
499
|
+
for (const path of paths) {
|
|
500
|
+
if (path.length === 0)
|
|
501
|
+
throw new TypeError('The JSON document root cannot be removed');
|
|
502
|
+
requireValueAtPath(parsed, path);
|
|
503
|
+
const parent = sourceNodeAtPath(tree, path.slice(0, -1));
|
|
504
|
+
const target = sourceNodeAtPath(tree, path);
|
|
505
|
+
const index = parent.kind === 'array'
|
|
506
|
+
? parent.items.indexOf(target)
|
|
507
|
+
: parent.properties.findIndex((property) => property.value === target);
|
|
508
|
+
const indices = groups.get(parent) ?? new Set();
|
|
509
|
+
if (!indices.has(index))
|
|
510
|
+
targets.push(target);
|
|
511
|
+
indices.add(index);
|
|
512
|
+
groups.set(parent, indices);
|
|
513
|
+
}
|
|
514
|
+
targets.sort((a, b) => a.start - b.start);
|
|
515
|
+
for (let index = 1; index < targets.length; index++) {
|
|
516
|
+
if (targets[index].start < targets[index - 1].end)
|
|
517
|
+
throw new TypeError('JSON removals overlap');
|
|
518
|
+
}
|
|
519
|
+
const ranges = [];
|
|
520
|
+
for (const [parent, selected] of groups) {
|
|
521
|
+
const siblings = parent.kind === 'array' ? parent.items : parent.properties;
|
|
522
|
+
const indices = [...selected].sort((a, b) => a - b);
|
|
523
|
+
for (let cursor = 0; cursor < indices.length; cursor++) {
|
|
524
|
+
const first = indices[cursor];
|
|
525
|
+
let last = first;
|
|
526
|
+
while (indices[cursor + 1] === last + 1)
|
|
527
|
+
last = indices[++cursor];
|
|
528
|
+
const next = siblings[last + 1];
|
|
529
|
+
ranges.push(next
|
|
530
|
+
? { start: siblings[first].start, end: next.start }
|
|
531
|
+
: { start: siblings[first - 1]?.end ?? siblings[first].start, end: siblings[last].end });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
535
|
+
const parts = [];
|
|
536
|
+
let cursor = 0;
|
|
537
|
+
for (const range of ranges) {
|
|
538
|
+
parts.push(source.slice(cursor, range.start));
|
|
539
|
+
cursor = range.end;
|
|
540
|
+
}
|
|
541
|
+
parts.push(source.slice(cursor));
|
|
542
|
+
return parts.join('');
|
|
543
|
+
}
|
|
544
|
+
export class JsonPatchError extends Error {
|
|
545
|
+
code;
|
|
546
|
+
path;
|
|
547
|
+
from;
|
|
548
|
+
operationIndex;
|
|
549
|
+
constructor(code, message, details = {}) {
|
|
550
|
+
super(message, details.cause === undefined ? undefined : { cause: details.cause });
|
|
551
|
+
this.name = 'JsonPatchError';
|
|
552
|
+
this.code = code;
|
|
553
|
+
this.path = details.path;
|
|
554
|
+
this.from = details.from;
|
|
555
|
+
this.operationIndex = details.operationIndex;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
function parseJsonPointer(pointer) {
|
|
559
|
+
if (pointer === '')
|
|
560
|
+
return [];
|
|
561
|
+
if (!pointer.startsWith('/')) {
|
|
562
|
+
throw new JsonPatchError('invalid-pointer', `JSON Pointer ${JSON.stringify(pointer)} must be empty or start with /`, { path: pointer });
|
|
563
|
+
}
|
|
564
|
+
return pointer.slice(1).split('/').map((token) => {
|
|
565
|
+
if (/~(?:[^01]|$)/.test(token)) {
|
|
566
|
+
throw new JsonPatchError('invalid-pointer', `JSON Pointer ${JSON.stringify(pointer)} contains an invalid ~ escape`, { path: pointer });
|
|
567
|
+
}
|
|
568
|
+
return token.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
function arrayIndex(token, length, pointer, allowEnd) {
|
|
572
|
+
if (!/^(0|[1-9][0-9]*)$/.test(token)) {
|
|
573
|
+
throw new JsonPatchError('invalid-array-index', `Array token ${JSON.stringify(token)} in ${JSON.stringify(pointer)} must be a non-negative integer without leading zeros`, { path: pointer });
|
|
574
|
+
}
|
|
575
|
+
const index = Number(token);
|
|
576
|
+
if (!Number.isSafeInteger(index) || index > length || (!allowEnd && index === length)) {
|
|
577
|
+
throw new JsonPatchError('missing-path', `JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
578
|
+
}
|
|
579
|
+
return index;
|
|
580
|
+
}
|
|
581
|
+
function resolvePointer(root, tokens, pointer) {
|
|
582
|
+
const path = [];
|
|
583
|
+
let current = root;
|
|
584
|
+
for (const token of tokens) {
|
|
585
|
+
if (Array.isArray(current)) {
|
|
586
|
+
if (token === '-')
|
|
587
|
+
throw new JsonPatchError('missing-path', `JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
588
|
+
const index = arrayIndex(token, current.length, pointer, false);
|
|
589
|
+
path.push(index);
|
|
590
|
+
current = current[index];
|
|
591
|
+
}
|
|
592
|
+
else if (current !== null && typeof current === 'object') {
|
|
593
|
+
if (!Object.prototype.hasOwnProperty.call(current, token)) {
|
|
594
|
+
throw new JsonPatchError('missing-path', `JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
595
|
+
}
|
|
596
|
+
path.push(token);
|
|
597
|
+
current = current[token];
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
throw new JsonPatchError('missing-path', `JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return { path, value: current };
|
|
604
|
+
}
|
|
605
|
+
function resolveAddPointer(root, tokens, pointer) {
|
|
606
|
+
if (tokens.length === 0)
|
|
607
|
+
return { path: [], parent: undefined };
|
|
608
|
+
const parentResult = resolvePointer(root, tokens.slice(0, -1), pointer);
|
|
609
|
+
const token = tokens[tokens.length - 1];
|
|
610
|
+
if (Array.isArray(parentResult.value)) {
|
|
611
|
+
const index = token === '-' ? parentResult.value.length : arrayIndex(token, parentResult.value.length, pointer, true);
|
|
612
|
+
return { path: [...parentResult.path, index], parent: parentResult.value };
|
|
613
|
+
}
|
|
614
|
+
if (parentResult.value !== null && typeof parentResult.value === 'object') {
|
|
615
|
+
return { path: [...parentResult.path, token], parent: parentResult.value };
|
|
616
|
+
}
|
|
617
|
+
throw new JsonPatchError('missing-path', `Parent of JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
618
|
+
}
|
|
619
|
+
function replaceJsonFragmentInSource(source, path, fragment) {
|
|
620
|
+
const target = sourceNodeAtPath(new JsonSourceScanner(source).parse(), path);
|
|
621
|
+
return source.slice(0, target.start) + fragment + source.slice(target.end);
|
|
622
|
+
}
|
|
623
|
+
function addJsonFragmentInSource(source, pointer, fragment) {
|
|
624
|
+
const root = JSON.parse(source);
|
|
625
|
+
const tokens = parseJsonPointer(pointer);
|
|
626
|
+
const target = resolveAddPointer(root, tokens, pointer);
|
|
627
|
+
if (target.path.length === 0)
|
|
628
|
+
return replaceJsonFragmentInSource(source, [], fragment);
|
|
629
|
+
const parentPath = target.path.slice(0, -1);
|
|
630
|
+
const segment = target.path[target.path.length - 1];
|
|
631
|
+
const parentNode = sourceNodeAtPath(new JsonSourceScanner(source).parse(), parentPath);
|
|
632
|
+
if (parentNode.kind === 'object' && typeof segment === 'string') {
|
|
633
|
+
if (Object.prototype.hasOwnProperty.call(target.parent, segment)) {
|
|
634
|
+
return replaceJsonFragmentInSource(source, target.path, fragment);
|
|
635
|
+
}
|
|
636
|
+
const properties = parentNode.properties ?? [];
|
|
637
|
+
const first = properties[0];
|
|
638
|
+
const whitespace = first ? source.slice(parentNode.start + 1, first.start) : source.slice(parentNode.start + 1, parentNode.end - 1);
|
|
639
|
+
const lineBreak = whitespace.lastIndexOf('\n');
|
|
640
|
+
const separator = lineBreak >= 0 ? `,\n${whitespace.slice(lineBreak + 1)}` : ', ';
|
|
641
|
+
const colon = first && source.slice(first.start, first.value.start).includes(': ') ? ': ' : ':';
|
|
642
|
+
const property = `${JSON.stringify(segment)}${colon}${fragment}`;
|
|
643
|
+
if (!first) {
|
|
644
|
+
return source.slice(0, parentNode.start + 1) + property + source.slice(parentNode.end - 1);
|
|
645
|
+
}
|
|
646
|
+
const insertionPoint = properties[properties.length - 1].end;
|
|
647
|
+
return source.slice(0, insertionPoint) + separator + property + source.slice(insertionPoint);
|
|
648
|
+
}
|
|
649
|
+
if (parentNode.kind === 'array' && typeof segment === 'number') {
|
|
650
|
+
const items = parentNode.items ?? [];
|
|
651
|
+
if (items.length === 0) {
|
|
652
|
+
const innerWhitespace = source.slice(parentNode.start + 1, parentNode.end - 1);
|
|
653
|
+
if (!innerWhitespace.includes('\n')) {
|
|
654
|
+
return source.slice(0, parentNode.start + 1) + fragment + source.slice(parentNode.end - 1);
|
|
655
|
+
}
|
|
656
|
+
const closingIndent = innerWhitespace.slice(innerWhitespace.lastIndexOf('\n') + 1);
|
|
657
|
+
const itemIndent = `${closingIndent} `;
|
|
658
|
+
return `${source.slice(0, parentNode.start + 1)}\n${itemIndent}${fragment}\n${closingIndent}${source.slice(parentNode.end - 1)}`;
|
|
659
|
+
}
|
|
660
|
+
const leadingWhitespace = source.slice(parentNode.start + 1, items[0].start);
|
|
661
|
+
const lineBreak = leadingWhitespace.lastIndexOf('\n');
|
|
662
|
+
const separator = lineBreak >= 0 ? `,\n${leadingWhitespace.slice(lineBreak + 1)}` : ', ';
|
|
663
|
+
if (segment === items.length) {
|
|
664
|
+
const insertionPoint = items[items.length - 1].end;
|
|
665
|
+
return source.slice(0, insertionPoint) + separator + fragment + source.slice(insertionPoint);
|
|
666
|
+
}
|
|
667
|
+
return source.slice(0, items[segment].start) + fragment + separator + source.slice(items[segment].start);
|
|
668
|
+
}
|
|
669
|
+
throw new JsonPatchError('missing-path', `Parent of JSON Pointer ${JSON.stringify(pointer)} does not exist`, { path: pointer });
|
|
670
|
+
}
|
|
671
|
+
function sourceFragmentAtPointer(source, pointer) {
|
|
672
|
+
const root = JSON.parse(source);
|
|
673
|
+
const resolved = resolvePointer(root, parseJsonPointer(pointer), pointer);
|
|
674
|
+
const node = sourceNodeAtPath(new JsonSourceScanner(source).parse(), resolved.path);
|
|
675
|
+
return { ...resolved, fragment: source.slice(node.start, node.end) };
|
|
676
|
+
}
|
|
677
|
+
function jsonValuesEqual(left, right) {
|
|
678
|
+
if (left === right)
|
|
679
|
+
return true;
|
|
680
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
681
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length
|
|
682
|
+
&& left.every((value, index) => jsonValuesEqual(value, right[index]));
|
|
683
|
+
}
|
|
684
|
+
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object')
|
|
685
|
+
return false;
|
|
686
|
+
const leftRecord = left;
|
|
687
|
+
const rightRecord = right;
|
|
688
|
+
const leftKeys = Object.keys(leftRecord);
|
|
689
|
+
const rightKeys = Object.keys(rightRecord);
|
|
690
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key) => Object.prototype.hasOwnProperty.call(rightRecord, key)
|
|
691
|
+
&& jsonValuesEqual(leftRecord[key], rightRecord[key]));
|
|
692
|
+
}
|
|
693
|
+
function requirePatchOperation(operation, index) {
|
|
694
|
+
if (operation === null || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
695
|
+
throw new JsonPatchError('invalid-operation', `Patch operation ${index} must be an object`, { operationIndex: index });
|
|
696
|
+
}
|
|
697
|
+
const item = operation;
|
|
698
|
+
if (typeof item.op !== 'string' || !['add', 'remove', 'replace', 'move', 'copy', 'test'].includes(item.op)) {
|
|
699
|
+
throw new JsonPatchError('invalid-operation', `Patch operation ${index} has an unsupported op`, { operationIndex: index });
|
|
700
|
+
}
|
|
701
|
+
if (typeof item.path !== 'string') {
|
|
702
|
+
throw new JsonPatchError('invalid-operation', `Patch operation ${index} requires a string path`, { operationIndex: index });
|
|
703
|
+
}
|
|
704
|
+
if ((item.op === 'add' || item.op === 'replace' || item.op === 'test') && !Object.prototype.hasOwnProperty.call(item, 'value')) {
|
|
705
|
+
throw new JsonPatchError('invalid-operation', `Patch operation ${index} with op ${item.op} requires value`, { operationIndex: index, path: item.path });
|
|
706
|
+
}
|
|
707
|
+
if ((item.op === 'move' || item.op === 'copy') && typeof item.from !== 'string') {
|
|
708
|
+
throw new JsonPatchError('invalid-operation', `Patch operation ${index} with op ${item.op} requires a string from`, { operationIndex: index, path: item.path });
|
|
709
|
+
}
|
|
710
|
+
return item;
|
|
711
|
+
}
|
|
712
|
+
/** Applies an RFC 6902 JSON Patch atomically while preserving every unrelated source byte. */
|
|
713
|
+
export function applyJsonPatchInSource(source, patch) {
|
|
714
|
+
if (!Array.isArray(patch))
|
|
715
|
+
throw new JsonPatchError('invalid-patch', 'JSON Patch must be an array');
|
|
716
|
+
try {
|
|
717
|
+
JSON.parse(source);
|
|
718
|
+
}
|
|
719
|
+
catch (cause) {
|
|
720
|
+
throw new JsonPatchError('invalid-document', 'The current document is not valid JSON', { cause });
|
|
721
|
+
}
|
|
722
|
+
let next = source;
|
|
723
|
+
for (let index = 0; index < patch.length; index += 1) {
|
|
724
|
+
const operation = requirePatchOperation(patch[index], index);
|
|
725
|
+
try {
|
|
726
|
+
const pathTokens = parseJsonPointer(operation.path);
|
|
727
|
+
if (operation.op === 'add') {
|
|
728
|
+
next = addJsonFragmentInSource(next, operation.path, serializeReplacement(operation.value, []));
|
|
729
|
+
}
|
|
730
|
+
else if (operation.op === 'remove') {
|
|
731
|
+
if (pathTokens.length === 0)
|
|
732
|
+
throw new JsonPatchError('root-removal-not-supported', 'A JSON document root cannot be removed', { path: operation.path });
|
|
733
|
+
const root = JSON.parse(next);
|
|
734
|
+
const target = resolvePointer(root, pathTokens, operation.path);
|
|
735
|
+
next = removeJsonValueInSource(next, target.path);
|
|
736
|
+
}
|
|
737
|
+
else if (operation.op === 'replace') {
|
|
738
|
+
const root = JSON.parse(next);
|
|
739
|
+
const target = resolvePointer(root, pathTokens, operation.path);
|
|
740
|
+
next = replaceJsonFragmentInSource(next, target.path, serializeReplacement(operation.value, target.path));
|
|
741
|
+
}
|
|
742
|
+
else if (operation.op === 'test') {
|
|
743
|
+
serializeReplacement(operation.value, []);
|
|
744
|
+
const root = JSON.parse(next);
|
|
745
|
+
const target = resolvePointer(root, pathTokens, operation.path);
|
|
746
|
+
if (!jsonValuesEqual(target.value, operation.value)) {
|
|
747
|
+
throw new JsonPatchError('test-failed', `Test operation failed at JSON Pointer ${JSON.stringify(operation.path)}`, { path: operation.path });
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
const fromTokens = parseJsonPointer(operation.from);
|
|
752
|
+
if (operation.op === 'move' && fromTokens.length < pathTokens.length
|
|
753
|
+
&& fromTokens.every((token, tokenIndex) => token === pathTokens[tokenIndex])) {
|
|
754
|
+
throw new JsonPatchError('move-into-descendant', 'A value cannot be moved into one of its descendants', { path: operation.path, from: operation.from });
|
|
755
|
+
}
|
|
756
|
+
const sourceValue = sourceFragmentAtPointer(next, operation.from);
|
|
757
|
+
if (operation.op === 'move') {
|
|
758
|
+
if (operation.from === operation.path)
|
|
759
|
+
continue;
|
|
760
|
+
if (sourceValue.path.length === 0)
|
|
761
|
+
throw new JsonPatchError('root-removal-not-supported', 'A JSON document root cannot be moved below itself', { path: operation.path, from: operation.from });
|
|
762
|
+
next = removeJsonValueInSource(next, sourceValue.path);
|
|
763
|
+
}
|
|
764
|
+
next = addJsonFragmentInSource(next, operation.path, sourceValue.fragment);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
if (error instanceof JsonPatchError) {
|
|
769
|
+
error.operationIndex ??= index;
|
|
770
|
+
throw error;
|
|
771
|
+
}
|
|
772
|
+
throw new JsonPatchError('invalid-value', error instanceof Error ? error.message : 'The patch value is not valid JSON', {
|
|
773
|
+
operationIndex: index, path: operation.path, cause: error,
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return next;
|
|
778
|
+
}
|