@stll/folio-core 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +1 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +61 -95
- package/dist/markdown/renderParagraph.js +7 -2
- package/dist/prosemirror/attrs/index.js +2 -1
- package/dist/prosemirror/bookmarkBoundaryAttrs.d.ts +8 -0
- package/dist/prosemirror/bookmarkBoundaryAttrs.js +66 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +170 -52
- package/dist/prosemirror/conversion/toProseDoc.js +169 -61
- package/dist/prosemirror/extensions/StarterKit.js +11 -3
- package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +8 -4
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/features/PasteCleanupExtension.js +9 -5
- package/dist/prosemirror/extensions/features/pasteCleanup.d.ts +10 -23
- package/dist/prosemirror/extensions/features/pasteCleanup.js +77 -22
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.d.ts +9 -0
- package/dist/prosemirror/extensions/nodes/BookmarkBoundaryExtension.js +67 -0
- package/dist/prosemirror/extensions/nodes/FieldExtension.d.ts +10 -4
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +77 -59
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +4 -1
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +4 -3
- package/dist/prosemirror/listMarker.d.ts +58 -0
- package/dist/prosemirror/listMarker.js +185 -0
- package/dist/prosemirror/numberedRefFields.d.ts +24 -0
- package/dist/prosemirror/numberedRefFields.js +276 -0
- package/dist/prosemirror/paraText.js +56 -17
- package/dist/prosemirror/plugins/anonymizationDecorations.js +1 -1
- package/dist/prosemirror/plugins/pmTextScan.d.ts +3 -1
- package/dist/prosemirror/plugins/pmTextScan.js +28 -7
- package/dist/prosemirror/plugins/templateDirectives.js +2 -2
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/nodes.d.ts +13 -1
- package/dist/prosemirror/validation.js +93 -0
- package/package.json +1 -1
|
@@ -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("");
|
|
@@ -71,7 +71,7 @@ const scanDirectives = (doc) => {
|
|
|
71
71
|
const last = chunks.at(-1);
|
|
72
72
|
ranges.push({
|
|
73
73
|
from: chunks[0]?.start ?? 0,
|
|
74
|
-
to: last ? last.start + last.text.length : 0,
|
|
74
|
+
to: last ? last.end ?? last.start + last.text.length : 0,
|
|
75
75
|
kind: sole.meta.kind,
|
|
76
76
|
expr: directiveExpr(sole.meta),
|
|
77
77
|
block: true
|
|
@@ -82,7 +82,7 @@ const scanDirectives = (doc) => {
|
|
|
82
82
|
const clauseVersion = marker.meta.kind === "clause" ? marker.meta.version : void 0;
|
|
83
83
|
ranges.push({
|
|
84
84
|
from: offsetToDocPos(chunks, marker.start),
|
|
85
|
-
to: offsetToDocPos(chunks, marker.end),
|
|
85
|
+
to: offsetToDocPos(chunks, marker.end, "end"),
|
|
86
86
|
kind: marker.meta.kind,
|
|
87
87
|
expr: directiveExpr(marker.meta),
|
|
88
88
|
block: false,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CharacterSpacingAttrs, CharacterStyleAttrs, CommentAttrs, EmphasisMarkAttrs, FontFamilyAttrs, FontSizeAttrs, FootnoteRefAttrs, HighlightAttrs, HyperlinkAttrs, LanguageAttrs, RunFormattingOverrideAttrs, RunPropertyChangeMarkAttrs, RunShadingAttrs, StrikeAttrs, TextColorAttrs, TextEffectAttrs, TrackedChangeMarkAttrs, UnderlineAttrs } from "./marks.js";
|
|
2
|
-
import { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
|
|
2
|
+
import { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAttrs } from "./nodes.js";
|
|
3
3
|
import { ExtensionManager } from "../extensions/ExtensionManager.js";
|
|
4
4
|
//#region src/prosemirror/schema/index.d.ts
|
|
5
5
|
declare const singletonManager: ExtensionManager;
|
|
@@ -11,4 +11,4 @@ type DocxSchema = typeof schema;
|
|
|
11
11
|
type DocxNode = ReturnType<typeof schema.node>;
|
|
12
12
|
type DocxMark = ReturnType<typeof schema.mark>;
|
|
13
13
|
//#endregion
|
|
14
|
-
export { type BlockSdtAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type LanguageAttrs, type MathAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type RunFormattingOverrideAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
|
|
14
|
+
export { type BlockSdtAttrs, type BookmarkBoundaryAttrs, type CharacterSpacingAttrs, type CharacterStyleAttrs, type CommentAttrs, DocxMark, DocxNode, DocxSchema, type EmphasisMarkAttrs, type FieldAttrs, type FontFamilyAttrs, type FontSizeAttrs, type FootnoteRefAttrs, type HardBreakAttrs, type HighlightAttrs, type HyperlinkAttrs, type ImageAttrs, type ImagePositionAttrs, type LanguageAttrs, type MathAttrs, type ParagraphAttrs, type ParagraphPropertyChangeAttrs, type RunFormattingOverrideAttrs, type RunPropertyChangeMarkAttrs, type RunShadingAttrs, type SdtAttrs, type ShapeAttrs, type StrikeAttrs, type SymbolAttrs, type TabAttrs, type TableAttrs, type TableCellAttrs, type TableRowAttrs, type TextBoxAttrs, type TextColorAttrs, type TextEffectAttrs, type TrackedChangeMarkAttrs, type UnderlineAttrs, schema, singletonManager };
|
|
@@ -15,6 +15,16 @@ type SymbolAttrs = {
|
|
|
15
15
|
font: string;
|
|
16
16
|
char: string;
|
|
17
17
|
};
|
|
18
|
+
type BookmarkBoundaryAttrs = {
|
|
19
|
+
type: "start";
|
|
20
|
+
id: number;
|
|
21
|
+
name: string;
|
|
22
|
+
colFirst?: number;
|
|
23
|
+
colLast?: number;
|
|
24
|
+
} | {
|
|
25
|
+
type: "end";
|
|
26
|
+
id: number;
|
|
27
|
+
};
|
|
18
28
|
/**
|
|
19
29
|
* Paragraph node attributes - maps to ParagraphFormatting
|
|
20
30
|
*/
|
|
@@ -292,6 +302,8 @@ type FieldAttrs = {
|
|
|
292
302
|
instruction: string;
|
|
293
303
|
/** Current/cached display text */
|
|
294
304
|
displayText: string;
|
|
305
|
+
/** Imported cache that proved numbered REF resolution for this field. */
|
|
306
|
+
_numberedRefBaseline?: string;
|
|
295
307
|
/** Whether the field came from w:fldSimple or a complex fldChar range */
|
|
296
308
|
fieldKind: "simple" | "complex";
|
|
297
309
|
/** Field is locked */
|
|
@@ -706,4 +718,4 @@ type TableCellAttrs = {
|
|
|
706
718
|
_docxVMergeContinuationCells?: document_d_exports.TableCell[];
|
|
707
719
|
};
|
|
708
720
|
//#endregion
|
|
709
|
-
export { BlockSdtAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs };
|
|
721
|
+
export { BlockSdtAttrs, BookmarkBoundaryAttrs, FieldAttrs, HardBreakAttrs, ImageAttrs, ImagePositionAttrs, MathAttrs, ParagraphAttrs, ParagraphPropertyChangeAttrs, SdtAttrs, ShapeAttrs, SuggestedStructuralMarker, SymbolAttrs, TabAttrs, TableAttrs, TableCellAttrs, TableRowAttrs, TextBoxAnchorAttrs, TextBoxAttrs };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readLanguageMarkAttrs, readMathAttrs, readParagraphAttrs, readRunFormattingOverrideMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs } from "./attrs/index.js";
|
|
2
|
+
import { readBookmarkBoundaryAttrs } from "./bookmarkBoundaryAttrs.js";
|
|
2
3
|
import { readTextBoxAnchorAttrs } from "./textBoxAnchorAttrs.js";
|
|
3
4
|
//#region src/prosemirror/validation.ts
|
|
4
5
|
var ProseMirrorDocumentValidationError = class extends Error {
|
|
@@ -18,11 +19,71 @@ const validateProseMirrorDocument = (doc) => {
|
|
|
18
19
|
message: `Expected doc, got ${doc.type.name}.`
|
|
19
20
|
});
|
|
20
21
|
validateNode(doc, "doc", issues);
|
|
22
|
+
validateBookmarkBoundaryStructure(doc, issues);
|
|
21
23
|
return {
|
|
22
24
|
valid: issues.length === 0,
|
|
23
25
|
issues
|
|
24
26
|
};
|
|
25
27
|
};
|
|
28
|
+
const validateBookmarkBoundaryStructure = (doc, issues) => {
|
|
29
|
+
const open = /* @__PURE__ */ new Map();
|
|
30
|
+
const startedIds = /* @__PURE__ */ new Set();
|
|
31
|
+
const registerStart = (id, path) => {
|
|
32
|
+
if (startedIds.has(id)) {
|
|
33
|
+
issues.push({
|
|
34
|
+
path,
|
|
35
|
+
message: `Bookmark id ${id} has more than one start boundary.`
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
startedIds.add(id);
|
|
40
|
+
open.set(id, {
|
|
41
|
+
id,
|
|
42
|
+
path
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const registerEnd = (id, path) => {
|
|
46
|
+
if (!open.get(id)) {
|
|
47
|
+
issues.push({
|
|
48
|
+
path,
|
|
49
|
+
message: `Bookmark id ${id} has no open start boundary.`
|
|
50
|
+
});
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
open.delete(id);
|
|
54
|
+
};
|
|
55
|
+
const visit = (node, path) => {
|
|
56
|
+
const paragraphAttrs = node.type.name === "paragraph" ? readParagraphAttrs(node) : null;
|
|
57
|
+
if (paragraphAttrs?.ok) for (const [index, bookmark] of (paragraphAttrs.value.bookmarks ?? []).entries()) registerStart(bookmark.id, `${path}.paragraph.attrs.bookmarks[${index}]`);
|
|
58
|
+
if (node.type.name === "bookmarkBoundary") {
|
|
59
|
+
const result = readBookmarkBoundaryAttrs(node);
|
|
60
|
+
if (result.ok) {
|
|
61
|
+
const attrs = result.value;
|
|
62
|
+
const hasHyperlink = node.marks.some((mark) => mark.type.name === "hyperlink");
|
|
63
|
+
const trackedChanges = node.marks.filter((mark) => mark.type.name === "insertion" || mark.type.name === "deletion");
|
|
64
|
+
if (trackedChanges.length > 1) issues.push({
|
|
65
|
+
path,
|
|
66
|
+
message: "Bookmark boundaries cannot carry multiple tracked-change parents."
|
|
67
|
+
});
|
|
68
|
+
else if (trackedChanges.length === 1 && !hasHyperlink) issues.push({
|
|
69
|
+
path,
|
|
70
|
+
message: "Bookmark boundaries inside tracked changes require a hyperlink serialization parent."
|
|
71
|
+
});
|
|
72
|
+
if (attrs.type === "start") registerStart(attrs.id, path);
|
|
73
|
+
else registerEnd(attrs.id, path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
node.forEach((child, _offset, index) => {
|
|
77
|
+
visit(child, `${path}.content[${index}]`);
|
|
78
|
+
});
|
|
79
|
+
if (paragraphAttrs?.ok) for (const [index, bookmark] of (paragraphAttrs.value.bookmarks ?? []).entries()) registerEnd(bookmark.id, `${path}.paragraph.attrs.bookmarks[${index}]`);
|
|
80
|
+
};
|
|
81
|
+
visit(doc, "doc");
|
|
82
|
+
for (const boundary of open.values()) issues.push({
|
|
83
|
+
path: boundary.path,
|
|
84
|
+
message: `Bookmark id ${boundary.id} has no matching end boundary.`
|
|
85
|
+
});
|
|
86
|
+
};
|
|
26
87
|
const assertValidProseMirrorDocument = (doc, context) => {
|
|
27
88
|
if (validDocumentCache.has(doc)) return;
|
|
28
89
|
const validation = validateProseMirrorDocument(doc);
|
|
@@ -50,6 +111,9 @@ const validateNodeAttrs = (node, path, issues) => {
|
|
|
50
111
|
case "horizontalRule":
|
|
51
112
|
case "pageBreak":
|
|
52
113
|
case "renderedPageBreak": return;
|
|
114
|
+
case "bookmarkBoundary":
|
|
115
|
+
appendAttrIssues(path, readBookmarkBoundaryAttrs(node), issues);
|
|
116
|
+
return;
|
|
53
117
|
case "tab":
|
|
54
118
|
appendAttrIssues(path, readTabAttrs(node), issues);
|
|
55
119
|
return;
|
|
@@ -77,6 +141,35 @@ const validateNodeAttrs = (node, path, issues) => {
|
|
|
77
141
|
return;
|
|
78
142
|
case "field":
|
|
79
143
|
appendAttrIssues(path, readFieldAttrs(node), issues);
|
|
144
|
+
if (node.childCount > 0) issues.push({
|
|
145
|
+
path: `${path}.content`,
|
|
146
|
+
message: "Ordinary fields cannot contain structured result children."
|
|
147
|
+
});
|
|
148
|
+
return;
|
|
149
|
+
case "structuredField":
|
|
150
|
+
{
|
|
151
|
+
const fieldAttrs = readFieldAttrs(node);
|
|
152
|
+
appendAttrIssues(path, fieldAttrs, issues);
|
|
153
|
+
if (fieldAttrs.ok) {
|
|
154
|
+
const hasStructuredHyperlink = node.content.content.some((child) => child.marks.some((mark) => mark.type.name === "hyperlink"));
|
|
155
|
+
if (fieldAttrs.value.fieldKind === "complex") issues.push({
|
|
156
|
+
path: `${path}.content`,
|
|
157
|
+
message: "Complex fields cannot contain structured result children."
|
|
158
|
+
});
|
|
159
|
+
else if (!hasStructuredHyperlink) issues.push({
|
|
160
|
+
path: `${path}.content`,
|
|
161
|
+
message: "Structured simple fields require hyperlink content."
|
|
162
|
+
});
|
|
163
|
+
node.forEach((child, _offset, index) => {
|
|
164
|
+
const childPath = `${path}.content[${index}]`;
|
|
165
|
+
const hasHyperlink = child.marks.some((mark) => mark.type.name === "hyperlink");
|
|
166
|
+
if (child.type.name === "bookmarkBoundary" && !hasHyperlink) issues.push({
|
|
167
|
+
path: childPath,
|
|
168
|
+
message: "Bookmark boundaries inside fields require a hyperlink parent."
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
80
173
|
return;
|
|
81
174
|
case "math":
|
|
82
175
|
appendAttrIssues(path, readMathAttrs(node), issues);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|