@stll/folio-core 0.28.1 → 0.29.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/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +61 -95
- package/dist/markdown/renderParagraph.js +7 -2
- package/dist/prosemirror/attrs/index.js +2 -1
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
- package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +170 -52
- package/dist/prosemirror/conversion/toProseDoc.js +169 -61
- package/dist/prosemirror/extensions/StarterKit.js +11 -3
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
- package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
- package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
- package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
- package/dist/prosemirror/listMarker.d.ts +58 -0
- package/dist/prosemirror/listMarker.js +185 -0
- package/dist/prosemirror/numberedRefFields.d.ts +24 -0
- package/dist/prosemirror/numberedRefFields.js +276 -0
- package/dist/prosemirror/paraText.js +56 -17
- package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
- package/dist/prosemirror/plugins/pmTextScan.js +28 -7
- package/dist/prosemirror/plugins/templateDirectives.js +2 -2
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/nodes.d.ts +13 -1
- package/dist/prosemirror/validation.js +93 -0
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
|
|
|
6
6
|
import { emuToPixels } from "../../utils/units.js";
|
|
7
7
|
import { setAutospacingBaseValue } from "../autospacingBase.js";
|
|
8
8
|
import { buildRunFormattingOverrideAttrs } from "../extensions/marks/RunFormattingOverrideExtension.js";
|
|
9
|
+
import { stampNumberedRefFieldBaselines } from "../numberedRefFields.js";
|
|
9
10
|
import { directionFromBidi } from "../paragraphDirection.js";
|
|
10
11
|
import { schema } from "../schema/index.js";
|
|
11
12
|
import { cascadeStyleTextFormatting } from "../styles/styleToggleCascade.js";
|
|
@@ -36,6 +37,56 @@ const createHyperlinkInstanceIndexAllocator = () => {
|
|
|
36
37
|
return () => index++;
|
|
37
38
|
};
|
|
38
39
|
/**
|
|
40
|
+
* Find bookmark pairs across every editable inline wrapper in a document story.
|
|
41
|
+
* Each endpoint is converted at its own structural position, so a range may
|
|
42
|
+
* start outside a hyperlink and end inside it (or the inverse).
|
|
43
|
+
*/
|
|
44
|
+
const collectPairedBookmarkIds = (blocks) => {
|
|
45
|
+
const counts = /* @__PURE__ */ new Map();
|
|
46
|
+
let position = 0;
|
|
47
|
+
const countBoundary = (id, type) => {
|
|
48
|
+
const count = counts.get(id) ?? {
|
|
49
|
+
starts: 0,
|
|
50
|
+
ends: 0
|
|
51
|
+
};
|
|
52
|
+
if (type === "start") {
|
|
53
|
+
count.starts += 1;
|
|
54
|
+
count.firstStart ??= position;
|
|
55
|
+
} else {
|
|
56
|
+
count.ends += 1;
|
|
57
|
+
count.firstEnd ??= position;
|
|
58
|
+
}
|
|
59
|
+
position += 1;
|
|
60
|
+
counts.set(id, count);
|
|
61
|
+
};
|
|
62
|
+
const visitRun = (run) => {
|
|
63
|
+
for (const content of run.content) if (content.type === "shape" && content.shape.textBody) visitBlocks(content.shape.textBody.content);
|
|
64
|
+
};
|
|
65
|
+
const visitHyperlink = (hyperlink) => {
|
|
66
|
+
for (const child of hyperlink.children) if (child.type === "bookmarkStart") countBoundary(child.id, "start");
|
|
67
|
+
else if (child.type === "bookmarkEnd") countBoundary(child.id, "end");
|
|
68
|
+
else visitRun(child);
|
|
69
|
+
};
|
|
70
|
+
const visitParagraphContent = (content) => {
|
|
71
|
+
if (content.type === "bookmarkStart") countBoundary(content.id, "start");
|
|
72
|
+
else if (content.type === "bookmarkEnd") countBoundary(content.id, "end");
|
|
73
|
+
else if (content.type === "run") visitRun(content);
|
|
74
|
+
else if (content.type === "hyperlink") visitHyperlink(content);
|
|
75
|
+
else if (content.type === "simpleField") for (const child of content.content) if (child.type === "hyperlink") visitHyperlink(child);
|
|
76
|
+
else visitRun(child);
|
|
77
|
+
else if (content.type === "complexField") for (const run of [...content.fieldCode ?? [], ...content.fieldResult]) visitRun(run);
|
|
78
|
+
else if (content.type === "inlineSdt") for (const child of content.content) visitParagraphContent(child);
|
|
79
|
+
else if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") for (const child of content.content) visitParagraphContent(child);
|
|
80
|
+
};
|
|
81
|
+
const visitBlocks = (nestedBlocks) => {
|
|
82
|
+
for (const block of nestedBlocks) if (block.type === "paragraph") for (const content of block.content) visitParagraphContent(content);
|
|
83
|
+
else if (block.type === "table") for (const row of block.rows) for (const cell of row.cells) visitBlocks(cell.content);
|
|
84
|
+
else visitBlocks(block.content);
|
|
85
|
+
};
|
|
86
|
+
visitBlocks(blocks);
|
|
87
|
+
return new Set([...counts].flatMap(([id, count]) => count.starts === 1 && count.ends === 1 && count.firstStart !== void 0 && count.firstEnd !== void 0 && count.firstStart < count.firstEnd ? [id] : []));
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
39
90
|
* Convert a Document to a ProseMirror document
|
|
40
91
|
*
|
|
41
92
|
* @param document - The Document to convert
|
|
@@ -50,7 +101,8 @@ function toProseDoc(document, options) {
|
|
|
50
101
|
const conversionContext = {
|
|
51
102
|
theme,
|
|
52
103
|
nextTextBoxGroupId,
|
|
53
|
-
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
|
|
104
|
+
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
|
|
105
|
+
pairedBookmarkIds: collectPairedBookmarkIds(paragraphs)
|
|
54
106
|
};
|
|
55
107
|
const convertBodyBlocks = (blocks) => {
|
|
56
108
|
const out = [];
|
|
@@ -85,7 +137,7 @@ function toProseDoc(document, options) {
|
|
|
85
137
|
};
|
|
86
138
|
nodes.push(...convertBodyBlocks(paragraphs));
|
|
87
139
|
if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
|
|
88
|
-
const pmDoc = schema.node("doc", null, nodes);
|
|
140
|
+
const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", null, nodes));
|
|
89
141
|
assertValidProseMirrorDocument(pmDoc, "Document conversion produced an invalid ProseMirror document");
|
|
90
142
|
return pmDoc;
|
|
91
143
|
}
|
|
@@ -126,7 +178,7 @@ function convertBlockSdt(blockSdt, convertBlocks) {
|
|
|
126
178
|
* Resolves style-based text formatting and passes it to runs so that
|
|
127
179
|
* paragraph styles (like Heading1) apply their font size, color, etc.
|
|
128
180
|
*/
|
|
129
|
-
function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
|
|
181
|
+
function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex, pairedBookmarkIds, activeCommentIds, extraRunFormatting, tableParagraphOverlay, textBoxAnchors) {
|
|
130
182
|
const attrs = paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay);
|
|
131
183
|
const isTocParagraph = attrs._tableOfContentsLevel !== void 0;
|
|
132
184
|
const inlineNodes = [];
|
|
@@ -209,42 +261,56 @@ function convertParagraph(paragraph, styleResolver, nextHyperlinkInstanceIndex,
|
|
|
209
261
|
const emitTrackedChange = (change, markType, moveKind) => {
|
|
210
262
|
emitInlineNodes(convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, moveKind, textBoxAnchors));
|
|
211
263
|
};
|
|
212
|
-
for (const content of paragraph.content)
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
...content.rId !== void 0 ? { rId: content.rId } : {}
|
|
232
|
-
});
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
emitInlineNodes(linkNodes);
|
|
236
|
-
} else if (content.type === "simpleField" || content.type === "complexField") emitInlineNode(convertField(content, getInheritedRunFormatting, styleResolver));
|
|
237
|
-
else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors));
|
|
238
|
-
else if (content.type === "insertion" || content.type === "moveTo") emitTrackedChange(content, "insertion", content.type === "moveTo" ? "moveTo" : null);
|
|
239
|
-
else if (content.type === "deletion" || content.type === "moveFrom") emitTrackedChange(content, "deletion", content.type === "moveFrom" ? "moveFrom" : null);
|
|
240
|
-
else if (content.type === "mathEquation") emitInlineNode(convertMathEquation(content));
|
|
241
|
-
if (content.type === "bookmarkStart") {
|
|
242
|
-
if (!bookmarksArr) bookmarksArr = [];
|
|
243
|
-
bookmarksArr.push({
|
|
244
|
-
id: content.id,
|
|
245
|
-
name: content.name
|
|
264
|
+
for (const content of paragraph.content) if (content.type === "commentRangeStart") commentIds.add(content.id);
|
|
265
|
+
else if (content.type === "commentRangeEnd") commentIds.delete(content.id);
|
|
266
|
+
else if (content.type === "commentReference") anchorPointComment(inlineNodes, content.id);
|
|
267
|
+
else if (content.type === "run") emitInlineNodes(convertRun(content, getInheritedRunFormatting(content.formatting), styleResolver, textBoxAnchors));
|
|
268
|
+
else if (content.type === "hyperlink") {
|
|
269
|
+
const linkNodes = convertHyperlink(content, {
|
|
270
|
+
getInheritedRunFormatting,
|
|
271
|
+
styleResolver,
|
|
272
|
+
hyperlinkIndex: nextHyperlinkInstanceIndex(),
|
|
273
|
+
textBoxAnchors
|
|
274
|
+
});
|
|
275
|
+
if (linkNodes.length === 0) {
|
|
276
|
+
emptyHyperlinks ??= [];
|
|
277
|
+
emptyHyperlinks.push({
|
|
278
|
+
offset: inlineOffset,
|
|
279
|
+
...content.href !== void 0 ? { href: content.href } : {},
|
|
280
|
+
...content.anchor !== void 0 ? { anchor: content.anchor } : {},
|
|
281
|
+
...content.tooltip !== void 0 ? { tooltip: content.tooltip } : {},
|
|
282
|
+
...content.rId !== void 0 ? { rId: content.rId } : {}
|
|
246
283
|
});
|
|
284
|
+
continue;
|
|
247
285
|
}
|
|
286
|
+
emitInlineNodes(linkNodes);
|
|
287
|
+
} else if (content.type === "simpleField" || content.type === "complexField") emitInlineNode(convertField(content, {
|
|
288
|
+
getInheritedRunFormatting,
|
|
289
|
+
styleResolver,
|
|
290
|
+
nextHyperlinkInstanceIndex,
|
|
291
|
+
textBoxAnchors
|
|
292
|
+
}));
|
|
293
|
+
else if (content.type === "inlineSdt") emitInlineNode(convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors));
|
|
294
|
+
else if (content.type === "insertion" || content.type === "moveTo") emitTrackedChange(content, "insertion", content.type === "moveTo" ? "moveTo" : null);
|
|
295
|
+
else if (content.type === "deletion" || content.type === "moveFrom") emitTrackedChange(content, "deletion", content.type === "moveFrom" ? "moveFrom" : null);
|
|
296
|
+
else if (content.type === "mathEquation") emitInlineNode(convertMathEquation(content));
|
|
297
|
+
else if (content.type === "bookmarkStart" && pairedBookmarkIds.has(content.id)) emitInlineNode(schema.node("bookmarkBoundary", {
|
|
298
|
+
type: "start",
|
|
299
|
+
id: content.id,
|
|
300
|
+
name: content.name,
|
|
301
|
+
colFirst: content.colFirst,
|
|
302
|
+
colLast: content.colLast
|
|
303
|
+
}));
|
|
304
|
+
else if (content.type === "bookmarkEnd" && pairedBookmarkIds.has(content.id)) emitInlineNode(schema.node("bookmarkBoundary", {
|
|
305
|
+
type: "end",
|
|
306
|
+
id: content.id
|
|
307
|
+
}));
|
|
308
|
+
else if (content.type === "bookmarkStart") {
|
|
309
|
+
if (!bookmarksArr) bookmarksArr = [];
|
|
310
|
+
bookmarksArr.push({
|
|
311
|
+
id: content.id,
|
|
312
|
+
name: content.name
|
|
313
|
+
});
|
|
248
314
|
}
|
|
249
315
|
if (bookmarksArr) attrs.bookmarks = bookmarksArr;
|
|
250
316
|
if (emptyHyperlinks) attrs._emptyHyperlinks = emptyHyperlinks;
|
|
@@ -313,7 +379,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, getI
|
|
|
313
379
|
});
|
|
314
380
|
}
|
|
315
381
|
function canCarryTrackedRunMark(node, markType) {
|
|
316
|
-
return node.isText || node.isInline && node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "textBoxAnchor");
|
|
382
|
+
return node.isText || node.isInline && node.type.allowsMarkType(markType) && (node.type.name === "image" || node.type.name === "shape" || node.type.name === "hardBreak" || node.type.name === "tab" || node.type.name === "symbol" || node.type.name === "bookmarkBoundary" || node.type.name === "textBoxAnchor");
|
|
317
383
|
}
|
|
318
384
|
/**
|
|
319
385
|
* Convert ParagraphFormatting to ProseMirror paragraph attrs
|
|
@@ -1011,7 +1077,8 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
|
|
|
1011
1077
|
context: {
|
|
1012
1078
|
theme: null,
|
|
1013
1079
|
nextTextBoxGroupId: createTextBoxGroupIdFactory(),
|
|
1014
|
-
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
|
|
1080
|
+
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
|
|
1081
|
+
pairedBookmarkIds: collectPairedBookmarkIds(cell.content)
|
|
1015
1082
|
},
|
|
1016
1083
|
isHeader: nodeType === "tableHeader",
|
|
1017
1084
|
gridWidthPercent: void 0,
|
|
@@ -1024,31 +1091,47 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
|
|
|
1024
1091
|
defaultCellMargins: void 0
|
|
1025
1092
|
});
|
|
1026
1093
|
}
|
|
1027
|
-
|
|
1028
|
-
* Convert a SimpleField or ComplexField to a ProseMirror field node.
|
|
1029
|
-
* Preserves run formatting (bold, fontSize, color, etc.) as PM marks.
|
|
1030
|
-
* Accepts a run formatting resolver so fields inherit paragraph-level
|
|
1031
|
-
* formatting the same way regular text runs do.
|
|
1032
|
-
*/
|
|
1033
|
-
function convertField(field, getInheritedRunFormatting, styleResolver) {
|
|
1094
|
+
function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyperlinkInstanceIndex, textBoxAnchors }) {
|
|
1034
1095
|
let displayText = "";
|
|
1035
1096
|
let fieldFormatting;
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1097
|
+
const inlineNodes = [];
|
|
1098
|
+
const hasStructuredSourceContent = field.type === "simpleField" && field.content.some((content) => content.type === "hyperlink");
|
|
1099
|
+
const appendRun = (run) => {
|
|
1100
|
+
for (const content of run.content) if (content.type === "text") displayText += content.text;
|
|
1101
|
+
fieldFormatting ??= run.formatting;
|
|
1102
|
+
if (!hasStructuredSourceContent) return;
|
|
1103
|
+
inlineNodes.push(...convertRun(run, getInheritedRunFormatting(run.formatting, field.fieldType), styleResolver, textBoxAnchors));
|
|
1104
|
+
};
|
|
1105
|
+
if (field.type === "simpleField") for (const content of field.content) {
|
|
1106
|
+
if (content.type === "run") {
|
|
1107
|
+
appendRun(content);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
for (const child of content.children) if (child.type === "run") {
|
|
1111
|
+
for (const runContent of child.content) if (runContent.type === "text") displayText += runContent.text;
|
|
1112
|
+
fieldFormatting ??= child.formatting;
|
|
1113
|
+
}
|
|
1114
|
+
inlineNodes.push(...convertHyperlink(content, {
|
|
1115
|
+
getInheritedRunFormatting: (formatting) => getInheritedRunFormatting(formatting, field.fieldType),
|
|
1116
|
+
styleResolver,
|
|
1117
|
+
hyperlinkIndex: nextHyperlinkInstanceIndex(),
|
|
1118
|
+
textBoxAnchors
|
|
1119
|
+
}));
|
|
1040
1120
|
}
|
|
1121
|
+
else for (const run of field.fieldResult) appendRun(run);
|
|
1041
1122
|
if (!fieldFormatting && field.type === "complexField" && field.fieldResult.length === 0) fieldFormatting = field.formatting;
|
|
1042
1123
|
const inheritedFormatting = getInheritedRunFormatting(fieldFormatting, field.fieldType);
|
|
1043
1124
|
const { marks } = buildRunMarks(fieldFormatting, inheritedFormatting, styleResolver);
|
|
1044
|
-
|
|
1125
|
+
const hasConvertedHyperlinkContent = inlineNodes.some((node) => node.marks.some((mark) => mark.type.name === "hyperlink"));
|
|
1126
|
+
const createStructuredField = hasStructuredSourceContent && hasConvertedHyperlinkContent;
|
|
1127
|
+
return schema.node(createStructuredField ? "structuredField" : "field", {
|
|
1045
1128
|
fieldType: field.fieldType,
|
|
1046
1129
|
instruction: field.instruction,
|
|
1047
1130
|
displayText,
|
|
1048
1131
|
fieldKind: field.type === "simpleField" ? "simple" : "complex",
|
|
1049
1132
|
fldLock: field.fldLock ?? false,
|
|
1050
1133
|
dirty: field.dirty ?? false
|
|
1051
|
-
}, void 0, marks);
|
|
1134
|
+
}, createStructuredField ? inlineNodes : void 0, marks);
|
|
1052
1135
|
}
|
|
1053
1136
|
/**
|
|
1054
1137
|
* Convert a MathEquation to a ProseMirror math node.
|
|
@@ -1078,7 +1161,12 @@ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, getInheritedRunFormat
|
|
|
1078
1161
|
});
|
|
1079
1162
|
inlineNodes.push(...linkNodes);
|
|
1080
1163
|
} else if (content.type === "simpleField" || content.type === "complexField") {
|
|
1081
|
-
const fieldNode = convertField(content,
|
|
1164
|
+
const fieldNode = convertField(content, {
|
|
1165
|
+
getInheritedRunFormatting,
|
|
1166
|
+
styleResolver,
|
|
1167
|
+
nextHyperlinkInstanceIndex,
|
|
1168
|
+
textBoxAnchors
|
|
1169
|
+
});
|
|
1082
1170
|
if (fieldNode) inlineNodes.push(fieldNode);
|
|
1083
1171
|
} else if (content.type === "inlineSdt") {
|
|
1084
1172
|
const nestedSdt = convertInlineSdt(content, nextHyperlinkInstanceIndex, getInheritedRunFormatting, styleResolver, textBoxAnchors);
|
|
@@ -1338,11 +1426,30 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
|
|
|
1338
1426
|
rId: hyperlink.rId,
|
|
1339
1427
|
_docxHyperlinkIndex: hyperlinkIndex
|
|
1340
1428
|
});
|
|
1341
|
-
for (const child of hyperlink.children)
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1429
|
+
for (const child of hyperlink.children) {
|
|
1430
|
+
if (child.type === "bookmarkStart") {
|
|
1431
|
+
nodes.push(schema.node("bookmarkBoundary", {
|
|
1432
|
+
type: "start",
|
|
1433
|
+
id: child.id,
|
|
1434
|
+
name: child.name,
|
|
1435
|
+
colFirst: child.colFirst,
|
|
1436
|
+
colLast: child.colLast
|
|
1437
|
+
}, void 0, [linkMark]));
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (child.type === "bookmarkEnd") {
|
|
1441
|
+
nodes.push(schema.node("bookmarkBoundary", {
|
|
1442
|
+
type: "end",
|
|
1443
|
+
id: child.id
|
|
1444
|
+
}, void 0, [linkMark]));
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
if (child.type === "run") {
|
|
1448
|
+
const inheritedFormatting = getInheritedRunFormatting(child.formatting);
|
|
1449
|
+
const { marks: runMarks, mergedFormatting } = buildRunMarks(child.formatting, inheritedFormatting, styleResolver);
|
|
1450
|
+
const allMarks = [...runMarks, linkMark];
|
|
1451
|
+
for (const content of child.content) nodes.push(...convertRunContent(content, allMarks, mergedFormatting, textBoxAnchors));
|
|
1452
|
+
}
|
|
1346
1453
|
}
|
|
1347
1454
|
return nodes;
|
|
1348
1455
|
}
|
|
@@ -1511,7 +1618,7 @@ function convertShape(shape) {
|
|
|
1511
1618
|
}
|
|
1512
1619
|
function convertParagraphWithTextBoxes(block, styleResolver, { textBoxGroupId, context, extraRunFormatting, tableParagraphOverlay }) {
|
|
1513
1620
|
const { textBoxes, textBoxAnchors } = extractTextBoxesFromParagraph(block, textBoxGroupId);
|
|
1514
|
-
const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
|
|
1621
|
+
const pmParagraph = convertParagraph(block, styleResolver, context.nextHyperlinkInstanceIndex, context.pairedBookmarkIds, void 0, extraRunFormatting, tableParagraphOverlay, textBoxAnchors);
|
|
1515
1622
|
const nodes = [];
|
|
1516
1623
|
const isEmptyAfterExtraction = textBoxes.length > 0 && !hasContentBesidesTextBoxAnchors(pmParagraph);
|
|
1517
1624
|
const keepWrapperParagraph = isEmptyAfterExtraction && hasParagraphBoundaryPayload(block, pmParagraph);
|
|
@@ -1773,7 +1880,8 @@ function headerFooterToProseDoc(content, options) {
|
|
|
1773
1880
|
const conversionContext = {
|
|
1774
1881
|
theme,
|
|
1775
1882
|
nextTextBoxGroupId,
|
|
1776
|
-
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator()
|
|
1883
|
+
nextHyperlinkInstanceIndex: createHyperlinkInstanceIndexAllocator(),
|
|
1884
|
+
pairedBookmarkIds: collectPairedBookmarkIds(content)
|
|
1777
1885
|
};
|
|
1778
1886
|
const convertBlocks = (blocks) => {
|
|
1779
1887
|
const out = [];
|
|
@@ -1787,7 +1895,7 @@ function headerFooterToProseDoc(content, options) {
|
|
|
1787
1895
|
};
|
|
1788
1896
|
nodes.push(...convertBlocks(content));
|
|
1789
1897
|
if (nodes.length === 0) nodes.push(schema.node("paragraph", {}, []));
|
|
1790
|
-
const pmDoc = schema.node("doc", null, nodes);
|
|
1898
|
+
const pmDoc = stampNumberedRefFieldBaselines(schema.node("doc", null, nodes));
|
|
1791
1899
|
assertValidProseMirrorDocument(pmDoc, "Header/footer conversion produced an invalid ProseMirror document");
|
|
1792
1900
|
return pmDoc;
|
|
1793
1901
|
}
|
|
@@ -43,7 +43,8 @@ import { EmbossExtension, EmphasisMarkExtension, ImprintExtension, TextOutlineEx
|
|
|
43
43
|
import { DeletionExtension, InsertionExtension, RunPropertyChangeExtension } from "./marks/TrackedChangeExtensions.js";
|
|
44
44
|
import { UnderlineExtension } from "./marks/UnderlineExtension.js";
|
|
45
45
|
import { BlockSdtExtension } from "./nodes/BlockSdtExtension.js";
|
|
46
|
-
import {
|
|
46
|
+
import { BookmarkBoundaryExtension } from "./nodes/BookmarkBoundaryExtension.js";
|
|
47
|
+
import { FieldExtension, StructuredFieldExtension } from "./nodes/FieldExtension.js";
|
|
47
48
|
import { HardBreakExtension } from "./nodes/HardBreakExtension.js";
|
|
48
49
|
import { HorizontalRuleExtension } from "./nodes/HorizontalRuleExtension.js";
|
|
49
50
|
import { ImageExtension } from "./nodes/ImageExtension.js";
|
|
@@ -64,6 +65,11 @@ import { TextBoxExtension } from "./nodes/TextBoxExtension.js";
|
|
|
64
65
|
function createStarterKit(options = {}) {
|
|
65
66
|
const disabled = options.disable ? new Set(options.disable) : /* @__PURE__ */ new Set();
|
|
66
67
|
const extensions = [];
|
|
68
|
+
let internalClipboardToken;
|
|
69
|
+
const getInternalClipboardToken = () => {
|
|
70
|
+
internalClipboardToken ??= globalThis.crypto.randomUUID();
|
|
71
|
+
return internalClipboardToken;
|
|
72
|
+
};
|
|
67
73
|
function add(name, ext) {
|
|
68
74
|
if (!disabled.has(name)) extensions.push(ext);
|
|
69
75
|
}
|
|
@@ -105,12 +111,13 @@ function createStarterKit(options = {}) {
|
|
|
105
111
|
add("insertion", InsertionExtension());
|
|
106
112
|
add("deletion", DeletionExtension());
|
|
107
113
|
add("runPropertyChange", RunPropertyChangeExtension());
|
|
114
|
+
add("bookmarkBoundary", BookmarkBoundaryExtension({ getInternalClipboardToken }));
|
|
108
115
|
add("hardBreak", HardBreakExtension());
|
|
109
116
|
add("tab", TabExtension());
|
|
110
117
|
add("symbol", SymbolExtension());
|
|
111
118
|
add("image", ImageExtension());
|
|
112
119
|
add("textBox", TextBoxExtension());
|
|
113
|
-
add("textBoxAnchor", TextBoxAnchorExtension());
|
|
120
|
+
add("textBoxAnchor", TextBoxAnchorExtension({ getInternalClipboardToken }));
|
|
114
121
|
add("shape", ShapeExtension());
|
|
115
122
|
add("imageDrag", ImageDragExtension());
|
|
116
123
|
add("imagePaste", ImagePasteExtension());
|
|
@@ -120,11 +127,12 @@ function createStarterKit(options = {}) {
|
|
|
120
127
|
add("pageBreak", PageBreakExtension());
|
|
121
128
|
add("renderedPageBreak", RenderedPageBreakExtension());
|
|
122
129
|
add("field", FieldExtension());
|
|
130
|
+
add("field", StructuredFieldExtension({ getInternalClipboardToken }));
|
|
123
131
|
add("sdt", SdtExtension());
|
|
124
132
|
add("blockSdt", BlockSdtExtension());
|
|
125
133
|
add("math", MathExtension());
|
|
126
134
|
if (!disabled.has("table")) extensions.push(...createTableExtensions());
|
|
127
|
-
add("pasteCleanup", PasteCleanupExtension());
|
|
135
|
+
add("pasteCleanup", PasteCleanupExtension({ getInternalClipboardToken }));
|
|
128
136
|
add("pasteStyleInliner", PasteStyleInlinerExtension());
|
|
129
137
|
add("list", ListExtension());
|
|
130
138
|
add("baseKeymap", BaseKeymapExtension());
|
|
@@ -9,12 +9,16 @@ const autoBidiDetectionKey = new PluginKey("autoBidiDetection");
|
|
|
9
9
|
const paragraphDirectionalText = (node) => {
|
|
10
10
|
let text = "";
|
|
11
11
|
node.descendants((child) => {
|
|
12
|
+
if (child.marks.some((mark) => mark.type.name === "deletion")) return false;
|
|
12
13
|
if (child.isText) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const display = child.attrs["displayText"];
|
|
16
|
-
if (typeof display === "string") text += display;
|
|
14
|
+
text += child.text ?? "";
|
|
15
|
+
return false;
|
|
17
16
|
}
|
|
17
|
+
if (child.isLeaf && child.textContent) {
|
|
18
|
+
text += child.textContent;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return true;
|
|
18
22
|
});
|
|
19
23
|
return text;
|
|
20
24
|
};
|
|
@@ -14,6 +14,9 @@ import { Extension } from "../types.js";
|
|
|
14
14
|
* 2. `Mod-Alt-v` — "paste without formatting", inserting clipboard text with
|
|
15
15
|
* the source formatting stripped (see {@link pasteWithoutFormatting}).
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
type PasteCleanupOptions = {
|
|
18
|
+
getInternalClipboardToken?: () => string;
|
|
19
|
+
};
|
|
20
|
+
declare const PasteCleanupExtension: (options?: Partial<PasteCleanupOptions> | undefined) => Extension;
|
|
18
21
|
//#endregion
|
|
19
22
|
export { PasteCleanupExtension };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { pasteWithoutFormatting } from "../../commands/pastePlainText.js";
|
|
2
2
|
import { createExtension } from "../create.js";
|
|
3
3
|
import { Priority } from "../types.js";
|
|
4
|
-
import { cleanPastedHtml } from "./pasteCleanup.js";
|
|
4
|
+
import { cleanPastedHtml, removeUnpairedBookmarkBoundaries } from "./pasteCleanup.js";
|
|
5
5
|
import { Plugin } from "prosemirror-state";
|
|
6
6
|
//#region src/prosemirror/extensions/features/PasteCleanupExtension.ts
|
|
7
7
|
/**
|
|
@@ -21,11 +21,15 @@ import { Plugin } from "prosemirror-state";
|
|
|
21
21
|
const PasteCleanupExtension = createExtension({
|
|
22
22
|
name: "pasteCleanup",
|
|
23
23
|
priority: Priority.Highest,
|
|
24
|
-
onSchemaReady() {
|
|
24
|
+
onSchemaReady(_context, options) {
|
|
25
25
|
return {
|
|
26
|
-
plugins: [new Plugin({ props: {
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
plugins: [new Plugin({ props: {
|
|
27
|
+
transformPastedHTML(html) {
|
|
28
|
+
const internalClipboardToken = options.getInternalClipboardToken?.();
|
|
29
|
+
return cleanPastedHtml(html, { ...internalClipboardToken ? { internalClipboardToken } : {} });
|
|
30
|
+
},
|
|
31
|
+
transformPasted: removeUnpairedBookmarkBoundaries
|
|
32
|
+
} })],
|
|
29
33
|
keyboardShortcuts: { "Mod-Alt-v": pasteWithoutFormatting }
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -1,31 +1,18 @@
|
|
|
1
|
+
import { Slice } from "prosemirror-model";
|
|
1
2
|
//#region src/prosemirror/extensions/features/pasteCleanup.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Office / web paste cleanup
|
|
4
|
-
*
|
|
5
|
-
* Content pasted from word processors and web pages arrives wrapped in a large
|
|
6
|
-
* amount of producer-specific cruft: conditional comments, XML processing
|
|
7
|
-
* instructions, namespaced markup (`<o:p>`, `<w:sdt>`, smart tags), `mso-*`
|
|
8
|
-
* style declarations, and empty spans. None of it maps to the editor schema,
|
|
9
|
-
* and some of it confuses the browser HTML parser that ProseMirror's clipboard
|
|
10
|
-
* pipeline relies on. Producer class names (`MsoNormal`, ...) are deliberately
|
|
11
|
-
* kept so the downstream style inliner can match `<style>` rules against them.
|
|
12
|
-
*
|
|
13
|
-
* `cleanPastedHtml` normalizes the raw clipboard HTML string into something the
|
|
14
|
-
* schema's `parseDOM` rules can read cleanly. It is a pure string transform so
|
|
15
|
-
* it runs in the editor and in tests without a DOM, and it is deliberately
|
|
16
|
-
* conservative: it strips producer metadata but never rewrites visible text
|
|
17
|
-
* (curly quotes, non-Latin scripts, and whitespace between words are left
|
|
18
|
-
* untouched, since this editor targets an international, typography-sensitive
|
|
19
|
-
* audience).
|
|
20
|
-
*/
|
|
21
3
|
/**
|
|
22
4
|
* Strip Office/web producer cruft from a raw clipboard HTML string.
|
|
23
5
|
*
|
|
24
|
-
* Best-effort and non-throwing: any unexpected failure returns
|
|
25
|
-
*
|
|
6
|
+
* Best-effort and non-throwing: any unexpected failure returns empty markup so
|
|
7
|
+
* reconstruction capabilities and other untrusted attributes fail closed.
|
|
26
8
|
* `<style>` blocks are intentionally left in place for the downstream style
|
|
27
9
|
* inliner, which resolves class-based CSS before the schema parser runs.
|
|
28
10
|
*/
|
|
29
|
-
|
|
11
|
+
type CleanPastedHtmlOptions = {
|
|
12
|
+
internalClipboardToken?: string;
|
|
13
|
+
};
|
|
14
|
+
declare function cleanPastedHtml(html: string, options?: CleanPastedHtmlOptions): string;
|
|
15
|
+
/** Remove incomplete or duplicate bookmark pairs at copied slice edges. */
|
|
16
|
+
declare function removeUnpairedBookmarkBoundaries(slice: Slice): Slice;
|
|
30
17
|
//#endregion
|
|
31
|
-
export { cleanPastedHtml };
|
|
18
|
+
export { cleanPastedHtml, removeUnpairedBookmarkBoundaries };
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { stripXmlDeclarations } from "../../../utils/stripXmlDeclarations.js";
|
|
2
|
+
import { readBookmarkBoundaryAttrs } from "../../bookmarkBoundaryAttrs.js";
|
|
3
|
+
import { Fragment, Slice } from "prosemirror-model";
|
|
2
4
|
//#region src/prosemirror/extensions/features/pasteCleanup.ts
|
|
3
5
|
/**
|
|
4
6
|
* Office / web paste cleanup
|
|
@@ -117,56 +119,109 @@ const STRAY_XML_TAG = new RegExp(`<\\/?xml${TAG_TAIL}>`, "gi");
|
|
|
117
119
|
const NOISE_TAG = new RegExp(`<\\/?(?:font|meta|link)${TAG_TAIL}>`, "gi");
|
|
118
120
|
const EMPTY_SPAN = new RegExp(`<span(?:\\s${TAG_TAIL})?><\\/span>`, "gi");
|
|
119
121
|
const MAX_EMPTY_SPAN_PASSES = 5;
|
|
120
|
-
const
|
|
122
|
+
const INTERNAL_CLIPBOARD_ATTR_PATTERN = /\s+data-docx-internal-clipboard(?:="([^"]*)"|='([^']*)'|=([^\s>]+))?/gi;
|
|
121
123
|
const TEXTBOX_ANCHOR_ATTR = /\s+data-docx-textbox-anchor(?:="[^"]*"|='[^']*')?/gi;
|
|
124
|
+
const BOOKMARK_BOUNDARY_ATTR = /\s+data-docx-bookmark-(?:boundary|id|name|col-first|col-last)(?:="[^"]*"|='[^']*'|=[^\s>]+)?/gi;
|
|
125
|
+
const STRUCTURED_FIELD_ATTR = /\s+data-field-structured(?:="[^"]*"|='[^']*'|=[^\s>]+)?/gi;
|
|
122
126
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* keeps working; anything else has the marker stripped defensively.
|
|
127
|
+
* Treat reconstruction attributes as internal only when the HTML carries the
|
|
128
|
+
* unguessable capability created with this editor schema. `data-pm-slice`
|
|
129
|
+
* cannot establish trust because arbitrary external HTML can forge it.
|
|
127
130
|
*/
|
|
128
|
-
function
|
|
129
|
-
if (
|
|
131
|
+
function hasInternalClipboardCapability(html, token) {
|
|
132
|
+
if (!token) return false;
|
|
133
|
+
INTERNAL_CLIPBOARD_ATTR_PATTERN.lastIndex = 0;
|
|
134
|
+
for (const match of html.matchAll(INTERNAL_CLIPBOARD_ATTR_PATTERN)) if ((match[1] ?? match[2] ?? match[3]) === token) return true;
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
function stripForeignTextBoxAnchors(html, internal) {
|
|
138
|
+
if (internal) return html;
|
|
130
139
|
return html.replace(TEXTBOX_ANCHOR_ATTR, "");
|
|
131
140
|
}
|
|
141
|
+
function stripForeignBookmarkBoundaries(html, internal) {
|
|
142
|
+
if (internal) return html;
|
|
143
|
+
return html.replace(BOOKMARK_BOUNDARY_ATTR, "");
|
|
144
|
+
}
|
|
145
|
+
function stripForeignStructuredFields(html, internal) {
|
|
146
|
+
if (internal) return html;
|
|
147
|
+
return html.replace(STRUCTURED_FIELD_ATTR, "");
|
|
148
|
+
}
|
|
132
149
|
/**
|
|
133
150
|
* Remove empty spans left behind after stripping `mso-*` styles. Only truly
|
|
134
151
|
* empty spans are removed (whitespace-only spans are kept so word-separating
|
|
135
152
|
* spacer runs never collapse two words together). Repeated a bounded number of
|
|
136
153
|
* times to unwrap nested empties (`<span><span></span></span>`).
|
|
137
154
|
*/
|
|
138
|
-
function stripEmptySpans(html) {
|
|
155
|
+
function stripEmptySpans(html, preserveInternalAtoms) {
|
|
139
156
|
let current = html;
|
|
140
157
|
for (let pass = 0; pass < MAX_EMPTY_SPAN_PASSES; pass++) {
|
|
141
|
-
const next = current.replace(EMPTY_SPAN,
|
|
158
|
+
const next = current.replace(EMPTY_SPAN, (span) => {
|
|
159
|
+
if (preserveInternalAtoms && /\bdata-docx-(?:bookmark-boundary|textbox-anchor)\s*=/i.test(span)) return span;
|
|
160
|
+
return "";
|
|
161
|
+
});
|
|
142
162
|
if (next === current) break;
|
|
143
163
|
current = next;
|
|
144
164
|
}
|
|
145
165
|
return current;
|
|
146
166
|
}
|
|
147
|
-
|
|
148
|
-
* Strip Office/web producer cruft from a raw clipboard HTML string.
|
|
149
|
-
*
|
|
150
|
-
* Best-effort and non-throwing: any unexpected failure returns the original
|
|
151
|
-
* markup so a paste degrades to the browser default rather than losing content.
|
|
152
|
-
* `<style>` blocks are intentionally left in place for the downstream style
|
|
153
|
-
* inliner, which resolves class-based CSS before the schema parser runs.
|
|
154
|
-
*/
|
|
155
|
-
function cleanPastedHtml(html) {
|
|
167
|
+
function cleanPastedHtml(html, options = {}) {
|
|
156
168
|
if (!html) return html;
|
|
157
169
|
try {
|
|
170
|
+
const internal = hasInternalClipboardCapability(html, options.internalClipboardToken);
|
|
158
171
|
let cleaned = stripHtmlComments(html);
|
|
159
172
|
cleaned = stripXmlDeclarations(cleaned);
|
|
160
173
|
cleaned = cleaned.replace(NAMESPACED_TAG, "");
|
|
161
174
|
cleaned = cleaned.replace(STRAY_XML_TAG, "");
|
|
162
175
|
cleaned = cleaned.replace(NOISE_TAG, "");
|
|
163
176
|
cleaned = stripMsoStyles(cleaned);
|
|
164
|
-
cleaned = stripEmptySpans(cleaned);
|
|
165
|
-
cleaned = stripForeignTextBoxAnchors(cleaned,
|
|
177
|
+
cleaned = stripEmptySpans(cleaned, internal);
|
|
178
|
+
cleaned = stripForeignTextBoxAnchors(cleaned, internal);
|
|
179
|
+
cleaned = stripForeignBookmarkBoundaries(cleaned, internal);
|
|
180
|
+
cleaned = stripForeignStructuredFields(cleaned, internal);
|
|
181
|
+
cleaned = cleaned.replace(INTERNAL_CLIPBOARD_ATTR_PATTERN, "");
|
|
166
182
|
return cleaned.trim();
|
|
167
183
|
} catch {
|
|
168
|
-
return
|
|
184
|
+
return "";
|
|
169
185
|
}
|
|
170
186
|
}
|
|
187
|
+
/** Remove incomplete or duplicate bookmark pairs at copied slice edges. */
|
|
188
|
+
function removeUnpairedBookmarkBoundaries(slice) {
|
|
189
|
+
const counts = /* @__PURE__ */ new Map();
|
|
190
|
+
let boundaryIndex = 0;
|
|
191
|
+
slice.content.descendants((node) => {
|
|
192
|
+
if (node.type.name !== "bookmarkBoundary") return true;
|
|
193
|
+
const result = readBookmarkBoundaryAttrs(node);
|
|
194
|
+
if (!result.ok) return false;
|
|
195
|
+
const attrs = result.value;
|
|
196
|
+
const count = counts.get(attrs.id) ?? {
|
|
197
|
+
starts: 0,
|
|
198
|
+
ends: 0
|
|
199
|
+
};
|
|
200
|
+
if (attrs.type === "start") {
|
|
201
|
+
count.starts += 1;
|
|
202
|
+
count.firstStart ??= boundaryIndex;
|
|
203
|
+
} else {
|
|
204
|
+
count.ends += 1;
|
|
205
|
+
count.firstEnd ??= boundaryIndex;
|
|
206
|
+
}
|
|
207
|
+
boundaryIndex += 1;
|
|
208
|
+
counts.set(attrs.id, count);
|
|
209
|
+
return false;
|
|
210
|
+
});
|
|
211
|
+
const pairedIds = new Set([...counts].flatMap(([id, count]) => count.starts === 1 && count.ends === 1 && count.firstStart !== void 0 && count.firstEnd !== void 0 && count.firstStart < count.firstEnd ? [id] : []));
|
|
212
|
+
const filterFragment = (fragment) => {
|
|
213
|
+
const children = [];
|
|
214
|
+
fragment.forEach((node) => {
|
|
215
|
+
if (node.type.name === "bookmarkBoundary") {
|
|
216
|
+
const result = readBookmarkBoundaryAttrs(node);
|
|
217
|
+
if (result.ok && pairedIds.has(result.value.id)) children.push(node);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
children.push(node.childCount === 0 ? node : node.copy(filterFragment(node.content)));
|
|
221
|
+
});
|
|
222
|
+
return Fragment.fromArray(children);
|
|
223
|
+
};
|
|
224
|
+
return new Slice(filterFragment(slice.content), slice.openStart, slice.openEnd);
|
|
225
|
+
}
|
|
171
226
|
//#endregion
|
|
172
|
-
export { cleanPastedHtml };
|
|
227
|
+
export { cleanPastedHtml, removeUnpairedBookmarkBoundaries };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NodeExtension } from "../types.js";
|
|
2
|
+
//#region src/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts
|
|
3
|
+
/** Zero-width bookmark boundary that preserves its position through ProseMirror edits. */
|
|
4
|
+
type BookmarkBoundaryOptions = {
|
|
5
|
+
getInternalClipboardToken?: () => string;
|
|
6
|
+
};
|
|
7
|
+
declare const BookmarkBoundaryExtension: (options?: Partial<BookmarkBoundaryOptions> | undefined) => NodeExtension;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { BookmarkBoundaryExtension };
|