@stll/folio-core 0.3.1 → 0.5.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 (120) hide show
  1. package/dist/ai-edits/apply.d.ts +3 -9
  2. package/dist/ai-edits/apply.js +94 -14
  3. package/dist/ai-edits/blockRange.d.ts +33 -0
  4. package/dist/ai-edits/blockRange.js +58 -0
  5. package/dist/ai-edits/headless.d.ts +35 -5
  6. package/dist/ai-edits/headless.js +92 -22
  7. package/dist/ai-edits/index.d.ts +5 -3
  8. package/dist/ai-edits/index.js +3 -2
  9. package/dist/ai-edits/snapshot.d.ts +14 -2
  10. package/dist/ai-edits/snapshot.js +12 -1
  11. package/dist/ai-edits/types.d.ts +42 -3
  12. package/dist/compat/eigenpal.d.ts +3 -2
  13. package/dist/compat/eigenpal.js +6 -5
  14. package/dist/controller/layoutPipeline.d.ts +1 -0
  15. package/dist/controller/layoutPipeline.js +35 -115
  16. package/dist/controller/layoutScheduler.d.ts +2 -1
  17. package/dist/controller/layoutScheduler.js +6 -0
  18. package/dist/document-operations.d.ts +122 -0
  19. package/dist/document-operations.js +575 -0
  20. package/dist/docx/blockContentParser.js +17 -7
  21. package/dist/docx/encryption/agileDecryption.d.ts +6 -0
  22. package/dist/docx/encryption/agileDecryption.js +209 -0
  23. package/dist/docx/encryption/compoundFile.d.ts +20 -0
  24. package/dist/docx/encryption/compoundFile.js +251 -0
  25. package/dist/docx/encryption/containerFormat.d.ts +15 -0
  26. package/dist/docx/encryption/containerFormat.js +40 -0
  27. package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
  28. package/dist/docx/encryption/cryptoBytes.js +55 -0
  29. package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
  30. package/dist/docx/encryption/encryptionInfo.js +128 -0
  31. package/dist/docx/encryption/errors.d.ts +18 -0
  32. package/dist/docx/encryption/errors.js +13 -0
  33. package/dist/docx/encryption/index.d.ts +4 -0
  34. package/dist/docx/encryption/index.js +4 -0
  35. package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
  36. package/dist/docx/encryption/openEncryptedDocx.js +65 -0
  37. package/dist/docx/footnoteParser.d.ts +1 -17
  38. package/dist/docx/footnoteParser.js +1 -27
  39. package/dist/docx/groupDrawingParser.d.ts +8 -0
  40. package/dist/docx/groupDrawingParser.js +131 -0
  41. package/dist/docx/index.d.ts +3 -1
  42. package/dist/docx/index.js +3 -1
  43. package/dist/docx/numberingParser.d.ts +3 -1
  44. package/dist/docx/numberingParser.js +8 -2
  45. package/dist/docx/paragraphParser.js +9 -3
  46. package/dist/docx/parser.d.ts +4 -5
  47. package/dist/docx/parser.js +8 -2
  48. package/dist/docx/runParser.js +8 -1
  49. package/dist/docx/selectiveXmlPatch.js +4 -10
  50. package/dist/docx/serializer/numberingSerializer.js +3 -1
  51. package/dist/docx/settingsParser.d.ts +6 -3
  52. package/dist/docx/settingsParser.js +2 -0
  53. package/dist/docx/unzip.d.ts +7 -5
  54. package/dist/docx/unzip.js +8 -2
  55. package/dist/docx/xmlParser.js +11 -4
  56. package/dist/index.d.ts +3 -2
  57. package/dist/index.js +6 -5
  58. package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
  59. package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
  60. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
  61. package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
  63. package/dist/layout-engine/index.d.ts +6 -5
  64. package/dist/layout-engine/index.js +116 -27
  65. package/dist/layout-engine/keep-together.d.ts +13 -8
  66. package/dist/layout-engine/keep-together.js +40 -25
  67. package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
  68. package/dist/layout-engine/layoutInstrumentation.js +10 -1
  69. package/dist/layout-engine/measure/measureBlocks.js +43 -19
  70. package/dist/layout-engine/measure/measureParagraph.js +91 -14
  71. package/dist/layout-engine/paginator.d.ts +2 -1
  72. package/dist/layout-engine/paginator.js +9 -3
  73. package/dist/layout-engine/textBoxGroup.d.ts +7 -0
  74. package/dist/layout-engine/textBoxGroup.js +8 -0
  75. package/dist/layout-engine/types.d.ts +33 -9
  76. package/dist/layout-engine/types.js +20 -2
  77. package/dist/layout-painter/renderPage.d.ts +15 -1
  78. package/dist/layout-painter/renderPage.js +29 -12
  79. package/dist/layout-painter/renderParagraph.js +44 -9
  80. package/dist/layout-painter/renderTable.js +27 -7
  81. package/dist/managers/DocumentLoaderManager.d.ts +3 -1
  82. package/dist/managers/DocumentLoaderManager.js +13 -5
  83. package/dist/managers/TableSelectionManager.js +1 -1
  84. package/dist/model.js +1 -1
  85. package/dist/paged-layout/pageText.d.ts +14 -0
  86. package/dist/paged-layout/pageText.js +21 -0
  87. package/dist/prosemirror/attrs/index.js +3 -0
  88. package/dist/prosemirror/commands/comments.js +75 -5
  89. package/dist/prosemirror/commands/hyperlink.js +4 -2
  90. package/dist/prosemirror/commands/image.js +2 -1
  91. package/dist/prosemirror/commands/index.js +1 -1
  92. package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
  93. package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
  94. package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
  95. package/dist/prosemirror/conversion/toProseDoc.js +43 -15
  96. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
  97. package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
  98. package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
  99. package/dist/prosemirror/findReplaceSelection.js +2 -1
  100. package/dist/prosemirror/index.js +1 -1
  101. package/dist/prosemirror/insertOperations.js +1 -1
  102. package/dist/prosemirror/schema/nodes.d.ts +14 -7
  103. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  104. package/dist/prosemirror/utils/tabCalculator.js +4 -2
  105. package/dist/redline.d.ts +20 -0
  106. package/dist/redline.js +127 -0
  107. package/dist/server.d.ts +7 -3
  108. package/dist/server.js +6 -2
  109. package/dist/symbols.d.ts +35 -0
  110. package/dist/symbols.js +512 -0
  111. package/dist/utils/clipboard.js +3 -3
  112. package/dist/utils/fontLoader.js +3 -0
  113. package/dist/utils/fontResolver.js +65 -1
  114. package/dist/utils/zoom.d.ts +21 -0
  115. package/dist/utils/zoom.js +21 -0
  116. package/dist/version-comparison.d.ts +92 -0
  117. package/dist/version-comparison.js +436 -0
  118. package/package.json +7 -3
  119. package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
  120. package/dist/paged-layout/headerFooterMargins.js +0 -120
@@ -1,6 +1,7 @@
1
1
  import { cloneWithXmlnsDeclarations, elementToXml, findChild, findChildren, getAttribute, getChildElements, getTextContent, mergeXmlnsDeclarations, parseBooleanElement, parseNumericAttribute } from "./xmlParser.js";
2
2
  import { EmphasisMarkSchema, FontThemeSchema, HighlightColorSchema, ShadingPatternSchema, TextEffectSchema, ThemeColorSlotSchema, UnderlineStyleSchema, narrowEnum } from "./parserEnums.js";
3
3
  import { parseImage } from "./imageParser.js";
4
+ import { parseGroupDrawing } from "./groupDrawingParser.js";
4
5
  import { parseShapeFromDrawing, shouldPreserveRawShapeDrawing } from "./shapeParser.js";
5
6
  import { parseVmlImageContent } from "./vmlImageParser.js";
6
7
  import { resolveThemeFontRef } from "./themeParser.js";
@@ -442,6 +443,12 @@ function parseInstrText(element) {
442
443
  * `shapeParser.parseShapeFromDrawing` into a `ShapeContent`.
443
444
  */
444
445
  function parseDrawingContent(element, rels, media) {
446
+ const groupImage = parseGroupDrawing(element);
447
+ if (groupImage) return {
448
+ type: "drawing",
449
+ image: groupImage,
450
+ rawXml: elementToXml(element)
451
+ };
445
452
  if (shouldPreserveRawShapeDrawing(element)) return {
446
453
  type: "drawing",
447
454
  image: {
@@ -550,7 +557,7 @@ function parseRunContents(runElement, rels, media, rootXmlns = {}) {
550
557
  if (innerName === "drawing") {
551
558
  const innerDrawing = parseDrawingContent(innerChild, rels, media);
552
559
  if (innerDrawing) {
553
- if (innerDrawing.type === "drawing" && !innerDrawing.image.src) innerDrawing.rawXml = elementToXml(child);
560
+ if (innerDrawing.type === "drawing" && (innerDrawing.rawXml !== void 0 || !innerDrawing.image.src)) innerDrawing.rawXml = elementToXml(child);
554
561
  contents.push(innerDrawing);
555
562
  }
556
563
  } else if (innerName === "pict") {
@@ -50,11 +50,8 @@ function findParagraphOffsets(xml, paraId) {
50
50
  start,
51
51
  end: tagEnd + 1
52
52
  };
53
- pos = tagEnd + 1;
54
- } else {
55
- depth++;
56
- pos = tagEnd + 1;
57
- }
53
+ } else depth++;
54
+ pos = tagEnd + 1;
58
55
  } else pos = tagStart + 1;
59
56
  } else if (xml.startsWith("</w:p>", tagStart)) {
60
57
  depth--;
@@ -226,11 +223,8 @@ function scanElementRange(xml, start, openLiteral, closeTag) {
226
223
  start,
227
224
  end: tagEnd + 1
228
225
  };
229
- pos = tagEnd + 1;
230
- } else {
231
- depth++;
232
- pos = tagEnd + 1;
233
- }
226
+ } else depth++;
227
+ pos = tagEnd + 1;
234
228
  } else if (xml.startsWith(closeTag, tagStart)) {
235
229
  depth--;
236
230
  if (depth === 0) return {
@@ -87,7 +87,9 @@ function serializeNum(instance) {
87
87
  * change detection and per-definition splicing, never to overwrite the part.
88
88
  */
89
89
  function serializeNumberingXml(numbering) {
90
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="${W_NS}">${numbering.abstractNums.map(serializeAbstractNum).join("")}${numbering.nums.map(serializeNum).join("")}</w:numbering>`;
90
+ const abstractNums = numbering.abstractNums.map(serializeAbstractNum).join("");
91
+ const nums = numbering.nums.map(serializeNum).join("");
92
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="${W_NS}">${abstractNums}${nums}</w:numbering>`;
91
93
  }
92
94
  //#endregion
93
95
  export { serializeNumberingXml };
@@ -1,8 +1,11 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
 
3
3
  //#region src/docx/settingsParser.d.ts
4
+ type FolioDocumentSettings = document_d_exports.DocumentSettings & {
5
+ /** Swap left/right section margins on even physical pages. */mirrorMargins?: boolean;
6
+ };
4
7
  /** OOXML default per §17.6.13 when `w:defaultTabStop` is absent. */
5
8
  declare const DEFAULT_TAB_STOP_TWIPS = 720;
6
- declare function parseSettings(xml: string | null): document_d_exports.DocumentSettings;
7
- type DocumentSettings = document_d_exports.DocumentSettings;
8
- export { DEFAULT_TAB_STOP_TWIPS, type DocumentSettings, parseSettings };
9
+ declare function parseSettings(xml: string | null): FolioDocumentSettings;
10
+ //#endregion
11
+ export { DEFAULT_TAB_STOP_TWIPS, FolioDocumentSettings, parseSettings };
@@ -13,6 +13,8 @@ function parseSettings(xml) {
13
13
  const settings = { defaultTabStop: parseDefaultTabStop(root) };
14
14
  const evenAndOddHeaders = root ? findChild(root, "w", "evenAndOddHeaders") : null;
15
15
  if (evenAndOddHeaders && parseBooleanElement(evenAndOddHeaders)) settings.evenAndOddHeaders = true;
16
+ const mirrorMargins = root ? findChild(root, "w", "mirrorMargins") : null;
17
+ if (mirrorMargins && parseBooleanElement(mirrorMargins)) settings.mirrorMargins = true;
16
18
  const themeFontLangEl = root ? findChild(root, "w", "themeFontLang") : null;
17
19
  const eastAsiaLang = themeFontLangEl ? getAttribute(themeFontLangEl, "w", "eastAsia") || void 0 : void 0;
18
20
  const bidiLang = themeFontLangEl ? getAttribute(themeFontLangEl, "w", "bidi") || void 0 : void 0;
@@ -13,8 +13,9 @@ type DocxUnzipLimits = {
13
13
  maxTotalUncompressedBytes: number;
14
14
  allowedMediaMimeTypes: ReadonlySet<string>;
15
15
  };
16
- type PartialDocxUnzipLimits = Partial<Omit<DocxUnzipLimits, "allowedMediaMimeTypes">> & {
17
- allowedMediaMimeTypes?: Iterable<string>;
16
+ type DocxUnzipOptions = Partial<Omit<DocxUnzipLimits, "allowedMediaMimeTypes">> & {
17
+ allowedMediaMimeTypes?: Iterable<string>; /** Password for Agile-encrypted .docx files (Office 2010+). */
18
+ password?: string | undefined;
18
19
  };
19
20
  /**
20
21
  * Raw extracted content from a DOCX file
@@ -45,7 +46,8 @@ type RawDocxContent = {
45
46
  allXml: Map<string, string>;
46
47
  originalZip: JSZip;
47
48
  originalBuffer: ArrayBuffer;
48
- warnings: string[];
49
+ warnings: string[]; /** True when the input was a password-protected CFB container. */
50
+ wasEncrypted: boolean;
49
51
  };
50
52
  /**
51
53
  * Extract all content from a DOCX file
@@ -53,7 +55,7 @@ type RawDocxContent = {
53
55
  * @param buffer - DOCX file as ArrayBuffer
54
56
  * @returns Promise resolving to extracted content
55
57
  */
56
- declare function unzipDocx(buffer: ArrayBuffer, options?: PartialDocxUnzipLimits): Promise<RawDocxContent>;
58
+ declare function unzipDocx(buffer: ArrayBuffer, options?: DocxUnzipOptions): Promise<RawDocxContent>;
57
59
  declare function isPreservableDocxEntry(path: string): boolean;
58
60
  /**
59
61
  * Get a list of all files in the DOCX
@@ -115,4 +117,4 @@ declare function getContentSummary(content: RawDocxContent): {
115
117
  totalFiles: number;
116
118
  };
117
119
  //#endregion
118
- export { DocxSecurityError, DocxUnzipLimits, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
120
+ export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
@@ -1,3 +1,5 @@
1
+ import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./encryption/containerFormat.js";
2
+ import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
1
3
  import JSZip from "jszip";
2
4
  //#region src/docx/unzip.ts
3
5
  /**
@@ -71,7 +73,10 @@ const ZIP_CENTRAL_DIRECTORY_FILE_HEADER_SIZE = 46;
71
73
  async function unzipDocx(buffer, options = {}) {
72
74
  const limits = createUnzipLimits(options);
73
75
  if (buffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
74
- const loaded = await loadDocxZip(buffer, limits.maxFiles);
76
+ const zipBuffer = await openDocxBuffer(buffer, { password: options.password });
77
+ if (zipBuffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
78
+ const wasEncrypted = detectDocxContainerType(buffer) === DOCX_CONTAINER_TYPES.CFB;
79
+ const loaded = await loadDocxZip(zipBuffer, limits.maxFiles);
75
80
  if (loaded.buffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
76
81
  const { zip } = loaded;
77
82
  const entries = Object.entries(zip.files).filter(([, file]) => !file.dir);
@@ -102,7 +107,8 @@ async function unzipDocx(buffer, options = {}) {
102
107
  allXml: /* @__PURE__ */ new Map(),
103
108
  originalZip: zip,
104
109
  originalBuffer: loaded.buffer,
105
- warnings: []
110
+ warnings: [],
111
+ wasEncrypted
106
112
  };
107
113
  let totalUncompressedBytes = 0;
108
114
  for (const [path, file] of entries) {
@@ -31,6 +31,7 @@ const fxpParserOptions = {
31
31
  parseAttributeValue: false,
32
32
  ignoreDeclaration: true,
33
33
  ignorePiTags: true,
34
+ jPath: false,
34
35
  processEntities: true,
35
36
  htmlEntities: true
36
37
  };
@@ -65,15 +66,19 @@ function fxpNodeToElement(node) {
65
66
  text: node[TEXT_KEY]
66
67
  };
67
68
  const attrs = node[ATTR_KEY];
68
- for (const key of Object.keys(node)) {
69
+ for (const key in node) {
69
70
  if (key === ATTR_KEY) continue;
70
71
  const children = node[key];
71
72
  const element = {
72
73
  type: "element",
73
74
  name: key
74
75
  };
75
- if (attrs && Object.keys(attrs).length > 0) element.attributes = attrs;
76
- if (children.length > 0) element.elements = children.map(fxpNodeToElement);
76
+ if (attrs) element.attributes = attrs;
77
+ if (children.length > 0) {
78
+ const elements = [];
79
+ for (const child of children) elements.push(fxpNodeToElement(child));
80
+ element.elements = elements;
81
+ }
77
82
  return element;
78
83
  }
79
84
  return { type: "element" };
@@ -84,7 +89,9 @@ function fxpNodeToElement(node) {
84
89
  * `{ elements: [...] }`.
85
90
  */
86
91
  function fxpToRootElement(nodes) {
87
- return { elements: nodes.map(fxpNodeToElement) };
92
+ const elements = [];
93
+ for (const node of nodes) elements.push(fxpNodeToElement(node));
94
+ return { elements };
88
95
  }
89
96
  /**
90
97
  * Convert an XmlElement back into the fast-xml-parser preserveOrder format
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./ai-edits/types.js";
1
+ import { FolioAIBlock, FolioAIBlockAnchor, FolioAIBlockKind, FolioAIBlockPreviewRun, FolioAIComment, FolioAIEditAppliedOperation, FolioAIEditApplyMode, FolioAIEditApplyResult, FolioAIEditOperation, FolioAIEditPrecondition, FolioAIEditReviewMeta, FolioAIEditSeverity, FolioAIEditSkipReason, FolioAIEditSkippedOperation, FolioAIEditSnapshot, FolioAISignatureParty } from "./ai-edits/types.js";
2
2
  import { applyFolioAIEditOperations } from "./ai-edits/apply.js";
3
3
  import { document_d_exports } from "./types/document.js";
4
+ import { ApplyFolioDocumentOperationsOptions, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, FolioDocumentOperation, FolioDocumentOperationAffectedTarget, FolioDocumentOperationBatch, FolioDocumentOperationCapabilities, FolioDocumentOperationIssue, FolioDocumentOperationMode, FolioDocumentOperationPrecondition, FolioDocumentOperationReceipt, FolioDocumentOperationRecovery, FolioDocumentOperationResult, FolioDocumentOperationStatus, FolioDocumentOperationType, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js";
4
5
  import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js";
5
6
  import { DeriveBlockIdInput, FolioBlockId, deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
6
7
  import { WordDiffSegment, diffWordSegments } from "./ai-edits/word-diff.js";
@@ -26,4 +27,4 @@ import { toMarkdown, toMarkdownResult } from "./markdown/index.js";
26
27
  import { EmbeddedFont, EmbeddedFontParts, extractEmbeddedFonts, getEmbeddedFontFaces } from "./fonts/embeddedFonts.js";
27
28
  import { getGoogleFontsEnabled, setGoogleFontsEnabled } from "./utils/fontResolver.js";
28
29
  type Document = document_d_exports.Document;
29
- export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type ImageMeta, type ImageRef, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
30
+ export { type AIBarStatus, type AIChatMode, type AICitation, type AICitationRange, type AICitationSource, type AIGenerateInput, type AISuggestion, type AISuggestionApplyMode, type AISuggestionPreset, type AISuggestionSeverity, type AISuggestionStatus, type AcceptAutocompleteResult, type AnonymizationMatch, type AnonymizationTerm, type ApplyFolioDocumentOperationsOptions, type ApplyResult, type AutocompleteSuggestionPluginOptions, type AutocompleteSuggestionState, type AutocompleteSuggestionStatus, type AutocompleteTriggerCheck, type AutocompleteTriggerOptions, type AutocompleteTriggerSkipReason, type CreateEmptyDocumentOptions, DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, type DeriveBlockIdInput, type DirectiveKind, type DirectiveRange, type Document, type DocxCompatibility, type EmbeddedFont, type EmbeddedFontParts, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, type FolioAIBlock, type FolioAIBlockAnchor, type FolioAIBlockKind, type FolioAIBlockPreviewRun, type FolioAIComment, type FolioAIEditAppliedOperation, type FolioAIEditApplyMode, type FolioAIEditApplyResult, type FolioAIEditOperation, type FolioAIEditPrecondition, type FolioAIEditReviewMeta, type FolioAIEditSeverity, type FolioAIEditSkipReason, type FolioAIEditSkippedOperation, type FolioAIEditSnapshot, type FolioAISignatureParty, type FolioBlockId, type FolioDocumentOperation, type FolioDocumentOperationAffectedTarget, type FolioDocumentOperationBatch, type FolioDocumentOperationCapabilities, type FolioDocumentOperationIssue, type FolioDocumentOperationMode, type FolioDocumentOperationPrecondition, type FolioDocumentOperationReceipt, type FolioDocumentOperationRecovery, type FolioDocumentOperationResult, type FolioDocumentOperationStatus, type FolioDocumentOperationType, type ImageMeta, type ImageRef, InvalidFolioDocumentOperationBatchError, type MarkdownOptions, type MarkdownResult, type PositionalText, type ResolvedAnchor, type TemplatePreviewSpan, type TemplatePreviewValue, type TemplatePreviewValues, type TemplateSlashMenuKeyAction, type TemplateSlashMenuState, UnsupportedFolioDocumentOperationVersionError, type WordDiffSegment, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
+ import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
2
+ import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js";
3
+ import { diffWordSegments } from "./ai-edits/word-diff.js";
4
+ import { applyFolioAIEditOperations } from "./ai-edits/apply.js";
5
+ import { FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, applyFolioDocumentOperations, assertSupportedFolioDocumentOperationVersion, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, isFolioDocumentOperationModeSupported, isSupportedFolioDocumentOperationVersion, parseFolioDocumentOperationBatch } from "./document-operations.js";
1
6
  import { createEmptyDocument } from "./utils/createDocument.js";
2
7
  import { createDocx } from "./docx/rezip.js";
3
- import { deriveBlockId, getFolioParaIdFromBlockId, isFolioBlockId, isSequentialFolioBlockId } from "./types/block-id.js";
4
8
  import { DEFAULT_AI_SUGGESTION_PRESETS } from "./ai-suggestions/types.js";
5
9
  import { buildPositionalText } from "./ai-suggestions/text-positions.js";
6
10
  import { isSuggestionStale, resolveSuggestionAnchor } from "./ai-suggestions/conflict.js";
7
11
  import { applySuggestions } from "./ai-suggestions/apply.js";
8
- import { createFolioAIEditSnapshot, hashFolioAIBlockText, normalizeFolioAIBlockText } from "./ai-edits/snapshot.js";
9
- import { diffWordSegments } from "./ai-edits/word-diff.js";
10
- import { applyFolioAIEditOperations } from "./ai-edits/apply.js";
11
12
  import { setAISuggestionsMeta, setFocusedSuggestionMeta } from "./prosemirror/plugins/aiSuggestionDecorations.js";
12
13
  import { getGoogleFontsEnabled, setGoogleFontsEnabled } from "./utils/fontResolver.js";
13
14
  import { scrollFolioPositionIntoView } from "./paged-layout/scrollToPmPosition.js";
@@ -21,4 +22,4 @@ import { DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, acceptAutocompleteSuggestion, acc
21
22
  import { fromMarkdown } from "./markdown/fromMarkdown.js";
22
23
  import { toMarkdown, toMarkdownResult } from "./markdown/index.js";
23
24
  import { extractEmbeddedFonts, getEmbeddedFontFaces } from "./fonts/embeddedFonts.js";
24
- export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applySuggestions, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isSequentialFolioBlockId, isSuggestionStale, normalizeFolioAIBlockText, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
25
+ export { DEFAULT_AI_SUGGESTION_PRESETS, DEFAULT_AUTOCOMPLETE_DEAD_ZONE_NODES, FOLIO_DOCUMENT_OPERATION_BATCH_MODES, FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION, FOLIO_DOCUMENT_OPERATION_MODES, FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, FOLIO_DOCUMENT_OPERATION_PRECONDITIONS, FOLIO_DOCUMENT_OPERATION_STORIES, FOLIO_DOCUMENT_OPERATION_TYPES, InvalidFolioDocumentOperationBatchError, UnsupportedFolioDocumentOperationVersionError, acceptAutocompleteSuggestion, acceptAutocompleteWord, anonymizationDecorationsKey, appendAutocompleteToken, applyFolioAIEditOperations, applyFolioDocumentOperations, applySuggestions, assertSupportedFolioDocumentOperationVersion, autocompleteSuggestionKey, autocompleteSuggestionPlugin, buildPositionalText, clearAutocompleteSuggestion, clearTemplateSlashMenu, consumeTemplateSlashQuery, createAICitationDecorationsPlugin, createDocx, createEmptyDocument, createFolioAIEditSnapshot, deriveBlockId, diffWordSegments, extractEmbeddedFonts, finishAutocompleteSuggestion, fromMarkdown, getAnonymizationMatches, getAutocompleteSuggestion, getEmbeddedFontFaces, getFolioCaretViewportRect, getFolioDocumentOperationCapabilities, getFolioDocumentOperationIssues, getFolioDocumentOperationReceipts, getFolioParaIdFromBlockId, getFolioSelectionViewportRect, getGoogleFontsEnabled, getTemplateDirectives, getTemplateSlashMenu, hashFolioAIBlockText, isFolioBlockId, isFolioDocumentOperationModeSupported, isSequentialFolioBlockId, isSuggestionStale, isSupportedFolioDocumentOperationVersion, normalizeFolioAIBlockText, parseFolioDocumentOperationBatch, resetTemplateSlashQuery, resolveSuggestionAnchor, scanDirectives, scrollFolioPositionIntoView, setAICitationsMeta, setAISuggestionsMeta, setActiveCitationMeta, setAnonymizationTermsMeta, setFocusedSuggestionMeta, setGoogleFontsEnabled, setTemplatePreviewValues, shouldTriggerAutocomplete, startAutocompleteSuggestion, templateSlashMenuKey, toMarkdown, toMarkdownResult };
@@ -23,6 +23,42 @@ declare function collectFootnoteRefs(blocks: FlowBlock[]): {
23
23
  footnoteId: number;
24
24
  pmPos: number;
25
25
  }[];
26
+ /**
27
+ * Scan FlowBlocks for runs with endnoteRefId set, in document order.
28
+ * Same recursive walk as `collectFootnoteRefs`.
29
+ */
30
+ declare function collectEndnoteRefs(blocks: FlowBlock[]): {
31
+ endnoteId: number;
32
+ pmPos: number;
33
+ }[];
34
+ type NumberableNote = {
35
+ id: number;
36
+ noteType?: document_d_exports.Footnote["noteType"];
37
+ };
38
+ /**
39
+ * Assign sequential display numbers (1, 2, 3, …) to notes in order of first
40
+ * reference. Word numbers footnotes/endnotes by reference order, not by their
41
+ * `w:id` values, which may be non-contiguous or out of document order. Only
42
+ * `normal` notes are numbered; separator/continuation notes and refs to
43
+ * missing notes consume no number.
44
+ */
45
+ declare function computeNoteDisplayNumbers(notes: readonly NumberableNote[], refNoteIds: readonly number[]): Map<number, number>;
46
+ type NoteDisplayNumberMaps = {
47
+ /** footnote `w:id` → sequential display number */footnoteNumbers?: ReadonlyMap<number, number>; /** endnote `w:id` → sequential display number */
48
+ endnoteNumbers?: ReadonlyMap<number, number>;
49
+ };
50
+ /**
51
+ * Rewrite body reference-marker run text from the raw `w:id` (which the PM
52
+ * doc stores as the marker text; the save path serializes from the mark
53
+ * attrs, never from this text) to the sequential display number, so the
54
+ * inline marker matches the number rendered in the note area. Must run
55
+ * before measurement so marker widths match the painted digits. Untouched
56
+ * blocks/runs are returned by reference so the painter's fingerprinting and
57
+ * the incremental measure path see them unchanged. Runs keep their PM range,
58
+ * so click-to-position still resolves into the marker (same contract as the
59
+ * template preview substitution).
60
+ */
61
+ declare function remapNoteMarkerText(blocks: FlowBlock[], maps: NoteDisplayNumberMaps): FlowBlock[];
26
62
  /**
27
63
  * After layout, determine which footnotes appear on which pages.
28
64
  * Checks each page's fragments to see if any footnoteRef PM positions fall within.
@@ -54,4 +90,4 @@ declare function calculateFootnoteReservedHeights(pageFootnoteMap: Map<number, n
54
90
  height: number;
55
91
  }>): Map<number, number>;
56
92
  //#endregion
57
- export { ConvertFootnoteOptions, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, MeasureBlocksFn, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectFootnoteRefs, convertFootnoteToContent, mapFootnotesToPages };
93
+ export { ConvertFootnoteOptions, FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, MeasureBlocksFn, NoteDisplayNumberMaps, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, convertFootnoteToContent, mapFootnotesToPages, remapNoteMarkerText };
@@ -16,20 +16,149 @@ const FOOTNOTE_FONT_SIZE = 8;
16
16
  * area.
17
17
  */
18
18
  function collectFootnoteRefs(blocks) {
19
+ return collectNoteRefs(blocks, "footnoteRefId").map(({ noteId, pmPos }) => ({
20
+ footnoteId: noteId,
21
+ pmPos
22
+ }));
23
+ }
24
+ /**
25
+ * Scan FlowBlocks for runs with endnoteRefId set, in document order.
26
+ * Same recursive walk as `collectFootnoteRefs`.
27
+ */
28
+ function collectEndnoteRefs(blocks) {
29
+ return collectNoteRefs(blocks, "endnoteRefId").map(({ noteId, pmPos }) => ({
30
+ endnoteId: noteId,
31
+ pmPos
32
+ }));
33
+ }
34
+ function collectNoteRefs(blocks, idKey) {
19
35
  const refs = [];
20
36
  const walk = (containerBlocks) => {
21
- for (const block of containerBlocks) if (block.kind === "paragraph") {
22
- for (const run of block.runs) if (run.kind === "text" && run.footnoteRefId !== void 0) refs.push({
23
- footnoteId: run.footnoteRefId,
37
+ for (const block of containerBlocks) if (block.kind === "paragraph") for (const run of block.runs) {
38
+ if (run.kind !== "text") continue;
39
+ const noteId = run[idKey];
40
+ if (noteId !== void 0) refs.push({
41
+ noteId,
24
42
  pmPos: run.pmStart ?? 0
25
43
  });
26
- } else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks);
44
+ }
45
+ else if (block.kind === "table") for (const row of block.rows) for (const cell of row.cells) walk(cell.blocks);
27
46
  else if (block.kind === "textBox") walk(block.content);
28
47
  };
29
48
  walk(blocks);
30
49
  return refs;
31
50
  }
32
51
  /**
52
+ * Assign sequential display numbers (1, 2, 3, …) to notes in order of first
53
+ * reference. Word numbers footnotes/endnotes by reference order, not by their
54
+ * `w:id` values, which may be non-contiguous or out of document order. Only
55
+ * `normal` notes are numbered; separator/continuation notes and refs to
56
+ * missing notes consume no number.
57
+ */
58
+ function computeNoteDisplayNumbers(notes, refNoteIds) {
59
+ const numberable = /* @__PURE__ */ new Set();
60
+ for (const note of notes) if (note.noteType === "normal") numberable.add(note.id);
61
+ const displayNumbers = /* @__PURE__ */ new Map();
62
+ let nextNumber = 1;
63
+ for (const noteId of refNoteIds) {
64
+ if (displayNumbers.has(noteId) || !numberable.has(noteId)) continue;
65
+ displayNumbers.set(noteId, nextNumber);
66
+ nextNumber++;
67
+ }
68
+ return displayNumbers;
69
+ }
70
+ /**
71
+ * Rewrite body reference-marker run text from the raw `w:id` (which the PM
72
+ * doc stores as the marker text; the save path serializes from the mark
73
+ * attrs, never from this text) to the sequential display number, so the
74
+ * inline marker matches the number rendered in the note area. Must run
75
+ * before measurement so marker widths match the painted digits. Untouched
76
+ * blocks/runs are returned by reference so the painter's fingerprinting and
77
+ * the incremental measure path see them unchanged. Runs keep their PM range,
78
+ * so click-to-position still resolves into the marker (same contract as the
79
+ * template preview substitution).
80
+ */
81
+ function remapNoteMarkerText(blocks, maps) {
82
+ if ((maps.footnoteNumbers?.size ?? 0) === 0 && (maps.endnoteNumbers?.size ?? 0) === 0) return blocks;
83
+ let changed = false;
84
+ const next = blocks.map((block) => {
85
+ const remapped = remapNoteMarkerBlock(block, maps);
86
+ changed ||= remapped !== block;
87
+ return remapped;
88
+ });
89
+ return changed ? next : blocks;
90
+ }
91
+ function remapNoteMarkerBlock(block, maps) {
92
+ if (block.kind === "paragraph") return remapNoteMarkerParagraph(block, maps);
93
+ if (block.kind === "table") {
94
+ let tableChanged = false;
95
+ const rows = block.rows.map((row) => {
96
+ let rowChanged = false;
97
+ const cells = row.cells.map((cell) => {
98
+ let cellChanged = false;
99
+ const cellBlocks = cell.blocks.map((cellBlock) => {
100
+ const remapped = remapNoteMarkerBlock(cellBlock, maps);
101
+ cellChanged ||= remapped !== cellBlock;
102
+ return remapped;
103
+ });
104
+ return cellChanged ? {
105
+ ...cell,
106
+ blocks: cellBlocks
107
+ } : cell;
108
+ });
109
+ rowChanged = cells.some((cell, index) => cell !== row.cells[index]);
110
+ tableChanged ||= rowChanged;
111
+ return rowChanged ? {
112
+ ...row,
113
+ cells
114
+ } : row;
115
+ });
116
+ return tableChanged ? {
117
+ ...block,
118
+ rows
119
+ } : block;
120
+ }
121
+ if (block.kind === "textBox") {
122
+ let boxChanged = false;
123
+ const content = block.content.map((paragraph) => {
124
+ const remapped = remapNoteMarkerParagraph(paragraph, maps);
125
+ boxChanged ||= remapped !== paragraph;
126
+ return remapped;
127
+ });
128
+ return boxChanged ? {
129
+ ...block,
130
+ content
131
+ } : block;
132
+ }
133
+ return block;
134
+ }
135
+ function remapNoteMarkerParagraph(block, maps) {
136
+ let changed = false;
137
+ const runs = block.runs.map((run) => {
138
+ const remapped = remapNoteMarkerRun(run, maps);
139
+ changed ||= remapped !== run;
140
+ return remapped;
141
+ });
142
+ return changed ? {
143
+ ...block,
144
+ runs
145
+ } : block;
146
+ }
147
+ function remapNoteMarkerRun(run, maps) {
148
+ if (run.kind !== "text") return run;
149
+ const displayNumber = getRunDisplayNumber(run, maps);
150
+ if (displayNumber === void 0) return run;
151
+ const text = String(displayNumber);
152
+ return run.text === text ? run : {
153
+ ...run,
154
+ text
155
+ };
156
+ }
157
+ function getRunDisplayNumber(run, maps) {
158
+ if (run.footnoteRefId !== void 0) return maps.footnoteNumbers?.get(run.footnoteRefId);
159
+ if (run.endnoteRefId !== void 0) return maps.endnoteNumbers?.get(run.endnoteRefId);
160
+ }
161
+ /**
33
162
  * After layout, determine which footnotes appear on which pages.
34
163
  * Checks each page's fragments to see if any footnoteRef PM positions fall within.
35
164
  *
@@ -279,16 +408,11 @@ function buildFootnoteContentMap(footnotes, footnoteRefs, contentWidth, options
279
408
  const contentMap = /* @__PURE__ */ new Map();
280
409
  const footnoteById = /* @__PURE__ */ new Map();
281
410
  for (const fn of footnotes) if (fn.noteType === "normal") footnoteById.set(fn.id, fn);
282
- let displayNumber = 1;
283
- const seen = /* @__PURE__ */ new Set();
284
- for (const ref of footnoteRefs) {
285
- if (seen.has(ref.footnoteId)) continue;
286
- seen.add(ref.footnoteId);
287
- const footnote = footnoteById.get(ref.footnoteId);
411
+ const displayNumbers = computeNoteDisplayNumbers(footnotes, footnoteRefs.map((ref) => ref.footnoteId));
412
+ for (const [footnoteId, displayNumber] of displayNumbers) {
413
+ const footnote = footnoteById.get(footnoteId);
288
414
  if (!footnote) continue;
289
- const content = convertFootnoteToContent(footnote, displayNumber, contentWidth, options);
290
- contentMap.set(ref.footnoteId, content);
291
- displayNumber++;
415
+ contentMap.set(footnoteId, convertFootnoteToContent(footnote, displayNumber, contentWidth, options));
292
416
  }
293
417
  return contentMap;
294
418
  }
@@ -313,4 +437,4 @@ function calculateFootnoteReservedHeights(pageFootnoteMap, footnoteContentMap) {
313
437
  return reserved;
314
438
  }
315
439
  //#endregion
316
- export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectFootnoteRefs, convertFootnoteToContent, mapFootnotesToPages };
440
+ export { FOOTNOTE_ENTRY_MARGIN_BOTTOM, FOOTNOTE_FALLBACK_LINE_HEIGHT, FOOTNOTE_SEPARATOR_HEIGHT, applyFootnotePresentation, buildFootnoteContentMap, calculateFootnoteReservedHeights, collectEndnoteRefs, collectFootnoteRefs, computeNoteDisplayNumbers, convertFootnoteToContent, mapFootnotesToPages, remapNoteMarkerText };
@@ -12,7 +12,7 @@ type HeaderFooterMetrics = {
12
12
  };
13
13
  margins: PageMargins;
14
14
  };
15
- declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[]): FlowBlock[];
15
+ declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[], section?: HeaderFooterMetrics["section"]): FlowBlock[];
16
16
  declare function resolveHeaderFooterPositionedVisualTop(position: ImageRunPosition | undefined, elementHeight: number, sourceY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
17
17
  declare function resolveHeaderFooterVisualTop(run: ImageRun, paragraphY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
18
18
  declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
@@ -20,13 +20,11 @@ declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures
20
20
  visualBottom: number;
21
21
  };
22
22
  /**
23
- * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender`
24
- * to push body margins clear of HF overflow. Excludes anchored/floating objects
25
- * (full-page letterheads, watermarks): Word positions them on the page instead
26
- * of in the header/footer flow, so they must not reserve body push-down. Keeping
27
- * them in `visualBottom` is still correct for the renderer (and for the
28
- * page-hash invalidation signal), but the margin extender needs the flow-only
29
- * extent.
23
+ * Compute flow-only header/footer bounds, excluding anchored/floating objects
24
+ * such as full-page letterheads and watermarks. Word positions those objects on
25
+ * the page independently from header/footer flow. Keeping them in
26
+ * `visualBottom` is still correct for rendering and page-hash invalidation;
27
+ * these bounds separately describe the in-flow portion for API consumers.
30
28
  */
31
29
  declare function calculateHeaderFooterMarginPushBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
32
30
  top: number;
@@ -16,13 +16,13 @@ function hasAuthoredVisualContent(block) {
16
16
  if (attrs.spacingExplicit?.before || attrs.spacingExplicit?.after) return true;
17
17
  return false;
18
18
  }
19
- function normalizeHeaderFooterMeasureBlocks(blocks) {
20
- return normalizeFlowBlockArray(blocks);
19
+ function normalizeHeaderFooterMeasureBlocks(blocks, section = "header") {
20
+ return normalizeFlowBlockArray(blocks, { suppressTrailingEmptyAfterTable: section === "header" });
21
21
  }
22
- function normalizeFlowBlockArray(blocks) {
22
+ function normalizeFlowBlockArray(blocks, normalization) {
23
23
  const trailingEmptyAfterTable = /* @__PURE__ */ new Set();
24
24
  const lastIndex = blocks.length - 1;
25
- if (lastIndex > 0) {
25
+ if (normalization.suppressTrailingEmptyAfterTable && lastIndex > 0) {
26
26
  const cur = blocks[lastIndex];
27
27
  if (blocks[lastIndex - 1]?.kind === "table" && cur?.kind === "paragraph" && cur.runs.length === 0 && !hasAuthoredVisualContent(cur)) trailingEmptyAfterTable.add(lastIndex);
28
28
  }
@@ -70,12 +70,21 @@ function normalizeFlowBlockArray(blocks) {
70
70
  };
71
71
  });
72
72
  }
73
+ function isCanonicalTrailingEmptyParagraphAfterTable(blocks, index) {
74
+ const cur = blocks[index];
75
+ const prev = blocks[index - 1];
76
+ return index === blocks.length - 1 && prev?.kind === "table" && cur?.kind === "paragraph" && isVisuallyEmptyParagraph(cur) && !hasAuthoredVisualContent(cur);
77
+ }
78
+ function isVisuallyEmptyParagraph(block) {
79
+ if (block.kind !== "paragraph") return false;
80
+ return block.runs.every((run) => run.kind === "text" && run.text.trim().length === 0);
81
+ }
73
82
  function normalizeTableBlock(block) {
74
83
  const blockState = { changed: false };
75
84
  const rows = block.rows.map((row) => {
76
85
  const rowState = { changed: false };
77
86
  const cells = row.cells.map((cell) => {
78
- const normalizedBlocks = normalizeFlowBlockArray(cell.blocks);
87
+ const normalizedBlocks = normalizeFlowBlockArray(cell.blocks, { suppressTrailingEmptyAfterTable: true });
79
88
  if (!normalizedBlocks.some((normalizedBlock, idx) => normalizedBlock !== cell.blocks[idx])) return cell;
80
89
  rowState.changed = true;
81
90
  return {
@@ -216,13 +225,11 @@ function calculateHeaderFooterVisualBounds(blocks, measures, flowHeight, metrics
216
225
  };
217
226
  }
218
227
  /**
219
- * Compute the header/footer bounds used by `computeHeaderFooterMarginExtender`
220
- * to push body margins clear of HF overflow. Excludes anchored/floating objects
221
- * (full-page letterheads, watermarks): Word positions them on the page instead
222
- * of in the header/footer flow, so they must not reserve body push-down. Keeping
223
- * them in `visualBottom` is still correct for the renderer (and for the
224
- * page-hash invalidation signal), but the margin extender needs the flow-only
225
- * extent.
228
+ * Compute flow-only header/footer bounds, excluding anchored/floating objects
229
+ * such as full-page letterheads and watermarks. Word positions those objects on
230
+ * the page independently from header/footer flow. Keeping them in
231
+ * `visualBottom` is still correct for rendering and page-hash invalidation;
232
+ * these bounds separately describe the in-flow portion for API consumers.
226
233
  */
227
234
  function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, metrics) {
228
235
  let top = 0;
@@ -232,6 +239,7 @@ function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, met
232
239
  const block = blocks[i];
233
240
  const measure = measures[i];
234
241
  if (!block || !measure) continue;
242
+ if (isCanonicalTrailingEmptyParagraphAfterTable(blocks, i)) continue;
235
243
  if (block.kind === "paragraph" && measure.kind === "paragraph") {
236
244
  const paragraphBottomY = cursorY + measure.totalHeight;
237
245
  top = Math.min(top, cursorY);
@@ -312,7 +320,7 @@ function convertHeaderFooterPmDocToContent(pmDoc, contentWidth, metrics, options
312
320
  }
313
321
  function finalizeHeaderFooterContent(blocks, contentWidth, metrics, options) {
314
322
  if (blocks.length === 0) return;
315
- const blocksForMeasure = normalizeHeaderFooterMeasureBlocks(blocks);
323
+ const blocksForMeasure = normalizeHeaderFooterMeasureBlocks(blocks, metrics.section);
316
324
  const measures = options.measureBlocks(blocksForMeasure, contentWidth);
317
325
  let flowHeight = 0;
318
326
  for (let i = 0; i < measures.length; i++) {