@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
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "ההתאמה הקודמת",
363
363
  "previousShortcut": "ההתאמה הקודמת (Shift+Enter)",
364
364
  "replaceAll": "החלף הכל",
365
+ "replaceAllSummary": "הוחלפו {replacedCount}, דולגו {skippedCount}",
365
366
  "replaceCurrent": "החלף את התוצאה הנוכחית",
366
367
  "replacePlaceholder": "טקסט חלופי...",
367
368
  "replaceText": "טקסט חלופי",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "पिछला मिलान",
363
363
  "previousShortcut": "पिछला मिलान (Shift+Enter)",
364
364
  "replaceAll": "सभी बदलें",
365
+ "replaceAllSummary": "{replacedCount} बदले गए, {skippedCount} छोड़े गए",
365
366
  "replaceCurrent": "वर्तमान मिलान को बदलें",
366
367
  "replacePlaceholder": "बदलने वाला पाठ...",
367
368
  "replaceText": "बदलने वाला पाठ",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Előző találat",
363
363
  "previousShortcut": "Előző találat (Shift+Enter)",
364
364
  "replaceAll": "Összes cseréje",
365
+ "replaceAllSummary": "Cserélve: {replacedCount}, kihagyva: {skippedCount}",
365
366
  "replaceCurrent": "Jelenlegi találat cseréje",
366
367
  "replacePlaceholder": "Csereszöveg…",
367
368
  "replaceText": "Csereszöveg",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Ankstesnis atitikmuo",
363
363
  "previousShortcut": "Ankstesnis atitikmuo (Shift+Enter)",
364
364
  "replaceAll": "Pakeisti viską",
365
+ "replaceAllSummary": "Pakeista: {replacedCount}, praleista: {skippedCount}",
365
366
  "replaceCurrent": "Pakeisti dabartinį atitikmenį",
366
367
  "replacePlaceholder": "Pakeičiantysis tekstas…",
367
368
  "replaceText": "Pakeičiantysis tekstas",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Iepriekšējā atbilstība",
363
363
  "previousShortcut": "Iepriekšējā atbilstība (Shift+Enter)",
364
364
  "replaceAll": "Aizstāt visu",
365
+ "replaceAllSummary": "Aizstāti: {replacedCount}, izlaisti: {skippedCount}",
365
366
  "replaceCurrent": "Aizstāt pašreizējo atbilstību",
366
367
  "replacePlaceholder": "Aizstājējteksts…",
367
368
  "replaceText": "Aizstājējteksts",
@@ -363,6 +363,7 @@ type Messages = {
363
363
  "previous": "Previous match";
364
364
  "previousShortcut": "Previous match (Shift+Enter)";
365
365
  "replaceAll": "Replace all";
366
+ "replaceAllSummary": "{replacedCount} replaced, {skippedCount} skipped";
366
367
  "replaceCurrent": "Replace current match";
367
368
  "replacePlaceholder": "Replacement text...";
368
369
  "replaceText": "Replacement text";
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Poprzednie wystąpienie",
363
363
  "previousShortcut": "Poprzednie wystąpienie (Shift+Enter)",
364
364
  "replaceAll": "Zamień wszystko",
365
+ "replaceAllSummary": "Zastąpiono: {replacedCount}, pominięto: {skippedCount}",
365
366
  "replaceCurrent": "Zamień bieżące wystąpienie",
366
367
  "replacePlaceholder": "Tekst zamienny...",
367
368
  "replaceText": "Tekst zamienny",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Ocorrência anterior",
363
363
  "previousShortcut": "Ocorrência anterior (Shift+Enter)",
364
364
  "replaceAll": "Substituir tudo",
365
+ "replaceAllSummary": "{replacedCount} substituídos, {skippedCount} ignorados",
365
366
  "replaceCurrent": "Substituir correspondência atual",
366
367
  "replacePlaceholder": "Texto de substituição...",
367
368
  "replaceText": "Texto de substituição",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Predchádzajúci výskyt",
363
363
  "previousShortcut": "Predchádzajúci výskyt (Shift+Enter)",
364
364
  "replaceAll": "Nahradiť všetko",
365
+ "replaceAllSummary": "Nahradené: {replacedCount}, preskočené: {skippedCount}",
365
366
  "replaceCurrent": "Nahradiť aktuálny výskyt",
366
367
  "replacePlaceholder": "Text náhrady...",
367
368
  "replaceText": "Text náhrady",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "Önceki eşleşme",
363
363
  "previousShortcut": "Önceki eşleşme (Shift+Enter)",
364
364
  "replaceAll": "Tümünü değiştir",
365
+ "replaceAllSummary": "{replacedCount} değiştirildi, {skippedCount} atlandı",
365
366
  "replaceCurrent": "Mevcut eşleşmeyi değiştir",
366
367
  "replacePlaceholder": "Yeni metin...",
367
368
  "replaceText": "Yeni metin",
@@ -362,6 +362,7 @@ const messages = { "folio": {
362
362
  "previous": "上一个匹配项",
363
363
  "previousShortcut": "上一个匹配项 (Shift+Enter)",
364
364
  "replaceAll": "全部替换",
365
+ "replaceAllSummary": "已替换 {replacedCount} 项,已跳过 {skippedCount} 项",
365
366
  "replaceCurrent": "替换当前匹配项",
366
367
  "replacePlaceholder": "替换为...",
367
368
  "replaceText": "替换文本",
@@ -1,7 +1,7 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
+ import { NoteStoryKey } from "../../types/editor-story.js";
2
3
  import { FlowBlock, Measure, NoteAreaLayout } from "../../layout-engine/types.js";
3
4
  import { ConvertFootnoteOptions, MeasureBlocksFn } from "./footnoteLayout.js";
4
- import { NoteStoryKey } from "../../types/editor-story.js";
5
5
  //#region src/layout-bridge/convert/endnoteLayout.d.ts
6
6
  /** One endnote area and the body position it follows. */
7
7
  type EndnoteArea = {
@@ -58,7 +58,6 @@ function prepareEndnoteAreas(input) {
58
58
  notesBySection.set(section, noteIds);
59
59
  }
60
60
  const separatorBlockIds = /* @__PURE__ */ new Map();
61
- const contentBlockIds = /* @__PURE__ */ new Set();
62
61
  const noteStoryByBlockId = /* @__PURE__ */ new Map();
63
62
  const areas = [];
64
63
  for (const [section, noteIds] of [...notesBySection].sort(([left], [right]) => left - right)) {
@@ -82,7 +81,6 @@ function prepareEndnoteAreas(input) {
82
81
  const id = `endnote-${String(noteId)}-${String(index)}`;
83
82
  blocks.push(withId(stripFlowBlockPmAnchors(block), id));
84
83
  measures.push(content.measures[index]);
85
- contentBlockIds.add(id);
86
84
  noteStoryByBlockId.set(id, {
87
85
  kind: "endnote",
88
86
  noteId
@@ -101,7 +99,7 @@ function prepareEndnoteAreas(input) {
101
99
  areas,
102
100
  noteAreas: {
103
101
  separatorBlockIds,
104
- contentBlockIds,
102
+ noteStoryByBlockId,
105
103
  continuationSeparator
106
104
  },
107
105
  noteStoryByBlockId
@@ -4,7 +4,7 @@ import { layoutImage } from "./imageLayout.js";
4
4
  import { calculateChainHeight, computeKeepNextChains, getMidChainIndices, hasKeepLines, hasPageBreakBefore } from "./keep-together.js";
5
5
  import "./layoutFlowShared.js";
6
6
  import { measuredLineAdvance } from "./lineFlow.js";
7
- import { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation } from "./noteAreaFlow.js";
7
+ import { annotateNoteAreas, continuesNoteArea, createNoteAreaContinuation } from "./noteAreaFlow.js";
8
8
  import { createPaginator } from "./paginator.js";
9
9
  import { layoutParagraph } from "./paragraphLayout.js";
10
10
  import { paragraphsShareStyle, resolveEffectiveParagraphSpacingTree } from "./paragraphSpacing.js";
@@ -63,7 +63,7 @@ function applyContextualSpacing(blocks) {
63
63
  */
64
64
  function layoutDocument(blocks, measures, options) {
65
65
  const initialLayout = layoutDocumentPass(blocks, measures, options);
66
- return annotateNoteSeparators(applySectionVerticalAlignment(options.footnoteHeightById ? reflowFootnoteColumns({
66
+ return annotateNoteAreas(applySectionVerticalAlignment(options.footnoteHeightById ? reflowFootnoteColumns({
67
67
  initialLayout,
68
68
  ...options.footnoteReservedHeights ? { initialReserveFloors: options.footnoteReservedHeights } : {},
69
69
  runLayout: (reserveFloors) => layoutDocumentPass(blocks, measures, {
@@ -10,10 +10,10 @@ declare function createNoteAreaContinuation(noteAreas: NoteAreaLayout | undefine
10
10
  /** Whether laying out `block` continues a note area onto any page it opens. */
11
11
  declare function continuesNoteArea(noteAreas: NoteAreaLayout | undefined, block: FlowBlock): boolean;
12
12
  /**
13
- * Record where each separator mark landed. A rule is centred on the first line
14
- * of the separator paragraph: a `w:separator` spans part of the column and a
13
+ * Record note identities and where each separator mark landed. A rule is centred
14
+ * on the first line of the separator paragraph: a `w:separator` spans part of the column and a
15
15
  * `w:continuationSeparator` all of it.
16
16
  */
17
- declare function annotateNoteSeparators(layout: Layout, blocks: readonly FlowBlock[], measures: readonly Measure[], noteAreas: NoteAreaLayout | undefined): Layout;
17
+ declare function annotateNoteAreas(layout: Layout, blocks: readonly FlowBlock[], measures: readonly Measure[], noteAreas: NoteAreaLayout | undefined): Layout;
18
18
  //#endregion
19
- export { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation };
19
+ export { annotateNoteAreas, continuesNoteArea, createNoteAreaContinuation };
@@ -39,15 +39,15 @@ function createNoteAreaContinuation(noteAreas, paginator) {
39
39
  }
40
40
  /** Whether laying out `block` continues a note area onto any page it opens. */
41
41
  function continuesNoteArea(noteAreas, block) {
42
- return noteAreas?.contentBlockIds.has(String(block.id)) === true;
42
+ return noteAreas?.noteStoryByBlockId.has(String(block.id)) === true;
43
43
  }
44
44
  /**
45
- * Record where each separator mark landed. A rule is centred on the first line
46
- * of the separator paragraph: a `w:separator` spans part of the column and a
45
+ * Record note identities and where each separator mark landed. A rule is centred
46
+ * on the first line of the separator paragraph: a `w:separator` spans part of the column and a
47
47
  * `w:continuationSeparator` all of it.
48
48
  */
49
- function annotateNoteSeparators(layout, blocks, measures, noteAreas) {
50
- if (noteAreas === void 0 || noteAreas.separatorBlockIds.size === 0) return layout;
49
+ function annotateNoteAreas(layout, blocks, measures, noteAreas) {
50
+ if (noteAreas === void 0) return layout;
51
51
  const measureById = /* @__PURE__ */ new Map();
52
52
  for (const [index, block] of blocks.entries()) {
53
53
  const measure = measures[index];
@@ -56,6 +56,12 @@ function annotateNoteSeparators(layout, blocks, measures, noteAreas) {
56
56
  const continuation = noteAreas.continuationSeparator;
57
57
  if (continuation !== void 0) measureById.set(String(continuation.block.id), continuation.measure);
58
58
  for (const page of layout.pages) {
59
+ const endnoteIds = /* @__PURE__ */ new Set();
60
+ for (const fragment of page.fragments) {
61
+ const story = noteAreas.noteStoryByBlockId.get(String(fragment.blockId));
62
+ if (story?.kind === "endnote") endnoteIds.add(story.noteId);
63
+ }
64
+ if (endnoteIds.size > 0) page.endnoteIds = [...endnoteIds];
59
65
  const rules = [];
60
66
  for (const fragment of page.fragments) {
61
67
  if (fragment.kind !== "paragraph" || fragment.fromLine !== 0) continue;
@@ -80,4 +86,4 @@ function separatorWidthFraction(kind) {
80
86
  }
81
87
  }
82
88
  //#endregion
83
- export { annotateNoteSeparators, continuesNoteArea, createNoteAreaContinuation };
89
+ export { annotateNoteAreas, continuesNoteArea, createNoteAreaContinuation };
@@ -1,4 +1,5 @@
1
1
  import { OutlineStyleAttr } from "../types/documentEnumValues.js";
2
+ import { NoteStoryKey } from "../types/editor-story.js";
2
3
  import { CssBorderStyle } from "../utils/borderCss.js";
3
4
  import { ImagePosition, ImageWrap, InlineWrapper, NumberFormat, OutlineLevel, ParagraphNumberingOverride, PreviewDescriptor, SdtProperties, SdtType, SectionStart, ShapeTextBody, TableCellFormatting, TableWidthType, UnderlineStyle, VerticalAlign } from "@stll/docx-core/model";
4
5
  declare namespace types_d_exports {
@@ -1268,6 +1269,8 @@ type Page = {
1268
1269
  headerFooterRefs?: PageHeaderFooterRefs;
1269
1270
  /** Footnote IDs that appear on this page (for rendering). */
1270
1271
  footnoteIds?: number[];
1272
+ /** Endnote IDs whose content fragments appear on this page. */
1273
+ endnoteIds?: number[];
1271
1274
  /** Height reserved for the footnote area at page bottom (pixels). */
1272
1275
  footnoteReservedHeight?: number;
1273
1276
  /**
@@ -1436,10 +1439,10 @@ type NoteAreaLayout = {
1436
1439
  */
1437
1440
  separatorBlockIds: ReadonlyMap<string, NoteSeparatorKind>;
1438
1441
  /**
1439
- * Note content blocks. A page opened while one of them is laid out
1442
+ * Note identities keyed by content block. A page opened while one is laid out
1440
1443
  * continues the area, so it starts with the continuation separator.
1441
1444
  */
1442
- contentBlockIds: ReadonlySet<string>;
1445
+ noteStoryByBlockId: ReadonlyMap<string, NoteStoryKey>;
1443
1446
  /** The `w:continuationSeparator` story's first paragraph, already measured. */
1444
1447
  continuationSeparator?: {
1445
1448
  block: ParagraphBlock;
@@ -1,5 +1,5 @@
1
- import { FlowBlock, Layout, Measure } from "../layout-engine/types.js";
2
1
  import { NoteStoryKey } from "../types/editor-story.js";
2
+ import { FlowBlock, Layout, Measure } from "../layout-engine/types.js";
3
3
  import { RenderContext } from "./renderUtils.js";
4
4
  import { BlockFor, FeatureDispatchInput, FeatureFallback, FeatureModule, FeatureRenderInput, FragmentFor, MeasureFor } from "./registry/types.js";
5
5
  import { FeatureRegistry, createFeatureRegistry } from "./registry/registry.js";
@@ -1,5 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
+ import { ReplaceAllSummary, ReplaceEligibility } from "../utils/findReplace.js";
2
3
  import { FindMatchPosition } from "../prosemirror/findReplaceSelection.js";
4
+ import { OpStory } from "@stll/docx-core/ops";
3
5
  //#region src/managers/FindReplaceManager.d.ts
4
6
  type FindDirection = "next" | "previous";
5
7
  type FindReplaceResult<TMatch> = {
@@ -7,16 +9,21 @@ type FindReplaceResult<TMatch> = {
7
9
  totalCount: number;
8
10
  currentIndex: number;
9
11
  };
10
- type ReplaceAllOutcome = {
12
+ type ReplaceAllOutcome = ReplaceAllSummary & {
11
13
  document: document_d_exports.Document;
12
- replacedCount: number;
13
14
  };
14
15
  /**
15
16
  * The cursor index after stepping `direction` over `totalCount` matches,
16
17
  * wrapping at both ends. Returns 0 when there are no matches.
17
18
  */
18
19
  declare const getAdjacentFindIndex: (currentIndex: number, totalCount: number, direction: FindDirection) => number;
19
- declare class FindReplaceManager<TMatch extends FindMatchPosition> {
20
+ type ReplaceableFindPosition = FindMatchPosition & {
21
+ story?: OpStory;
22
+ textBoxIndex?: number;
23
+ commentId?: number;
24
+ replacement: ReplaceEligibility;
25
+ };
26
+ declare class FindReplaceManager<TMatch extends ReplaceableFindPosition> {
20
27
  private result;
21
28
  /** The active find result, or null when there is no active search. */
22
29
  getResult(): FindReplaceResult<TMatch> | null;
@@ -27,7 +34,7 @@ declare class FindReplaceManager<TMatch extends FindMatchPosition> {
27
34
  * first match. The result is kept even when `matches` is empty so callers can
28
35
  * surface a "no results" state.
29
36
  */
30
- setMatches(matches: TMatch[]): FindReplaceResult<TMatch>;
37
+ setMatches(matches: TMatch[], currentIndex?: number): FindReplaceResult<TMatch>;
31
38
  /**
32
39
  * Step the cursor and return the now-current match with its index, or null
33
40
  * when there are no matches to navigate.
@@ -51,8 +58,8 @@ declare class FindReplaceManager<TMatch extends FindMatchPosition> {
51
58
  replaceCurrent(document: document_d_exports.Document, replaceText: string): document_d_exports.Document | null;
52
59
  /**
53
60
  * Replace every match. Matches are replaced from the end of the document
54
- * backwards so earlier offsets stay valid as the text shifts. Clears the
55
- * active result and returns the new document plus the match count, or null
61
+ * backwards so earlier offsets stay valid as the text shifts. Ineligible
62
+ * matches are skipped and counted. Clears the active result, or returns null
56
63
  * when there is nothing to replace.
57
64
  */
58
65
  replaceAll(document: document_d_exports.Document, matches: readonly TMatch[], replaceText: string): ReplaceAllOutcome | null;
@@ -1,3 +1,4 @@
1
+ import { replaceEligibility } from "../utils/findReplace.js";
1
2
  import { replaceTextInDocument } from "../utils/replaceText.js";
2
3
  //#region src/managers/FindReplaceManager.ts
3
4
  /**
@@ -34,11 +35,11 @@ var FindReplaceManager = class {
34
35
  * first match. The result is kept even when `matches` is empty so callers can
35
36
  * surface a "no results" state.
36
37
  */
37
- setMatches(matches) {
38
+ setMatches(matches, currentIndex = 0) {
38
39
  this.result = {
39
40
  matches,
40
41
  totalCount: matches.length,
41
- currentIndex: 0
42
+ currentIndex: Math.max(0, Math.min(currentIndex, matches.length - 1))
42
43
  };
43
44
  return this.result;
44
45
  }
@@ -86,7 +87,7 @@ var FindReplaceManager = class {
86
87
  const result = this.result;
87
88
  if (!result || result.matches.length === 0) return null;
88
89
  const match = result.matches[result.currentIndex];
89
- if (!match) return null;
90
+ if (!match || replaceEligibility(match).status === "blocked") return null;
90
91
  try {
91
92
  return replaceTextInDocument(document, toReplaceRange(match), replaceText);
92
93
  } catch {
@@ -95,26 +96,31 @@ var FindReplaceManager = class {
95
96
  }
96
97
  /**
97
98
  * Replace every match. Matches are replaced from the end of the document
98
- * backwards so earlier offsets stay valid as the text shifts. Clears the
99
- * active result and returns the new document plus the match count, or null
99
+ * backwards so earlier offsets stay valid as the text shifts. Ineligible
100
+ * matches are skipped and counted. Clears the active result, or returns null
100
101
  * when there is nothing to replace.
101
102
  */
102
103
  replaceAll(document, matches, replaceText) {
103
104
  if (matches.length === 0) return null;
104
- const sortedMatches = [...matches].toSorted((a, b) => {
105
+ const eligibleMatches = matches.filter((match) => replaceEligibility(match).status === "allowed");
106
+ let skippedCount = matches.length - eligibleMatches.length;
107
+ let replacedCount = 0;
108
+ const sortedMatches = [...eligibleMatches].toSorted((a, b) => {
105
109
  if (a.paragraphIndex !== b.paragraphIndex) return b.paragraphIndex - a.paragraphIndex;
106
110
  return b.startOffset - a.startOffset;
107
111
  });
108
112
  let nextDocument = document;
109
113
  for (const match of sortedMatches) try {
110
114
  nextDocument = replaceTextInDocument(nextDocument, toReplaceRange(match), replaceText);
115
+ replacedCount++;
111
116
  } catch {
112
- continue;
117
+ skippedCount++;
113
118
  }
114
- this.result = null;
119
+ if (replacedCount > 0) this.result = null;
115
120
  return {
116
121
  document: nextDocument,
117
- replacedCount: matches.length
122
+ replacedCount,
123
+ skippedCount
118
124
  };
119
125
  }
120
126
  };
@@ -8,8 +8,10 @@ type ScrollEditorElementOptions = {
8
8
  behavior?: ScrollBehavior;
9
9
  /** Space to leave between the target and the visible root edge. */
10
10
  margin?: number;
11
+ /** Measured target range when only part of the element should be revealed. */
12
+ rect?: DOMRectReadOnly;
11
13
  };
12
14
  /** Scroll only the marked root, including when the target is inside a scaled viewport. */
13
- declare const scrollEditorElementIntoView: (element: HTMLElement | null, { block, behavior, margin }?: ScrollEditorElementOptions) => void;
15
+ declare const scrollEditorElementIntoView: (element: HTMLElement | null, { block, behavior, margin, rect }?: ScrollEditorElementOptions) => void;
14
16
  //#endregion
15
17
  export { getEditorScrollRoot, scrollEditorBy, scrollEditorElementIntoView, scrollEditorTo };
@@ -25,10 +25,10 @@ const nearestScrollOffset = ({ targetStart, targetEnd, viewportStart, viewportEn
25
25
  return 0;
26
26
  };
27
27
  /** Scroll only the marked root, including when the target is inside a scaled viewport. */
28
- const scrollEditorElementIntoView = (element, { block = "center", behavior = prefersReducedMotionBehavior(), margin = 0 } = {}) => {
28
+ const scrollEditorElementIntoView = (element, { block = "center", behavior = prefersReducedMotionBehavior(), margin = 0, rect } = {}) => {
29
29
  const root = getEditorScrollRoot(element);
30
30
  if (!root || !element) return;
31
- const targetRect = element.getBoundingClientRect();
31
+ const targetRect = rect ?? element.getBoundingClientRect();
32
32
  const rootRect = root.getBoundingClientRect();
33
33
  const visibleTop = rootRect.top + root.clientTop;
34
34
  const targetTop = targetRect.top - visibleTop + root.scrollTop;
@@ -566,6 +566,7 @@ const decodeImage = (source) => {
566
566
  switch (source.format) {
567
567
  case "png": return decodePng(source.bytes);
568
568
  case "jpeg": return decodeJpeg(source.bytes);
569
+ case "svg": return Result.err(new PdfImageError({ message: "SVG images are not supported in PDF" }));
569
570
  default: {
570
571
  const unreachable = source.format;
571
572
  return Result.err(new PdfImageError({ message: `unsupported image format ${String(unreachable)}` }));
@@ -0,0 +1,40 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/prosemirror/commentRangeInlineAtoms.d.ts
3
+ /**
4
+ * Whether an inline atom extends a comment range it sits in.
5
+ *
6
+ * - `extends`: a comment mark on the atom keeps the comment's range alive.
7
+ * - `inert`: the atom is invisible to the reader, so a comment mark on it would
8
+ * be a ghost range nobody can see or select. Comment integrity strips the mark
9
+ * and never counts the atom as range content.
10
+ *
11
+ * This is separate from revision ownership: a cached rendered page break must
12
+ * stay inside its tracked-change wrapper, but it can never hold a comment on
13
+ * its own.
14
+ */
15
+ type CommentRangeInlineAtomParticipation = "extends" | "inert";
16
+ /** Keyed by the tracked-run atom set, which the fixed-point test binds to the schema. */
17
+ declare const COMMENT_RANGE_INLINE_ATOM_PARTICIPATION: Readonly<{
18
+ readonly bookmarkBoundary: "extends";
19
+ readonly commentReference: "inert";
20
+ readonly field: "extends";
21
+ readonly hardBreak: "extends";
22
+ readonly image: "extends";
23
+ readonly math: "extends";
24
+ readonly moveRangeBoundary: "extends";
25
+ readonly noteMarker: "extends";
26
+ readonly pageBreakRun: "extends";
27
+ readonly preservedXml: "extends";
28
+ readonly rangeAnchor: "inert";
29
+ readonly renderedPageBreak: "inert";
30
+ readonly shape: "extends";
31
+ readonly structuredField: "extends";
32
+ readonly symbol: "extends";
33
+ readonly tab: "extends";
34
+ readonly text: "extends";
35
+ readonly textBoxAnchor: "extends";
36
+ }>;
37
+ /** Whether a node is an inline atom that must never carry or extend a comment range. */
38
+ declare const isCommentRangeInertAtom: (node: Node) => boolean;
39
+ //#endregion
40
+ export { COMMENT_RANGE_INLINE_ATOM_PARTICIPATION, CommentRangeInlineAtomParticipation, isCommentRangeInertAtom };
@@ -0,0 +1,33 @@
1
+ import { panic } from "better-result";
2
+ //#region src/prosemirror/commentRangeInlineAtoms.ts
3
+ /** Keyed by the tracked-run atom set, which the fixed-point test binds to the schema. */
4
+ const COMMENT_RANGE_INLINE_ATOM_PARTICIPATION = Object.freeze({
5
+ bookmarkBoundary: "extends",
6
+ commentReference: "inert",
7
+ field: "extends",
8
+ hardBreak: "extends",
9
+ image: "extends",
10
+ math: "extends",
11
+ moveRangeBoundary: "extends",
12
+ noteMarker: "extends",
13
+ pageBreakRun: "extends",
14
+ preservedXml: "extends",
15
+ rangeAnchor: "inert",
16
+ renderedPageBreak: "inert",
17
+ shape: "extends",
18
+ structuredField: "extends",
19
+ symbol: "extends",
20
+ tab: "extends",
21
+ text: "extends",
22
+ textBoxAnchor: "extends"
23
+ });
24
+ const participationByName = new Map(Object.entries(COMMENT_RANGE_INLINE_ATOM_PARTICIPATION));
25
+ /** Whether a node is an inline atom that must never carry or extend a comment range. */
26
+ const isCommentRangeInertAtom = (node) => {
27
+ if (!node.isInline || !node.isAtom) return false;
28
+ const participation = participationByName.get(node.type.name);
29
+ if (!participation) panic(`Inline atom ${JSON.stringify(node.type.name)} has no comment-range participation`);
30
+ return participation === "inert";
31
+ };
32
+ //#endregion
33
+ export { COMMENT_RANGE_INLINE_ATOM_PARTICIPATION, isCommentRangeInertAtom };
@@ -17,6 +17,7 @@ import { createStyleEngine } from "../../style-engine/styleEngine.js";
17
17
  import { normalizeShapeTextAnchor, presetDashForOutlineAttr } from "../../types/documentEnumValues.js";
18
18
  import { presetDashForCssBorderStyle } from "../../utils/borderCss.js";
19
19
  import { canonicalJson } from "../../utils/canonicalJson.js";
20
+ import { getFindParagraphText } from "../../utils/findReplace.js";
20
21
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
21
22
  import { emuToPixels, emuToStrokePixels, pixelsToEmu } from "../../utils/units.js";
22
23
  import { unchangedAlternateContentXml } from "../alternateContentAttrs.js";
@@ -2615,7 +2616,10 @@ const reconcileEmptyFieldResultRuns = (runs, displayRun) => {
2615
2616
  return reconciled;
2616
2617
  };
2617
2618
  const synchronizeFieldDisplayText = (content, displayText, fallbackRun) => {
2618
- let currentText = "";
2619
+ const currentText = getFindParagraphText({
2620
+ type: "paragraph",
2621
+ content
2622
+ });
2619
2623
  const visitRunsIn = (items, visit) => {
2620
2624
  for (const child of items) {
2621
2625
  if (child.type === "run") {
@@ -2628,9 +2632,6 @@ const synchronizeFieldDisplayText = (content, displayText, fallbackRun) => {
2628
2632
  const visitRuns = (visit) => {
2629
2633
  visitRunsIn(content, visit);
2630
2634
  };
2631
- visitRuns((run) => {
2632
- for (const runContent of run.content) if (runContent.type === "text") currentText += runContent.text;
2633
- });
2634
2635
  if (currentText === displayText) return content;
2635
2636
  let replacedText = false;
2636
2637
  visitRuns((run) => {
@@ -31,6 +31,7 @@ import { MOVE_RANGE_BOUNDARY_NODE_NAME } from "../extensions/nodes/MoveRangeBoun
31
31
  import { RANGE_ANCHOR_NODE_NAME } from "../extensions/nodes/RangeAnchorExtension.js";
32
32
  import "../extensions/nodes/SdtExtension.js";
33
33
  import { fieldRequiresStructuredContent } from "../fieldRepresentation.js";
34
+ import { getInlineDisplayText } from "../inlineDisplayText.js";
34
35
  import { inlineWrapperLayer } from "../inlineWrapperStack.js";
35
36
  import { CLEARED_LIST_RENDERING_ATTRS } from "../listMarker.js";
36
37
  import { listRenderingFor } from "../listRendering.js";
@@ -1368,27 +1369,24 @@ function standaloneTableCellToProseMirror(cell, nodeType) {
1368
1369
  });
1369
1370
  }
1370
1371
  function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyperlinkInstanceIndex, runScope, textBoxAnchors }) {
1371
- let displayText = "";
1372
1372
  let fieldFormatting;
1373
1373
  let fieldPropertyChanges;
1374
1374
  const inlineNodes = [];
1375
- const hasStructuredSourceContent = fieldResultHasPageBreakContent(field) || field.type === "simpleField" && field.content.some((content) => content.type === "hyperlink" || content.type === "preservedInline" || content.type === "inlineWrapper" || content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo");
1375
+ const hasPageBreakContent = fieldResultHasPageBreakContent(field);
1376
+ const resultRuns = field.type === "complexField" ? field.fieldResult : field.content.flatMap((content) => content.type === "run" ? [content] : []);
1376
1377
  const appendRun = (run, into) => {
1377
- for (const content of run.content) if (content.type === "text") displayText += content.text;
1378
1378
  fieldFormatting ??= run.formatting;
1379
1379
  fieldPropertyChanges ??= run.propertyChanges;
1380
- if (field.type !== "simpleField" && !hasStructuredSourceContent) return;
1381
1380
  into.push(...convertRun(run, getInheritedRunFormatting(run.formatting, field.fieldType), runScope, styleResolver, textBoxAnchors));
1382
1381
  };
1383
- const collectDisplay = (items) => {
1382
+ const collectFormatting = (items) => {
1384
1383
  for (const item of items) switch (item.type) {
1385
1384
  case "run":
1386
- for (const child of item.content) if (child.type === "text") displayText += child.text;
1387
1385
  fieldFormatting ??= item.formatting;
1388
1386
  fieldPropertyChanges ??= item.propertyChanges;
1389
1387
  break;
1390
1388
  case "hyperlink":
1391
- collectDisplay(item.children);
1389
+ collectFormatting(item.children);
1392
1390
  break;
1393
1391
  case "inlineWrapper":
1394
1392
  case "inlineSdt":
@@ -1396,20 +1394,16 @@ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyp
1396
1394
  case "deletion":
1397
1395
  case "moveFrom":
1398
1396
  case "moveTo":
1399
- collectDisplay(item.content);
1397
+ collectFormatting(item.content);
1400
1398
  break;
1401
1399
  case "simpleField":
1402
- collectDisplay(item.content);
1400
+ collectFormatting(item.content);
1403
1401
  break;
1404
1402
  case "complexField":
1405
- collectDisplay(item.fieldResult);
1403
+ collectFormatting(item.fieldResult);
1406
1404
  break;
1407
1405
  case "preservedInline":
1408
- displayText += item.text;
1409
- break;
1410
- case "mathEquation":
1411
- displayText += item.plainText ?? "";
1412
- break;
1406
+ case "mathEquation": break;
1413
1407
  case "bookmarkStart":
1414
1408
  case "bookmarkEnd":
1415
1409
  case "commentRangeStart":
@@ -1433,7 +1427,6 @@ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyp
1433
1427
  break;
1434
1428
  case "hyperlink":
1435
1429
  for (const { content: child } of withInlineWrapperStacks(content.children)) if (child.type === "run") {
1436
- for (const runContent of child.content) if (runContent.type === "text") displayText += runContent.text;
1437
1430
  fieldFormatting ??= child.formatting;
1438
1431
  fieldPropertyChanges ??= child.propertyChanges;
1439
1432
  }
@@ -1449,7 +1442,7 @@ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyp
1449
1442
  case "deletion":
1450
1443
  case "moveFrom":
1451
1444
  case "moveTo":
1452
- collectDisplay(content.content);
1445
+ collectFormatting(content.content);
1453
1446
  itemNodes.push(...convertTrackedChange(content, content.type === "insertion" || content.type === "moveTo" ? "insertion" : "deletion", nextHyperlinkInstanceIndex, runScope, {
1454
1447
  current: getInheritedRunFormatting,
1455
1448
  historical: getInheritedRunFormatting
@@ -1468,8 +1461,8 @@ function convertField(field, { getInheritedRunFormatting, styleResolver, nextHyp
1468
1461
  styleResolver,
1469
1462
  createMark: runScope.createMark
1470
1463
  });
1471
- const createStructuredField = fieldRequiresStructuredContent(inlineNodes);
1472
- const resultRuns = field.type === "simpleField" ? field.content.flatMap((content) => content.type === "run" ? [content] : []) : field.fieldResult;
1464
+ const createStructuredField = (field.type === "simpleField" || hasPageBreakContent) && fieldRequiresStructuredContent(inlineNodes);
1465
+ const displayText = getInlineDisplayText(schema.node("paragraph", null, inlineNodes));
1473
1466
  const emptyResultRuns = displayText === "" && resultRuns.length > 0 && (field.type === "complexField" || field.content.length === resultRuns.length) && resultRuns.every((run) => run.content.every((item) => item.type === "text" && item.text === "")) ? resultRuns : void 0;
1474
1467
  const resultPropertyChanges = emptyResultRuns ? emptyFieldRunPropertyChanges(emptyResultRuns) : fieldPropertyChanges;
1475
1468
  if (!createStructuredField && resultPropertyChanges && resultPropertyChanges.length > 0) marks.push(schema.mark("runPropertyChange", { changes: [...resultPropertyChanges] }));
@@ -1,7 +1,7 @@
1
+ import { isCommentRangeInertAtom } from "../../commentRangeInlineAtoms.js";
1
2
  import { readCommentReferenceAttrs } from "../../commentReferenceAttrs.js";
2
3
  import { planCommentReferenceRepairs } from "../../commentReferenceIntegrity.js";
3
4
  import { readRangeAnchorAttrs } from "../../rangeAnchorAttrs.js";
4
- import { trackedRunInlineAtomDisposition } from "../../trackedRunInlineAtoms.js";
5
5
  import { createNodeExtension } from "../create.js";
6
6
  import "./RangeAnchorExtension.js";
7
7
  import { Plugin } from "prosemirror-state";
@@ -13,8 +13,8 @@ const surveyCommentReferences = (doc) => {
13
13
  const markEnds = /* @__PURE__ */ new Map();
14
14
  const unanchoredMarks = [];
15
15
  doc.descendants((node, position) => {
16
- const transparent = trackedRunInlineAtomDisposition(node) === "transparent";
17
- if (transparent) {
16
+ const inert = isCommentRangeInertAtom(node);
17
+ if (inert) {
18
18
  for (const mark of node.marks) if (mark.type.name === "comment") unanchoredMarks.push({
19
19
  position,
20
20
  size: node.nodeSize,
@@ -43,7 +43,7 @@ const surveyCommentReferences = (doc) => {
43
43
  }
44
44
  return false;
45
45
  }
46
- if (transparent) return false;
46
+ if (inert) return false;
47
47
  if (!node.isInline) return true;
48
48
  for (const mark of node.marks) {
49
49
  if (mark.type.name !== "comment") continue;