@stll/folio-core 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +35 -5
  6. package/dist/ai-edits/headless.js +92 -22
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -115
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.d.ts +6 -0
  22. package/dist/docx/encryption/agileDecryption.js +209 -0
  23. package/dist/docx/encryption/compoundFile.d.ts +20 -0
  24. package/dist/docx/encryption/compoundFile.js +251 -0
  25. package/dist/docx/encryption/containerFormat.d.ts +15 -0
  26. package/dist/docx/encryption/containerFormat.js +40 -0
  27. package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
  28. package/dist/docx/encryption/cryptoBytes.js +55 -0
  29. package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
  30. package/dist/docx/encryption/encryptionInfo.js +128 -0
  31. package/dist/docx/encryption/errors.d.ts +18 -0
  32. package/dist/docx/encryption/errors.js +13 -0
  33. package/dist/docx/encryption/index.d.ts +4 -0
  34. package/dist/docx/encryption/index.js +4 -0
  35. package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
  36. package/dist/docx/encryption/openEncryptedDocx.js +65 -0
  37. package/dist/docx/footnoteParser.d.ts +1 -17
  38. package/dist/docx/footnoteParser.js +1 -27
  39. package/dist/docx/groupDrawingParser.d.ts +8 -0
  40. package/dist/docx/groupDrawingParser.js +131 -0
  41. package/dist/docx/index.d.ts +3 -1
  42. package/dist/docx/index.js +3 -1
  43. package/dist/docx/numberingParser.d.ts +3 -1
  44. package/dist/docx/numberingParser.js +8 -2
  45. package/dist/docx/paragraphParser.js +9 -3
  46. package/dist/docx/parser.d.ts +4 -5
  47. package/dist/docx/parser.js +8 -2
  48. package/dist/docx/runParser.js +8 -1
  49. package/dist/docx/selectiveXmlPatch.js +4 -10
  50. package/dist/docx/serializer/numberingSerializer.js +3 -1
  51. package/dist/docx/settingsParser.d.ts +6 -3
  52. package/dist/docx/settingsParser.js +2 -0
  53. package/dist/docx/unzip.d.ts +7 -5
  54. package/dist/docx/unzip.js +8 -2
  55. package/dist/docx/xmlParser.js +11 -4
  56. package/dist/index.d.ts +3 -2
  57. package/dist/index.js +6 -5
  58. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  59. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  60. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
  61. package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
  63. package/dist/layout-engine/index.d.ts +6 -5
  64. package/dist/layout-engine/index.js +116 -27
  65. package/dist/layout-engine/keep-together.d.ts +13 -8
  66. package/dist/layout-engine/keep-together.js +40 -25
  67. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  68. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  69. package/dist/layout-engine/measure/measureBlocks.js +43 -19
  70. package/dist/layout-engine/measure/measureParagraph.js +91 -14
  71. package/dist/layout-engine/paginator.d.ts +2 -1
  72. package/dist/layout-engine/paginator.js +9 -3
  73. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  74. package/dist/layout-engine/textBoxGroup.js +8 -0
  75. package/dist/layout-engine/types.d.ts +33 -9
  76. package/dist/layout-engine/types.js +20 -2
  77. package/dist/layout-painter/renderPage.d.ts +15 -1
  78. package/dist/layout-painter/renderPage.js +29 -12
  79. package/dist/layout-painter/renderParagraph.js +44 -9
  80. package/dist/layout-painter/renderTable.js +27 -7
  81. package/dist/managers/DocumentLoaderManager.d.ts +3 -1
  82. package/dist/managers/DocumentLoaderManager.js +13 -5
  83. package/dist/managers/TableSelectionManager.js +1 -1
  84. package/dist/model.js +1 -1
  85. package/dist/paged-layout/pageText.d.ts +14 -0
  86. package/dist/paged-layout/pageText.js +21 -0
  87. package/dist/prosemirror/attrs/index.js +3 -0
  88. package/dist/prosemirror/commands/comments.js +75 -5
  89. package/dist/prosemirror/commands/hyperlink.js +4 -2
  90. package/dist/prosemirror/commands/image.js +2 -1
  91. package/dist/prosemirror/commands/index.js +1 -1
  92. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  93. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  94. package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
  95. package/dist/prosemirror/conversion/toProseDoc.js +43 -15
  96. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
  97. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
  99. package/dist/prosemirror/findReplaceSelection.js +2 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/insertOperations.js +1 -1
  102. package/dist/prosemirror/schema/nodes.d.ts +14 -7
  103. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  104. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  105. package/dist/redline.d.ts +20 -0
  106. package/dist/redline.js +127 -0
  107. package/dist/server.d.ts +7 -3
  108. package/dist/server.js +6 -2
  109. package/dist/symbols.d.ts +35 -0
  110. package/dist/symbols.js +512 -0
  111. package/dist/utils/clipboard.js +3 -3
  112. package/dist/utils/fontLoader.js +3 -0
  113. package/dist/utils/fontResolver.js +65 -1
  114. package/dist/utils/zoom.d.ts +21 -0
  115. package/dist/utils/zoom.js +21 -0
  116. package/dist/version-comparison.d.ts +92 -0
  117. package/dist/version-comparison.js +436 -0
  118. package/package.json +7 -3
  119. package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
  120. package/dist/paged-layout/headerFooterMargins.js +0 -120
@@ -152,7 +152,8 @@ const tableSpec = {
152
152
  cellMargins: { default: null },
153
153
  look: { default: null },
154
154
  borders: { default: null },
155
- _originalFormatting: { default: null }
155
+ _originalFormatting: { default: null },
156
+ tblPrChange: { default: null }
156
157
  },
157
158
  parseDOM: [{
158
159
  tag: "table",
@@ -171,17 +172,12 @@ const tableSpec = {
171
172
  const domAttrs = { class: "docx-table" };
172
173
  if (attrs.styleId) domAttrs["data-style-id"] = attrs.styleId;
173
174
  const styles = ["border-collapse: collapse"];
174
- if (attrs.width !== void 0 && attrs.widthType === "pct") {
175
- styles.push(`width: ${attrs.width / 50}%`);
176
- styles.push("table-layout: fixed");
177
- } else if (attrs.width !== void 0 && attrs.widthType === "dxa") {
175
+ if (attrs.width !== void 0 && attrs.widthType === "pct") styles.push(`width: ${attrs.width / 50}%`);
176
+ else if (attrs.width !== void 0 && attrs.widthType === "dxa") {
178
177
  const widthPx = Math.round(attrs.width / 20 * 1.333);
179
178
  styles.push(`width: ${widthPx}px`);
180
- styles.push("table-layout: fixed");
181
- } else {
182
- styles.push("width: 100%");
183
- styles.push("table-layout: fixed");
184
- }
179
+ } else styles.push("width: 100%");
180
+ styles.push("table-layout: fixed");
185
181
  if (attrs.justification === "center") styles.push("margin-left: auto", "margin-right: auto");
186
182
  else if (attrs.justification === "right") styles.push("margin-left: auto");
187
183
  domAttrs["style"] = styles.join("; ");
@@ -199,13 +195,16 @@ const tableRowSpec = {
199
195
  height: { default: null },
200
196
  heightRule: { default: null },
201
197
  isHeader: { default: false },
202
- _originalFormatting: { default: null }
198
+ hidden: { default: false },
199
+ _originalFormatting: { default: null },
200
+ trPrChange: { default: null }
203
201
  },
204
202
  parseDOM: [{ tag: "tr" }],
205
203
  toDOM(node) {
206
204
  const attrs = expectTableRowAttrs(node);
207
205
  const domAttrs = {};
208
206
  if (typeof attrs.height === "number") domAttrs["style"] = `height: ${Math.round(attrs.height / 20 * 1.333)}px`;
207
+ if (attrs.hidden) domAttrs["style"] = `${domAttrs["style"] ? `${domAttrs["style"]}; ` : ""}display: none`;
209
208
  return [
210
209
  "tr",
211
210
  domAttrs,
@@ -315,6 +314,7 @@ const tableCellSpec = {
315
314
  textDirection: { default: null },
316
315
  noWrap: { default: false },
317
316
  _originalFormatting: { default: null },
317
+ tcPrChange: { default: null },
318
318
  _preserveVMergeRestart: { default: null },
319
319
  _docxVMergeContinuationCells: { default: null }
320
320
  },
@@ -367,6 +367,7 @@ const tableHeaderSpec = {
367
367
  textDirection: { default: null },
368
368
  noWrap: { default: false },
369
369
  _originalFormatting: { default: null },
370
+ tcPrChange: { default: null },
370
371
  _preserveVMergeRestart: { default: null },
371
372
  _docxVMergeContinuationCells: { default: null }
372
373
  },
@@ -44,7 +44,8 @@ function resolveFindMatchRange(doc, match) {
44
44
  cellOffset += cell.nodeSize;
45
45
  continue;
46
46
  }
47
- if (!walkBlocks(cell, rowPos + 1 + cellOffset + 1)) return false;
47
+ const cellPos = rowPos + 1 + cellOffset;
48
+ if (!walkBlocks(cell, cellPos + 1)) return false;
48
49
  cellOffset += cell.nodeSize;
49
50
  }
50
51
  rowOffset += row.nodeSize;
@@ -11,10 +11,10 @@ import { createSelectionTrackerPlugin, extractSelectionContext, getSelectionCont
11
11
  import { getHyperlinkAttrs, getSelectedText, isHyperlinkActive } from "./extensions/marks/HyperlinkExtension.js";
12
12
  import { schema, singletonManager } from "./schema/index.js";
13
13
  import { createEmptyDoc, toProseDoc } from "./conversion/toProseDoc.js";
14
- import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./commands/table.js";
15
14
  import { extractSelectionState } from "./selectionState.js";
16
15
  import { clearFontFamily, clearFontSize, clearHighlight, clearTextColor, insertHyperlink, removeHyperlink, setFontFamily, setFontSize, setHighlight, setHyperlink, setTextColor, toggleBold, toggleItalic, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline } from "./commands/formatting.js";
17
16
  import { addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyStyle, clearStyle, decreaseIndent, decreaseListLevel, generateTOC, increaseIndent, increaseListLevel, removeList, removeTabStop, setAlignment, setIndentFirstLine, setIndentLeft, setIndentRight, setLineSpacing, setLtr, setRtl, toggleBidi, toggleBulletList, toggleNumberedList } from "./commands/paragraph.js";
17
+ import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./commands/table.js";
18
18
  import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./commands/formatPainter.js";
19
19
  import { insertPageBreak } from "./commands/pageBreak.js";
20
20
  import { insertImageFromFile } from "./commands/image.js";
@@ -1,5 +1,5 @@
1
- import { insertTable } from "./commands/table.js";
2
1
  import { generateTOC } from "./commands/paragraph.js";
2
+ import { insertTable } from "./commands/table.js";
3
3
  import { insertPageBreak } from "./commands/pageBreak.js";
4
4
  import { insertImageFromFile } from "./commands/image.js";
5
5
  //#region src/prosemirror/insertOperations.ts
@@ -18,7 +18,8 @@ type ParagraphAttrs = {
18
18
  spaceAfter?: number;
19
19
  lineSpacing?: number;
20
20
  lineSpacingRule?: document_d_exports.LineSpacingRule;
21
- spacingExplicit?: SpacingExplicit;
21
+ spacingExplicit?: SpacingExplicit; /** Layout provenance: document defaults survive on empty paragraphs. */
22
+ spacingFromDocDefaults?: SpacingExplicit;
22
23
  indentLeft?: number;
23
24
  indentRight?: number;
24
25
  indentFirstLine?: number;
@@ -65,7 +66,8 @@ type ParagraphAttrs = {
65
66
  * inline LISTNUM "(a)" with the deeper level's marker column.
66
67
  */
67
68
  listMarkerSecondSlotOffsetTwips?: number; /** Number format for each level used by multi-level marker templates. */
68
- listLevelNumFmts?: document_d_exports.NumberFormat[]; /** Abstract numbering ID shared by numbering instances. */
69
+ listLevelNumFmts?: document_d_exports.NumberFormat[]; /** Initial counter for each level used by multi-level marker templates. */
70
+ listLevelStarts?: number[]; /** Abstract numbering ID shared by numbering instances. */
69
71
  listAbstractNumId?: number; /** Numbering start override for this numId/level. */
70
72
  listStartOverride?: number;
71
73
  styleId?: string;
@@ -82,7 +84,8 @@ type ParagraphAttrs = {
82
84
  pageBreakBefore?: boolean; /** Word's cached rendered-page-break marker; preserved for round-trip only. */
83
85
  renderedPageBreakBefore?: boolean;
84
86
  keepNext?: boolean;
85
- keepLines?: boolean; /** Contextual spacing — suppress space between same-style paragraphs */
87
+ keepLines?: boolean;
88
+ widowControl?: boolean; /** Contextual spacing — suppress space between same-style paragraphs */
86
89
  contextualSpacing?: boolean;
87
90
  defaultTextFormatting?: document_d_exports.TextFormatting;
88
91
  sectionBreakType?: "nextPage" | "continuous" | "oddPage" | "evenPage";
@@ -345,7 +348,8 @@ type TableAttrs = {
345
348
  }; /** Table look flags for conditional formatting (w:tblLook) */
346
349
  look?: document_d_exports.TableLook; /** Table-level borders (w:tblBorders) — full BorderSpec per side */
347
350
  borders?: document_d_exports.TableBorders; /** Original table formatting from DOCX for lossless round-trip serialization */
348
- _originalFormatting?: document_d_exports.TableFormatting;
351
+ _originalFormatting?: document_d_exports.TableFormatting; /** Tracked table property changes (w:tblPrChange) for round-trip + accept/reject */
352
+ tblPrChange?: document_d_exports.TablePropertyChange[];
349
353
  };
350
354
  /**
351
355
  * Table row attributes
@@ -353,8 +357,10 @@ type TableAttrs = {
353
357
  type TableRowAttrs = {
354
358
  /** Row height (in twips) */height?: number; /** Height rule ('auto', 'exact', 'atLeast') */
355
359
  heightRule?: NonNullable<document_d_exports.TableRowFormatting["heightRule"]>; /** Is header row */
356
- isHeader?: boolean; /** Original row formatting from DOCX for lossless round-trip serialization */
357
- _originalFormatting?: document_d_exports.TableRowFormatting;
360
+ isHeader?: boolean; /** Whether the row is hidden (`w:hidden`) */
361
+ hidden?: boolean; /** Original row formatting from DOCX for lossless round-trip serialization */
362
+ _originalFormatting?: document_d_exports.TableRowFormatting; /** Tracked row property changes (w:trPrChange) for round-trip + accept/reject */
363
+ trPrChange?: document_d_exports.TableRowPropertyChange[];
358
364
  };
359
365
  /**
360
366
  * Table cell attributes
@@ -381,7 +387,8 @@ type TableCellAttrs = {
381
387
  left?: number;
382
388
  right?: number;
383
389
  }; /** Original cell formatting from DOCX for lossless round-trip serialization */
384
- _originalFormatting?: document_d_exports.TableCellFormatting; /** Preserve a DOCX vMerge restart even when PM cannot model it as a rowspan. */
390
+ _originalFormatting?: document_d_exports.TableCellFormatting; /** Tracked cell property changes (w:tcPrChange) for round-trip + accept/reject */
391
+ tcPrChange?: document_d_exports.TableCellPropertyChange[]; /** Preserve a DOCX vMerge restart even when PM cannot model it as a rowspan. */
385
392
  _preserveVMergeRestart?: boolean; /** Original DOCX vMerge continuation cells skipped into this PM rowspan. */
386
393
  _docxVMergeContinuationCells?: document_d_exports.TableCell[];
387
394
  };
@@ -91,6 +91,7 @@ function listAttrsFromResolvedStyle(resolved, numbering) {
91
91
  listImplicitChildLevelAdvances: null,
92
92
  listMarkerSecondSlotOffsetTwips: null,
93
93
  listLevelNumFmts: rendering?.levelNumFmts ?? null,
94
+ listLevelStarts: rendering?.levelStarts ?? null,
94
95
  listAbstractNumId: rendering?.abstractNumId ?? null,
95
96
  listStartOverride: rendering?.startOverride ?? null
96
97
  };
@@ -5,6 +5,8 @@ const DEFAULT_TAB_INTERVAL_TWIPS = 720;
5
5
  const TWIPS_PER_INCH = 1440;
6
6
  /** Pixels per inch (96 dpi standard for CSS) */
7
7
  const PIXELS_PER_INCH = 96;
8
+ /** Ignore sub-twip round trips when the cursor already occupies a tab stop. */
9
+ const TAB_POSITION_EPSILON_TWIPS = .01;
8
10
  /**
9
11
  * Convert twips to pixels
10
12
  * @param twips - Value in twips (1/1440 inch)
@@ -44,7 +46,7 @@ function computeTabStops(context) {
44
46
  leader: "none"
45
47
  });
46
48
  }
47
- let pos = maxExplicit > 0 ? Math.max(maxExplicit, leftIndent) : leftIndent;
49
+ let pos = Math.floor(Math.max(maxExplicit, leftIndent) / defaultTabInterval) * defaultTabInterval;
48
50
  const limitPos = leftIndent + 14400;
49
51
  while (pos < limitPos) {
50
52
  pos += defaultTabInterval;
@@ -84,7 +86,7 @@ function computeTabStops(context) {
84
86
  function calculateTabWidth(currentXPx, context, following = {}) {
85
87
  const { defaultTabInterval = 720 } = context;
86
88
  const currentXTwips = pixelsToTwips(currentXPx);
87
- const nextStop = computeTabStops(context).find((stop) => stop.pos > currentXTwips);
89
+ const nextStop = computeTabStops(context).find((stop) => stop.pos > currentXTwips + TAB_POSITION_EPSILON_TWIPS);
88
90
  if (!nextStop) {
89
91
  const defaultTabPx = twipsToPixels(defaultTabInterval);
90
92
  let tabWidth = defaultTabPx - currentXPx % defaultTabPx;
@@ -0,0 +1,20 @@
1
+ import { FolioAIEditApplyResult } from "./ai-edits/types.js";
2
+
3
+ //#region src/redline.d.ts
4
+ /** Options for {@link generateRedlineDocx}. */
5
+ type GenerateRedlineDocxOptions = {
6
+ /** Author recorded on the generated tracked changes. (default: `"folio compare"`) */author?: string;
7
+ };
8
+ /** Result of {@link generateRedlineDocx}. */
9
+ type GenerateRedlineDocxResult = FolioAIEditApplyResult & {
10
+ /** The redline `.docx`: the base document with base → revised tracked changes. */buffer: ArrayBuffer;
11
+ };
12
+ /**
13
+ * Compare `base` and `revised` and return the base document with every
14
+ * difference recorded as a tracked change. See the module doc comment for
15
+ * semantics and limitations; `skipped` reports any block the generator could
16
+ * not redline (e.g. additions with no anchor in an empty base document).
17
+ */
18
+ declare const generateRedlineDocx: (base: ArrayBuffer, revised: ArrayBuffer, options?: GenerateRedlineDocxOptions) => Promise<GenerateRedlineDocxResult>;
19
+ //#endregion
20
+ export { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, generateRedlineDocx };
@@ -0,0 +1,127 @@
1
+ import { FolioDocxReviewer } from "./ai-edits/headless.js";
2
+ import { alignFolioBlocks } from "./version-comparison.js";
3
+ //#region src/redline.ts
4
+ /**
5
+ * Redline generator: compare two `.docx` buffers and produce a THIRD `.docx`
6
+ * whose differences are recorded as real Word tracked changes (`w:ins` /
7
+ * `w:del`), ready for review in Word or the folio editor — the
8
+ * document-producing counterpart to {@link compareDocxVersions}'s structured
9
+ * diff.
10
+ *
11
+ * The pipeline is a composition of existing machinery, not a new engine:
12
+ * the base buffer is opened in a headless {@link FolioDocxReviewer}, the two
13
+ * snapshots are aligned with {@link alignFolioBlocks} (the same three-pass
14
+ * alignment the comparer uses), each alignment event maps onto a
15
+ * tracked-changes {@link FolioAIEditOperation}, and the reviewer's shared
16
+ * apply path records them as redlines. Word-level minimality comes free:
17
+ * the apply engine word-diffs a `replaceBlock` and only marks the divergent
18
+ * tokens.
19
+ *
20
+ * ## Semantics and limitations
21
+ *
22
+ * - **As-accepted inputs.** Pending tracked changes in either input count as
23
+ * applied before comparison (mirroring {@link compareDocxVersions}): the
24
+ * base's own pending redlines are accepted in the output, so the tracked
25
+ * changes it carries are exactly base → revised. This is intentionally
26
+ * lossy about the inputs' own revision history and authorship.
27
+ * - **Text redlines only.** Format-only changes and relocated blocks are
28
+ * redlined as plain edits (a move becomes a delete + insert, like Word
29
+ * compare with move detection off); block-level formatting is carried via
30
+ * the revised block's `styleId` on inserted blocks.
31
+ * - **Anchoring.** Insertions anchor `before` the next surviving base block
32
+ * so consecutive additions keep their order; additions past the last base
33
+ * block anchor `after` it. A base document with no content blocks offers
34
+ * no anchor at all — such additions surface in `skipped` rather than
35
+ * silently vanishing.
36
+ */
37
+ /**
38
+ * For each event index, the base block id the next `revisedOnly` event
39
+ * should anchor before: the base side of the nearest later `pair` or
40
+ * `baseOnly` event, or `null` when only additions remain until the end of
41
+ * the document. One backward pass, so a long run of trailing additions
42
+ * (huge revised document vs. a small base) stays linear instead of
43
+ * re-scanning the tail per added block.
44
+ */
45
+ const nextBaseBlockIdByIndex = (events) => {
46
+ const nextIds = Array.from({ length: events.length });
47
+ let nextId = null;
48
+ for (let i = events.length - 1; i >= 0; i--) {
49
+ nextIds[i] = nextId;
50
+ const event = events[i];
51
+ if (event?.type === "pair") nextId = event.baseBlock.id;
52
+ else if (event?.type === "baseOnly") nextId = event.block.id;
53
+ }
54
+ return nextIds;
55
+ };
56
+ /**
57
+ * Compare `base` and `revised` and return the base document with every
58
+ * difference recorded as a tracked change. See the module doc comment for
59
+ * semantics and limitations; `skipped` reports any block the generator could
60
+ * not redline (e.g. additions with no anchor in an empty base document).
61
+ */
62
+ const generateRedlineDocx = async (base, revised, options = {}) => {
63
+ const [baseReviewer, revisedReviewer] = await Promise.all([FolioDocxReviewer.fromBuffer(base, { author: options.author ?? "folio compare" }), FolioDocxReviewer.fromBuffer(revised)]);
64
+ baseReviewer.acceptAll();
65
+ const baseSnapshot = baseReviewer.snapshot();
66
+ const revisedBlocks = revisedReviewer.snapshot().blocks;
67
+ const events = alignFolioBlocks(baseSnapshot.blocks, revisedBlocks);
68
+ const anchorIds = nextBaseBlockIdByIndex(events);
69
+ const operations = [];
70
+ let operationSeq = 0;
71
+ const nextOperationId = () => `redline-${++operationSeq}`;
72
+ /** Additions past the last base block, inserted after it in reverse so the final order matches the revised document. */
73
+ const trailingAdditions = [];
74
+ const lastBaseBlockId = baseSnapshot.blocks.at(-1)?.id ?? null;
75
+ events.forEach((event, eventIndex) => {
76
+ if (event.type === "pair") {
77
+ if (event.baseBlock.text !== event.revisedBlock.text) operations.push({
78
+ id: nextOperationId(),
79
+ type: "replaceBlock",
80
+ blockId: event.baseBlock.id,
81
+ text: event.revisedBlock.text
82
+ });
83
+ return;
84
+ }
85
+ if (event.type === "baseOnly") {
86
+ operations.push({
87
+ id: nextOperationId(),
88
+ type: "deleteBlock",
89
+ blockId: event.block.id
90
+ });
91
+ return;
92
+ }
93
+ const anchorId = anchorIds[eventIndex] ?? null;
94
+ if (anchorId === null) {
95
+ trailingAdditions.push({
96
+ text: event.block.text,
97
+ ...event.block.styleId !== void 0 && { styleId: event.block.styleId }
98
+ });
99
+ return;
100
+ }
101
+ operations.push({
102
+ id: nextOperationId(),
103
+ type: "insertBeforeBlock",
104
+ blockId: anchorId,
105
+ text: event.block.text,
106
+ ...event.block.styleId !== void 0 && { styleId: event.block.styleId }
107
+ });
108
+ });
109
+ for (const addition of trailingAdditions.toReversed()) operations.push({
110
+ id: nextOperationId(),
111
+ type: "insertAfterBlock",
112
+ blockId: lastBaseBlockId ?? "redline-unanchored",
113
+ text: addition.text,
114
+ ...addition.styleId !== void 0 && { styleId: addition.styleId }
115
+ });
116
+ const { applied, skipped } = baseReviewer.applyOperations(operations, {
117
+ mode: "tracked-changes",
118
+ snapshot: baseSnapshot
119
+ });
120
+ return {
121
+ buffer: await baseReviewer.toBuffer(),
122
+ applied,
123
+ skipped
124
+ };
125
+ };
126
+ //#endregion
127
+ export { generateRedlineDocx };
package/dist/server.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditSnapshot } from "./ai-edits/types.js";
1
+ import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditSnapshot, FolioAIInlineFormatting, FolioAITextRangeHandle } from "./ai-edits/types.js";
2
+ 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, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js";
2
3
  import { FolioReviewChange, FolioReviewChangeKind } from "./ai-edits/read.js";
3
- import { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyOperationsOptions, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChangeFilter, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js";
4
+ import { ApplyFolioAIEditsToBufferOptions, ApplyFolioAIEditsToBufferResult, FolioApplyDocumentOperationsOptions, FolioApplyOperationsOptions, FolioDocumentStory, FolioDocumentStoryHandle, FolioDocxReviewer, FolioDocxReviewerOptions, FolioReviewChangeFilter, FolioReviewComment, FolioReviewCommentFilter, FolioReviewCommentReply, FolioReviewReplyInput, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js";
5
+ import { createFolioAITextRangeHandle } from "./ai-edits/snapshot.js";
4
6
  import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
5
7
  import { CreateEmptyDocumentOptions, createEmptyDocument } from "./utils/createDocument.js";
6
8
  import { createDocx } from "./docx/rezip.js";
7
9
  import { CreateCommentReplyInput, replyToComment } from "./docx/replyToComment.js";
8
- export { type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, type DeriveBlockIdInput, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditSnapshot, type FolioApplyOperationsOptions, type FolioBlockId, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, applyFolioAIEditsToBuffer, createDocx, createEmptyDocument, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId, replyToComment };
10
+ import { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, generateRedlineDocx } from "./redline.js";
11
+ import { FolioBlockDiff, FolioFormatProperty, FolioVersionDiff, FolioVersionDiffSegment, compareDocxVersions } from "./version-comparison.js";
12
+ export { type ApplyFolioAIEditsToBufferOptions, type ApplyFolioAIEditsToBufferResult, type CreateCommentReplyInput, type CreateEmptyDocumentOptions, type DeriveBlockIdInput, 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 FolioAIComment, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditSnapshot, type FolioAIInlineFormatting, type FolioAITextRangeHandle, type FolioApplyDocumentOperationsOptions, type FolioApplyOperationsOptions, type FolioBlockDiff, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type FolioDocumentStory, type FolioDocumentStoryHandle, FolioDocxReviewer, type FolioDocxReviewerOptions, type FolioFormatProperty, type FolioReviewChange, type FolioReviewChangeFilter, type FolioReviewChangeKind, type FolioReviewComment, type FolioReviewCommentFilter, type FolioReviewCommentReply, type FolioReviewReplyInput, type FolioVersionDiff, type FolioVersionDiffSegment, type GenerateRedlineDocxOptions, type GenerateRedlineDocxResult, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditsToBuffer, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, deriveBlockId, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch, replyToComment };
package/dist/server.js CHANGED
@@ -1,6 +1,10 @@
1
+ import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
2
+ import { createFolioAITextRangeHandle } from "./ai-edits/snapshot.js";
3
+ 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, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js";
1
4
  import { createEmptyDocument } from "./utils/createDocument.js";
2
5
  import { createDocx } from "./docx/rezip.js";
3
- import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
4
6
  import { replyToComment } from "./docx/replyToComment.js";
5
7
  import { FolioDocxReviewer, applyFolioAIEditsToBuffer } from "./ai-edits/headless.js";
6
- export { FolioDocxReviewer, applyFolioAIEditsToBuffer, createDocx, createEmptyDocument, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId, replyToComment };
8
+ import { compareDocxVersions } from "./version-comparison.js";
9
+ import { generateRedlineDocx } from "./redline.js";
10
+ export { 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, FolioDocxReviewer, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioAIEditsToBuffer, assertSupportedFolioDocumentOperationVersion, compareDocxVersions, createDocx, createEmptyDocument, createFolioAITextRangeHandle, deriveBlockId, generateRedlineDocx, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch, replyToComment };
@@ -0,0 +1,35 @@
1
+ //#region src/symbols.d.ts
2
+ /**
3
+ * Symbol catalog for the "Insert Symbol" dialog.
4
+ *
5
+ * Framework-neutral data + filtering shared by every folio adapter (React,
6
+ * Vue, ...), so the two never drift on which symbols they offer. Category
7
+ * display names are i18n keys (`dialogs.insertSymbol.categories.*`) resolved by
8
+ * each adapter's own translator; the `char`/`name` pairs are the raw catalog.
9
+ */
10
+ type SymbolEntry = {
11
+ /** The character to insert. */char: string; /** English descriptive name, used for search matching. */
12
+ name: string;
13
+ };
14
+ /** i18n message keys for the symbol-category display labels. */
15
+ type SymbolCategoryNameKey = "dialogs.insertSymbol.categories.common" | "dialogs.insertSymbol.categories.arrows" | "dialogs.insertSymbol.categories.math" | "dialogs.insertSymbol.categories.greek" | "dialogs.insertSymbol.categories.currency" | "dialogs.insertSymbol.categories.shapes";
16
+ type SymbolCategory = {
17
+ /** Stable category id (also the default active tab). */name: string; /** i18n key for the category's display label. */
18
+ nameKey: SymbolCategoryNameKey;
19
+ symbols: SymbolEntry[];
20
+ };
21
+ /** A catalog entry tagged with the category it belongs to (search results). */
22
+ type SymbolSearchEntry = SymbolEntry & {
23
+ category: string;
24
+ };
25
+ declare const SYMBOL_CATEGORIES: SymbolCategory[];
26
+ /** Every catalog symbol, flattened and tagged with its category. */
27
+ declare const ALL_SYMBOLS: SymbolSearchEntry[];
28
+ /**
29
+ * Symbols whose descriptive name contains `query` (case-insensitive), or whose
30
+ * character is exactly `query`. An empty/whitespace query returns `[]` so
31
+ * callers fall back to the active category.
32
+ */
33
+ declare function filterSymbols(query: string): SymbolSearchEntry[];
34
+ //#endregion
35
+ export { ALL_SYMBOLS, SYMBOL_CATEGORIES, SymbolCategory, SymbolCategoryNameKey, SymbolEntry, SymbolSearchEntry, filterSymbols };