@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.
- package/dist/controller/folioEditor.d.ts +28 -1
- package/dist/controller/folioEditor.js +37 -26
- package/dist/controller/fontReadiness.js +28 -24
- package/dist/controller/hiddenEditorManager.d.ts +2 -7
- package/dist/controller/layoutPipeline.js +33 -3
- package/dist/controller/noteEditorManager.d.ts +3 -6
- package/dist/docx/metafileSvg.d.ts +12 -0
- package/dist/docx/metafileSvg.js +462 -0
- package/dist/docx/paragraphTextBoxEnrichment.js +55 -4
- package/dist/docx/parser.d.ts +2 -2
- package/dist/docx/parser.js +26 -0
- package/dist/docx/runParser.js +2 -1
- package/dist/docx/serializer/runSerializer.js +14 -2
- package/dist/docx/styleParser.js +2 -1
- package/dist/docx/textBoxParser.js +14 -0
- package/dist/docx/vmlImageParser.js +72 -152
- package/dist/docx/vmlPreview.d.ts +31 -0
- package/dist/docx/vmlPreview.js +535 -0
- package/dist/docx/wrapTypes.d.ts +2 -36
- package/dist/docx/wrapTypes.js +1 -39
- package/dist/fonts/fontAlternates.d.ts +8 -0
- package/dist/fonts/fontAlternates.js +16 -0
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +34 -3
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +5 -0
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +136 -120
- package/dist/layout-bridge/dom/noteStoryDom.d.ts +1 -1
- package/dist/layout-bridge/engine/selectionRects.js +3 -1
- package/dist/layout-engine/index.js +65 -32
- package/dist/layout-engine/justifiedLineFit.d.ts +7 -0
- package/dist/layout-engine/justifiedLineFit.js +6 -0
- package/dist/layout-engine/keep-together.js +15 -10
- package/dist/layout-engine/measure/complexScriptFormatting.d.ts +6 -1
- package/dist/layout-engine/measure/complexScriptFormatting.js +11 -2
- package/dist/layout-engine/measure/floatingTablePosition.d.ts +16 -1
- package/dist/layout-engine/measure/floatingTablePosition.js +29 -6
- package/dist/layout-engine/measure/lineBreakProvider.js +67 -8
- package/dist/layout-engine/measure/listMarkerWidth.d.ts +1 -0
- package/dist/layout-engine/measure/listMarkerWidth.js +23 -10
- package/dist/layout-engine/measure/measureBlocks.d.ts +15 -0
- package/dist/layout-engine/measure/measureBlocks.js +197 -30
- package/dist/layout-engine/measure/measureContainer.js +20 -24
- package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
- package/dist/layout-engine/measure/measureHelpers.js +14 -8
- package/dist/layout-engine/measure/measureParagraph.js +111 -37
- package/dist/layout-engine/measure/measureTypes.d.ts +3 -0
- package/dist/layout-engine/measure/tabCalculator.d.ts +123 -0
- package/dist/layout-engine/measure/tabCalculator.js +144 -0
- package/dist/layout-engine/measure/tableCellFlow.d.ts +21 -1
- package/dist/layout-engine/measure/tableCellFlow.js +45 -8
- package/dist/layout-engine/measure/tableInlinePlacement.d.ts +10 -2
- package/dist/layout-engine/measure/tableInlinePlacement.js +17 -15
- package/dist/layout-engine/paginator.d.ts +26 -2
- package/dist/layout-engine/paginator.js +76 -14
- package/dist/layout-engine/paragraphSpacing.d.ts +3 -1
- package/dist/layout-engine/paragraphSpacing.js +4 -3
- package/dist/layout-engine/renderedBreakReconciliation.d.ts +14 -3
- package/dist/layout-engine/renderedBreakReconciliation.js +45 -14
- package/dist/layout-engine/section-breaks.d.ts +3 -1
- package/dist/layout-engine/section-breaks.js +6 -2
- package/dist/layout-engine/types.d.ts +18 -1
- package/dist/layout-engine/types.js +1 -1
- package/dist/layout-painter/imageLayout.d.ts +1 -1
- package/dist/layout-painter/renderPage.js +6 -1
- package/dist/layout-painter/renderParagraph.js +169 -82
- package/dist/layout-painter/renderTable.d.ts +1 -1
- package/dist/layout-painter/renderTable.js +18 -7
- package/dist/layout-painter/renderTextBox.d.ts +1 -1
- package/dist/layout-painter/renderTextBox.js +6 -1
- package/dist/markdown/renderParagraph.js +7 -2
- package/dist/model.d.ts +3 -3
- package/dist/model.js +2 -2
- package/dist/paged-layout/sectionBlockWidths.d.ts +14 -0
- package/dist/paged-layout/sectionBlockWidths.js +103 -14
- package/dist/prosemirror/attrs/index.js +14 -2
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
- package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +176 -54
- package/dist/prosemirror/conversion/toProseDoc.js +179 -64
- 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/CharacterSpacingExtension.js +9 -5
- 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/TableExtension.js +2 -0
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +6 -2
- package/dist/prosemirror/listMarker.d.ts +58 -0
- package/dist/prosemirror/listMarker.js +185 -0
- package/dist/prosemirror/numberedRefFields.d.ts +24 -0
- package/dist/prosemirror/numberedRefFields.js +276 -0
- package/dist/prosemirror/paraText.js +56 -17
- package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
- package/dist/prosemirror/plugins/pmTextScan.js +28 -7
- package/dist/prosemirror/plugins/templateDirectives.js +2 -2
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/nodes.d.ts +17 -1
- package/dist/prosemirror/utils/tabCalculator.d.ts +2 -123
- package/dist/prosemirror/utils/tabCalculator.js +1 -140
- package/dist/prosemirror/validation.js +93 -0
- package/dist/render-dom/RemoteSelectionOverlay.d.ts +4 -4
- package/dist/types/documentEnumValues.d.ts +4 -1
- package/dist/types/documentEnumValues.js +12 -1
- package/dist/types/editor-story.d.ts +8 -0
- package/dist/types/editor-story.js +0 -0
- package/dist/types/remote-selection.d.ts +11 -0
- package/dist/types/remote-selection.js +0 -0
- package/dist/types/wrap.d.ts +36 -0
- package/dist/types/wrap.js +40 -0
- package/dist/utils/fontResolver.d.ts +1 -1
- package/dist/utils/fontResolver.js +42 -2
- package/dist/utils/formatToStyle.js +4 -2
- package/dist/utils/horizontalScale.d.ts +28 -0
- package/dist/utils/horizontalScale.js +42 -0
- package/package.json +2 -2
|
@@ -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 };
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { expectFieldAttrs, expectParagraphAttrs } from "./attrs/index.js";
|
|
2
|
+
import { expectBookmarkBoundaryAttrs } from "./bookmarkBoundaryAttrs.js";
|
|
3
|
+
import { advanceVisibleListMarker, createListCounterState, resolveListTemplateWithComponents } from "./listMarker.js";
|
|
4
|
+
import { Fragment } from "prosemirror-model";
|
|
5
|
+
//#region src/prosemirror/numberedRefFields.ts
|
|
6
|
+
const MAX_FIELD_INSTRUCTION_CHARS = 2048;
|
|
7
|
+
const MAX_BOOKMARK_NAME_CHARS = 256;
|
|
8
|
+
const MAX_NUMBERED_REF_FIELDS = 1024;
|
|
9
|
+
const MAX_BOOKMARK_TARGETS = 2048;
|
|
10
|
+
function tokenizeInstruction(instruction) {
|
|
11
|
+
const tokens = [];
|
|
12
|
+
let index = 0;
|
|
13
|
+
while (index < instruction.length) {
|
|
14
|
+
while (/\s/u.test(instruction[index] ?? "")) index += 1;
|
|
15
|
+
if (index >= instruction.length) break;
|
|
16
|
+
if (instruction[index] === "\"") {
|
|
17
|
+
const close = instruction.indexOf("\"", index + 1);
|
|
18
|
+
if (close === -1) return null;
|
|
19
|
+
tokens.push(instruction.slice(index + 1, close));
|
|
20
|
+
index = close + 1;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
let end = index;
|
|
24
|
+
while (end < instruction.length && !/\s/u.test(instruction[end] ?? "")) end += 1;
|
|
25
|
+
tokens.push(instruction.slice(index, end));
|
|
26
|
+
index = end;
|
|
27
|
+
}
|
|
28
|
+
return tokens;
|
|
29
|
+
}
|
|
30
|
+
function parseNumberedRefInstruction(instruction) {
|
|
31
|
+
if (instruction.length > MAX_FIELD_INSTRUCTION_CHARS) return null;
|
|
32
|
+
const tokens = tokenizeInstruction(instruction);
|
|
33
|
+
if (tokens === null || tokens.length < 3 || tokens.at(0)?.toUpperCase() !== "REF") return null;
|
|
34
|
+
const bookmark = tokens.at(1);
|
|
35
|
+
if (!bookmark || bookmark.length > MAX_BOOKMARK_NAME_CHARS || bookmark.startsWith("\\")) return null;
|
|
36
|
+
let numberSwitch = null;
|
|
37
|
+
for (let index = 2; index < tokens.length; index += 1) {
|
|
38
|
+
const token = tokens[index]?.toUpperCase();
|
|
39
|
+
if (token === "\\N" || token === "\\R" || token === "\\W") {
|
|
40
|
+
if (numberSwitch !== null) return null;
|
|
41
|
+
if (token === "\\N") numberSwitch = "n";
|
|
42
|
+
else if (token === "\\R") numberSwitch = "r";
|
|
43
|
+
else numberSwitch = "w";
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (token === "\\H") continue;
|
|
47
|
+
if (token === "\\*MERGEFORMAT") continue;
|
|
48
|
+
if (token === "\\*" && tokens[index + 1]?.toUpperCase() === "MERGEFORMAT") {
|
|
49
|
+
index += 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return numberSwitch === null ? null : {
|
|
55
|
+
bookmark,
|
|
56
|
+
numberSwitch
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function trimTrailingPeriod(value) {
|
|
60
|
+
return value.length > 1 && value.endsWith(".") ? value.slice(0, -1) : value;
|
|
61
|
+
}
|
|
62
|
+
function firstReferencedLevel(template) {
|
|
63
|
+
let first = null;
|
|
64
|
+
for (const match of template.matchAll(/%(?<level>[1-9])/gu)) {
|
|
65
|
+
const level = Number.parseInt(match.groups?.["level"] ?? "", 10) - 1;
|
|
66
|
+
if (Number.isNaN(level)) continue;
|
|
67
|
+
first = first === null ? level : Math.min(first, level);
|
|
68
|
+
}
|
|
69
|
+
return first;
|
|
70
|
+
}
|
|
71
|
+
function numberTargetForParagraph(node, contextStreams, streams) {
|
|
72
|
+
const resolution = advanceVisibleListMarker(expectParagraphAttrs(node), streams);
|
|
73
|
+
let visibleTarget = null;
|
|
74
|
+
for (const advanced of resolution.advances) {
|
|
75
|
+
const target = numberTargetForAdvancedMarker(advanced.counterAttrs, advanced.marker, advanced.counterState, contextStreams[advanced.stream]);
|
|
76
|
+
if (advanced.stream === resolution.stream && advanced.counterAttrs === resolution.counterAttrs) visibleTarget = target;
|
|
77
|
+
}
|
|
78
|
+
return visibleTarget;
|
|
79
|
+
}
|
|
80
|
+
function numberTargetForAdvancedMarker(attrs, resolvedMarker, state, contexts) {
|
|
81
|
+
const numId = attrs.numPr?.numId;
|
|
82
|
+
if (numId === void 0 || numId === 0 || attrs.listIsBullet || attrs.listMarkerHidden || attrs.listNumFmt === "none" || !resolvedMarker) return null;
|
|
83
|
+
const marker = attrs.listMarkerAllCaps ? resolvedMarker.toLocaleUpperCase() : resolvedMarker;
|
|
84
|
+
const level = attrs.numPr?.ilvl ?? 0;
|
|
85
|
+
if (!Number.isInteger(level) || level < 0 || level > 8) return null;
|
|
86
|
+
const counters = state.counters.get(numId)?.slice(0, level + 1);
|
|
87
|
+
const levelFormats = attrs.listLevelNumFmts ?? (attrs.listNumFmt ? [attrs.listNumFmt] : void 0);
|
|
88
|
+
let renderedMarker = {
|
|
89
|
+
value: marker,
|
|
90
|
+
components: []
|
|
91
|
+
};
|
|
92
|
+
if (attrs.listMarker?.includes("%") && counters?.length === level + 1) {
|
|
93
|
+
const rendered = resolveListTemplateWithComponents({
|
|
94
|
+
template: attrs.listMarker,
|
|
95
|
+
counters,
|
|
96
|
+
levelFormats,
|
|
97
|
+
forceDecimal: attrs.listIsLegal
|
|
98
|
+
});
|
|
99
|
+
if ((attrs.listMarkerAllCaps ? rendered.value.toLocaleUpperCase() : rendered.value) === marker) renderedMarker = {
|
|
100
|
+
value: marker,
|
|
101
|
+
components: rendered.components
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const abstractContexts = attrs.listAbstractNumId === void 0 ? void 0 : contexts.byAbstractNumId.get(attrs.listAbstractNumId);
|
|
105
|
+
const ownContexts = contexts.byNumId.get(numId) ?? [...abstractContexts ?? []];
|
|
106
|
+
const firstLevel = firstReferencedLevel(attrs.listMarker ?? "");
|
|
107
|
+
let renderedFull;
|
|
108
|
+
if (level === 0 || firstLevel === 0) renderedFull = renderedMarker;
|
|
109
|
+
else {
|
|
110
|
+
const parentLevel = (firstLevel ?? level) - 1;
|
|
111
|
+
const prefix = ownContexts[parentLevel] ?? abstractContexts?.[parentLevel];
|
|
112
|
+
renderedFull = prefix === void 0 ? null : {
|
|
113
|
+
value: `${prefix.value}${marker}`,
|
|
114
|
+
components: [...prefix.components, ...renderedMarker.components.map((component) => ({
|
|
115
|
+
level: component.level,
|
|
116
|
+
relativeStart: component.relativeStart + prefix.value.length,
|
|
117
|
+
start: component.start + prefix.value.length,
|
|
118
|
+
end: component.end + prefix.value.length
|
|
119
|
+
}))]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
ownContexts[level] = renderedFull ?? void 0;
|
|
123
|
+
ownContexts.splice(level + 1);
|
|
124
|
+
contexts.byNumId.set(numId, ownContexts);
|
|
125
|
+
if (attrs.listAbstractNumId !== void 0) contexts.byAbstractNumId.set(attrs.listAbstractNumId, [...ownContexts]);
|
|
126
|
+
const path = counters?.length === level + 1 && counters.every(Number.isFinite) && renderedFull !== null ? {
|
|
127
|
+
scheme: attrs.listAbstractNumId === void 0 ? {
|
|
128
|
+
type: "instance",
|
|
129
|
+
id: numId
|
|
130
|
+
} : {
|
|
131
|
+
type: "abstract",
|
|
132
|
+
id: attrs.listAbstractNumId
|
|
133
|
+
},
|
|
134
|
+
counters,
|
|
135
|
+
rendered: renderedFull
|
|
136
|
+
} : null;
|
|
137
|
+
return {
|
|
138
|
+
current: trimTrailingPeriod(marker),
|
|
139
|
+
full: renderedFull === null ? null : trimTrailingPeriod(renderedFull.value),
|
|
140
|
+
path
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function sameNumberingScheme(left, right) {
|
|
144
|
+
return left.type === right.type && left.id === right.id;
|
|
145
|
+
}
|
|
146
|
+
function relativeNumber(target, source) {
|
|
147
|
+
if (source === null || source.path === null || target.path === null) return target.full;
|
|
148
|
+
if (!sameNumberingScheme(source.path.scheme, target.path.scheme)) return target.full;
|
|
149
|
+
let sharedLevels = 0;
|
|
150
|
+
const comparedLevels = Math.min(source.path.counters.length, target.path.counters.length);
|
|
151
|
+
while (sharedLevels < comparedLevels && source.path.counters[sharedLevels] === target.path.counters[sharedLevels]) sharedLevels += 1;
|
|
152
|
+
if (sharedLevels === 0) return target.full;
|
|
153
|
+
if (sharedLevels >= target.path.counters.length) return null;
|
|
154
|
+
const firstRelativeComponent = target.path.rendered.components.find((component) => component.level >= sharedLevels);
|
|
155
|
+
if (firstRelativeComponent === void 0) return null;
|
|
156
|
+
const suffix = target.path.rendered.value.slice(firstRelativeComponent.relativeStart).replace(/^[\s,.:;/-]+/u, "");
|
|
157
|
+
return suffix.length === 0 ? null : trimTrailingPeriod(suffix);
|
|
158
|
+
}
|
|
159
|
+
function normalizeCachedResult(value) {
|
|
160
|
+
return value.replaceAll("\xA0", " ").replace(/\s+/gu, " ").trim();
|
|
161
|
+
}
|
|
162
|
+
function collectNumberedRefCandidates(doc, options) {
|
|
163
|
+
const fields = [];
|
|
164
|
+
doc.descendants((node) => {
|
|
165
|
+
if (node.type.name !== "field" && node.type.name !== "structuredField") return true;
|
|
166
|
+
if (fields.length >= MAX_NUMBERED_REF_FIELDS) return false;
|
|
167
|
+
const attrs = expectFieldAttrs(node);
|
|
168
|
+
if (attrs.fieldType !== "REF" || attrs.fldLock) return false;
|
|
169
|
+
const spec = parseNumberedRefInstruction(attrs.instruction);
|
|
170
|
+
if (spec !== null) fields.push({
|
|
171
|
+
node,
|
|
172
|
+
spec,
|
|
173
|
+
cached: attrs.displayText
|
|
174
|
+
});
|
|
175
|
+
return false;
|
|
176
|
+
});
|
|
177
|
+
if (fields.length === 0) return [];
|
|
178
|
+
const referencedBookmarks = new Set(fields.map(({ spec }) => spec.bookmark));
|
|
179
|
+
const fieldNodes = new Set(fields.map(({ node }) => node));
|
|
180
|
+
const sourceByField = /* @__PURE__ */ new WeakMap();
|
|
181
|
+
const targets = /* @__PURE__ */ new Map();
|
|
182
|
+
const streams = {
|
|
183
|
+
final: options.listCounterState ?? createListCounterState(),
|
|
184
|
+
original: options.originalListCounterState ?? createListCounterState()
|
|
185
|
+
};
|
|
186
|
+
const createContexts = () => ({
|
|
187
|
+
byNumId: /* @__PURE__ */ new Map(),
|
|
188
|
+
byAbstractNumId: /* @__PURE__ */ new Map()
|
|
189
|
+
});
|
|
190
|
+
const contextStreams = {
|
|
191
|
+
final: createContexts(),
|
|
192
|
+
original: createContexts()
|
|
193
|
+
};
|
|
194
|
+
doc.descendants((node) => {
|
|
195
|
+
if (node.type.name !== "paragraph") return true;
|
|
196
|
+
const target = numberTargetForParagraph(node, contextStreams, streams);
|
|
197
|
+
node.descendants((descendant) => {
|
|
198
|
+
if (descendant.type.name === "bookmarkBoundary") {
|
|
199
|
+
const attrs = expectBookmarkBoundaryAttrs(descendant);
|
|
200
|
+
if (attrs.type === "start" && targets.size < MAX_BOOKMARK_TARGETS && referencedBookmarks.has(attrs.name) && !targets.has(attrs.name)) targets.set(attrs.name, target);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (descendant.type.name !== "field" && descendant.type.name !== "structuredField") return true;
|
|
204
|
+
if (fieldNodes.has(descendant)) sourceByField.set(descendant, target);
|
|
205
|
+
return true;
|
|
206
|
+
});
|
|
207
|
+
const paragraphAttrs = expectParagraphAttrs(node);
|
|
208
|
+
if (targets.size < MAX_BOOKMARK_TARGETS) {
|
|
209
|
+
for (const bookmark of paragraphAttrs.bookmarks ?? []) if (referencedBookmarks.has(bookmark.name) && !targets.has(bookmark.name)) targets.set(bookmark.name, target);
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
});
|
|
213
|
+
const candidates = [];
|
|
214
|
+
for (const field of fields) {
|
|
215
|
+
const target = targets.get(field.spec.bookmark);
|
|
216
|
+
if (target === void 0 || target === null) continue;
|
|
217
|
+
let value;
|
|
218
|
+
if (field.spec.numberSwitch === "n") value = target.current;
|
|
219
|
+
else if (field.spec.numberSwitch === "r") value = relativeNumber(target, sourceByField.get(field.node) ?? null);
|
|
220
|
+
else value = target.full;
|
|
221
|
+
if (value !== null) candidates.push({
|
|
222
|
+
node: field.node,
|
|
223
|
+
cached: field.cached,
|
|
224
|
+
value
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return candidates;
|
|
228
|
+
}
|
|
229
|
+
const isCalibratedCandidate = ({ node, cached, value }) => {
|
|
230
|
+
const normalizedCache = normalizeCachedResult(cached);
|
|
231
|
+
const baseline = expectFieldAttrs(node)._numberedRefBaseline;
|
|
232
|
+
return normalizedCache.length === 0 || normalizedCache === normalizeCachedResult(value) || baseline !== void 0 && normalizedCache === normalizeCachedResult(baseline);
|
|
233
|
+
};
|
|
234
|
+
const mapDocument = (node, transform) => {
|
|
235
|
+
let mapped = node;
|
|
236
|
+
if (node.childCount > 0) {
|
|
237
|
+
const children = [];
|
|
238
|
+
let changed = false;
|
|
239
|
+
node.forEach((child) => {
|
|
240
|
+
const mappedChild = mapDocument(child, transform);
|
|
241
|
+
children.push(mappedChild);
|
|
242
|
+
changed ||= mappedChild !== child;
|
|
243
|
+
});
|
|
244
|
+
if (changed) mapped = node.copy(Fragment.fromArray(children));
|
|
245
|
+
}
|
|
246
|
+
return transform(mapped);
|
|
247
|
+
};
|
|
248
|
+
/** Stamp only proven numbered REF fields with clone-safe calibration state. */
|
|
249
|
+
function stampNumberedRefFieldBaselines(doc) {
|
|
250
|
+
const baselines = /* @__PURE__ */ new WeakMap();
|
|
251
|
+
let hasBaselines = false;
|
|
252
|
+
for (const candidate of collectNumberedRefCandidates(doc, {})) if (isCalibratedCandidate(candidate)) {
|
|
253
|
+
baselines.set(candidate.node, candidate.cached);
|
|
254
|
+
hasBaselines = true;
|
|
255
|
+
}
|
|
256
|
+
if (!hasBaselines) return doc;
|
|
257
|
+
return mapDocument(doc, (node) => {
|
|
258
|
+
const baseline = baselines.get(node);
|
|
259
|
+
if (baseline === void 0 || node.type.name !== "field" && node.type.name !== "structuredField") return node;
|
|
260
|
+
return node.type.create({
|
|
261
|
+
...node.attrs,
|
|
262
|
+
_numberedRefBaseline: baseline
|
|
263
|
+
}, node.content, node.marks);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Resolve bounded, numbered REF fields against bookmarks in the ProseMirror body story.
|
|
268
|
+
* Unsupported or uncalibrated fields are absent from the returned map and retain their cache.
|
|
269
|
+
*/
|
|
270
|
+
function resolveNumberedRefFields(doc, options = {}) {
|
|
271
|
+
const results = /* @__PURE__ */ new Map();
|
|
272
|
+
for (const candidate of collectNumberedRefCandidates(doc, options)) if (isCalibratedCandidate(candidate)) results.set(candidate.node, candidate.value);
|
|
273
|
+
return results;
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
export { parseNumberedRefInstruction, resolveNumberedRefFields, stampNumberedRefFieldBaselines };
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
function getVanillaNodeText(node) {
|
|
4
4
|
const parts = [];
|
|
5
5
|
node.descendants((child) => {
|
|
6
|
-
if (!child.isText || !child.text) return true;
|
|
7
6
|
if (child.marks.some((m) => m.type.name === "insertion")) return false;
|
|
8
|
-
|
|
7
|
+
if (child.isText && child.text) {
|
|
8
|
+
parts.push(child.text);
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (child.isLeaf && child.textContent) {
|
|
12
|
+
parts.push(child.textContent);
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
9
15
|
return true;
|
|
10
16
|
});
|
|
11
17
|
return parts.join("");
|
|
@@ -15,11 +21,18 @@ function getVanillaTextBetween(doc, from, to) {
|
|
|
15
21
|
if (from >= to) return "";
|
|
16
22
|
const parts = [];
|
|
17
23
|
doc.nodesBetween(from, to, (child, pos) => {
|
|
18
|
-
if (
|
|
19
|
-
if (child.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
if (child.marks.some((m) => m.type.name === "insertion")) return false;
|
|
25
|
+
if (child.isText && child.text) {
|
|
26
|
+
const start = Math.max(from, pos);
|
|
27
|
+
const end = Math.min(to, pos + child.text.length);
|
|
28
|
+
if (start < end) parts.push(child.text.slice(start - pos, end - pos));
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (child.isLeaf && child.textContent) {
|
|
32
|
+
parts.push(child.textContent);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
23
36
|
});
|
|
24
37
|
return parts.join("");
|
|
25
38
|
}
|
|
@@ -41,25 +54,51 @@ function findTextInPmParagraph(doc, paragraphFrom, paragraphTo, searchText) {
|
|
|
41
54
|
let fullText = "";
|
|
42
55
|
const textPositions = [];
|
|
43
56
|
doc.nodesBetween(paragraphFrom, paragraphTo, (node, pos) => {
|
|
44
|
-
if (
|
|
45
|
-
if (node.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
if (node.marks.some((m) => m.type.name === "insertion")) return false;
|
|
58
|
+
if (node.isText && node.text) {
|
|
59
|
+
textPositions.push({
|
|
60
|
+
text: node.text,
|
|
61
|
+
pos,
|
|
62
|
+
pmLength: node.text.length,
|
|
63
|
+
atomic: false
|
|
64
|
+
});
|
|
65
|
+
fullText += node.text;
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
if (node.isLeaf && node.textContent) {
|
|
69
|
+
textPositions.push({
|
|
70
|
+
text: node.textContent,
|
|
71
|
+
pos,
|
|
72
|
+
pmLength: node.nodeSize,
|
|
73
|
+
atomic: true
|
|
74
|
+
});
|
|
75
|
+
fullText += node.textContent;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
51
79
|
});
|
|
52
80
|
const firstMatch = fullText.indexOf(searchText);
|
|
53
81
|
if (firstMatch === -1) return null;
|
|
54
82
|
if (fullText.indexOf(searchText, firstMatch + 1) !== -1) return null;
|
|
83
|
+
const matchEnd = firstMatch + searchText.length;
|
|
84
|
+
let segmentStart = 0;
|
|
85
|
+
for (const position of textPositions) {
|
|
86
|
+
const segmentEnd = segmentStart + position.text.length;
|
|
87
|
+
if (position.atomic && (segmentStart < firstMatch && firstMatch < segmentEnd || segmentStart < matchEnd && matchEnd < segmentEnd)) return null;
|
|
88
|
+
segmentStart = segmentEnd;
|
|
89
|
+
}
|
|
55
90
|
let charOffset = 0;
|
|
56
91
|
let fromPos = paragraphFrom;
|
|
57
92
|
let toPos = paragraphFrom;
|
|
58
93
|
for (const tp of textPositions) {
|
|
59
|
-
const segEnd = charOffset + tp.
|
|
60
|
-
if (charOffset <= firstMatch && firstMatch < segEnd)
|
|
94
|
+
const segEnd = charOffset + tp.text.length;
|
|
95
|
+
if (charOffset <= firstMatch && firstMatch < segEnd) {
|
|
96
|
+
const localOffset = firstMatch - charOffset;
|
|
97
|
+
fromPos = tp.atomic ? tp.pos : tp.pos + localOffset;
|
|
98
|
+
}
|
|
61
99
|
if (charOffset <= firstMatch + searchText.length && firstMatch + searchText.length <= segEnd) {
|
|
62
|
-
|
|
100
|
+
const localOffset = firstMatch + searchText.length - charOffset;
|
|
101
|
+
toPos = tp.atomic ? tp.pos + tp.pmLength : tp.pos + localOffset;
|
|
63
102
|
break;
|
|
64
103
|
}
|
|
65
104
|
charOffset = segEnd;
|
|
@@ -36,7 +36,7 @@ const buildMatches = (doc, terms) => {
|
|
|
36
36
|
let match = regex.exec(joined);
|
|
37
37
|
while (match !== null) {
|
|
38
38
|
const from = offsetToDocPos(chunks, match.index);
|
|
39
|
-
const to = offsetToDocPos(chunks, match.index + match[0].length);
|
|
39
|
+
const to = offsetToDocPos(chunks, match.index + match[0].length, "end");
|
|
40
40
|
matches.push({
|
|
41
41
|
from,
|
|
42
42
|
to,
|
|
@@ -4,6 +4,8 @@ type TextChunk = {
|
|
|
4
4
|
text: string;
|
|
5
5
|
/** PM doc position where this chunk's first char lives. */
|
|
6
6
|
start: number;
|
|
7
|
+
/** PM position after this chunk; differs from text length for atomic fields. */
|
|
8
|
+
end?: number;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
9
11
|
* Collect every block-level node's text content as an array of
|
|
@@ -12,7 +14,7 @@ type TextChunk = {
|
|
|
12
14
|
*/
|
|
13
15
|
declare const collectBlockChunks: (doc: Node) => TextChunk[][];
|
|
14
16
|
/** Map a joined-string offset back to its PM doc position. */
|
|
15
|
-
declare const offsetToDocPos: (chunks: TextChunk[], offset: number) => number;
|
|
17
|
+
declare const offsetToDocPos: (chunks: TextChunk[], offset: number, bias?: "start" | "end") => number;
|
|
16
18
|
/** Join a block's chunks into the single string callers scan. */
|
|
17
19
|
declare const joinChunks: (chunks: TextChunk[]) => string;
|
|
18
20
|
//#endregion
|
|
@@ -10,10 +10,24 @@ const collectBlockChunks = (doc) => {
|
|
|
10
10
|
if (node.isTextblock) {
|
|
11
11
|
const chunks = [];
|
|
12
12
|
node.descendants((child, offset) => {
|
|
13
|
-
if (child.isText && child.text !== void 0)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
if (child.isText && child.text !== void 0) {
|
|
14
|
+
const start = pos + 1 + offset;
|
|
15
|
+
chunks.push({
|
|
16
|
+
text: child.text,
|
|
17
|
+
start,
|
|
18
|
+
end: start + child.text.length
|
|
19
|
+
});
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (child.isLeaf && child.textContent) {
|
|
23
|
+
const start = pos + 1 + offset;
|
|
24
|
+
chunks.push({
|
|
25
|
+
text: child.textContent,
|
|
26
|
+
start,
|
|
27
|
+
end: start + child.nodeSize
|
|
28
|
+
});
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
17
31
|
return true;
|
|
18
32
|
});
|
|
19
33
|
if (chunks.length > 0) blocks.push(chunks);
|
|
@@ -24,14 +38,21 @@ const collectBlockChunks = (doc) => {
|
|
|
24
38
|
return blocks;
|
|
25
39
|
};
|
|
26
40
|
/** Map a joined-string offset back to its PM doc position. */
|
|
27
|
-
const offsetToDocPos = (chunks, offset) => {
|
|
41
|
+
const offsetToDocPos = (chunks, offset, bias = "start") => {
|
|
28
42
|
let consumed = 0;
|
|
29
43
|
for (const chunk of chunks) {
|
|
30
|
-
if (offset <= consumed + chunk.text.length)
|
|
44
|
+
if (offset <= consumed + chunk.text.length) {
|
|
45
|
+
const localOffset = offset - consumed;
|
|
46
|
+
const end = chunk.end ?? chunk.start + chunk.text.length;
|
|
47
|
+
if (end - chunk.start === chunk.text.length) return chunk.start + localOffset;
|
|
48
|
+
if (localOffset === 0) return chunk.start;
|
|
49
|
+
if (localOffset === chunk.text.length || bias === "end") return end;
|
|
50
|
+
return chunk.start;
|
|
51
|
+
}
|
|
31
52
|
consumed += chunk.text.length;
|
|
32
53
|
}
|
|
33
54
|
const last = chunks.at(-1);
|
|
34
|
-
return last ? last.start + last.text.length : 0;
|
|
55
|
+
return last ? last.end ?? last.start + last.text.length : 0;
|
|
35
56
|
};
|
|
36
57
|
/** Join a block's chunks into the single string callers scan. */
|
|
37
58
|
const joinChunks = (chunks) => chunks.map((c) => c.text).join("");
|