mdld-parse 0.6.2 → 0.7.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/README.md +119 -473
- package/package.json +1 -4
- package/src/generate.js +5 -89
- package/src/index.js +1 -1
- package/src/locate.js +21 -58
- package/src/merge.js +131 -0
- package/src/parse.js +134 -24
- package/src/utils.js +37 -120
- package/src/applyDiff.js +0 -583
package/src/applyDiff.js
DELETED
|
@@ -1,583 +0,0 @@
|
|
|
1
|
-
import { parse } from './parse.js';
|
|
2
|
-
import {
|
|
3
|
-
shortenIRI,
|
|
4
|
-
normalizeQuad,
|
|
5
|
-
quadToKeyForOrigin,
|
|
6
|
-
parseQuadIndexKey,
|
|
7
|
-
findVacantSlot,
|
|
8
|
-
occupySlot,
|
|
9
|
-
markSlotAsVacant,
|
|
10
|
-
normalizeAttrsTokens,
|
|
11
|
-
writeAttrsTokens,
|
|
12
|
-
removeOneToken,
|
|
13
|
-
addObjectToken,
|
|
14
|
-
removeObjectToken,
|
|
15
|
-
addSoftFragmentToken,
|
|
16
|
-
removeSoftFragmentToken,
|
|
17
|
-
objectSignature,
|
|
18
|
-
expandIRI,
|
|
19
|
-
DataFactory
|
|
20
|
-
} from './utils.js';
|
|
21
|
-
|
|
22
|
-
function getBlockById(base, blockId) {
|
|
23
|
-
return blockId ? base?.quadMap?.get(blockId) : null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function getEntryByQuadKey(base, quadKey) {
|
|
27
|
-
return quadKey ? base?.quadMap?.get(quadKey) : null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Helper functions for cleaner term type checking
|
|
31
|
-
function isLiteral(term) {
|
|
32
|
-
return term?.termType === 'Literal';
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isNamedNode(term) {
|
|
36
|
-
return term?.termType === 'NamedNode';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isRdfType(term) {
|
|
40
|
-
return term?.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function createAnnotationForQuad(quad, ctx) {
|
|
44
|
-
const predShort = shortenIRI(quad.predicate.value, ctx);
|
|
45
|
-
if (isLiteral(quad.object)) {
|
|
46
|
-
const value = String(quad.object.value ?? '');
|
|
47
|
-
const ann = createLiteralAnnotation(value, predShort, quad.object.language, quad.object.datatype, ctx);
|
|
48
|
-
return { text: `[${value}] {${ann}}`, isLiteral: true };
|
|
49
|
-
} else if (isNamedNode(quad.object)) {
|
|
50
|
-
const objectShort = shortenIRI(quad.object.value, ctx);
|
|
51
|
-
const objectAnn = createObjectAnnotation(objectShort, predShort);
|
|
52
|
-
return { text: objectAnn, isLiteral: false };
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function createSubjectBlockForQuad(quad, ctx) {
|
|
58
|
-
const subjectShort = shortenIRI(quad.subject.value, ctx);
|
|
59
|
-
const predShort = shortenIRI(quad.predicate.value, ctx);
|
|
60
|
-
const subjectName = extractLocalName(quad.subject.value);
|
|
61
|
-
|
|
62
|
-
if (isNamedNode(quad.object)) {
|
|
63
|
-
// IRI object: create object reference
|
|
64
|
-
const objectShort = shortenIRI(quad.object.value, ctx);
|
|
65
|
-
return { text: `\n\n# ${subjectName.charAt(0).toUpperCase() + subjectName.slice(1)} {=${subjectShort}}\n[${objectShort}] {${predShort}}\n`, isNewSubject: true };
|
|
66
|
-
} else {
|
|
67
|
-
// Literal object: create property on separate line
|
|
68
|
-
const value = String(quad.object.value ?? '');
|
|
69
|
-
const annotation = createLiteralAnnotation(value, predShort, quad.object.language, quad.object.datatype, ctx);
|
|
70
|
-
return { text: `\n\n# ${subjectName.charAt(0).toUpperCase() + subjectName.slice(1)} {=${subjectShort}}\n[${value}] {${annotation}}\n`, isNewSubject: true };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function extractLocalName(iri) {
|
|
75
|
-
return iri.split('/').pop() || iri.split('#').pop() || iri;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isValidQuad(quad) {
|
|
79
|
-
return quad && quad.subject && quad.predicate && quad.object;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeDiffQuads(quads, ctx) {
|
|
83
|
-
// Use DataFactory.fromQuad for proper RDF/JS compatibility
|
|
84
|
-
// But first expand any CURIEs in the quads to ensure proper matching
|
|
85
|
-
return quads.map(quad => {
|
|
86
|
-
// Expand CURIEs to full IRIs before normalization
|
|
87
|
-
const expandedQuad = {
|
|
88
|
-
subject: quad.subject.termType === 'NamedNode'
|
|
89
|
-
? { ...quad.subject, value: expandIRI(quad.subject.value, ctx) }
|
|
90
|
-
: quad.subject,
|
|
91
|
-
predicate: quad.predicate.termType === 'NamedNode'
|
|
92
|
-
? { ...quad.predicate, value: expandIRI(quad.predicate.value, ctx) }
|
|
93
|
-
: quad.predicate,
|
|
94
|
-
object: quad.object,
|
|
95
|
-
graph: quad.graph
|
|
96
|
-
};
|
|
97
|
-
return DataFactory.fromQuad(expandedQuad);
|
|
98
|
-
}).filter(isValidQuad);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function createLiteralAnnotation(value, predicate, language, datatype, ctx) {
|
|
102
|
-
let ann = predicate;
|
|
103
|
-
if (language) ann += ` @${language}`;
|
|
104
|
-
else if (datatype?.value && datatype.value !== DataFactory.literal('').datatype.value) {
|
|
105
|
-
ann += ` ^^${shortenIRI(datatype.value, ctx)}`;
|
|
106
|
-
}
|
|
107
|
-
return ann;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function createObjectAnnotation(objectShort, predicateShort, isSoftFragment = false, fragment = null) {
|
|
111
|
-
if (isSoftFragment) {
|
|
112
|
-
return `[${objectShort}] {+#${fragment} ?${predicateShort}}`;
|
|
113
|
-
}
|
|
114
|
-
return `[${objectShort}] {+${objectShort} ?${predicateShort}}`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function readSpan(block, text, spanType = 'attrs') {
|
|
118
|
-
const range = spanType === 'attrs' ? block?.attrsRange : block?.valueRange;
|
|
119
|
-
if (!range) return null;
|
|
120
|
-
const { start, end } = range;
|
|
121
|
-
return (Number.isFinite(start) && Number.isFinite(end) && start >= 0 && end >= start)
|
|
122
|
-
? { start, end, text: text.substring(start, end) }
|
|
123
|
-
: null;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function sanitizeCarrierValueForBlock(block, raw) {
|
|
127
|
-
const s = String(raw ?? '');
|
|
128
|
-
const t = block?.carrierType;
|
|
129
|
-
if (t === 'code') return s.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
130
|
-
const oneLine = s.replace(/[\n\r]+/g, ' ').trim();
|
|
131
|
-
return (t === 'span' || t === 'link') ? oneLine.replace(/[\[\]]/g, ' ') : oneLine;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function blockTokensFromEntries(block) {
|
|
135
|
-
return block?.entries?.length ? block.entries.map(e => e.raw).filter(Boolean) : null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function removeEntryAt(block, entryIndex) {
|
|
139
|
-
if (!block?.entries || entryIndex == null || entryIndex < 0 || entryIndex >= block.entries.length) return null;
|
|
140
|
-
return [...block.entries.slice(0, entryIndex), ...block.entries.slice(entryIndex + 1)];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function replaceLangDatatypeEntries(block, lit, ctx) {
|
|
144
|
-
if (!block?.entries) return null;
|
|
145
|
-
const filtered = block.entries.filter(e => e.kind !== 'language' && e.kind !== 'datatype');
|
|
146
|
-
const extras = [];
|
|
147
|
-
if (lit?.language) extras.push({ kind: 'language', language: lit.language, raw: `@${lit.language}`, relRange: { start: 0, end: 0 } });
|
|
148
|
-
const dt = lit?.datatype?.value;
|
|
149
|
-
if (!lit?.language && dt && dt !== 'http://www.w3.org/2001/XMLSchema#string') {
|
|
150
|
-
extras.push({ kind: 'datatype', datatype: shortenIRI(dt, ctx), raw: `^^${shortenIRI(dt, ctx)}`, relRange: { start: 0, end: 0 } });
|
|
151
|
-
}
|
|
152
|
-
return [...filtered, ...extras];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function updateAttrsDatatypeLang(tokens, newLit, ctx) {
|
|
156
|
-
const predicatesAndTypes = tokens.filter(t => !t.startsWith('@') && !t.startsWith('^^'));
|
|
157
|
-
if (newLit?.language) return [...predicatesAndTypes, `@${newLit.language}`];
|
|
158
|
-
const dt = newLit?.datatype?.value;
|
|
159
|
-
if (dt && dt !== 'http://www.w3.org/2001/XMLSchema#string') {
|
|
160
|
-
return [...predicatesAndTypes, `^^${shortenIRI(dt, ctx)}`];
|
|
161
|
-
}
|
|
162
|
-
return predicatesAndTypes;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Direct slot operations - no class abstraction needed
|
|
166
|
-
function removeTokenFromSlot(entry, tokens, ctx, quad) {
|
|
167
|
-
if (!entry) return { tokens, removed: false };
|
|
168
|
-
|
|
169
|
-
if (entry.kind === 'object') {
|
|
170
|
-
const objectIRI = shortenIRI(quad.object.value, ctx);
|
|
171
|
-
return removeObjectToken(tokens, objectIRI);
|
|
172
|
-
} else if (entry.kind === 'softFragment') {
|
|
173
|
-
const fragment = entry.fragment;
|
|
174
|
-
return removeSoftFragmentToken(tokens, fragment);
|
|
175
|
-
} else if (entry.kind === 'type' && quad.predicate.value.endsWith('rdf-syntax-ns#type')) {
|
|
176
|
-
const expectedType = entry.expandedType || quad.object.value;
|
|
177
|
-
return removeOneToken(tokens, t => {
|
|
178
|
-
if (!t.startsWith('.')) return false;
|
|
179
|
-
const raw = t.slice(1);
|
|
180
|
-
return expandIRI(raw, ctx) === expectedType;
|
|
181
|
-
});
|
|
182
|
-
} else {
|
|
183
|
-
const expectedPred = entry.expandedPredicate || quad.predicate.value;
|
|
184
|
-
const expectedForm = entry.form;
|
|
185
|
-
return removeOneToken(tokens, t => {
|
|
186
|
-
const m = String(t).match(/^(\^\?|\^|\?|)(.+)$/);
|
|
187
|
-
if (!m) return false;
|
|
188
|
-
const form = m[1] || '';
|
|
189
|
-
const raw = m[2];
|
|
190
|
-
if (expectedForm != null && form !== expectedForm) return false;
|
|
191
|
-
return expandIRI(raw, ctx) === expectedPred;
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function addTokenToSlot(tokens, ctx, quad) {
|
|
197
|
-
// Use cleaner helper functions
|
|
198
|
-
if (isRdfType(quad.predicate) && isNamedNode(quad.object)) {
|
|
199
|
-
const typeShort = shortenIRI(quad.object.value, ctx);
|
|
200
|
-
const typeToken = typeShort.includes(':') || !typeShort.startsWith('http') ? `.${typeShort}` : null;
|
|
201
|
-
if (typeToken && !tokens.includes(typeToken)) {
|
|
202
|
-
return [...tokens, typeToken];
|
|
203
|
-
}
|
|
204
|
-
} else if (isNamedNode(quad.object)) {
|
|
205
|
-
const objectShort = shortenIRI(quad.object.value, ctx);
|
|
206
|
-
const isSoftFragment = quad.object.value.includes('#');
|
|
207
|
-
const fragment = isSoftFragment ? quad.object.value.split('#')[1] : null;
|
|
208
|
-
|
|
209
|
-
if (fragment) {
|
|
210
|
-
return addSoftFragmentToken(tokens, objectShort, fragment);
|
|
211
|
-
} else {
|
|
212
|
-
return addObjectToken(tokens, objectShort);
|
|
213
|
-
}
|
|
214
|
-
} else if (isLiteral(quad.object)) {
|
|
215
|
-
const predShort = shortenIRI(quad.predicate.value, ctx);
|
|
216
|
-
if (!tokens.includes(predShort)) {
|
|
217
|
-
return [...tokens, predShort];
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return tokens;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function markEntryAsVacant(entry, quad) {
|
|
224
|
-
if (entry && entry.slotId) {
|
|
225
|
-
return markSlotAsVacant(entry, quad.object);
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export function applyDiff({ text, diff, origin, options = {} }) {
|
|
231
|
-
if (!diff || (!diff.add?.length && !diff.delete?.length)) {
|
|
232
|
-
const reparsed = parse(text, { context: options.context || {} });
|
|
233
|
-
return { text, origin: reparsed.origin };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const base = origin || parse(text, { context: options.context || {} }).origin;
|
|
237
|
-
const ctx = options.context || {};
|
|
238
|
-
|
|
239
|
-
// Phase 1: Plan operations (pure, no text edits)
|
|
240
|
-
const plan = planOperations(diff, base, ctx);
|
|
241
|
-
|
|
242
|
-
// Phase 2: Materialize edits (ranges + strings)
|
|
243
|
-
const edits = materializeEdits(plan, text, ctx, base);
|
|
244
|
-
|
|
245
|
-
// Phase 3: Apply edits + reparse
|
|
246
|
-
return applyEdits(text, edits, ctx, base);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
function planOperations(diff, base, ctx) {
|
|
251
|
-
// Normalize quads using DataFactory for proper RDF/JS compatibility
|
|
252
|
-
const normAdds = normalizeDiffQuads(diff.add || [], ctx);
|
|
253
|
-
const normDeletes = normalizeDiffQuads(diff.delete || [], ctx);
|
|
254
|
-
|
|
255
|
-
const plan = {
|
|
256
|
-
literalUpdates: [],
|
|
257
|
-
vacantSlotOccupations: [],
|
|
258
|
-
deletes: [],
|
|
259
|
-
adds: [],
|
|
260
|
-
consumedAdds: new Set()
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// Build lookup maps
|
|
264
|
-
const addBySP = new Map();
|
|
265
|
-
for (const quad of normAdds) {
|
|
266
|
-
const k = JSON.stringify([quad.subject.value, quad.predicate.value]);
|
|
267
|
-
const list = addBySP.get(k) || [];
|
|
268
|
-
list.push(quad);
|
|
269
|
-
addBySP.set(k, list);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Build anchors for delete operations
|
|
273
|
-
const anchors = new Map();
|
|
274
|
-
for (const quad of normDeletes) {
|
|
275
|
-
const key = JSON.stringify([quad.subject.value, objectSignature(quad.object)]);
|
|
276
|
-
const quadKey = quadToKeyForOrigin(quad);
|
|
277
|
-
const entry = getEntryByQuadKey(base, quadKey);
|
|
278
|
-
const block = entry; // In unified structure, entry is the block
|
|
279
|
-
if (block?.attrsRange) {
|
|
280
|
-
anchors.set(key, { block, entry });
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Detect literal updates early
|
|
285
|
-
for (const deleteQuad of normDeletes) {
|
|
286
|
-
if (!isLiteral(deleteQuad.object)) continue;
|
|
287
|
-
|
|
288
|
-
const k = JSON.stringify([deleteQuad.subject.value, deleteQuad.predicate.value]);
|
|
289
|
-
const candidates = addBySP.get(k) || [];
|
|
290
|
-
const addQuad = candidates.find(x =>
|
|
291
|
-
isLiteral(x?.object) && !plan.consumedAdds.has(quadToKeyForOrigin(x))
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
if (!addQuad) continue;
|
|
295
|
-
|
|
296
|
-
const entry = resolveOriginEntry(deleteQuad, base);
|
|
297
|
-
const block = entry; // In unified structure, the entry is the block
|
|
298
|
-
|
|
299
|
-
if (block) {
|
|
300
|
-
plan.literalUpdates.push({ deleteQuad, addQuad, entry, block });
|
|
301
|
-
plan.consumedAdds.add(quadToKeyForOrigin(addQuad));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Find vacant slot occupations
|
|
306
|
-
for (const quad of normAdds) {
|
|
307
|
-
if (!isLiteral(quad.object)) continue;
|
|
308
|
-
if (plan.consumedAdds.has(quadToKeyForOrigin(quad))) continue;
|
|
309
|
-
|
|
310
|
-
const vacantSlot = findVacantSlot(base?.quadMap, quad.subject, quad.predicate);
|
|
311
|
-
if (!vacantSlot) continue;
|
|
312
|
-
|
|
313
|
-
const block = vacantSlot; // In unified structure, the slot is the block
|
|
314
|
-
if (block) {
|
|
315
|
-
plan.vacantSlotOccupations.push({ quad, vacantSlot, block });
|
|
316
|
-
plan.consumedAdds.add(quadToKeyForOrigin(quad));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Plan remaining deletes
|
|
321
|
-
for (const quad of normDeletes) {
|
|
322
|
-
if (isLiteral(quad.object)) {
|
|
323
|
-
const isUpdated = plan.literalUpdates.some(u =>
|
|
324
|
-
u.deleteQuad.subject.value === quad.subject.value &&
|
|
325
|
-
u.deleteQuad.predicate.value === quad.predicate.value &&
|
|
326
|
-
u.deleteQuad.object.value === quad.object.value
|
|
327
|
-
);
|
|
328
|
-
if (isUpdated) continue;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const entry = resolveOriginEntry(quad, base);
|
|
332
|
-
const block = entry; // In unified structure, entry is the block
|
|
333
|
-
if (block) {
|
|
334
|
-
plan.deletes.push({ quad, entry, block });
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Plan remaining adds
|
|
339
|
-
for (const quad of normAdds) {
|
|
340
|
-
if (plan.consumedAdds.has(quadToKeyForOrigin(quad))) continue;
|
|
341
|
-
|
|
342
|
-
const targetBlock = findTargetBlock(quad, base, anchors);
|
|
343
|
-
plan.adds.push({ quad, targetBlock });
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return plan;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
function materializeEdits(plan, text, ctx, base) {
|
|
350
|
-
const edits = [];
|
|
351
|
-
|
|
352
|
-
// Materialize vacant slot occupations
|
|
353
|
-
for (const { quad, vacantSlot, block } of plan.vacantSlotOccupations) {
|
|
354
|
-
const span = readSpan(block, text, 'attrs');
|
|
355
|
-
if (!span) continue;
|
|
356
|
-
|
|
357
|
-
// Update carrier value
|
|
358
|
-
const valueSpan = readSpan(block, text, 'value');
|
|
359
|
-
if (valueSpan) {
|
|
360
|
-
edits.push({ start: valueSpan.start, end: valueSpan.end, text: quad.object.value });
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Update annotation block
|
|
364
|
-
const tokens = normalizeAttrsTokens(span.text);
|
|
365
|
-
const predToken = `${vacantSlot.form || ''}${shortenIRI(quad.predicate.value, ctx)}`;
|
|
366
|
-
|
|
367
|
-
if (tokens.length === 0) {
|
|
368
|
-
edits.push({ start: span.start, end: span.end, text: `{${predToken}}` });
|
|
369
|
-
} else if (!tokens.includes(predToken)) {
|
|
370
|
-
const updated = [...tokens, predToken];
|
|
371
|
-
edits.push({ start: span.start, end: span.end, text: writeAttrsTokens(updated) });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Materialize literal updates
|
|
376
|
-
for (const { deleteQuad, addQuad, entry, block } of plan.literalUpdates) {
|
|
377
|
-
const span = readSpan(block, text, 'value');
|
|
378
|
-
if (span) {
|
|
379
|
-
const newValue = sanitizeCarrierValueForBlock(block, addQuad.object.value);
|
|
380
|
-
edits.push({ start: span.start, end: span.end, text: newValue });
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const aSpan = readSpan(block, text, 'attrs');
|
|
384
|
-
if (aSpan) {
|
|
385
|
-
if (block?.entries?.length) {
|
|
386
|
-
const nextEntries = replaceLangDatatypeEntries(block, addQuad.object, ctx);
|
|
387
|
-
if (nextEntries) {
|
|
388
|
-
const nextTokens = nextEntries.map(e => e.raw).filter(Boolean);
|
|
389
|
-
const newText = nextTokens.length === 0 ? '{}' : writeAttrsTokens(nextTokens);
|
|
390
|
-
edits.push({ start: aSpan.start, end: aSpan.end, text: newText });
|
|
391
|
-
}
|
|
392
|
-
} else {
|
|
393
|
-
const tokens = normalizeAttrsTokens(aSpan.text);
|
|
394
|
-
const updated = updateAttrsDatatypeLang(tokens, addQuad.object, ctx);
|
|
395
|
-
if (updated.join(' ') !== tokens.join(' ')) {
|
|
396
|
-
const newText = updated.length === 0 ? '{}' : writeAttrsTokens(updated);
|
|
397
|
-
edits.push({ start: aSpan.start, end: aSpan.end, text: newText });
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Materialize deletes
|
|
404
|
-
for (const { quad, entry, block } of plan.deletes) {
|
|
405
|
-
// Mark slot as vacant
|
|
406
|
-
const vacantSlot = markEntryAsVacant(entry, quad);
|
|
407
|
-
if (vacantSlot && block) {
|
|
408
|
-
const blockInfo = {
|
|
409
|
-
id: entry.blockId,
|
|
410
|
-
range: block.range,
|
|
411
|
-
attrsRange: block.attrsRange,
|
|
412
|
-
valueRange: block.valueRange,
|
|
413
|
-
carrierType: block.carrierType,
|
|
414
|
-
subject: block.subject,
|
|
415
|
-
context: block.context
|
|
416
|
-
};
|
|
417
|
-
vacantSlot.blockInfo = blockInfo;
|
|
418
|
-
const key = quadToKeyForOrigin(quad);
|
|
419
|
-
if (key) base.quadMap.set(key, vacantSlot);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const span = readSpan(block, text, 'attrs');
|
|
423
|
-
if (!span) continue;
|
|
424
|
-
|
|
425
|
-
// Handle entry removal by index
|
|
426
|
-
if (entry?.entryIndex != null && block?.entries?.length) {
|
|
427
|
-
const nextEntries = removeEntryAt(block, entry.entryIndex);
|
|
428
|
-
if (nextEntries) {
|
|
429
|
-
const nextTokens = nextEntries.map(e => e.raw).filter(Boolean);
|
|
430
|
-
const newText = nextTokens.length === 0 ? '{}' : writeAttrsTokens(nextTokens);
|
|
431
|
-
edits.push({ start: span.start, end: span.end, text: newText });
|
|
432
|
-
continue;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Handle token-based removals using direct functions
|
|
437
|
-
const tokens = normalizeAttrsTokens(span.text);
|
|
438
|
-
const { tokens: updated, removed } = removeTokenFromSlot(entry, tokens, ctx, quad);
|
|
439
|
-
|
|
440
|
-
if (removed) {
|
|
441
|
-
const newText = updated.length === 0 ? '{}' : writeAttrsTokens(updated);
|
|
442
|
-
edits.push({ start: span.start, end: span.end, text: newText });
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Materialize adds
|
|
447
|
-
for (const { quad, targetBlock } of plan.adds) {
|
|
448
|
-
const quadKey = quadToKeyForOrigin(quad);
|
|
449
|
-
if (plan.consumedAdds.has(quadKey)) {
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
if (isLiteral(quad.object) || isNamedNode(quad.object)) {
|
|
454
|
-
if (!targetBlock) {
|
|
455
|
-
// No target block - check if subject already exists in document
|
|
456
|
-
const subjectExists = Array.from(base?.quadMap?.values() || [])
|
|
457
|
-
.some(block => block.subject?.value === quad.subject.value);
|
|
458
|
-
|
|
459
|
-
let annotation;
|
|
460
|
-
if (!subjectExists && isNamedNode(quad.object)) {
|
|
461
|
-
// New subject with IRI object - create subject block
|
|
462
|
-
annotation = createSubjectBlockForQuad(quad, ctx);
|
|
463
|
-
} else if (subjectExists) {
|
|
464
|
-
// Existing subject - create simple annotation
|
|
465
|
-
annotation = createAnnotationForQuad(quad, ctx);
|
|
466
|
-
} else {
|
|
467
|
-
// New subject with literal - create subject block
|
|
468
|
-
annotation = createSubjectBlockForQuad(quad, ctx);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (annotation) {
|
|
472
|
-
edits.push({ start: text.length, end: text.length, text: annotation.text });
|
|
473
|
-
}
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Insert annotation after target block's range
|
|
478
|
-
const annotation = createAnnotationForQuad(quad, ctx);
|
|
479
|
-
if (annotation) {
|
|
480
|
-
// Find the end of the target block's content, not just its range
|
|
481
|
-
const targetBlockEnd = targetBlock.range.end;
|
|
482
|
-
let insertPos = targetBlockEnd;
|
|
483
|
-
|
|
484
|
-
// Skip past the target block's content to find the right insertion point
|
|
485
|
-
while (insertPos < text.length && text[insertPos] !== '\n') {
|
|
486
|
-
insertPos++;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Insert after the target block's content
|
|
490
|
-
const finalInsertPos = insertPos < text.length ? insertPos : text.length;
|
|
491
|
-
edits.push({ start: finalInsertPos, end: finalInsertPos, text: `\n${annotation.text}` });
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return edits;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function applyEdits(text, edits, ctx, base) {
|
|
500
|
-
let result = text;
|
|
501
|
-
|
|
502
|
-
// Sort edits descending to avoid position shifts
|
|
503
|
-
edits.sort((a, b) => b.start - a.start);
|
|
504
|
-
edits.forEach(edit => {
|
|
505
|
-
result = result.substring(0, edit.start) + edit.text + result.substring(edit.end);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
// Extract vacant slots before reparsing
|
|
509
|
-
const vacantSlots = new Map();
|
|
510
|
-
base?.quadMap?.forEach((slot, key) => {
|
|
511
|
-
if (slot.isVacant) vacantSlots.set(key, slot);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
const reparsed = parse(result, { context: ctx });
|
|
515
|
-
|
|
516
|
-
// Merge vacant slots back
|
|
517
|
-
vacantSlots.forEach((vacantSlot, key) => {
|
|
518
|
-
if (!reparsed.origin.quadMap.has(vacantSlot.id) && vacantSlot.blockInfo) {
|
|
519
|
-
const { blockInfo } = vacantSlot;
|
|
520
|
-
const emptyBlock = {
|
|
521
|
-
id: blockInfo.id,
|
|
522
|
-
range: blockInfo.range || { start: 0, end: 0 },
|
|
523
|
-
attrsRange: blockInfo.attrsRange,
|
|
524
|
-
valueRange: blockInfo.valueRange,
|
|
525
|
-
carrierType: blockInfo.carrierType || 'span',
|
|
526
|
-
subject: blockInfo.subject || '',
|
|
527
|
-
types: [],
|
|
528
|
-
predicates: [],
|
|
529
|
-
context: blockInfo.context || { ...ctx }
|
|
530
|
-
};
|
|
531
|
-
reparsed.origin.quadMap.set(vacantSlot.id, emptyBlock);
|
|
532
|
-
}
|
|
533
|
-
reparsed.origin.quadMap.set(key, vacantSlot);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
return { text: result, origin: reparsed.origin };
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Helper functions for origin lookup
|
|
540
|
-
function resolveOriginEntry(quad, base) {
|
|
541
|
-
const key = quadToKeyForOrigin(quad);
|
|
542
|
-
let entry = key ? base?.quadMap?.get(key) : null;
|
|
543
|
-
|
|
544
|
-
if (!entry && isLiteral(quad.object)) {
|
|
545
|
-
// Fallback: search by value
|
|
546
|
-
for (const [k, e] of base?.quadMap || []) {
|
|
547
|
-
const parsed = parseQuadIndexKey(k);
|
|
548
|
-
if (parsed && parsed.s === quad.subject.value &&
|
|
549
|
-
parsed.p === quad.predicate.value &&
|
|
550
|
-
parsed.o?.t === 'Literal' &&
|
|
551
|
-
parsed.o?.v === quad.object.value) {
|
|
552
|
-
entry = e;
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return entry;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function findTargetBlock(quad, base, anchors) {
|
|
562
|
-
const anchorKey = JSON.stringify([quad.subject.value, objectSignature(quad.object)]);
|
|
563
|
-
const anchored = anchors.get(anchorKey);
|
|
564
|
-
if (anchored?.block) return anchored.block;
|
|
565
|
-
|
|
566
|
-
// Find the best position within the subject's section
|
|
567
|
-
// Look for blocks with the same subject and sort by position
|
|
568
|
-
const subjectBlocks = Array.from(base?.quadMap?.values() || [])
|
|
569
|
-
.filter(block => block.subject?.value === quad.subject.value)
|
|
570
|
-
.sort((a, b) => a.range.start - b.range.start);
|
|
571
|
-
|
|
572
|
-
if (subjectBlocks.length === 0) return null;
|
|
573
|
-
|
|
574
|
-
// Strategy: Find the last block with attrsRange to maintain consistency
|
|
575
|
-
// For identical subject blocks, prefer the first one to avoid creating duplicates
|
|
576
|
-
const blocksWithAttrs = subjectBlocks.filter(block => block.attrsRange);
|
|
577
|
-
if (blocksWithAttrs.length > 0) {
|
|
578
|
-
return blocksWithAttrs[blocksWithAttrs.length - 1]; // Return last matching block
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Fallback: return the last block in the subject's section
|
|
582
|
-
return subjectBlocks[subjectBlocks.length - 1];
|
|
583
|
-
}
|