@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.
@@ -1171,7 +1171,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1171
1171
  }
1172
1172
  case "deleteBlock":
1173
1173
  if (mode === "direct") {
1174
- tr = tr.delete(item.blockFrom, item.blockTo);
1174
+ const at = tr.doc.resolve(item.blockFrom);
1175
+ tr = !(at.index() === at.parent.childCount - 1) || at.nodeBefore?.type.name === item.blockNode.type.name ? tr.delete(item.blockFrom, item.blockTo) : tr.delete(item.blockFrom + 1, item.blockTo - 1);
1175
1176
  break;
1176
1177
  }
1177
1178
  if (deletionType) {
@@ -1189,7 +1190,8 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations, mode =
1189
1190
  const inlineRevisionApplied = item.from < item.to || atomRanges.length > 0;
1190
1191
  appliedRevisionIds = inlineRevisionApplied ? [revisionId] : [];
1191
1192
  const markPosition = tr.mapping.map(item.blockFrom);
1192
- if (tr.doc.resolve(markPosition).parent.childCount > 1 && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1193
+ const markPlace = tr.doc.resolve(markPosition);
1194
+ if (!(markPlace.index() === markPlace.parent.childCount - 1) && tr.doc.nodeAt(markPosition)?.attrs["pPrMark"] == null) {
1193
1195
  const markRevisionId = revisionSeed++;
1194
1196
  tr = tr.setNodeAttribute(markPosition, "pPrMark", {
1195
1197
  kind: isPairedMove(item.operation.moveId) ? "moveFrom" : "del",
@@ -13,11 +13,26 @@
13
13
  * gets written.
14
14
  */
15
15
  type CellContent = string | readonly BodyItem[];
16
+ /**
17
+ * One inline of a paragraph: plain text, or text carrying an external
18
+ * hyperlink. A link is the case where a revision wrapper and the linked runs
19
+ * have to nest one inside the other, so the fixture has to be able to author
20
+ * one.
21
+ */
22
+ type ParagraphInline = string | {
23
+ text: string;
24
+ href: string;
25
+ };
16
26
  /** One body-level item: a paragraph, or a table given row by row. */
17
27
  type BodyItem = {
18
28
  kind: "paragraph";
19
- text: string;
29
+ text: string | readonly ParagraphInline[];
20
30
  styleId?: string;
31
+ /**
32
+ * An authored `w14:paraId`, for a package whose ids a producer wrote
33
+ * without respecting the 31-bit bound the schema puts on them.
34
+ */
35
+ paraId?: string;
21
36
  } | {
22
37
  kind: "table";
23
38
  rows: readonly (readonly CellContent[])[];
@@ -28,6 +43,14 @@ type BodyItem = {
28
43
  */
29
44
  hiddenRows?: readonly number[];
30
45
  };
31
- declare const buildBodySequenceDocx: (items: readonly BodyItem[]) => Promise<ArrayBuffer>;
46
+ type BodySequenceOptions = {
47
+ /**
48
+ * A default header part, written as its own sequence. A header is a story of
49
+ * its own: it ends with its own paragraph, and a comparison writes it with
50
+ * its own revision ids.
51
+ */
52
+ header?: readonly BodyItem[];
53
+ };
54
+ declare const buildBodySequenceDocx: (items: readonly BodyItem[], { header }?: BodySequenceOptions) => Promise<ArrayBuffer>;
32
55
  //#endregion
33
- export { BodyItem, CellContent, buildBodySequenceDocx };
56
+ export { BodyItem, BodySequenceOptions, CellContent, ParagraphInline, buildBodySequenceDocx };
@@ -12,6 +12,10 @@ const NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
12
12
  const RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships";
13
13
  const OFFICE_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
14
14
  const WORDPROCESSING = "application/vnd.openxmlformats-officedocument.wordprocessingml";
15
+ const MARKUP_COMPATIBILITY = "http://schemas.openxmlformats.org/markup-compatibility/2006";
16
+ const PACKAGE_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006";
17
+ const CORE_PROPERTIES_TYPE = "application/vnd.openxmlformats-package.core-properties+xml";
18
+ const WORDML_2010 = "http://schemas.microsoft.com/office/word/2010/wordml";
15
19
  /**
16
20
  * `createFolders: false` because JSZip stamps the folder entries it
17
21
  * synthesizes with `new Date()`, which the fixed date above does not reach.
@@ -21,13 +25,52 @@ const ZIP_ENTRY_OPTIONS = {
21
25
  createFolders: false
22
26
  };
23
27
  /**
28
+ * A paragraph's id is derived from its own content, not from its position.
29
+ *
30
+ * Two packages authored from two descriptions are a base and a target, and a
31
+ * paragraph that appears in both is the same paragraph. Numbering the ids in
32
+ * document order would instead give the same id to the paragraph that happens
33
+ * to sit at the same index, which is how the fixture would tell a comparison
34
+ * that a removed paragraph was a rewrite of the one after it.
35
+ */
36
+ const createParaIdAllocator = () => {
37
+ const taken = /* @__PURE__ */ new Set();
38
+ return (content) => {
39
+ let hash = 2166136261;
40
+ for (let index = 0; index < content.length; index += 1) hash = Math.imul(hash ^ content.charCodeAt(index), 16777619) >>> 0;
41
+ let candidate = hash % 2147483646;
42
+ while (taken.has((candidate + 1).toString(16).toUpperCase().padStart(8, "0"))) candidate = (candidate + 1) % 2147483646;
43
+ const paraId = (candidate + 1).toString(16).toUpperCase().padStart(8, "0");
44
+ taken.add(paraId);
45
+ return paraId;
46
+ };
47
+ };
48
+ const run = (text) => `<w:r><w:t xml:space="preserve">${text}</w:t></w:r>`;
49
+ const inlineXml = (inline, { links }) => typeof inline === "string" ? run(inline) : `<w:hyperlink r:id="${links.get(inline.href) ?? ""}">${run(inline.text)}</w:hyperlink>`;
50
+ /**
24
51
  * An empty paragraph is a `w:p` with no run at all, which is what a package
25
52
  * holds for a blank line or an empty cell. It is not the same thing as a
26
53
  * paragraph whose run carries an empty string, and both shapes occur.
27
54
  */
28
- const paragraph = (text, styleId) => {
55
+ /** A blank line carries no run at all, so an empty string is no inline. */
56
+ const nonEmptyInlines = (text) => text.length === 0 ? [] : [text];
57
+ const paragraph = (text, context, { styleId, paraId } = {}) => {
29
58
  const properties = styleId === void 0 ? "" : `<w:pPr><w:pStyle w:val="${styleId}"/></w:pPr>`;
30
- return text.length === 0 ? `<w:p>${properties}</w:p>` : `<w:p>${properties}<w:r><w:t xml:space="preserve">${text}</w:t></w:r></w:p>`;
59
+ const inlines = typeof text === "string" ? nonEmptyInlines(text) : text;
60
+ const content = inlines.map((inline) => typeof inline === "string" ? inline : inline.text).join("");
61
+ const id = paraId ?? context.paraId(`${styleId ?? ""}|${content}`);
62
+ return `<w:p w14:paraId="${id}" w14:textId="${id}">${properties}${inlines.map((inline) => inlineXml(inline, context)).join("")}</w:p>`;
63
+ };
64
+ /** Every href the body carries, in document order, so the ids are stable. */
65
+ const collectHrefs = (items, hrefs) => {
66
+ for (const item of items) {
67
+ if (item.kind === "paragraph") {
68
+ if (typeof item.text === "string") continue;
69
+ for (const inline of item.text) if (typeof inline !== "string" && !hrefs.includes(inline.href)) hrefs.push(inline.href);
70
+ continue;
71
+ }
72
+ for (const row of item.rows) for (const cell of row) if (typeof cell !== "string") collectHrefs(cell, hrefs);
73
+ }
31
74
  };
32
75
  const EMPTY_PARAGRAPH = {
33
76
  kind: "paragraph",
@@ -44,20 +87,46 @@ const closedSequence = (items) => {
44
87
  return last === void 0 || last.kind === "table" ? [...items, EMPTY_PARAGRAPH] : items;
45
88
  };
46
89
  /** A cell must also contain a paragraph, which the empty sequence supplies. */
47
- const cellXml = (content) => typeof content === "string" ? paragraph(content) : itemsXml(closedSequence(content));
48
- const table = (item) => {
90
+ const cellXml = (content, context) => typeof content === "string" ? paragraph(content, context) : itemsXml(closedSequence(content), context);
91
+ /** `w:tbl` is `w:tblPr, w:tblGrid, rows`: a fixture without the grid is not one. */
92
+ const tableGrid = (rows) => {
93
+ let columns = 0;
94
+ for (const cells of rows) columns = Math.max(columns, cells.length);
95
+ return `<w:tblGrid>${`<w:gridCol w:w="2000"/>`.repeat(columns)}</w:tblGrid>`;
96
+ };
97
+ const table = (item, context) => {
49
98
  const hidden = new Set(item.hiddenRows ?? []);
50
- return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>` + item.rows.map((cells, rowIndex) => `<w:tr>${hidden.has(rowIndex) ? `<w:trPr><w:hidden/></w:trPr>` : ""}${cells.map((content) => `<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>${cellXml(content)}</w:tc>`).join("")}</w:tr>`).join("") + `</w:tbl>`;
99
+ return `<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/></w:tblPr>` + tableGrid(item.rows) + item.rows.map((cells, rowIndex) => `<w:tr>${hidden.has(rowIndex) ? `<w:trPr><w:hidden/></w:trPr>` : ""}${cells.map((content) => `<w:tc><w:tcPr><w:tcW w:w="2000" w:type="dxa"/></w:tcPr>${cellXml(content, context)}</w:tc>`).join("")}</w:tr>`).join("") + `</w:tbl>`;
51
100
  };
52
- const itemsXml = (items) => items.map((item) => item.kind === "paragraph" ? paragraph(item.text, item.styleId) : table(item)).join("");
53
- const bodyXml = (items) => itemsXml(closedSequence(items));
54
- const buildBodySequenceDocx = async (items) => {
101
+ const itemsXml = (items, context) => items.map((item) => item.kind === "paragraph" ? paragraph(item.text, context, {
102
+ ...item.styleId === void 0 ? {} : { styleId: item.styleId },
103
+ ...item.paraId === void 0 ? {} : { paraId: item.paraId }
104
+ }) : table(item, context)).join("");
105
+ const bodyXml = (items, context) => itemsXml(closedSequence(items), context);
106
+ /** The relationship id the default header takes when a fixture asks for one. */
107
+ const HEADER_RELATIONSHIP_ID = "rId2";
108
+ const buildBodySequenceDocx = async (items, { header } = {}) => {
109
+ const hrefs = [];
110
+ collectHrefs(closedSequence(items), hrefs);
111
+ collectHrefs(closedSequence(header ?? []), hrefs);
112
+ const firstLinkRelationship = header ? 3 : 2;
113
+ const links = new Map(hrefs.map((href, index) => [href, `rId${index + firstLinkRelationship}`]));
114
+ const linkRelationships = hrefs.map((href, index) => `<Relationship Id="rId${index + firstLinkRelationship}" Type="${OFFICE_RELATIONSHIPS}/hyperlink" Target="${href}" TargetMode="External"/>`).join("");
115
+ const paraId = createParaIdAllocator();
55
116
  const parts = {
56
- "[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="${WORDPROCESSING}.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="${WORDPROCESSING}.styles+xml"/></Types>`,
57
- "_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/officeDocument" Target="word/document.xml"/></Relationships>`,
58
- "word/_rels/document.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/styles" Target="styles.xml"/></Relationships>`,
117
+ "[Content_Types].xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="${WORDPROCESSING}.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="${WORDPROCESSING}.styles+xml"/>` + (header ? `<Override PartName="/word/header1.xml" ContentType="${WORDPROCESSING}.header+xml"/>` : "") + `<Override PartName="/docProps/core.xml" ContentType="${CORE_PROPERTIES_TYPE}"/></Types>`,
118
+ "_rels/.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="${PACKAGE_RELATIONSHIPS}/metadata/core-properties" Target="docProps/core.xml"/></Relationships>`,
119
+ "docProps/core.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="${PACKAGE_RELATIONSHIPS}/metadata/core-properties" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dcterms:created xsi:type="dcterms:W3CDTF">2000-01-01T00:00:00Z</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">2000-01-01T00:00:00Z</dcterms:modified></cp:coreProperties>`,
120
+ "word/_rels/document.xml.rels": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="${RELATIONSHIPS}"><Relationship Id="rId1" Type="${OFFICE_RELATIONSHIPS}/styles" Target="styles.xml"/>` + (header ? `<Relationship Id="${HEADER_RELATIONSHIP_ID}" Type="${OFFICE_RELATIONSHIPS}/header" Target="header1.xml"/>` : "") + linkRelationships + `</Relationships>`,
59
121
  "word/styles.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="${NAMESPACE}"><w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/></w:style><w:style w:type="paragraph" w:styleId="Heading1"><w:name w:val="heading 1"/></w:style></w:styles>`,
60
- "word/document.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="${NAMESPACE}"><w:body>` + bodyXml(items) + "<w:sectPr><w:pgSz w:w=\"12240\" w:h=\"15840\"/><w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" w:left=\"1440\"/></w:sectPr></w:body></w:document>"
122
+ "word/document.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="${NAMESPACE}" xmlns:r="${OFFICE_RELATIONSHIPS}" xmlns:mc="${MARKUP_COMPATIBILITY}" xmlns:w14="${WORDML_2010}" mc:Ignorable="w14"><w:body>` + bodyXml(items, {
123
+ links,
124
+ paraId
125
+ }) + `<w:sectPr>` + (header ? `<w:headerReference w:type="default" r:id="${HEADER_RELATIONSHIP_ID}"/>` : "") + "<w:pgSz w:w=\"12240\" w:h=\"15840\"/><w:pgMar w:top=\"1440\" w:right=\"1440\" w:bottom=\"1440\" w:left=\"1440\"/></w:sectPr></w:body></w:document>",
126
+ ...header ? { "word/header1.xml": `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:hdr xmlns:w="${NAMESPACE}" xmlns:r="${OFFICE_RELATIONSHIPS}" xmlns:mc="${MARKUP_COMPATIBILITY}" xmlns:w14="${WORDML_2010}" mc:Ignorable="w14">${itemsXml(closedSequence(header), {
127
+ links,
128
+ paraId
129
+ })}</w:hdr>` } : {}
61
130
  };
62
131
  const zip = new JSZip();
63
132
  for (const name of Object.keys(parts).toSorted()) zip.file(name, parts[name] ?? "", ZIP_ENTRY_OPTIONS);
@@ -3,7 +3,7 @@ import { WordDiffGranularity } from "../ai-edits/word-diff.js";
3
3
  import { FolioRevisionStamp } from "../ai-edits/apply.js";
4
4
  import { FolioDocumentStoryHandle, FolioDocxReviewer } from "../ai-edits/headless.js";
5
5
  import { CompareVerification } from "./verification.js";
6
- import { CompareChange, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxSerializeError, CompareResult, CompareUnsupportedPart, InvalidCompareDocxOptionsError } from "./types.js";
6
+ import { CompareChange, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxSerializeError, CompareResult, CompareUnsupportedPart, InvalidCompareDocxOptionsError } from "./types.js";
7
7
  import { CompareStoryPlan } from "./plan.js";
8
8
  import { Result } from "better-result";
9
9
  //#region src/compare/compare.d.ts
@@ -89,7 +89,7 @@ declare const applyComparison: ({ reviewer, revisionStamp, granularity, numberin
89
89
  * composition let the benchmark's own composition disagree with the shipped
90
90
  * one within a single run.
91
91
  */
92
- declare const serializeComparison: ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Promise<Result<ArrayBuffer, CompareDocxSerializeError>>;
92
+ declare const serializeComparison: ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate }: ParsedComparison, planned: readonly PlannedStoryComparison[]) => Promise<Result<ArrayBuffer, CompareDocxSerializeError | CompareDocxFinalParagraphMarkError>>;
93
93
  /**
94
94
  * Compare `base` against `target` and return `base` carrying the tracked
95
95
  * changes that turn it into `target`, alongside the change list describing
@@ -3,8 +3,8 @@ import "../document-operations.js";
3
3
  import { pairFolioDocumentStories } from "../document-stories.js";
4
4
  import { planStoryCompare } from "./plan.js";
5
5
  import { withFixedPackageDates } from "./reproducible-package.js";
6
- import { CompareDocxApplyError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
7
- import { classifyProjectionMismatch, projectSupportedInlineFormatting } from "./verification.js";
6
+ import { CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "./types.js";
7
+ import { classifyProjectionMismatch, deletedFinalParagraphMarks, projectSupportedInlineFormatting } from "./verification.js";
8
8
  import { Result, panic } from "better-result";
9
9
  //#region src/compare/compare.ts
10
10
  /**
@@ -354,6 +354,12 @@ const applyComparison = ({ reviewer, revisionStamp, granularity, numberingChange
354
354
  */
355
355
  const serializeComparison = async ({ baseBuffer, baseCarriedRevisions, reviewer, packageDate }, planned) => {
356
356
  if (!baseCarriedRevisions && planned.every(({ plan }) => plan.operations.length === 0)) return Result.ok(baseBuffer);
357
+ const deletions = deletedFinalParagraphMarks(reviewer.toDocument());
358
+ const [firstDeletion] = deletions;
359
+ if (firstDeletion !== void 0) return Result.err(new CompareDocxFinalParagraphMarkError({
360
+ message: `A container's final paragraph mark carries a ${firstDeletion.kind}, which no consumer can resolve: ${firstDeletion.container} paragraph ${String(firstDeletion.paragraphIndex)}.`,
361
+ deletions
362
+ }));
357
363
  return await Result.tryPromise({
358
364
  try: async () => await withFixedPackageDates(await reviewer.toBuffer(), packageDate),
359
365
  catch: (cause) => new CompareDocxSerializeError({
@@ -701,6 +701,82 @@ const locationOf = (story, block) => block.table ? {
701
701
  cell: block.table
702
702
  } : { story };
703
703
  /**
704
+ * The container a paragraph mark ends: the story's body, or one table cell. A
705
+ * mark joins the paragraph it ends with the next paragraph of the SAME
706
+ * container, so two blocks on either side of a boundary are not neighbours for
707
+ * this purpose however adjacent they read.
708
+ */
709
+ const containerKeyOf = ({ table }) => table ? `t${String(table.tableIndex)}r${String(table.rowIndex)}c${String(table.cellIndex)}` : "body";
710
+ /** Each container's last block, in the order the story holds them. */
711
+ const lastBlockByContainer = (blocks) => {
712
+ const last = /* @__PURE__ */ new Map();
713
+ for (const block of blocks) last.set(containerKeyOf(block), block);
714
+ return last;
715
+ };
716
+ /**
717
+ * The operations that keep a container's final paragraph mark when the plan
718
+ * deletes the paragraphs that end it.
719
+ *
720
+ * A deleted paragraph mark means "merge this paragraph into the following
721
+ * one". A container's final paragraph has no following one, so the mark cannot
722
+ * say it: the applier leaves that mark alone, and the removal has to be
723
+ * expressed one paragraph earlier. The chain therefore runs from the last
724
+ * SURVIVING paragraph forward — its mark goes, each removed paragraph's mark
725
+ * goes with it, and the container's final paragraph stays as the carrier the
726
+ * merged text lands in.
727
+ *
728
+ * The carrier keeps its own mark, and a paragraph's properties live on its
729
+ * mark, so the merged paragraph ends up with the carrier's. Those properties
730
+ * therefore become the target's, written as `w:pPrChange` so rejecting
731
+ * restores what the carrier had. That is bookkeeping for the merge rather than
732
+ * an edit of its own, so it adds no entry to the change list: what the reader
733
+ * is told is that the paragraphs were removed.
734
+ *
735
+ * Nothing is rotated when the plan puts a block inside or after the run. The
736
+ * carrier is then no longer what ends the container, its mark is deleted like
737
+ * any other, and the count already works out.
738
+ */
739
+ const trailingDeletionOperations = ({ baseSnapshot, targetSnapshot, operations, nextOperationId }) => {
740
+ const blocks = baseSnapshot.blocks;
741
+ const deletedBlockIds = new Set(operations.flatMap((operation) => operation.type === "deleteBlock" ? [operation.blockId] : []));
742
+ if (deletedBlockIds.size === 0) return [];
743
+ const placedAtBlockIds = new Set(operations.flatMap((operation) => operation.type === "insertBeforeBlock" || operation.type === "insertAfterBlock" || operation.type === "insertTable" ? [operation.blockId] : []));
744
+ const markedBlockIds = new Set(operations.flatMap((operation) => operation.type === "splitBlock" || operation.type === "mergeBlockWithNext" ? [operation.blockId] : []));
745
+ const targetLastByContainer = lastBlockByContainer(targetSnapshot.blocks);
746
+ const indexById = new Map(blocks.map((block, index) => [block.id, index]));
747
+ const added = [];
748
+ for (const [container, carrier] of lastBlockByContainer(blocks)) {
749
+ if (!deletedBlockIds.has(carrier.id)) continue;
750
+ let index = indexById.get(carrier.id) ?? panic("A container's last block is not in the snapshot it came from", { blockId: carrier.id });
751
+ let chainStart = null;
752
+ let placedInRun = false;
753
+ for (; index >= 0; index--) {
754
+ const block = blocks[index];
755
+ if (block === void 0 || containerKeyOf(block) !== container) break;
756
+ if (!deletedBlockIds.has(block.id)) {
757
+ chainStart = markedBlockIds.has(block.id) ? null : block;
758
+ break;
759
+ }
760
+ placedInRun ||= placedAtBlockIds.has(block.id);
761
+ }
762
+ if (placedInRun) continue;
763
+ if (chainStart) added.push({
764
+ id: nextOperationId(),
765
+ type: "mergeBlockWithNext",
766
+ blockId: chainStart.id
767
+ });
768
+ const targetCarrier = targetLastByContainer.get(container);
769
+ const properties = targetCarrier && changedParagraphProperties(carrier, targetCarrier);
770
+ if (properties) added.push({
771
+ id: nextOperationId(),
772
+ type: "setBlockParagraphProperties",
773
+ blockId: carrier.id,
774
+ properties
775
+ });
776
+ }
777
+ return added;
778
+ };
779
+ /**
704
780
  * Plan one story's comparison, or `null` when it needs more operations than
705
781
  * `maxOperations`.
706
782
  */
@@ -1050,6 +1126,12 @@ const planStoryCompare = ({ story, baseSnapshot, targetSnapshot, maxOperations }
1050
1126
  }
1051
1127
  if (operations.length > maxOperations) return null;
1052
1128
  }
1129
+ operations.push(...trailingDeletionOperations({
1130
+ baseSnapshot,
1131
+ targetSnapshot,
1132
+ operations,
1133
+ nextOperationId
1134
+ }));
1053
1135
  return operations.length > maxOperations ? null : {
1054
1136
  changes,
1055
1137
  operations
@@ -1,6 +1,6 @@
1
1
  //#region src/compare/reproducible-package.d.ts
2
2
  /**
3
- * The last clock in the compare path is the ZIP container itself.
3
+ * The last two clocks in the compare path are outside the document body.
4
4
  *
5
5
  * Every part the serializer rewrites is stored with JSZip's default entry
6
6
  * date, which is `new Date()`. The XML is identical between two runs, but the
@@ -8,8 +8,12 @@
8
8
  * both runs land in the same two-second bucket — so the packages match most of
9
9
  * the time and differ occasionally, which is worse than differing always.
10
10
  *
11
- * Restamping every entry from the comparison's own timestamp removes it. It
12
- * also states the truth about the package: a generated redline is dated by the
11
+ * The save also stamps `dcterms:modified` in `docProps/core.xml` from the wall
12
+ * clock, which is the same failure one part deeper: two runs over identical
13
+ * inputs differ in that part alone.
14
+ *
15
+ * Restamping both from the comparison's own timestamp removes them. It also
16
+ * states the truth about the package: a generated redline is dated by the
13
17
  * comparison that produced it, not by the second it happened to be written.
14
18
  */
15
19
  declare const withFixedPackageDates: (buffer: ArrayBuffer, date: Date) => Promise<ArrayBuffer>;
@@ -1,7 +1,7 @@
1
1
  import JSZip from "jszip";
2
2
  //#region src/compare/reproducible-package.ts
3
3
  /**
4
- * The last clock in the compare path is the ZIP container itself.
4
+ * The last two clocks in the compare path are outside the document body.
5
5
  *
6
6
  * Every part the serializer rewrites is stored with JSZip's default entry
7
7
  * date, which is `new Date()`. The XML is identical between two runs, but the
@@ -9,14 +9,31 @@ import JSZip from "jszip";
9
9
  * both runs land in the same two-second bucket — so the packages match most of
10
10
  * the time and differ occasionally, which is worse than differing always.
11
11
  *
12
- * Restamping every entry from the comparison's own timestamp removes it. It
13
- * also states the truth about the package: a generated redline is dated by the
12
+ * The save also stamps `dcterms:modified` in `docProps/core.xml` from the wall
13
+ * clock, which is the same failure one part deeper: two runs over identical
14
+ * inputs differ in that part alone.
15
+ *
16
+ * Restamping both from the comparison's own timestamp removes them. It also
17
+ * states the truth about the package: a generated redline is dated by the
14
18
  * comparison that produced it, not by the second it happened to be written.
15
19
  */
16
20
  /** JSZip deflate level `repackDocx` writes DOCX parts at. */
17
21
  const DOCX_COMPRESSION_LEVEL = 6;
22
+ const CORE_PROPERTIES_PATH = "docProps/core.xml";
23
+ const MODIFIED_ELEMENT = /<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u;
24
+ /**
25
+ * Rewrite `dcterms:modified` where the save already wrote one. An absent
26
+ * element stays absent: the comparison edits the document it was handed, and
27
+ * synthesizing metadata the input never carried is a different decision.
28
+ */
29
+ const withFixedModifiedDate = (corePropsXml, date) => corePropsXml.replace(MODIFIED_ELEMENT, `<dcterms:modified xsi:type="dcterms:W3CDTF">${date.toISOString()}</dcterms:modified>`);
18
30
  const withFixedPackageDates = async (buffer, date) => {
19
31
  const zip = await JSZip.loadAsync(buffer);
32
+ const coreProps = zip.file(CORE_PROPERTIES_PATH);
33
+ if (coreProps) zip.file(CORE_PROPERTIES_PATH, withFixedModifiedDate(await coreProps.async("text"), date), {
34
+ compression: "DEFLATE",
35
+ compressionOptions: { level: DOCX_COMPRESSION_LEVEL }
36
+ });
20
37
  zip.forEach((_path, file) => {
21
38
  file.date = date;
22
39
  });
@@ -1,7 +1,7 @@
1
1
  import { FolioAIBlockParagraphProperties, FolioAIBlockTableLocation, FolioAIEditSkippedOperation, FolioAIInlineFormatting } from "../ai-edits/types.js";
2
2
  import { WordDiffGranularity } from "../ai-edits/word-diff.js";
3
3
  import { FolioDocumentStoryHandle, FolioNumberingLevel } from "../ai-edits/headless.js";
4
- import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant } from "./verification.js";
4
+ import { CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion } from "./verification.js";
5
5
  //#region src/compare/types.d.ts
6
6
  /** Everything {@link compareDocx} needs; nothing it reads from the ambient clock. */
7
7
  type CompareDocxOptions = {
@@ -277,6 +277,21 @@ declare class CompareDocxSerializeError extends CompareDocxSerializeError_base<{
277
277
  message: string;
278
278
  cause: unknown;
279
279
  }> {}
280
- type CompareDocxError = CompareDocxApplyError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
280
+ declare const CompareDocxFinalParagraphMarkError_base: import("better-result").TaggedErrorClass<"CompareDocxFinalParagraphMarkError">;
281
+ /**
282
+ * A container's final paragraph mark carries a deletion, so the package would
283
+ * not open.
284
+ *
285
+ * A deleted paragraph mark means "merge this paragraph into the following
286
+ * one", and a container's last paragraph has no following one. Checked before
287
+ * the package is written, and fatal under either `onUnverified` setting: there
288
+ * is no redline to emit when a consumer refuses the file.
289
+ */
290
+ declare class CompareDocxFinalParagraphMarkError extends CompareDocxFinalParagraphMarkError_base<{
291
+ message: string;
292
+ /** Every container that carries one, each named structurally. */
293
+ deletions: readonly FinalParagraphMarkDeletion[];
294
+ }> {}
295
+ type CompareDocxError = CompareDocxApplyError | CompareDocxFinalParagraphMarkError | CompareDocxOperationLimitError | CompareDocxParseError | CompareDocxRoundTripError | CompareDocxSerializeError | InvalidCompareDocxOptionsError;
281
296
  //#endregion
282
- export { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };
297
+ export { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError };
@@ -28,5 +28,15 @@ var CompareDocxRoundTripError = class extends TaggedError("CompareDocxRoundTripE
28
28
  /** The difference needs more operations than the engine will generate. */
29
29
  var CompareDocxOperationLimitError = class extends TaggedError("CompareDocxOperationLimitError") {};
30
30
  var CompareDocxSerializeError = class extends TaggedError("CompareDocxSerializeError") {};
31
+ /**
32
+ * A container's final paragraph mark carries a deletion, so the package would
33
+ * not open.
34
+ *
35
+ * A deleted paragraph mark means "merge this paragraph into the following
36
+ * one", and a container's last paragraph has no following one. Checked before
37
+ * the package is written, and fatal under either `onUnverified` setting: there
38
+ * is no redline to emit when a consumer refuses the file.
39
+ */
40
+ var CompareDocxFinalParagraphMarkError = class extends TaggedError("CompareDocxFinalParagraphMarkError") {};
31
41
  //#endregion
32
- export { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError };
42
+ export { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError };
@@ -51,5 +51,33 @@ type ClassifyOptions = {
51
51
  * a divergence always produces a failure rather than being dropped.
52
52
  */
53
53
  declare const classifyProjectionMismatch: ({ invariant, story, actual, expected }: ClassifyOptions) => CompareVerificationFailure | null;
54
+ /**
55
+ * A container whose last paragraph carries a deletion on its mark.
56
+ *
57
+ * Structural facts only — a path through the package model and an index — so
58
+ * the finding is safe to log, report or quote.
59
+ */
60
+ type FinalParagraphMarkDeletion = {
61
+ /** Where the container sits in the package model, e.g. `package.document.content`. */
62
+ container: string;
63
+ /** The paragraph's index among its container's children. */
64
+ paragraphIndex: number;
65
+ /** The mark kind found there: `del`, or `moveFrom` for a relocation's source. */
66
+ kind: "del" | "moveFrom";
67
+ };
68
+ /**
69
+ * Every container in a package whose final paragraph mark carries a deletion.
70
+ *
71
+ * A deleted paragraph mark says "join this paragraph with the one after it".
72
+ * The last paragraph of a body, a table cell, a header or footer, a note or a
73
+ * text box has no paragraph after it, so the mark states an edit that cannot
74
+ * be carried out, and a consumer refuses the package rather than opening it.
75
+ *
76
+ * The walk is over the package model rather than over a list of the containers
77
+ * known today: a container is any sequence that ends in a paragraph, so a part
78
+ * the model grows later is covered the day it arrives instead of the day
79
+ * someone remembers this function.
80
+ */
81
+ declare const deletedFinalParagraphMarks: (packageModel: unknown) => FinalParagraphMarkDeletion[];
54
82
  //#endregion
55
- export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, classifyProjectionMismatch, projectSupportedInlineFormatting, sameProjection };
83
+ export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion, classifyProjectionMismatch, deletedFinalParagraphMarks, projectSupportedInlineFormatting, sameProjection };
@@ -142,5 +142,48 @@ const classifyProjectionMismatch = ({ invariant, story, actual, expected }) => {
142
142
  if (actual.length !== expected.length) return failure("block-count", `${counts}, first differing ${at}`);
143
143
  return failure("text", `a ${containerKind(left.container)} block's text does not match ${at}, length ${String(left.text.length)} against ${String(right.text.length)} (${counts})`);
144
144
  };
145
+ /** The mark kinds that resolve by joining the paragraph with the one after it. */
146
+ const JOINS_FORWARD_ON_ACCEPT = Object.freeze(["del", "moveFrom"]);
147
+ const joinsForwardOnAccept = (value) => JOINS_FORWARD_ON_ACCEPT.some((kind) => kind === value);
148
+ const isRecord = (value) => typeof value === "object" && value !== null;
149
+ const isParagraph = (value) => isRecord(value) && value["type"] === "paragraph";
150
+ /**
151
+ * Every container in a package whose final paragraph mark carries a deletion.
152
+ *
153
+ * A deleted paragraph mark says "join this paragraph with the one after it".
154
+ * The last paragraph of a body, a table cell, a header or footer, a note or a
155
+ * text box has no paragraph after it, so the mark states an edit that cannot
156
+ * be carried out, and a consumer refuses the package rather than opening it.
157
+ *
158
+ * The walk is over the package model rather than over a list of the containers
159
+ * known today: a container is any sequence that ends in a paragraph, so a part
160
+ * the model grows later is covered the day it arrives instead of the day
161
+ * someone remembers this function.
162
+ */
163
+ const deletedFinalParagraphMarks = (packageModel) => {
164
+ const found = [];
165
+ const visit = (value, path) => {
166
+ if (Array.isArray(value)) {
167
+ const last = value.at(-1);
168
+ const mark = isParagraph(last) ? last["pPrMark"] : void 0;
169
+ const kind = isRecord(mark) ? mark["kind"] : void 0;
170
+ if (joinsForwardOnAccept(kind)) found.push({
171
+ container: path,
172
+ paragraphIndex: value.length - 1,
173
+ kind
174
+ });
175
+ for (const [index, item] of value.entries()) visit(item, `${path}[${String(index)}]`);
176
+ return;
177
+ }
178
+ if (value instanceof Map) {
179
+ for (const [key, item] of value) visit(item, `${path}.${String(key)}`);
180
+ return;
181
+ }
182
+ if (!isRecord(value) || value instanceof Date || ArrayBuffer.isView(value)) return;
183
+ for (const [key, item] of Object.entries(value)) visit(item, `${path}.${key}`);
184
+ };
185
+ visit(packageModel, "package");
186
+ return found;
187
+ };
145
188
  //#endregion
146
- export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, classifyProjectionMismatch, projectSupportedInlineFormatting, sameProjection };
189
+ export { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, classifyProjectionMismatch, deletedFinalParagraphMarks, projectSupportedInlineFormatting, sameProjection };
@@ -9,8 +9,8 @@ import { AIBarStatus, AIChatMode, AICitation, AICitationSource, AIGenerateInput,
9
9
  import { ApplyResult, applySuggestions } from "../ai-suggestions/apply.js";
10
10
  import { ResolvedAnchor, isSuggestionStale, resolveSuggestionAnchor } from "../ai-suggestions/conflict.js";
11
11
  import { PositionalText, buildPositionalText } from "../ai-suggestions/text-positions.js";
12
- import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant } from "../compare/verification.js";
13
- import { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError } from "../compare/types.js";
12
+ import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareVerification, CompareVerificationCause, CompareVerificationFailure, CompareVerificationInvariant, FinalParagraphMarkDeletion } from "../compare/verification.js";
13
+ import { COMPARE_UNSUPPORTED_REASONS, CompareChange, CompareChangeLocation, CompareDocxApplyError, CompareDocxError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxOptions, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, CompareFormatRange, CompareResult, CompareUnsupportedPart, CompareUnsupportedReason, InvalidCompareDocxOptionsError } from "../compare/types.js";
14
14
  import { MAX_COMPARE_OPERATIONS, compareDocx } from "../compare/compare.js";
15
15
  import { DOCUMENT_PRESET_VERSION, DOCUMENT_STYLE_SET_VERSION, DocumentPreset, DocumentStyleSet } from "../style-sets/types.js";
16
16
  import { CreateEmptyDocumentOptions, createEmptyDocument } from "../utils/createDocument.js";
@@ -37,4 +37,4 @@ import { getGoogleFontsEnabled, setEmbeddedFontFamilyMap, setGoogleFontsEnabled
37
37
  import { DOCX_CONFORMANCE_CLASSES } from "../index.js";
38
38
  type Document = document_d_exports.Document;
39
39
  type DocxConformanceClass = document_d_exports.DocxConformanceClass;
40
- 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 };
40
+ 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 };
@@ -6,7 +6,7 @@ import { isSuggestionStale, resolveSuggestionAnchor } from "../ai-suggestions/co
6
6
  import { buildPositionalText } from "../ai-suggestions/text-positions.js";
7
7
  import { DEFAULT_AI_SUGGESTION_PRESETS } from "../ai-suggestions/types.js";
8
8
  import { MAX_COMPARE_OPERATIONS, compareDocx } from "../compare/compare.js";
9
- import { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "../compare/types.js";
9
+ import { COMPARE_UNSUPPORTED_REASONS, CompareDocxApplyError, CompareDocxFinalParagraphMarkError, CompareDocxOperationLimitError, CompareDocxParseError, CompareDocxRoundTripError, CompareDocxSerializeError, InvalidCompareDocxOptionsError } from "../compare/types.js";
10
10
  import { COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS } from "../compare/verification.js";
11
11
  import { 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, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "../document-operations.js";
12
12
  import { inspectDocxCompatibility } from "../docx/compatibility.js";
@@ -31,4 +31,4 @@ import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialF
31
31
  import { createEmptyDocument } from "../utils/createDocument.js";
32
32
  import { getGoogleFontsEnabled, setEmbeddedFontFamilyMap, setGoogleFontsEnabled } from "../utils/fontResolver.js";
33
33
  import { mergeDocumentContent } from "../utils/mergeDocumentContent.js";
34
- export { COMPARE_UNSUPPORTED_REASONS, COMPARE_VERIFICATION_CAUSES, COMPARE_VERIFICATION_INVARIANTS, CompareDocxApplyError, 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, 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 };