@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.
Files changed (33) hide show
  1. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
  2. package/dist/layout-bridge/convert/toFlowBlocks.js +61 -95
  3. package/dist/markdown/renderParagraph.js +7 -2
  4. package/dist/prosemirror/attrs/index.js +2 -1
  5. package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
  6. package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
  7. package/dist/prosemirror/conversion/fromProseDoc.js +170 -52
  8. package/dist/prosemirror/conversion/toProseDoc.js +169 -61
  9. package/dist/prosemirror/extensions/StarterKit.js +11 -3
  10. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
  11. package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
  12. package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
  13. package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
  14. package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
  15. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
  16. package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
  17. package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
  18. package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
  19. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
  20. package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
  21. package/dist/prosemirror/listMarker.d.ts +58 -0
  22. package/dist/prosemirror/listMarker.js +185 -0
  23. package/dist/prosemirror/numberedRefFields.d.ts +24 -0
  24. package/dist/prosemirror/numberedRefFields.js +276 -0
  25. package/dist/prosemirror/paraText.js +56 -17
  26. package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
  27. package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
  28. package/dist/prosemirror/plugins/pmTextScan.js +28 -7
  29. package/dist/prosemirror/plugins/templateDirectives.js +2 -2
  30. package/dist/prosemirror/schema/index.d.ts +2 -2
  31. package/dist/prosemirror/schema/nodes.d.ts +13 -1
  32. package/dist/prosemirror/validation.js +93 -0
  33. package/package.json +1 -1
@@ -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 };
@@ -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 };
@@ -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 };
@@ -0,0 +1,185 @@
1
+ import { convertBulletToUnicode } from "../docx/bulletMarkers.js";
2
+ import { formatOoxmlCounter } from "../docx/ooxmlCounterFormatter.js";
3
+ //#region src/prosemirror/listMarker.ts
4
+ const MAX_LIST_LEVEL = 8;
5
+ function createListCounterState() {
6
+ return {
7
+ counters: /* @__PURE__ */ new Map(),
8
+ abstractCounters: /* @__PURE__ */ new Map(),
9
+ seenLevels: /* @__PURE__ */ new Set()
10
+ };
11
+ }
12
+ function cloneListCounterState(state) {
13
+ const counters = /* @__PURE__ */ new Map();
14
+ for (const [numId, values] of state.counters) counters.set(numId, [...values]);
15
+ const abstractCounters = /* @__PURE__ */ new Map();
16
+ for (const [abstractNumId, values] of state.abstractCounters) abstractCounters.set(abstractNumId, [...values]);
17
+ return {
18
+ counters,
19
+ abstractCounters,
20
+ seenLevels: new Set(state.seenLevels),
21
+ ...state.lastAdvancedNumId !== void 0 ? { lastAdvancedNumId: state.lastAdvancedNumId } : {}
22
+ };
23
+ }
24
+ const isListNumPr = (value) => value !== void 0 && value !== null;
25
+ const sameListNumPr = (left, right) => left.numId === right.numId && left.ilvl === right.ilvl;
26
+ function previousListAttrs(attrs) {
27
+ const previous = (attrs._propertyChanges?.find(({ previousFormatting }) => {
28
+ const previousNumPr = previousFormatting?.numPr;
29
+ return isListNumPr(previousNumPr) && (!attrs.numPr || !sameListNumPr(previousNumPr, attrs.numPr));
30
+ }))?.previousFormatting;
31
+ if (!previous || !isListNumPr(previous.numPr)) return null;
32
+ return {
33
+ numPr: previous.numPr,
34
+ ...previous.listIsBullet !== void 0 ? { listIsBullet: previous.listIsBullet } : {},
35
+ ...previous.listIsLegal !== void 0 ? { listIsLegal: previous.listIsLegal } : {},
36
+ ...previous.listNumFmt !== void 0 ? { listNumFmt: previous.listNumFmt } : {},
37
+ ...previous.listMarker !== void 0 ? { listMarker: previous.listMarker } : {},
38
+ ...previous.listMarkerHidden !== void 0 ? { listMarkerHidden: previous.listMarkerHidden } : {},
39
+ ...previous.listLevelNumFmts !== void 0 ? { listLevelNumFmts: previous.listLevelNumFmts } : {},
40
+ ...previous.listLevelStarts !== void 0 ? { listLevelStarts: previous.listLevelStarts } : {},
41
+ ...previous.listAbstractNumId !== void 0 ? { listAbstractNumId: previous.listAbstractNumId } : {},
42
+ ...previous.listStartOverride !== void 0 ? { listStartOverride: previous.listStartOverride } : {}
43
+ };
44
+ }
45
+ /**
46
+ * Advance the counter stream that paints this paragraph's marker. Normal
47
+ * paragraphs advance both final and original streams; inserted/deleted list
48
+ * paragraphs advance only the document view in which they exist.
49
+ */
50
+ function advanceVisibleListMarker(attrs, streams) {
51
+ const advances = [];
52
+ const advance = (counterAttrs, stream) => {
53
+ const counterState = streams[stream];
54
+ const advanced = {
55
+ marker: advanceListMarker(counterAttrs, counterState),
56
+ counterAttrs,
57
+ counterState,
58
+ stream
59
+ };
60
+ advances.push(advanced);
61
+ return advanced;
62
+ };
63
+ const previous = previousListAttrs(attrs);
64
+ if (!attrs.numPr) return {
65
+ ...advance(previous ?? attrs, previous ? "original" : "final"),
66
+ advances
67
+ };
68
+ if (attrs.pPrMark?.kind === "del") return {
69
+ ...advance(attrs, "original"),
70
+ advances
71
+ };
72
+ const numberingWasAdded = attrs._propertyChanges?.some(({ previousFormatting }) => previousFormatting && Object.hasOwn(previousFormatting, "numPr") && previousFormatting.numPr == null);
73
+ const numberingChanged = previous?.numPr !== void 0 && !sameListNumPr(previous.numPr, attrs.numPr);
74
+ const visible = advance(attrs, "final");
75
+ if (attrs.pPrMark?.kind !== "ins" && !numberingWasAdded && !numberingChanged) advance(attrs, "original");
76
+ else if (numberingChanged && previous) advance(previous, "original");
77
+ return {
78
+ ...visible,
79
+ advances
80
+ };
81
+ }
82
+ function formatCounter(value, format) {
83
+ return formatOoxmlCounter(value, format);
84
+ }
85
+ function resolveListTemplate(options) {
86
+ return resolveListTemplateWithComponents(options).value;
87
+ }
88
+ function resolveListTemplateWithComponents({ template, counters, levelFormats, forceDecimal = false }) {
89
+ const components = [];
90
+ let value = "";
91
+ let sourceEnd = 0;
92
+ for (const match of template.matchAll(/%(?<digit>\d)(?<punct>[.):\]])?/gu)) {
93
+ const sourceStart = match.index;
94
+ const relativeStart = value.length;
95
+ value += template.slice(sourceEnd, sourceStart);
96
+ sourceEnd = sourceStart + match[0].length;
97
+ const index = Number.parseInt(match.groups?.["digit"] ?? "", 10) - 1;
98
+ if (index < 0) continue;
99
+ const counter = counters[index];
100
+ if (counter === void 0 || Number.isNaN(counter)) continue;
101
+ const formatted = formatCounter(counter, forceDecimal ? "decimal" : levelFormats?.[index]);
102
+ if (!formatted) continue;
103
+ const start = value.length;
104
+ value += formatted;
105
+ components.push({
106
+ level: index,
107
+ relativeStart,
108
+ start,
109
+ end: value.length
110
+ });
111
+ value += match.groups?.["punct"] ?? "";
112
+ }
113
+ value += template.slice(sourceEnd);
114
+ return {
115
+ value,
116
+ components
117
+ };
118
+ }
119
+ function getLastListCounters(state) {
120
+ return state.lastAdvancedNumId === void 0 ? void 0 : state.counters.get(state.lastAdvancedNumId);
121
+ }
122
+ function formatNumberedMarker(counters, level) {
123
+ const parts = [];
124
+ for (let index = 0; index <= level; index += 1) {
125
+ const value = counters[index] ?? 0;
126
+ if (!Number.isFinite(value) || value <= 0) break;
127
+ parts.push(value);
128
+ }
129
+ return parts.length === 0 ? "1." : `${parts.join(".")}.`;
130
+ }
131
+ function advanceListMarker(attrs, state) {
132
+ const level = attrs.numPr?.ilvl ?? 0;
133
+ if (!Number.isInteger(level) || level < 0 || level > 8) return null;
134
+ const numId = attrs.numPr?.numId;
135
+ if (numId === void 0 || numId === 0) {
136
+ if (attrs.listMarker?.includes("%") && !attrs.listIsBullet) {
137
+ const counters = getLastListCounters(state);
138
+ if (counters) return resolveListTemplate({
139
+ template: attrs.listMarker,
140
+ counters,
141
+ levelFormats: attrs.listLevelNumFmts,
142
+ forceDecimal: attrs.listIsLegal
143
+ });
144
+ }
145
+ return null;
146
+ }
147
+ if (attrs.listIsBullet) return convertBulletToUnicode(attrs.listMarker ?? "");
148
+ const counters = state.counters.get(numId) ?? Array.from({ length: 9 }, () => NaN);
149
+ const abstractNumId = attrs.listAbstractNumId;
150
+ if (level > 0) {
151
+ const latestAbstractCounters = abstractNumId === void 0 ? void 0 : state.abstractCounters.get(abstractNumId);
152
+ if (counters.slice(0, level).every((counter) => !Number.isFinite(counter))) for (let index = 0; index < level; index += 1) {
153
+ const latestCounter = latestAbstractCounters?.[index];
154
+ counters[index] = latestCounter !== void 0 && Number.isFinite(latestCounter) ? latestCounter : attrs.listLevelStarts?.[index] ?? 1;
155
+ }
156
+ }
157
+ const seenKey = `${numId}:${level}`;
158
+ if (!state.seenLevels.has(seenKey)) {
159
+ state.seenLevels.add(seenKey);
160
+ if (attrs.listStartOverride != null) counters[level] = attrs.listStartOverride - 1;
161
+ }
162
+ if (!Number.isFinite(counters[level])) counters[level] = (attrs.listLevelStarts?.[level] ?? 1) - 1;
163
+ counters[level] = (counters[level] ?? 0) + 1;
164
+ for (let index = level + 1; index < counters.length; index += 1) counters[index] = NaN;
165
+ const childAdvances = attrs.listImplicitChildLevelAdvances ?? 0;
166
+ if (childAdvances > 0 && level + 1 < counters.length) {
167
+ const childCounter = counters[level + 1];
168
+ counters[level + 1] = (childCounter === void 0 || !Number.isFinite(childCounter) ? 0 : childCounter) + childAdvances;
169
+ }
170
+ state.counters.set(numId, counters);
171
+ state.lastAdvancedNumId = numId;
172
+ if (abstractNumId !== void 0) state.abstractCounters.set(abstractNumId, [...counters]);
173
+ const levelFormats = attrs.listLevelNumFmts ?? (attrs.listNumFmt ? [attrs.listNumFmt] : void 0);
174
+ if (attrs.listMarker?.includes("%")) return resolveListTemplate({
175
+ template: attrs.listMarker,
176
+ counters,
177
+ levelFormats,
178
+ forceDecimal: attrs.listIsLegal
179
+ });
180
+ if (attrs.listMarker) return attrs.listMarker;
181
+ if ((levelFormats?.[level] ?? attrs.listNumFmt) === "none" || attrs.listMarker === "") return null;
182
+ return formatNumberedMarker(counters, level);
183
+ }
184
+ //#endregion
185
+ export { MAX_LIST_LEVEL, advanceListMarker, advanceVisibleListMarker, cloneListCounterState, createListCounterState, formatCounter, resolveListTemplate, resolveListTemplateWithComponents };
@@ -0,0 +1,24 @@
1
+ import { ListCounterState } from "./listMarker.js";
2
+ import { Node } from "prosemirror-model";
3
+ //#region src/prosemirror/numberedRefFields.d.ts
4
+ type NumberSwitch = "n" | "r" | "w";
5
+ type NumberedRefSpec = {
6
+ bookmark: string;
7
+ numberSwitch: NumberSwitch;
8
+ };
9
+ type NumberedRefResolutionOptions = {
10
+ /** Counter snapshot at the start of this body story. The resolver consumes it in place. */
11
+ listCounterState?: ListCounterState;
12
+ /** Counter snapshot for tracked deletions in the original document view. */
13
+ originalListCounterState?: ListCounterState;
14
+ };
15
+ declare function parseNumberedRefInstruction(instruction: string): NumberedRefSpec | null;
16
+ /** Stamp only proven numbered REF fields with clone-safe calibration state. */
17
+ declare function stampNumberedRefFieldBaselines(doc: Node): Node;
18
+ /**
19
+ * Resolve bounded, numbered REF fields against bookmarks in the ProseMirror body story.
20
+ * Unsupported or uncalibrated fields are absent from the returned map and retain their cache.
21
+ */
22
+ declare function resolveNumberedRefFields(doc: Node, options?: NumberedRefResolutionOptions): ReadonlyMap<Node, string>;
23
+ //#endregion
24
+ export { NumberedRefResolutionOptions, parseNumberedRefInstruction, resolveNumberedRefFields, stampNumberedRefFieldBaselines };