@stll/folio-core 0.61.0 → 0.62.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 (59) hide show
  1. package/dist/ai-edits/headless.js +13 -12
  2. package/dist/controller/noteEditorManager.d.ts +1 -1
  3. package/dist/display-list/build/imagePrimitives.js +26 -7
  4. package/dist/display-list/build/paragraphPrimitives.js +15 -2
  5. package/dist/display-list/dom/renderDisplayListToDom.js +8 -5
  6. package/dist/display-list/types.d.ts +7 -3
  7. package/dist/docx/groupDrawingParser.js +22 -12
  8. package/dist/i18n/messages/ar.gen.js +1 -0
  9. package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
  10. package/dist/i18n/messages/catalogs.gen.js +17 -0
  11. package/dist/i18n/messages/cs.gen.js +1 -0
  12. package/dist/i18n/messages/de.gen.js +1 -0
  13. package/dist/i18n/messages/en.gen.js +1 -0
  14. package/dist/i18n/messages/es.gen.js +1 -0
  15. package/dist/i18n/messages/et.gen.js +1 -0
  16. package/dist/i18n/messages/fr.gen.js +1 -0
  17. package/dist/i18n/messages/he.gen.js +1 -0
  18. package/dist/i18n/messages/hi.gen.js +1 -0
  19. package/dist/i18n/messages/hu.gen.js +1 -0
  20. package/dist/i18n/messages/lt.gen.js +1 -0
  21. package/dist/i18n/messages/lv.gen.js +1 -0
  22. package/dist/i18n/messages/messages.gen.d.ts +1 -0
  23. package/dist/i18n/messages/pl.gen.js +1 -0
  24. package/dist/i18n/messages/pt-BR.gen.js +1 -0
  25. package/dist/i18n/messages/sk.gen.js +1 -0
  26. package/dist/i18n/messages/tr.gen.js +1 -0
  27. package/dist/i18n/messages/zh-CN.gen.js +1 -0
  28. package/dist/layout-bridge/convert/endnoteLayout.d.ts +1 -1
  29. package/dist/layout-bridge/convert/endnoteLayout.js +1 -3
  30. package/dist/layout-engine/index.js +2 -2
  31. package/dist/layout-engine/noteAreaFlow.d.ts +4 -4
  32. package/dist/layout-engine/noteAreaFlow.js +12 -6
  33. package/dist/layout-engine/types.d.ts +5 -2
  34. package/dist/layout-painter/index.d.ts +1 -1
  35. package/dist/managers/FindReplaceManager.d.ts +13 -6
  36. package/dist/managers/FindReplaceManager.js +15 -9
  37. package/dist/paged-layout/editorScrollRoot.d.ts +3 -1
  38. package/dist/paged-layout/editorScrollRoot.js +2 -2
  39. package/dist/pdf/images.js +1 -0
  40. package/dist/prosemirror/commentRangeInlineAtoms.d.ts +40 -0
  41. package/dist/prosemirror/commentRangeInlineAtoms.js +33 -0
  42. package/dist/prosemirror/conversion/fromProseDoc.js +5 -4
  43. package/dist/prosemirror/conversion/toProseDoc.js +12 -19
  44. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +4 -4
  45. package/dist/prosemirror/fieldRepresentation.js +4 -2
  46. package/dist/prosemirror/findReplaceSelection.d.ts +38 -2
  47. package/dist/prosemirror/findReplaceSelection.js +143 -42
  48. package/dist/prosemirror/inlineDisplayText.d.ts +10 -0
  49. package/dist/prosemirror/inlineDisplayText.js +29 -0
  50. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  51. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  52. package/dist/utils/findMatchView.d.ts +32 -0
  53. package/dist/utils/findMatchView.js +406 -0
  54. package/dist/utils/findReplace.d.ts +62 -4
  55. package/dist/utils/findReplace.js +359 -37
  56. package/dist/utils/findSearchKey.d.ts +29 -0
  57. package/dist/utils/findSearchKey.js +34 -0
  58. package/dist/utils/replaceText.js +138 -111
  59. package/package.json +1 -1
@@ -1,5 +1,18 @@
1
+ import { parseFieldType } from "../docx/fieldParser.js";
2
+ import { HYPHEN_TEXT_CARRIERS } from "../prosemirror/conversion/hyphenTextCarriers.js";
3
+ import { isTableCellRetainedInReviewView } from "../prosemirror/tableCellRevisionVisibility.js";
4
+ import { decodeOoxmlSymbolCharacter } from "./ooxmlSymbol.js";
1
5
  import { panic } from "better-result";
6
+ import { documentStories, sameStory, storyBody } from "@stll/docx-core/ops";
2
7
  //#region src/utils/findReplace.ts
8
+ /** One replacement policy for every host; searchability does not imply editability. */
9
+ const replaceEligibility = (match) => {
10
+ if (match.story !== void 0 && match.story !== "main" || match.textBoxIndex !== void 0 || match.commentId !== void 0) return {
11
+ status: "blocked",
12
+ reason: "non-body-story"
13
+ };
14
+ return match.replacement;
15
+ };
3
16
  const createDefaultFindOptions = () => ({
4
17
  matchCase: false,
5
18
  matchWholeWord: false,
@@ -10,7 +23,7 @@ const createSearchPattern = (searchText, options) => {
10
23
  if (!searchText) return null;
11
24
  try {
12
25
  const source = options.useRegex ? searchText : escapeRegexString(searchText);
13
- const pattern = options.matchWholeWord ? `\\b${source}\\b` : source;
26
+ const pattern = options.matchWholeWord ? `\\b(?:${source})\\b` : source;
14
27
  return new RegExp(pattern, options.matchCase ? "gu" : "giu");
15
28
  } catch {
16
29
  return null;
@@ -18,98 +31,385 @@ const createSearchPattern = (searchText, options) => {
18
31
  };
19
32
  const findAllMatches = (content, searchText, options) => {
20
33
  if (!content || !searchText) return [];
21
- const searchFor = options.matchCase ? searchText : searchText.toLowerCase();
22
- const source = escapeRegexString(searchFor);
23
- const pattern = new RegExp(options.matchWholeWord ? `\\b${source}\\b` : source, options.matchCase ? "gu" : "giu");
34
+ const pattern = createSearchPattern(searchText, options);
35
+ if (!pattern) return [];
24
36
  const matches = [];
25
37
  let match;
26
38
  while ((match = pattern.exec(content)) !== null) {
39
+ if (match[0].length === 0) {
40
+ const codePoint = content.codePointAt(pattern.lastIndex);
41
+ pattern.lastIndex += codePoint !== void 0 && codePoint > 65535 ? 2 : 1;
42
+ continue;
43
+ }
27
44
  matches.push({
28
45
  start: match.index,
29
46
  end: match.index + match[0].length
30
47
  });
31
- if (match[0].length === 0) pattern.lastIndex++;
32
48
  }
33
49
  return matches;
34
50
  };
35
- const findInDocument = (document, searchText, options) => {
51
+ const findInDocument = (document, searchText, options, context) => {
36
52
  if (!document || !searchText) return [];
37
53
  const body = document.package.document;
38
54
  if (!isRecord(body) || !Array.isArray(body.content)) return [];
39
55
  const matches = [];
40
- forEachParagraph(body.content, (paragraph, paragraphIndex) => {
41
- matches.push(...findInParagraph(paragraph, searchText, options, paragraphIndex));
56
+ for (const story of orderedSearchStories(document)) {
57
+ if (story !== "main" && (story.kind === "header" || story.kind === "footer") && context?.renderedStories && !context.renderedStories.some((rendered) => sameStory(rendered, story))) continue;
58
+ const blocks = storyBody(document, story).content;
59
+ if (story !== "main" && (story.kind === "footnote" || story.kind === "endnote")) {
60
+ const noteType = (story.kind === "footnote" ? document.package.footnotes : document.package.endnotes)?.find(({ id }) => id === story.id)?.noteType;
61
+ if (noteType && noteType !== "normal") continue;
62
+ }
63
+ let textBoxIndex = 0;
64
+ const searchBlocks = (items, shapeAddress, visible = true) => {
65
+ forEachParagraph(items, (paragraph, paragraphIndex, structureVisible) => {
66
+ const paragraphVisible = visible && structureVisible;
67
+ if (paragraphVisible) for (const match of findInParagraph(paragraph, searchText, options, paragraphIndex)) if (shapeAddress) matches.push({
68
+ ...match,
69
+ story,
70
+ textBoxIndex: shapeAddress.textBoxIndex,
71
+ anchorParagraphIndex: shapeAddress.anchorParagraphIndex
72
+ });
73
+ else matches.push({
74
+ ...match,
75
+ story
76
+ });
77
+ for (const { shape, visible: shapeVisible } of paragraphShapes(paragraph.content, options.displayMode)) {
78
+ if (!shape.textBody) continue;
79
+ searchBlocks(shape.textBody.content, {
80
+ textBoxIndex: textBoxIndex++,
81
+ anchorParagraphIndex: shapeAddress?.anchorParagraphIndex ?? paragraphIndex
82
+ }, paragraphVisible && shapeVisible);
83
+ }
84
+ }, options.displayMode);
85
+ };
86
+ searchBlocks(blocks);
87
+ }
88
+ if (options.includeComments) for (const comment of document.package.document.comments ?? []) forEachParagraph(comment.content, (paragraph, paragraphIndex, visible) => {
89
+ if (!visible) return;
90
+ for (const match of findInParagraph(paragraph, searchText, {
91
+ ...options,
92
+ displayMode: "all-markup"
93
+ }, paragraphIndex)) matches.push({
94
+ ...match,
95
+ story: "main",
96
+ commentId: comment.id
97
+ });
42
98
  });
99
+ for (const match of matches) match.replacement = replaceEligibility(match);
43
100
  return matches;
44
101
  };
102
+ const orderedSearchStories = (document) => {
103
+ const stories = documentStories(document);
104
+ const ordered = ["main"];
105
+ const seen = /* @__PURE__ */ new Set();
106
+ for (const kind of ["header", "footer"]) {
107
+ const add = (rId) => {
108
+ const key = `${kind}:${rId}`;
109
+ const parts = kind === "header" ? document.package.headers : document.package.footers;
110
+ if (seen.has(key) || !parts?.has(rId)) return;
111
+ seen.add(key);
112
+ ordered.push({
113
+ kind,
114
+ rId
115
+ });
116
+ };
117
+ for (const section of document.package.document.sections ?? []) {
118
+ const refs = kind === "header" ? section.properties.headerReferences : section.properties.footerReferences;
119
+ for (const variant of [
120
+ "default",
121
+ "first",
122
+ "even"
123
+ ]) for (const ref of refs ?? []) if (ref.type === variant) add(ref.rId);
124
+ }
125
+ for (const story of stories) if (story !== "main" && story.kind === kind) add(story.rId);
126
+ }
127
+ for (const story of stories) if (story !== "main" && (story.kind === "footnote" || story.kind === "endnote")) ordered.push(story);
128
+ return ordered;
129
+ };
130
+ const isHiddenRevision = (type, mode) => {
131
+ if (mode === "original") return type === "insertion" || type === "moveTo";
132
+ if (mode === "no-markup" || mode === "simple-markup") return type === "deletion" || type === "moveFrom";
133
+ return false;
134
+ };
135
+ const paragraphShapes = (content, mode) => {
136
+ const shapes = [];
137
+ const visit = (items, visible = true) => {
138
+ for (const item of items) {
139
+ const itemVisible = visible && !isHiddenRevision(item.type, mode);
140
+ if (item.type === "run") {
141
+ for (const child of item.content) if (child.type === "shape") shapes.push({
142
+ shape: child.shape,
143
+ visible: itemVisible
144
+ });
145
+ } else if (item.type === "hyperlink") visit(item.children, itemVisible);
146
+ else if (item.type === "complexField") visit(item.fieldResult, itemVisible);
147
+ else if ("content" in item) visit(item.content, itemVisible);
148
+ }
149
+ };
150
+ visit(content);
151
+ return shapes;
152
+ };
153
+ /** Visible characters retain offsets in the authored paragraph, including hidden revisions. */
154
+ const getVisibleParagraphProjection = (paragraph, mode) => {
155
+ if (mode === void 0 || mode === "all-markup") return {
156
+ ...joinSearchProjections(paragraph.content.map(getParagraphContentSearchProjection)),
157
+ offsets: [],
158
+ hiddenContentBarriers: []
159
+ };
160
+ let text = "";
161
+ let authoredOffset = 0;
162
+ const offsets = [];
163
+ const hiddenContentBarriers = [];
164
+ const replacementBarriers = [];
165
+ const atomicContentRanges = [];
166
+ const fieldTextRanges = [];
167
+ const excludedTextRanges = [];
168
+ const pageBreakOffsets = [];
169
+ const visit = (items) => {
170
+ for (const item of items) {
171
+ if (isHiddenRevision(item.type, mode)) {
172
+ hiddenContentBarriers.push(text.length);
173
+ authoredOffset += getParagraphContentSearchProjection(item).text.length;
174
+ continue;
175
+ }
176
+ if (item.type === "hyperlink") {
177
+ visit(item.children);
178
+ continue;
179
+ }
180
+ if (item.type === "complexField" || item.type === "simpleField") {
181
+ const start = text.length;
182
+ visit(item.type === "complexField" ? item.fieldResult : item.content);
183
+ fieldTextRanges.push({
184
+ start,
185
+ end: text.length
186
+ });
187
+ if (isPageDependentSearchField(item.instruction)) excludedTextRanges.push({
188
+ start,
189
+ end: text.length
190
+ });
191
+ continue;
192
+ }
193
+ if (item.type !== "run" && "content" in item) {
194
+ visit(item.content);
195
+ continue;
196
+ }
197
+ const part = getParagraphContentSearchProjection(item);
198
+ for (const offset of part.replacementBarriers ?? []) replacementBarriers.push(text.length + offset);
199
+ for (const range of part.atomicContentRanges ?? []) atomicContentRanges.push({
200
+ start: text.length + range.start,
201
+ end: text.length + range.end
202
+ });
203
+ for (const range of part.fieldTextRanges ?? []) fieldTextRanges.push({
204
+ start: text.length + range.start,
205
+ end: text.length + range.end
206
+ });
207
+ for (const range of part.excludedTextRanges ?? []) excludedTextRanges.push({
208
+ start: text.length + range.start,
209
+ end: text.length + range.end
210
+ });
211
+ for (const offset of part.pageBreakOffsets) pageBreakOffsets.push(text.length + offset);
212
+ for (let index = 0; index < part.text.length; index++) offsets.push(authoredOffset + index);
213
+ text += part.text;
214
+ authoredOffset += part.text.length;
215
+ }
216
+ };
217
+ visit(paragraph.content);
218
+ return {
219
+ text,
220
+ offsets,
221
+ hiddenContentBarriers,
222
+ pageBreakOffsets,
223
+ excludedTextRanges,
224
+ fieldTextRanges,
225
+ atomicContentRanges,
226
+ replacementBarriers
227
+ };
228
+ };
229
+ /** Authored paragraph text displayed by comment UI, using the search owner. */
230
+ const getFindParagraphText = (paragraph) => getVisibleParagraphProjection(paragraph, void 0).text;
45
231
  const findInParagraph = (paragraph, searchText, options, paragraphIndex) => {
46
- const projection = getParagraphSearchProjection(paragraph);
232
+ const projection = getVisibleParagraphProjection(paragraph, options.displayMode);
47
233
  if (!projection.text) return [];
48
- return findAllMatches(projection.text, searchText, options).filter(({ start, end }) => !projection.pageBreakOffsets.some((offset) => offset > start && offset < end)).map(({ start, end }) => ({
234
+ return findAllMatches(projection.text, searchText, options).filter(({ start, end }) => !projection.pageBreakOffsets.some((offset) => offset > start && offset < end) && !(projection.excludedTextRanges ?? []).some((range) => range.start < end && range.end > start)).map(({ start, end }) => ({
49
235
  paragraphIndex,
50
- contentIndex: findContentIndexAtOffset(paragraph, start),
51
- startOffset: start,
52
- endOffset: end,
236
+ replacement: replacementForProjection({
237
+ projection,
238
+ start,
239
+ end
240
+ }),
241
+ contentIndex: findContentIndexAtOffset(paragraph, projection.offsets.at(start) ?? start),
242
+ startOffset: projection.offsets.at(start) ?? start,
243
+ endOffset: (projection.offsets.at(end - 1) ?? end - 1) + 1,
53
244
  text: projection.text.slice(start, end)
54
245
  }));
55
246
  };
56
- const forEachParagraph = (blocks, visit) => {
247
+ const replacementForProjection = ({ projection, start, end }) => {
248
+ if (projection.hiddenContentBarriers.some((offset) => offset > start && offset < end)) return {
249
+ status: "blocked",
250
+ reason: "hidden-content"
251
+ };
252
+ if ((projection.fieldTextRanges ?? []).some((range) => range.start < end && range.end > start)) return {
253
+ status: "blocked",
254
+ reason: "field-result"
255
+ };
256
+ if ((projection.atomicContentRanges ?? []).some((range) => range.start < end && range.end > start)) return {
257
+ status: "blocked",
258
+ reason: "atomic-content"
259
+ };
260
+ if ((projection.replacementBarriers ?? []).some((offset) => offset > start && offset < end)) return {
261
+ status: "blocked",
262
+ reason: "atomic-content"
263
+ };
264
+ for (let offset = start + 1; offset < end; offset++) if ((projection.offsets.at(offset) ?? offset) !== (projection.offsets.at(offset - 1) ?? offset - 1) + 1) return {
265
+ status: "blocked",
266
+ reason: "hidden-content"
267
+ };
268
+ return { status: "allowed" };
269
+ };
270
+ const forEachParagraph = (blocks, visit, displayMode) => {
57
271
  let paragraphIndex = 0;
58
- const walkBlocks = (items) => {
272
+ const walkBlocks = (items, visible = true) => {
59
273
  for (const block of items) {
60
274
  if (isParagraph(block)) {
61
- visit(block, paragraphIndex);
275
+ visit(block, paragraphIndex, visible);
62
276
  paragraphIndex++;
63
277
  continue;
64
278
  }
65
279
  if (isTable(block)) {
66
- walkTable(block);
280
+ walkTable(block, visible);
67
281
  continue;
68
282
  }
69
- if (isBlockContainer(block)) walkBlocks(block.content);
283
+ if (isBlockContainer(block)) walkBlocks(block.content, visible);
70
284
  }
71
285
  };
72
- const walkTable = (table) => {
286
+ const walkTable = (table, visible) => {
73
287
  for (const row of table.rows) {
74
288
  if (!isTableRow(row)) continue;
75
- for (const cell of row.cells) if (isTableCell(cell)) walkBlocks(cell.content);
289
+ for (const cell of row.cells) if (isTableCell(cell)) walkBlocks(cell.content, visible && isVisibleStructure(row.structuralChange, displayMode) && isVisibleStructure(cell.structuralChange, displayMode));
76
290
  }
77
291
  };
78
292
  walkBlocks(blocks);
79
293
  };
294
+ const isVisibleStructure = (change, mode) => {
295
+ if (mode === void 0 || mode === "all-markup" || !change) return true;
296
+ let kind;
297
+ switch (change.type) {
298
+ case "tableRowInsertion":
299
+ case "tableCellInsertion":
300
+ kind = "ins";
301
+ break;
302
+ case "tableRowDeletion":
303
+ case "tableCellDeletion":
304
+ kind = "del";
305
+ break;
306
+ case "tableCellMerge":
307
+ kind = "merge";
308
+ break;
309
+ default: return change;
310
+ }
311
+ return isTableCellRetainedInReviewView(kind, mode === "original" ? "original" : "final");
312
+ };
313
+ const PAGE_DEPENDENT_SEARCH_FIELDS = /* @__PURE__ */ new Set([
314
+ "PAGE",
315
+ "NUMPAGES",
316
+ "SECTIONPAGES",
317
+ "PAGEREF"
318
+ ]);
319
+ const isPageDependentSearchField = (instruction) => PAGE_DEPENDENT_SEARCH_FIELDS.has(parseFieldType(instruction));
80
320
  const joinSearchProjections = (parts) => {
81
321
  let text = "";
82
322
  const pageBreakOffsets = [];
323
+ const replacementBarriers = [];
324
+ const atomicContentRanges = [];
325
+ const fieldTextRanges = [];
326
+ const excludedTextRanges = [];
83
327
  for (const part of parts) {
84
328
  const baseOffset = text.length;
85
329
  text += part.text;
330
+ for (const offset of part.replacementBarriers ?? []) replacementBarriers.push(baseOffset + offset);
331
+ for (const range of part.atomicContentRanges ?? []) atomicContentRanges.push({
332
+ start: baseOffset + range.start,
333
+ end: baseOffset + range.end
334
+ });
335
+ for (const range of part.fieldTextRanges ?? []) fieldTextRanges.push({
336
+ start: baseOffset + range.start,
337
+ end: baseOffset + range.end
338
+ });
339
+ for (const range of part.excludedTextRanges ?? []) excludedTextRanges.push({
340
+ start: baseOffset + range.start,
341
+ end: baseOffset + range.end
342
+ });
86
343
  for (const offset of part.pageBreakOffsets) pageBreakOffsets.push(baseOffset + offset);
87
344
  }
88
345
  return {
89
346
  text,
90
- pageBreakOffsets
347
+ pageBreakOffsets,
348
+ excludedTextRanges,
349
+ fieldTextRanges,
350
+ atomicContentRanges,
351
+ replacementBarriers
91
352
  };
92
353
  };
93
354
  const getRunSearchProjection = (run) => {
94
355
  let text = "";
95
356
  const pageBreakOffsets = [];
96
- for (const item of run.content) if (item.type === "text") text += item.text;
97
- else if (item.type === "tab") text += " ";
98
- else if (item.type === "break" && (item.breakType === void 0 || item.breakType === "textWrapping")) text += "\n";
99
- else if (item.type === "break" && item.breakType === "page") pageBreakOffsets.push(text.length);
357
+ const replacementBarriers = [];
358
+ const atomicContentRanges = [];
359
+ for (const item of run.content) switch (item.type) {
360
+ case "preservedXml":
361
+ if (!item.text) replacementBarriers.push(text.length);
362
+ atomicContentRanges.push({
363
+ start: text.length,
364
+ end: text.length + item.text.length
365
+ });
366
+ text += item.text;
367
+ break;
368
+ case "text":
369
+ text += item.text;
370
+ break;
371
+ case "symbol": {
372
+ const character = decodeOoxmlSymbolCharacter(item.char);
373
+ if (character) text += character;
374
+ else replacementBarriers.push(text.length);
375
+ break;
376
+ }
377
+ case "tab":
378
+ text += " ";
379
+ break;
380
+ case "softHyphen":
381
+ case "noBreakHyphen":
382
+ text += HYPHEN_TEXT_CARRIERS[item.type];
383
+ break;
384
+ case "break":
385
+ if (item.breakType === "page") pageBreakOffsets.push(text.length);
386
+ else text += "\n";
387
+ break;
388
+ case "footnoteRef":
389
+ case "endnoteRef":
390
+ case "drawing":
391
+ case "shape":
392
+ case "noteMarker":
393
+ case "fieldChar":
394
+ case "instrText":
395
+ case "renderedPageBreak":
396
+ replacementBarriers.push(text.length);
397
+ break;
398
+ default: panic(`Unsupported run content: ${JSON.stringify(item)}`);
399
+ }
100
400
  return {
101
401
  text,
102
- pageBreakOffsets
402
+ pageBreakOffsets,
403
+ atomicContentRanges,
404
+ replacementBarriers
103
405
  };
104
406
  };
105
- const getHyperlinkSearchProjection = (hyperlink) => joinSearchProjections(hyperlink.children.flatMap((child) => {
106
- if (child.type === "run") return [getRunSearchProjection(child)];
107
- return child.type === "inlineWrapper" ? [getParagraphContentSearchProjection(child)] : [];
108
- }));
407
+ const getHyperlinkSearchProjection = (hyperlink) => joinSearchProjections(hyperlink.children.map(getParagraphContentSearchProjection));
109
408
  /** Nothing on the line: a marker, or markup with no text under it. */
110
409
  const noSearchText = () => ({
111
410
  text: "",
112
- pageBreakOffsets: []
411
+ pageBreakOffsets: [],
412
+ replacementBarriers: [0]
113
413
  });
114
414
  /**
115
415
  * The text one paragraph content item puts on the line, for searching.
@@ -134,13 +434,37 @@ const getParagraphContentSearchProjection = (content) => {
134
434
  case "run": return getRunSearchProjection(content);
135
435
  case "hyperlink": return getHyperlinkSearchProjection(content);
136
436
  case "inlineSdt":
137
- case "simpleField":
138
437
  case "inlineWrapper":
139
438
  case "insertion":
140
439
  case "deletion":
141
440
  case "moveFrom":
142
441
  case "moveTo": return joinSearchProjections(content.content.map(getParagraphContentSearchProjection));
143
- case "complexField": return joinSearchProjections(content.fieldResult.map(getRunSearchProjection));
442
+ case "simpleField":
443
+ case "complexField": {
444
+ const projection = joinSearchProjections(content.type === "simpleField" ? content.content.map(getParagraphContentSearchProjection) : content.fieldResult.map(getRunSearchProjection));
445
+ const part = {
446
+ ...projection,
447
+ fieldTextRanges: [{
448
+ start: 0,
449
+ end: projection.text.length
450
+ }]
451
+ };
452
+ return isPageDependentSearchField(content.instruction) ? {
453
+ ...part,
454
+ excludedTextRanges: [{
455
+ start: 0,
456
+ end: part.text.length
457
+ }]
458
+ } : part;
459
+ }
460
+ case "preservedInline": return {
461
+ text: content.text,
462
+ pageBreakOffsets: [],
463
+ atomicContentRanges: [{
464
+ start: 0,
465
+ end: content.text.length
466
+ }]
467
+ };
144
468
  case "bookmarkStart":
145
469
  case "bookmarkEnd":
146
470
  case "commentRangeStart":
@@ -150,12 +474,10 @@ const getParagraphContentSearchProjection = (content) => {
150
474
  case "moveFromRangeEnd":
151
475
  case "moveToRangeStart":
152
476
  case "moveToRangeEnd":
153
- case "mathEquation":
154
- case "preservedInline": return noSearchText();
477
+ case "mathEquation": return noSearchText();
155
478
  default: panic(`Unsupported paragraph content: ${JSON.stringify(content)}`);
156
479
  }
157
480
  };
158
- const getParagraphSearchProjection = (paragraph) => joinSearchProjections(paragraph.content.map(getParagraphContentSearchProjection));
159
481
  const findContentIndexAtOffset = (paragraph, offset) => {
160
482
  let currentOffset = 0;
161
483
  for (let contentIndex = 0; contentIndex < paragraph.content.length; contentIndex++) {
@@ -174,4 +496,4 @@ const isTableRow = (value) => isRecord(value) && Array.isArray(value["cells"]);
174
496
  const isTableCell = (value) => isRecord(value) && Array.isArray(value["content"]);
175
497
  const isBlockContainer = (value) => isRecord(value) && (value["type"] === "blockSdt" || value["type"] === "blockCustomXml") && Array.isArray(value["content"]);
176
498
  //#endregion
177
- export { createDefaultFindOptions, createSearchPattern, escapeRegexString, findAllMatches, findInDocument, findInParagraph };
499
+ export { createDefaultFindOptions, createSearchPattern, escapeRegexString, findAllMatches, findInDocument, findInParagraph, getFindParagraphText, isPageDependentSearchField, replaceEligibility };
@@ -0,0 +1,29 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ import { FindOptions, FindSearchContext } from "./findReplace.js";
3
+ //#region src/utils/findSearchKey.d.ts
4
+ /** Every matching option must make an explicit decision in the search key. */
5
+ declare const createFindQueryKey: (query: string, options: FindOptions) => string;
6
+ type FindSearchKeyOptions = {
7
+ query: string;
8
+ options: FindOptions;
9
+ document: document_d_exports.Document;
10
+ /** Immutable editor document identity, changed by every body transaction. */
11
+ bodyVersion?: object;
12
+ context?: FindSearchContext;
13
+ };
14
+ /**
15
+ * Getters can return fresh model clones. Keep the immutable body version and
16
+ * snapshot package stories separately: secondary edits do not change that body.
17
+ * Story enumeration also binds this snapshot to the package's story resolver.
18
+ */
19
+ declare const createFindSearchKey: ({ query, options, document, bodyVersion, context }: FindSearchKeyOptions) => {
20
+ query: string;
21
+ bodyVersion: string | object;
22
+ documentVersion: string;
23
+ storyScope: string;
24
+ };
25
+ declare const sameFindSearchKey: (previous: ReturnType<typeof createFindSearchKey> | null, current: ReturnType<typeof createFindSearchKey>) => boolean;
26
+ /** Secondary edits cannot acknowledge a pending body replacement. */
27
+ declare const sameFindBodyVersion: (previous: ReturnType<typeof createFindSearchKey>, current: ReturnType<typeof createFindSearchKey>) => boolean;
28
+ //#endregion
29
+ export { createFindQueryKey, createFindSearchKey, sameFindBodyVersion, sameFindSearchKey };
@@ -0,0 +1,34 @@
1
+ import { documentStories, storyBody } from "@stll/docx-core/ops";
2
+ //#region src/utils/findSearchKey.ts
3
+ /** Every matching option must make an explicit decision in the search key. */
4
+ const createFindQueryKey = (query, options) => {
5
+ const matchingOptions = {
6
+ matchCase: options.matchCase,
7
+ matchWholeWord: options.matchWholeWord,
8
+ useRegex: options.useRegex ?? false,
9
+ displayMode: options.displayMode ?? "all-markup",
10
+ includeComments: options.includeComments ?? false
11
+ };
12
+ return JSON.stringify([query, matchingOptions]);
13
+ };
14
+ /**
15
+ * Getters can return fresh model clones. Keep the immutable body version and
16
+ * snapshot package stories separately: secondary edits do not change that body.
17
+ * Story enumeration also binds this snapshot to the package's story resolver.
18
+ */
19
+ const createFindSearchKey = ({ query, options, document, bodyVersion, context }) => ({
20
+ query: createFindQueryKey(query, options),
21
+ bodyVersion: bodyVersion ?? JSON.stringify(document.package.document.content),
22
+ documentVersion: JSON.stringify([
23
+ documentStories(document).filter((story) => story !== "main").map((story) => [story, storyBody(document, story)]),
24
+ document.package.document.comments ?? [],
25
+ document.package.document.sections?.map(({ properties }) => properties) ?? []
26
+ ]),
27
+ storyScope: JSON.stringify(context?.renderedStories?.map((story) => JSON.stringify(story)).toSorted() ?? null)
28
+ });
29
+ const sameFindSearchKey = (previous, current) => previous !== null && previous.query === current.query && sameFindDocumentVersion(previous, current) && previous.storyScope === current.storyScope;
30
+ const sameFindDocumentVersion = (previous, current) => sameFindBodyVersion(previous, current) && previous.documentVersion === current.documentVersion;
31
+ /** Secondary edits cannot acknowledge a pending body replacement. */
32
+ const sameFindBodyVersion = (previous, current) => previous.bodyVersion === current.bodyVersion;
33
+ //#endregion
34
+ export { createFindQueryKey, createFindSearchKey, sameFindBodyVersion, sameFindSearchKey };