@stll/folio-core 0.32.2 → 0.33.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.
- package/dist/ai-edits/__fixtures__/paragraphs.d.ts +17 -0
- package/dist/ai-edits/__fixtures__/paragraphs.js +33 -0
- package/dist/ai-edits/apply.d.ts +49 -3
- package/dist/ai-edits/apply.js +649 -85
- package/dist/ai-edits/blockRange.d.ts +7 -3
- package/dist/ai-edits/blockRange.js +14 -6
- package/dist/ai-edits/headless.d.ts +39 -5
- package/dist/ai-edits/headless.js +76 -17
- package/dist/ai-edits/index.d.ts +6 -6
- package/dist/ai-edits/index.js +3 -3
- package/dist/ai-edits/read.d.ts +4 -0
- package/dist/ai-edits/read.js +30 -6
- package/dist/ai-edits/scoped-reading.js +4 -3
- package/dist/ai-edits/snapshot.d.ts +42 -2
- package/dist/ai-edits/snapshot.js +127 -62
- package/dist/ai-edits/table-row-column-mutations.d.ts +24 -1
- package/dist/ai-edits/table-row-column-mutations.js +98 -6
- package/dist/ai-edits/table-targets.d.ts +12 -3
- package/dist/ai-edits/table-targets.js +15 -5
- package/dist/ai-edits/types.d.ts +219 -15
- package/dist/ai-edits/word-diff.d.ts +54 -11
- package/dist/ai-edits/word-diff.js +221 -61
- package/dist/compare/__fixtures__/body-sequence.d.ts +33 -0
- package/dist/compare/__fixtures__/body-sequence.js +67 -0
- package/dist/compare/__fixtures__/nested-table.d.ts +16 -0
- package/dist/compare/__fixtures__/nested-table.js +50 -0
- package/dist/compare/__fixtures__/numbered-list.d.ts +34 -0
- package/dist/compare/__fixtures__/numbered-list.js +85 -0
- package/dist/compare/column-alignment.d.ts +27 -0
- package/dist/compare/column-alignment.js +162 -0
- package/dist/compare/compare.d.ts +107 -0
- package/dist/compare/compare.js +406 -0
- package/dist/compare/formatting.d.ts +29 -0
- package/dist/compare/formatting.js +84 -0
- package/dist/compare/plan.d.ts +22 -0
- package/dist/compare/plan.js +1059 -0
- package/dist/compare/reproducible-package.d.ts +17 -0
- package/dist/compare/reproducible-package.js +30 -0
- package/dist/compare/scenario.d.ts +77 -0
- package/dist/compare/scenario.js +263 -0
- package/dist/compare/types.d.ts +282 -0
- package/dist/compare/types.js +32 -0
- package/dist/compare/verification.d.ts +55 -0
- package/dist/compare/verification.js +146 -0
- package/dist/compat/eigenpal.d.ts +9 -6
- package/dist/compat/eigenpal.js +6 -3
- package/dist/controller/layoutPipeline.d.ts +13 -0
- package/dist/controller/layoutPipeline.js +15 -1
- package/dist/display-list/build/buildContext.d.ts +39 -0
- package/dist/display-list/build/buildContext.js +31 -0
- package/dist/display-list/build/buildDisplayList.d.ts +79 -0
- package/dist/display-list/build/buildDisplayList.js +283 -0
- package/dist/display-list/build/colors.d.ts +20 -0
- package/dist/display-list/build/colors.js +136 -0
- package/dist/display-list/build/floatingImages.d.ts +24 -0
- package/dist/display-list/build/floatingImages.js +41 -0
- package/dist/display-list/build/fontTable.d.ts +31 -0
- package/dist/display-list/build/fontTable.js +97 -0
- package/dist/display-list/build/furniture.d.ts +86 -0
- package/dist/display-list/build/furniture.js +98 -0
- package/dist/display-list/build/glyphs.d.ts +55 -0
- package/dist/display-list/build/glyphs.js +103 -0
- package/dist/display-list/build/headerFooterPrimitives.d.ts +17 -0
- package/dist/display-list/build/headerFooterPrimitives.js +40 -0
- package/dist/display-list/build/imagePrimitives.d.ts +35 -0
- package/dist/display-list/build/imagePrimitives.js +264 -0
- package/dist/display-list/build/pageBorderPrimitives.d.ts +23 -0
- package/dist/display-list/build/pageBorderPrimitives.js +111 -0
- package/dist/display-list/build/pageFurniture.d.ts +35 -0
- package/dist/display-list/build/pageFurniture.js +141 -0
- package/dist/display-list/build/paragraphPrimitives.d.ts +19 -0
- package/dist/display-list/build/paragraphPrimitives.js +819 -0
- package/dist/display-list/build/regions.d.ts +46 -0
- package/dist/display-list/build/regions.js +56 -0
- package/dist/display-list/build/storyPrimitives.d.ts +29 -0
- package/dist/display-list/build/storyPrimitives.js +279 -0
- package/dist/display-list/build/strokes.d.ts +19 -0
- package/dist/display-list/build/strokes.js +78 -0
- package/dist/display-list/build/tablePrimitives.d.ts +38 -0
- package/dist/display-list/build/tablePrimitives.js +409 -0
- package/dist/display-list/build/textBoxPrimitives.d.ts +14 -0
- package/dist/display-list/build/textBoxPrimitives.js +129 -0
- package/dist/display-list/build/textDecorations.d.ts +29 -0
- package/dist/display-list/build/textDecorations.js +29 -0
- package/dist/display-list/build/unsupported.d.ts +57 -0
- package/dist/display-list/build/unsupported.js +0 -0
- package/dist/display-list/build/watermarkPrimitives.d.ts +15 -0
- package/dist/display-list/build/watermarkPrimitives.js +128 -0
- package/dist/display-list/dom/renderDisplayListToDom.d.ts +26 -0
- package/dist/display-list/dom/renderDisplayListToDom.js +620 -0
- package/dist/display-list/editor/displayListPagePainter.d.ts +22 -0
- package/dist/display-list/editor/displayListPagePainter.js +63 -0
- package/dist/display-list/editor/pageRenderer.d.ts +24 -0
- package/dist/display-list/editor/pageRenderer.js +36 -0
- package/dist/display-list/primitives.d.ts +95 -0
- package/dist/display-list/primitives.js +127 -0
- package/dist/display-list/types.d.ts +472 -0
- package/dist/display-list/types.js +0 -0
- package/dist/document-operations.d.ts +39 -8
- package/dist/document-operations.js +169 -16
- package/dist/document-stories.d.ts +16 -0
- package/dist/document-stories.js +48 -4
- package/dist/docx/packageParts.d.ts +35 -0
- package/dist/docx/packageParts.js +120 -0
- package/dist/docx/paragraphParser.js +22 -22
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/renderedPageBreakNormalization.js +3 -1
- package/dist/docx/revisionIdNormalization.d.ts +13 -0
- package/dist/docx/revisionIdNormalization.js +103 -0
- package/dist/docx/rezip.js +26 -17
- package/dist/docx/selectiveSave.js +2 -2
- package/dist/docx/serializer/commentSerializer.js +6 -1
- package/dist/docx/serializer/paragraphSerializer.js +33 -4
- package/dist/docx/serializer/runSerializer.js +11 -10
- package/dist/docx/serializer/tableSerializer.js +2 -1
- package/dist/docx/server/applyDocxXmlPatchProposal.js +1 -1
- package/dist/docx/server/createBilingualDocx.js +3 -2
- package/dist/docx/streamingXmlParser.d.ts +13 -1
- package/dist/docx/streamingXmlParser.js +49 -11
- package/dist/docx/unzip.d.ts +1 -2
- package/dist/docx/unzip.js +25 -6
- package/dist/docx/xmlParser.d.ts +3 -1
- package/dist/docx/xmlParser.js +33 -27
- package/dist/export-pdf.d.ts +43 -0
- package/dist/export-pdf.js +98 -0
- package/dist/fonts/headlessMeasure.d.ts +76 -0
- package/dist/fonts/headlessMeasure.js +0 -0
- package/dist/fonts/sfnt/parse.d.ts +51 -0
- package/dist/fonts/sfnt/parse.js +525 -0
- package/dist/fonts/sfnt/subset.d.ts +20 -0
- package/dist/fonts/sfnt/subset.js +350 -0
- package/dist/fonts/sfnt/tables.d.ts +88 -0
- package/dist/fonts/sfnt/tables.js +131 -0
- package/dist/fonts/sfnt/woff.d.ts +14 -0
- package/dist/fonts/sfnt/woff.js +163 -0
- package/dist/generated/text_shaper.js +324 -0
- package/dist/generated/text_shaper_bg.wasm +0 -0
- package/dist/headless-layout.d.ts +75 -0
- package/dist/headless-layout.js +350 -0
- package/dist/index.d.ts +9 -6
- package/dist/index.js +6 -3
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +2 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +3 -1
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
- package/dist/layout-engine/measure/advanceComposition.d.ts +62 -0
- package/dist/layout-engine/measure/advanceComposition.js +148 -0
- package/dist/layout-engine/measure/cache.d.ts +2 -0
- package/dist/layout-engine/measure/measureContainer.js +13 -0
- package/dist/layout-engine/measure/measureParagraph.js +1 -2
- package/dist/layout-engine/measure/measureTypes.d.ts +12 -0
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +3 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +14 -1
- package/dist/layout-engine/tableIndentCompatibility.d.ts +5 -0
- package/dist/layout-engine/tableIndentCompatibility.js +15 -0
- package/dist/layout-engine/types.d.ts +8 -0
- package/dist/layout-painter/renderPage.d.ts +29 -1
- package/dist/layout-painter/renderPage.js +47 -19
- package/dist/layout-painter/renderParagraph.d.ts +21 -2
- package/dist/layout-painter/renderParagraph.js +1 -1
- package/dist/layout-painter/renderTable.js +37 -4
- package/dist/managers/editorShortcuts.d.ts +33 -1
- package/dist/managers/editorShortcuts.js +20 -1
- package/dist/markdown/renderRuns.js +7 -2
- package/dist/markdown/renderTable.js +5 -1
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/pdf/contentStream.d.ts +73 -0
- package/dist/pdf/contentStream.js +143 -0
- package/dist/pdf/fonts.d.ts +96 -0
- package/dist/pdf/fonts.js +593 -0
- package/dist/pdf/images.d.ts +28 -0
- package/dist/pdf/images.js +439 -0
- package/dist/pdf/objects.d.ts +93 -0
- package/dist/pdf/objects.js +237 -0
- package/dist/pdf/pageSpace.d.ts +43 -0
- package/dist/pdf/pageSpace.js +57 -0
- package/dist/pdf/paint.d.ts +32 -0
- package/dist/pdf/paint.js +442 -0
- package/dist/pdf/writePdf.d.ts +56 -0
- package/dist/pdf/writePdf.js +345 -0
- package/dist/prosemirror/commands/comments.js +91 -6
- package/dist/prosemirror/conversion/fromProseDoc.js +41 -7
- package/dist/prosemirror/conversion/toProseDoc.js +41 -4
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +15 -2
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +35 -6
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +2 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +4 -3
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +5 -3
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +9 -3
- package/dist/prosemirror/extensions/nodes/TableExtension.js +56 -39
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +3 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +5 -3
- package/dist/prosemirror/plugins/suggestionMode.js +1 -0
- package/dist/prosemirror/revisionCarriers.js +19 -2
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +2 -2
- package/dist/prosemirror/utils/extractTrackedChanges.js +59 -23
- package/dist/prosemirror/validation.js +55 -27
- package/dist/prosemirror/zeroWidthAnchors.d.ts +10 -0
- package/dist/prosemirror/zeroWidthAnchors.js +23 -0
- package/dist/redline.js +12 -79
- package/dist/server.d.ts +3 -3
- package/dist/server.js +2 -2
- package/dist/shaping/placeRun.d.ts +51 -0
- package/dist/shaping/placeRun.js +84 -0
- package/dist/shaping/shaper.d.ts +92 -0
- package/dist/shaping/shaper.js +102 -0
- package/dist/types/block-id.d.ts +18 -2
- package/dist/types/block-id.js +49 -5
- package/dist/types/content.d.ts +2 -2
- package/dist/utils/clipboard.js +4 -3
- package/dist/utils/fontResolver.js +183 -0
- package/dist/utils/htmlComments.d.ts +17 -0
- package/dist/utils/htmlComments.js +22 -0
- package/dist/utils/scriptSegments.d.ts +12 -1
- package/dist/utils/scriptSegments.js +17 -1
- package/dist/version-comparison.d.ts +1 -1
- package/package.json +7 -3
|
@@ -1,7 +1,46 @@
|
|
|
1
|
-
import { deriveBlockId } from "../types/block-id.js";
|
|
1
|
+
import { deriveBlankBlockId, deriveBlockId } from "../types/block-id.js";
|
|
2
2
|
import { buildCleanBlockText } from "./clean-text.js";
|
|
3
|
+
import { TableMap } from "prosemirror-tables";
|
|
3
4
|
//#region src/ai-edits/snapshot.ts
|
|
4
5
|
const normalizeFolioAIBlockText = (text) => text.replace(/\s+/gu, " ").trim();
|
|
6
|
+
/**
|
|
7
|
+
* Whether a block carries text a reader would see.
|
|
8
|
+
*
|
|
9
|
+
* The snapshot holds every paragraph, blank ones included: an empty cell is
|
|
10
|
+
* part of a table's shape, an empty row is a row, and a comparison that cannot
|
|
11
|
+
* address them cannot describe what changed around them. A reading surface
|
|
12
|
+
* wants the opposite — a model shown a document should see its content, not a
|
|
13
|
+
* line per blank paragraph.
|
|
14
|
+
*
|
|
15
|
+
* So the two needs are split here rather than in the walk: the snapshot is
|
|
16
|
+
* complete, and every surface that reads it for a person or a model states
|
|
17
|
+
* that it wants content by calling this. One helper, so "what counts as
|
|
18
|
+
* content" has a single answer; five inline emptiness checks would drift.
|
|
19
|
+
*/
|
|
20
|
+
const isFolioAIContentBlock = ({ text }) => normalizeFolioAIBlockText(text).length > 0;
|
|
21
|
+
/**
|
|
22
|
+
* The block that content appended to the end of a story hangs from: the last
|
|
23
|
+
* paragraph at body level.
|
|
24
|
+
*
|
|
25
|
+
* Not simply the last block. A story's last block is often inside a table, and
|
|
26
|
+
* a paragraph cannot be appended after one: the insertion escapes to the
|
|
27
|
+
* table's boundary, where the block it was anchored to is not adjacent to it
|
|
28
|
+
* and its paragraph mark ends a different container's paragraph. Neither
|
|
29
|
+
* container-edge rule then applies and the addition cannot be rejected
|
|
30
|
+
* cleanly.
|
|
31
|
+
*
|
|
32
|
+
* A body always ends with a paragraph — a table may not be the last child of a
|
|
33
|
+
* body or a cell, so a package that ends in a table carries a trailing, often
|
|
34
|
+
* empty, paragraph after it — so this is only `null` for a story with no
|
|
35
|
+
* body-level paragraph at all, which is a malformed document.
|
|
36
|
+
*/
|
|
37
|
+
const trailingBodyBlockId = ({ blocks }) => {
|
|
38
|
+
for (let index = blocks.length - 1; index >= 0; index--) {
|
|
39
|
+
const block = blocks[index];
|
|
40
|
+
if (block !== void 0 && block.table === void 0) return block.id;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
};
|
|
5
44
|
const hashFolioAIBlockText = (text) => {
|
|
6
45
|
let hash = 5381;
|
|
7
46
|
for (const character of text) hash = (hash * 33 + (character.codePointAt(0) ?? 0)) % 2147483647;
|
|
@@ -19,63 +58,102 @@ const createFolioAITextRangeHandle = ({ blockId, text, startOffset, endOffset })
|
|
|
19
58
|
};
|
|
20
59
|
};
|
|
21
60
|
const TABLE_ROW_NODE_NAME = "tableRow";
|
|
61
|
+
const TABLE_ROLE_TABLE = "table";
|
|
62
|
+
const TABLE_ROLE_ROW = "row";
|
|
22
63
|
/**
|
|
23
|
-
* True
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
64
|
+
* True for a `tableRow` marked `hidden` (OOXML `w:trPr/w:hidden` — Word never
|
|
65
|
+
* renders the row). Nothing inside such a row may surface its text to the
|
|
66
|
+
* AI/agent snapshot: an attacker DOCX could otherwise smuggle prompt-injection
|
|
67
|
+
* instructions into a row that no human ever sees. The walk skips the row's
|
|
68
|
+
* whole subtree, so a table nested inside a hidden row is hidden with it.
|
|
28
69
|
*/
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
70
|
+
const isHiddenTableRow = (node) => node.type.name === TABLE_ROW_NODE_NAME && node.attrs["hidden"] === true;
|
|
71
|
+
/**
|
|
72
|
+
* Locate the block inside its innermost enclosing table, or `undefined` when
|
|
73
|
+
* it sits outside every table.
|
|
74
|
+
*/
|
|
75
|
+
const getTableLocation = ({ path, blockIndex, tableIndexByStart }) => {
|
|
76
|
+
for (let cellDepth = path.length - 1; cellDepth > 1; cellDepth--) {
|
|
77
|
+
const cell = path[cellDepth];
|
|
78
|
+
if (!cell) continue;
|
|
79
|
+
const role = cell.node.type.spec["tableRole"];
|
|
80
|
+
if (role !== "cell" && role !== "header_cell") continue;
|
|
81
|
+
const row = path[cellDepth - 1];
|
|
82
|
+
const table = path[cellDepth - 2];
|
|
83
|
+
if (!row || !table || row.node.type.spec["tableRole"] !== TABLE_ROLE_ROW || table.node.type.spec["tableRole"] !== TABLE_ROLE_TABLE) return;
|
|
84
|
+
const tableIndex = tableIndexByStart.get(table.start);
|
|
85
|
+
const outerTable = path.find((entry) => entry.node.type.spec["tableRole"] === TABLE_ROLE_TABLE);
|
|
86
|
+
const outerTableIndex = outerTable === void 0 ? void 0 : tableIndexByStart.get(outerTable.start);
|
|
87
|
+
if (tableIndex === void 0 || outerTableIndex === void 0) return;
|
|
88
|
+
const rectangle = TableMap.get(table.node).findCell(cell.start - table.start - 1);
|
|
89
|
+
return {
|
|
90
|
+
outerTableIndex,
|
|
91
|
+
tableIndex,
|
|
92
|
+
rowIndex: row.index,
|
|
93
|
+
cellIndex: cell.index,
|
|
94
|
+
gridColumnIndex: rectangle.left,
|
|
95
|
+
columnSpan: rectangle.right - rectangle.left,
|
|
96
|
+
rowSpan: rectangle.bottom - rectangle.top,
|
|
97
|
+
paragraphIndex: blockIndex
|
|
98
|
+
};
|
|
34
99
|
}
|
|
35
|
-
return false;
|
|
36
100
|
};
|
|
37
101
|
const createFolioAIEditSnapshot = (doc) => {
|
|
38
102
|
const draftBlocks = [];
|
|
39
103
|
const hashCounts = /* @__PURE__ */ new Map();
|
|
40
104
|
const usedBlockIds = /* @__PURE__ */ new Set();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
textblockCount: 0
|
|
44
|
-
};
|
|
105
|
+
const tableIndexByStart = /* @__PURE__ */ new Map();
|
|
106
|
+
const path = [];
|
|
45
107
|
let blockIndex = 0;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
to: pos + node.nodeSize,
|
|
59
|
-
paraId: typeof paraIdAttr === "string" && paraIdAttr.length > 0 ? paraIdAttr : null
|
|
60
|
-
};
|
|
61
|
-
}
|
|
108
|
+
let blankIndex = 0;
|
|
109
|
+
doc.descendants((node, pos, _parent, index) => {
|
|
110
|
+
while (pos >= (path.at(-1)?.end ?? Number.POSITIVE_INFINITY)) path.pop();
|
|
111
|
+
if (!node.isTextblock) {
|
|
112
|
+
if (isHiddenTableRow(node)) return false;
|
|
113
|
+
if (node.type.spec["tableRole"] === TABLE_ROLE_TABLE) tableIndexByStart.set(pos, tableIndexByStart.size);
|
|
114
|
+
if (!node.isLeaf) path.push({
|
|
115
|
+
node,
|
|
116
|
+
start: pos,
|
|
117
|
+
end: pos + node.nodeSize,
|
|
118
|
+
index
|
|
119
|
+
});
|
|
62
120
|
return true;
|
|
63
121
|
}
|
|
122
|
+
const { text } = buildCleanBlockText(node, pos);
|
|
123
|
+
const normalizedText = normalizeFolioAIBlockText(text);
|
|
64
124
|
const textHash = hashFolioAIBlockText(normalizedText);
|
|
65
125
|
hashCounts.set(textHash, (hashCounts.get(textHash) ?? 0) + 1);
|
|
66
|
-
blockIndex++;
|
|
67
126
|
const paraIdAttr = node.attrs["paraId"];
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
127
|
+
const paraId = typeof paraIdAttr === "string" && paraIdAttr.length > 0 ? paraIdAttr : null;
|
|
128
|
+
const isBlank = normalizedText.length === 0;
|
|
129
|
+
let id;
|
|
130
|
+
if (isBlank) {
|
|
131
|
+
blankIndex++;
|
|
132
|
+
id = deriveBlankBlockId({
|
|
133
|
+
paraId,
|
|
134
|
+
index: blankIndex,
|
|
135
|
+
taken: usedBlockIds
|
|
136
|
+
});
|
|
137
|
+
} else {
|
|
138
|
+
blockIndex++;
|
|
139
|
+
id = deriveBlockId({
|
|
140
|
+
paraId,
|
|
141
|
+
index: blockIndex,
|
|
142
|
+
taken: usedBlockIds
|
|
143
|
+
});
|
|
144
|
+
}
|
|
73
145
|
usedBlockIds.add(id);
|
|
74
146
|
const headingLevel = getHeadingLevel(node);
|
|
75
147
|
const kind = getBlockKind(node, headingLevel);
|
|
76
148
|
const displayLabel = getDisplayLabel(node);
|
|
77
149
|
const styleId = getStyleId(node);
|
|
150
|
+
const listLevel = getListLevel(node);
|
|
78
151
|
const previewRuns = getPreviewRuns(node);
|
|
152
|
+
const table = getTableLocation({
|
|
153
|
+
path,
|
|
154
|
+
blockIndex: index,
|
|
155
|
+
tableIndexByStart
|
|
156
|
+
});
|
|
79
157
|
draftBlocks.push({
|
|
80
158
|
block: {
|
|
81
159
|
id,
|
|
@@ -84,7 +162,9 @@ const createFolioAIEditSnapshot = (doc) => {
|
|
|
84
162
|
...headingLevel !== void 0 && { headingLevel },
|
|
85
163
|
...displayLabel !== void 0 && { displayLabel },
|
|
86
164
|
...styleId !== void 0 && { styleId },
|
|
87
|
-
...
|
|
165
|
+
...listLevel !== void 0 && { listLevel },
|
|
166
|
+
...previewRuns !== void 0 && { previewRuns },
|
|
167
|
+
...table !== void 0 && { table }
|
|
88
168
|
},
|
|
89
169
|
anchor: {
|
|
90
170
|
id,
|
|
@@ -106,30 +186,9 @@ const createFolioAIEditSnapshot = (doc) => {
|
|
|
106
186
|
hashOccurrenceCount: hashCounts.get(draft.anchor.textHash) ?? 0
|
|
107
187
|
};
|
|
108
188
|
}
|
|
109
|
-
const emptyAnchorCandidate = emptyAnchorState.candidate;
|
|
110
|
-
if (blocks.length > 0 || emptyAnchorCandidate === null) return {
|
|
111
|
-
blocks,
|
|
112
|
-
anchors
|
|
113
|
-
};
|
|
114
|
-
const emptyDocumentAnchorId = deriveBlockId({
|
|
115
|
-
paraId: emptyAnchorCandidate.paraId,
|
|
116
|
-
index: 1,
|
|
117
|
-
taken: usedBlockIds
|
|
118
|
-
});
|
|
119
|
-
const normalizedText = "";
|
|
120
|
-
anchors[emptyDocumentAnchorId] = {
|
|
121
|
-
id: emptyDocumentAnchorId,
|
|
122
|
-
from: emptyAnchorCandidate.from,
|
|
123
|
-
to: emptyAnchorCandidate.to,
|
|
124
|
-
text: "",
|
|
125
|
-
normalizedText,
|
|
126
|
-
textHash: hashFolioAIBlockText(normalizedText),
|
|
127
|
-
hashOccurrenceCount: emptyAnchorState.textblockCount
|
|
128
|
-
};
|
|
129
189
|
return {
|
|
130
190
|
blocks,
|
|
131
|
-
anchors
|
|
132
|
-
emptyDocumentAnchorId
|
|
191
|
+
anchors
|
|
133
192
|
};
|
|
134
193
|
};
|
|
135
194
|
const getBlockKind = (node, headingLevel) => {
|
|
@@ -153,6 +212,12 @@ const getDisplayLabel = (node) => {
|
|
|
153
212
|
const styleId = node.attrs["styleId"];
|
|
154
213
|
if (typeof styleId === "string" && /^heading/iu.test(styleId)) return styleId;
|
|
155
214
|
};
|
|
215
|
+
const getListLevel = (node) => {
|
|
216
|
+
const numPr = node.attrs["numPr"];
|
|
217
|
+
if (typeof numPr !== "object" || numPr === null || !("ilvl" in numPr)) return;
|
|
218
|
+
const { ilvl } = numPr;
|
|
219
|
+
return typeof ilvl === "number" && Number.isInteger(ilvl) && ilvl >= 0 ? ilvl : void 0;
|
|
220
|
+
};
|
|
156
221
|
const getStyleId = (node) => {
|
|
157
222
|
const styleId = node.attrs["styleId"];
|
|
158
223
|
return typeof styleId === "string" && styleId.length > 0 ? styleId : void 0;
|
|
@@ -267,4 +332,4 @@ const getColorFromAttrs = (attrs) => {
|
|
|
267
332
|
const samePreviewRunStyle = (run, style) => run.bold === style.bold && run.italic === style.italic && run.underline === style.underline && run.strike === style.strike && run.fontFamily === style.fontFamily && run.fontSizePt === style.fontSizePt && run.color === style.color;
|
|
268
333
|
const isUnstyledPreviewRun = ({ bold, italic, underline, strike, fontFamily, fontSizePt, color }) => bold === void 0 && italic === void 0 && underline === void 0 && strike === void 0 && fontFamily === void 0 && fontSizePt === void 0 && color === void 0;
|
|
269
334
|
//#endregion
|
|
270
|
-
export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, normalizeFolioAIBlockText };
|
|
335
|
+
export { createFolioAIEditSnapshot, createFolioAITextRangeHandle, hashFolioAIBlockText, isFolioAIContentBlock, isHiddenTableRow, normalizeFolioAIBlockText, trailingBodyBlockId };
|
|
@@ -9,6 +9,17 @@ type TableRowInsertion = {
|
|
|
9
9
|
rowType: Node["type"];
|
|
10
10
|
cells: readonly Node[];
|
|
11
11
|
rowspanUpdates: readonly number[];
|
|
12
|
+
/**
|
|
13
|
+
* The table's column count, which is not `cells.length`. One cell can occupy
|
|
14
|
+
* several columns (a `colspan`), and one column can have no cell in this row
|
|
15
|
+
* at all (a `rowspan` from a row above reaches down into it).
|
|
16
|
+
*
|
|
17
|
+
* It is what `cellTexts` is SIZED against — the most cells any row of this
|
|
18
|
+
* table could have — while the texts themselves fill the new row's cells in
|
|
19
|
+
* order. Sizing against `cells.length` refused a row the table can hold
|
|
20
|
+
* whenever a span made this row narrower than the table.
|
|
21
|
+
*/
|
|
22
|
+
columnCount: number;
|
|
12
23
|
};
|
|
13
24
|
type TableRowDeletion = Omit<TableRowTarget, "table">;
|
|
14
25
|
type TableColumnInsertion = {
|
|
@@ -74,5 +85,17 @@ type ApplyTableRowDeletionOptions = {
|
|
|
74
85
|
revision: TableStructureRevision | null;
|
|
75
86
|
};
|
|
76
87
|
declare const applyTableRowDeletion: ({ tr, deletion, revision }: ApplyTableRowDeletionOptions) => TableRowColumnMutationResult;
|
|
88
|
+
/**
|
|
89
|
+
* Split one cell's text into the paragraphs it describes, one string each.
|
|
90
|
+
*
|
|
91
|
+
* A cell holds paragraphs, not lines: a line break starts a new one, and a
|
|
92
|
+
* blank line is a blank paragraph, exactly as `text: ""` is for an insertion.
|
|
93
|
+
*
|
|
94
|
+
* This is deliberately not the prose rule `insertAfterBlock` follows, where a
|
|
95
|
+
* blank line between two model-written clauses is formatting noise and is
|
|
96
|
+
* dropped. A cell's text is read off a cell that HAS those paragraphs, so
|
|
97
|
+
* dropping one would lose a block that was there.
|
|
98
|
+
*/
|
|
99
|
+
declare const splitCellParagraphTexts: (text: string) => string[];
|
|
77
100
|
//#endregion
|
|
78
|
-
export { TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, TableStructureRevision, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey };
|
|
101
|
+
export { TableColumnDeletion, TableColumnInsertion, TableRowDeletion, TableRowInsertion, TableStructureRevision, applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
|
|
@@ -46,8 +46,74 @@ const applyTableRowInsertion = ({ tr, insertion, cellTexts, revision }) => {
|
|
|
46
46
|
const rowAttrs = revision ? { trIns: revision } : null;
|
|
47
47
|
const row = insertion.rowType.create(rowAttrs, insertion.cells);
|
|
48
48
|
tr.insert(insertion.rowPosition, populateTableRow(row, cellTexts));
|
|
49
|
+
if (revision) markTableRowContent({
|
|
50
|
+
tr,
|
|
51
|
+
rowPosition: insertion.rowPosition,
|
|
52
|
+
kind: "insertion",
|
|
53
|
+
revision
|
|
54
|
+
});
|
|
49
55
|
return applied(tr, revision);
|
|
50
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Mark the runs inside a row whose structural revision was just written.
|
|
59
|
+
*
|
|
60
|
+
* Word records a tracked row insertion or deletion twice: on the row
|
|
61
|
+
* (`w:trPr/w:ins` | `w:trPr/w:del`) AND around every run in its cells
|
|
62
|
+
* (`w:ins` | `w:del`, the latter with `w:delText`). A consumer that reads only
|
|
63
|
+
* run-level revisions — which is most of them — keeps a deleted row's text on
|
|
64
|
+
* accept and an inserted row's text on reject when the row marker stands
|
|
65
|
+
* alone, so the two must always be written together.
|
|
66
|
+
*
|
|
67
|
+
* Two kinds of run stay unmarked. A run that already carries a revision keeps
|
|
68
|
+
* it, because OOXML nests `w:ins`/`w:del` but the editable model holds one
|
|
69
|
+
* wrapper per run and overwriting would drop the earlier revision. A cell that
|
|
70
|
+
* spans into the row below survives the deletion — `removeRow` moves it down
|
|
71
|
+
* and shortens its span — so marking its text would delete content the
|
|
72
|
+
* accepted document must still hold.
|
|
73
|
+
*/
|
|
74
|
+
const markTableRowContent = ({ tr, rowPosition, kind, revision }) => {
|
|
75
|
+
const row = tr.doc.nodeAt(rowPosition);
|
|
76
|
+
const markType = tr.doc.type.schema.marks[kind];
|
|
77
|
+
if (!row || row.type.spec["tableRole"] !== "row" || !markType) return;
|
|
78
|
+
const mark = markType.create({
|
|
79
|
+
revisionId: revision.revisionId,
|
|
80
|
+
author: revision.author,
|
|
81
|
+
date: revision.date,
|
|
82
|
+
...revision.initials != null && { initials: revision.initials },
|
|
83
|
+
...revision.provenance != null && { provenance: revision.provenance },
|
|
84
|
+
...revision.suggestionId != null && { suggestionId: revision.suggestionId }
|
|
85
|
+
});
|
|
86
|
+
const contentFrom = rowPosition + 1;
|
|
87
|
+
const markable = [];
|
|
88
|
+
let wholeRowMarkable = true;
|
|
89
|
+
row.descendants((node, offset) => {
|
|
90
|
+
if (isRowSpanningCell(node)) {
|
|
91
|
+
wholeRowMarkable = false;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!node.isInline) return true;
|
|
95
|
+
if (node.marks.some(isTrackedChangeMark)) {
|
|
96
|
+
wholeRowMarkable = false;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const from = contentFrom + offset;
|
|
100
|
+
markable.push({
|
|
101
|
+
from,
|
|
102
|
+
to: from + node.nodeSize
|
|
103
|
+
});
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
106
|
+
if (wholeRowMarkable) {
|
|
107
|
+
tr.addMark(contentFrom, rowPosition + row.nodeSize - 1, mark);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const range of markable) tr.addMark(range.from, range.to, mark);
|
|
111
|
+
};
|
|
112
|
+
const isTrackedChangeMark = (mark) => mark.type.name === "insertion" || mark.type.name === "deletion";
|
|
113
|
+
const isRowSpanningCell = (node) => {
|
|
114
|
+
const role = node.type.spec["tableRole"];
|
|
115
|
+
return (role === "cell" || role === "header_cell") && Number(node.attrs["rowspan"]) > 1;
|
|
116
|
+
};
|
|
51
117
|
const applyTableColumnInsertion = ({ tr, insertion, cellTexts, revision }) => {
|
|
52
118
|
const tablePosition = tr.mapping.map(insertion.tablePosition, 1);
|
|
53
119
|
const table = tr.doc.nodeAt(tablePosition);
|
|
@@ -123,6 +189,12 @@ const applyTableRowDeletion = ({ tr, deletion, revision }) => {
|
|
|
123
189
|
const row = tr.doc.nodeAt(rowPosition);
|
|
124
190
|
if (!row || row.type.spec["tableRole"] !== "row" || row.attrs["trIns"] != null || row.attrs["trDel"] != null) return { type: "unsupported" };
|
|
125
191
|
tr.setNodeAttribute(rowPosition, "trDel", revision);
|
|
192
|
+
markTableRowContent({
|
|
193
|
+
tr,
|
|
194
|
+
rowPosition,
|
|
195
|
+
kind: "deletion",
|
|
196
|
+
revision
|
|
197
|
+
});
|
|
126
198
|
markStructuralChange(tr);
|
|
127
199
|
return applied(tr, revision);
|
|
128
200
|
}
|
|
@@ -194,22 +266,42 @@ const buildTableRowInsertion = ({ map, table, tableStart, rowIndex }) => {
|
|
|
194
266
|
rowPosition,
|
|
195
267
|
rowType: tableNodeTypes(table.type.schema).row,
|
|
196
268
|
cells,
|
|
197
|
-
rowspanUpdates
|
|
269
|
+
rowspanUpdates,
|
|
270
|
+
columnCount: map.width
|
|
198
271
|
};
|
|
199
272
|
};
|
|
273
|
+
const CELL_LINE_BREAK_PATTERN = /\r\n|\r|\n/;
|
|
274
|
+
/**
|
|
275
|
+
* Split one cell's text into the paragraphs it describes, one string each.
|
|
276
|
+
*
|
|
277
|
+
* A cell holds paragraphs, not lines: a line break starts a new one, and a
|
|
278
|
+
* blank line is a blank paragraph, exactly as `text: ""` is for an insertion.
|
|
279
|
+
*
|
|
280
|
+
* This is deliberately not the prose rule `insertAfterBlock` follows, where a
|
|
281
|
+
* blank line between two model-written clauses is formatting noise and is
|
|
282
|
+
* dropped. A cell's text is read off a cell that HAS those paragraphs, so
|
|
283
|
+
* dropping one would lose a block that was there.
|
|
284
|
+
*/
|
|
285
|
+
const splitCellParagraphTexts = (text) => text.split(CELL_LINE_BREAK_PATTERN);
|
|
286
|
+
/**
|
|
287
|
+
* Fill the new row's cells from `cellTexts`, in order: the nth text goes in
|
|
288
|
+
* the nth cell the row actually has, which is how a caller reading the row
|
|
289
|
+
* through the block snapshot counts them (`table.cellIndex`). Cells the caller
|
|
290
|
+
* did not name stay empty.
|
|
291
|
+
*/
|
|
200
292
|
const populateTableRow = (row, cellTexts) => {
|
|
201
293
|
if (cellTexts === void 0) return row;
|
|
202
294
|
const cells = [];
|
|
203
295
|
row.forEach((cell, _offset, index) => {
|
|
296
|
+
const text = cellTexts[index] ?? "";
|
|
204
297
|
const paragraph = cell.firstChild;
|
|
205
298
|
if (!paragraph?.isTextblock) {
|
|
206
299
|
cells.push(cell);
|
|
207
300
|
return;
|
|
208
301
|
}
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
cells.push(cell.type.create(cell.attrs, nextParagraph));
|
|
302
|
+
const attrs = stripBlockIdentityAttrs(paragraph.attrs);
|
|
303
|
+
const paragraphs = splitCellParagraphTexts(text).map((line) => paragraph.type.create(attrs, line.length > 0 ? row.type.schema.text(line) : null));
|
|
304
|
+
cells.push(cell.type.create(cell.attrs, paragraphs));
|
|
213
305
|
});
|
|
214
306
|
return row.type.create(row.attrs, cells);
|
|
215
307
|
};
|
|
@@ -297,4 +389,4 @@ const getTableColumnDeletionCellPositions = ({ map, table, columnIndex }) => {
|
|
|
297
389
|
return [...positions];
|
|
298
390
|
};
|
|
299
391
|
//#endregion
|
|
300
|
-
export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey };
|
|
392
|
+
export { applyTableColumnDeletion, applyTableColumnInsertion, applyTableRowDeletion, applyTableRowInsertion, findTableColumnInsertion, findTableRowInsertion, getTableColumnCoordinateKey, splitCellParagraphTexts };
|
|
@@ -22,10 +22,19 @@ type TableCellTarget = {
|
|
|
22
22
|
topRowIndex: number;
|
|
23
23
|
bottomRowIndex: number;
|
|
24
24
|
};
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* The boundary a block-adjacent insertion escapes to: the outermost table the
|
|
27
|
+
* anchor sits in, so the new block lands as a peer of that table at document
|
|
28
|
+
* level.
|
|
29
|
+
*
|
|
30
|
+
* The nearest table is the wrong answer once tables nest. Escaping one level
|
|
31
|
+
* out of a table inside a cell leaves the insertion in the enclosing cell,
|
|
32
|
+
* which is still "inside a table" and is never what a block-level insertion
|
|
33
|
+
* meant.
|
|
34
|
+
*/
|
|
35
|
+
declare const findOutermostTableBoundary: (doc: Node, blockFrom: number) => TableBoundary | null;
|
|
27
36
|
declare const findEnclosingTableRow: (doc: Node, blockFrom: number) => TableRowTarget | null;
|
|
28
37
|
declare const findEnclosingTableCell: (doc: Node, blockFrom: number) => TableCellTarget | null;
|
|
29
38
|
declare const tableRectangleCutsMergedCell: (map: TableMap, rectangle: TableRectangle) => boolean;
|
|
30
39
|
//#endregion
|
|
31
|
-
export { TableBoundary, TableCellTarget, TableRowTarget,
|
|
40
|
+
export { TableBoundary, TableCellTarget, TableRowTarget, findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell };
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import { TableMap } from "prosemirror-tables";
|
|
2
2
|
//#region src/ai-edits/table-targets.ts
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The boundary a block-adjacent insertion escapes to: the outermost table the
|
|
5
|
+
* anchor sits in, so the new block lands as a peer of that table at document
|
|
6
|
+
* level.
|
|
7
|
+
*
|
|
8
|
+
* The nearest table is the wrong answer once tables nest. Escaping one level
|
|
9
|
+
* out of a table inside a cell leaves the insertion in the enclosing cell,
|
|
10
|
+
* which is still "inside a table" and is never what a block-level insertion
|
|
11
|
+
* meant.
|
|
12
|
+
*/
|
|
13
|
+
const findOutermostTableBoundary = (doc, blockFrom) => {
|
|
5
14
|
const resolved = doc.resolve(blockFrom);
|
|
6
|
-
|
|
15
|
+
let boundary = null;
|
|
16
|
+
for (let depth = resolved.depth; depth > 0; depth--) if (resolved.node(depth).type.name === "table") boundary = {
|
|
7
17
|
before: resolved.before(depth),
|
|
8
18
|
after: resolved.after(depth)
|
|
9
19
|
};
|
|
10
|
-
return
|
|
20
|
+
return boundary;
|
|
11
21
|
};
|
|
12
22
|
const findEnclosingTableRow = (doc, blockFrom) => {
|
|
13
23
|
const resolved = doc.resolve(blockFrom);
|
|
@@ -67,4 +77,4 @@ const tableRectangleCutsMergedCell = (map, rectangle) => {
|
|
|
67
77
|
return false;
|
|
68
78
|
};
|
|
69
79
|
//#endregion
|
|
70
|
-
export {
|
|
80
|
+
export { findEnclosingTableCell, findEnclosingTableRow, findOutermostTableBoundary, tableRectangleCutsMergedCell };
|