@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.
Files changed (50) hide show
  1. package/dist/compare/inline-atoms.js +218 -31
  2. package/dist/compare/section-boundary-properties.js +1 -0
  3. package/dist/content-controls/mutateContentControls.js +1 -3
  4. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  5. package/dist/controller/headerFooterEditorManager.js +2 -1
  6. package/dist/controller/noteEditorManager.d.ts +2 -1
  7. package/dist/controller/noteEditorManager.js +2 -1
  8. package/dist/docx/commentRangeIntegrity.js +7 -0
  9. package/dist/docx/documentParser.js +55 -3
  10. package/dist/docx/inlineWrapperContent.d.ts +18 -16
  11. package/dist/docx/inlineWrapperContent.js +18 -16
  12. package/dist/docx/normalizeBaseDirection.js +4 -0
  13. package/dist/docx/rezip.js +1 -0
  14. package/dist/docx/selectiveSave.js +11 -2
  15. package/dist/docx/serializer/documentSerializer.js +16 -1
  16. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  17. package/dist/docx/serializer/tableSerializer.js +5 -2
  18. package/dist/docx/server/createBilingualDocument.js +23 -14
  19. package/dist/docx/tableParser.js +101 -75
  20. package/dist/markdown/renderRuns.js +4 -0
  21. package/dist/markdown/renderTable.js +46 -12
  22. package/dist/prosemirror/attrs/index.d.ts +3 -2
  23. package/dist/prosemirror/attrs/index.js +85 -4
  24. package/dist/prosemirror/commands/comments.js +61 -3
  25. package/dist/prosemirror/commentReferenceAttrs.js +2 -2
  26. package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
  27. package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
  28. package/dist/prosemirror/conversion/fromProseDoc.js +177 -51
  29. package/dist/prosemirror/conversion/toProseDoc.js +108 -55
  30. package/dist/prosemirror/extensions/markRegistry.d.ts +5 -8
  31. package/dist/prosemirror/extensions/markRegistry.js +5 -8
  32. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
  33. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
  34. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +2 -1
  35. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +32 -7
  36. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
  37. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
  38. package/dist/prosemirror/runIdentity.d.ts +5 -5
  39. package/dist/prosemirror/runIdentity.js +5 -5
  40. package/dist/prosemirror/schema/index.d.ts +2 -2
  41. package/dist/prosemirror/schema/marks.d.ts +28 -2
  42. package/dist/prosemirror/schema/nodes.d.ts +6 -4
  43. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  44. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  45. package/dist/prosemirror/utils/extractTrackedChanges.js +21 -6
  46. package/dist/prosemirror/validation.js +48 -7
  47. package/dist/types/content.d.ts +2 -2
  48. package/dist/types/index.d.ts +3 -1
  49. package/dist/utils/mergeDocumentContent.js +1 -7
  50. 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: "omitted" });
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(`${offset}:${child.type.name}:${canonicalJson(documentFactAttrs(child))}`);
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(`${offset}:${child.type.name}:${canonicalJson(documentFactAttrs(child))}`);
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) => left.node.type === right.node.type && left.cleanText === right.cleanText && canonicalJson(left.unsupportedTopology) === canonicalJson(right.unsupportedTopology);
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: "omitted" });
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
- for (const [index, live] of liveBlocks.entries()) {
280
- const target = targetBlocks[index];
281
- if (!target) return { status: "unalignable" };
282
- if (live.supported.length === 0 && target.supported.length === 0) continue;
283
- if (!sameBlockTopology(live, target)) return { status: "unalignable" };
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
- actions.push({
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
- actions.push({
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: actions.length
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 && sameBlockTopology(left, right) && canonicalJson(left.supported.map(({ offset, key }) => ({
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: cellContent
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 = new EditorView(mountNode, {
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 = new EditorView(mountNode, {
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 { collectXmlnsDeclarations, findChild, getLocalName, parseXml } from "./xmlParser.js";
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(" "));