@stll/folio-core 0.33.1 → 0.34.0

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 (121) hide show
  1. package/dist/ai-edits/apply.d.ts +15 -0
  2. package/dist/ai-edits/apply.js +271 -48
  3. package/dist/ai-edits/headless.d.ts +47 -3
  4. package/dist/ai-edits/headless.js +53 -3
  5. package/dist/ai-edits/index.d.ts +1 -1
  6. package/dist/ai-edits/snapshot.d.ts +19 -1
  7. package/dist/ai-edits/snapshot.js +49 -5
  8. package/dist/ai-edits/table-geometry.d.ts +56 -0
  9. package/dist/ai-edits/table-geometry.js +217 -0
  10. package/dist/ai-edits/table-row-column-mutations.d.ts +34 -16
  11. package/dist/ai-edits/table-row-column-mutations.js +12 -4
  12. package/dist/ai-edits/table-template.d.ts +60 -0
  13. package/dist/ai-edits/table-template.js +176 -0
  14. package/dist/ai-edits/types.d.ts +13 -6
  15. package/dist/compare/__fixtures__/body-sequence.d.ts +61 -2
  16. package/dist/compare/__fixtures__/body-sequence.js +94 -6
  17. package/dist/compare/compare.d.ts +14 -7
  18. package/dist/compare/compare.js +136 -38
  19. package/dist/compare/formatting.d.ts +1 -1
  20. package/dist/compare/formatting.js +38 -9
  21. package/dist/compare/plan.d.ts +31 -1
  22. package/dist/compare/plan.js +233 -66
  23. package/dist/compare/types.d.ts +9 -7
  24. package/dist/compare/types.js +7 -5
  25. package/dist/compare/verification.d.ts +31 -12
  26. package/dist/compare/verification.js +106 -18
  27. package/dist/compat/eigenpal.d.ts +4 -4
  28. package/dist/compat/eigenpal.js +2 -2
  29. package/dist/controller/headerFooterEditorManager.js +11 -9
  30. package/dist/controller/layoutPipeline.js +24 -3
  31. package/dist/display-list/build/watermarkPrimitives.js +15 -3
  32. package/dist/display-list/primitives.d.ts +1 -1
  33. package/dist/document-operations.d.ts +10 -1
  34. package/dist/document-operations.js +33 -5
  35. package/dist/docx/appVersionNormalization.d.ts +49 -0
  36. package/dist/docx/appVersionNormalization.js +74 -0
  37. package/dist/docx/blockContentParser.js +3 -2
  38. package/dist/docx/drawingUtils.js +4 -3
  39. package/dist/docx/headerFooterParser.js +8 -14
  40. package/dist/docx/paragraphParser.js +48 -2
  41. package/dist/docx/rezip.d.ts +10 -4
  42. package/dist/docx/rezip.js +54 -18
  43. package/dist/docx/runParser.js +9 -8
  44. package/dist/docx/sdtProperties.js +4 -3
  45. package/dist/docx/selectiveSave.js +7 -6
  46. package/dist/docx/serializer/commentSerializer.d.ts +1 -1
  47. package/dist/docx/serializer/commentSerializer.js +43 -19
  48. package/dist/docx/serializer/documentSerializer.d.ts +3 -1
  49. package/dist/docx/serializer/documentSerializer.js +42 -78
  50. package/dist/docx/serializer/fontTableSerializer.js +8 -5
  51. package/dist/docx/serializer/headerFooterSerializer.d.ts +4 -1
  52. package/dist/docx/serializer/headerFooterSerializer.js +55 -31
  53. package/dist/docx/serializer/noteSerializer.js +34 -30
  54. package/dist/docx/serializer/numberingSerializer.js +8 -4
  55. package/dist/docx/serializer/paragraphSerializer.d.ts +1 -1
  56. package/dist/docx/serializer/paragraphSerializer.js +22 -12
  57. package/dist/docx/serializer/partNamespaces.d.ts +78 -0
  58. package/dist/docx/serializer/partNamespaces.js +324 -0
  59. package/dist/docx/serializer/runSerializer.js +1 -1
  60. package/dist/docx/serializer/settingsSerializer.js +8 -2
  61. package/dist/docx/serializer/stylesSerializer.js +8 -5
  62. package/dist/docx/serializer/tableSerializer.js +83 -18
  63. package/dist/docx/serializer/themeSerializer.js +10 -2
  64. package/dist/docx/server/build.d.ts +1 -1
  65. package/dist/docx/settingsParser.js +3 -0
  66. package/dist/docx/strictValueEncodings.gen.d.ts +15 -0
  67. package/dist/docx/strictValueEncodings.gen.js +275 -0
  68. package/dist/docx/tableParser.d.ts +0 -6
  69. package/dist/docx/tableParser.js +52 -7
  70. package/dist/docx/transitionalSpelling.d.ts +23 -0
  71. package/dist/docx/transitionalSpelling.js +36 -0
  72. package/dist/docx/universalMeasure.d.ts +20 -0
  73. package/dist/docx/universalMeasure.js +32 -0
  74. package/dist/docx/verbatimCapture.d.ts +20 -0
  75. package/dist/docx/verbatimCapture.js +131 -0
  76. package/dist/docx/vmlImageParser.js +4 -3
  77. package/dist/docx/watermarkParser.d.ts +2 -4
  78. package/dist/docx/watermarkParser.js +8 -9
  79. package/dist/docx/xmlParser.d.ts +23 -2
  80. package/dist/docx/xmlParser.js +43 -11
  81. package/dist/headless-layout.js +14 -2
  82. package/dist/index.d.ts +4 -4
  83. package/dist/index.js +2 -2
  84. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -2
  85. package/dist/layout-bridge/convert/footnoteLayout.js +23 -10
  86. package/dist/layout-bridge/convert/headerFooterLayout.js +13 -2
  87. package/dist/layout-bridge/convert/toFlowBlocks.js +96 -13
  88. package/dist/layout-engine/index.js +11 -4
  89. package/dist/layout-engine/justifiedLineFit.d.ts +4 -4
  90. package/dist/layout-engine/justifiedLineFit.js +4 -4
  91. package/dist/layout-engine/measure/lineBreakProvider.js +1 -0
  92. package/dist/layout-engine/measure/measureBlocks.js +1 -1
  93. package/dist/layout-engine/measure/measureParagraph.js +29 -29
  94. package/dist/layout-painter/renderPage.js +6 -1
  95. package/dist/layout-painter/renderParagraph.js +18 -8
  96. package/dist/layout-painter/renderWatermark.js +11 -4
  97. package/dist/model.d.ts +3 -3
  98. package/dist/model.js +2 -2
  99. package/dist/prosemirror/attrs/index.js +11 -0
  100. package/dist/prosemirror/commands/comments.js +24 -3
  101. package/dist/prosemirror/containerFinalParagraph.d.ts +47 -0
  102. package/dist/prosemirror/containerFinalParagraph.js +92 -0
  103. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  104. package/dist/prosemirror/conversion/fromProseDoc.js +47 -9
  105. package/dist/prosemirror/conversion/toProseDoc.js +61 -14
  106. package/dist/prosemirror/extensions/core/DocExtension.js +7 -1
  107. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
  108. package/dist/prosemirror/extensions/marks/RunFormattingOverrideExtension.js +1 -0
  109. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -0
  110. package/dist/prosemirror/schema/marks.d.ts +3 -9
  111. package/dist/prosemirror/schema/nodes.d.ts +22 -0
  112. package/dist/server.d.ts +1 -1
  113. package/dist/types/block-id.d.ts +12 -1
  114. package/dist/types/block-id.js +17 -1
  115. package/dist/utils/canonicalJson.d.ts +14 -0
  116. package/dist/utils/canonicalJson.js +19 -0
  117. package/dist/utils/fontResolver.js +51 -0
  118. package/dist/utils/formatToStyle.d.ts +1 -1
  119. package/dist/utils/formatToStyle.js +41 -1
  120. package/dist/watermark/index.js +7 -0
  121. package/package.json +2 -2
@@ -0,0 +1,131 @@
1
+ import { TRANSITIONAL_NAMESPACE_BY_STRICT_URI } from "./strictValueEncodings.gen.js";
2
+ import { isStrictNamespaceUri, toTransitionalNamespaceUri, transitionalSlotEncoding } from "./transitionalSpelling.js";
3
+ import { roundHalfAwayFromZero, universalMeasureAs } from "./universalMeasure.js";
4
+ import { elementToXml, getLocalName } from "./xmlParser.js";
5
+ import { TaggedError } from "better-result";
6
+ //#region src/docx/verbatimCapture.ts
7
+ /**
8
+ * The one way to turn parsed markup into a string a rebuilt part replays.
9
+ *
10
+ * folio parses Strict and Transitional packages and rebuilds every package as
11
+ * Transitional. Modeled properties are re-serialized, so they come out
12
+ * Transitional whatever the source was; content the parser keeps verbatim —
13
+ * raw property XML, drawing and shape bodies, text-box markup, unmodeled
14
+ * extensions — does not, and a Strict fragment copied under a Transitional root
15
+ * changes meaning: the namespaces it declares stay Strict while the root is
16
+ * not, and its values are spelled the way Strict spells them (`155.85pt`,
17
+ * `20%`) where the Transitional attribute is a number.
18
+ *
19
+ * A fragment is therefore converted here, at capture, where its own namespace
20
+ * scope still says which conformance class produced it. Nothing downstream has
21
+ * to know: {@link captureVerbatimXml} is the only producer of replayed markup,
22
+ * and a fragment that carries no Strict namespace passes through byte for byte.
23
+ */
24
+ /** A Strict fragment uses a namespace that has no Transitional counterpart. */
25
+ var UntranslatableStrictNamespaceError = class extends TaggedError("UntranslatableStrictNamespaceError") {};
26
+ const TRANSITIONAL_URIS = new Set(TRANSITIONAL_NAMESPACE_BY_STRICT_URI.values());
27
+ /**
28
+ * Drawing extensions the repository has no schema for.
29
+ *
30
+ * `wp14:pctWidth` and its siblings are Microsoft vocabularies, but they are
31
+ * DrawingML by construction and DrawingML has one integer encoding for a
32
+ * percentage, so a Strict producer's `20%` there converts like `a:alpha`'s.
33
+ * Lengths are left alone: only an attribute's own type says what unit its
34
+ * number counts, and guessing one would corrupt the value silently.
35
+ */
36
+ const DRAWING_EXTENSION_MARKERS = [
37
+ "/wordprocessingCanvas",
38
+ "/wordprocessingDrawing",
39
+ "/wordprocessingGroup",
40
+ "/wordprocessingInk",
41
+ "/wordprocessingShape"
42
+ ];
43
+ const isUnschemadDrawingNamespace = (uri) => !TRANSITIONAL_URIS.has(uri) && DRAWING_EXTENSION_MARKERS.some((marker) => uri.includes(marker));
44
+ /** `-?12.5%`: the one shape ECMA-376 gives a percentage that carries its unit. */
45
+ const PERCENTAGE = /^(-?[0-9]+(?:\.[0-9]+)?)%$/u;
46
+ const NUMBERS_PER_PERCENT = {
47
+ fiftiethPercent: 50,
48
+ thousandthPercent: 1e3,
49
+ wholePercent: 1
50
+ };
51
+ /** The Transitional spelling of one Strict-produced value, or the value unchanged. */
52
+ const transitionalValue = (value, namespaceUri, encoding) => {
53
+ const measureUnit = encoding?.measure;
54
+ if (measureUnit !== void 0) {
55
+ const measure = universalMeasureAs(value, measureUnit);
56
+ if (measure !== void 0) return String(measure);
57
+ }
58
+ const percentage = PERCENTAGE.exec(value);
59
+ if (percentage === null) return value;
60
+ const percentUnit = encoding?.percent ?? (isUnschemadDrawingNamespace(namespaceUri) ? "thousandthPercent" : void 0);
61
+ if (percentUnit === void 0) return value;
62
+ return String(roundHalfAwayFromZero(Number(percentage[1]) * NUMBERS_PER_PERCENT[percentUnit]));
63
+ };
64
+ const originOf = (element, inherited) => {
65
+ const uri = element.namespaceUri;
66
+ if (uri === void 0) return inherited;
67
+ if (isStrictNamespaceUri(uri)) return "strict";
68
+ if (uri.startsWith("http://purl.oclc.org/ooxml/")) throw new UntranslatableStrictNamespaceError({
69
+ message: `Strict namespace ${uri} has no Transitional counterpart; folio will not replay it under a Transitional root.`,
70
+ uri
71
+ });
72
+ return TRANSITIONAL_URIS.has(uri) ? "transitional" : inherited;
73
+ };
74
+ const convertAttributes = (element, namespaceUri, localName) => {
75
+ const attributes = element.attributes ?? {};
76
+ let converted;
77
+ for (const [name, raw] of Object.entries(attributes)) {
78
+ if (typeof raw !== "string") continue;
79
+ const next = name === "xmlns" || name.startsWith("xmlns:") ? toTransitionalNamespaceUri(raw) : transitionalValue(raw, namespaceUri, transitionalSlotEncoding(element.namespaceUri, localName, getLocalName(name)));
80
+ if (next === raw) continue;
81
+ converted ??= { ...attributes };
82
+ converted[name] = next;
83
+ }
84
+ return converted;
85
+ };
86
+ const childWithValue = (child, text) => text === child.text ? child : {
87
+ ...child,
88
+ text
89
+ };
90
+ const toTransitional = (element, inherited) => {
91
+ if (element.type === "text") return element;
92
+ const origin = originOf(element, inherited);
93
+ const namespaceUri = element.namespaceUri === void 0 ? "" : toTransitionalNamespaceUri(element.namespaceUri);
94
+ const localName = getLocalName(element.name);
95
+ const textEncoding = transitionalSlotEncoding(element.namespaceUri, localName);
96
+ const attributes = origin === "strict" && element.attributes ? convertAttributes(element, namespaceUri, localName) : void 0;
97
+ let elements;
98
+ for (const [index, child] of (element.elements ?? []).entries()) {
99
+ const next = origin === "strict" && child.type === "text" && typeof child.text === "string" ? childWithValue(child, transitionalValue(child.text, namespaceUri, textEncoding)) : toTransitional(child, origin);
100
+ if (next === child) continue;
101
+ elements ??= [...element.elements ?? []];
102
+ elements[index] = next;
103
+ }
104
+ if (attributes === void 0 && elements === void 0) return element;
105
+ return {
106
+ ...element,
107
+ ...attributes === void 0 ? {} : { attributes },
108
+ ...elements === void 0 ? {} : { elements }
109
+ };
110
+ };
111
+ /** True when any binding in scope, including inherited ones, is a Strict namespace. */
112
+ const scopeDeclaresStrict = (scope) => {
113
+ for (let current = scope; current; current = current.parent) for (const uri of current.bindings.values()) if (uri.startsWith("http://purl.oclc.org/ooxml/")) return true;
114
+ return false;
115
+ };
116
+ /**
117
+ * Serialize parsed markup for replay inside a rebuilt Transitional part.
118
+ *
119
+ * Every verbatim capture goes through here — a lint rule keeps
120
+ * {@link elementToXml} out of reach of the parsers — so a replay path added
121
+ * later cannot skip the conversion. Markup that binds no Strict namespace,
122
+ * anywhere in scope or in the fragment itself, is serialized once and returned
123
+ * unchanged.
124
+ */
125
+ const captureVerbatimXml = (element) => {
126
+ const xml = elementToXml(element);
127
+ if (!xml.includes("http://purl.oclc.org/ooxml/") && !scopeDeclaresStrict(element.namespaceScope)) return xml;
128
+ return elementToXml(toTransitional(element, "transitional"));
129
+ };
130
+ //#endregion
131
+ export { UntranslatableStrictNamespaceError, captureVerbatimXml };
@@ -1,9 +1,10 @@
1
1
  import { sanitizeImageSrc } from "../utils/sanitizeImageSrc.js";
2
2
  import { pixelsToEmu } from "../utils/units.js";
3
3
  import { resolveImageData } from "./imageParser.js";
4
+ import { captureVerbatimXml } from "./verbatimCapture.js";
4
5
  import { isValidVmlPreviewDimension, parseVmlNumber, parseVmlStyle, renderStandaloneVmlPreview, renderVmlGroupPreview, vmlCssLengthToPx, vmlSvgDataUrl } from "./vmlPreview.js";
5
6
  import { isWatermarkShape } from "./watermarkParser.js";
6
- import { cloneWithXmlnsDeclarations, elementToXml, findAllDeep, findChild, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
7
+ import { cloneWithXmlnsDeclarations, findAllDeep, findChild, getAttribute, getChildElements, getLocalName } from "./xmlParser.js";
7
8
  //#region src/docx/vmlImageParser.ts
8
9
  const VML_POSITION_ABSOLUTE = "absolute";
9
10
  const IMAGE_WRAP_INLINE = "inline";
@@ -90,7 +91,7 @@ const previewImage = (pictElement, svg, widthPx, heightPx, style, rootXmlns) =>
90
91
  }
91
92
  }
92
93
  },
93
- rawXml: elementToXml(cloneWithXmlnsDeclarations(pictElement, rootXmlns))
94
+ rawXml: captureVerbatimXml(cloneWithXmlnsDeclarations(pictElement, rootXmlns))
94
95
  };
95
96
  };
96
97
  const previewDrawing = (pictElement, preview, rootXmlns) => {
@@ -154,7 +155,7 @@ function parseVmlImageContent(pictElement, rels, media, rootXmlns = {}) {
154
155
  return {
155
156
  type: "drawing",
156
157
  image,
157
- rawXml: elementToXml(cloneWithXmlnsDeclarations(pictElement, rootXmlns))
158
+ rawXml: captureVerbatimXml(cloneWithXmlnsDeclarations(pictElement, rootXmlns))
158
159
  };
159
160
  }
160
161
  for (const child of getChildElements(pictElement)) {
@@ -14,10 +14,8 @@ type ParsedWatermark = {
14
14
  hostingParagraph: XmlElement;
15
15
  /**
16
16
  * Index where the watermark paragraph sat among block-level siblings
17
- * (`w:p` / `w:tbl`) in the source header. After the host paragraph is
18
- * filtered out of `content`, the serializer inserts the watermark
19
- * back at this index so a header that originally placed the
20
- * watermark after visible text rounds-trips with the same flow.
17
+ * (`w:p` / `w:tbl` / `w:sdt`) in the source header. The serializer uses
18
+ * this index to replace the retained host paragraph with watermark XML.
21
19
  */
22
20
  blockIndex: number;
23
21
  };
@@ -1,4 +1,5 @@
1
- import { cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findChild, findChildren, findDeep, getAttribute, getLocalName, getTextContent } from "./xmlParser.js";
1
+ import { captureVerbatimXml } from "./verbatimCapture.js";
2
+ import { cloneWithXmlnsDeclarations, collectXmlnsDeclarations, findChild, findChildren, findDeep, getAttribute, getLocalName, getTextContent } from "./xmlParser.js";
2
3
  //#region src/docx/watermarkParser.ts
3
4
  const VML_TEXT_WATERMARK_SHAPETYPE = "#_x0000_t136";
4
5
  /**
@@ -38,7 +39,7 @@ function parseWatermark(header) {
38
39
  const hosting = findEnclosingParagraph(header, shape);
39
40
  if (hosting) return {
40
41
  watermark,
41
- rawParagraphXml: elementToXml(cloneWithXmlnsDeclarations(hosting, headerXmlns)),
42
+ rawParagraphXml: captureVerbatimXml(cloneWithXmlnsDeclarations(hosting, headerXmlns)),
42
43
  hostingParagraph: hosting,
43
44
  blockIndex: blockIndexOf(header, hosting)
44
45
  };
@@ -50,7 +51,7 @@ function parseWatermark(header) {
50
51
  const hosting = findEnclosingParagraph(header, anchor);
51
52
  if (hosting) return {
52
53
  watermark,
53
- rawParagraphXml: elementToXml(cloneWithXmlnsDeclarations(hosting, headerXmlns)),
54
+ rawParagraphXml: captureVerbatimXml(cloneWithXmlnsDeclarations(hosting, headerXmlns)),
54
55
  hostingParagraph: hosting,
55
56
  blockIndex: blockIndexOf(header, hosting)
56
57
  };
@@ -58,11 +59,9 @@ function parseWatermark(header) {
58
59
  }
59
60
  }
60
61
  /**
61
- * Index of `target` among block-level children of `header` (only
62
- * `w:p` and `w:tbl` count as blocks). Since the hosting paragraph
63
- * is filtered out of `content` after parse, this is the index the
64
- * serializer needs to splice the watermark XML back into so the
65
- * header rounds-trips with the same flow as the source.
62
+ * Index of `target` among block-level children of `header`. Keep this list
63
+ * aligned with `parseBlockContent`, which emits paragraphs, tables, and
64
+ * block-level content controls as one content item each.
66
65
  */
67
66
  function blockIndexOf(header, target) {
68
67
  let blockIdx = 0;
@@ -70,7 +69,7 @@ function blockIndexOf(header, target) {
70
69
  if (child === target) return blockIdx;
71
70
  if (child.type !== "element") continue;
72
71
  const local = getLocalName(child.name ?? "");
73
- if (local === "p" || local === "tbl") blockIdx++;
72
+ if (local === "p" || local === "tbl" || local === "sdt") blockIdx++;
74
73
  }
75
74
  return 0;
76
75
  }
@@ -77,7 +77,18 @@ declare const NAMESPACES: {
77
77
  * @param xml - XML string to parse
78
78
  * @returns Parsed element tree
79
79
  */
80
- declare function parseXml(xml: string): XmlElement;
80
+ /**
81
+ * The prefix bindings a WordprocessingML part declares on its root.
82
+ *
83
+ * A property element captured out of a document carries no `xmlns` of its
84
+ * own, so parsing it on its own resolves no prefix and every lookup by
85
+ * namespace URI misses. Parsing it under this scope puts it back in the
86
+ * context it was written in. A part that binds `w` to something else — or
87
+ * binds it to a prefix this map does not know — simply fails to match, and
88
+ * the caller falls back to whatever it does without the capture.
89
+ */
90
+ declare const OOXML_NAMESPACE_SCOPE: XmlNamespaceScope;
91
+ declare function parseXml(xml: string, inheritedNamespaceScope?: XmlNamespaceScope): XmlElement;
81
92
  /**
82
93
  * Serialize an XmlElement back to an XML string
83
94
  */
@@ -345,5 +356,15 @@ declare function mergeXmlnsDeclarations(inherited: Record<string, string>, eleme
345
356
  * canonical binding so the detached subtree is valid on its first replay.
346
357
  */
347
358
  declare function cloneWithXmlnsDeclarations(element: XmlElement, xmlnsDecls: Record<string, string>): XmlElement;
359
+ /**
360
+ * Shallow clone of an element that keeps its resolved namespace metadata.
361
+ *
362
+ * `namespaceUri` and `namespaceScope` are non-enumerable so structural
363
+ * comparisons stay stable, which also means a plain spread drops them and
364
+ * every namespace-URI lookup on the copy misses. Re-attaching over the
365
+ * original's scope leaves resolution unchanged whether or not the clone
366
+ * carries `xmlns` attributes of its own.
367
+ */
368
+ declare function cloneElement(element: XmlElement, overrides: Partial<XmlElement>): XmlElement;
348
369
  //#endregion
349
- export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
370
+ export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, XmlAttributeMatch, XmlElement, XmlNamespaceScope, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
@@ -1,3 +1,5 @@
1
+ import { transitionalSlotEncoding } from "./transitionalSpelling.js";
2
+ import { universalMeasureAs } from "./universalMeasure.js";
1
3
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
2
4
  import { OOXML_NS } from "@stll/docx-utils";
3
5
  //#region src/docx/xmlParser.ts
@@ -130,8 +132,8 @@ function fxpNodeToElement(node, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE)
130
132
  * XmlElement that matches xml-js's non-compact root structure:
131
133
  * `{ elements: [...] }`.
132
134
  */
133
- function fxpToRootElement(nodes) {
134
- for (let index = 0; index < nodes.length; index += 1) nodes[index] = fxpNodeToElement(nodes[index]);
135
+ function fxpToRootElement(nodes, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE) {
136
+ for (let index = 0; index < nodes.length; index += 1) nodes[index] = fxpNodeToElement(nodes[index], inheritedNamespaceScope);
135
137
  return { elements: nodes };
136
138
  }
137
139
  /**
@@ -156,8 +158,19 @@ const NAMESPACES = OOXML_NS;
156
158
  * @param xml - XML string to parse
157
159
  * @returns Parsed element tree
158
160
  */
159
- function parseXml(xml) {
160
- return fxpToRootElement((xml.includes("binData") ? fxpParserWithStopNodes : fxpParser).parse(xml));
161
+ /**
162
+ * The prefix bindings a WordprocessingML part declares on its root.
163
+ *
164
+ * A property element captured out of a document carries no `xmlns` of its
165
+ * own, so parsing it on its own resolves no prefix and every lookup by
166
+ * namespace URI misses. Parsing it under this scope puts it back in the
167
+ * context it was written in. A part that binds `w` to something else — or
168
+ * binds it to a prefix this map does not know — simply fails to match, and
169
+ * the caller falls back to whatever it does without the capture.
170
+ */
171
+ const OOXML_NAMESPACE_SCOPE = { bindings: new Map(Object.entries(OOXML_NS)) };
172
+ function parseXml(xml, inheritedNamespaceScope = EMPTY_NAMESPACE_SCOPE) {
173
+ return fxpToRootElement((xml.includes("binData") ? fxpParserWithStopNodes : fxpParser).parse(xml), inheritedNamespaceScope);
161
174
  }
162
175
  /**
163
176
  * Serialize an XmlElement back to an XML string
@@ -495,6 +508,9 @@ function parseColorElement(element) {
495
508
  function parseNumericAttribute(element, namespace, name, scale = 1) {
496
509
  const value = getAttribute(element, namespace, name);
497
510
  if (value === null) return;
511
+ const unit = transitionalSlotEncoding(element?.namespaceUri, getLocalName(element?.name), name)?.measure;
512
+ const measure = unit === void 0 ? void 0 : universalMeasureAs(value, unit);
513
+ if (measure !== void 0) return measure * scale;
498
514
  const num = Number.parseInt(value, 10);
499
515
  if (Number.isNaN(num)) return;
500
516
  return num * scale;
@@ -523,6 +539,8 @@ function parseTableMeasurementValue(element, widthType) {
523
539
  const pct = Number.parseFloat(trimmed.slice(0, -1));
524
540
  if (!Number.isNaN(pct)) return Math.round(pct * 50);
525
541
  }
542
+ const measure = universalMeasureAs(trimmed, "twips");
543
+ if (measure !== void 0) return measure;
526
544
  const num = Number.parseInt(trimmed, 10);
527
545
  return Number.isNaN(num) ? void 0 : num;
528
546
  }
@@ -741,14 +759,28 @@ function cloneWithXmlnsDeclarations(element, xmlnsDecls) {
741
759
  if (!prefix || attributes[name] !== void 0 || additions[name] !== void 0 || !isCanonicalNamespacePrefix(prefix)) continue;
742
760
  additions[name] = NAMESPACES[prefix];
743
761
  }
744
- for (const _key in additions) return {
762
+ for (const _key in additions) return cloneElement(element, { attributes: {
763
+ ...attributes,
764
+ ...additions
765
+ } });
766
+ return element;
767
+ }
768
+ /**
769
+ * Shallow clone of an element that keeps its resolved namespace metadata.
770
+ *
771
+ * `namespaceUri` and `namespaceScope` are non-enumerable so structural
772
+ * comparisons stay stable, which also means a plain spread drops them and
773
+ * every namespace-URI lookup on the copy misses. Re-attaching over the
774
+ * original's scope leaves resolution unchanged whether or not the clone
775
+ * carries `xmlns` attributes of its own.
776
+ */
777
+ function cloneElement(element, overrides) {
778
+ const clone = {
745
779
  ...element,
746
- attributes: {
747
- ...attributes,
748
- ...additions
749
- }
780
+ ...overrides
750
781
  };
751
- return element;
782
+ attachXmlNamespaceContext(clone, element.namespaceScope);
783
+ return clone;
752
784
  }
753
785
  //#endregion
754
- export { NAMESPACES, WORDPROCESSINGML_NAMESPACE_URIS, attachXmlNamespaceContext, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
786
+ export { NAMESPACES, OOXML_NAMESPACE_SCOPE, WORDPROCESSINGML_NAMESPACE_URIS, attachXmlNamespaceContext, cloneElement, cloneWithXmlnsDeclarations, collectXmlnsDeclarations, elementToXml, findAllDeep, findAttributeByNamespaceUri, findByFullName, findChild, findChildByLocalName, findChildByNamespaceUri, findChildren, findChildrenByLocalName, findDeep, getAttribute, getAttributeAny, getAttributeAnyPrefix, getAttributeByNamespaceUri, getAttributes, getChildElements, getLocalName, getNamespacePrefix, getNamespaceUri, getTextContent, hasChild, hasFlag, matchesName, mergeXmlnsDeclarations, parseBooleanElement, parseColorElement, parseNumberingLevelAttribute, parseNumericAttribute, parseOnOffValue, parseTableMeasurementValue, parseXml, parseXmlDocument };
@@ -1,3 +1,5 @@
1
+ import { resolveDocumentGridLinePitch } from "./docx/documentGrid.js";
2
+ import { formatOoxmlCounter } from "./docx/ooxmlCounterFormatter.js";
1
3
  import { parseDocx } from "./docx/parser.js";
2
4
  import { buildHeaderFooterFieldValues } from "./fields/resolveFieldValues.js";
3
5
  import { extractEmbeddedFonts } from "./fonts/embeddedFonts.js";
@@ -50,6 +52,11 @@ import { createHash } from "node:crypto";
50
52
  * backend in a browser); this module refuses to guess, because a silently
51
53
  * wrong provider produces a plausible layout that is wrong everywhere.
52
54
  */
55
+ const formatEndnoteTexts = (numbers, formatNumber) => {
56
+ const texts = /* @__PURE__ */ new Map();
57
+ for (const [id, displayNumber] of numbers) texts.set(id, formatNumber(displayNumber));
58
+ return texts;
59
+ };
53
60
  var HeadlessLayoutError = class extends TaggedError("HeadlessLayoutError") {};
54
61
  /**
55
62
  * Stories this entry still does not lay out, keyed by story so the map is total
@@ -93,6 +100,8 @@ const buildFlowOptions = (document, pageContentHeight) => {
93
100
  ...settings.consecutiveHyphenLimit === void 0 ? {} : { consecutiveLineLimit: settings.consecutiveHyphenLimit },
94
101
  ...settings.hyphenationZoneTwips === void 0 ? {} : { hyphenationZoneTwips: settings.hyphenationZoneTwips }
95
102
  };
103
+ const finalSectionDocumentGridLinePitchTwips = resolveDocumentGridLinePitch(document.package.document.sections?.at(-1)?.properties.docGrid);
104
+ if (finalSectionDocumentGridLinePitchTwips !== void 0) options.finalSectionDocumentGridLinePitchTwips = finalSectionDocumentGridLinePitchTwips;
96
105
  return options;
97
106
  };
98
107
  /**
@@ -237,9 +246,12 @@ const layoutDocxHeadless = async (input, options = {}) => {
237
246
  const footnotes = document.package.footnotes ?? [];
238
247
  const endnotes = document.package.endnotes ?? [];
239
248
  const footnoteRefs = collectFootnoteRefs(authored);
249
+ const footnoteNumbers = computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId));
250
+ const endnoteNumbers = computeNoteDisplayNumbers(endnotes, collectEndnoteRefs(authored).map((ref) => ref.endnoteId));
251
+ const endnoteNumberFormat = finalSection?.endnotePr?.numFmt ?? "lowerRoman";
240
252
  const blocks = remapNoteMarkerText(authored, {
241
- footnoteNumbers: computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId)),
242
- endnoteNumbers: computeNoteDisplayNumbers(endnotes, collectEndnoteRefs(authored).map((ref) => ref.endnoteId))
253
+ footnoteNumbers,
254
+ endnoteTexts: formatEndnoteTexts(endnoteNumbers, (displayNumber) => formatOoxmlCounter(displayNumber, endnoteNumberFormat))
243
255
  });
244
256
  const measures = measureBlocks(blocks, contentWidth);
245
257
  const footnoteContentById = footnoteRefs.length === 0 ? void 0 : buildFootnoteContentMap(footnotes, footnoteRefs, contentWidth, {
package/dist/index.d.ts CHANGED
@@ -1,16 +1,16 @@
1
+ import { document_d_exports } from "./types/document.js";
1
2
  import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIBlockTableLocation, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditNormalization, FolioAIEditNormalizationCode, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./ai-edits/types.js";
2
3
  import { WORD_DIFF_GRANULARITIES, WordDiffGranularity, WordDiffNormalization, WordDiffOptions, WordDiffSegment, diffWordSegments } from "./ai-edits/word-diff.js";
3
4
  import { FolioAIEditApplyOutcome, FolioRevisionStamp, FolioWordDiffOptions, applyFolioAIEditOperations } from "./ai-edits/apply.js";
4
- import { document_d_exports } from "./types/document.js";
5
5
  import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationResultBase, FolioDocumentOperationStatus, FolioDocumentOperationStory, FolioDocumentOperationType, FolioDocumentOperationUndoFailureReason, FolioDocumentOperationUndoHandle, FolioDocumentOperationUndoResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js";
6
6
  import { createFolioAIEditSnapshot, hashFolioAIBlockText, isFolioAIContentBlock, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js";
7
- import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
7
+ import { DeriveBlockIdInput, FolioBlockId, currentFolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
8
8
  import "./ai-edits/index.js";
9
9
  import { AIBarStatus, AIChatMode, AICitation, AICitationSource, AIGenerateInput, AISuggestion, AISuggestionApplyMode, AISuggestionPreset, AISuggestionSeverity, AISuggestionStatus, DEFAULT_AI_SUGGESTION_PRESETS } from "./ai-suggestions/types.js";
10
10
  import { ApplyResult, applySuggestions } from "./ai-suggestions/apply.js";
11
11
  import { ResolvedAnchor, isSuggestionStale, resolveSuggestionAnchor } from "./ai-suggestions/conflict.js";
12
12
  import { PositionalText, buildPositionalText } from "./ai-suggestions/text-positions.js";
13
- import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion } from "./compare/verification.js";
13
+ import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkRevision } from "./compare/verification.js";
14
14
  import { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError } from "./compare/types.js";
15
15
  import { MAX_COMPARE_OPERATIONS, compareDocx } from "./compare/compare.js";
16
16
  import { DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DocumentPreset, DocumentStyleSet } from "./style-sets/types.js";
@@ -38,4 +38,4 @@ import { getGoogleFontsEnabled, setEmbeddedFontFamilyMap, setGoogleFontsEnabled
38
38
  import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
39
39
  type Document = document_d_exports.Document;
40
40
  type DocxConformanceClass = document_d_exports.DocxConformanceClass;
41
- export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type BlockRect, COMPARE_UNSUPPORTED_REASONS, COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, type CompareChange, type CompareChangeLocation, CompareDocxApplyError, type CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, type CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, type CompareFormatRange, type CompareResult, type CompareUnsupportedPart, type CompareUnsupportedReason, type CompareVerification, type CompareVerificationCause, type CompareVerificationFailure, type CompareVerificationInvariant, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, type DocxCompatibility, type DocxCompatibilityContext, type DocxCompatibilityIssue, type DocxCompatibilityLocation, type DocxCompatibilityPart, type DocxConformanceClass, type EmbeddedFont, type EmbeddedFontParts, type ExtractDocumentStyleSetOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FinalParagraphMarkDeletion, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIBlockTableLocation, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyOutcome, type FolioAIEditApplyResult, type FolioAIEditNormalization, type FolioAIEditNormalizationCode, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationResultBase, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocxCompatibilityHost, type FolioDocxCompatibilityProfile, type FolioRevisionStamp, type FolioWordDiffOptions, type ImageMeta, type ImageRef, type InspectDocxCompatibilityOptions, InvalidCompareDocxOptionsError, InvalidFolioDocumentOperationBatchError, MAX_COMPARE_OPERATIONS, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, STELLA_STYLE_SET_NAME, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, WORD_DIFF_GRANULARITIES, type WordDiffGranularity, type WordDiffNormalization, type WordDiffOptions, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, compareDocx, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioAIContentBlock, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
41
+ export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type BlockRect, COMPARE_UNSUPPORTED_REASONS, COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, type CompareChange, type CompareChangeLocation, CompareDocxApplyError, type CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, type CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, type CompareFormatRange, type CompareResult, type CompareUnsupportedPart, type CompareUnsupportedReason, type CompareVerification, type CompareVerificationCause, type CompareVerificationFailure, type CompareVerificationInvariant, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocumentPreset, type DocumentStyleCatalog, type DocumentStyleCatalogEntry, type DocumentStyleSet, type DocxCompatibility, type DocxCompatibilityContext, type DocxCompatibilityIssue, type DocxCompatibilityLocation, type DocxCompatibilityPart, type DocxConformanceClass, type EmbeddedFont, type EmbeddedFontParts, type ExtractDocumentStyleSetOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FinalParagraphMarkRevision, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIBlockTableLocation, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyOutcome, type FolioAIEditApplyResult, type FolioAIEditNormalization, type FolioAIEditNormalizationCode, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationResultBase, type FolioDocumentOperationStatus, type FolioDocumentOperationStory, type FolioDocumentOperationType, type FolioDocumentOperationUndoFailureReason, type FolioDocumentOperationUndoHandle, type FolioDocumentOperationUndoResult, type FolioDocxCompatibilityHost, type FolioDocxCompatibilityProfile, type FolioRevisionStamp, type FolioWordDiffOptions, type ImageMeta, type ImageRef, type InspectDocxCompatibilityOptions, InvalidCompareDocxOptionsError, InvalidFolioDocumentOperationBatchError, MAX_COMPARE_OPERATIONS, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, STELLA_STYLE_SET_NAME, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, WORD_DIFF_GRANULARITIES, type WordDiffGranularity, type WordDiffNormalization, type WordDiffOptions, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, compareDocx, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, currentFolioBlockId, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioAIContentBlock, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
package/dist/index.js CHANGED
@@ -26,9 +26,9 @@ import { clearTemplateSlashMenu, consumeTemplateSlashQuery, getTemplateSlashMenu
26
26
  import { extractDocumentStyleSet, extractDocumentStyleSetFromDocx, inspectDocumentStyles, inspectDocumentStylesFromDocx } from "./style-sets/extract.js";
27
27
  import { STELLA_STYLE_SET_NAME, createStellaStyleDocumentPreset, createStellaStyleSet } from "./style-sets/stellaStyle.js";
28
28
  import { DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION } from "./style-sets/types.js";
29
- import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
29
+ import { currentFolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
30
30
  import { createEmptyDocument } from "./utils/createDocument.js";
31
31
  import { getGoogleFontsEnabled, setEmbeddedFontFamilyMap, setGoogleFontsEnabled } from "./utils/fontResolver.js";
32
32
  import { mergeDocumentContent } from "./utils/mergeDocumentContent.js";
33
33
  import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
34
- export { COMPARE_UNSUPPORTED_REASONS, COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidCompareDocxOptionsError, InvalidFolioDocumentOperationBatchError, MAX_COMPARE_OPERATIONS, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, WORD_DIFF_GRANULARITIES, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, compareDocx, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioAIContentBlock, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
34
+ export { COMPARE_UNSUPPORTED_REASONS, COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DOCX_CONFORMANCE_CLASSES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidCompareDocxOptionsError, InvalidFolioDocumentOperationBatchError, MAX_COMPARE_OPERATIONS, STELLA_STYLE_SET_NAME, UnsupportedFolioDocumentOperationVersionError, WORD_DIFF_GRANULARITIES, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildEmbeddedFontFamilyMap, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, compareDocx, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, createStellaStyleDocumentPreset, createStellaStyleSet, currentFolioBlockId, deriveBlockId, diffWordSegments, extractDocumentStyleSet, extractDocumentStyleSetFromDocx, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, inspectDocumentStyles, inspectDocumentStylesFromDocx, inspectDocxCompatibility, isFolioAIContentBlock, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, mergeDocumentContent, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scopeEmbeddedFontFamily, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setEmbeddedFontFamilyMap, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
@@ -52,8 +52,8 @@ declare function computeNoteDisplayNumbers(notes: readonly NumberableNote[], ref
52
52
  type NoteDisplayNumberMaps = {
53
53
  /** footnote `w:id` → sequential display number */
54
54
  footnoteNumbers?: ReadonlyMap<number, number>;
55
- /** endnote `w:id` → sequential display number */
56
- endnoteNumbers?: ReadonlyMap<number, number>;
55
+ /** endnote `w:id` → formatted display text */
56
+ endnoteTexts?: ReadonlyMap<number, string>;
57
57
  };
58
58
  /**
59
59
  * Rewrite body reference-marker run text from the raw `w:id` (which the PM
@@ -80,7 +80,7 @@ function computeNoteDisplayNumbers(notes, refNoteIds) {
80
80
  * template preview substitution).
81
81
  */
82
82
  function remapNoteMarkerText(blocks, maps) {
83
- if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteNumbers?.size ?? 0) === 0) return blocks;
83
+ if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteTexts?.size ?? 0) === 0) return blocks;
84
84
  let changed = false;
85
85
  const next = blocks.map((block) => {
86
86
  const remapped = remapNoteMarkerBlock(block, maps);
@@ -147,17 +147,19 @@ function remapNoteMarkerParagraph(block, maps) {
147
147
  }
148
148
  function remapNoteMarkerRun(run, maps) {
149
149
  if (run.kind !== "text") return run;
150
- const displayNumber = getRunDisplayNumber(run, maps);
151
- if (displayNumber === void 0) return run;
152
- const text = String(displayNumber);
153
- return run.text === text ? run : {
150
+ const displayText = getRunDisplayText(run, maps);
151
+ if (displayText === void 0) return run;
152
+ return run.text === displayText ? run : {
154
153
  ...run,
155
- text
154
+ text: displayText
156
155
  };
157
156
  }
158
- function getRunDisplayNumber(run, maps) {
159
- if (run.footnoteRefId !== void 0) return maps.footnoteNumbers?.get(run.footnoteRefId);
160
- if (run.endnoteRefId !== void 0) return maps.endnoteNumbers?.get(run.endnoteRefId);
157
+ function getRunDisplayText(run, maps) {
158
+ if (run.footnoteRefId !== void 0) {
159
+ const displayNumber = maps.footnoteNumbers?.get(run.footnoteRefId);
160
+ return displayNumber === void 0 ? void 0 : String(displayNumber);
161
+ }
162
+ if (run.endnoteRefId !== void 0) return maps.endnoteTexts?.get(run.endnoteRefId);
161
163
  }
162
164
  /**
163
165
  * After layout, determine which footnotes appear on which pages.
@@ -202,7 +204,7 @@ function convertFootnoteToContent(footnote, displayNumber, contentWidth, options
202
204
  if (options.justificationCompatibility) flowOptions.justificationCompatibility = options.justificationCompatibility;
203
205
  if (options.tableIndentCompatibility) flowOptions.tableIndentCompatibility = options.tableIndentCompatibility;
204
206
  if (options.automaticHyphenation) flowOptions.automaticHyphenation = options.automaticHyphenation;
205
- const blocks = applyFootnotePresentation(toFlowBlocks(pmDoc, flowOptions), displayNumber);
207
+ const blocks = applyFootnotePresentation(preserveAuthoredFootnoteTerminalParagraph(toFlowBlocks(pmDoc, flowOptions)), displayNumber);
206
208
  const measures = options.measureBlocks ? options.measureBlocks(blocks, contentWidth) : measureFootnoteBlocks(blocks, contentWidth);
207
209
  let totalHeight = 0;
208
210
  for (const measure of measures) if (measure.kind === "paragraph") totalHeight += measure.totalHeight;
@@ -216,6 +218,17 @@ function convertFootnoteToContent(footnote, displayNumber, contentWidth, options
216
218
  height: totalHeight
217
219
  };
218
220
  }
221
+ /** Restore an authored footnote line that the body-only terminal-table policy collapsed. */
222
+ function preserveAuthoredFootnoteTerminalParagraph(blocks) {
223
+ const finalBlock = blocks.at(-1);
224
+ if (blocks.at(-2)?.kind !== "table" || finalBlock?.kind !== "paragraph" || finalBlock.attrs?.suppressEmptyParagraphHeight !== true) return blocks;
225
+ const attrs = { ...finalBlock.attrs };
226
+ delete attrs.suppressEmptyParagraphHeight;
227
+ return [...blocks.slice(0, -1), {
228
+ ...finalBlock,
229
+ attrs
230
+ }];
231
+ }
219
232
  function measureFootnoteBlocks(blocks, contentWidth) {
220
233
  return blocks.map((block) => measureFootnoteBlock(block, contentWidth));
221
234
  }
@@ -3,6 +3,16 @@ import { headerFooterToProseDoc } from "../../prosemirror/conversion/toProseDoc.
3
3
  import { emuToPixels } from "../../utils/units.js";
4
4
  import { toFlowBlocks } from "./toFlowBlocks.js";
5
5
  //#region src/layout-bridge/convert/headerFooterLayout.ts
6
+ const DETACHED_WATERMARK_HOST = Symbol.for("stll.detachedWatermarkHost");
7
+ const headerFooterToProseDocWithDetachedWatermarkHost = (headerFooter, options) => {
8
+ return headerFooterToProseDoc(headerFooter.content.map((block, blockIndex) => {
9
+ if (blockIndex !== headerFooter.watermarkBlockIndex || block.type !== "paragraph") return block;
10
+ return {
11
+ ...block,
12
+ [DETACHED_WATERMARK_HOST]: true
13
+ };
14
+ }), options);
15
+ };
6
16
  function isAnchoredImageRun(run) {
7
17
  if (run.kind !== "image") return false;
8
18
  if (run.position) return true;
@@ -241,7 +251,7 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
241
251
  * these bounds separately describe the in-flow portion for API consumers.
242
252
  */
243
253
  function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics) {
244
- if (blocks.length > 0 && blocks.every((block) => isPaintlessParagraph(block) && !hasAuthoredVisualContent(block))) return {
254
+ if (blocks.length > 0 && blocks.every((block) => block.kind === "paragraph" && isPaintlessParagraph(block) && !hasAuthoredVisualContent(block) && block.attrs?.suppressEmptyParagraphHeight !== false && !preservesInheritedSpacing(block))) return {
245
255
  top: 0,
246
256
  bottom: 0
247
257
  };
@@ -330,7 +340,7 @@ function convertHeaderFooterToContent(headerFooter, contentWidth, metrics, optio
330
340
  const proseDocOptions = {};
331
341
  if (options.styles) proseDocOptions.styles = options.styles;
332
342
  if (options.theme !== void 0) proseDocOptions.theme = options.theme;
333
- const pmDoc = headerFooterToProseDoc(headerFooter.content, proseDocOptions);
343
+ const pmDoc = headerFooter.watermarkBlockIndex === void 0 ? headerFooterToProseDoc(headerFooter.content, proseDocOptions) : headerFooterToProseDocWithDetachedWatermarkHost(headerFooter, proseDocOptions);
334
344
  const flowOptions = {};
335
345
  if (options.theme !== void 0) flowOptions.theme = options.theme;
336
346
  if (options.fontAlternates !== void 0) flowOptions.fontAlternates = options.fontAlternates;
@@ -458,6 +468,7 @@ function serializeParagraphAttrs(attrs) {
458
468
  const keys = [
459
469
  "alignment",
460
470
  "bidi",
471
+ "suppressEmptyParagraphHeight",
461
472
  "indent",
462
473
  "spacing",
463
474
  "styleId",