@stll/folio-core 0.33.0 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@ import { hasComplexScript, isComplexScriptCodePoint } from "../utils/scriptSegme
15
15
  * advances rightward from its origin whatever the paragraph does.
16
16
  */
17
17
  declare const glyphCellOffsetsPx: (run: DisplayGlyphRun) => readonly number[];
18
- declare const DISPLAY_PRIMITIVE_KINDS: ("glyphRun" | "rect" | "line" | "image" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
18
+ declare const DISPLAY_PRIMITIVE_KINDS: ("line" | "rect" | "image" | "glyphRun" | "clipGroup" | "rotateGroup" | "opacityGroup")[];
19
19
  /**
20
20
  * Dash geometry of each stroke pattern, as multiples of the stroke thickness.
21
21
  *
@@ -16,6 +16,14 @@ import { XmlElement } from "./xmlParser.js";
16
16
  * @returns Parsed Hyperlink object
17
17
  */
18
18
  declare function parseHyperlink(node: XmlElement, rels: document_d_exports.RelationshipMap | null, styles?: StyleMap | null, theme?: document_d_exports.Theme | null, media?: Map<string, document_d_exports.MediaFile> | null, rootXmlns?: Record<string, string>): document_d_exports.Hyperlink;
19
+ /**
20
+ * One `w:hyperlink` child, or `null` for markup the model does not carry.
21
+ *
22
+ * Exposed so a caller that has to segment a hyperlink — one holding revision
23
+ * wrappers, which the model nests the other way round — parses its plain
24
+ * children exactly as {@link parseHyperlink} does.
25
+ */
26
+ declare function parseHyperlinkChild(node: XmlElement, styles: StyleMap | null, theme: document_d_exports.Theme | null, rels: document_d_exports.RelationshipMap | null, media: Map<string, document_d_exports.MediaFile> | null, inScopeXmlns: Record<string, string>): document_d_exports.Hyperlink["children"][number] | null;
19
27
  /**
20
28
  * Get the display text of a hyperlink
21
29
  *
@@ -104,4 +112,4 @@ declare function createExternalHyperlink(url: string, children: document_d_expor
104
112
  target?: string;
105
113
  }): document_d_exports.Hyperlink;
106
114
  //#endregion
107
- export { createExternalHyperlink, createInternalHyperlink, getHyperlinkRuns, getHyperlinkText, getHyperlinkUrl, hasContent, isExternalLink, isInternalLink, parseHyperlink, resolveHyperlinkUrl };
115
+ export { createExternalHyperlink, createInternalHyperlink, getHyperlinkRuns, getHyperlinkText, getHyperlinkUrl, hasContent, isExternalLink, isInternalLink, parseHyperlink, parseHyperlinkChild, resolveHyperlinkUrl };
@@ -71,22 +71,28 @@ function parseHyperlink(node, rels, styles = null, theme = null, media = null, r
71
71
  const docLocation = getAttribute(node, "w", "docLocation");
72
72
  if (docLocation) hyperlink.docLocation = docLocation;
73
73
  const inScopeXmlns = mergeXmlnsDeclarations(rootXmlns, node);
74
- const children = getChildElements(node);
75
- for (const child of children) switch (getLocalName(child.name)) {
76
- case "r":
77
- hyperlink.children.push(parseRun(child, styles, theme, rels, media, inScopeXmlns));
78
- break;
79
- case "bookmarkStart":
80
- hyperlink.children.push(parseBookmarkStart(child));
81
- break;
82
- case "bookmarkEnd":
83
- hyperlink.children.push(parseBookmarkEnd(child));
84
- break;
85
- default: break;
74
+ for (const child of getChildElements(node)) {
75
+ const parsed = parseHyperlinkChild(child, styles, theme, rels, media, inScopeXmlns);
76
+ if (parsed) hyperlink.children.push(parsed);
86
77
  }
87
78
  return hyperlink;
88
79
  }
89
80
  /**
81
+ * One `w:hyperlink` child, or `null` for markup the model does not carry.
82
+ *
83
+ * Exposed so a caller that has to segment a hyperlink — one holding revision
84
+ * wrappers, which the model nests the other way round — parses its plain
85
+ * children exactly as {@link parseHyperlink} does.
86
+ */
87
+ function parseHyperlinkChild(node, styles, theme, rels, media, inScopeXmlns) {
88
+ switch (getLocalName(node.name)) {
89
+ case "r": return parseRun(node, styles, theme, rels, media, inScopeXmlns);
90
+ case "bookmarkStart": return parseBookmarkStart(node);
91
+ case "bookmarkEnd": return parseBookmarkEnd(node);
92
+ default: return null;
93
+ }
94
+ }
95
+ /**
90
96
  * Get the display text of a hyperlink
91
97
  *
92
98
  * Concatenates text from all child runs.
@@ -219,4 +225,4 @@ function createExternalHyperlink(url, children, options) {
219
225
  };
220
226
  }
221
227
  //#endregion
222
- export { createExternalHyperlink, createInternalHyperlink, getHyperlinkRuns, getHyperlinkText, getHyperlinkUrl, hasContent, isExternalLink, isInternalLink, parseHyperlink, resolveHyperlinkUrl };
228
+ export { createExternalHyperlink, createInternalHyperlink, getHyperlinkRuns, getHyperlinkText, getHyperlinkUrl, hasContent, isExternalLink, isInternalLink, parseHyperlink, parseHyperlinkChild, resolveHyperlinkUrl };
@@ -0,0 +1,40 @@
1
+ //#region src/docx/paraIdRangeNormalization.d.ts
2
+ /**
3
+ * Keep every paragraph id a package carries inside the range the schema gives
4
+ * it.
5
+ *
6
+ * `w14:paraId`, `w14:textId` and the comment-part ids that reference a
7
+ * paragraph are `ST_LongHexNumber` with a maximum: the value has to be below
8
+ * `0x80000000`, so the ids are 31-bit. Producers exist that write eight hex
9
+ * digits without that bound, and folio preserves the ids a document arrives
10
+ * with — so a package can carry an out-of-range id in, and a save that copies
11
+ * it through hands a consumer a package it will refuse.
12
+ *
13
+ * {@link paraIdInRange} is the one mapping, and it is a pure function of the
14
+ * value alone. That is what lets the parser and the save agree without
15
+ * consulting each other: a paragraph's id in the model is the id the file gets,
16
+ * so bringing an id into range does not make a document's own identity move
17
+ * under it between reading and writing. The package pass below is the same
18
+ * mapping applied to every attribute that carries such an id, so a paragraph
19
+ * and every reference to it move together.
20
+ */
21
+ /**
22
+ * `value` when it already fits, and a value derived from it when it does not.
23
+ *
24
+ * The replacement is derived from the id being replaced and from nothing else.
25
+ * A package-aware search for a free id would read better here and would be
26
+ * wrong: the parser has no package to search, and an id that means one
27
+ * paragraph while reading and another while writing is worse than the
28
+ * vanishing chance of two rewritten ids landing on one value, which is a
29
+ * duplicate rather than a package a consumer refuses.
30
+ */
31
+ declare const paraIdInRange: (value: string) => string;
32
+ /**
33
+ * Rewrite out-of-range paragraph ids across a whole package.
34
+ *
35
+ * Returns the parts unchanged when every id already fits, so a save of a
36
+ * document that never carried one is byte-identical.
37
+ */
38
+ declare const normalizeParaIdRangeInXmlParts: (parts: ReadonlyMap<string, string>) => Map<string, string>;
39
+ //#endregion
40
+ export { normalizeParaIdRangeInXmlParts, paraIdInRange };
@@ -0,0 +1,64 @@
1
+ import { deterministicHexId } from "../utils/hexId.js";
2
+ //#region src/docx/paraIdRangeNormalization.ts
3
+ /**
4
+ * Keep every paragraph id a package carries inside the range the schema gives
5
+ * it.
6
+ *
7
+ * `w14:paraId`, `w14:textId` and the comment-part ids that reference a
8
+ * paragraph are `ST_LongHexNumber` with a maximum: the value has to be below
9
+ * `0x80000000`, so the ids are 31-bit. Producers exist that write eight hex
10
+ * digits without that bound, and folio preserves the ids a document arrives
11
+ * with — so a package can carry an out-of-range id in, and a save that copies
12
+ * it through hands a consumer a package it will refuse.
13
+ *
14
+ * {@link paraIdInRange} is the one mapping, and it is a pure function of the
15
+ * value alone. That is what lets the parser and the save agree without
16
+ * consulting each other: a paragraph's id in the model is the id the file gets,
17
+ * so bringing an id into range does not make a document's own identity move
18
+ * under it between reading and writing. The package pass below is the same
19
+ * mapping applied to every attribute that carries such an id, so a paragraph
20
+ * and every reference to it move together.
21
+ */
22
+ /** Exclusive upper bound on a paragraph id: the values are 31-bit. */
23
+ const MAX_PARA_ID_EXCLUSIVE = 2147483648;
24
+ /**
25
+ * Every attribute that carries a paragraph id or the text-revision marker
26
+ * written beside one: the paragraph's own `w14:paraId` / `w14:textId` (the
27
+ * parser accepts a `w:` prefix too), the comment part's `w15:paraId` and the
28
+ * `w15:paraIdParent` that links a reply to its thread, and the durable-comment
29
+ * part's `w16cid:paraId`.
30
+ */
31
+ const PARA_ID_ATTRIBUTE = /\b(w|w14|w15|w16cid):(paraId|paraIdParent|textId)=(?<quote>["'])([0-9A-Fa-f]{8})\k<quote>/gu;
32
+ /** A candidate part is one that mentions any of those attributes at all. */
33
+ const PARA_ID_CANDIDATE = /\b(?:w|w14|w15|w16cid):(?:paraId|paraIdParent|textId)=/u;
34
+ /**
35
+ * `value` when it already fits, and a value derived from it when it does not.
36
+ *
37
+ * The replacement is derived from the id being replaced and from nothing else.
38
+ * A package-aware search for a free id would read better here and would be
39
+ * wrong: the parser has no package to search, and an id that means one
40
+ * paragraph while reading and another while writing is worse than the
41
+ * vanishing chance of two rewritten ids landing on one value, which is a
42
+ * duplicate rather than a package a consumer refuses.
43
+ */
44
+ const paraIdInRange = (value) => Number.parseInt(value, 16) < MAX_PARA_ID_EXCLUSIVE ? value : deterministicHexId(value);
45
+ /**
46
+ * Rewrite out-of-range paragraph ids across a whole package.
47
+ *
48
+ * Returns the parts unchanged when every id already fits, so a save of a
49
+ * document that never carried one is byte-identical.
50
+ */
51
+ const normalizeParaIdRangeInXmlParts = (parts) => {
52
+ const normalized = new Map(parts);
53
+ for (const [path, xml] of parts) {
54
+ if (!PARA_ID_CANDIDATE.test(xml)) continue;
55
+ const rewritten = xml.replaceAll(PARA_ID_ATTRIBUTE, (whole, prefix, name, quote, value) => {
56
+ const replacement = paraIdInRange(value);
57
+ return replacement === value ? whole : `${prefix}:${name}=${quote}${replacement}${quote}`;
58
+ });
59
+ if (rewritten !== xml) normalized.set(path, rewritten);
60
+ }
61
+ return normalized;
62
+ };
63
+ //#endregion
64
+ export { normalizeParaIdRangeInXmlParts, paraIdInRange };
@@ -2,8 +2,9 @@ import { isValidHexColor } from "../utils/colorResolver.js";
2
2
  import { isValidHexId } from "../utils/hexId.js";
3
3
  import { parseBookmarkEnd as parseBookmarkEnd$1, parseBookmarkStart as parseBookmarkStart$1 } from "./bookmarkParser.js";
4
4
  import { parseFieldType } from "./fieldParser.js";
5
- import { parseHyperlink as parseHyperlink$1 } from "./hyperlinkParser.js";
5
+ import { parseHyperlink as parseHyperlink$1, parseHyperlinkChild } from "./hyperlinkParser.js";
6
6
  import { markerFormattingFromLevel } from "./numberingParser.js";
7
+ import { paraIdInRange } from "./paraIdRangeNormalization.js";
7
8
  import { BorderStyleSchema, FrameWrapSchema, FrameXAlignSchema, FrameYAlignSchema, LineSpacingRuleSchema, ParagraphAlignmentSchema, ShadingPatternSchema, TabLeaderSchema, TabStopAlignmentSchema, ThemeColorSlotSchema, narrowEnum } from "./parserEnums.js";
8
9
  import { consolidateParagraphContent } from "./runConsolidator.js";
9
10
  import { parseRun, parseRunProperties } from "./runParser.js";
@@ -646,6 +647,80 @@ function pushInlineSdtSegments({ contents, properties, parsedContent }) {
646
647
  function parseHyperlink(node, rels, styles, theme, media, rootXmlns = {}) {
647
648
  return parseHyperlink$1(node, rels, styles, theme, media, rootXmlns);
648
649
  }
650
+ /** The revision wrapper a `w:hyperlink` child is, when it is one. */
651
+ const hyperlinkRevisionWrapperType = (node) => {
652
+ switch (getLocalName(node.name)) {
653
+ case "ins": return "insertion";
654
+ case "del": return "deletion";
655
+ case "moveFrom": return "moveFrom";
656
+ case "moveTo": return "moveTo";
657
+ default: return;
658
+ }
659
+ };
660
+ const isHyperlinkChildContent = (content) => content.type === "run" || content.type === "bookmarkStart" || content.type === "bookmarkEnd";
661
+ /**
662
+ * A `w:hyperlink` as paragraph content, with any revision wrapper it holds
663
+ * hoisted around it.
664
+ *
665
+ * OOXML nests `w:ins`/`w:del` INSIDE `w:hyperlink`; the model nests the
666
+ * hyperlink inside the revision, because a revision is the unit a redline
667
+ * reads and a link that is half deleted is two links to it. This is the exact
668
+ * inverse of what the serializer writes, so a package survives the round trip.
669
+ */
670
+ function parseHyperlinkParagraphContents(node, rels, styles, theme, media, rootXmlns) {
671
+ const children = getChildElements(node);
672
+ if (!children.some((child) => hyperlinkRevisionWrapperType(child) !== void 0)) return [parseHyperlink(node, rels, styles, theme, media, rootXmlns)];
673
+ const inScopeXmlns = mergeXmlnsDeclarations(rootXmlns, node);
674
+ const shell = parseHyperlink(node, rels, styles, theme, media, rootXmlns);
675
+ const linkOver = (linkChildren) => ({
676
+ ...shell,
677
+ children: [...linkChildren]
678
+ });
679
+ const contents = [];
680
+ let plain = [];
681
+ const flushPlain = () => {
682
+ if (plain.length > 0) {
683
+ contents.push(linkOver(plain));
684
+ plain = [];
685
+ }
686
+ };
687
+ for (const child of children) {
688
+ const wrapperType = hyperlinkRevisionWrapperType(child);
689
+ if (wrapperType === void 0) {
690
+ const parsed = parseHyperlinkChild(child, styles, theme, rels, media, inScopeXmlns);
691
+ if (parsed) plain.push(parsed);
692
+ continue;
693
+ }
694
+ flushPlain();
695
+ const wrapped = parseParagraphContents(child, styles, theme, null, rels, media, wrapperType === "deletion" || wrapperType === "moveFrom" ? "deletion" : "default", inScopeXmlns);
696
+ const content = [];
697
+ let linked = [];
698
+ const flushLinked = () => {
699
+ if (linked.length > 0) {
700
+ content.push(linkOver(linked));
701
+ linked = [];
702
+ }
703
+ };
704
+ for (const item of wrapped) {
705
+ if (isHyperlinkChildContent(item)) {
706
+ linked.push(item);
707
+ continue;
708
+ }
709
+ flushLinked();
710
+ if (isTrackedChangeWrapperChild(item)) content.push(item);
711
+ }
712
+ flushLinked();
713
+ pushTrackedChangeWrapper({
714
+ contents,
715
+ type: wrapperType,
716
+ info: parseTrackedChangeInfo(child),
717
+ content,
718
+ preserveEmpty: true
719
+ });
720
+ }
721
+ flushPlain();
722
+ return contents;
723
+ }
649
724
  /**
650
725
  * Parse bookmark start (w:bookmarkStart)
651
726
  * Delegates to bookmarkParser module.
@@ -798,7 +873,7 @@ function parseParagraphContents(paraElement, styles, theme, _numbering, rels, me
798
873
  break;
799
874
  }
800
875
  case "hyperlink":
801
- contents.push(parseHyperlink(child, rels, styles, theme, media, inScopeXmlns));
876
+ contents.push(...parseHyperlinkParagraphContents(child, rels, styles, theme, media, inScopeXmlns));
802
877
  break;
803
878
  case "bookmarkStart":
804
879
  contents.push(parseBookmarkStart(child));
@@ -961,9 +1036,9 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
961
1036
  content: []
962
1037
  };
963
1038
  const paraId = getAttribute(node, "w14", "paraId") ?? getAttribute(node, "w", "paraId");
964
- if (paraId && isValidHexId(paraId)) paragraph.paraId = paraId;
1039
+ if (paraId && isValidHexId(paraId)) paragraph.paraId = paraIdInRange(paraId);
965
1040
  const textId = getAttribute(node, "w14", "textId") ?? getAttribute(node, "w", "textId");
966
- if (textId && isValidHexId(textId)) paragraph.textId = textId;
1041
+ if (textId && isValidHexId(textId)) paragraph.textId = paraIdInRange(textId);
967
1042
  if (!options?.inHeaderFooter && paragraphStartsWithRenderedPageBreak(node)) paragraph.renderedPageBreakBefore = true;
968
1043
  const pPr = findChild(node, "w", "pPr");
969
1044
  if (pPr) {
@@ -1,4 +1,18 @@
1
1
  //#region src/docx/revisionIdNormalization.d.ts
2
+ declare const RevisionIdCollisionError_base: import("better-result").TaggedErrorClass<"RevisionIdCollisionError">;
3
+ /**
4
+ * Two revision elements in one package claimed the same `w:id`.
5
+ *
6
+ * `w:id` on a revision element is unique across the package, so a collision is
7
+ * a package a consumer may reject rather than a cosmetic detail. The
8
+ * normalization below hands every id it emits to one choke point, which throws
9
+ * this rather than letting the duplicate reach the ZIP.
10
+ */
11
+ declare class RevisionIdCollisionError extends RevisionIdCollisionError_base<{
12
+ message: string;
13
+ revisionId: number;
14
+ part: string;
15
+ }> {}
2
16
  declare const REVISION_ELEMENT_NAMES: Set<string>;
3
17
  /**
4
18
  * Keep physical tracked-change element ids unique across a package.
@@ -10,4 +24,4 @@ declare const REVISION_ELEMENT_NAMES: Set<string>;
10
24
  */
11
25
  declare const normalizeRevisionIdsInXmlParts: (parts: ReadonlyMap<string, string>) => Map<string, string>;
12
26
  //#endregion
13
- export { REVISION_ELEMENT_NAMES, normalizeRevisionIdsInXmlParts };
27
+ export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
@@ -1,7 +1,17 @@
1
1
  import { rewriteStreamingXmlDecimalAttributes } from "./streamingXmlParser.js";
2
2
  import { WORDPROCESSINGML_NAMESPACE_URIS, findAttributeByNamespaceUri, getLocalName, getNamespaceUri } from "./xmlParser.js";
3
3
  import { XmlResourceLimitError, assertXmlResourceLimits } from "./xmlResourceLimits.js";
4
+ import { TaggedError } from "better-result";
4
5
  //#region src/docx/revisionIdNormalization.ts
6
+ /**
7
+ * Two revision elements in one package claimed the same `w:id`.
8
+ *
9
+ * `w:id` on a revision element is unique across the package, so a collision is
10
+ * a package a consumer may reject rather than a cosmetic detail. The
11
+ * normalization below hands every id it emits to one choke point, which throws
12
+ * this rather than letting the duplicate reach the ZIP.
13
+ */
14
+ var RevisionIdCollisionError = class extends TaggedError("RevisionIdCollisionError") {};
5
15
  const REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
6
16
  "cellDel",
7
17
  "cellIns",
@@ -76,19 +86,27 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
76
86
  for (const [path, xml] of candidates) {
77
87
  const ids = occurrencesByPath.get(path);
78
88
  if (!ids) continue;
89
+ const claim = (id) => {
90
+ if (seen.has(id)) throw new RevisionIdCollisionError({
91
+ message: `Revision id ${String(id)} is claimed twice in ${path}`,
92
+ revisionId: id,
93
+ part: path
94
+ });
95
+ seen.add(id);
96
+ };
79
97
  if (!repeatedPaths.has(path) && ids.every((id) => !seen.has(id))) {
80
- for (const id of ids) seen.add(id);
98
+ for (const id of ids) claim(id);
81
99
  continue;
82
100
  }
83
101
  const rewritten = rewriteStreamingXmlDecimalAttributes(xml, (element) => {
84
102
  const attribute = revisionAttribute(element);
85
103
  if (!attribute) return null;
86
104
  if (!seen.has(attribute.id)) {
87
- seen.add(attribute.id);
105
+ claim(attribute.id);
88
106
  return null;
89
107
  }
90
108
  const replacement = allocate();
91
- seen.add(replacement);
109
+ claim(replacement);
92
110
  return /* @__PURE__ */ new Map([[attribute.name, String(replacement)]]);
93
111
  });
94
112
  if (rewritten.status === "unsupported") throw new XmlResourceLimitError({
@@ -100,4 +118,4 @@ const normalizeRevisionIdsInXmlParts = (parts) => {
100
118
  return normalized;
101
119
  };
102
120
  //#endregion
103
- export { REVISION_ELEMENT_NAMES, normalizeRevisionIdsInXmlParts };
121
+ export { REVISION_ELEMENT_NAMES, RevisionIdCollisionError, normalizeRevisionIdsInXmlParts };
@@ -105,6 +105,12 @@ declare function updateMultipleFiles(originalBuffer: ArrayBuffer, updates: Map<s
105
105
  /**
106
106
  * Apply file updates to an already-loaded JSZip instance and generate the output.
107
107
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
108
+ *
109
+ * This is the selective save's exit, so it owes the package the same id passes
110
+ * {@link generateDocxZip} runs: a save that rewrites only the changed
111
+ * paragraphs still has to see the parts it left alone, both to know which
112
+ * revision ids are free and because an out-of-range paragraph id can sit in a
113
+ * part it never touched.
108
114
  */
109
115
  declare function applyUpdatesToZip(zip: JSZip, updates: Map<string, string | ArrayBuffer>, options?: RepackOptions): Promise<ArrayBuffer>;
110
116
  /**
@@ -6,6 +6,7 @@ import { assertValidFolioDocumentModel } from "./modelValidation.js";
6
6
  import { isNewDataUrlDrawing } from "./newImage.js";
7
7
  import { parseNumbering } from "./numberingParser.js";
8
8
  import { isUnsafePackagePath, reconcilePackageReferences, removeUnsafeEntries } from "./packageParts.js";
9
+ import { normalizeParaIdRangeInXmlParts } from "./paraIdRangeNormalization.js";
9
10
  import { RELATIONSHIP_TYPES, parseRelationships, resolveRelativePath } from "./relsParser.js";
10
11
  import { normalizeRevisionIdsInXmlParts } from "./revisionIdNormalization.js";
11
12
  import { appendNumberingDefs, buildPatchedNotePartXml, buildPatchedNumberingXml, collectAddedNumberingDefs, collectChangedNoteParaIds, collectChangedNumberingDefs, collectParaIds } from "./selectiveXmlPatch.js";
@@ -407,19 +408,34 @@ async function processNewHyperlinks(parts, zip, compressionLevel) {
407
408
  }
408
409
  }
409
410
  /**
410
- * The single exit for a repacked package. Reconciliation runs here rather than
411
- * at each caller so no save path can emit a package whose relationships or
412
- * content types name a part it does not hold.
411
+ * Bring the ids a package addresses itself by inside the bounds the format
412
+ * gives them.
413
+ *
414
+ * Both bounds belong to the package rather than to a part, and neither is
415
+ * something one serializer can see on its own. A revision `w:id` is unique
416
+ * across the package, and one logical revision serializes as several physical
417
+ * wrappers — a word diff cut around unchanged words, a revision split around a
418
+ * hyperlink. A paragraph id is 31-bit, and a paragraph is referenced by id
419
+ * from parts other than the one it lives in. So the passes run at the exits,
420
+ * over every `word/*.xml` part including the ones a save left untouched.
413
421
  */
414
- const generateDocxZip = async (zip, compressionLevel) => {
415
- await reconcilePackageReferences(zip, compressionLevel);
422
+ const normalizePackageIdsInZip = async (zip, compressionLevel) => {
416
423
  const xmlParts = /* @__PURE__ */ new Map();
417
424
  for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.startsWith("word/") && path.endsWith(".xml")) xmlParts.set(path, await file.async("text"));
418
- const normalizedParts = normalizeRevisionIdsInXmlParts(xmlParts);
425
+ const normalizedParts = normalizeParaIdRangeInXmlParts(normalizeRevisionIdsInXmlParts(xmlParts));
419
426
  for (const [path, xml] of normalizedParts) if (xml !== xmlParts.get(path)) zip.file(path, xml, {
420
427
  compression: "DEFLATE",
421
428
  compressionOptions: { level: compressionLevel }
422
429
  });
430
+ };
431
+ /**
432
+ * The single exit for a repacked package. Reconciliation runs here rather than
433
+ * at each caller so no save path can emit a package whose relationships or
434
+ * content types name a part it does not hold.
435
+ */
436
+ const generateDocxZip = async (zip, compressionLevel) => {
437
+ await reconcilePackageReferences(zip, compressionLevel);
438
+ await normalizePackageIdsInZip(zip, compressionLevel);
423
439
  return zip.generateAsync({
424
440
  type: "arraybuffer",
425
441
  compression: "DEFLATE",
@@ -758,14 +774,21 @@ async function updateMultipleFiles(originalBuffer, updates, options = {}) {
758
774
  /**
759
775
  * Apply file updates to an already-loaded JSZip instance and generate the output.
760
776
  * Use this when the zip is already loaded to avoid a redundant decompression pass.
777
+ *
778
+ * This is the selective save's exit, so it owes the package the same id passes
779
+ * {@link generateDocxZip} runs: a save that rewrites only the changed
780
+ * paragraphs still has to see the parts it left alone, both to know which
781
+ * revision ids are free and because an out-of-range paragraph id can sit in a
782
+ * part it never touched.
761
783
  */
762
- function applyUpdatesToZip(zip, updates, options = {}) {
784
+ async function applyUpdatesToZip(zip, updates, options = {}) {
763
785
  const { compressionLevel = 6 } = options;
764
786
  for (const [path, content] of updates) zip.file(path, content, {
765
787
  compression: "DEFLATE",
766
788
  compressionOptions: { level: compressionLevel }
767
789
  });
768
- return zip.generateAsync({
790
+ await normalizePackageIdsInZip(zip, compressionLevel);
791
+ return await zip.generateAsync({
769
792
  type: "arraybuffer",
770
793
  compression: "DEFLATE",
771
794
  compressionOptions: { level: compressionLevel }
@@ -210,10 +210,8 @@ function serializeParagraphPropertyChange(change) {
210
210
  const normalizedPreviousPPr = previousPPrInner.length > 0 ? `<w:pPr>${previousPPrInner}</w:pPr>` : "<w:pPr/>";
211
211
  return `<w:pPrChange ${attrs.join(" ")}>${normalizedPreviousPPr}</w:pPrChange>`;
212
212
  }
213
- /**
214
- * Serialize a hyperlink (w:hyperlink)
215
- */
216
- function serializeHyperlink(hyperlink) {
213
+ /** The attribute list of a `w:hyperlink`, without its children. */
214
+ function hyperlinkAttributes(hyperlink) {
217
215
  const attrs = [];
218
216
  if (hyperlink.rId) attrs.push(`r:id="${escapeXml(hyperlink.rId)}"`);
219
217
  if (hyperlink.anchor) attrs.push(`w:anchor="${escapeXml(hyperlink.anchor)}"`);
@@ -222,12 +220,19 @@ function serializeHyperlink(hyperlink) {
222
220
  if (hyperlink.history === true) attrs.push("w:history=\"1\"");
223
221
  else if (hyperlink.history === false) attrs.push("w:history=\"0\"");
224
222
  if (hyperlink.docLocation) attrs.push(`w:docLocation="${escapeXml(hyperlink.docLocation)}"`);
225
- const childrenXml = hyperlink.children.map((child) => {
226
- if (child.type === "run") return serializeRun(child);
227
- if (child.type === "bookmarkStart") return serializeBookmarkStart(child);
228
- return serializeBookmarkEnd(child);
229
- }).join("");
230
- return `<w:hyperlink${attrs.length > 0 ? ` ${attrs.join(" ")}` : ""}>${childrenXml}</w:hyperlink>`;
223
+ return attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
224
+ }
225
+ /** One `w:hyperlink` child, with the caller deciding how a run is written. */
226
+ function serializeHyperlinkChild(child, serializeChildRun) {
227
+ if (child.type === "run") return serializeChildRun(child);
228
+ return child.type === "bookmarkStart" ? serializeBookmarkStart(child) : serializeBookmarkEnd(child);
229
+ }
230
+ /**
231
+ * Serialize a hyperlink (w:hyperlink)
232
+ */
233
+ function serializeHyperlink(hyperlink) {
234
+ const childrenXml = hyperlink.children.map((child) => serializeHyperlinkChild(child, serializeRun)).join("");
235
+ return `<w:hyperlink${hyperlinkAttributes(hyperlink)}>${childrenXml}</w:hyperlink>`;
231
236
  }
232
237
  /**
233
238
  * Serialize bookmark start (w:bookmarkStart)
@@ -397,20 +402,39 @@ function serializeTrackedChange(tag, change) {
397
402
  return rewriteRunTextAsDeleted(contentXml);
398
403
  }).join("");
399
404
  };
400
- const contentXml = change.content.map((item) => {
401
- if (item.type === "run") {
402
- if (tag === "del" || tag === "moveFrom") return serializeDeletedRun(item);
403
- return serializeRun(item);
404
- }
405
- if (item.type === "hyperlink") return serializeHyperlink(item);
405
+ const serializeContentRun = (run) => tag === "del" || tag === "moveFrom" ? serializeDeletedRun(run) : serializeRun(run);
406
+ const serializeWrappedItem = (item) => {
407
+ if (item.type === "run") return serializeContentRun(item);
406
408
  if (item.type === "simpleField" || item.type === "complexField") {
407
409
  const xml = item.type === "simpleField" ? serializeSimpleField(item) : serializeComplexField(item);
408
410
  return tag === "del" || tag === "moveFrom" ? rewriteRunTextAsDeleted(xml) : xml;
409
411
  }
410
412
  if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") return serializeTrackedChange(trackedChangeTag(item), item);
411
413
  return item.type === "bookmarkStart" ? serializeBookmarkStart(item) : serializeBookmarkEnd(item);
412
- }).join("");
413
- return `<w:${tag} ${attrs.join(" ")}>${contentXml}</w:${tag}>`;
414
+ };
415
+ const open = `<w:${tag} ${attrs.join(" ")}>`;
416
+ const close = `</w:${tag}>`;
417
+ const wrap = (inner) => inner.length === 0 ? "" : `${open}${inner}${close}`;
418
+ if (change.content.length === 0) return `${open}${close}`;
419
+ const segments = [];
420
+ const pending = [];
421
+ const flushPending = () => {
422
+ if (pending.length > 0) {
423
+ segments.push(wrap(pending.join("")));
424
+ pending.length = 0;
425
+ }
426
+ };
427
+ for (const item of change.content) {
428
+ if (item.type === "hyperlink") {
429
+ flushPending();
430
+ const childrenXml = item.children.map((child) => serializeHyperlinkChild(child, serializeContentRun)).join("");
431
+ segments.push(`<w:hyperlink${hyperlinkAttributes(item)}>${open}${childrenXml}${close}</w:hyperlink>`);
432
+ continue;
433
+ }
434
+ pending.push(serializeWrappedItem(item));
435
+ }
436
+ flushPending();
437
+ return segments.join("");
414
438
  }
415
439
  /** Emit the `<w:commentReference>` run Word places after a comment range end. */
416
440
  function serializeCommentReferenceRun(id) {
@@ -277,11 +277,30 @@ function serializeTableCellPropertyChange(change) {
277
277
  return `<w:tcPrChange ${attrs}>${previousTcPrInner.length > 0 ? `<w:tcPr>${previousTcPrInner}</w:tcPr>` : "<w:tcPr/>"}</w:tcPrChange>`;
278
278
  }
279
279
  /**
280
- * Serialize table grid (w:tblGrid)
280
+ * Columns the grid has to declare: the widest row's span total, because a grid
281
+ * narrower than a row leaves cells with no column to sit in.
281
282
  */
282
- function serializeTableGrid(columnWidths) {
283
- if (!columnWidths || columnWidths.length === 0) return "";
284
- return `<w:tblGrid>${columnWidths.map((w) => `<w:gridCol w:w="${intAttr(w)}"/>`).join("")}</w:tblGrid>`;
283
+ function gridColumnCount(table) {
284
+ let widest = 0;
285
+ for (const row of table.rows) {
286
+ let columns = 0;
287
+ for (const cell of row.cells) columns += cell.formatting?.gridSpan ?? 1;
288
+ widest = Math.max(widest, columns);
289
+ }
290
+ return widest;
291
+ }
292
+ /**
293
+ * Serialize table grid (w:tblGrid).
294
+ *
295
+ * `w:tblGrid` is required on every `w:tbl` and `w:w` is optional on a
296
+ * `w:gridCol`, so a table whose column widths were never measured — one the
297
+ * comparison creates, say — still declares a grid, just without widths.
298
+ */
299
+ function serializeTableGrid(table) {
300
+ const columnWidths = table.columnWidths;
301
+ if (columnWidths && columnWidths.length > 0) return `<w:tblGrid>${columnWidths.map((w) => `<w:gridCol w:w="${intAttr(w)}"/>`).join("")}</w:tblGrid>`;
302
+ const columns = gridColumnCount(table);
303
+ return columns === 0 ? "<w:tblGrid/>" : `<w:tblGrid>${"<w:gridCol/>".repeat(columns)}</w:tblGrid>`;
285
304
  }
286
305
  /**
287
306
  * Serialize cell content (paragraphs, nested tables)
@@ -320,11 +339,7 @@ function serializeTableRow(row, serializeParagraph) {
320
339
  * @returns XML string for the table
321
340
  */
322
341
  function serializeTable(table, serializeParagraph) {
323
- const parts = [];
324
- const tblPrXml = serializeTableFormatting(table.formatting, table.propertyChanges);
325
- if (tblPrXml) parts.push(tblPrXml);
326
- const tblGridXml = serializeTableGrid(table.columnWidths);
327
- if (tblGridXml) parts.push(tblGridXml);
342
+ const parts = [serializeTableFormatting(table.formatting, table.propertyChanges) || "<w:tblPr/>", serializeTableGrid(table)];
328
343
  for (const row of table.rows) parts.push(serializeTableRow(row, serializeParagraph));
329
344
  return `<w:tbl>${parts.join("")}</w:tbl>`;
330
345
  }
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@ import { AIBarStatus, AIChatMode, AICitation, AICitationSource, AIGenerateInput,
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 } from "./compare/verification.js";
14
- import { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError } from "./compare/types.js";
13
+ import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion } from "./compare/verification.js";
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";
17
17
  import { CreateEmptyDocumentOptions, createEmptyDocument } from "./utils/createDocument.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, 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 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 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 };