@stll/folio-core 0.28.1 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/dist/controller/folioEditor.d.ts +28 -1
  2. package/dist/controller/folioEditor.js +37 -26
  3. package/dist/controller/fontReadiness.js +28 -24
  4. package/dist/controller/hiddenEditorManager.d.ts +2 -7
  5. package/dist/controller/layoutPipeline.js +33 -3
  6. package/dist/controller/noteEditorManager.d.ts +3 -6
  7. package/dist/docx/metafileSvg.d.ts +12 -0
  8. package/dist/docx/metafileSvg.js +462 -0
  9. package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
  10. package/dist/docx/parser.d.ts +2 -2
  11. package/dist/docx/parser.js +26 -0
  12. package/dist/docx/runParser.js +2 -1
  13. package/dist/docx/serializer/runSerializer.js +14 -2
  14. package/dist/docx/styleParser.js +2 -1
  15. package/dist/docx/textBoxParser.js +14 -0
  16. package/dist/docx/vmlImageParser.js +72 -152
  17. package/dist/docx/vmlPreview.d.ts +31 -0
  18. package/dist/docx/vmlPreview.js +535 -0
  19. package/dist/docx/wrapTypes.d.ts +2 -36
  20. package/dist/docx/wrapTypes.js +1 -39
  21. package/dist/fonts/fontAlternates.d.ts +8 -0
  22. package/dist/fonts/fontAlternates.js +16 -0
  23. package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
  24. package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
  25. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
  26. package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
  27. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
  28. package/dist/layout-bridge/convert/toFlowBlocks.js +136 -120
  29. package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
  30. package/dist/layout-bridge/engine/selectionRects.js +3 -1
  31. package/dist/layout-engine/index.js +65 -32
  32. package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
  33. package/dist/layout-engine/justifiedLineFit.js +6 -0
  34. package/dist/layout-engine/keep-together.js +15 -10
  35. package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
  36. package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
  37. package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
  38. package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
  39. package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
  40. package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
  41. package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
  42. package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
  43. package/dist/layout-engine/measure/measureBlocks.js +197 -30
  44. package/dist/layout-engine/measure/measureContainer.js +20 -24
  45. package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
  46. package/dist/layout-engine/measure/measureHelpers.js +14 -8
  47. package/dist/layout-engine/measure/measureParagraph.js +111 -37
  48. package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
  49. package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
  50. package/dist/layout-engine/measure/tabCalculator.js +144 -0
  51. package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
  52. package/dist/layout-engine/measure/tableCellFlow.js +45 -8
  53. package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
  54. package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
  55. package/dist/layout-engine/paginator.d.ts +26 -2
  56. package/dist/layout-engine/paginator.js +76 -14
  57. package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
  58. package/dist/layout-engine/paragraphSpacing.js +4 -3
  59. package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
  60. package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
  61. package/dist/layout-engine/section-breaks.d.ts +3 -1
  62. package/dist/layout-engine/section-breaks.js +6 -2
  63. package/dist/layout-engine/types.d.ts +18 -1
  64. package/dist/layout-engine/types.js +1 -1
  65. package/dist/layout-painter/imageLayout.d.ts +1 -1
  66. package/dist/layout-painter/renderPage.js +6 -1
  67. package/dist/layout-painter/renderParagraph.js +169 -82
  68. package/dist/layout-painter/renderTable.d.ts +1 -1
  69. package/dist/layout-painter/renderTable.js +18 -7
  70. package/dist/layout-painter/renderTextBox.d.ts +1 -1
  71. package/dist/layout-painter/renderTextBox.js +6 -1
  72. package/dist/markdown/renderParagraph.js +7 -2
  73. package/dist/model.d.ts +3 -3
  74. package/dist/model.js +2 -2
  75. package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
  76. package/dist/paged-layout/sectionBlockWidths.js +103 -14
  77. package/dist/prosemirror/attrs/index.js +14 -2
  78. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  79. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  80. package/dist/prosemirror/conversion/fromProseDoc.js +176 -54
  81. package/dist/prosemirror/conversion/toProseDoc.js +179 -64
  82. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  83. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  84. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  85. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  86. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  87. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  88. package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +9 -5
  89. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  90. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  91. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  92. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  93. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
  94. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  95. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  96. package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
  97. package/dist/prosemirror/listMarker.d.ts +58 -0
  98. package/dist/prosemirror/listMarker.js +185 -0
  99. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  100. package/dist/prosemirror/numberedRefFields.js +276 -0
  101. package/dist/prosemirror/paraText.js +56 -17
  102. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  103. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  104. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  105. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  106. package/dist/prosemirror/schema/index.d.ts +2 -2
  107. package/dist/prosemirror/schema/nodes.d.ts +17 -1
  108. package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
  109. package/dist/prosemirror/utils/tabCalculator.js +1 -140
  110. package/dist/prosemirror/validation.js +93 -0
  111. package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
  112. package/dist/types/documentEnumValues.d.ts +4 -1
  113. package/dist/types/documentEnumValues.js +12 -1
  114. package/dist/types/editor-story.d.ts +8 -0
  115. package/dist/types/editor-story.js +0 -0
  116. package/dist/types/remote-selection.d.ts +11 -0
  117. package/dist/types/remote-selection.js +0 -0
  118. package/dist/types/wrap.d.ts +36 -0
  119. package/dist/types/wrap.js +40 -0
  120. package/dist/utils/fontResolver.d.ts +1 -1
  121. package/dist/utils/fontResolver.js +42 -2
  122. package/dist/utils/formatToStyle.js +4 -2
  123. package/dist/utils/horizontalScale.d.ts +28 -0
  124. package/dist/utils/horizontalScale.js +42 -0
  125. package/package.json +2 -2
@@ -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 PM_SLICE_MARKER = /\bdata-pm-slice\s*=/i;
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
- * Remove the `data-docx-textbox-anchor` marker from HTML that did not come
124
- * from a ProseMirror clipboard slice (see {@link PM_SLICE_MARKER}). Internal
125
- * copy/paste of a real text box carries the marker HTML unmodified so it
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 stripForeignTextBoxAnchors(html, originalHtml) {
129
- if (PM_SLICE_MARKER.test(originalHtml)) return html;
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, html);
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 html;
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 };
@@ -1,3 +1,4 @@
1
+ import { getHorizontalScaleFactor, normalizeHorizontalScalePercent, parseHorizontalScalePercent } from "../../../utils/horizontalScale.js";
1
2
  import { formatPx, twipsToPixels } from "../../../utils/units.js";
2
3
  import { expectCharacterSpacingMarkAttrs } from "../../attrs/index.js";
3
4
  import { createMarkExtension } from "../create.js";
@@ -28,12 +29,13 @@ const CharacterSpacingExtension = createMarkExtension({
28
29
  getAttrs: (dom) => ({
29
30
  spacing: dom.dataset["spacing"] ? Number(dom.dataset["spacing"]) : null,
30
31
  position: dom.dataset["position"] ? Number(dom.dataset["position"]) : null,
31
- scale: dom.dataset["scale"] ? Number(dom.dataset["scale"]) : null,
32
+ scale: parseHorizontalScalePercent(dom.dataset["scale"]),
32
33
  kerning: dom.dataset["kerning"] ? Number(dom.dataset["kerning"]) : null
33
34
  })
34
35
  }],
35
36
  toDOM(mark) {
36
- const { spacing, position, scale, kerning } = expectCharacterSpacingMarkAttrs(mark);
37
+ const { spacing, position, scale: authoredScale, kerning } = expectCharacterSpacingMarkAttrs(mark);
38
+ const scale = normalizeHorizontalScalePercent(authoredScale);
37
39
  const styles = [];
38
40
  const dataAttrs = { class: "docx-char-spacing" };
39
41
  if (spacing !== void 0 && spacing !== 0) {
@@ -45,10 +47,12 @@ const CharacterSpacingExtension = createMarkExtension({
45
47
  styles.push(`vertical-align: ${formatPx(px)}`);
46
48
  dataAttrs["data-position"] = String(position);
47
49
  }
48
- if (scale !== void 0 && scale !== 100) {
49
- styles.push(`transform: scaleX(${scale / 100})`);
50
- styles.push("display: inline-block");
50
+ if (scale !== void 0) {
51
51
  dataAttrs["data-scale"] = String(scale);
52
+ if (scale !== 100) {
53
+ styles.push(`transform: scaleX(${getHorizontalScaleFactor(scale)})`);
54
+ styles.push("display: inline-block");
55
+ }
52
56
  }
53
57
  if (kerning !== void 0) dataAttrs["data-kerning"] = String(kerning);
54
58
  if (styles.length > 0) dataAttrs["style"] = styles.join("; ");
@@ -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 Extension — inline field node (PAGE, NUMPAGES, DATE, MERGEFIELD, etc.)
4
+ * Field extensionsatomic inline fields (PAGE, NUMPAGES, REF, etc.).
5
5
  *
6
- * Represents OOXML simple and complex fields as an atomic inline node.
7
- * At render time, field values are substituted (e.g., PAGE → actual page number).
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
- * Field Extension inline field node (PAGE, NUMPAGES, DATE, MERGEFIELD, etc.)
7
- *
8
- * Represents OOXML simple and complex fields as an atomic inline node.
9
- * At render time, field values are substituted (e.g., PAGE → actual page number).
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
- /** Field type: PAGE, NUMPAGES, DATE, MERGEFIELD, etc. */
21
- fieldType: { default: "UNKNOWN" },
22
- /** Full field instruction (e.g., "PAGE \\* MERGEFORMAT") */
23
- instruction: { default: "" },
24
- /** Display text (the current/cached field value) */
25
- displayText: { default: "" },
26
- /** Whether this is a simple or complex field */
27
- fieldKind: { default: "simple" },
28
- /** Field is locked */
29
- fldLock: { default: false },
30
- /** Field is dirty (needs update) */
31
- dirty: { default: false }
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
- fieldType: dom.dataset["fieldType"] ?? "UNKNOWN",
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
- class: `docx-field docx-field-${fieldType.toLowerCase()}`,
71
- "data-field-type": fieldType,
72
- "data-instruction": instruction,
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
- text
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 };
@@ -154,6 +154,7 @@ const tableSpec = {
154
154
  look: { default: null },
155
155
  borders: { default: null },
156
156
  _resolvedIndent: { default: null },
157
+ _resolvedJustification: { default: null },
157
158
  _resolvedBidi: { default: null },
158
159
  _originalFormatting: { default: null },
159
160
  tblPrChange: { default: null },
@@ -200,6 +201,7 @@ const tableRowSpec = {
200
201
  heightRule: { default: null },
201
202
  isHeader: { default: false },
202
203
  hidden: { default: false },
204
+ _resolvedJustification: { default: null },
203
205
  _originalFormatting: { default: null },
204
206
  trPrChange: { default: null },
205
207
  trIns: { default: null },
@@ -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
- declare const TextBoxAnchorExtension: (options?: Partial<Record<string, unknown>> | undefined) => NodeExtension;
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 };
@@ -1,4 +1,4 @@
1
- import { IMAGE_WRAP_TYPE_VALUES } from "../../../types/documentEnumValues.js";
1
+ import { IMAGE_WRAP_TYPE_VALUES, normalizeShapeTextAnchor } from "../../../types/documentEnumValues.js";
2
2
  import { expectTextBoxAttrs } from "../../attrs/index.js";
3
3
  import { createNodeExtension } from "../create.js";
4
4
  //#region src/prosemirror/extensions/nodes/TextBoxExtension.ts
@@ -21,6 +21,9 @@ function parseTextBoxAutoFit(raw) {
21
21
  function parseTextBoxTextWrap(raw) {
22
22
  if (raw === "none" || raw === "square") return raw;
23
23
  }
24
+ function parseTextBoxVerticalAlign(raw) {
25
+ return normalizeShapeTextAnchor(raw);
26
+ }
24
27
  const TextBoxExtension = createNodeExtension({
25
28
  name: "textBox",
26
29
  schemaNodeName: "textBox",
@@ -68,6 +71,7 @@ const TextBoxExtension = createNodeExtension({
68
71
  const wrapType = parseTextBoxWrapType(d["wrapType"]);
69
72
  const autoFit = parseTextBoxAutoFit(d["autoFit"]);
70
73
  const textWrap = parseTextBoxTextWrap(d["textWrap"]);
74
+ const verticalAlign = parseTextBoxVerticalAlign(d["verticalAlign"]);
71
75
  return {
72
76
  ...d["width"] ? { width: Number(d["width"]) } : {},
73
77
  ...d["height"] ? { height: Number(d["height"]) } : {},
@@ -82,7 +86,7 @@ const TextBoxExtension = createNodeExtension({
82
86
  ...d["marginBottom"] ? { marginBottom: Number(d["marginBottom"]) } : {},
83
87
  ...d["marginLeft"] ? { marginLeft: Number(d["marginLeft"]) } : {},
84
88
  ...d["marginRight"] ? { marginRight: Number(d["marginRight"]) } : {},
85
- ...d["verticalAlign"] ? { verticalAlign: d["verticalAlign"] } : {},
89
+ ...verticalAlign ? { verticalAlign } : {},
86
90
  ...d["displayMode"] ? { displayMode: d["displayMode"] } : {},
87
91
  ...d["cssFloat"] ? { cssFloat: d["cssFloat"] } : {},
88
92
  ...wrapType ? { wrapType } : {},
@@ -0,0 +1,58 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { ParagraphAttrs } from "./schema/nodes.js";
3
+ //#region src/prosemirror/listMarker.d.ts
4
+ type ListCounterState = {
5
+ counters: Map<number, number[]>;
6
+ abstractCounters: Map<number, number[]>;
7
+ seenLevels: Set<string>;
8
+ lastAdvancedNumId?: number;
9
+ };
10
+ type ListCounterStreams = {
11
+ final: ListCounterState;
12
+ original: ListCounterState;
13
+ };
14
+ type ListCounterStream = keyof ListCounterStreams;
15
+ type AdvancedListMarker = {
16
+ marker: string | null;
17
+ counterAttrs: ParagraphAttrs;
18
+ counterState: ListCounterState;
19
+ stream: ListCounterStream;
20
+ };
21
+ type VisibleListMarker = {
22
+ marker: string | null;
23
+ counterAttrs: ParagraphAttrs;
24
+ counterState: ListCounterState;
25
+ stream: ListCounterStream;
26
+ advances: readonly AdvancedListMarker[];
27
+ };
28
+ type ResolvedListComponent = {
29
+ level: number;
30
+ relativeStart: number;
31
+ start: number;
32
+ end: number;
33
+ };
34
+ type ResolvedListTemplate = {
35
+ value: string;
36
+ components: readonly ResolvedListComponent[];
37
+ };
38
+ declare const MAX_LIST_LEVEL = 8;
39
+ declare function createListCounterState(): ListCounterState;
40
+ declare function cloneListCounterState(state: ListCounterState): ListCounterState;
41
+ /**
42
+ * Advance the counter stream that paints this paragraph's marker. Normal
43
+ * paragraphs advance both final and original streams; inserted/deleted list
44
+ * paragraphs advance only the document view in which they exist.
45
+ */
46
+ declare function advanceVisibleListMarker(attrs: ParagraphAttrs, streams: ListCounterStreams): VisibleListMarker;
47
+ declare function formatCounter(value: number, format: document_d_exports.NumberFormat | undefined): string;
48
+ type ResolveListTemplateOptions = {
49
+ template: string;
50
+ counters: number[];
51
+ levelFormats?: document_d_exports.NumberFormat[] | undefined;
52
+ forceDecimal?: boolean | undefined;
53
+ };
54
+ declare function resolveListTemplate(options: ResolveListTemplateOptions): string;
55
+ declare function resolveListTemplateWithComponents({ template, counters, levelFormats, forceDecimal }: ResolveListTemplateOptions): ResolvedListTemplate;
56
+ declare function advanceListMarker(attrs: ParagraphAttrs, state: ListCounterState): string | null;
57
+ //#endregion
58
+ export { AdvancedListMarker, ListCounterState, ListCounterStream, ListCounterStreams, MAX_LIST_LEVEL, ResolvedListComponent, ResolvedListTemplate, VisibleListMarker, advanceListMarker, advanceVisibleListMarker, cloneListCounterState, createListCounterState, formatCounter, resolveListTemplate, resolveListTemplateWithComponents };