@stll/folio-core 0.47.3 → 0.47.5

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 (36) hide show
  1. package/dist/ai-edits/headless.js +46 -11
  2. package/dist/compare/inline-atoms.js +218 -31
  3. package/dist/compare/section-boundary-properties.js +1 -0
  4. package/dist/content-controls/mutateContentControls.js +1 -3
  5. package/dist/docx/commentRangeIntegrity.js +7 -0
  6. package/dist/docx/documentParser.js +55 -3
  7. package/dist/docx/inlineWrapperContent.d.ts +18 -16
  8. package/dist/docx/inlineWrapperContent.js +18 -16
  9. package/dist/docx/normalizeBaseDirection.js +4 -0
  10. package/dist/docx/rezip.js +1 -0
  11. package/dist/docx/serializer/documentSerializer.js +16 -1
  12. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  13. package/dist/docx/serializer/tableSerializer.js +5 -2
  14. package/dist/docx/server/createBilingualDocument.js +23 -14
  15. package/dist/docx/tableParser.js +101 -75
  16. package/dist/markdown/renderRuns.js +4 -0
  17. package/dist/markdown/renderTable.js +46 -12
  18. package/dist/prosemirror/attrs/index.js +67 -3
  19. package/dist/prosemirror/commentReferenceAttrs.js +2 -2
  20. package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
  21. package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
  22. package/dist/prosemirror/conversion/fromProseDoc.js +74 -18
  23. package/dist/prosemirror/conversion/toProseDoc.js +75 -42
  24. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
  25. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
  26. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
  27. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
  28. package/dist/prosemirror/schema/marks.d.ts +11 -0
  29. package/dist/prosemirror/schema/nodes.d.ts +6 -4
  30. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  31. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  32. package/dist/prosemirror/validation.js +48 -7
  33. package/dist/types/content.d.ts +2 -2
  34. package/dist/types/index.d.ts +3 -1
  35. package/dist/utils/mergeDocumentContent.js +1 -7
  36. package/package.json +2 -2
@@ -3,17 +3,17 @@ import { document_d_exports } from "../types/document.js";
3
3
  /**
4
4
  * What a run-level tracked-change wrapper keeps inside itself (CT_RunTrackChange).
5
5
  *
6
- * Runs, links, fields, bookmark boundaries, equations, nested revisions and
7
- * markup folio does not model.
6
+ * Runs, links, fields, bookmark and move-range boundaries, equations, nested
7
+ * revisions and markup folio does not model.
8
8
  *
9
9
  * The transparent wrappers are admitted too: `w:bdo` / `w:dir` state how their
10
10
  * content is laid out, and `w:sdt` states what the content is bound to.
11
11
  * Neither says anything about the revision, so lifting one out of a revision
12
12
  * takes its text out of the revision with it.
13
13
  *
14
- * The range markers are not: a `w:commentRangeStart` or `w:moveFromRangeStart`
15
- * inside a revision is a marker the revision does not own, and the pairing
16
- * passes read it as a paragraph-level sibling.
14
+ * Move-range markers stay inside the revision that owns their authored
15
+ * position. Comment ranges remain paragraph-level because the editor projects
16
+ * them as marks over content rather than as boundary atoms.
17
17
  */
18
18
  declare const TRACKED_CHANGE_WRAPPER_CONTENT: {
19
19
  readonly inlineWrapper: true;
@@ -33,10 +33,10 @@ declare const TRACKED_CHANGE_WRAPPER_CONTENT: {
33
33
  readonly commentRangeEnd: false;
34
34
  readonly commentRangeStart: false;
35
35
  readonly commentReference: false;
36
- readonly moveFromRangeEnd: false;
37
- readonly moveFromRangeStart: false;
38
- readonly moveToRangeEnd: false;
39
- readonly moveToRangeStart: false;
36
+ readonly moveFromRangeEnd: true;
37
+ readonly moveFromRangeStart: true;
38
+ readonly moveToRangeEnd: true;
39
+ readonly moveToRangeStart: true;
40
40
  };
41
41
  /**
42
42
  * What an inline content control keeps inside `<w:sdtContent>`.
@@ -54,9 +54,11 @@ declare const TRACKED_CHANGE_WRAPPER_CONTENT: {
54
54
  * control's content comes back covering the control plus whatever follows, so
55
55
  * a `REF` field or a link to it resolves to the wrong text.
56
56
  *
57
- * The other range markers are not: a `w:commentRangeStart` or a
58
- * `w:moveFromRangeStart` inside the control is a marker the control does not
59
- * own, and the pairing passes read it as a paragraph-level sibling.
57
+ * Move-range boundaries stay where the source put them. The editor has
58
+ * zero-width boundary nodes for them, so lifting one beside the control would
59
+ * change which content the tracked move covers without any representational
60
+ * need. Comment ranges remain paragraph-level because the editor projects
61
+ * them as marks shared across the whole story rather than boundary atoms.
60
62
  */
61
63
  declare const INLINE_SDT_CONTENT: {
62
64
  readonly inlineWrapper: true;
@@ -76,10 +78,10 @@ declare const INLINE_SDT_CONTENT: {
76
78
  readonly commentRangeEnd: false;
77
79
  readonly commentRangeStart: false;
78
80
  readonly commentReference: false;
79
- readonly moveFromRangeEnd: false;
80
- readonly moveFromRangeStart: false;
81
- readonly moveToRangeEnd: false;
82
- readonly moveToRangeStart: false;
81
+ readonly moveFromRangeEnd: true;
82
+ readonly moveFromRangeStart: true;
83
+ readonly moveToRangeEnd: true;
84
+ readonly moveToRangeStart: true;
83
85
  };
84
86
  /**
85
87
  * What a `w:hyperlink` keeps inside itself (CT_Hyperlink).
@@ -2,17 +2,17 @@
2
2
  /**
3
3
  * What a run-level tracked-change wrapper keeps inside itself (CT_RunTrackChange).
4
4
  *
5
- * Runs, links, fields, bookmark boundaries, equations, nested revisions and
6
- * markup folio does not model.
5
+ * Runs, links, fields, bookmark and move-range boundaries, equations, nested
6
+ * revisions and markup folio does not model.
7
7
  *
8
8
  * The transparent wrappers are admitted too: `w:bdo` / `w:dir` state how their
9
9
  * content is laid out, and `w:sdt` states what the content is bound to.
10
10
  * Neither says anything about the revision, so lifting one out of a revision
11
11
  * takes its text out of the revision with it.
12
12
  *
13
- * The range markers are not: a `w:commentRangeStart` or `w:moveFromRangeStart`
14
- * inside a revision is a marker the revision does not own, and the pairing
15
- * passes read it as a paragraph-level sibling.
13
+ * Move-range markers stay inside the revision that owns their authored
14
+ * position. Comment ranges remain paragraph-level because the editor projects
15
+ * them as marks over content rather than as boundary atoms.
16
16
  */
17
17
  const TRACKED_CHANGE_WRAPPER_CONTENT = {
18
18
  inlineWrapper: true,
@@ -32,10 +32,10 @@ const TRACKED_CHANGE_WRAPPER_CONTENT = {
32
32
  commentRangeEnd: false,
33
33
  commentRangeStart: false,
34
34
  commentReference: false,
35
- moveFromRangeEnd: false,
36
- moveFromRangeStart: false,
37
- moveToRangeEnd: false,
38
- moveToRangeStart: false
35
+ moveFromRangeEnd: true,
36
+ moveFromRangeStart: true,
37
+ moveToRangeEnd: true,
38
+ moveToRangeStart: true
39
39
  };
40
40
  /**
41
41
  * What an inline content control keeps inside `<w:sdtContent>`.
@@ -53,9 +53,11 @@ const TRACKED_CHANGE_WRAPPER_CONTENT = {
53
53
  * control's content comes back covering the control plus whatever follows, so
54
54
  * a `REF` field or a link to it resolves to the wrong text.
55
55
  *
56
- * The other range markers are not: a `w:commentRangeStart` or a
57
- * `w:moveFromRangeStart` inside the control is a marker the control does not
58
- * own, and the pairing passes read it as a paragraph-level sibling.
56
+ * Move-range boundaries stay where the source put them. The editor has
57
+ * zero-width boundary nodes for them, so lifting one beside the control would
58
+ * change which content the tracked move covers without any representational
59
+ * need. Comment ranges remain paragraph-level because the editor projects
60
+ * them as marks shared across the whole story rather than boundary atoms.
59
61
  */
60
62
  const INLINE_SDT_CONTENT = {
61
63
  inlineWrapper: true,
@@ -75,10 +77,10 @@ const INLINE_SDT_CONTENT = {
75
77
  commentRangeEnd: false,
76
78
  commentRangeStart: false,
77
79
  commentReference: false,
78
- moveFromRangeEnd: false,
79
- moveFromRangeStart: false,
80
- moveToRangeEnd: false,
81
- moveToRangeStart: false
80
+ moveFromRangeEnd: true,
81
+ moveFromRangeStart: true,
82
+ moveToRangeEnd: true,
83
+ moveToRangeStart: true
82
84
  };
83
85
  /**
84
86
  * What a `w:hyperlink` keeps inside itself (CT_Hyperlink).
@@ -52,6 +52,10 @@ const normalizeCellBlock = (block, styles) => {
52
52
  switch (block.type) {
53
53
  case "paragraph": return normalizeParagraph(block, styles);
54
54
  case "table": return normalizeTable(block, styles);
55
+ case "blockSdt": return {
56
+ ...block,
57
+ content: block.content.map((child) => normalizeCellBlock(child, styles))
58
+ };
55
59
  case "preservedBlock":
56
60
  case "bookmarkStart":
57
61
  case "bookmarkEnd": return block;
@@ -404,6 +404,7 @@ function collectNewImages(blocks) {
404
404
  for (const sub of item.content) if (sub.type === "run") collectFromRun(sub);
405
405
  }
406
406
  } else if (block.type === "table") for (const row of block.rows) for (const cell of row.cells) images.push(...collectNewImages(cell.content));
407
+ else if (block.type === "blockSdt") images.push(...collectNewImages(block.content));
407
408
  return images;
408
409
  }
409
410
  /** Map MIME type to file extension (inverse of getContentTypeForExtension) */
@@ -5,6 +5,8 @@ import { serializePartElement } from "./partNamespaces.js";
5
5
  import { resetAutoIdCounter } from "./runSerializer.js";
6
6
  import { serializeSectionProperties } from "./sectionPropertiesSerializer.js";
7
7
  import { serializeTable } from "./tableSerializer.js";
8
+ import { escapeXmlAttribute } from "@stll/docx-core";
9
+ import { themeColorToken } from "@stll/docx-core/model";
8
10
  //#region src/docx/serializer/documentSerializer.ts
9
11
  /**
10
12
  * Prefixes document.xml declares whether or not the body uses them.
@@ -55,6 +57,19 @@ function serializeBlockContent(block) {
55
57
  function serializeBodyContent(content) {
56
58
  return content.map((block) => serializeBlockContent(block)).join("");
57
59
  }
60
+ /** Serialize the optional page background that precedes `w:body`. */
61
+ function serializeDocumentBackground(background) {
62
+ if (background === void 0) return "";
63
+ const attributes = [];
64
+ if (background.color?.auto) attributes.push("w:color=\"auto\"");
65
+ else if (background.color?.rgb !== void 0) attributes.push(`w:color="${escapeXmlAttribute(background.color.rgb)}"`);
66
+ if (background.themeColor !== void 0) attributes.push(`w:themeColor="${escapeXmlAttribute(themeColorToken(background.themeColor))}"`);
67
+ if (background.themeTint !== void 0) attributes.push(`w:themeTint="${escapeXmlAttribute(background.themeTint)}"`);
68
+ if (background.themeShade !== void 0) attributes.push(`w:themeShade="${escapeXmlAttribute(background.themeShade)}"`);
69
+ const attributeText = attributes.length === 0 ? "" : ` ${attributes.join(" ")}`;
70
+ const drawing = background.drawing?.rawXml;
71
+ return drawing === void 0 ? `<w:background${attributeText}/>` : `<w:background${attributeText}>${drawing}</w:background>`;
72
+ }
58
73
  /**
59
74
  * Serialize a DocumentBody to document.xml body content
60
75
  *
@@ -77,7 +92,7 @@ function serializeDocumentBody(body) {
77
92
  */
78
93
  function serializeDocument(doc, sourceBindings) {
79
94
  resetAutoIdCounter();
80
- const body = `<w:body>${serializeDocumentBody(doc.package.document)}</w:body>`;
95
+ const body = serializeDocumentBackground(doc.package.document.background) + `<w:body>${serializeDocumentBody(doc.package.document)}</w:body>`;
81
96
  return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + serializePartElement({
82
97
  partPath: "word/document.xml",
83
98
  rootName: "w:document",
@@ -358,6 +358,10 @@ function serializeInlineSdt(sdt, disposition = "kept") {
358
358
  case "inlineWrapper": return serializeParagraphContent(item, disposition);
359
359
  case "bookmarkStart": return serializeBookmarkMarker(item);
360
360
  case "bookmarkEnd": return serializeBookmarkMarker(item);
361
+ case "moveFromRangeStart": return serializeMoveRangeStart("moveFromRangeStart", item);
362
+ case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
363
+ case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", item);
364
+ case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
361
365
  case "insertion": return serializeTrackedChange("ins", item);
362
366
  case "deletion": return serializeTrackedChange("del", item);
363
367
  case "moveFrom": return serializeTrackedChange("moveFrom", item);
@@ -420,6 +424,10 @@ function serializeTrackedChange(tag, change) {
420
424
  case "moveTo": return serializeTrackedChange(trackedChangeTag(item), item);
421
425
  case "bookmarkStart":
422
426
  case "bookmarkEnd": return serializeBookmarkMarker(item);
427
+ case "moveFromRangeStart": return serializeMoveRangeStart("moveFromRangeStart", item);
428
+ case "moveFromRangeEnd": return `<w:moveFromRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
429
+ case "moveToRangeStart": return serializeMoveRangeStart("moveToRangeStart", item);
430
+ case "moveToRangeEnd": return `<w:moveToRangeEnd ${markupRangeAttributes(item).join(" ")}/>`;
423
431
  case "preservedInline": return item.xml;
424
432
  case "inlineWrapper":
425
433
  case "inlineSdt": return serializeParagraphContent(item, disposition);
@@ -6,6 +6,7 @@ import { TRANSITIONAL_NAME_BY_STRICT_NAME } from "../strictNames.gen.js";
6
6
  import { TABLE_LOOK_FLAGS } from "../tableLook.js";
7
7
  import { parseTableCellProperties, parseTableGrid, parseTableProperties, parseTablePropertyExceptions, parseTableRowProperties } from "../tableParser.js";
8
8
  import { OOXML_NAMESPACE_SCOPE, parseXml } from "../xmlParser.js";
9
+ import { serializeBlockSdt } from "./blockSdtSerializer.js";
9
10
  import { serializeBorder } from "./borderSerializer.js";
10
11
  import { serializeBookmarkMarker } from "./markupRangeAttributes.js";
11
12
  import { serializeSdtWrapper } from "./sdtPropertiesSerializer.js";
@@ -511,16 +512,18 @@ const preservedChild = (xml, child) => ({
511
512
  * Serialize cell content (paragraphs, nested tables)
512
513
  */
513
514
  function serializeCellContent(content, serializeParagraph) {
514
- const parts = content.map((block) => {
515
+ const serializeCellBlock = (block) => {
515
516
  switch (block.type) {
516
517
  case "paragraph": return serializeParagraph(block);
517
518
  case "table": return serializeTable(block, serializeParagraph);
519
+ case "blockSdt": return serializeBlockSdt(block, serializeCellBlock);
518
520
  case "preservedBlock": return block.xml;
519
521
  case "bookmarkStart":
520
522
  case "bookmarkEnd": return serializeBookmarkMarker(block);
521
523
  default: return block;
522
524
  }
523
- });
525
+ };
526
+ const parts = content.map(serializeCellBlock);
524
527
  if (parts.length === 0) parts.push("<w:p/>");
525
528
  return parts.join("");
526
529
  }
@@ -464,30 +464,39 @@ const remapListRendering = (rendering, cloner) => {
464
464
  };
465
465
  const collectTableParagraphs = (table) => {
466
466
  const out = [];
467
- for (const row of table.rows) for (const cell of row.cells) for (const item of cell.content) if (item.type === "paragraph") out.push(item);
468
- else if (item.type === "table") out.push(...collectTableParagraphs(item));
467
+ const collectBlocks = (blocks) => {
468
+ for (const item of blocks) if (item.type === "paragraph") out.push(item);
469
+ else if (item.type === "table") out.push(...collectTableParagraphs(item));
470
+ else if (item.type === "blockSdt") collectBlocks(item.content);
471
+ };
472
+ for (const row of table.rows) for (const cell of row.cells) collectBlocks(cell.content);
469
473
  return out;
470
474
  };
471
475
  const cloneTableForTarget = ({ table, editableParagraphIds, paraIds, styleCloner, cloner, bookmarkIds }) => {
472
476
  const paragraphs = [];
477
+ const cloneBlocks = (blocks) => blocks.map((item) => {
478
+ if (item.type === "table") return cloneTable(item);
479
+ if (item.type === "blockSdt") return {
480
+ ...structuredClone(item),
481
+ content: cloneBlocks(item.content)
482
+ };
483
+ if (item.type !== "paragraph") return structuredClone(item);
484
+ const targetParaId = paraIds.mint(item.paraId);
485
+ const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner, bookmarkIds);
486
+ if (isTranslatableParagraph(item, editableParagraphIds)) paragraphs.push({
487
+ sourceParaId: item.paraId,
488
+ targetParaId,
489
+ sourceText: getParagraphText(item)
490
+ });
491
+ return copy;
492
+ });
473
493
  const cloneTable = (source) => ({
474
494
  ...structuredClone(source),
475
495
  rows: source.rows.map((row) => ({
476
496
  ...structuredClone(row),
477
497
  cells: row.cells.map((cell) => ({
478
498
  ...structuredClone(cell),
479
- content: cell.content.map((item) => {
480
- if (item.type === "table") return cloneTable(item);
481
- if (item.type !== "paragraph") return structuredClone(item);
482
- const targetParaId = paraIds.mint(item.paraId);
483
- const copy = cloneParagraphForTarget(item, targetParaId, styleCloner, cloner, bookmarkIds);
484
- if (isTranslatableParagraph(item, editableParagraphIds)) paragraphs.push({
485
- sourceParaId: item.paraId,
486
- targetParaId,
487
- sourceText: getParagraphText(item)
488
- });
489
- return copy;
490
- })
499
+ content: cloneBlocks(cell.content)
491
500
  }))
492
501
  }))
493
502
  });
@@ -766,81 +766,104 @@ function withContainerXmlns(options, element) {
766
766
  };
767
767
  }
768
768
  function parseCellContent(tcElement, styles, theme, numbering, rels, media, options) {
769
- const modelled = [];
770
- const captured = [];
771
- const dispatchCellChildren = (element, childOptions) => {
772
- const preserved = dispatchChildren({
773
- element,
774
- container: "block-content",
775
- capturePosition: () => modelled.length,
776
- undeclared: { AlternateContent: (child) => {
777
- const selectedBranch = selectAlternateContentBranch(child);
778
- if (!selectedBranch) return;
779
- dispatchCellChildren(selectedBranch, withContainerXmlns(withContainerXmlns(childOptions, child), selectedBranch));
780
- } },
781
- handlers: {
782
- p: (child) => {
783
- const para = parseParagraph(child, styles, theme, numbering, rels, media, {
784
- ...childOptions,
785
- runConsolidation: "deferred"
786
- });
787
- enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media, parseTable);
788
- modelled.push(para);
789
- },
790
- tbl: (child) => {
791
- const table = parseTable(child, styles, theme, numbering, rels, media, childOptions);
792
- if (!table) return;
793
- modelled.push(table);
794
- },
795
- sdt: (child) => {
796
- const sdtContent = findWordprocessingChild(child, "sdtContent");
797
- if (!sdtContent) return;
798
- dispatchCellChildren(sdtContent, withContainerXmlns(withContainerXmlns(childOptions, child), sdtContent));
799
- },
800
- bookmarkStart: (child) => {
801
- modelled.push(parseBookmarkStart(child));
802
- },
803
- bookmarkEnd: (child) => {
804
- modelled.push(parseBookmarkEnd(child));
805
- },
806
- tcPr: CELL_PROPERTIES_OWNER,
807
- sectPr: CAPTURE,
808
- altChunk: CAPTURE,
809
- commentRangeEnd: CAPTURE,
810
- commentRangeStart: CAPTURE,
811
- customXml: CAPTURE,
812
- customXmlDelRangeEnd: CAPTURE,
813
- customXmlDelRangeStart: CAPTURE,
814
- customXmlInsRangeEnd: CAPTURE,
815
- customXmlInsRangeStart: CAPTURE,
816
- customXmlMoveFromRangeEnd: CAPTURE,
817
- customXmlMoveFromRangeStart: CAPTURE,
818
- customXmlMoveToRangeEnd: CAPTURE,
819
- customXmlMoveToRangeStart: CAPTURE,
820
- del: CAPTURE,
821
- ins: CAPTURE,
822
- moveFrom: CAPTURE,
823
- moveFromRangeEnd: CAPTURE,
824
- moveFromRangeStart: CAPTURE,
825
- moveTo: CAPTURE,
826
- moveToRangeEnd: CAPTURE,
827
- moveToRangeStart: CAPTURE,
828
- permEnd: CAPTURE,
829
- permStart: CAPTURE,
830
- proofErr: CAPTURE
769
+ const findLastFlowBlock = (blocks) => {
770
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
771
+ const block = blocks[index];
772
+ if (block?.type === "paragraph" || block?.type === "table") return block;
773
+ if (block?.type === "blockSdt") {
774
+ const nested = findLastFlowBlock(block.content);
775
+ if (nested) return nested;
831
776
  }
832
- });
833
- if (preserved?.children) captured.push(...preserved.children);
777
+ }
834
778
  };
835
- dispatchCellChildren(tcElement, options);
836
- if (modelled.findLast((block) => block.type === "paragraph" || block.type === "table")?.type !== "paragraph") modelled.push({
837
- type: "paragraph",
838
- content: []
839
- });
840
- return withPreservedChildren(modelled, { children: captured }, (xml) => ({
841
- type: "preservedBlock",
842
- xml
843
- }));
779
+ const parseCellChildren = (element, childOptions, requireTrailingParagraph) => {
780
+ const modelled = [];
781
+ const captured = [];
782
+ const dispatchCellChildren = (childContainer, nestedOptions) => {
783
+ const preserved = dispatchChildren({
784
+ element: childContainer,
785
+ container: "block-content",
786
+ capturePosition: () => modelled.length,
787
+ undeclared: { AlternateContent: (child) => {
788
+ const selectedBranch = selectAlternateContentBranch(child);
789
+ if (!selectedBranch) return;
790
+ dispatchCellChildren(selectedBranch, withContainerXmlns(withContainerXmlns(nestedOptions, child), selectedBranch));
791
+ } },
792
+ handlers: {
793
+ p: (child) => {
794
+ const para = parseParagraph(child, styles, theme, numbering, rels, media, {
795
+ ...nestedOptions,
796
+ runConsolidation: "deferred"
797
+ });
798
+ enrichParagraphTextBoxes(para, child, styles, theme, numbering, rels, media, parseTable);
799
+ modelled.push(para);
800
+ },
801
+ tbl: (child) => {
802
+ const table = parseTable(child, styles, theme, numbering, rels, media, nestedOptions);
803
+ if (!table) return;
804
+ modelled.push(table);
805
+ },
806
+ sdt: (child) => {
807
+ const properties = parseSdtProperties(findWordprocessingChild(child, "sdtPr"), findWordprocessingChild(child, "sdtEndPr"));
808
+ const siblings = captureSdtSiblingMarkers(child);
809
+ if (siblings.before.length > 0) properties.rawSdtChildrenBeforeContent = siblings.before;
810
+ if (siblings.after.length > 0) properties.rawSdtChildrenAfterContent = siblings.after;
811
+ const sdtContent = findWordprocessingChild(child, "sdtContent");
812
+ if (!sdtContent) return CAPTURE;
813
+ modelled.push({
814
+ type: "blockSdt",
815
+ properties,
816
+ content: parseCellChildren(sdtContent, withContainerXmlns(withContainerXmlns(nestedOptions, child), sdtContent), false)
817
+ });
818
+ },
819
+ bookmarkStart: (child) => {
820
+ modelled.push(parseBookmarkStart(child));
821
+ },
822
+ bookmarkEnd: (child) => {
823
+ modelled.push(parseBookmarkEnd(child));
824
+ },
825
+ tcPr: CELL_PROPERTIES_OWNER,
826
+ sectPr: CAPTURE,
827
+ altChunk: CAPTURE,
828
+ commentRangeEnd: CAPTURE,
829
+ commentRangeStart: CAPTURE,
830
+ customXml: CAPTURE,
831
+ customXmlDelRangeEnd: CAPTURE,
832
+ customXmlDelRangeStart: CAPTURE,
833
+ customXmlInsRangeEnd: CAPTURE,
834
+ customXmlInsRangeStart: CAPTURE,
835
+ customXmlMoveFromRangeEnd: CAPTURE,
836
+ customXmlMoveFromRangeStart: CAPTURE,
837
+ customXmlMoveToRangeEnd: CAPTURE,
838
+ customXmlMoveToRangeStart: CAPTURE,
839
+ del: CAPTURE,
840
+ ins: CAPTURE,
841
+ moveFrom: CAPTURE,
842
+ moveFromRangeEnd: CAPTURE,
843
+ moveFromRangeStart: CAPTURE,
844
+ moveTo: CAPTURE,
845
+ moveToRangeEnd: CAPTURE,
846
+ moveToRangeStart: CAPTURE,
847
+ permEnd: CAPTURE,
848
+ permStart: CAPTURE,
849
+ proofErr: CAPTURE
850
+ }
851
+ });
852
+ if (preserved?.children) captured.push(...preserved.children);
853
+ };
854
+ dispatchCellChildren(element, childOptions);
855
+ if (requireTrailingParagraph) {
856
+ if (findLastFlowBlock(modelled)?.type !== "paragraph") modelled.push({
857
+ type: "paragraph",
858
+ content: []
859
+ });
860
+ }
861
+ return withPreservedChildren(modelled, { children: captured }, (xml) => ({
862
+ type: "preservedBlock",
863
+ xml
864
+ }));
865
+ };
866
+ return parseCellChildren(tcElement, options, true);
844
867
  }
845
868
  /**
846
869
  * Parse a table cell (w:tc)
@@ -993,12 +1016,15 @@ function parseTableRow(trElement, styles, theme, numbering, rels, media, options
993
1016
  function parseTableGrid(tblGridElement) {
994
1017
  if (!tblGridElement) return;
995
1018
  const widths = [];
1019
+ let hasExplicitZero = false;
996
1020
  const gridCols = findChildren(tblGridElement, "w", "gridCol");
997
1021
  for (const col of gridCols) {
998
- const width = parseNumericAttribute(col, "w", "w") ?? 0;
1022
+ const statedWidth = parseNumericAttribute(col, "w", "w");
1023
+ hasExplicitZero ||= statedWidth === 0;
1024
+ const width = statedWidth ?? 0;
999
1025
  widths.push(width);
1000
1026
  }
1001
- if (widths.length > 0 && widths.every((width) => width <= 0)) return;
1027
+ if (widths.length > 0 && widths.every((width) => width <= 0) && !hasExplicitZero) return;
1002
1028
  return widths.length > 0 ? widths : void 0;
1003
1029
  }
1004
1030
  /**
@@ -165,6 +165,10 @@ function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
165
165
  case "complexField": return renderParagraphInline(ctx, pkg, [child], paraId);
166
166
  case "bookmarkStart":
167
167
  case "bookmarkEnd":
168
+ case "moveFromRangeStart":
169
+ case "moveFromRangeEnd":
170
+ case "moveToRangeStart":
171
+ case "moveToRangeEnd":
168
172
  case "preservedInline": return "";
169
173
  default: return child;
170
174
  }
@@ -17,10 +17,11 @@ function needsHtmlFallback(rows) {
17
17
  for (const row of rows) for (const cell of row.cells) {
18
18
  if ((cell.formatting?.gridSpan ?? 1) > 1) return true;
19
19
  if (cell.formatting?.vMerge) return true;
20
- if (cell.content.some((b) => b.type === "table")) return true;
20
+ if (containsTable(cell.content)) return true;
21
21
  }
22
22
  return false;
23
23
  }
24
+ const containsTable = (blocks) => blocks.some((block) => block.type === "table" || block.type === "blockSdt" && containsTable(block.content));
24
25
  function renderGfmTable(ctx, pkg, rows, firstRowIsHeader) {
25
26
  const cellTexts = rows.map((row) => renderGfmRow(ctx, pkg, row));
26
27
  let maxCols = 0;
@@ -55,10 +56,24 @@ function renderGfmRow(ctx, pkg, row) {
55
56
  }
56
57
  function renderGfmCell(ctx, pkg, cell) {
57
58
  const blocks = [];
58
- for (const item of cell.content) if (item.type === "paragraph") {
59
- const md = renderParagraph(ctx, pkg, item);
60
- if (md.trim()) blocks.push(md);
61
- }
59
+ const renderBlocks = (content) => {
60
+ for (const item of content) switch (item.type) {
61
+ case "paragraph": {
62
+ const md = renderParagraph(ctx, pkg, item);
63
+ if (md.trim()) blocks.push(md);
64
+ break;
65
+ }
66
+ case "blockSdt":
67
+ renderBlocks(item.content);
68
+ break;
69
+ case "table":
70
+ case "preservedBlock":
71
+ case "bookmarkStart":
72
+ case "bookmarkEnd": break;
73
+ default: return item;
74
+ }
75
+ };
76
+ renderBlocks(cell.content);
62
77
  return escapeTableCell(blocks.join("\n"));
63
78
  }
64
79
  function renderHtmlTable(ctx, pkg, rows, firstRowIsHeader) {
@@ -111,13 +126,28 @@ function cellAtGridColumn(row, gridCol) {
111
126
  }
112
127
  function renderHtmlCell(ctx, pkg, cell) {
113
128
  const parts = [];
114
- for (const item of cell.content) if (item.type === "paragraph") {
115
- const inner = renderHtmlInline(ctx, pkg, item.content, item.paraId);
116
- if (inner) parts.push(inner);
117
- } else if (item.type === "table") {
118
- const nested = renderHtmlTable(ctx, pkg, item.rows, true);
119
- if (nested) parts.push(nested);
120
- }
129
+ const renderBlocks = (content) => {
130
+ for (const item of content) switch (item.type) {
131
+ case "paragraph": {
132
+ const inner = renderHtmlInline(ctx, pkg, item.content, item.paraId);
133
+ if (inner) parts.push(inner);
134
+ break;
135
+ }
136
+ case "table": {
137
+ const nested = renderHtmlTable(ctx, pkg, item.rows, true);
138
+ if (nested) parts.push(nested);
139
+ break;
140
+ }
141
+ case "blockSdt":
142
+ renderBlocks(item.content);
143
+ break;
144
+ case "preservedBlock":
145
+ case "bookmarkStart":
146
+ case "bookmarkEnd": break;
147
+ default: return item;
148
+ }
149
+ };
150
+ renderBlocks(cell.content);
121
151
  return parts.join("<br>");
122
152
  }
123
153
  function escapeHtml(text) {
@@ -175,6 +205,10 @@ function renderHtmlChildren(ctx, pkg, children, paraId) {
175
205
  case "moveTo": return renderHtmlInline(ctx, pkg, [child], paraId);
176
206
  case "bookmarkStart":
177
207
  case "bookmarkEnd":
208
+ case "moveFromRangeStart":
209
+ case "moveFromRangeEnd":
210
+ case "moveToRangeStart":
211
+ case "moveToRangeEnd":
178
212
  case "preservedInline": return "";
179
213
  default: return child;
180
214
  }