@stll/folio-core 0.9.0 → 0.11.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 (69) hide show
  1. package/dist/ai-edits/headless.d.ts +52 -12
  2. package/dist/ai-edits/headless.js +181 -55
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/controller/collaborationModules.d.ts +7 -0
  6. package/dist/controller/collaborationModules.js +15 -0
  7. package/dist/controller/folioEditor.d.ts +1 -1
  8. package/dist/controller/headerFooterEditorManager.d.ts +41 -0
  9. package/dist/controller/headerFooterEditorManager.js +182 -0
  10. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  11. package/dist/controller/layoutPipeline.js +2 -1
  12. package/dist/document-operations.d.ts +11 -2
  13. package/dist/document-operations.js +7 -1
  14. package/dist/document-stories.d.ts +10 -0
  15. package/dist/document-stories.js +27 -0
  16. package/dist/docx/corePropertiesParser.d.ts +8 -0
  17. package/dist/docx/corePropertiesParser.js +53 -0
  18. package/dist/docx/index.d.ts +2 -1
  19. package/dist/docx/index.js +2 -1
  20. package/dist/docx/metadataPrivacy.d.ts +40 -0
  21. package/dist/docx/metadataPrivacy.js +131 -0
  22. package/dist/docx/parser.js +4 -1
  23. package/dist/docx/rezip.d.ts +5 -4
  24. package/dist/docx/rezip.js +6 -8
  25. package/dist/layout-bridge/convert/toFlowBlocks.js +1 -0
  26. package/dist/layout-engine/measure/cache.js +2 -7
  27. package/dist/layout-engine/measure/effectiveLineBreakPolicy.d.ts +29 -0
  28. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +60 -0
  29. package/dist/layout-engine/measure/measureParagraph.js +32 -36
  30. package/dist/layout-engine/measure/tableCellFloating.js +39 -33
  31. package/dist/layout-engine/types.d.ts +5 -3
  32. package/dist/managers/FindReplaceManager.d.ts +8 -0
  33. package/dist/managers/FindReplaceManager.js +18 -0
  34. package/dist/managers/TableSelectionManager.d.ts +6 -0
  35. package/dist/managers/TableSelectionManager.js +14 -0
  36. package/dist/prosemirror/attrs/index.js +1 -0
  37. package/dist/prosemirror/conversion/effectiveTableCellFormatting.d.ts +62 -0
  38. package/dist/prosemirror/conversion/effectiveTableCellFormatting.js +131 -0
  39. package/dist/prosemirror/conversion/fromProseDoc.js +1 -0
  40. package/dist/prosemirror/conversion/toProseDoc.js +40 -68
  41. package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -1
  42. package/dist/prosemirror/extensions/marks/HighlightExtension.js +1 -1
  43. package/dist/prosemirror/extensions/marks/RunShadingExtension.js +1 -1
  44. package/dist/prosemirror/extensions/marks/TextColorExtension.js +1 -1
  45. package/dist/prosemirror/extensions/nodes/ImageExtension.js +1 -0
  46. package/dist/prosemirror/extensions/nodes/TableExtension.js +1 -1
  47. package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
  48. package/dist/prosemirror/findReplaceSelection.js +57 -13
  49. package/dist/prosemirror/schema/nodes.d.ts +2 -1
  50. package/dist/redline.d.ts +26 -11
  51. package/dist/redline.js +95 -62
  52. package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
  53. package/dist/render-dom/BodySelectionOverlay.js +76 -0
  54. package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
  55. package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
  56. package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
  57. package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
  58. package/dist/render-dom/RenderedDomContext.d.ts +40 -0
  59. package/dist/render-dom/RenderedDomContext.js +126 -0
  60. package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
  61. package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
  62. package/dist/server.d.ts +5 -4
  63. package/dist/server.js +5 -4
  64. package/dist/utils/findReplace.d.ts +32 -0
  65. package/dist/utils/findReplace.js +118 -0
  66. package/dist/utils/headerFooter.js +24 -1
  67. package/dist/version-comparison.d.ts +65 -11
  68. package/dist/version-comparison.js +187 -29
  69. package/package.json +1 -1
@@ -7,10 +7,9 @@ import { assertValidProseMirrorDocument } from "../validation.js";
7
7
  import { shadingToRunShadingAttrs } from "./runShadingMark.js";
8
8
  import { marksToTextFormatting } from "./fromProseDoc.js";
9
9
  import { createStyleEngine } from "../../style-engine/styleEngine.js";
10
- import { resolveColor } from "../../utils/colorResolver.js";
11
- import { resolveShadingFill } from "../../utils/formatToStyle.js";
12
10
  import { buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
13
11
  import { schema } from "../schema/index.js";
12
+ import { resolveEffectiveTableCellFormatting } from "./effectiveTableCellFormatting.js";
14
13
  //#region src/prosemirror/conversion/toProseDoc.ts
15
14
  const TOC_STYLE_ID = /^TOC\d*$/iu;
16
15
  /**
@@ -725,88 +724,60 @@ function convertTableRow(row, styleResolver, context, isHeaderRow, columnWidths,
725
724
  if (cellIsFirstRow && cellIsLastCol && (tableLook?.firstRow || rowCnf?.firstRow || cellCnf?.firstRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.neCell);
726
725
  if (cellIsLastRow && cellIsFirstCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.firstColumn || rowCnf?.firstColumn || cellCnf?.firstColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.swCell);
727
726
  if (cellIsLastRow && cellIsLastCol && (tableLook?.lastRow || rowCnf?.lastRow || cellCnf?.lastRow) && (tableLook?.lastColumn || rowCnf?.lastColumn || cellCnf?.lastColumn)) cellConditionalStyle = mergeConditionalStyles(cellConditionalStyle, conditionalStyles?.seCell);
728
- cells.push(convertTableCell(cell, styleResolver, context, isHeaderRow, gridWidth, cellConditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, rowSpanInfo?.continuationCells, defaultCellMargins));
727
+ cells.push(convertTableCell({
728
+ cell,
729
+ styleResolver,
730
+ context,
731
+ isHeader: isHeaderRow,
732
+ gridWidthPercent: gridWidth,
733
+ conditionalStyle: cellConditionalStyle,
734
+ tableBorders,
735
+ position: {
736
+ isFirstRow,
737
+ isLastRow,
738
+ isFirstColumn: isFirstCol,
739
+ isLastColumn: isLastCol
740
+ },
741
+ calculatedRowSpan,
742
+ preserveVMergeRestart,
743
+ vMergeContinuationCells: rowSpanInfo?.continuationCells,
744
+ defaultCellMargins
745
+ }));
729
746
  }
730
747
  return schema.node("tableRow", attrs, cells);
731
748
  }
732
- const TABLE_BORDER_SIDES = [
733
- "top",
734
- "bottom",
735
- "left",
736
- "right",
737
- "insideH",
738
- "insideV",
739
- "topLeftToBottomRight",
740
- "topRightToBottomLeft"
741
- ];
742
- function resolveThemedBorderColors(borders, theme) {
743
- if (!borders || !theme?.colorScheme) return borders;
744
- let resolved;
745
- for (const side of TABLE_BORDER_SIDES) {
746
- const border = borders[side];
747
- if (!border?.color?.themeColor || border.color.auto) continue;
748
- resolved ??= { ...borders };
749
- resolved[side] = {
750
- ...border,
751
- color: { rgb: resolveColor(border.color, theme).replace(/^#/u, "") }
752
- };
753
- }
754
- return resolved ?? borders;
755
- }
756
749
  /**
757
750
  * Convert a TableCell to a ProseMirror table cell node
758
751
  */
759
- function convertTableCell(cell, styleResolver, context, isHeader, gridWidthPercent, conditionalStyle, tableBorders, isFirstRow, isLastRow, isFirstCol, isLastCol, calculatedRowSpan, preserveVMergeRestart, vMergeContinuationCells, defaultCellMargins) {
760
- const { theme } = context;
752
+ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPercent, conditionalStyle, tableBorders, position, calculatedRowSpan, preserveVMergeRestart, vMergeContinuationCells, defaultCellMargins }) {
761
753
  const formatting = cell.formatting;
762
754
  const rowspan = calculatedRowSpan ?? 1;
763
- let width = formatting?.width?.value;
764
- let widthType = formatting?.width?.type;
765
- if (width === void 0 && gridWidthPercent !== void 0) {
766
- width = gridWidthPercent;
767
- widthType = "pct";
768
- }
769
- const backgroundColor = resolveShadingFill(formatting?.shading ?? conditionalStyle?.tcPr?.shading, theme).replace(/^#/u, "");
770
- const baseBorders = (() => {
771
- if (tableBorders) return {
772
- top: isFirstRow ? tableBorders.top : tableBorders.insideH,
773
- bottom: isLastRow ? tableBorders.bottom : tableBorders.insideH,
774
- left: isFirstCol ? tableBorders.left : tableBorders.insideV,
775
- right: isLastCol ? tableBorders.right : tableBorders.insideV
776
- };
777
- })();
778
- const conditionalBorders = conditionalStyle?.tcPr?.borders;
779
- const cellBorders = formatting?.borders;
780
- const borders = resolveThemedBorderColors(baseBorders || conditionalBorders || cellBorders ? {
781
- ...baseBorders,
782
- ...conditionalBorders,
783
- ...cellBorders
784
- } : void 0, theme);
785
- const buildMarginsAttr = (src) => {
786
- const m = {};
787
- if (src.top?.value !== void 0) m.top = src.top.value;
788
- if (src.bottom?.value !== void 0) m.bottom = src.bottom.value;
789
- if (src.left?.value !== void 0) m.left = src.left.value;
790
- if (src.right?.value !== void 0) m.right = src.right.value;
791
- return m;
792
- };
755
+ const effectiveFormatting = resolveEffectiveTableCellFormatting({
756
+ directFormatting: formatting,
757
+ styleFormatting: conditionalStyle?.tcPr,
758
+ tableBorders,
759
+ position,
760
+ gridWidthPercent,
761
+ defaultMargins: defaultCellMargins,
762
+ theme: context.theme
763
+ });
793
764
  const attrs = {
794
765
  colspan: formatting?.gridSpan ?? 1,
795
766
  rowspan
796
767
  };
797
- if (width !== void 0) attrs.width = width;
798
- if (widthType) attrs.widthType = widthType;
768
+ if (effectiveFormatting.width.type === "value") {
769
+ attrs.width = effectiveFormatting.width.value;
770
+ if (effectiveFormatting.width.widthType) attrs.widthType = effectiveFormatting.width.widthType;
771
+ }
799
772
  if (formatting?.verticalAlign) attrs.verticalAlign = formatting.verticalAlign;
800
- if (backgroundColor) {
801
- attrs.backgroundColor = backgroundColor;
802
- attrs._resolvedBackgroundColor = backgroundColor;
773
+ if (effectiveFormatting.background.type === "color") {
774
+ attrs.backgroundColor = effectiveFormatting.background.rgb;
775
+ attrs._resolvedBackgroundColor = effectiveFormatting.background.rgb;
803
776
  }
804
777
  if (formatting?.textDirection) attrs.textDirection = formatting.textDirection;
805
778
  if (formatting?.noWrap !== void 0) attrs.noWrap = formatting.noWrap;
806
- if (borders) attrs.borders = borders;
807
- if (formatting?.margins) attrs.margins = buildMarginsAttr(formatting.margins);
808
- else if (conditionalStyle?.tcPr?.margins) attrs.margins = buildMarginsAttr(conditionalStyle.tcPr.margins);
809
- else if (defaultCellMargins) attrs.margins = defaultCellMargins;
779
+ if (effectiveFormatting.borders) attrs.borders = effectiveFormatting.borders;
780
+ if (effectiveFormatting.margins) attrs.margins = effectiveFormatting.margins;
810
781
  if (formatting) attrs._originalFormatting = formatting;
811
782
  if (cell.propertyChanges && cell.propertyChanges.length > 0) attrs.tcPrChange = [...cell.propertyChanges];
812
783
  if (preserveVMergeRestart) attrs._preserveVMergeRestart = true;
@@ -1072,6 +1043,7 @@ function convertImage(image, rawXml) {
1072
1043
  cropBottom: image.crop?.bottom,
1073
1044
  cropLeft: image.crop?.left,
1074
1045
  position,
1046
+ layoutInCell: image.layoutInCell,
1075
1047
  borderWidth,
1076
1048
  borderColor,
1077
1049
  borderStyle,
@@ -1,8 +1,8 @@
1
1
  import { autospacingMatchesBase } from "../../autospacingBase.js";
2
2
  import { directionIsRtl } from "../../paragraphDirection.js";
3
3
  import { expectParagraphAttrs } from "../../attrs/index.js";
4
- import { paragraphToStyle } from "../../../utils/formatToStyle.js";
5
4
  import { createNodeExtension } from "../create.js";
5
+ import { paragraphToStyle } from "../../../utils/formatToStyle.js";
6
6
  import { collectHeadings } from "../../../utils/headingCollector.js";
7
7
  import { listAttrsFromResolvedStyle, paragraphAttrsFromResolvedStyle } from "../../styles/resolvedStyleAttrs.js";
8
8
  import { Fragment } from "prosemirror-model";
@@ -1,7 +1,7 @@
1
1
  import { HIGHLIGHT_COLOR_VALUES } from "../../../types/documentEnumValues.js";
2
2
  import { expectHighlightMarkAttrs } from "../../attrs/index.js";
3
- import { resolveHighlightToCss } from "../../../utils/colorResolver.js";
4
3
  import { createMarkExtension } from "../create.js";
4
+ import { resolveHighlightToCss } from "../../../utils/colorResolver.js";
5
5
  import { removeMark, setMark } from "./markUtils.js";
6
6
  import { panic } from "better-result";
7
7
  //#region src/prosemirror/extensions/marks/HighlightExtension.ts
@@ -1,6 +1,6 @@
1
1
  import { expectRunShadingMarkAttrs } from "../../attrs/index.js";
2
- import { ensureHexPrefix } from "../../../utils/colorResolver.js";
3
2
  import { createMarkExtension } from "../create.js";
3
+ import { ensureHexPrefix } from "../../../utils/colorResolver.js";
4
4
  import { normalizeCssColorKey, parseDOMHighlightColor } from "./HighlightExtension.js";
5
5
  //#region src/prosemirror/extensions/marks/RunShadingExtension.ts
6
6
  const RunShadingExtension = createMarkExtension({
@@ -1,6 +1,6 @@
1
1
  import { expectTextColorMarkAttrs } from "../../attrs/index.js";
2
- import { textToStyle } from "../../../utils/formatToStyle.js";
3
2
  import { createMarkExtension } from "../create.js";
3
+ import { textToStyle } from "../../../utils/formatToStyle.js";
4
4
  import { removeMark, setMark } from "./markUtils.js";
5
5
  import { panic } from "better-result";
6
6
  //#region src/prosemirror/extensions/marks/TextColorExtension.ts
@@ -33,6 +33,7 @@ const ImageExtension = createNodeExtension({
33
33
  cropBottom: { default: null },
34
34
  cropLeft: { default: null },
35
35
  position: { default: null },
36
+ layoutInCell: { default: null },
36
37
  borderWidth: { default: null },
37
38
  borderColor: { default: null },
38
39
  borderStyle: { default: null },
@@ -1,7 +1,7 @@
1
1
  import { TABLE_CELL_TEXT_DIRECTION_VALUES, TABLE_WIDTH_TYPE_VALUES } from "../../../types/documentEnumValues.js";
2
2
  import { expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs } from "../../attrs/index.js";
3
- import { resolveColor } from "../../../utils/colorResolver.js";
4
3
  import { createExtension, createNodeExtension } from "../create.js";
4
+ import { resolveColor } from "../../../utils/colorResolver.js";
5
5
  import { panic } from "better-result";
6
6
  import { Plugin, PluginKey, Selection, TextSelection } from "prosemirror-state";
7
7
  import { CellSelection, TableMap, columnResizing, mergeCells, removeRow, selectedRect, splitCell, tableEditing } from "prosemirror-tables";
@@ -1,3 +1,4 @@
1
+ import { FindOptions } from "../utils/findReplace.js";
1
2
  import { Node } from "prosemirror-model";
2
3
 
3
4
  //#region src/prosemirror/findReplaceSelection.d.ts
@@ -18,6 +19,10 @@ type FindMatchRange = {
18
19
  from: number;
19
20
  to: number;
20
21
  };
22
+ type ProseMirrorFindMatch = FindMatchPosition & FindMatchRange & {
23
+ text: string;
24
+ };
21
25
  declare function resolveFindMatchRange(doc: Node, match: FindMatchPosition): FindMatchRange | null;
26
+ declare function findInProseMirrorDocument(doc: Node, searchText: string, options: FindOptions): ProseMirrorFindMatch[];
22
27
  //#endregion
23
- export { FindMatchPosition, FindMatchRange, resolveFindMatchRange };
28
+ export { FindMatchPosition, FindMatchRange, ProseMirrorFindMatch, findInProseMirrorDocument, resolveFindMatchRange };
@@ -1,29 +1,55 @@
1
+ import { findAllMatches } from "../utils/findReplace.js";
1
2
  //#region src/prosemirror/findReplaceSelection.ts
2
3
  function resolveFindMatchRange(doc, match) {
3
- let paragraphIndex = 0;
4
4
  let resolved = null;
5
- const visitParagraph = (node, pos) => {
6
- if (resolved) return false;
7
- if (paragraphIndex !== match.paragraphIndex) {
8
- paragraphIndex++;
9
- return true;
10
- }
5
+ forEachSearchParagraph(doc, (paragraph, paragraphPos, paragraphIndex) => {
6
+ if (paragraphIndex !== match.paragraphIndex) return true;
11
7
  resolved = resolveTextRangeInParagraph({
12
- paragraph: node,
13
- paragraphPos: pos,
8
+ paragraph,
9
+ paragraphPos,
14
10
  startOffset: match.startOffset,
15
11
  endOffset: match.endOffset
16
12
  });
17
13
  return false;
18
- };
14
+ });
15
+ return resolved;
16
+ }
17
+ function findInProseMirrorDocument(doc, searchText, options) {
18
+ if (!searchText) return [];
19
+ const matches = [];
20
+ forEachSearchParagraph(doc, (paragraph, paragraphPos, paragraphIndex) => {
21
+ const text = getSearchableParagraphText(paragraph);
22
+ for (const { start, end } of findAllMatches(text, searchText, options)) {
23
+ const range = resolveTextRangeInParagraph({
24
+ paragraph,
25
+ paragraphPos,
26
+ startOffset: start,
27
+ endOffset: end
28
+ });
29
+ if (range) matches.push({
30
+ paragraphIndex,
31
+ startOffset: start,
32
+ endOffset: end,
33
+ text: text.slice(start, end),
34
+ ...range
35
+ });
36
+ }
37
+ return true;
38
+ });
39
+ return matches;
40
+ }
41
+ function forEachSearchParagraph(doc, visit) {
42
+ let paragraphIndex = 0;
19
43
  const walkBlocks = (container, contentStart) => {
20
44
  let offset = 0;
21
45
  for (let childIndex = 0; childIndex < container.childCount; childIndex++) {
22
46
  const child = container.child(childIndex);
23
47
  const childPos = contentStart + offset;
24
48
  if (child.type.name === "paragraph") {
25
- if (!visitParagraph(child, childPos)) return false;
49
+ if (!visit(child, childPos, paragraphIndex)) return false;
50
+ paragraphIndex++;
26
51
  } else if (child.type.name === "table" && !walkTable(child, childPos)) return false;
52
+ else if (child.type.name === "blockSdt" && !walkBlocks(child, childPos + 1)) return false;
27
53
  offset += child.nodeSize;
28
54
  }
29
55
  return true;
@@ -53,7 +79,6 @@ function resolveFindMatchRange(doc, match) {
53
79
  return true;
54
80
  };
55
81
  walkBlocks(doc, 0);
56
- return resolved;
57
82
  }
58
83
  function resolveTextRangeInParagraph({ paragraph, paragraphPos, startOffset, endOffset }) {
59
84
  let textOffset = 0;
@@ -83,5 +108,24 @@ function getSearchTextTokenLength(node) {
83
108
  if (node.type.name === "tab" || node.type.name === "hardBreak") return 1;
84
109
  return 0;
85
110
  }
111
+ function getSearchableParagraphText(paragraph) {
112
+ let text = "";
113
+ paragraph.descendants((node) => {
114
+ if (node.isText) {
115
+ text += node.text ?? "";
116
+ return false;
117
+ }
118
+ if (node.type.name === "tab") {
119
+ text += " ";
120
+ return false;
121
+ }
122
+ if (node.type.name === "hardBreak") {
123
+ text += "\n";
124
+ return false;
125
+ }
126
+ return true;
127
+ });
128
+ return text;
129
+ }
86
130
  //#endregion
87
- export { resolveFindMatchRange };
131
+ export { findInProseMirrorDocument, resolveFindMatchRange };
@@ -197,7 +197,8 @@ type ImageAttrs = {
197
197
  cropRight?: number;
198
198
  cropBottom?: number;
199
199
  cropLeft?: number; /** Position for floating images (horizontal and vertical alignment) */
200
- position?: ImagePositionAttrs; /** Border width in pixels */
200
+ position?: ImagePositionAttrs; /** Use the containing table cell as the anchor's positioning scope (the OOXML default). */
201
+ layoutInCell?: boolean; /** Border width in pixels */
201
202
  borderWidth?: number; /** Border color as CSS color string */
202
203
  borderColor?: string; /** Border style (CSS border-style value) */
203
204
  borderStyle?: string; /** Wrap text setting from DOCX (left, right, bothSides, largest) for round-trip */
package/dist/redline.d.ts CHANGED
@@ -1,20 +1,35 @@
1
- import { FolioAIEditApplyResult } from "./ai-edits/types.js";
1
+ import { FolioAIEditAppliedOperation, FolioAIEditSkippedOperation } from "./ai-edits/types.js";
2
+ import { FolioDocumentStoryHandle, FolioResolvedReviewedView } from "./ai-edits/headless.js";
3
+ import { FolioDocumentPrivacyOptions, FolioDocumentPrivacyReport } from "./docx/metadataPrivacy.js";
2
4
 
3
5
  //#region src/redline.d.ts
4
6
  /** Options for {@link generateRedlineDocx}. */
5
7
  type GenerateRedlineDocxOptions = {
6
- /** Author recorded on the generated tracked changes. (default: `"folio compare"`) */author?: string;
8
+ /** Author recorded on the generated tracked changes. (default: `"folio compare"`) */author?: string; /** Resolved base input state. (default: `"final"`) */
9
+ baseView?: FolioResolvedReviewedView; /** Resolved revised input state. (default: `"final"`) */
10
+ revisedView?: FolioResolvedReviewedView; /** Optional output-only package-metadata privacy transforms. */
11
+ privacy?: FolioDocumentPrivacyOptions;
12
+ };
13
+ declare const InvalidGenerateRedlineDocxOptionsError_base: import("better-result").TaggedErrorClass<"InvalidGenerateRedlineDocxOptionsError", {
14
+ message: string;
15
+ option: "baseView" | "revisedView";
16
+ receivedValue: unknown;
17
+ }>;
18
+ declare class InvalidGenerateRedlineDocxOptionsError extends InvalidGenerateRedlineDocxOptionsError_base {}
19
+ type GenerateRedlineUnprocessedStory = {
20
+ baseStory: FolioDocumentStoryHandle | null;
21
+ revisedStory: FolioDocumentStoryHandle | null;
22
+ reason: "missing-base-story" | "missing-revised-story";
7
23
  };
8
24
  /** Result of {@link generateRedlineDocx}. */
9
- type GenerateRedlineDocxResult = FolioAIEditApplyResult & {
10
- /** The redline `.docx`: the base document with base → revised tracked changes. */buffer: ArrayBuffer;
25
+ type GenerateRedlineDocxResult = {
26
+ /** The base package with generated tracked changes. */buffer: ArrayBuffer; /** Operations applied across every matched story. */
27
+ applied: FolioAIEditAppliedOperation[]; /** Block operations that could not be applied. */
28
+ skipped: FolioAIEditSkippedOperation[]; /** Package parts that could not be represented as story-scoped text edits. */
29
+ unprocessedStories: GenerateRedlineUnprocessedStory[]; /** Privacy transforms applied to the generated package. */
30
+ privacyReport: FolioDocumentPrivacyReport;
11
31
  };
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
- */
32
+ /** Compare two buffers and return tracked changes for every matched editable story. */
18
33
  declare const generateRedlineDocx: (base: ArrayBuffer, revised: ArrayBuffer, options?: GenerateRedlineDocxOptions) => Promise<GenerateRedlineDocxResult>;
19
34
  //#endregion
20
- export { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, generateRedlineDocx };
35
+ export { GenerateRedlineDocxOptions, GenerateRedlineDocxResult, GenerateRedlineUnprocessedStory, InvalidGenerateRedlineDocxOptionsError, generateRedlineDocx };
package/dist/redline.js CHANGED
@@ -1,75 +1,35 @@
1
- import { FolioDocxReviewer } from "./ai-edits/headless.js";
1
+ import "./document-operations.js";
2
+ import { pairFolioDocumentStories } from "./document-stories.js";
3
+ import { FolioDocxReviewer, isFolioResolvedReviewedView } from "./ai-edits/headless.js";
4
+ import { resolveFolioDocumentPrivacyTransforms, rewriteDocxMetadataPrivacy } from "./docx/metadataPrivacy.js";
2
5
  import { alignFolioBlocks } from "./version-comparison.js";
6
+ import { TaggedError, panic } from "better-result";
3
7
  //#region src/redline.ts
4
8
  /**
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.
9
+ * Compare two `.docx` buffers and produce a third buffer whose text
10
+ * differences are represented as tracked changes.
10
11
  *
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.
12
+ * Each matched editable story is processed independently through the shared
13
+ * block alignment and document-operation paths. Package parts that exist on
14
+ * only one side are reported because creating or removing those parts is a
15
+ * distinct package-level operation.
44
16
  */
17
+ var InvalidGenerateRedlineDocxOptionsError = class extends TaggedError("InvalidGenerateRedlineDocxOptionsError")() {};
45
18
  const nextBaseBlockIdByIndex = (events) => {
46
19
  const nextIds = Array.from({ length: events.length });
47
20
  let nextId = null;
48
- for (let i = events.length - 1; i >= 0; i--) {
49
- nextIds[i] = nextId;
50
- const event = events[i];
21
+ for (let index = events.length - 1; index >= 0; index--) {
22
+ nextIds[index] = nextId;
23
+ const event = events[index];
51
24
  if (event?.type === "pair") nextId = event.baseBlock.id;
52
25
  else if (event?.type === "baseOnly") nextId = event.block.id;
53
26
  }
54
27
  return nextIds;
55
28
  };
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;
29
+ const buildRedlineOperations = ({ baseSnapshot, revisedBlocks, nextOperationId }) => {
67
30
  const events = alignFolioBlocks(baseSnapshot.blocks, revisedBlocks);
68
31
  const anchorIds = nextBaseBlockIdByIndex(events);
69
32
  const operations = [];
70
- let operationSeq = 0;
71
- const nextOperationId = () => `redline-${++operationSeq}`;
72
- /** Additions past the last base block, kept in revised-document order. */
73
33
  const trailingAdditions = [];
74
34
  const lastBaseBlockId = baseSnapshot.blocks.at(-1)?.id ?? null;
75
35
  events.forEach((event, eventIndex) => {
@@ -123,15 +83,88 @@ const generateRedlineDocx = async (base, revised, options = {}) => {
123
83
  text: addition.text,
124
84
  ...addition.styleId !== void 0 && { styleId: addition.styleId }
125
85
  });
126
- const { applied, skipped } = baseReviewer.applyOperations(operations, {
127
- mode: "tracked-changes",
128
- snapshot: baseSnapshot
86
+ return operations;
87
+ };
88
+ const resolveInputView = (value, option) => {
89
+ if (value === void 0) return "final";
90
+ if (!isFolioResolvedReviewedView(value)) throw new InvalidGenerateRedlineDocxOptionsError({
91
+ message: `${option} must be original or final.`,
92
+ option,
93
+ receivedValue: value
129
94
  });
95
+ return value;
96
+ };
97
+ /** Compare two buffers and return tracked changes for every matched editable story. */
98
+ const generateRedlineDocx = async (base, revised, options = {}) => {
99
+ const baseView = resolveInputView(options.baseView, "baseView");
100
+ const revisedView = resolveInputView(options.revisedView, "revisedView");
101
+ const privacyTransforms = resolveFolioDocumentPrivacyTransforms(options.privacy?.transforms ?? []);
102
+ const [baseReviewer, revisedReviewer] = await Promise.all([FolioDocxReviewer.fromBuffer(base, { author: options.author ?? "folio compare" }), FolioDocxReviewer.fromBuffer(revised)]);
103
+ const baseStories = baseReviewer.listStories().map(({ handle }) => handle);
104
+ const revisedStories = revisedReviewer.listStories().map(({ handle }) => handle);
105
+ for (const story of baseStories) if (!baseReviewer.resolveReviewedStory({
106
+ story,
107
+ view: baseView
108
+ })) panic("A listed base story could not be resolved");
109
+ const applied = [];
110
+ const skipped = [];
111
+ const unprocessedStories = [];
112
+ let operationSequence = 0;
113
+ const nextOperationId = () => `redline-${++operationSequence}`;
114
+ for (const pair of pairFolioDocumentStories(baseStories, revisedStories)) {
115
+ if (!pair.baseStory) {
116
+ unprocessedStories.push({
117
+ ...pair,
118
+ reason: "missing-base-story"
119
+ });
120
+ continue;
121
+ }
122
+ if (!pair.revisedStory) {
123
+ unprocessedStories.push({
124
+ ...pair,
125
+ reason: "missing-revised-story"
126
+ });
127
+ continue;
128
+ }
129
+ const baseSnapshot = baseReviewer.snapshotStory(pair.baseStory);
130
+ const revisedSnapshot = revisedReviewer.readReviewedStory({
131
+ story: pair.revisedStory,
132
+ view: revisedView
133
+ })?.snapshot;
134
+ if (!baseSnapshot || !revisedSnapshot) panic("A matched document story could not be read");
135
+ const operations = buildRedlineOperations({
136
+ baseSnapshot,
137
+ revisedBlocks: revisedSnapshot.blocks,
138
+ nextOperationId
139
+ });
140
+ if (operations.length === 0) continue;
141
+ const result = baseReviewer.applyDocumentOperationsToStory({
142
+ story: pair.baseStory,
143
+ snapshot: baseSnapshot,
144
+ batch: {
145
+ version: 1,
146
+ mode: "tracked-changes",
147
+ operations
148
+ }
149
+ });
150
+ applied.push(...result.applied);
151
+ skipped.push(...result.skipped);
152
+ }
153
+ const redlineBuffer = await baseReviewer.toBuffer();
154
+ const privacyResult = privacyTransforms.length === 0 ? {
155
+ buffer: redlineBuffer,
156
+ privacyReport: {
157
+ appliedTransforms: [],
158
+ removedMetadataProperties: []
159
+ }
160
+ } : await rewriteDocxMetadataPrivacy(redlineBuffer, { transforms: privacyTransforms });
130
161
  return {
131
- buffer: await baseReviewer.toBuffer(),
162
+ buffer: privacyResult.buffer,
132
163
  applied,
133
- skipped
164
+ skipped,
165
+ unprocessedStories,
166
+ privacyReport: privacyResult.privacyReport
134
167
  };
135
168
  };
136
169
  //#endregion
137
- export { generateRedlineDocx };
170
+ export { InvalidGenerateRedlineDocxOptionsError, generateRedlineDocx };
@@ -0,0 +1,31 @@
1
+ import { EditorState } from "prosemirror-state";
2
+
3
+ //#region src/render-dom/BodySelectionOverlay.d.ts
4
+ type BodySelectionOverlayResult = {
5
+ type: "text";
6
+ } | {
7
+ type: "image";
8
+ element: HTMLElement;
9
+ pmPos: number;
10
+ };
11
+ type SyncBodySelectionOverlayOptions = {
12
+ pagesContainer: HTMLElement;
13
+ state: EditorState;
14
+ zoom: number;
15
+ zIndex?: number;
16
+ caretColor?: string;
17
+ selectionColor?: string;
18
+ };
19
+ declare class BodySelectionOverlay {
20
+ clear(pagesContainer: HTMLElement): void;
21
+ sync({
22
+ pagesContainer,
23
+ state,
24
+ zoom,
25
+ zIndex,
26
+ caretColor,
27
+ selectionColor
28
+ }: SyncBodySelectionOverlayOptions): BodySelectionOverlayResult;
29
+ }
30
+ //#endregion
31
+ export { BodySelectionOverlay, BodySelectionOverlayResult, SyncBodySelectionOverlayOptions };