@stll/folio-core 0.28.0 → 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/docx/styleParser.js +15 -14
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +135 -118
- package/dist/layout-engine/footnoteColumnReflow.d.ts +15 -0
- package/dist/layout-engine/footnoteColumnReflow.js +74 -0
- package/dist/layout-engine/index.d.ts +3 -5
- package/dist/layout-engine/index.js +36 -15
- package/dist/layout-engine/measure/cache.d.ts +1 -1
- package/dist/layout-engine/measure/cache.js +11 -2
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +3 -1
- package/dist/layout-engine/measure/listMarkerWidth.js +6 -4
- package/dist/layout-engine/paginator.d.ts +6 -0
- package/dist/layout-engine/paginator.js +30 -11
- package/dist/layout-engine/types.d.ts +11 -6
- package/dist/markdown/renderParagraph.js +7 -2
- package/dist/prosemirror/attrs/index.js +5 -3
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
- package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
- package/dist/prosemirror/commands/formatPainter.js +45 -1
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +224 -91
- package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -1
- package/dist/prosemirror/conversion/toProseDoc.js +325 -94
- 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/marks/RunFormattingOverrideExtension.js +22 -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/marks.d.ts +3 -1
- package/dist/prosemirror/schema/nodes.d.ts +13 -1
- package/dist/prosemirror/styles/styleResolver.d.ts +0 -1
- package/dist/prosemirror/styles/styleResolver.js +22 -29
- package/dist/prosemirror/styles/styleToggleCascade.d.ts +37 -0
- package/dist/prosemirror/styles/styleToggleCascade.js +52 -0
- package/dist/prosemirror/validation.js +93 -0
- package/dist/utils/textFormattingMerge.d.ts +9 -1
- package/dist/utils/textFormattingMerge.js +32 -1
- package/package.json +1 -1
|
@@ -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 };
|
|
@@ -4,38 +4,38 @@ import { createMarkExtension } from "../create.js";
|
|
|
4
4
|
function buildRunFormattingOverrideAttrs(formatting) {
|
|
5
5
|
if (!formatting) return;
|
|
6
6
|
const attrs = {};
|
|
7
|
-
if (formatting.bold
|
|
8
|
-
if (formatting.italic
|
|
7
|
+
if (formatting.bold !== void 0) attrs.bold = formatting.bold;
|
|
8
|
+
if (formatting.italic !== void 0) attrs.italic = formatting.italic;
|
|
9
9
|
if (formatting.underline?.style === "none") attrs.underline = "none";
|
|
10
|
-
if (formatting.strike
|
|
10
|
+
if (formatting.strike !== void 0) attrs.strike = formatting.strike;
|
|
11
11
|
if (formatting.doubleStrike === false) attrs.doubleStrike = false;
|
|
12
|
-
if (formatting.allCaps
|
|
13
|
-
if (formatting.smallCaps
|
|
14
|
-
if (formatting.hidden
|
|
15
|
-
if (formatting.emboss
|
|
16
|
-
if (formatting.imprint
|
|
17
|
-
if (formatting.shadow
|
|
18
|
-
if (formatting.outline
|
|
12
|
+
if (formatting.allCaps !== void 0) attrs.allCaps = formatting.allCaps;
|
|
13
|
+
if (formatting.smallCaps !== void 0) attrs.smallCaps = formatting.smallCaps;
|
|
14
|
+
if (formatting.hidden !== void 0) attrs.hidden = formatting.hidden;
|
|
15
|
+
if (formatting.emboss !== void 0) attrs.emboss = formatting.emboss;
|
|
16
|
+
if (formatting.imprint !== void 0) attrs.imprint = formatting.imprint;
|
|
17
|
+
if (formatting.shadow !== void 0) attrs.shadow = formatting.shadow;
|
|
18
|
+
if (formatting.outline !== void 0) attrs.outline = formatting.outline;
|
|
19
19
|
if (formatting.rtl === false) attrs.rtl = false;
|
|
20
|
-
if (formatting.boldCs !== void 0
|
|
21
|
-
if (formatting.italicCs !== void 0
|
|
20
|
+
if (formatting.boldCs !== void 0) attrs.boldCs = formatting.boldCs;
|
|
21
|
+
if (formatting.italicCs !== void 0) attrs.italicCs = formatting.italicCs;
|
|
22
22
|
if (formatting.fontSizeCs !== void 0 && formatting.fontSizeCs !== formatting.fontSize) attrs.fontSizeCs = formatting.fontSizeCs;
|
|
23
23
|
if (formatting.cs !== void 0) attrs.cs = formatting.cs;
|
|
24
24
|
return Object.keys(attrs).length > 0 ? attrs : void 0;
|
|
25
25
|
}
|
|
26
26
|
function applyRunFormattingOverrideAttrs(formatting, attrs) {
|
|
27
|
-
if (attrs.bold
|
|
28
|
-
if (attrs.italic
|
|
27
|
+
if (attrs.bold !== void 0) formatting.bold = attrs.bold;
|
|
28
|
+
if (attrs.italic !== void 0) formatting.italic = attrs.italic;
|
|
29
29
|
if (attrs.underline === "none") formatting.underline = { style: "none" };
|
|
30
|
-
if (attrs.strike
|
|
30
|
+
if (attrs.strike !== void 0) formatting.strike = attrs.strike;
|
|
31
31
|
if (attrs.doubleStrike === false) formatting.doubleStrike = false;
|
|
32
|
-
if (attrs.allCaps
|
|
33
|
-
if (attrs.smallCaps
|
|
34
|
-
if (attrs.hidden
|
|
35
|
-
if (attrs.emboss
|
|
36
|
-
if (attrs.imprint
|
|
37
|
-
if (attrs.shadow
|
|
38
|
-
if (attrs.outline
|
|
32
|
+
if (attrs.allCaps !== void 0) formatting.allCaps = attrs.allCaps;
|
|
33
|
+
if (attrs.smallCaps !== void 0) formatting.smallCaps = attrs.smallCaps;
|
|
34
|
+
if (attrs.hidden !== void 0) formatting.hidden = attrs.hidden;
|
|
35
|
+
if (attrs.emboss !== void 0) formatting.emboss = attrs.emboss;
|
|
36
|
+
if (attrs.imprint !== void 0) formatting.imprint = attrs.imprint;
|
|
37
|
+
if (attrs.shadow !== void 0) formatting.shadow = attrs.shadow;
|
|
38
|
+
if (attrs.outline !== void 0) formatting.outline = attrs.outline;
|
|
39
39
|
if (attrs.rtl === false) formatting.rtl = false;
|
|
40
40
|
if (attrs.boldCs !== void 0) formatting.boldCs = attrs.boldCs;
|
|
41
41
|
if (attrs.italicCs !== void 0) formatting.italicCs = attrs.italicCs;
|
|
@@ -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 };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { expectBookmarkBoundaryAttrs } from "../../bookmarkBoundaryAttrs.js";
|
|
2
|
+
import { createNodeExtension } from "../create.js";
|
|
3
|
+
//#region src/prosemirror/extensions/nodes/BookmarkBoundaryExtension.ts
|
|
4
|
+
/** Zero-width bookmark boundary that preserves its position through ProseMirror edits. */
|
|
5
|
+
function readNonnegativeInteger(value) {
|
|
6
|
+
if (value === null || !/^(?:0|[1-9]\d*)$/.test(value)) return false;
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isSafeInteger(parsed) ? parsed : false;
|
|
9
|
+
}
|
|
10
|
+
function readOptionalColumn(dom, attribute) {
|
|
11
|
+
const value = dom.getAttribute(attribute);
|
|
12
|
+
return value === null ? void 0 : readNonnegativeInteger(value);
|
|
13
|
+
}
|
|
14
|
+
const BookmarkBoundaryExtension = createNodeExtension({
|
|
15
|
+
name: "bookmarkBoundary",
|
|
16
|
+
schemaNodeName: "bookmarkBoundary",
|
|
17
|
+
nodeSpec: (options) => ({
|
|
18
|
+
inline: true,
|
|
19
|
+
group: "inline",
|
|
20
|
+
marks: "_",
|
|
21
|
+
atom: true,
|
|
22
|
+
selectable: false,
|
|
23
|
+
attrs: {
|
|
24
|
+
type: {},
|
|
25
|
+
id: {},
|
|
26
|
+
name: { default: null },
|
|
27
|
+
colFirst: { default: null },
|
|
28
|
+
colLast: { default: null }
|
|
29
|
+
},
|
|
30
|
+
parseDOM: [{
|
|
31
|
+
tag: "span[data-docx-bookmark-boundary]",
|
|
32
|
+
getAttrs(dom) {
|
|
33
|
+
const type = dom.getAttribute("data-docx-bookmark-boundary");
|
|
34
|
+
const id = readNonnegativeInteger(dom.getAttribute("data-docx-bookmark-id"));
|
|
35
|
+
if (type !== "start" && type !== "end" || id === false) return false;
|
|
36
|
+
const name = dom.getAttribute("data-docx-bookmark-name");
|
|
37
|
+
if (type === "start" && !name) return false;
|
|
38
|
+
const colFirst = readOptionalColumn(dom, "data-docx-bookmark-col-first");
|
|
39
|
+
const colLast = readOptionalColumn(dom, "data-docx-bookmark-col-last");
|
|
40
|
+
if (colFirst === false || colLast === false) return false;
|
|
41
|
+
return {
|
|
42
|
+
type,
|
|
43
|
+
id,
|
|
44
|
+
...name ? { name } : {},
|
|
45
|
+
...colFirst !== void 0 ? { colFirst } : {},
|
|
46
|
+
...colLast !== void 0 ? { colLast } : {}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}],
|
|
50
|
+
toDOM(node) {
|
|
51
|
+
const attrs = expectBookmarkBoundaryAttrs(node);
|
|
52
|
+
return ["span", {
|
|
53
|
+
"data-docx-bookmark-boundary": attrs.type,
|
|
54
|
+
"data-docx-bookmark-id": String(attrs.id),
|
|
55
|
+
...attrs.type === "start" ? { "data-docx-bookmark-name": attrs.name } : {},
|
|
56
|
+
...attrs.type === "start" && attrs.colFirst !== void 0 ? { "data-docx-bookmark-col-first": String(attrs.colFirst) } : {},
|
|
57
|
+
...attrs.type === "start" && attrs.colLast !== void 0 ? { "data-docx-bookmark-col-last": String(attrs.colLast) } : {},
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
contenteditable: "false",
|
|
60
|
+
style: "display: none;",
|
|
61
|
+
...options.getInternalClipboardToken ? { "data-docx-internal-clipboard": options.getInternalClipboardToken() } : {}
|
|
62
|
+
}];
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
//#endregion
|
|
67
|
+
export { BookmarkBoundaryExtension };
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { NodeExtension } from "../types.js";
|
|
2
2
|
//#region src/prosemirror/extensions/nodes/FieldExtension.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Field
|
|
4
|
+
* Field extensions — atomic inline fields (PAGE, NUMPAGES, REF, etc.).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Ordinary fields are true ProseMirror leaves, so `textContent`, `textBetween`,
|
|
7
|
+
* and the default plain-text clipboard serializer can use `leafText`.
|
|
8
|
+
* Simple fields that must preserve hyperlink structure use a distinct atomic
|
|
9
|
+
* node with inline children; their visible text comes from those children.
|
|
8
10
|
*/
|
|
11
|
+
type StructuredFieldOptions = {
|
|
12
|
+
getInternalClipboardToken?: () => string;
|
|
13
|
+
};
|
|
9
14
|
declare const FieldExtension: (options?: Partial<Record<string, unknown>> | undefined) => NodeExtension;
|
|
15
|
+
declare const StructuredFieldExtension: (options?: Partial<StructuredFieldOptions> | undefined) => NodeExtension;
|
|
10
16
|
//#endregion
|
|
11
|
-
export { FieldExtension };
|
|
17
|
+
export { FieldExtension, StructuredFieldExtension };
|
|
@@ -2,12 +2,49 @@ import { parseFieldInstruction } from "../../../docx/fieldParser.js";
|
|
|
2
2
|
import { expectFieldAttrs } from "../../attrs/index.js";
|
|
3
3
|
import { createNodeExtension } from "../create.js";
|
|
4
4
|
//#region src/prosemirror/extensions/nodes/FieldExtension.ts
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const createFieldAttrs = () => ({
|
|
6
|
+
fieldType: { default: "UNKNOWN" },
|
|
7
|
+
instruction: { default: "" },
|
|
8
|
+
displayText: { default: "" },
|
|
9
|
+
_numberedRefBaseline: { default: void 0 },
|
|
10
|
+
fieldKind: { default: "simple" },
|
|
11
|
+
fldLock: { default: false },
|
|
12
|
+
dirty: { default: false }
|
|
13
|
+
});
|
|
14
|
+
const readFieldDomAttrs = (dom) => ({
|
|
15
|
+
fieldType: dom.dataset["fieldType"] ?? "UNKNOWN",
|
|
16
|
+
instruction: dom.dataset["instruction"] ?? "",
|
|
17
|
+
displayText: dom.textContent ?? "",
|
|
18
|
+
fieldKind: dom.dataset["fieldKind"] ?? "simple",
|
|
19
|
+
fldLock: dom.dataset["fldLock"] === "true",
|
|
20
|
+
dirty: dom.dataset["dirty"] === "true"
|
|
21
|
+
});
|
|
22
|
+
const getFieldDomAttrs = (node) => {
|
|
23
|
+
const { fieldType, instruction, fieldKind, fldLock, dirty } = expectFieldAttrs(node);
|
|
24
|
+
return {
|
|
25
|
+
class: `docx-field docx-field-${fieldType.toLowerCase()}`,
|
|
26
|
+
"data-field-type": fieldType,
|
|
27
|
+
"data-instruction": instruction,
|
|
28
|
+
"data-field-kind": fieldKind,
|
|
29
|
+
...fldLock ? { "data-fld-lock": "true" } : {},
|
|
30
|
+
...dirty ? { "data-dirty": "true" } : {},
|
|
31
|
+
style: "outline: 1px solid var(--doc-field-outline, rgba(200,200,200,0.4)); padding: 0 1px; border-radius: 2px;"
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const getFieldVisibleText = (node) => {
|
|
35
|
+
const { fieldType, instruction, displayText } = expectFieldAttrs(node);
|
|
36
|
+
if (displayText) return displayText;
|
|
37
|
+
switch (fieldType) {
|
|
38
|
+
case "PAGE": return "{page}";
|
|
39
|
+
case "NUMPAGES": return "{pages}";
|
|
40
|
+
case "DATE":
|
|
41
|
+
case "TIME":
|
|
42
|
+
case "CREATEDATE":
|
|
43
|
+
case "SAVEDATE": return (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
44
|
+
case "MERGEFIELD": return `«${getMergeFieldName(instruction)}»`;
|
|
45
|
+
default: return `{${fieldType}}`;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
11
48
|
const FieldExtension = createNodeExtension({
|
|
12
49
|
name: "field",
|
|
13
50
|
schemaNodeName: "field",
|
|
@@ -16,72 +53,53 @@ const FieldExtension = createNodeExtension({
|
|
|
16
53
|
group: "inline",
|
|
17
54
|
atom: true,
|
|
18
55
|
selectable: true,
|
|
19
|
-
attrs:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
attrs: createFieldAttrs(),
|
|
57
|
+
leafText: getFieldVisibleText,
|
|
58
|
+
parseDOM: [{
|
|
59
|
+
tag: "span.docx-field:not([data-field-structured])",
|
|
60
|
+
getAttrs: readFieldDomAttrs
|
|
61
|
+
}],
|
|
62
|
+
toDOM(node) {
|
|
63
|
+
return [
|
|
64
|
+
"span",
|
|
65
|
+
getFieldDomAttrs(node),
|
|
66
|
+
getFieldVisibleText(node)
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
const StructuredFieldExtension = createNodeExtension({
|
|
72
|
+
name: "structuredField",
|
|
73
|
+
schemaNodeName: "structuredField",
|
|
74
|
+
nodeSpec: (options) => ({
|
|
75
|
+
inline: true,
|
|
76
|
+
group: "inline",
|
|
77
|
+
content: "(text | bookmarkBoundary | tab | symbol | hardBreak | image | shape | renderedPageBreak | textBoxAnchor)+",
|
|
78
|
+
atom: true,
|
|
79
|
+
selectable: true,
|
|
80
|
+
attrs: createFieldAttrs(),
|
|
33
81
|
parseDOM: [{
|
|
34
|
-
tag: "span.docx-field",
|
|
82
|
+
tag: "span.docx-field[data-field-structured=\"true\"]",
|
|
35
83
|
getAttrs(dom) {
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
instruction: dom.dataset["instruction"] ?? "",
|
|
39
|
-
displayText: dom.textContent,
|
|
40
|
-
fieldKind: dom.dataset["fieldKind"] ?? "simple",
|
|
41
|
-
fldLock: dom.dataset["fldLock"] === "true",
|
|
42
|
-
dirty: dom.dataset["dirty"] === "true"
|
|
43
|
-
};
|
|
84
|
+
if (dom.dataset["fieldKind"] !== "simple" || !dom.querySelector("a[href]")) return false;
|
|
85
|
+
return readFieldDomAttrs(dom);
|
|
44
86
|
}
|
|
45
87
|
}],
|
|
46
88
|
toDOM(node) {
|
|
47
|
-
const { fieldType, instruction, displayText, fieldKind, fldLock, dirty } = expectFieldAttrs(node);
|
|
48
|
-
let text = displayText || "";
|
|
49
|
-
if (!text) switch (fieldType) {
|
|
50
|
-
case "PAGE":
|
|
51
|
-
text = "{page}";
|
|
52
|
-
break;
|
|
53
|
-
case "NUMPAGES":
|
|
54
|
-
text = "{pages}";
|
|
55
|
-
break;
|
|
56
|
-
case "DATE":
|
|
57
|
-
case "TIME":
|
|
58
|
-
case "CREATEDATE":
|
|
59
|
-
case "SAVEDATE":
|
|
60
|
-
text = (/* @__PURE__ */ new Date()).toLocaleDateString();
|
|
61
|
-
break;
|
|
62
|
-
case "MERGEFIELD":
|
|
63
|
-
text = `«${getMergeFieldName(instruction)}»`;
|
|
64
|
-
break;
|
|
65
|
-
default: text = `{${fieldType}}`;
|
|
66
|
-
}
|
|
67
89
|
return [
|
|
68
90
|
"span",
|
|
69
91
|
{
|
|
70
|
-
|
|
71
|
-
"data-field-
|
|
72
|
-
"data-
|
|
73
|
-
"data-field-kind": fieldKind,
|
|
74
|
-
...fldLock ? { "data-fld-lock": "true" } : {},
|
|
75
|
-
...dirty ? { "data-dirty": "true" } : {},
|
|
76
|
-
style: "outline: 1px solid var(--doc-field-outline, rgba(200,200,200,0.4)); padding: 0 1px; border-radius: 2px;"
|
|
92
|
+
...getFieldDomAttrs(node),
|
|
93
|
+
"data-field-structured": "true",
|
|
94
|
+
...options.getInternalClipboardToken ? { "data-docx-internal-clipboard": options.getInternalClipboardToken() } : {}
|
|
77
95
|
},
|
|
78
|
-
|
|
96
|
+
0
|
|
79
97
|
];
|
|
80
98
|
}
|
|
81
|
-
}
|
|
99
|
+
})
|
|
82
100
|
});
|
|
83
101
|
function getMergeFieldName(instruction) {
|
|
84
102
|
return parseFieldInstruction(instruction).argument ?? "";
|
|
85
103
|
}
|
|
86
104
|
//#endregion
|
|
87
|
-
export { FieldExtension };
|
|
105
|
+
export { FieldExtension, StructuredFieldExtension };
|
|
@@ -7,6 +7,9 @@ import { NodeExtension } from "../types.js";
|
|
|
7
7
|
* map its position as surrounding text is edited. The save conversion consumes
|
|
8
8
|
* it when the sibling text box is restored to the DOCX run stream.
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
type TextBoxAnchorOptions = {
|
|
11
|
+
getInternalClipboardToken?: () => string;
|
|
12
|
+
};
|
|
13
|
+
declare const TextBoxAnchorExtension: (options?: Partial<TextBoxAnchorOptions> | undefined) => NodeExtension;
|
|
11
14
|
//#endregion
|
|
12
15
|
export { TextBoxAnchorExtension };
|
|
@@ -11,7 +11,7 @@ import { createNodeExtension } from "../create.js";
|
|
|
11
11
|
const TextBoxAnchorExtension = createNodeExtension({
|
|
12
12
|
name: "textBoxAnchor",
|
|
13
13
|
schemaNodeName: "textBoxAnchor",
|
|
14
|
-
nodeSpec: {
|
|
14
|
+
nodeSpec: (options) => ({
|
|
15
15
|
inline: true,
|
|
16
16
|
group: "inline",
|
|
17
17
|
marks: "_",
|
|
@@ -32,10 +32,11 @@ const TextBoxAnchorExtension = createNodeExtension({
|
|
|
32
32
|
"data-docx-textbox-anchor": anchorId,
|
|
33
33
|
"aria-hidden": "true",
|
|
34
34
|
contenteditable: "false",
|
|
35
|
-
style: "display: inline-block; width: 0; height: 0; overflow: hidden; pointer-events: none;"
|
|
35
|
+
style: "display: inline-block; width: 0; height: 0; overflow: hidden; pointer-events: none;",
|
|
36
|
+
...options.getInternalClipboardToken ? { "data-docx-internal-clipboard": options.getInternalClipboardToken() } : {}
|
|
36
37
|
}];
|
|
37
38
|
}
|
|
38
|
-
}
|
|
39
|
+
})
|
|
39
40
|
});
|
|
40
41
|
//#endregion
|
|
41
42
|
export { TextBoxAnchorExtension };
|