@stll/folio-core 0.55.0 → 0.56.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 (119) hide show
  1. package/dist/ai-edits/apply.js +169 -38
  2. package/dist/ai-edits/headless.js +2 -2
  3. package/dist/ai-edits/types.d.ts +7 -1
  4. package/dist/compare/compare.js +1 -0
  5. package/dist/compare/reproducible-package.d.ts +0 -17
  6. package/dist/compare/reproducible-package.js +12 -18
  7. package/dist/controller/canonicalClipboard.d.ts +23 -0
  8. package/dist/controller/canonicalClipboard.js +476 -0
  9. package/dist/controller/canonicalClipboardResources.d.ts +21 -0
  10. package/dist/controller/canonicalClipboardResources.js +382 -0
  11. package/dist/controller/canonicalComposition.d.ts +20 -3
  12. package/dist/controller/canonicalComposition.js +166 -38
  13. package/dist/controller/canonicalInlineProjection.d.ts +113 -0
  14. package/dist/controller/canonicalInlineProjection.js +393 -0
  15. package/dist/controller/canonicalInput.d.ts +13 -4
  16. package/dist/controller/canonicalInput.js +52 -13
  17. package/dist/controller/canonicalInputTimer.d.ts +7 -0
  18. package/dist/controller/canonicalInputTimer.js +11 -0
  19. package/dist/controller/canonicalPublicOperations.d.ts +48 -0
  20. package/dist/controller/canonicalPublicOperations.js +556 -0
  21. package/dist/controller/canonicalSession.d.ts +24 -6
  22. package/dist/controller/canonicalSession.js +259 -111
  23. package/dist/controller/canonicalStoryEditor.d.ts +5 -3
  24. package/dist/controller/canonicalStoryEditor.js +7 -2
  25. package/dist/controller/canonicalStructure.js +57 -4
  26. package/dist/controller/canonicalTocSelection.d.ts +11 -0
  27. package/dist/controller/canonicalTocSelection.js +43 -0
  28. package/dist/controller/folioEditor.d.ts +2 -9
  29. package/dist/controller/folioEditor.js +5 -4
  30. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  31. package/dist/controller/headerFooterEditorManager.js +8 -7
  32. package/dist/controller/hiddenEditorApi.d.ts +12 -2
  33. package/dist/controller/hiddenEditorApi.js +4 -0
  34. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  35. package/dist/controller/hiddenEditorManager.js +216 -34
  36. package/dist/controller/noteEditorManager.d.ts +2 -1
  37. package/dist/controller/noteEditorManager.js +8 -7
  38. package/dist/document-operations.d.ts +8 -1
  39. package/dist/document-operations.js +6 -4
  40. package/dist/docx/blockPlainText.d.ts +1 -1
  41. package/dist/docx/blockPlainText.js +11 -3
  42. package/dist/docx/canonicalResourceSave.d.ts +16 -0
  43. package/dist/docx/canonicalResourceSave.js +27 -0
  44. package/dist/docx/canonicalSave.d.ts +29 -0
  45. package/dist/docx/canonicalSave.js +101 -0
  46. package/dist/docx/canonicalStoryRepack.d.ts +1 -1
  47. package/dist/docx/canonicalStoryRepack.js +38 -5
  48. package/dist/docx/documentParser.d.ts +1 -13
  49. package/dist/docx/documentParser.js +1 -47
  50. package/dist/docx/ensureParaIds.js +10 -4
  51. package/dist/docx/footnoteParser.d.ts +1 -11
  52. package/dist/docx/footnoteParser.js +1 -16
  53. package/dist/docx/headerFooterParser.d.ts +1 -9
  54. package/dist/docx/headerFooterParser.js +1 -12
  55. package/dist/docx/headerFooterVerbatim.d.ts +26 -1
  56. package/dist/docx/headerFooterVerbatim.js +81 -3
  57. package/dist/docx/imageParser.d.ts +13 -1
  58. package/dist/docx/imageParser.js +20 -11
  59. package/dist/docx/metadataPrivacy.js +6 -1
  60. package/dist/docx/packageParts.js +9 -3
  61. package/dist/docx/paragraphPropertySource.js +2 -1
  62. package/dist/docx/parser.js +2 -1
  63. package/dist/docx/removeHeaderFooterParts.js +17 -6
  64. package/dist/docx/rezip.d.ts +49 -5
  65. package/dist/docx/rezip.js +745 -229
  66. package/dist/docx/saveDiagnostics.d.ts +11 -0
  67. package/dist/docx/selectiveSave.d.ts +2 -1
  68. package/dist/docx/selectiveSave.js +32 -3
  69. package/dist/docx/selectiveXmlPatch.js +7 -5
  70. package/dist/docx/serializer/documentSerializer.d.ts +9 -2
  71. package/dist/docx/serializer/documentSerializer.js +29 -10
  72. package/dist/docx/server/applyDocxXmlPatchProposal.js +14 -9
  73. package/dist/docx/storyBlockReplay.d.ts +21 -1
  74. package/dist/docx/storyBlockReplay.js +113 -9
  75. package/dist/docx/storyPlainText.d.ts +20 -0
  76. package/dist/docx/storyPlainText.js +23 -0
  77. package/dist/docx/structuralXmlPatch.js +10 -22
  78. package/dist/docx/styleParser.d.ts +5 -1
  79. package/dist/docx/styleParser.js +1 -1
  80. package/dist/docx/tableParser.d.ts +1 -8
  81. package/dist/docx/tableParser.js +1 -28
  82. package/dist/docx/textBoxParser.d.ts +1 -5
  83. package/dist/docx/textBoxParser.js +1 -20
  84. package/dist/docx/unzip.js +6 -1
  85. package/dist/internal/acceptedBlockProjection.d.ts +12 -0
  86. package/dist/internal/acceptedBlockProjection.js +45 -0
  87. package/dist/managers/DocumentLoaderManager.js +2 -1
  88. package/dist/markdown/escape.d.ts +3 -1
  89. package/dist/markdown/escape.js +9 -2
  90. package/dist/markdown/renderBlock.js +5 -120
  91. package/dist/markdown/renderParagraph.js +4 -2
  92. package/dist/markdown/renderRuns.d.ts +7 -1
  93. package/dist/markdown/renderRuns.js +32 -13
  94. package/dist/prosemirror/canonicalCommands.d.ts +28 -4
  95. package/dist/prosemirror/canonicalCommands.js +11 -4
  96. package/dist/prosemirror/canonicalSelectionRange.d.ts +9 -0
  97. package/dist/prosemirror/canonicalSelectionRange.js +15 -0
  98. package/dist/prosemirror/clipboardIntent.d.ts +7 -0
  99. package/dist/prosemirror/clipboardIntent.js +9 -0
  100. package/dist/prosemirror/commands/pageBreak.js +2 -2
  101. package/dist/prosemirror/commands/pastePlainText.js +2 -1
  102. package/dist/prosemirror/conversion/hyphenTextCarriers.d.ts +8 -0
  103. package/dist/prosemirror/conversion/hyphenTextCarriers.js +8 -0
  104. package/dist/prosemirror/conversion/toProseDoc.d.ts +7 -1
  105. package/dist/prosemirror/conversion/toProseDoc.js +5 -4
  106. package/dist/prosemirror/extensions/core/ParagraphExtension.js +39 -30
  107. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +56 -28
  108. package/dist/prosemirror/extensions/marks/markUtils.js +3 -1
  109. package/dist/prosemirror/insertOperations.js +2 -1
  110. package/dist/prosemirror/paragraphPropertyCarry.d.ts +7 -1
  111. package/dist/prosemirror/paragraphPropertyCarry.js +8 -3
  112. package/dist/prosemirror/plugins/suggestionMode.js +7 -5
  113. package/dist/types/canonicalCapabilities.d.ts +215 -0
  114. package/dist/types/canonicalCapabilities.js +212 -0
  115. package/dist/types/canonicalSave.d.ts +11 -0
  116. package/dist/types/canonicalSave.js +0 -0
  117. package/dist/types/docxSerialization.d.ts +14 -0
  118. package/dist/types/docxSerialization.js +7 -0
  119. package/package.json +4 -4
@@ -1406,33 +1406,6 @@ function isCellHorizontallyMerged(cell) {
1406
1406
  return (cell.formatting?.gridSpan ?? 1) > 1;
1407
1407
  }
1408
1408
  /**
1409
- * Get the plain text content of a table
1410
- *
1411
- * @param table - The table to extract text from
1412
- * @returns Plain text content
1413
- */
1414
- function getTableText(table) {
1415
- const rows = [];
1416
- for (const row of table.rows) {
1417
- const cells = [];
1418
- for (const cell of row.cells) {
1419
- const cellText = cell.content.filter((c) => c.type === "paragraph").map((p) => getParagraphText(p)).join("\n");
1420
- cells.push(cellText);
1421
- }
1422
- rows.push(cells.join(" "));
1423
- }
1424
- return rows.join("\n");
1425
- }
1426
- /**
1427
- * Helper to get paragraph text (simplified)
1428
- */
1429
- function getParagraphText(para) {
1430
- return para.content.filter((c) => "content" in c).flatMap((run) => {
1431
- if (!("content" in run) || !Array.isArray(run.content)) return [];
1432
- return run.content.filter((c) => typeof c === "object" && c !== null && "type" in c && c.type === "text" && "text" in c).map((c) => c.text);
1433
- }).join("");
1434
- }
1435
- /**
1436
1409
  * Check if table has header row
1437
1410
  *
1438
1411
  * @param table - The table to check
@@ -1461,4 +1434,4 @@ function isFloatingTable(table) {
1461
1434
  return table.formatting?.floating !== void 0;
1462
1435
  }
1463
1436
  //#endregion
1464
- export { CELL_CONTENT_HANDLERS, MAX_TABLE_COLUMNS, ROW_CONTENT_HANDLERS, TABLE_CONTENT_HANDLERS, getHeaderRows, getTableColumnCount, getTableRowCount, getTableText, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, isTableCellMergeRevisionContinuation, isTableCellMergeRevisionValue, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTablePropertyExceptions, parseTableRow, parseTableRowProperties };
1437
+ export { CELL_CONTENT_HANDLERS, MAX_TABLE_COLUMNS, ROW_CONTENT_HANDLERS, TABLE_CONTENT_HANDLERS, getHeaderRows, getTableColumnCount, getTableRowCount, hasHeaderRow, isCellHorizontallyMerged, isCellMergeContinuation, isCellMergeStart, isFloatingTable, isTableCellMergeRevisionContinuation, isTableCellMergeRevisionValue, parseCellMargins, parseConditionalFormatStyle, parseFloatingTableProperties, parseTable, parseTableBorders, parseTableCell, parseTableCellProperties, parseTableGrid, parseTableLook, parseTableMeasurement, parseTableProperties, parseTablePropertyExceptions, parseTableRow, parseTableRowProperties };
@@ -103,10 +103,6 @@ declare function hasTextBoxOutline(textBox: document_d_exports.TextBox): boolean
103
103
  * Check if text box has content
104
104
  */
105
105
  declare function hasTextBoxContent(textBox: document_d_exports.TextBox): boolean;
106
- /**
107
- * Get plain text from text box (helper for search/indexing)
108
- */
109
- declare function getTextBoxText(textBox: document_d_exports.TextBox): string;
110
106
  /**
111
107
  * Resolve fill color to CSS color string
112
108
  */
@@ -151,4 +147,4 @@ type ScanRunForTextBoxDrawingsOptions = {
151
147
  };
152
148
  declare const scanRunForTextBoxDrawings: ({ xmlRun, claimedByRunParser }: ScanRunForTextBoxDrawingsOptions) => TextBoxRunScan;
153
149
  //#endregion
154
- export { ParagraphParserFn, ScanRunForTextBoxDrawingsOptions, TableParserFn, TextBoxDrawing, TextBoxRunScan, extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxText, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
150
+ export { ParagraphParserFn, ScanRunForTextBoxDrawingsOptions, TableParserFn, TextBoxDrawing, TextBoxRunScan, extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
@@ -285,25 +285,6 @@ function hasTextBoxContent(textBox) {
285
285
  return textBox.content.length > 0;
286
286
  }
287
287
  /**
288
- * Get plain text from text box (helper for search/indexing)
289
- */
290
- function getTextBoxText(textBox) {
291
- return textBox.content.map(getTextBoxBlockText).join("\n");
292
- }
293
- const getTextBoxBlockText = (block) => {
294
- if (block.type === "paragraph") {
295
- const runTexts = [];
296
- for (const item of block.content) {
297
- if (item.type !== "run") continue;
298
- for (const content of item.content) if (content.type === "text") runTexts.push(content.text);
299
- }
300
- return runTexts.join("");
301
- }
302
- if (block.type === "table") return block.rows.map((row) => row.cells.map((cell) => cell.content.map(getTextBoxBlockText).join("\n")).join(" ")).join("\n");
303
- if (block.type !== "blockSdt" && block.type !== "blockCustomXml") return "";
304
- return block.content.map(getTextBoxBlockText).join("\n");
305
- };
306
- /**
307
288
  * Resolve fill color to CSS color string
308
289
  */
309
290
  function resolveTextBoxFillColor(textBox) {
@@ -380,4 +361,4 @@ const scanRunForTextBoxDrawings = ({ xmlRun, claimedByRunParser }) => {
380
361
  };
381
362
  };
382
363
  //#endregion
383
- export { extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxText, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
364
+ export { extractTextBoxContentElements, getTextBoxContentElement, getTextBoxDimensionsPx, getTextBoxHeightPx, getTextBoxMarginsPx, getTextBoxOutlineWidthPx, getTextBoxWidthPx, hasTextBoxContent, hasTextBoxFill, hasTextBoxOutline, isFloatingTextBox, isShapeTextBox, isTextBoxDrawing, parseTextBox, parseTextBoxContent, parseTextBoxFromShape, resolveTextBoxFillColor, resolveTextBoxOutlineColor, scanRunForTextBoxDrawings };
@@ -5,6 +5,7 @@ import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
5
5
  import { RASTER_MIME_TYPES, detectRasterMimeType } from "./rasterMime.js";
6
6
  import { decodeXmlBytes } from "./xmlEncoding.js";
7
7
  import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "./xmlResourceLimits.js";
8
+ import { writeZipPart } from "@stll/docx-core/zip";
8
9
  import JSZip from "jszip";
9
10
  //#region src/docx/unzip.ts
10
11
  /**
@@ -282,7 +283,11 @@ const replaceRawDocxXmlParts = async (content, parts) => {
282
283
  lowerPath: path.toLowerCase(),
283
284
  content: xmlContent
284
285
  });
285
- content.originalZip.file(path, xmlContent);
286
+ writeZipPart({
287
+ zip: content.originalZip,
288
+ path,
289
+ data: xmlContent
290
+ });
286
291
  identitiesChanged = true;
287
292
  }
288
293
  if (identitiesChanged) content.originalBuffer = await content.originalZip.generateAsync({
@@ -0,0 +1,12 @@
1
+ import { document_d_exports } from "../types/document.js";
2
+ //#region src/internal/acceptedBlockProjection.d.ts
3
+ /**
4
+ * Read a model story through the same structural resolver as Accept All and
5
+ * the editor's No Markup view. The authored model is never changed.
6
+ */
7
+ declare const projectAcceptedBlocks: (blocks: readonly document_d_exports.BlockContent[], pkg: document_d_exports.DocxPackage | undefined) => {
8
+ blocks: readonly document_d_exports.BlockContent[];
9
+ completeness: string;
10
+ } | null;
11
+ //#endregion
12
+ export { projectAcceptedBlocks };
@@ -0,0 +1,45 @@
1
+ import { getCachedNumberingMap } from "../docx/numberingParser.js";
2
+ import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
3
+ import { headerFooterToProseDoc } from "../prosemirror/conversion/toProseDoc.js";
4
+ import { createStyleEngine } from "../style-engine/styleEngine.js";
5
+ import { resolveWholeStory } from "./wholeStoryRevisionResolution.js";
6
+ import { panic } from "better-result";
7
+ //#region src/internal/acceptedBlockProjection.ts
8
+ /** Inline revisions alone are already handled by the Markdown run renderer. */
9
+ const hasBlockRevision = (blocks) => blocks.some((block) => {
10
+ switch (block.type) {
11
+ case "paragraph": return block.pPrMark !== void 0;
12
+ case "table": return block.rows.some((row) => row.structuralChange !== void 0 || row.cells.some((cell) => cell.structuralChange !== void 0 || hasBlockRevision(cell.content)));
13
+ case "blockSdt":
14
+ case "blockCustomXml": return hasBlockRevision(block.content);
15
+ case "preservedBlock":
16
+ case "bookmarkStart":
17
+ case "bookmarkEnd": return false;
18
+ default: return panic("Unsupported block in accepted projection", { unsupported: block });
19
+ }
20
+ });
21
+ /**
22
+ * Read a model story through the same structural resolver as Accept All and
23
+ * the editor's No Markup view. The authored model is never changed.
24
+ */
25
+ const projectAcceptedBlocks = (blocks, pkg) => {
26
+ if (!hasBlockRevision(blocks)) return null;
27
+ const options = {
28
+ ...pkg?.styles !== void 0 && { styles: pkg.styles },
29
+ ...pkg?.theme !== void 0 && { theme: pkg.theme },
30
+ ...pkg?.numbering !== void 0 && { numbering: pkg.numbering }
31
+ };
32
+ const doc = headerFooterToProseDoc([...blocks], options);
33
+ const result = resolveWholeStory({
34
+ doc,
35
+ mode: "accept",
36
+ styleResolver: createStyleEngine(pkg?.styles),
37
+ numbering: pkg?.numbering ? getCachedNumberingMap(pkg.numbering) : null
38
+ });
39
+ return {
40
+ blocks: result.resolved.eq(doc) ? blocks : proseDocToBlocks(result.resolved, [...blocks], pkg?.styles),
41
+ completeness: result.failed ? "partial" : "complete"
42
+ };
43
+ };
44
+ //#endregion
45
+ export { projectAcceptedBlocks };
@@ -2,6 +2,7 @@ import { prepareCanonicalDocxInput } from "../docx/canonicalSessionInput.js";
2
2
  import { inspectDocxCompatibility } from "../docx/compatibility.js";
3
3
  import { parseDocx } from "../docx/parser.js";
4
4
  import { recordDocumentLoadPhase } from "../layout-engine/layoutInstrumentation.js";
5
+ import { CANONICAL_GAP, usesCanonicalSession } from "../types/canonicalCapabilities.js";
5
6
  import { resetAuthorColors } from "../utils/authorColors.js";
6
7
  import { loadFontsWithMapping } from "../utils/fontLoader.js";
7
8
  //#region src/managers/DocumentLoaderManager.ts
@@ -62,7 +63,7 @@ var DocumentLoaderManager = class {
62
63
  let doc;
63
64
  try {
64
65
  let input = buffer;
65
- if (this.callbacks.getExperimentalSession?.() === "canonical") {
66
+ if (usesCanonicalSession(this.callbacks.getExperimentalSession?.(), CANONICAL_GAP.authorityRouting)) {
66
67
  const prepared = await prepareCanonicalDocxInput(buffer);
67
68
  if (prepared.isErr()) throw prepared.error;
68
69
  input = prepared.value;
@@ -25,6 +25,8 @@ declare function escapeInline(text: string): string;
25
25
  * line.
26
26
  */
27
27
  declare function escapeTableCell(text: string): string;
28
+ /** Single-line Markdown blocks express inline breaks without their two-space syntax. */
29
+ declare const inlineBreaksToHtml: (text: string) => string;
28
30
  declare function escapeLinkUrl(url: string): string;
29
31
  /**
30
32
  * Alt text inside `![alt](url)`. Raw (un-escaped) text, so escape the backslash
@@ -33,4 +35,4 @@ declare function escapeLinkUrl(url: string): string;
33
35
  */
34
36
  declare function escapeAltText(text: string): string;
35
37
  //#endregion
36
- export { escapeAltText, escapeInline, escapeLinkUrl, escapeTableCell };
38
+ export { escapeAltText, escapeInline, escapeLinkUrl, escapeTableCell, inlineBreaksToHtml };
@@ -30,8 +30,15 @@ function escapeInline(text) {
30
30
  * line.
31
31
  */
32
32
  function escapeTableCell(text) {
33
- return text.replace(/\|/gu, "&#124;").replace(/\r?\n/gu, "<br>");
33
+ return inlineBreaksToHtml(text.replace(/\|/gu, "&#124;"));
34
34
  }
35
+ /** Single-line Markdown blocks express inline breaks without their two-space syntax. */
36
+ const inlineBreaksToHtml = (text) => text.split(/\r?\n/u).map((line, index, lines) => {
37
+ if (index === lines.length - 1) return line;
38
+ let end = line.length;
39
+ while (end > 0 && line.at(end - 1) === " ") end--;
40
+ return line.slice(0, end);
41
+ }).join("<br>");
35
42
  /**
36
43
  * Hyperlink URLs in inline form need parens balanced; we URL-encode the
37
44
  * problematic characters rather than escape, so the link still resolves.
@@ -55,4 +62,4 @@ function escapeAltText(text) {
55
62
  return text.replace(/(?<ch>[\\[\]])/gu, "\\$<ch>").replace(/\r?\n/gu, " ");
56
63
  }
57
64
  //#endregion
58
- export { escapeAltText, escapeInline, escapeLinkUrl, escapeTableCell };
65
+ export { escapeAltText, escapeInline, escapeLinkUrl, escapeTableCell, inlineBreaksToHtml };
@@ -1,6 +1,7 @@
1
1
  import { ALT_CHUNK_READER_DIAGNOSTIC, isAltChunkMarkup } from "../docx/altChunk.js";
2
2
  import { OPAQUE_REVISION_CARRIER_READER_DIAGNOSTIC, isOpaqueNestedRowMarkup, opaqueRevisionCarrierName } from "../docx/opaqueCarrier.js";
3
- import { cloneParagraphWithoutPropertySource } from "../docx/paragraphPropertySource.js";
3
+ import { projectAcceptedBlocks } from "../internal/acceptedBlockProjection.js";
4
+ import { pushWarning } from "./internals.js";
4
5
  import { renderParagraphBlock } from "./renderParagraph.js";
5
6
  import { renderTable } from "./renderTable.js";
6
7
  import { panic } from "better-result";
@@ -10,130 +11,14 @@ import { panic } from "better-result";
10
11
  * markdown for each block, suppressing redundant blank lines between list items
11
12
  * of the same list. Ported from eigenpal/docx-editor PR #595.
12
13
  */
13
- /**
14
- * In `trackedChanges: "clean"` mode every change is accepted, as the editor's
15
- * accept-all resolves the story (`internal/wholeStoryRevisionResolution.ts`).
16
- *
17
- * A paragraph whose mark is pending deletion loses its break on accept and
18
- * runs on into the paragraph after it, which is the paragraph left: its mark
19
- * ends the joined text, and a paragraph's properties (style, list) live on its
20
- * mark. A run of consecutive deletions collapses into that last paragraph.
21
- *
22
- * With a table right after it, its words run on into the table's first cell
23
- * and lead that cell's first paragraph (`prosemirror/tableRunIn.ts`), unless
24
- * it ends a section, keeps nothing once accepted, or that cell is pending.
25
- * Otherwise a paragraph that cannot join keeps its place, or goes when it
26
- * keeps nothing and something follows it.
27
- */
28
- function mergeAcceptedParagraphBreaks(blocks) {
29
- const reversed = [];
30
- let survivor = null;
31
- let tableFollows = false;
32
- let followed = false;
33
- const flush = () => {
34
- if (survivor) reversed.push(survivor);
35
- survivor = null;
36
- };
37
- for (let index = blocks.length - 1; index >= 0; index--) {
38
- const block = blocks[index];
39
- if (!block) continue;
40
- if (block.type !== "paragraph") {
41
- flush();
42
- reversed.push(block);
43
- tableFollows = block.type === "table";
44
- followed ||= tableFollows;
45
- continue;
46
- }
47
- const goes = block.pPrMark?.kind === "del" || block.pPrMark?.kind === "moveFrom";
48
- if (goes && survivor) {
49
- survivor = cloneParagraphWithoutPropertySource(survivor, { content: [...block.content, ...survivor.content] });
50
- continue;
51
- }
52
- const table = reversed.at(-1);
53
- if (goes && tableFollows && table?.type === "table") {
54
- const ranIn = tableWithParagraphRunIn(table, block);
55
- if (ranIn) {
56
- reversed[reversed.length - 1] = ranIn;
57
- continue;
58
- }
59
- }
60
- flush();
61
- tableFollows = false;
62
- if (goes && holdsNothingOnAccept(block) && followed && index + reversed.length > 0) continue;
63
- survivor = block;
64
- followed = true;
65
- }
66
- flush();
67
- return reversed.reverse();
68
- }
69
- /**
70
- * Whether the cell a paragraph's words would run into is itself pending (its
71
- * row or the cell, through nested tables' first cells), as
72
- * `prosemirror/tableRunIn.ts` reads it.
73
- */
74
- const runInCellPending = (table) => {
75
- const row = table.rows[0];
76
- const cell = row?.cells[0];
77
- if (!row || !cell) return false;
78
- if (row.structuralChange !== void 0 || cell.structuralChange !== void 0) return true;
79
- const first = cell.content[0];
80
- return first?.type === "table" && runInCellPending(first);
81
- };
82
- /**
83
- * `table` with `paragraph`'s words leading its first cell's first paragraph,
84
- * descending into nested tables, or null when the paragraph keeps its place.
85
- */
86
- const tableWithParagraphRunIn = (table, paragraph) => {
87
- if (paragraph.sectionProperties !== void 0 || holdsNothingOnAccept(paragraph)) return null;
88
- if (runInCellPending(table)) return null;
89
- const runIn = (current) => {
90
- const [row, ...rows] = current.rows;
91
- const [cell, ...cells] = row?.cells ?? [];
92
- const [first, ...rest] = cell?.content ?? [];
93
- if (!row || !cell || !first) return null;
94
- let led = null;
95
- if (first.type === "paragraph") led = cloneParagraphWithoutPropertySource(first, { content: [...paragraph.content, ...first.content] });
96
- else if (first.type === "table") led = runIn(first);
97
- if (!led) return null;
98
- return {
99
- ...current,
100
- rows: [{
101
- ...row,
102
- cells: [{
103
- ...cell,
104
- content: [led, ...rest]
105
- }, ...cells]
106
- }, ...rows]
107
- };
108
- };
109
- return runIn(table);
110
- };
111
- /** Paragraph content that shows nothing: range boundaries and anchors. */
112
- const ZERO_WIDTH_CONTENT = /* @__PURE__ */ new Set([
113
- "bookmarkStart",
114
- "bookmarkEnd",
115
- "commentRangeStart",
116
- "commentRangeEnd",
117
- "commentReference",
118
- "moveFromRangeStart",
119
- "moveFromRangeEnd",
120
- "moveToRangeStart",
121
- "moveToRangeEnd"
122
- ]);
123
- /** Whether an item shows nothing once every change is accepted. */
124
- const showsNothingOnAccept = (item) => {
125
- if (item.type === "deletion" || item.type === "moveFrom" || ZERO_WIDTH_CONTENT.has(item.type)) return true;
126
- if (item.type === "insertion" || item.type === "moveTo") return item.content.every(showsNothingOnAccept);
127
- return item.type === "run" && item.content.every((content) => content.type === "renderedPageBreak");
128
- };
129
- /** Whether a paragraph keeps no visible content once every change is accepted. */
130
- const holdsNothingOnAccept = (paragraph) => paragraph.content.every(showsNothingOnAccept);
131
14
  function renderBlocks(ctx, pkg, blocks) {
132
15
  const outerListIndentWidths = ctx.listIndentWidths;
133
16
  ctx.listIndentWidths = [];
134
17
  const out = [];
135
18
  let prevWasListItem = false;
136
- const ordered = ctx.opts.trackedChanges === "clean" ? mergeAcceptedParagraphBreaks(blocks) : blocks;
19
+ const projection = ctx.opts.trackedChanges === "clean" ? projectAcceptedBlocks(blocks, pkg) : null;
20
+ if (projection?.completeness === "partial") pushWarning(ctx, "Some tracked structural changes could not be resolved.");
21
+ const ordered = projection?.blocks ?? blocks;
137
22
  for (const block of ordered) switch (block.type) {
138
23
  case "paragraph": {
139
24
  const { markdown: md, isListItem } = renderParagraphBlock(ctx, pkg, block);
@@ -1,5 +1,6 @@
1
1
  import { isQuoteStyle, resolveHeadingLevel } from "../docx/builtInStyles.js";
2
2
  import { listLabelAttrsFromRendering } from "../prosemirror/listLabels.js";
3
+ import { inlineBreaksToHtml } from "./escape.js";
3
4
  import { renderParagraphInline } from "./renderRuns.js";
4
5
  //#region src/markdown/renderParagraph.ts
5
6
  /**
@@ -37,13 +38,14 @@ function renderParagraphBlock(ctx, pkg, para) {
37
38
  const headingLevel = markdownHeadingLevel(ctx, para);
38
39
  if (headingLevel !== void 0) {
39
40
  ctx.listIndentWidths = [];
40
- if (!inline) return {
41
+ if (!inline || !inline.trim() && label === void 0) return {
41
42
  markdown: "",
42
43
  isListItem: false
43
44
  };
44
45
  const hashes = "#".repeat(Math.min(MAX_MARKDOWN_HEADING_LEVEL, headingLevel + 1));
46
+ const headingInline = inlineBreaksToHtml(inline);
45
47
  return {
46
- markdown: label ? `${hashes} ${label} ${inline}` : `${hashes} ${inline}`,
48
+ markdown: label ? `${hashes} ${label} ${headingInline}` : `${hashes} ${headingInline}`,
47
49
  isListItem: false
48
50
  };
49
51
  }
@@ -1,6 +1,12 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { RenderContext } from "./types.js";
3
3
  //#region src/markdown/renderRuns.d.ts
4
+ /**
5
+ * Inline marks we recognize. Order matters: we open from outermost to innermost
6
+ * so the output reads cleanly (`***bold italic***`), not the reverse.
7
+ */
8
+ type MarkKey = "bold" | "italic" | "code" | "strike";
9
+ declare function applyMarks(text: string, marks: readonly MarkKey[]): string;
4
10
  /**
5
11
  * The character beside a run's delimiters: `""` at the paragraph's edge,
6
12
  * `undefined` when it cannot be told (read as a letter, the strictest case).
@@ -16,4 +22,4 @@ type RunNeighbors = {
16
22
  */
17
23
  declare function renderParagraphInline(ctx: RenderContext, pkg: document_d_exports.DocxPackage | undefined, content: readonly document_d_exports.ParagraphContent[], paraId: string | undefined, edges?: RunNeighbors): string;
18
24
  //#endregion
19
- export { renderParagraphInline };
25
+ export { applyMarks, renderParagraphInline };
@@ -40,22 +40,32 @@ function marksFor(run) {
40
40
  if (ascii && MONOSPACE_FONTS.has(ascii)) out.push("code");
41
41
  return out;
42
42
  }
43
+ const splitEdgeWhitespace = (text) => {
44
+ const start = text.length - text.trimStart().length;
45
+ const end = text.trimEnd().length;
46
+ return {
47
+ leading: text.slice(0, start),
48
+ core: text.slice(start, Math.max(start, end)),
49
+ trailing: text.slice(Math.max(start, end))
50
+ };
51
+ };
43
52
  function applyMarks(text, marks) {
44
- if (!text) return text;
53
+ const { leading, core, trailing } = splitEdgeWhitespace(text);
54
+ if (!core || marks.length === 0) return text;
45
55
  if (marks.includes("code")) {
46
- const literal = text.replace(/\\(?<char>[\\`*[\]_<~])/gu, "$<char>");
56
+ const literal = core.replace(/\\(?<char>[\\`*[\]_<~])/gu, "$<char>");
47
57
  let longestRun = 0;
48
58
  for (const m of literal.matchAll(/`+/gu)) longestRun = Math.max(longestRun, m[0].length);
49
59
  const fence = "`".repeat(longestRun + 1);
50
60
  const pad = literal.startsWith("`") || literal.endsWith("`") ? " " : "";
51
- return `${fence}${pad}${literal}${pad}${fence}`;
61
+ return `${leading}${fence}${pad}${literal}${pad}${fence}${trailing}`;
52
62
  }
53
- let out = text;
63
+ let out = core;
54
64
  for (const m of marks) {
55
65
  const d = MARK_DELIMS[m];
56
66
  out = `${d}${out}${d}`;
57
67
  }
58
- return out;
68
+ return `${leading}${out}${trailing}`;
59
69
  }
60
70
  const UNKNOWN_NEIGHBORS = {
61
71
  before: void 0,
@@ -250,11 +260,8 @@ function pieceOf(ctx, pkg, run, paraId) {
250
260
  text: inner,
251
261
  marks: new Set(marks)
252
262
  };
253
- const leadLen = inner.length - inner.trimStart().length;
254
- const trailLen = inner.length - inner.trimEnd().length;
255
- const core = inner.slice(leadLen, inner.length - trailLen);
256
263
  return {
257
- text: core ? `${inner.slice(0, leadLen)}${applyMarks(core, marks)}${inner.slice(inner.length - trailLen)}` : inner,
264
+ text: inner.split("\n").map((line) => applyMarks(line, marks)).join("\n"),
258
265
  marks: /* @__PURE__ */ new Set()
259
266
  };
260
267
  }
@@ -346,6 +353,8 @@ function renderHyperlink(ctx, pkg, link, paraId) {
346
353
  after: "]"
347
354
  } : UNKNOWN_NEIGHBORS);
348
355
  if (!inner) return "";
356
+ const { leading, core, trailing } = splitEdgeWhitespace(inner);
357
+ if (!core) return inner;
349
358
  const href = link.href ?? (link.anchor ? `#${link.anchor}` : "");
350
359
  if (!href) {
351
360
  pushWarning(ctx, "hyperlink missing href and anchor; rendered as plain text");
@@ -357,9 +366,9 @@ function renderHyperlink(ctx, pkg, link, paraId) {
357
366
  href,
358
367
  refNumber
359
368
  });
360
- return `[${inner}][${refNumber}]`;
369
+ return `${leading}[${core}][${refNumber}]${trailing}`;
361
370
  }
362
- return `[${inner}](${escapeLinkUrl(href)})`;
371
+ return `${leading}[${core}](${escapeLinkUrl(href)})${trailing}`;
363
372
  }
364
373
  function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
365
374
  const renderChild = (child) => {
@@ -419,6 +428,16 @@ function renderTrackedWrapper(ctx, pkg, wrapper, paraId) {
419
428
  default: return wrapMoveTo(ctx, wrapper.info, inner);
420
429
  }
421
430
  }
431
+ /** Clean revisions have no inline boundary; group their runs with surrounding runs. */
432
+ const cleanRevisionContent = (content) => content.flatMap((item) => {
433
+ switch (item.type) {
434
+ case "insertion":
435
+ case "moveTo": return cleanRevisionContent(item.content);
436
+ case "deletion":
437
+ case "moveFrom": return [];
438
+ default: return [item];
439
+ }
440
+ });
422
441
  /**
423
442
  * Render the full inline content of a paragraph, tracking comment-range
424
443
  * boundaries to apply the configured wrapper.
@@ -429,7 +448,7 @@ function renderParagraphInline(ctx, pkg, content, paraId, edges = {
429
448
  }) {
430
449
  let out = "";
431
450
  const openComments = [];
432
- const items = content.filter((item) => !SILENT_MARKERS.has(item.type) && !(ctx.opts.comments === "strip" && COMMENT_MARKERS.has(item.type)));
451
+ const items = (ctx.opts.trackedChanges === "clean" ? cleanRevisionContent(content) : content).filter((item) => !SILENT_MARKERS.has(item.type) && !(ctx.opts.comments === "strip" && COMMENT_MARKERS.has(item.type)));
433
452
  const around = (index) => {
434
453
  const next = items[index + 1];
435
454
  return {
@@ -557,4 +576,4 @@ function applyCommentWrapping(ctx, slot, current) {
557
576
  }, inner);
558
577
  }
559
578
  //#endregion
560
- export { renderParagraphInline };
579
+ export { applyMarks, renderParagraphInline };
@@ -4,6 +4,28 @@ import { ParagraphPropsPatch, RunPropsPatch } from "@stll/docx-core/ops";
4
4
  //#region src/prosemirror/canonicalCommands.d.ts
5
5
  /** Command meaning before canonical positions are resolved by the session. */
6
6
  type CanonicalCommandIntent = {
7
+ type: "generateTOC";
8
+ at: number;
9
+ title: string;
10
+ } | {
11
+ type: "setHyperlink";
12
+ from: number;
13
+ to: number;
14
+ href: string;
15
+ tooltip?: string;
16
+ } | {
17
+ type: "removeHyperlink";
18
+ from: number;
19
+ to: number;
20
+ hyperlinkStyleId?: string;
21
+ } | {
22
+ type: "insertHyperlink";
23
+ from: number;
24
+ to: number;
25
+ text: string;
26
+ href: string;
27
+ tooltip?: string;
28
+ } | {
7
29
  type: "formatRun";
8
30
  from: number;
9
31
  to: number;
@@ -36,9 +58,6 @@ type CanonicalCommandDescriptor = (state: EditorState) => readonly CanonicalComm
36
58
  declare const withCanonicalCommand: (command: Command, descriptor: CanonicalCommandDescriptor) => Command;
37
59
  declare const getCanonicalCommandIntents: (command: Command, state: EditorState) => readonly CanonicalCommandIntent[] | undefined;
38
60
  declare const canonicalRunFormatting: (state: EditorState, patch: RunPropsPatch) => readonly [{
39
- readonly type: "formatRun";
40
- readonly from: number;
41
- readonly to: number;
42
61
  readonly patch: import("@stll/docx-core/ops").FormattingPatch<{
43
62
  bold?: boolean | undefined;
44
63
  boldCs?: boolean | undefined;
@@ -91,7 +110,12 @@ declare const canonicalRunFormatting: (state: EditorState, patch: RunPropsPatch)
91
110
  styleId?: string | undefined;
92
111
  preserved?: import("@stll/docx-core/model").PreservedMarkup | undefined;
93
112
  }>;
113
+ readonly from: number;
114
+ readonly to: number;
115
+ readonly type: "formatRun";
94
116
  }];
95
117
  declare const withCanonicalParagraphFormatting: (command: Command, patch: ParagraphPropsPatch | ((paragraph: Node) => ParagraphPropsPatch)) => Command;
118
+ /** Commands that read the selection start apply that same patch across selected paragraphs. */
119
+ declare const withCanonicalStartParagraphFormatting: (command: Command, patch: (paragraph: Node) => ParagraphPropsPatch) => Command;
96
120
  //#endregion
97
- export { CanonicalCommandIntent, canonicalRunFormatting, getCanonicalCommandIntents, withCanonicalCommand, withCanonicalParagraphFormatting };
121
+ export { CanonicalCommandIntent, canonicalRunFormatting, getCanonicalCommandIntents, withCanonicalCommand, withCanonicalParagraphFormatting, withCanonicalStartParagraphFormatting };
@@ -1,3 +1,4 @@
1
+ import { canonicalSelectionRange } from "./canonicalSelectionRange.js";
1
2
  //#region src/prosemirror/canonicalCommands.ts
2
3
  const descriptors = /* @__PURE__ */ new WeakMap();
3
4
  /** Registration leaves execution and side-effect-free command probes unchanged. */
@@ -8,11 +9,10 @@ const withCanonicalCommand = (command, descriptor) => {
8
9
  const getCanonicalCommandIntents = (command, state) => descriptors.get(command)?.(state);
9
10
  const canonicalRunFormatting = (state, patch) => [{
10
11
  type: "formatRun",
11
- from: state.selection.from,
12
- to: state.selection.to,
12
+ ...canonicalSelectionRange(state),
13
13
  patch
14
14
  }];
15
- const withCanonicalParagraphFormatting = (command, patch) => withCanonicalCommand(command, (state) => {
15
+ const canonicalParagraphFormatting = (state, patch) => {
16
16
  const intents = [];
17
17
  state.doc.nodesBetween(state.selection.from, state.selection.to, (node, position) => {
18
18
  if (node.type.name !== "paragraph") return;
@@ -23,6 +23,13 @@ const withCanonicalParagraphFormatting = (command, patch) => withCanonicalComman
23
23
  });
24
24
  });
25
25
  return intents;
26
+ };
27
+ const withCanonicalParagraphFormatting = (command, patch) => withCanonicalCommand(command, (state) => canonicalParagraphFormatting(state, patch));
28
+ /** Commands that read the selection start apply that same patch across selected paragraphs. */
29
+ const withCanonicalStartParagraphFormatting = (command, patch) => withCanonicalCommand(command, (state) => {
30
+ const paragraph = state.selection.$from.parent;
31
+ if (paragraph.type.name !== "paragraph") return [];
32
+ return canonicalParagraphFormatting(state, patch(paragraph));
26
33
  });
27
34
  //#endregion
28
- export { canonicalRunFormatting, getCanonicalCommandIntents, withCanonicalCommand, withCanonicalParagraphFormatting };
35
+ export { canonicalRunFormatting, getCanonicalCommandIntents, withCanonicalCommand, withCanonicalParagraphFormatting, withCanonicalStartParagraphFormatting };
@@ -0,0 +1,9 @@
1
+ import { EditorState } from "prosemirror-state";
2
+ //#region src/prosemirror/canonicalSelectionRange.d.ts
3
+ /** Whole-document selection addresses paragraph content rather than document edges. */
4
+ declare const canonicalSelectionRange: ({ doc, selection }: EditorState) => {
5
+ from: number;
6
+ to: number;
7
+ };
8
+ //#endregion
9
+ export { canonicalSelectionRange };
@@ -0,0 +1,15 @@
1
+ import { AllSelection, Selection } from "prosemirror-state";
2
+ //#region src/prosemirror/canonicalSelectionRange.ts
3
+ /** Whole-document selection addresses paragraph content rather than document edges. */
4
+ const canonicalSelectionRange = ({ doc, selection }) => {
5
+ if (selection instanceof AllSelection) return {
6
+ from: Selection.atStart(doc).from,
7
+ to: Selection.atEnd(doc).to
8
+ };
9
+ return {
10
+ from: selection.from,
11
+ to: selection.to
12
+ };
13
+ };
14
+ //#endregion
15
+ export { canonicalSelectionRange };
@@ -0,0 +1,7 @@
1
+ import { Slice } from "prosemirror-model";
2
+ import { EditorView } from "prosemirror-view";
3
+ //#region src/prosemirror/clipboardIntent.d.ts
4
+ declare const registerClipboardIntentHandler: (view: EditorView, handler: (slice: Slice, plain: boolean) => boolean) => void;
5
+ declare const dispatchClipboardIntent: (view: EditorView, slice: Slice) => boolean | undefined;
6
+ //#endregion
7
+ export { dispatchClipboardIntent, registerClipboardIntentHandler };
@@ -0,0 +1,9 @@
1
+ //#region src/prosemirror/clipboardIntent.ts
2
+ /** Async clipboard reads resolve against the current owning session and selection. */
3
+ const handlers = /* @__PURE__ */ new WeakMap();
4
+ const registerClipboardIntentHandler = (view, handler) => {
5
+ handlers.set(view, handler);
6
+ };
7
+ const dispatchClipboardIntent = (view, slice) => handlers.get(view)?.(slice, true);
8
+ //#endregion
9
+ export { dispatchClipboardIntent, registerClipboardIntentHandler };