@stll/folio-core 0.47.4 → 0.47.6
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/dist/compare/inline-atoms.js +218 -31
- package/dist/compare/section-boundary-properties.js +1 -0
- package/dist/content-controls/mutateContentControls.js +1 -3
- package/dist/controller/headerFooterEditorManager.d.ts +2 -1
- package/dist/controller/headerFooterEditorManager.js +2 -1
- package/dist/controller/noteEditorManager.d.ts +2 -1
- package/dist/controller/noteEditorManager.js +2 -1
- package/dist/docx/commentRangeIntegrity.js +7 -0
- package/dist/docx/documentParser.js +55 -3
- package/dist/docx/inlineWrapperContent.d.ts +18 -16
- package/dist/docx/inlineWrapperContent.js +18 -16
- package/dist/docx/normalizeBaseDirection.js +4 -0
- package/dist/docx/rezip.js +1 -0
- package/dist/docx/selectiveSave.js +11 -2
- package/dist/docx/serializer/documentSerializer.js +16 -1
- package/dist/docx/serializer/paragraphSerializer.js +8 -0
- package/dist/docx/serializer/tableSerializer.js +5 -2
- package/dist/docx/server/createBilingualDocument.js +23 -14
- package/dist/docx/tableParser.js +101 -75
- package/dist/markdown/renderRuns.js +4 -0
- package/dist/markdown/renderTable.js +46 -12
- package/dist/prosemirror/attrs/index.d.ts +3 -2
- package/dist/prosemirror/attrs/index.js +85 -4
- package/dist/prosemirror/commands/comments.js +61 -3
- package/dist/prosemirror/commentReferenceAttrs.js +2 -2
- package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
- package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +177 -51
- package/dist/prosemirror/conversion/toProseDoc.js +108 -55
- package/dist/prosemirror/extensions/markRegistry.d.ts +5 -8
- package/dist/prosemirror/extensions/markRegistry.js +5 -8
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
- package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
- package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +2 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +32 -7
- package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
- package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
- package/dist/prosemirror/runIdentity.d.ts +5 -5
- package/dist/prosemirror/runIdentity.js +5 -5
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +28 -2
- package/dist/prosemirror/schema/nodes.d.ts +6 -4
- package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
- package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
- package/dist/prosemirror/utils/extractTrackedChanges.js +21 -6
- package/dist/prosemirror/validation.js +48 -7
- package/dist/types/content.d.ts +2 -2
- package/dist/types/index.d.ts +3 -1
- package/dist/utils/mergeDocumentContent.js +1 -7
- package/package.json +2 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { buildCleanBlockText } from "../ai-edits/clean-text.js";
|
|
2
2
|
import { sourceDocumentOf } from "../ai-edits/snapshot.js";
|
|
3
3
|
import { resolveAllChangesInHeadlessStateWithMapping } from "../prosemirror/commands/comments.js";
|
|
4
|
+
import { runFormattingInlineAtomResultText } from "../prosemirror/runFormattingInlineCarriers.js";
|
|
4
5
|
import { canonicalJson } from "../utils/canonicalJson.js";
|
|
5
6
|
import { prepareTargetInlineAtom } from "./inline-atom-resources.js";
|
|
6
7
|
//#region src/compare/inline-atoms.ts
|
|
8
|
+
const actionRangeCount = (action) => action.kind === "replace-text" && action.textDisposition === "retained" || action.kind === "replace-atom" && action.atomDisposition === "retained" ? 2 : 1;
|
|
7
9
|
const MAX_INLINE_ATOM_LCS_CELLS = 16384;
|
|
8
10
|
const isSupportedAtom = (node) => node.isInline && node.isAtom && (node.type.name === "field" || node.type.name === "image" || node.type.name === "pageBreakRun");
|
|
9
11
|
const hasSupportedAtom = (doc) => {
|
|
@@ -72,8 +74,8 @@ const atomKey = (node) => canonicalJson({
|
|
|
72
74
|
attrs: documentFactAttrs(node),
|
|
73
75
|
content: node.content.toJSON()
|
|
74
76
|
});
|
|
75
|
-
const atomBlockOf = ({ node, from }) => {
|
|
76
|
-
const clean = buildCleanBlockText(node, from, { fieldResults
|
|
77
|
+
const atomBlockOf = ({ node, from }, fieldResults) => {
|
|
78
|
+
const clean = buildCleanBlockText(node, from, { fieldResults });
|
|
77
79
|
const supported = [];
|
|
78
80
|
const unsupportedTopology = [];
|
|
79
81
|
node.descendants((child, relativePosition) => {
|
|
@@ -86,12 +88,18 @@ const atomBlockOf = ({ node, from }) => {
|
|
|
86
88
|
});
|
|
87
89
|
if (!isSupportedAtom(child)) {
|
|
88
90
|
if (child.type.name === "renderedPageBreak") return false;
|
|
89
|
-
unsupportedTopology.push(
|
|
91
|
+
unsupportedTopology.push({
|
|
92
|
+
offset,
|
|
93
|
+
key: `${child.type.name}:${canonicalJson(documentFactAttrs(child))}`
|
|
94
|
+
});
|
|
90
95
|
return false;
|
|
91
96
|
}
|
|
92
97
|
const prepared = prepareTargetInlineAtom(child);
|
|
93
98
|
if (!prepared) {
|
|
94
|
-
unsupportedTopology.push(
|
|
99
|
+
unsupportedTopology.push({
|
|
100
|
+
offset,
|
|
101
|
+
key: `${child.type.name}:${canonicalJson(documentFactAttrs(child))}`
|
|
102
|
+
});
|
|
95
103
|
return false;
|
|
96
104
|
}
|
|
97
105
|
supported.push({
|
|
@@ -130,8 +138,33 @@ const targetBlockIdLookup = (snapshot) => {
|
|
|
130
138
|
return owner?.id;
|
|
131
139
|
};
|
|
132
140
|
};
|
|
133
|
-
const atomBlocksOf = (doc) => textBlocksOf(doc).map(atomBlockOf);
|
|
134
|
-
const sameBlockTopology = (left, right) =>
|
|
141
|
+
const atomBlocksOf = (doc, fieldResults) => textBlocksOf(doc).map((block) => atomBlockOf(block, fieldResults));
|
|
142
|
+
const sameBlockTopology = (left, right) => {
|
|
143
|
+
if (left.node.type !== right.node.type || left.cleanText !== right.cleanText) return false;
|
|
144
|
+
const supportedOffsets = /* @__PURE__ */ new Set([...left.supported.map(({ offset }) => offset), ...right.supported.map(({ offset }) => offset)]);
|
|
145
|
+
const relevantTopology = ({ unsupportedTopology }) => unsupportedTopology.filter(({ offset }) => supportedOffsets.has(offset));
|
|
146
|
+
return canonicalJson(relevantTopology(left)) === canonicalJson(relevantTopology(right));
|
|
147
|
+
};
|
|
148
|
+
const supportedAtomProjection = (block) => canonicalJson(block.supported.map(({ offset, key }) => ({
|
|
149
|
+
offset,
|
|
150
|
+
key
|
|
151
|
+
})));
|
|
152
|
+
const sameSupportedAtoms = (left, right) => left.node.type === right.node.type && supportedAtomProjection(left) === supportedAtomProjection(right);
|
|
153
|
+
const fieldResultOwnershipOf = (atoms) => {
|
|
154
|
+
const ownership = /* @__PURE__ */ new Map();
|
|
155
|
+
for (const atom of atoms) {
|
|
156
|
+
const resultText = runFormattingInlineAtomResultText(atom.node);
|
|
157
|
+
if (!resultText) continue;
|
|
158
|
+
let resultsAtOffset = ownership.get(atom.offset);
|
|
159
|
+
if (!resultsAtOffset) {
|
|
160
|
+
resultsAtOffset = /* @__PURE__ */ new Set();
|
|
161
|
+
ownership.set(atom.offset, resultsAtOffset);
|
|
162
|
+
}
|
|
163
|
+
resultsAtOffset.add(resultText);
|
|
164
|
+
}
|
|
165
|
+
return ownership;
|
|
166
|
+
};
|
|
167
|
+
const ownsFieldResult = ({ ownership, offset, resultText }) => ownership.get(offset)?.has(resultText) ?? false;
|
|
135
168
|
const matchingAtomGroup = ({ live, target, liveStart, targetStart }) => {
|
|
136
169
|
if (live.length === target.length && live.every((atom, index) => atom.key === target[index]?.key)) return live.map((_, index) => [liveStart + index, targetStart + index]);
|
|
137
170
|
if (live.length === 0 || target.length === 0) return [];
|
|
@@ -246,14 +279,58 @@ const sourceTextBlockIds = (source) => {
|
|
|
246
279
|
}
|
|
247
280
|
return blocks;
|
|
248
281
|
};
|
|
249
|
-
const sameParagraphSourcePosition = ({ sourceBlocks, reviewed, offset }) => {
|
|
282
|
+
const sameParagraphSourcePosition = ({ sourceBlocks, reviewed, offset, fieldResults }) => {
|
|
250
283
|
const paraId = reviewed.node.attrs["paraId"];
|
|
251
284
|
if (typeof paraId !== "string" || paraId.length === 0) return null;
|
|
252
285
|
const source = sourceBlocks.get(paraId);
|
|
253
286
|
if (!source || source.node.type !== reviewed.node.type) return null;
|
|
254
|
-
const clean = buildCleanBlockText(source.node, source.from, { fieldResults
|
|
287
|
+
const clean = buildCleanBlockText(source.node, source.from, { fieldResults });
|
|
255
288
|
return clean.text === reviewed.cleanText ? clean.offsets[offset] ?? null : null;
|
|
256
289
|
};
|
|
290
|
+
const textRangeDisposition = ({ doc, from, to, expectedText, originalRevisionIdSeed }) => {
|
|
291
|
+
if (from >= to || doc.textBetween(from, to) !== expectedText) return null;
|
|
292
|
+
let disposition;
|
|
293
|
+
let covered = 0;
|
|
294
|
+
let invalid = false;
|
|
295
|
+
doc.nodesBetween(from, to, (node, position) => {
|
|
296
|
+
if (!node.isText) {
|
|
297
|
+
if (position > from && position < to) invalid = true;
|
|
298
|
+
return !invalid;
|
|
299
|
+
}
|
|
300
|
+
const overlapFrom = Math.max(from, position);
|
|
301
|
+
const overlapTo = Math.min(to, position + node.nodeSize);
|
|
302
|
+
if (overlapFrom >= overlapTo) return false;
|
|
303
|
+
covered += overlapTo - overlapFrom;
|
|
304
|
+
if (node.marks.some(({ type }) => type.name === "deletion")) {
|
|
305
|
+
invalid = true;
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const insertion = node.marks.find(({ type }) => type.name === "insertion");
|
|
309
|
+
let current = "retained";
|
|
310
|
+
if (insertion) {
|
|
311
|
+
const revisionId = insertion.attrs["revisionId"];
|
|
312
|
+
if (typeof revisionId !== "number" || revisionId < originalRevisionIdSeed) {
|
|
313
|
+
invalid = true;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
current = "inserted";
|
|
317
|
+
}
|
|
318
|
+
if (disposition !== void 0 && disposition !== current) {
|
|
319
|
+
invalid = true;
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
disposition = current;
|
|
323
|
+
return false;
|
|
324
|
+
});
|
|
325
|
+
return !invalid && covered === to - from ? disposition ?? null : null;
|
|
326
|
+
};
|
|
327
|
+
const atomDisposition = ({ atom, originalRevisionIdSeed }) => {
|
|
328
|
+
if (atom.node.marks.some(({ type }) => type.name === "deletion")) return null;
|
|
329
|
+
const insertion = atom.node.marks.find(({ type }) => type.name === "insertion");
|
|
330
|
+
if (!insertion) return "retained";
|
|
331
|
+
const revisionId = insertion.attrs["revisionId"];
|
|
332
|
+
return typeof revisionId === "number" && revisionId >= originalRevisionIdSeed ? "inserted" : null;
|
|
333
|
+
};
|
|
257
334
|
/**
|
|
258
335
|
* Restore field, image, and page-break atoms omitted by text-only comparison operations.
|
|
259
336
|
* Positions come from the legacy review resolver's mapping, never a guessed
|
|
@@ -272,15 +349,38 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
272
349
|
const reviewed = resolveAllChangesInHeadlessStateWithMapping(state, "accept");
|
|
273
350
|
const targetBlockIdAt = targetBlockIdLookup(targetSnapshot);
|
|
274
351
|
let sourceBlocks;
|
|
275
|
-
const liveBlocks = atomBlocksOf(reviewed.state.doc);
|
|
276
|
-
const targetBlocks = atomBlocksOf(targetDocument);
|
|
352
|
+
const liveBlocks = atomBlocksOf(reviewed.state.doc, "text");
|
|
353
|
+
const targetBlocks = atomBlocksOf(targetDocument, "text");
|
|
277
354
|
if (liveBlocks.length !== targetBlocks.length) return { status: "unalignable" };
|
|
278
355
|
const actions = [];
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
|
|
356
|
+
let rangeCount = 0;
|
|
357
|
+
const planAction = (action) => {
|
|
358
|
+
const nextRangeCount = rangeCount + actionRangeCount(action);
|
|
359
|
+
if (nextRangeCount > maxRanges) return false;
|
|
360
|
+
rangeCount = nextRangeCount;
|
|
361
|
+
actions.push(action);
|
|
362
|
+
return true;
|
|
363
|
+
};
|
|
364
|
+
let omittedLiveBlocks;
|
|
365
|
+
let omittedTargetBlocks;
|
|
366
|
+
for (const [index, fullLive] of liveBlocks.entries()) {
|
|
367
|
+
const fullTarget = targetBlocks[index];
|
|
368
|
+
if (!fullTarget) return { status: "unalignable" };
|
|
369
|
+
if (fullLive.supported.length === 0 && fullTarget.supported.length === 0) continue;
|
|
370
|
+
if (sameSupportedAtoms(fullLive, fullTarget)) continue;
|
|
371
|
+
let live = fullLive;
|
|
372
|
+
let target = fullTarget;
|
|
373
|
+
let fieldResults = "text";
|
|
374
|
+
if (!sameBlockTopology(live, target)) {
|
|
375
|
+
omittedLiveBlocks ??= atomBlocksOf(reviewed.state.doc, "omitted");
|
|
376
|
+
omittedTargetBlocks ??= atomBlocksOf(targetDocument, "omitted");
|
|
377
|
+
const omittedLive = omittedLiveBlocks[index];
|
|
378
|
+
const omittedTarget = omittedTargetBlocks[index];
|
|
379
|
+
if (!omittedLive || !omittedTarget || !sameBlockTopology(omittedLive, omittedTarget)) return { status: "unalignable" };
|
|
380
|
+
live = omittedLive;
|
|
381
|
+
target = omittedTarget;
|
|
382
|
+
fieldResults = "omitted";
|
|
383
|
+
}
|
|
284
384
|
const matches = matchingAtoms({
|
|
285
385
|
live: live.supported,
|
|
286
386
|
target: target.supported
|
|
@@ -292,6 +392,8 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
292
392
|
target: target.supported,
|
|
293
393
|
matches
|
|
294
394
|
});
|
|
395
|
+
const liveFieldResultOwnership = fieldResultOwnershipOf(live.supported);
|
|
396
|
+
const targetFieldResultOwnership = fieldResultOwnershipOf(target.supported);
|
|
295
397
|
for (const [liveIndex, atom] of live.supported.entries()) {
|
|
296
398
|
if (matchedLive.has(liveIndex)) continue;
|
|
297
399
|
const from = mappedSourcePosition({
|
|
@@ -299,12 +401,34 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
299
401
|
position: atom.from
|
|
300
402
|
});
|
|
301
403
|
if (from === null) return { status: "unalignable" };
|
|
302
|
-
|
|
404
|
+
const resultText = runFormattingInlineAtomResultText(atom.node);
|
|
405
|
+
if (fieldResults === "text" && resultText && !ownsFieldResult({
|
|
406
|
+
ownership: targetFieldResultOwnership,
|
|
407
|
+
offset: atom.offset,
|
|
408
|
+
resultText
|
|
409
|
+
})) {
|
|
410
|
+
const disposition = atomDisposition({
|
|
411
|
+
atom,
|
|
412
|
+
originalRevisionIdSeed
|
|
413
|
+
});
|
|
414
|
+
if (disposition === null) return { status: "unalignable" };
|
|
415
|
+
if (!planAction({
|
|
416
|
+
kind: "replace-atom",
|
|
417
|
+
from,
|
|
418
|
+
to: from + atom.node.nodeSize,
|
|
419
|
+
text: resultText,
|
|
420
|
+
atomDisposition: disposition,
|
|
421
|
+
marks: atom.node.marks,
|
|
422
|
+
targetBlockId: targetBlockIdAt(target.from)
|
|
423
|
+
})) return { status: "budget-exceeded" };
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (!planAction({
|
|
303
427
|
kind: "delete",
|
|
304
428
|
from,
|
|
305
429
|
to: from + atom.node.nodeSize,
|
|
306
430
|
targetBlockId: targetBlockIdAt(target.from)
|
|
307
|
-
});
|
|
431
|
+
})) return { status: "budget-exceeded" };
|
|
308
432
|
}
|
|
309
433
|
for (const [targetIndex, atom] of target.supported.entries()) {
|
|
310
434
|
if (matchedTarget.has(targetIndex)) continue;
|
|
@@ -316,18 +440,48 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
316
440
|
}) ?? sameParagraphSourcePosition({
|
|
317
441
|
sourceBlocks: sourceBlocks ??= sourceTextBlockIds(state.doc),
|
|
318
442
|
reviewed: live,
|
|
319
|
-
offset: atom.offset
|
|
443
|
+
offset: atom.offset,
|
|
444
|
+
fieldResults
|
|
320
445
|
});
|
|
321
446
|
if (from === null) return { status: "unalignable" };
|
|
322
|
-
|
|
447
|
+
const resultText = runFormattingInlineAtomResultText(atom.node);
|
|
448
|
+
if (fieldResults === "text" && resultText && !ownsFieldResult({
|
|
449
|
+
ownership: liveFieldResultOwnership,
|
|
450
|
+
offset: atom.offset,
|
|
451
|
+
resultText
|
|
452
|
+
})) {
|
|
453
|
+
const reviewedTo = live.offsets[atom.offset + resultText.length];
|
|
454
|
+
const to = reviewedTo === void 0 ? null : mappedSourcePosition({
|
|
455
|
+
mapping: reviewed.mapping,
|
|
456
|
+
position: reviewedTo
|
|
457
|
+
});
|
|
458
|
+
if (to === null) return { status: "unalignable" };
|
|
459
|
+
const textDisposition = textRangeDisposition({
|
|
460
|
+
doc: state.doc,
|
|
461
|
+
from,
|
|
462
|
+
to,
|
|
463
|
+
expectedText: resultText,
|
|
464
|
+
originalRevisionIdSeed
|
|
465
|
+
});
|
|
466
|
+
if (textDisposition === null) return { status: "unalignable" };
|
|
467
|
+
if (!planAction({
|
|
468
|
+
kind: "replace-text",
|
|
469
|
+
from,
|
|
470
|
+
to,
|
|
471
|
+
node: atom.node,
|
|
472
|
+
textDisposition,
|
|
473
|
+
targetBlockId: targetBlockIdAt(target.from)
|
|
474
|
+
})) return { status: "budget-exceeded" };
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (!planAction({
|
|
323
478
|
kind: "insert",
|
|
324
479
|
from,
|
|
325
480
|
node: atom.node,
|
|
326
481
|
targetBlockId: targetBlockIdAt(target.from)
|
|
327
|
-
});
|
|
482
|
+
})) return { status: "budget-exceeded" };
|
|
328
483
|
}
|
|
329
484
|
}
|
|
330
|
-
if (actions.length > maxRanges) return { status: "budget-exceeded" };
|
|
331
485
|
const insertionType = state.schema.marks["insertion"];
|
|
332
486
|
const deletionType = state.schema.marks["deletion"];
|
|
333
487
|
if (!insertionType || !deletionType) return { status: "unalignable" };
|
|
@@ -347,6 +501,45 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
347
501
|
if (action.targetBlockId) changedTargetBlockIds.add(action.targetBlockId);
|
|
348
502
|
continue;
|
|
349
503
|
}
|
|
504
|
+
if (action.kind === "replace-text") {
|
|
505
|
+
const from = transaction.mapping.map(action.from, 1);
|
|
506
|
+
const to = transaction.mapping.map(action.to, -1);
|
|
507
|
+
if (from >= to) return { status: "unalignable" };
|
|
508
|
+
if (action.textDisposition === "inserted") transaction.delete(from, to);
|
|
509
|
+
else transaction.addMark(from, to, deletionType.create({
|
|
510
|
+
revisionId: nextRevisionId++,
|
|
511
|
+
author,
|
|
512
|
+
date: revisionStamp.date
|
|
513
|
+
}));
|
|
514
|
+
const at = transaction.mapping.map(action.from, -1);
|
|
515
|
+
transaction.insert(at, action.node.mark(insertionType.create({
|
|
516
|
+
revisionId: nextRevisionId++,
|
|
517
|
+
author,
|
|
518
|
+
date: revisionStamp.date
|
|
519
|
+
}).addToSet(action.node.marks)));
|
|
520
|
+
if (action.targetBlockId) changedTargetBlockIds.add(action.targetBlockId);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (action.kind === "replace-atom") {
|
|
524
|
+
const from = transaction.mapping.map(action.from, 1);
|
|
525
|
+
const to = transaction.mapping.map(action.to, -1);
|
|
526
|
+
const node = transaction.doc.nodeAt(from);
|
|
527
|
+
if (!node || node.nodeSize !== to - from || node.type.name !== "field") return { status: "unalignable" };
|
|
528
|
+
if (action.atomDisposition === "inserted") transaction.delete(from, to);
|
|
529
|
+
else transaction.addMark(from, to, deletionType.create({
|
|
530
|
+
revisionId: nextRevisionId++,
|
|
531
|
+
author,
|
|
532
|
+
date: revisionStamp.date
|
|
533
|
+
}));
|
|
534
|
+
const at = transaction.mapping.map(action.from, -1);
|
|
535
|
+
transaction.insert(at, state.schema.text(action.text, insertionType.create({
|
|
536
|
+
revisionId: nextRevisionId++,
|
|
537
|
+
author,
|
|
538
|
+
date: revisionStamp.date
|
|
539
|
+
}).addToSet(action.marks)));
|
|
540
|
+
if (action.targetBlockId) changedTargetBlockIds.add(action.targetBlockId);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
350
543
|
const from = transaction.mapping.map(action.from, 1);
|
|
351
544
|
const to = transaction.mapping.map(action.to, -1);
|
|
352
545
|
const node = transaction.doc.nodeAt(from);
|
|
@@ -372,25 +565,19 @@ const matchInlineAtoms = ({ state, targetSnapshot, revisionStamp, originalRevisi
|
|
|
372
565
|
transaction,
|
|
373
566
|
nextRevisionId,
|
|
374
567
|
changedTargetBlockIds: [...changedTargetBlockIds],
|
|
375
|
-
rangeCount
|
|
568
|
+
rangeCount
|
|
376
569
|
};
|
|
377
570
|
};
|
|
378
571
|
/** Compare supported inline atom identity after accept or reject projection. */
|
|
379
572
|
const sameInlineAtoms = (leftDocument, rightDocument) => {
|
|
380
573
|
if (!hasSupportedAtom(leftDocument) && !hasSupportedAtom(rightDocument)) return true;
|
|
381
|
-
const leftBlocks = atomBlocksOf(leftDocument);
|
|
382
|
-
const rightBlocks = atomBlocksOf(rightDocument);
|
|
574
|
+
const leftBlocks = atomBlocksOf(leftDocument, "omitted");
|
|
575
|
+
const rightBlocks = atomBlocksOf(rightDocument, "omitted");
|
|
383
576
|
if (leftBlocks.length !== rightBlocks.length) return false;
|
|
384
577
|
return leftBlocks.every((left, index) => {
|
|
385
578
|
const right = rightBlocks[index];
|
|
386
579
|
if (left.supported.length === 0 && right?.supported.length === 0) return true;
|
|
387
|
-
return right !== void 0 &&
|
|
388
|
-
offset,
|
|
389
|
-
key
|
|
390
|
-
}))) === canonicalJson(right.supported.map(({ offset, key }) => ({
|
|
391
|
-
offset,
|
|
392
|
-
key
|
|
393
|
-
})));
|
|
580
|
+
return right !== void 0 && left.cleanText === right.cleanText && sameSupportedAtoms(left, right);
|
|
394
581
|
});
|
|
395
582
|
};
|
|
396
583
|
//#endregion
|
|
@@ -12,6 +12,7 @@ const boundaryChildrenOf = (document) => {
|
|
|
12
12
|
let hasChangeHistory = false;
|
|
13
13
|
document.forEach((node, position) => {
|
|
14
14
|
if (node.type.name !== "paragraph") {
|
|
15
|
+
if (node.type.name === "preservedBlock") return;
|
|
15
16
|
children.push({
|
|
16
17
|
type: "other",
|
|
17
18
|
nodeType: node.type.name
|
|
@@ -162,11 +162,9 @@ function mapBlock(block, match) {
|
|
|
162
162
|
...row,
|
|
163
163
|
cells: row.cells.map((cell) => {
|
|
164
164
|
const transformed = transformBlocks(cell.content, match);
|
|
165
|
-
const cellContent = [];
|
|
166
|
-
for (const child of transformed) if (child.type === "paragraph" || child.type === "table") cellContent.push(child);
|
|
167
165
|
return {
|
|
168
166
|
...cell,
|
|
169
|
-
content:
|
|
167
|
+
content: transformed
|
|
170
168
|
};
|
|
171
169
|
})
|
|
172
170
|
}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
|
-
import { EditorView } from "prosemirror-view";
|
|
2
|
+
import { DirectEditorProps, EditorView } from "prosemirror-view";
|
|
3
3
|
//#region src/controller/headerFooterEditorManager.d.ts
|
|
4
4
|
type HeaderFooterPartKind = "header" | "footer";
|
|
5
5
|
type HeaderFooterPartKey = {
|
|
@@ -14,6 +14,7 @@ type HeaderFooterEditorTransaction = {
|
|
|
14
14
|
view: EditorView;
|
|
15
15
|
};
|
|
16
16
|
type HeaderFooterEditorManagerDeps = {
|
|
17
|
+
createView?: ((mountNode: HTMLElement, props: DirectEditorProps) => EditorView) | undefined;
|
|
17
18
|
getDocument: () => document_d_exports.Document | null;
|
|
18
19
|
getHost: () => HTMLElement | null;
|
|
19
20
|
getStyles: () => document_d_exports.StyleDefinitions | null | undefined;
|
|
@@ -12,6 +12,7 @@ import { schema } from "../prosemirror/schema/index.js";
|
|
|
12
12
|
import { EditorState } from "prosemirror-state";
|
|
13
13
|
import { EditorView } from "prosemirror-view";
|
|
14
14
|
//#region src/controller/headerFooterEditorManager.ts
|
|
15
|
+
const createEditorView = (mountNode, props) => new EditorView(mountNode, props);
|
|
15
16
|
const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
|
|
16
17
|
const headerFooterToProseDocWithDetachedWatermarkHost = (headerFooter, options) => {
|
|
17
18
|
return headerFooterToProseDoc(headerFooter.content.map((block, blockIndex) => {
|
|
@@ -109,7 +110,7 @@ const createHeaderFooterEditorManager = (deps) => {
|
|
|
109
110
|
mountNode.dataset["hfRId"] = part.rId;
|
|
110
111
|
mountNode.dataset["hfKind"] = part.kind;
|
|
111
112
|
host.append(mountNode);
|
|
112
|
-
const view =
|
|
113
|
+
const view = (deps.createView ?? createEditorView)(mountNode, {
|
|
113
114
|
state: buildInitialState(headerFooter, styles, theme, numbering, manager),
|
|
114
115
|
dispatchTransaction(transaction) {
|
|
115
116
|
const nextState = view.state.apply(transaction);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
2
|
import { NoteStoryKey } from "../types/editor-story.js";
|
|
3
3
|
import { Plugin } from "prosemirror-state";
|
|
4
|
-
import { EditorView } from "prosemirror-view";
|
|
4
|
+
import { DirectEditorProps, EditorView } from "prosemirror-view";
|
|
5
5
|
//#region src/controller/noteEditorManager.d.ts
|
|
6
6
|
type NoteStoryKind = NoteStoryKey["kind"];
|
|
7
7
|
type NoteEditorTransaction = NoteStoryKey & {
|
|
@@ -10,6 +10,7 @@ type NoteEditorTransaction = NoteStoryKey & {
|
|
|
10
10
|
view: EditorView;
|
|
11
11
|
};
|
|
12
12
|
type NoteEditorManagerDeps = {
|
|
13
|
+
createView?: ((mountNode: HTMLElement, props: DirectEditorProps) => EditorView) | undefined;
|
|
13
14
|
getDocument: () => document_d_exports.Document | null;
|
|
14
15
|
getHost: () => HTMLElement | null;
|
|
15
16
|
getPlugins?: (() => Plugin[]) | undefined;
|
|
@@ -12,6 +12,7 @@ import { EditorState } from "prosemirror-state";
|
|
|
12
12
|
import { EditorView } from "prosemirror-view";
|
|
13
13
|
//#region src/controller/noteEditorManager.ts
|
|
14
14
|
/** Framework-neutral lifecycle owner for editable footnote and endnote stories. */
|
|
15
|
+
const createEditorView = (mountNode, props) => new EditorView(mountNode, props);
|
|
15
16
|
const EMPTY_PLUGINS = [];
|
|
16
17
|
const storyMapKey = ({ kind, noteId }) => `${kind}:${String(noteId)}`;
|
|
17
18
|
const isNormalFootnote = (note) => !isSeparatorFootnote(note);
|
|
@@ -110,7 +111,7 @@ const createNoteEditorManager = (deps) => {
|
|
|
110
111
|
mountNode.dataset["noteId"] = String(storyKey.noteId);
|
|
111
112
|
host.append(mountNode);
|
|
112
113
|
const proseDocument = noteToProseDocument(note, styles, theme);
|
|
113
|
-
const view =
|
|
114
|
+
const view = (deps.createView ?? createEditorView)(mountNode, {
|
|
114
115
|
state: buildInitialState(proseDocument, styles, numbering, manager, externalPlugins),
|
|
115
116
|
dispatchTransaction(transaction) {
|
|
116
117
|
view.updateState(view.state.apply(transaction));
|
|
@@ -204,6 +204,13 @@ const withoutOrphanTableCellBlockMarkers = (blocks, validCommentIds) => {
|
|
|
204
204
|
const withoutOrphanTableCellBlockMarker = (block, validCommentIds) => {
|
|
205
205
|
if (block.type === "paragraph") return withoutOrphanParagraphMarkers(block, validCommentIds);
|
|
206
206
|
if (block.type === "table") return withoutOrphanTableMarkers(block, validCommentIds);
|
|
207
|
+
if (block.type === "blockSdt") {
|
|
208
|
+
const content = withoutOrphanTableCellBlockMarkers(block.content, validCommentIds);
|
|
209
|
+
return content ? {
|
|
210
|
+
...block,
|
|
211
|
+
content
|
|
212
|
+
} : block;
|
|
213
|
+
}
|
|
207
214
|
return block;
|
|
208
215
|
};
|
|
209
216
|
const withoutOrphanParagraphMarkers = (paragraph, validCommentIds) => {
|
|
@@ -2,7 +2,9 @@ import { parseBlockContent } from "./blockContentParser.js";
|
|
|
2
2
|
import { getParagraphText } from "./paragraphParser.js";
|
|
3
3
|
import { getDefaultSectionProperties, parseSectionProperties } from "./sectionParser.js";
|
|
4
4
|
import { parseStreamingXml } from "./streamingXmlParser.js";
|
|
5
|
-
import {
|
|
5
|
+
import { parseThemeColorAttribute } from "./themeColorAttribute.js";
|
|
6
|
+
import { captureVerbatimXml } from "./verbatimCapture.js";
|
|
7
|
+
import { WORDPROCESSINGML_NAMESPACE_URIS, collectXmlnsDeclarations, findChild, findChildByNamespaceUri, getAttributeByNamespaceUri, getLocalName, parseXml } from "./xmlParser.js";
|
|
6
8
|
//#region src/docx/documentParser.ts
|
|
7
9
|
/**
|
|
8
10
|
* Regular expression to match template variables {{...}}
|
|
@@ -38,6 +40,8 @@ function extractAllTemplateVariables(content) {
|
|
|
38
40
|
} else if (block.type === "table") {
|
|
39
41
|
const tableVars = extractTableVariables(block);
|
|
40
42
|
for (const v of tableVars) if (!variables.includes(v)) variables.push(v);
|
|
43
|
+
} else if (block.type === "blockSdt") {
|
|
44
|
+
for (const v of extractAllTemplateVariables(block.content)) if (!variables.includes(v)) variables.push(v);
|
|
41
45
|
}
|
|
42
46
|
return variables;
|
|
43
47
|
}
|
|
@@ -52,6 +56,8 @@ function extractTableVariables(table) {
|
|
|
52
56
|
} else if (cellContent.type === "table") {
|
|
53
57
|
const nestedVars = extractTableVariables(cellContent);
|
|
54
58
|
for (const v of nestedVars) if (!variables.includes(v)) variables.push(v);
|
|
59
|
+
} else if (cellContent.type === "blockSdt") {
|
|
60
|
+
for (const v of extractAllTemplateVariables(cellContent.content)) if (!variables.includes(v)) variables.push(v);
|
|
55
61
|
}
|
|
56
62
|
return variables;
|
|
57
63
|
}
|
|
@@ -108,6 +114,28 @@ function canonicalizeLeadingBodySectionProperties(bodyElement, body) {
|
|
|
108
114
|
}
|
|
109
115
|
body.finalSectionProperties = nextProperties;
|
|
110
116
|
}
|
|
117
|
+
/** Read the page background declared directly under `w:document`. */
|
|
118
|
+
function parseDocumentBackground(documentEl, context) {
|
|
119
|
+
const element = findChildByNamespaceUri(documentEl, WORDPROCESSINGML_NAMESPACE_URIS, "background");
|
|
120
|
+
if (!element) return;
|
|
121
|
+
const background = {};
|
|
122
|
+
const color = getAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "color");
|
|
123
|
+
if (color === "auto") background.color = { auto: true };
|
|
124
|
+
else if (color !== null) background.color = { rgb: color };
|
|
125
|
+
const themeColor = parseThemeColorAttribute({
|
|
126
|
+
raw: getAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "themeColor"),
|
|
127
|
+
element: element.name ?? "w:background",
|
|
128
|
+
context
|
|
129
|
+
});
|
|
130
|
+
if (themeColor !== void 0) background.themeColor = themeColor;
|
|
131
|
+
const themeTint = getAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "themeTint");
|
|
132
|
+
if (themeTint !== null) background.themeTint = themeTint;
|
|
133
|
+
const themeShade = getAttributeByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "themeShade");
|
|
134
|
+
if (themeShade !== null) background.themeShade = themeShade;
|
|
135
|
+
const drawing = findChildByNamespaceUri(element, WORDPROCESSINGML_NAMESPACE_URIS, "drawing");
|
|
136
|
+
if (drawing) background.drawing = { rawXml: captureVerbatimXml(drawing) };
|
|
137
|
+
return background;
|
|
138
|
+
}
|
|
111
139
|
/**
|
|
112
140
|
* Parse document.xml body content
|
|
113
141
|
*
|
|
@@ -125,6 +153,8 @@ function parseDocumentBody(xml, styles = null, theme = null, numbering = null, r
|
|
|
125
153
|
const streamed = parseStreamingXml(xml);
|
|
126
154
|
const documentEl = ((streamed.status === "parsed" ? streamed.value : parseXml(xml)).elements ?? []).find((el) => el.type === "element" && (el.name === "w:document" || el.name?.endsWith(":document")));
|
|
127
155
|
if (!documentEl) return result;
|
|
156
|
+
const background = parseDocumentBackground(documentEl, context);
|
|
157
|
+
if (background !== void 0) result.background = background;
|
|
128
158
|
const bodyEl = findChild(documentEl, "w", "body");
|
|
129
159
|
if (!bodyEl) return result;
|
|
130
160
|
result.content = parseBlockContent(bodyEl, styles, theme, numbering, rels, media, {
|
|
@@ -144,8 +174,16 @@ function getAllParagraphs(body) {
|
|
|
144
174
|
const paragraphs = [];
|
|
145
175
|
for (const block of body.content) if (block.type === "paragraph") paragraphs.push(block);
|
|
146
176
|
else if (block.type === "table") paragraphs.push(...getTableParagraphs(block));
|
|
177
|
+
else if (block.type === "blockSdt") paragraphs.push(...getParagraphsFromBlocks(block.content));
|
|
147
178
|
return paragraphs;
|
|
148
179
|
}
|
|
180
|
+
const getParagraphsFromBlocks = (blocks) => {
|
|
181
|
+
const paragraphs = [];
|
|
182
|
+
for (const block of blocks) if (block.type === "paragraph") paragraphs.push(block);
|
|
183
|
+
else if (block.type === "table") paragraphs.push(...getTableParagraphs(block));
|
|
184
|
+
else if (block.type === "blockSdt") paragraphs.push(...getParagraphsFromBlocks(block.content));
|
|
185
|
+
return paragraphs;
|
|
186
|
+
};
|
|
149
187
|
/**
|
|
150
188
|
* Get all paragraphs from a table (recursively)
|
|
151
189
|
*/
|
|
@@ -153,6 +191,7 @@ function getTableParagraphs(table) {
|
|
|
153
191
|
const paragraphs = [];
|
|
154
192
|
for (const row of table.rows) for (const cell of row.cells) for (const content of cell.content) if (content.type === "paragraph") paragraphs.push(content);
|
|
155
193
|
else if (content.type === "table") paragraphs.push(...getTableParagraphs(content));
|
|
194
|
+
else if (content.type === "blockSdt") paragraphs.push(...getParagraphsFromBlocks(content.content));
|
|
156
195
|
return paragraphs;
|
|
157
196
|
}
|
|
158
197
|
/**
|
|
@@ -163,9 +202,15 @@ function getAllTables(body) {
|
|
|
163
202
|
for (const block of body.content) if (block.type === "table") {
|
|
164
203
|
tables.push(block);
|
|
165
204
|
tables.push(...getNestedTables(block));
|
|
166
|
-
}
|
|
205
|
+
} else if (block.type === "blockSdt") tables.push(...getTablesFromBlocks(block.content));
|
|
167
206
|
return tables;
|
|
168
207
|
}
|
|
208
|
+
const getTablesFromBlocks = (blocks) => {
|
|
209
|
+
const tables = [];
|
|
210
|
+
for (const block of blocks) if (block.type === "table") tables.push(block, ...getNestedTables(block));
|
|
211
|
+
else if (block.type === "blockSdt") tables.push(...getTablesFromBlocks(block.content));
|
|
212
|
+
return tables;
|
|
213
|
+
};
|
|
169
214
|
/**
|
|
170
215
|
* Get nested tables from a table (recursively)
|
|
171
216
|
*/
|
|
@@ -174,7 +219,7 @@ function getNestedTables(table) {
|
|
|
174
219
|
for (const row of table.rows) for (const cell of row.cells) for (const content of cell.content) if (content.type === "table") {
|
|
175
220
|
tables.push(content);
|
|
176
221
|
tables.push(...getNestedTables(content));
|
|
177
|
-
}
|
|
222
|
+
} else if (content.type === "blockSdt") tables.push(...getTablesFromBlocks(content.content));
|
|
178
223
|
return tables;
|
|
179
224
|
}
|
|
180
225
|
/**
|
|
@@ -184,8 +229,14 @@ function getDocumentText(body) {
|
|
|
184
229
|
const lines = [];
|
|
185
230
|
for (const block of body.content) if (block.type === "paragraph") lines.push(getParagraphText(block));
|
|
186
231
|
else if (block.type === "table") lines.push(getTableText(block));
|
|
232
|
+
else if (block.type === "blockSdt") lines.push(getTextFromBlocks(block.content));
|
|
187
233
|
return lines.join("\n");
|
|
188
234
|
}
|
|
235
|
+
const getTextFromBlocks = (blocks) => blocks.flatMap((block) => {
|
|
236
|
+
if (block.type === "paragraph") return [getParagraphText(block)];
|
|
237
|
+
if (block.type === "table") return [getTableText(block)];
|
|
238
|
+
return block.type === "blockSdt" ? [getTextFromBlocks(block.content)] : [];
|
|
239
|
+
}).join("\n");
|
|
189
240
|
/**
|
|
190
241
|
* Get plain text from a table
|
|
191
242
|
*/
|
|
@@ -197,6 +248,7 @@ function getTableText(table) {
|
|
|
197
248
|
const cellTexts = [];
|
|
198
249
|
for (const content of cell.content) if (content.type === "paragraph") cellTexts.push(getParagraphText(content));
|
|
199
250
|
else if (content.type === "table") cellTexts.push(getTableText(content));
|
|
251
|
+
else if (content.type === "blockSdt") cellTexts.push(getTextFromBlocks(content.content));
|
|
200
252
|
rowTexts.push(cellTexts.join("\n"));
|
|
201
253
|
}
|
|
202
254
|
lines.push(rowTexts.join(" "));
|