@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,38 +1,33 @@
1
1
  import { cloneDocumentWithParagraphPropertySources } from "../docx/paragraphPropertySource.js";
2
- import { textFormattingToMarks } from "../prosemirror/commands/formatting.js";
3
- import { fromProseDoc } from "../prosemirror/conversion/fromProseDoc.js";
4
- import { toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
5
- import { resolveFindMatchRange } from "../prosemirror/findReplaceSelection.js";
6
- import { schema } from "../prosemirror/schema/index.js";
7
- import { EditorState } from "prosemirror-state";
2
+ import { getFindParagraphText } from "./findReplace.js";
3
+ import { panic } from "better-result";
8
4
  //#region src/utils/replaceText.ts
9
- /**
10
- * Lightweight text replacement on the document model.
11
- * Used by find & replace. Operates immutably.
12
- */
13
5
  /** Replace text within a range in the document. Returns a new document. */
14
6
  function replaceTextInDocument(doc, range, text, formatting) {
15
7
  const newDoc = cloneDocumentWithParagraphPropertySources(doc);
16
8
  const body = newDoc.package.document;
17
9
  const { start, end } = range;
18
10
  if (start.paragraphIndex === end.paragraphIndex) {
19
- const proseDoc = toProseDoc(newDoc);
20
- const proseRange = resolveFindMatchRange(proseDoc, {
21
- paragraphIndex: start.paragraphIndex,
11
+ const paragraph = getParagraphByIndex(body, start.paragraphIndex);
12
+ if (!paragraph) return newDoc;
13
+ replaceTextInParagraph({
14
+ paragraph,
22
15
  startOffset: start.offset,
23
- endOffset: end.offset
16
+ endOffset: end.offset,
17
+ text,
18
+ ...formatting && { formatting }
24
19
  });
25
- if (proseRange === null) return newDoc;
26
- const transaction = EditorState.create({ doc: proseDoc }).tr;
27
- if (text.length > 0 && formatting !== void 0) transaction.replaceWith(proseRange.from, proseRange.to, schema.text(text, textFormattingToMarks(formatting)));
28
- else transaction.insertText(text, proseRange.from, proseRange.to);
29
- return fromProseDoc(transaction.doc, newDoc, { stylesheetSource: { type: "package" } });
30
20
  } else {
31
21
  const startParagraph = getParagraphByIndex(body, start.paragraphIndex);
32
22
  if (!startParagraph) return newDoc;
33
- const startText = getParagraphText(startParagraph);
34
- startParagraph.content = deleteTextInParagraph(startParagraph, start.offset, startText.length);
35
- startParagraph.content = insertTextAtOffset(startParagraph, start.offset, text, formatting);
23
+ const startText = getFindParagraphText(startParagraph);
24
+ replaceTextInParagraph({
25
+ paragraph: startParagraph,
26
+ startOffset: start.offset,
27
+ endOffset: startText.length,
28
+ text,
29
+ ...formatting && { formatting }
30
+ });
36
31
  const paragraphsToRemove = [];
37
32
  for (let i = start.paragraphIndex + 1; i <= end.paragraphIndex; i++) paragraphsToRemove.push(getTopLevelParagraphBlockIndex(body, i));
38
33
  for (let i = paragraphsToRemove.length - 1; i >= 0; i--) {
@@ -74,107 +69,139 @@ function getParagraphByIndex(body, paragraphIndex) {
74
69
  };
75
70
  return walkBlocks(body.content);
76
71
  }
77
- function getParagraphText(paragraph) {
78
- let text = "";
79
- for (const item of paragraph.content) if (item.type === "run") {
80
- for (const c of item.content) if (c.type === "text") text += c.text;
81
- } else if (item.type === "hyperlink") for (const child of item.children) {
82
- if (child.type !== "run") continue;
83
- for (const c of child.content) if (c.type === "text") text += c.text;
84
- }
85
- return text;
86
- }
87
- function deleteTextInParagraph(paragraph, startOffset, endOffset) {
88
- const result = [];
89
- let currentOffset = 0;
90
- for (const item of paragraph.content) if (item.type === "run") {
91
- const newContent = [];
92
- for (const content of item.content) if (content.type === "text") {
93
- const textStart = currentOffset;
94
- const textEnd = currentOffset + content.text.length;
95
- if (textEnd <= startOffset || textStart >= endOffset) newContent.push(content);
96
- else {
97
- let newText = "";
98
- if (textStart < startOffset) newText += content.text.slice(0, startOffset - textStart);
99
- if (textEnd > endOffset) newText += content.text.slice(endOffset - textStart);
100
- if (newText) newContent.push({
101
- ...content,
102
- text: newText
103
- });
104
- }
105
- currentOffset = textEnd;
106
- } else newContent.push(content);
107
- if (newContent.length > 0) result.push({
108
- ...item,
109
- content: newContent
110
- });
111
- } else result.push(item);
112
- return result;
113
- }
114
- function insertTextAtOffset(paragraph, offset, text, formatting) {
115
- if (!text) return paragraph.content;
72
+ const itemTextLength = (item) => getFindParagraphText({
73
+ type: "paragraph",
74
+ content: [item]
75
+ }).length;
76
+ /** Walk authored offsets once, preserving wrappers and opaque source units. */
77
+ const replaceTextInParagraph = ({ paragraph, startOffset, endOffset, text, formatting }) => {
116
78
  let currentOffset = 0;
117
- for (const item of paragraph.content) if (item.type === "run") for (let ci = 0; ci < item.content.length; ci++) {
118
- const content = item.content[ci];
119
- if (content.type === "text") {
120
- const textStart = currentOffset;
121
- const textEnd = currentOffset + content.text.length;
122
- if (offset >= textStart && offset <= textEnd) {
123
- const relOffset = offset - textStart;
124
- if (formatting) {
125
- const before = content.text.slice(0, relOffset);
126
- const after = content.text.slice(relOffset);
127
- const newContent = [];
128
- if (before) newContent.push({
129
- type: "text",
130
- text: before
79
+ let inserted = false;
80
+ const visit = (items) => {
81
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
82
+ const item = items.at(itemIndex);
83
+ if (!item) continue;
84
+ switch (item.type) {
85
+ case "run": {
86
+ const before = [];
87
+ const after = [];
88
+ let splitAtInsertion = false;
89
+ for (const unit of item.content) {
90
+ const unitStart = currentOffset;
91
+ const unitText = getFindParagraphText({
92
+ type: "paragraph",
93
+ content: [{
94
+ type: "run",
95
+ content: [unit]
96
+ }]
97
+ });
98
+ const unitLength = unitText.length;
99
+ const unitEnd = unitStart + unitLength;
100
+ currentOffset = unitEnd;
101
+ const output = splitAtInsertion ? after : before;
102
+ if (unit.type === "preservedXml" || unitLength === 0) {
103
+ output.push(unit);
104
+ continue;
105
+ }
106
+ const overlaps = unitEnd > startOffset && unitStart < endOffset;
107
+ if (!overlaps && !(!inserted && startOffset === endOffset && startOffset >= unitStart && startOffset <= unitEnd)) {
108
+ output.push(unit);
109
+ continue;
110
+ }
111
+ if (unit.type !== "text" && !overlaps) {
112
+ if (startOffset === unitEnd) output.push(unit);
113
+ inserted = true;
114
+ if (formatting) splitAtInsertion = true;
115
+ else if (text) output.push({
116
+ type: "text",
117
+ text
118
+ });
119
+ if (startOffset === unitStart) (splitAtInsertion ? after : before).push(unit);
120
+ continue;
121
+ }
122
+ const prefix = unitText.slice(0, Math.max(0, startOffset - unitStart));
123
+ const suffix = unitText.slice(Math.max(0, endOffset - unitStart));
124
+ if (prefix) output.push({
125
+ type: "text",
126
+ text: prefix
127
+ });
128
+ if (!inserted) {
129
+ inserted = true;
130
+ if (formatting) splitAtInsertion = true;
131
+ else if (text) output.push({
132
+ type: "text",
133
+ text
134
+ });
135
+ }
136
+ if (suffix) (splitAtInsertion ? after : before).push({
137
+ type: "text",
138
+ text: suffix
139
+ });
140
+ }
141
+ if (!splitAtInsertion) {
142
+ item.content = before;
143
+ break;
144
+ }
145
+ const replacement = [];
146
+ if (before.length > 0) replacement.push({
147
+ ...item,
148
+ content: before
131
149
  });
132
- const newRun = {
133
- type: "text",
134
- text
135
- };
136
- item.content.splice(ci, 1, ...newContent);
137
- const insertRun = {
150
+ if (text) replacement.push({
138
151
  type: "run",
139
- content: [newRun],
140
- formatting
141
- };
142
- const afterItems = [];
143
- if (after) afterItems.push({
144
- type: "text",
145
- text: after
152
+ content: [{
153
+ type: "text",
154
+ text
155
+ }],
156
+ ...formatting && { formatting }
146
157
  });
147
- const remaining = item.content.splice(ci + newContent.length);
148
- const afterRun = {
149
- type: "run",
150
- content: [...remaining, ...afterItems],
151
- ...item.formatting !== void 0 ? { formatting: item.formatting } : {}
152
- };
153
- const idx = paragraph.content.indexOf(item);
154
- paragraph.content.splice(idx + 1, 0, insertRun);
155
- if (afterItems.length > 0 || remaining.length > 0) paragraph.content.splice(idx + 2, 0, afterRun);
156
- return paragraph.content;
158
+ if (after.length > 0) replacement.push({
159
+ ...item,
160
+ content: after
161
+ });
162
+ items.splice(itemIndex, 1, ...replacement);
163
+ itemIndex += replacement.length - 1;
164
+ break;
157
165
  }
158
- content.text = content.text.slice(0, relOffset) + text + content.text.slice(relOffset);
159
- return paragraph.content;
166
+ case "hyperlink":
167
+ visit(item.children);
168
+ break;
169
+ case "inlineWrapper":
170
+ case "inlineSdt":
171
+ case "insertion":
172
+ case "deletion":
173
+ case "moveFrom":
174
+ case "moveTo":
175
+ visit(item.content);
176
+ break;
177
+ case "simpleField":
178
+ case "complexField":
179
+ case "preservedInline":
180
+ case "mathEquation":
181
+ case "bookmarkStart":
182
+ case "bookmarkEnd":
183
+ case "commentRangeStart":
184
+ case "commentRangeEnd":
185
+ case "commentReference":
186
+ case "moveFromRangeStart":
187
+ case "moveFromRangeEnd":
188
+ case "moveToRangeStart":
189
+ case "moveToRangeEnd":
190
+ currentOffset += itemTextLength(item);
191
+ break;
192
+ default: panic("Unsupported replacement paragraph content", { content: item });
160
193
  }
161
- currentOffset = textEnd;
162
194
  }
163
- }
164
- const lastRun = paragraph.content.findLast((item) => item.type === "run");
165
- if (lastRun) lastRun.content.push({
166
- type: "text",
167
- text
168
- });
169
- else paragraph.content.push({
195
+ };
196
+ visit(paragraph.content);
197
+ if (!inserted && startOffset === endOffset && startOffset === currentOffset && text) paragraph.content.push({
170
198
  type: "run",
171
199
  content: [{
172
200
  type: "text",
173
201
  text
174
202
  }],
175
- formatting: formatting ?? {}
203
+ ...formatting ? { formatting } : {}
176
204
  });
177
- return paragraph.content;
178
- }
205
+ };
179
206
  //#endregion
180
207
  export { replaceTextInDocument };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stll/folio-core",
3
- "version": "0.61.0",
3
+ "version": "0.62.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",