@stll/folio-core 0.53.0 → 0.54.1

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 (155) hide show
  1. package/dist/ai-edits/apply.js +415 -166
  2. package/dist/ai-edits/batch-claims.d.ts +21 -6
  3. package/dist/ai-edits/batch-claims.js +47 -16
  4. package/dist/ai-edits/headless.d.ts +20 -0
  5. package/dist/ai-edits/headless.js +140 -11
  6. package/dist/ai-edits/pending-suggestions.js +1 -1
  7. package/dist/ai-edits/read.d.ts +5 -3
  8. package/dist/ai-edits/read.js +97 -47
  9. package/dist/ai-edits/revisionStretches.d.ts +17 -0
  10. package/dist/ai-edits/revisionStretches.js +80 -0
  11. package/dist/ai-edits/snapshot.js +3 -2
  12. package/dist/ai-edits/table-cell-mutations.d.ts +3 -1
  13. package/dist/ai-edits/table-cell-mutations.js +2 -1
  14. package/dist/ai-edits/table-row-column-mutations.d.ts +6 -1
  15. package/dist/ai-edits/table-row-column-mutations.js +27 -10
  16. package/dist/compare/content-alignment.d.ts +5 -1
  17. package/dist/compare/content-alignment.js +39 -32
  18. package/dist/compare/inline-provenance.js +1 -1
  19. package/dist/controller/layoutPipeline.js +3 -2
  20. package/dist/docx/archiveInflation.d.ts +127 -0
  21. package/dist/docx/archiveInflation.js +181 -0
  22. package/dist/docx/metadataPrivacy.js +24 -1
  23. package/dist/docx/noteReferenceMark.d.ts +15 -0
  24. package/dist/docx/noteReferenceMark.js +48 -0
  25. package/dist/docx/paragraphParser.js +5 -3
  26. package/dist/docx/paragraphPropertySource.d.ts +1 -1
  27. package/dist/docx/paragraphPropertySource.js +4 -2
  28. package/dist/docx/parser.js +2 -2
  29. package/dist/docx/rezip.js +4 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +3 -1
  31. package/dist/docx/selectiveXmlPatch.js +81 -3
  32. package/dist/docx/server/boundedArchive.d.ts +8 -13
  33. package/dist/docx/server/boundedArchive.js +70 -59
  34. package/dist/docx/server/extractDocxText.d.ts +8 -2
  35. package/dist/docx/server/extractDocxText.js +2 -2
  36. package/dist/docx/server/validateDocxConformance.d.ts +1 -1
  37. package/dist/docx/server/validateDocxConformance.js +1 -0
  38. package/dist/docx/unzip.d.ts +19 -2
  39. package/dist/docx/unzip.js +135 -24
  40. package/dist/docx/verticalMergeProjection.d.ts +17 -0
  41. package/dist/docx/verticalMergeProjection.js +120 -0
  42. package/dist/fonts/embeddedFonts.js +1 -1
  43. package/dist/i18n/messages/catalogs.gen.d.ts +34 -34
  44. package/dist/i18n/messages/catalogs.gen.js +34 -34
  45. package/dist/i18n/messages/messages.gen.d.ts +2 -2
  46. package/dist/internal/wholeStoryRevisionResolution.js +95 -25
  47. package/dist/layout-engine/measure/measureParagraph.js +2 -2
  48. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +12 -1
  49. package/dist/layout-engine/measure/paragraphMeasureShared.js +14 -1
  50. package/dist/markdown/escape.d.ts +2 -2
  51. package/dist/markdown/escape.js +4 -5
  52. package/dist/markdown/renderBlock.js +91 -25
  53. package/dist/markdown/renderRuns.d.ts +10 -1
  54. package/dist/markdown/renderRuns.js +320 -59
  55. package/dist/prosemirror/clearRunColor.d.ts +6 -0
  56. package/dist/prosemirror/clearRunColor.js +46 -0
  57. package/dist/prosemirror/commands/comments.js +62 -22
  58. package/dist/prosemirror/commands/formatPainter.js +1 -1
  59. package/dist/prosemirror/commands/hyperlink.js +2 -1
  60. package/dist/prosemirror/commands/propertyChangeScope.d.ts +3 -2
  61. package/dist/prosemirror/commands/propertyChangeScope.js +28 -4
  62. package/dist/prosemirror/commands/resolveAllTableChanges.js +40 -41
  63. package/dist/prosemirror/commands/resolveParagraphProperties.js +33 -8
  64. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +7 -1
  65. package/dist/prosemirror/commands/tableCellMergeResolution.js +39 -20
  66. package/dist/prosemirror/commands/tableMergeFoldDecisions.d.ts +61 -0
  67. package/dist/prosemirror/commands/tableMergeFoldDecisions.js +79 -0
  68. package/dist/prosemirror/containerFinalParagraph.d.ts +11 -5
  69. package/dist/prosemirror/containerFinalParagraph.js +7 -6
  70. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  71. package/dist/prosemirror/conversion/fromProseDoc.js +12 -590
  72. package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -13
  73. package/dist/prosemirror/conversion/toProseDoc.js +28 -262
  74. package/dist/prosemirror/documentSchema.d.ts +8 -0
  75. package/dist/prosemirror/documentSchema.js +18 -0
  76. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  77. package/dist/prosemirror/extensions/core/ParagraphExtension.js +104 -63
  78. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -1
  79. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +24 -3
  80. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.d.ts +20 -0
  81. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +43 -0
  82. package/dist/prosemirror/extensions/features/ListExtension.js +10 -9
  83. package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +1 -1
  84. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +2 -4
  85. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +14 -8
  86. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +8 -4
  87. package/dist/prosemirror/extensions/marks/StrikeExtension.js +2 -2
  88. package/dist/prosemirror/extensions/marks/SubscriptExtension.js +2 -2
  89. package/dist/prosemirror/extensions/marks/SuperscriptExtension.js +2 -2
  90. package/dist/prosemirror/extensions/marks/TextColorExtension.js +4 -3
  91. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -6
  92. package/dist/prosemirror/extensions/marks/markUtils.js +17 -8
  93. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +3 -1
  94. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +12 -1
  95. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +2 -2
  96. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +5 -3
  97. package/dist/prosemirror/extensions/nodes/TableExtension.js +104 -210
  98. package/dist/prosemirror/extensions/types.d.ts +7 -0
  99. package/dist/prosemirror/hyperlinkRemoval.d.ts +9 -0
  100. package/dist/prosemirror/hyperlinkRemoval.js +129 -0
  101. package/dist/prosemirror/index.d.ts +2 -1
  102. package/dist/prosemirror/index.js +2 -1
  103. package/dist/prosemirror/listNumbering.d.ts +3 -21
  104. package/dist/prosemirror/listNumbering.js +24 -39
  105. package/dist/prosemirror/listRendering.d.ts +29 -0
  106. package/dist/prosemirror/listRendering.js +26 -0
  107. package/dist/prosemirror/markupViewNotes.d.ts +8 -0
  108. package/dist/prosemirror/markupViewNotes.js +49 -0
  109. package/dist/prosemirror/markupViewProjection.d.ts +3 -1
  110. package/dist/prosemirror/markupViewProjection.js +7 -2
  111. package/dist/prosemirror/noteReferenceReview.d.ts +77 -0
  112. package/dist/prosemirror/noteReferenceReview.js +332 -0
  113. package/dist/prosemirror/paragraphIndentation.js +5 -2
  114. package/dist/prosemirror/paragraphMarkJoin.d.ts +6 -4
  115. package/dist/prosemirror/paragraphMarkJoin.js +21 -19
  116. package/dist/prosemirror/paragraphPropertyCarry.d.ts +64 -0
  117. package/dist/prosemirror/paragraphPropertyCarry.js +155 -0
  118. package/dist/prosemirror/plugins/documentStyleState.d.ts +26 -0
  119. package/dist/prosemirror/plugins/documentStyleState.js +32 -0
  120. package/dist/prosemirror/plugins/documentStyles.d.ts +2 -21
  121. package/dist/prosemirror/plugins/documentStyles.js +66 -29
  122. package/dist/prosemirror/plugins/index.d.ts +2 -1
  123. package/dist/prosemirror/plugins/index.js +2 -1
  124. package/dist/prosemirror/plugins/paragraphStyleResolution.d.ts +15 -0
  125. package/dist/prosemirror/plugins/paragraphStyleResolution.js +230 -0
  126. package/dist/prosemirror/plugins/suggestionMode.d.ts +12 -2
  127. package/dist/prosemirror/plugins/suggestionMode.js +387 -28
  128. package/dist/prosemirror/plugins/templateDirectives.d.ts +6 -0
  129. package/dist/prosemirror/plugins/templateDirectives.js +21 -7
  130. package/dist/prosemirror/plugins/templatePreviewValues.js +14 -21
  131. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +22 -2
  132. package/dist/prosemirror/rebaseParagraphRunFormatting.js +39 -13
  133. package/dist/prosemirror/rebaseParagraphRuns.d.ts +36 -0
  134. package/dist/prosemirror/rebaseParagraphRuns.js +99 -0
  135. package/dist/prosemirror/runFormattingFromMarks.d.ts +18 -0
  136. package/dist/prosemirror/runFormattingFromMarks.js +567 -0
  137. package/dist/prosemirror/runFormattingReconciliation.js +10 -9
  138. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  139. package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +59 -0
  140. package/dist/prosemirror/styles/paragraphStyleCascade.js +145 -0
  141. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +21 -2
  142. package/dist/prosemirror/styles/resolvedStyleAttrs.js +38 -1
  143. package/dist/prosemirror/styles/tableStyleRegions.d.ts +31 -0
  144. package/dist/prosemirror/styles/tableStyleRegions.js +55 -0
  145. package/dist/prosemirror/tableCellPaste.d.ts +74 -0
  146. package/dist/prosemirror/tableCellPaste.js +525 -0
  147. package/dist/prosemirror/tableGridMutation.d.ts +53 -14
  148. package/dist/prosemirror/tableGridMutation.js +166 -18
  149. package/dist/prosemirror/tableRunIn.d.ts +51 -0
  150. package/dist/prosemirror/tableRunIn.js +165 -0
  151. package/dist/prosemirror/textInput.js +11 -2
  152. package/dist/prosemirror/trackedRevisionPath.d.ts +17 -0
  153. package/dist/prosemirror/trackedRevisionPath.js +32 -0
  154. package/dist/server.d.ts +2 -2
  155. package/package.json +6 -6
@@ -5,6 +5,7 @@ import { wrapComment, wrapDeletion, wrapInsertion, wrapMoveFrom, wrapMoveTo } fr
5
5
  import { escapeAltText, escapeInline, escapeLinkUrl } from "./escape.js";
6
6
  import { registerImage } from "./images.js";
7
7
  import { numberNoteReference, pushWarning } from "./internals.js";
8
+ import { compileMarkdownToContent } from "@stll/docx-core";
8
9
  //#region src/markdown/renderRuns.ts
9
10
  const MARK_DELIMS = {
10
11
  bold: "**",
@@ -32,9 +33,9 @@ function marksFor(run) {
32
33
  const f = run.formatting;
33
34
  if (!f) return [];
34
35
  const out = [];
36
+ if (f.strike) out.push("strike");
35
37
  if (f.bold) out.push("bold");
36
38
  if (f.italic) out.push("italic");
37
- if (f.strike) out.push("strike");
38
39
  const ascii = f.fontFamily?.ascii?.toLowerCase();
39
40
  if (ascii && MONOSPACE_FONTS.has(ascii)) out.push("code");
40
41
  return out;
@@ -42,7 +43,7 @@ function marksFor(run) {
42
43
  function applyMarks(text, marks) {
43
44
  if (!text) return text;
44
45
  if (marks.includes("code")) {
45
- const literal = text.replace(/\\(?<char>[\\`*[\]_<])/gu, "$<char>");
46
+ const literal = text.replace(/\\(?<char>[\\`*[\]_<~])/gu, "$<char>");
46
47
  let longestRun = 0;
47
48
  for (const m of literal.matchAll(/`+/gu)) longestRun = Math.max(longestRun, m[0].length);
48
49
  const fence = "`".repeat(longestRun + 1);
@@ -56,6 +57,133 @@ function applyMarks(text, marks) {
56
57
  }
57
58
  return out;
58
59
  }
60
+ const UNKNOWN_NEIGHBORS = {
61
+ before: void 0,
62
+ after: void 0
63
+ };
64
+ const isBlank = (ch) => ch === "" || ch !== void 0 && /\s/u.test(ch);
65
+ /**
66
+ * Which mark wraps a stretch of runs first, outermost first. Strikethrough
67
+ * goes inside emphasis: an emphasis delimiter beside a tilde opens and closes
68
+ * wherever it stands, where the other way round (`~~*`) needs a blank or
69
+ * punctuation outside.
70
+ */
71
+ const GROUPING_ORDER = [
72
+ "italic",
73
+ "bold",
74
+ "strike"
75
+ ];
76
+ /** The stackings tried, in turn, where the reader misreads one. */
77
+ const GROUPING_ORDERS = [GROUPING_ORDER, [
78
+ "bold",
79
+ "italic",
80
+ "strike"
81
+ ]];
82
+ /**
83
+ * CommonMark punctuation (Unicode P and S categories) as a delimiter of
84
+ * `mark` meets it. Beside an emphasis delimiter, an asterisk is another
85
+ * delimiter that runs together with it, and a tilde does not let it close
86
+ * (the GFM reader's rule): both count as letters. (Words carry them escaped,
87
+ * as a two-character unit.)
88
+ */
89
+ const punctuationFor = (mark) => (ch) => {
90
+ if (ch === void 0 || ch === "" || ch === "~") return false;
91
+ if (mark !== "strike" && ch === "*") return false;
92
+ return /[\p{P}\p{S}]/u.test(ch);
93
+ };
94
+ /** One escape pair (`\*`) or one code point of escaped inline text. */
95
+ const INLINE_UNIT = /\\[\s\S]|[\s\S]/gu;
96
+ /**
97
+ * Wrap `inner` in `mark` so a CommonMark reader reads it back. A delimiter
98
+ * opens only before a character that is not blank, and before punctuation
99
+ * only where blank or punctuation precedes it; closing mirrors that. Where
100
+ * the words at an edge would stop a delimiter from opening or closing (a
101
+ * space at the edge, emphasis ending in a full stop right before a letter),
102
+ * those characters are left outside the emphasis rather than written as
103
+ * literal asterisks.
104
+ */
105
+ function wrapFlanking(inner, mark, { before, after }) {
106
+ const isPunctuation = punctuationFor(mark);
107
+ const opensAfter = (ch) => isBlank(ch) || isPunctuation(ch);
108
+ const units = Array.from(inner.matchAll(INLINE_UNIT), ([unit]) => unit);
109
+ const isPunctuationUnit = (unit) => unit.length === 2 && unit.startsWith("\\") || isPunctuation(unit);
110
+ let start = 0;
111
+ let end = units.length;
112
+ let outside = before;
113
+ for (;;) {
114
+ const unit = units[start];
115
+ if (unit === void 0 || start >= end) break;
116
+ if (!isBlank(unit) && (!isPunctuationUnit(unit) || opensAfter(outside))) break;
117
+ outside = unit.at(-1);
118
+ start++;
119
+ }
120
+ outside = after;
121
+ for (;;) {
122
+ const unit = units[end - 1];
123
+ if (unit === void 0 || end <= start) break;
124
+ if (!isBlank(unit) && (!isPunctuationUnit(unit) || opensAfter(outside))) break;
125
+ outside = unit.at(0);
126
+ end--;
127
+ }
128
+ return units.slice(0, start).join("") + applyMarks(units.slice(start, end).join(""), start < end ? [mark] : []) + units.slice(end).join("");
129
+ }
130
+ /** Paragraph content that writes nothing to the line. */
131
+ const SILENT_MARKERS = /* @__PURE__ */ new Set([
132
+ "bookmarkStart",
133
+ "bookmarkEnd",
134
+ "moveFromRangeStart",
135
+ "moveFromRangeEnd",
136
+ "moveToRangeStart",
137
+ "moveToRangeEnd"
138
+ ]);
139
+ /** Comment range markers, which write nothing when comments are stripped. */
140
+ const COMMENT_MARKERS = /* @__PURE__ */ new Set(["commentRangeStart", "commentRangeEnd"]);
141
+ /** The first character `piece` puts on the line, inside the marks `outer` already opened. */
142
+ const pieceStart = (piece, outer, order) => {
143
+ const first = piece.text.match(INLINE_UNIT)?.[0];
144
+ if (first === void 0 || isBlank(first)) return first;
145
+ const opens = order.find((mark) => piece.marks.has(mark) && !outer.has(mark));
146
+ if (opens === void 0) return first;
147
+ return opens === "strike" ? "~" : "*";
148
+ };
149
+ /**
150
+ * Wrap consecutive pieces in their emphasis, one mark at a time: the pieces
151
+ * sharing the outermost mark are wrapped once, around the rest of their
152
+ * emphasis. Written run by run, the closing delimiter of one would meet the
153
+ * opening one of the next (`*a**b*`), which no reader takes for two
154
+ * emphases.
155
+ */
156
+ function renderPieces(pieces, outer, edges, order = GROUPING_ORDER) {
157
+ let out = "";
158
+ let index = 0;
159
+ while (index < pieces.length) {
160
+ const piece = pieces[index];
161
+ if (!piece) break;
162
+ const mark = order.find((key) => piece.marks.has(key) && !outer.has(key));
163
+ if (mark === void 0) {
164
+ out += piece.text;
165
+ index++;
166
+ continue;
167
+ }
168
+ let last = index;
169
+ while (pieces[last + 1]?.marks.has(mark)) last++;
170
+ const next = pieces[last + 1];
171
+ const around = {
172
+ before: out.length > 0 ? out.at(-1) : edges.before,
173
+ after: next === void 0 ? edges.after : pieceStart(next, outer, order)
174
+ };
175
+ const inner = renderPieces(pieces.slice(index, last + 1), /* @__PURE__ */ new Set([...outer, mark]), around, order);
176
+ out += wrapFlanking(inner, mark, around);
177
+ index = last + 1;
178
+ }
179
+ return out;
180
+ }
181
+ /** The first character an item that is not a run puts on the line, where it is fixed. */
182
+ function renderedStart(item) {
183
+ if (item?.type !== "hyperlink") return void 0;
184
+ const link = item;
185
+ return link.href || link.anchor ? "[" : void 0;
186
+ }
59
187
  /** Render a single run's RunContent array into the inline text fragment. */
60
188
  function renderRunContent(ctx, pkg, content, paraId) {
61
189
  let out = "";
@@ -111,21 +239,111 @@ function renderRunContent(ctx, pkg, content, paraId) {
111
239
  }
112
240
  return out;
113
241
  }
114
- /** Render a single Run with its formatting applied. */
115
- function renderRun(ctx, pkg, run, paraId) {
116
- if (run.formatting?.hidden) return "";
242
+ /** A run's rendered text and the emphasis it still takes; `null` when it writes nothing. */
243
+ function pieceOf(ctx, pkg, run, paraId) {
244
+ if (run.formatting?.hidden) return null;
117
245
  const inner = renderRunContent(ctx, pkg, run.content, paraId);
118
- if (!inner) return "";
246
+ if (!inner) return null;
247
+ const marks = marksFor(run);
248
+ if (!marks.includes("code")) return {
249
+ text: inner,
250
+ marks: new Set(marks)
251
+ };
119
252
  const leadLen = inner.length - inner.trimStart().length;
120
253
  const trailLen = inner.length - inner.trimEnd().length;
121
254
  const core = inner.slice(leadLen, inner.length - trailLen);
122
- if (!core) return inner;
123
- const lead = inner.slice(0, leadLen);
124
- const trail = inner.slice(inner.length - trailLen);
125
- return `${lead}${applyMarks(core, marksFor(run))}${trail}`;
255
+ return {
256
+ text: core ? `${inner.slice(0, leadLen)}${applyMarks(core, marks)}${inner.slice(inner.length - trailLen)}` : inner,
257
+ marks: /* @__PURE__ */ new Set()
258
+ };
126
259
  }
260
+ /** What a markdown reader reads from one line: each character with its emphasis. */
261
+ const readLine = (line) => compileMarkdownToContent(line).content.flatMap((block) => block.type === "paragraph" ? block.content : []).flatMap((item) => item.type === "hyperlink" ? item.children : [item]).flatMap((item) => {
262
+ if (item.type !== "run") return [{
263
+ char: "\0",
264
+ marks: /* @__PURE__ */ new Set()
265
+ }];
266
+ const marks = new Set(GROUPING_ORDER.filter((key) => item.formatting?.[key] === true));
267
+ return item.content.flatMap((content) => content.type === "text" ? Array.from(content.text, (char) => ({
268
+ char,
269
+ marks
270
+ })) : [{
271
+ char: "\0",
272
+ marks
273
+ }]);
274
+ });
275
+ const sameMarks = (left, right) => left.size === right.size && [...left].every((key) => right.has(key));
276
+ /**
277
+ * How `rendered`, between `edges`, reads back: `"exact"` with the pieces'
278
+ * text and each letter's emphasis, `"text"` with their text alone (emphasis
279
+ * lost somewhere, none of it read as literal asterisks), or `null`.
280
+ */
281
+ const readBack = (rendered, pieces, edges) => {
282
+ const side = (ch) => {
283
+ if (ch === void 0) return "x";
284
+ return ch === "" ? " " : ch;
285
+ };
286
+ const line = (inner) => `x ${side(edges.before)}${inner}${side(edges.after)} x`;
287
+ const read = readLine(line(rendered));
288
+ const plain = readLine(line(pieces.map(({ text }) => text).join("")));
289
+ if (read.map(({ char }) => char).join("") !== plain.map(({ char }) => char).join("")) return null;
290
+ const expected = pieces.flatMap(({ text, marks }) => readLine(`x${text}x`).slice(1, -1).map(({ char }) => ({
291
+ char,
292
+ marks
293
+ })));
294
+ const start = Array.from(`x ${side(edges.before)}`).length;
295
+ const end = Array.from(`${side(edges.after)} x`).length;
296
+ if (read.length !== start + expected.length + end) return read.every(({ marks }) => marks.size === 0) ? "text" : null;
297
+ let exact = true;
298
+ for (const [index, { char, marks }] of expected.entries()) {
299
+ const at = read[start + index];
300
+ if (at?.char !== char) return null;
301
+ if ([...at.marks].some((mark) => !marks.has(mark))) return null;
302
+ if (/[\p{L}\p{N}]/u.test(char) && !sameMarks(at.marks, marks)) exact = false;
303
+ }
304
+ return exact ? "exact" : "text";
305
+ };
306
+ /**
307
+ * Render consecutive runs with their formatting applied. The delimiters are
308
+ * placed by the flanking rules the reader applies, and the result is checked
309
+ * against the reader: a stacking it misreads is tried the other way round,
310
+ * or with more punctuation left outside, and failing all of those, the runs
311
+ * go out without emphasis rather than with stray asterisks.
312
+ */
313
+ function renderRuns(ctx, pkg, runs, paraId, edges = UNKNOWN_NEIGHBORS) {
314
+ const pieces = runs.flatMap((run) => pieceOf(ctx, pkg, run, paraId) ?? []);
315
+ const plain = pieces.map(({ text }) => text).join("");
316
+ if (!pieces.some(({ marks }) => marks.size > 0)) return plain;
317
+ let fallback;
318
+ for (const split of [pieces, splitAtBlanks(pieces)]) for (const order of GROUPING_ORDERS) for (const neighbors of [edges, UNKNOWN_NEIGHBORS]) {
319
+ const rendered = renderPieces(split, /* @__PURE__ */ new Set(), neighbors, order);
320
+ const read = readBack(rendered, pieces, edges);
321
+ if (read === "exact") return rendered;
322
+ if (read === "text") fallback ??= rendered;
323
+ }
324
+ return fallback ?? plain;
325
+ }
326
+ /** The pieces with every blank a piece of its own, without emphasis. */
327
+ const splitAtBlanks = (pieces) => pieces.flatMap((piece) => {
328
+ if (piece.marks.size === 0) return [piece];
329
+ const parts = [];
330
+ for (const [unit] of piece.text.matchAll(INLINE_UNIT)) {
331
+ const marks = isBlank(unit) ? /* @__PURE__ */ new Set() : piece.marks;
332
+ const last = parts.at(-1);
333
+ if (last && last.marks === marks) last.text += unit;
334
+ else parts.push({
335
+ text: unit,
336
+ marks
337
+ });
338
+ }
339
+ return parts;
340
+ });
127
341
  function renderHyperlink(ctx, pkg, link, paraId) {
128
- const inner = getHyperlinkRuns(link).map((run) => renderRun(ctx, pkg, run, paraId)).join("");
342
+ const bracketed = Boolean(link.href ?? link.anchor);
343
+ const inner = renderRuns(ctx, pkg, getHyperlinkRuns(link), paraId, bracketed ? {
344
+ before: "[",
345
+ after: "]"
346
+ } : UNKNOWN_NEIGHBORS);
129
347
  if (!inner) return "";
130
348
  const href = link.href ?? (link.anchor ? `#${link.anchor}` : "");
131
349
  if (!href) {
@@ -145,17 +363,17 @@ function renderHyperlink(ctx, pkg, link, paraId) {
145
363
  function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
146
364
  const renderChild = (child) => {
147
365
  switch (child.type) {
148
- case "run": return renderRun(ctx, pkg, child, paraId);
366
+ case "run": return renderRuns(ctx, pkg, [child], paraId);
149
367
  case "hyperlink": return renderHyperlink(ctx, pkg, child, paraId);
150
368
  case "mathEquation": return child.plainText ? escapeInline(child.plainText) : "";
151
369
  case "inlineWrapper":
152
- case "inlineSdt": return renderParagraphInline(ctx, pkg, child.content, paraId);
370
+ case "inlineSdt": return renderParagraphInline(ctx, pkg, child.content, paraId, UNKNOWN_NEIGHBORS);
153
371
  case "insertion":
154
372
  case "deletion":
155
373
  case "moveFrom":
156
374
  case "moveTo": return renderTrackedWrapper(ctx, pkg, child, paraId);
157
375
  case "simpleField":
158
- case "complexField": return renderParagraphInline(ctx, pkg, [child], paraId);
376
+ case "complexField": return renderParagraphInline(ctx, pkg, [child], paraId, UNKNOWN_NEIGHBORS);
159
377
  case "bookmarkStart":
160
378
  case "bookmarkEnd":
161
379
  case "moveFromRangeStart":
@@ -166,11 +384,33 @@ function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
166
384
  default: return child;
167
385
  }
168
386
  };
387
+ const renderChildren = () => {
388
+ let rendered = "";
389
+ let runs = [];
390
+ const flush = (after) => {
391
+ if (runs.length === 0) return;
392
+ rendered += renderRuns(ctx, pkg, runs, paraId, {
393
+ before: rendered.length > 0 ? rendered.at(-1) : void 0,
394
+ after
395
+ });
396
+ runs = [];
397
+ };
398
+ for (const child of wrapper.content) {
399
+ if (child.type === "run") {
400
+ runs.push(child);
401
+ continue;
402
+ }
403
+ flush(renderedStart(child));
404
+ rendered += renderChild(child);
405
+ }
406
+ flush(void 0);
407
+ return rendered;
408
+ };
169
409
  if (ctx.opts.trackedChanges === "clean") {
170
- if (wrapper.type === "insertion" || wrapper.type === "moveTo") return wrapper.content.map(renderChild).join("");
410
+ if (wrapper.type === "insertion" || wrapper.type === "moveTo") return renderChildren();
171
411
  return "";
172
412
  }
173
- const inner = wrapper.content.map(renderChild).join("");
413
+ const inner = renderChildren();
174
414
  switch (wrapper.type) {
175
415
  case "insertion": return wrapInsertion(ctx, wrapper.info, inner);
176
416
  case "deletion": return wrapDeletion(ctx, wrapper.info, inner);
@@ -182,55 +422,76 @@ function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
182
422
  * Render the full inline content of a paragraph, tracking comment-range
183
423
  * boundaries to apply the configured wrapper.
184
424
  */
185
- function renderParagraphInline(ctx, pkg, content, paraId) {
425
+ function renderParagraphInline(ctx, pkg, content, paraId, edges = {
426
+ before: "",
427
+ after: ""
428
+ }) {
186
429
  let out = "";
187
430
  const openComments = [];
188
- for (const item of content) switch (item.type) {
189
- case "run":
190
- out += renderRun(ctx, pkg, item, paraId);
191
- break;
192
- case "hyperlink":
193
- out += renderHyperlink(ctx, pkg, item, paraId);
194
- break;
195
- case "insertion":
196
- case "deletion":
197
- case "moveFrom":
198
- case "moveTo":
199
- out += renderTrackedWrapper(ctx, pkg, item, paraId);
200
- break;
201
- case "commentRangeStart":
202
- out += handleCommentStart(ctx, pkg, item, openComments, out.length);
203
- break;
204
- case "commentRangeEnd":
205
- out = handleCommentEnd(ctx, item, openComments, out);
206
- break;
207
- case "commentReference":
208
- out += renderPointComment(ctx, pkg, item.id);
209
- break;
210
- case "simpleField":
211
- case "complexField": {
212
- const runs = item.type === "simpleField" ? item.content : item.fieldResult;
213
- for (const child of runs) {
214
- if (child.type === "run") {
215
- out += renderRun(ctx, pkg, child, paraId);
216
- continue;
431
+ const items = content.filter((item) => !SILENT_MARKERS.has(item.type) && !(ctx.opts.comments === "strip" && COMMENT_MARKERS.has(item.type)));
432
+ const around = (index) => {
433
+ const next = items[index + 1];
434
+ return {
435
+ before: out.length > 0 ? out.at(-1) : edges.before,
436
+ after: next === void 0 ? edges.after : renderedStart(next)
437
+ };
438
+ };
439
+ for (let index = 0; index < items.length; index++) {
440
+ const item = items[index];
441
+ if (item === void 0) break;
442
+ switch (item.type) {
443
+ case "run": {
444
+ const runs = [item];
445
+ while (items[index + 1]?.type === "run") {
446
+ runs.push(items[index + 1]);
447
+ index++;
217
448
  }
218
- if (child.type === "hyperlink") {
219
- out += renderHyperlink(ctx, pkg, child, paraId);
220
- continue;
449
+ out += renderRuns(ctx, pkg, runs, paraId, around(index));
450
+ break;
451
+ }
452
+ case "hyperlink":
453
+ out += renderHyperlink(ctx, pkg, item, paraId);
454
+ break;
455
+ case "insertion":
456
+ case "deletion":
457
+ case "moveFrom":
458
+ case "moveTo":
459
+ out += renderTrackedWrapper(ctx, pkg, item, paraId);
460
+ break;
461
+ case "commentRangeStart":
462
+ out += handleCommentStart(ctx, pkg, item, openComments, out.length);
463
+ break;
464
+ case "commentRangeEnd":
465
+ out = handleCommentEnd(ctx, item, openComments, out);
466
+ break;
467
+ case "commentReference":
468
+ out += renderPointComment(ctx, pkg, item.id);
469
+ break;
470
+ case "simpleField":
471
+ case "complexField": {
472
+ const runs = item.type === "simpleField" ? item.content : item.fieldResult;
473
+ for (const child of runs) {
474
+ if (child.type === "run") {
475
+ out += renderRuns(ctx, pkg, [child], paraId);
476
+ continue;
477
+ }
478
+ if (child.type === "hyperlink") {
479
+ out += renderHyperlink(ctx, pkg, child, paraId);
480
+ continue;
481
+ }
482
+ out += child.type === "inlineWrapper" ? renderParagraphInline(ctx, pkg, child.content, paraId, UNKNOWN_NEIGHBORS) : child.text;
221
483
  }
222
- out += child.type === "inlineWrapper" ? renderParagraphInline(ctx, pkg, child.content, paraId) : child.text;
484
+ break;
223
485
  }
224
- break;
486
+ case "inlineSdt":
487
+ case "inlineWrapper":
488
+ out += renderParagraphInline(ctx, pkg, item.content, paraId, around(index));
489
+ break;
490
+ case "mathEquation":
491
+ if (item.plainText) out += escapeInline(item.plainText);
492
+ break;
493
+ default: break;
225
494
  }
226
- case "inlineSdt":
227
- case "inlineWrapper":
228
- out += renderParagraphInline(ctx, pkg, item.content, paraId);
229
- break;
230
- case "mathEquation":
231
- if (item.plainText) out += escapeInline(item.plainText);
232
- break;
233
- default: break;
234
495
  }
235
496
  while (openComments.length) {
236
497
  const slot = openComments.pop();
@@ -0,0 +1,6 @@
1
+ import { Command } from "prosemirror-state";
2
+ //#region src/prosemirror/clearRunColor.d.ts
3
+ /** Clearing direct color reveals the run's character/paragraph style color. */
4
+ declare const clearRunColor: () => Command;
5
+ //#endregion
6
+ export { clearRunColor };
@@ -0,0 +1,46 @@
1
+ import { getDocumentStyleResolver } from "./plugins/documentStyleState.js";
2
+ import { applyMarksToRunFormattingRepresentation, selectRunFormattingCarrierRepresentations } from "./runFormattingInlineCarriers.js";
3
+ import { readAuthoredRunFormatting, reconcileRunFormattingMarks } from "./runFormattingReconciliation.js";
4
+ import { paragraphRunStyleContextAt } from "./runStyleFormatting.js";
5
+ //#region src/prosemirror/clearRunColor.ts
6
+ /** Clearing direct color reveals the run's character/paragraph style color. */
7
+ const clearRunColor = () => (state, dispatch) => {
8
+ if (!dispatch) return true;
9
+ const styleResolver = getDocumentStyleResolver(state);
10
+ const tr = state.tr;
11
+ const { from, to, empty } = state.selection;
12
+ const clearedMarks = (node, position) => {
13
+ const context = paragraphRunStyleContextAt({
14
+ doc: state.doc,
15
+ pos: position,
16
+ styleResolver
17
+ });
18
+ const { color: _color, ...authoredFormatting } = readAuthoredRunFormatting({
19
+ context,
20
+ marks: node.marks,
21
+ styleResolver
22
+ });
23
+ return reconcileRunFormattingMarks({
24
+ authoredFormatting,
25
+ context,
26
+ node,
27
+ styleResolver
28
+ });
29
+ };
30
+ if (empty) {
31
+ const node = state.schema.text(" ", state.storedMarks ?? state.selection.$from.marks());
32
+ tr.setStoredMarks(clearedMarks(node, from));
33
+ } else for (const representation of selectRunFormattingCarrierRepresentations({
34
+ doc: state.doc,
35
+ from,
36
+ to
37
+ })) applyMarksToRunFormattingRepresentation({
38
+ tr,
39
+ representation,
40
+ marks: clearedMarks(representation.node, representation.position)
41
+ });
42
+ dispatch(tr.scrollIntoView());
43
+ return true;
44
+ };
45
+ //#endregion
46
+ export { clearRunColor };