@harbour-enterprises/superdoc 1.5.0 → 1.6.0-next.2

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.
@@ -1,6 +1,6 @@
1
1
  import { B as Buffer$2 } from "./jszip-B1fkPkPJ.es.js";
2
2
  import { t as twipsToInches, i as inchesToTwips, p as ptToTwips, l as linesToTwips, a as twipsToLines, b as pixelsToTwips, h as halfPointToPoints, c as twipsToPixels$2, d as convertSizeToCSS, e as inchesToPixels } from "./helpers-C8e9wR5l.es.js";
3
- import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as resolveRunProperties, u as encodeMarksFromRPr, v as liftTarget, x as canJoin, y as joinPoint, z as replaceStep$1, R as ReplaceAroundStep$1, B as htmlHandler, C as ReplaceStep, E as getResolvedParagraphProperties, G as changeListLevel, H as isList$1, I as updateNumberingProperties, L as ListHelpers, J as inputRulesPlugin, K as TrackDeleteMarkName$1, O as TrackInsertMarkName$1, Q as TrackFormatMarkName$1, U as AddMarkStep, V as RemoveMarkStep, W as CommandService, S as SuperConverter, X as EditorState, Y as unflattenListsInHtml, Z as SelectionRange, _ as Transform, $ as createOoxmlResolver, a0 as translator, a1 as translator$1, a2 as resolveDocxFontFamily, a3 as combineIndentProperties, a4 as _getReferencedTableStyles, a5 as decodeRPrFromMarks, a6 as calculateResolvedParagraphProperties, a7 as encodeCSSFromPPr, a8 as encodeCSSFromRPr, a9 as generateOrderedListIndex, aa as docxNumberingHelpers, ab as InputRule, ac as insertNewRelationship, ad as kebabCase$1, ae as getUnderlineCssString } from "./SuperConverter-Dfxp14RO.es.js";
3
+ import { g as generateDocxRandomId, T as TextSelection$1, o as objectIncludes, w as wrapTextsInRuns, D as DOMParser$1, c as createDocFromMarkdown, a as createDocFromHTML, b as chainableEditorState, d as convertMarkdownToHTML, f as findParentNode, e as findParentNodeClosestToPos, h as generateRandom32BitHex, i as generateRandomSigned32BitIntStrId, P as PluginKey, j as Plugin, M as Mapping, N as NodeSelection, k as Selection, l as Slice, m as DOMSerializer, F as Fragment, n as Mark$1, p as dropPoint, A as AllSelection, q as Schema$1, s as canSplit, t as resolveRunProperties, u as encodeMarksFromRPr, v as liftTarget, x as canJoin, y as joinPoint, z as replaceStep$1, R as ReplaceAroundStep$1, B as htmlHandler, C as ReplaceStep, E as getResolvedParagraphProperties, G as changeListLevel, H as isList$1, I as updateNumberingProperties, L as ListHelpers, J as inputRulesPlugin, K as TrackDeleteMarkName$1, O as TrackInsertMarkName$1, Q as TrackFormatMarkName$1, U as AddMarkStep, V as RemoveMarkStep, W as CommandService, S as SuperConverter, X as EditorState, Y as unflattenListsInHtml, Z as SelectionRange, _ as Transform, $ as createOoxmlResolver, a0 as translator, a1 as translator$1, a2 as resolveDocxFontFamily, a3 as combineIndentProperties, a4 as _getReferencedTableStyles, a5 as decodeRPrFromMarks, a6 as calculateResolvedParagraphProperties, a7 as encodeCSSFromPPr, a8 as encodeCSSFromRPr, a9 as generateOrderedListIndex, aa as docxNumberingHelpers, ab as InputRule, ac as insertNewRelationship, ad as kebabCase$1, ae as getUnderlineCssString } from "./SuperConverter-BAUfsE-s.es.js";
4
4
  import { p as process$1, r as ref, C as global$1, c as computed, E as createElementBlock, F as Fragment$1, S as renderList, O as withModifiers, G as openBlock, P as normalizeClass, M as createCommentVNode, H as toDisplayString, K as createBaseVNode, U as createApp, f as onMounted, X as onUnmounted, R as withDirectives, v as unref, Y as vModelText, y as nextTick, L as normalizeStyle, u as watch, Z as withKeys, _ as createTextVNode, I as createVNode, h as h$1, $ as readonly, s as getCurrentInstance, o as onBeforeUnmount, j as reactive, b as onBeforeMount, i as inject, a0 as onActivated, a1 as onDeactivated, a2 as Comment, d as defineComponent, a as provide, g as Teleport, t as toRef, a3 as renderSlot, a4 as isVNode, D as shallowRef, w as watchEffect, T as Transition, a5 as mergeProps, a6 as vShow, a7 as cloneVNode, a8 as Text$2, m as markRaw, N as createBlock, J as withCtx, a9 as useCssVars, V as resolveDynamicComponent, aa as normalizeProps, ab as guardReactiveProps } from "./vue-BnBKJwCW.es.js";
5
5
  import "./jszip.min-DCl8qkFO.es.js";
6
6
  import { E as EventEmitter$1 } from "./eventemitter3-CwrdEv8r.es.js";
@@ -9370,6 +9370,158 @@ class Schema {
9370
9370
  return Object.fromEntries(markEntries);
9371
9371
  }
9372
9372
  }
9373
+ const positionTrackerKey = new PluginKey("positionTracker");
9374
+ function createPositionTrackerPlugin() {
9375
+ return new Plugin({
9376
+ key: positionTrackerKey,
9377
+ state: {
9378
+ init() {
9379
+ return {
9380
+ decorations: DecorationSet.empty,
9381
+ generation: 0
9382
+ };
9383
+ },
9384
+ apply(tr, state) {
9385
+ let { decorations, generation } = state;
9386
+ const meta = tr.getMeta(positionTrackerKey);
9387
+ if (meta?.action === "add") {
9388
+ decorations = decorations.add(tr.doc, meta.decorations);
9389
+ } else if (meta?.action === "remove") {
9390
+ const toRemove = decorations.find().filter((decoration) => meta.ids.includes(decoration.spec.id));
9391
+ decorations = decorations.remove(toRemove);
9392
+ } else if (meta?.action === "removeByType") {
9393
+ const toRemove = decorations.find().filter((decoration) => decoration.spec.type === meta.type);
9394
+ decorations = decorations.remove(toRemove);
9395
+ }
9396
+ if (tr.docChanged) {
9397
+ decorations = decorations.map(tr.mapping, tr.doc);
9398
+ generation += 1;
9399
+ }
9400
+ return { decorations, generation };
9401
+ }
9402
+ },
9403
+ props: {
9404
+ decorations() {
9405
+ return DecorationSet.empty;
9406
+ }
9407
+ }
9408
+ });
9409
+ }
9410
+ class PositionTracker {
9411
+ #editor;
9412
+ constructor(editor) {
9413
+ this.#editor = editor;
9414
+ }
9415
+ #getState() {
9416
+ if (!this.#editor?.state) return null;
9417
+ return positionTrackerKey.getState(this.#editor.state) ?? null;
9418
+ }
9419
+ track(from3, to, spec) {
9420
+ const id = v4();
9421
+ if (!this.#editor?.state) return id;
9422
+ const fullSpec = { kind: "range", ...spec, id };
9423
+ const deco = Decoration.inline(from3, to, {}, fullSpec);
9424
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9425
+ action: "add",
9426
+ decorations: [deco]
9427
+ }).setMeta("addToHistory", false);
9428
+ this.#editor.dispatch(tr);
9429
+ return id;
9430
+ }
9431
+ trackMany(ranges) {
9432
+ if (!this.#editor?.state) {
9433
+ return ranges.map(() => v4());
9434
+ }
9435
+ const ids = [];
9436
+ const decorations = [];
9437
+ for (const { from: from3, to, spec } of ranges) {
9438
+ const id = v4();
9439
+ ids.push(id);
9440
+ const fullSpec = { kind: "range", ...spec, id };
9441
+ decorations.push(Decoration.inline(from3, to, {}, fullSpec));
9442
+ }
9443
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9444
+ action: "add",
9445
+ decorations
9446
+ }).setMeta("addToHistory", false);
9447
+ this.#editor.dispatch(tr);
9448
+ return ids;
9449
+ }
9450
+ untrack(id) {
9451
+ if (!this.#editor?.state) return;
9452
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9453
+ action: "remove",
9454
+ ids: [id]
9455
+ }).setMeta("addToHistory", false);
9456
+ this.#editor.dispatch(tr);
9457
+ }
9458
+ untrackMany(ids) {
9459
+ if (!this.#editor?.state || ids.length === 0) return;
9460
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9461
+ action: "remove",
9462
+ ids
9463
+ }).setMeta("addToHistory", false);
9464
+ this.#editor.dispatch(tr);
9465
+ }
9466
+ untrackByType(type) {
9467
+ if (!this.#editor?.state) return;
9468
+ const tr = this.#editor.state.tr.setMeta(positionTrackerKey, {
9469
+ action: "removeByType",
9470
+ type
9471
+ }).setMeta("addToHistory", false);
9472
+ this.#editor.dispatch(tr);
9473
+ }
9474
+ resolve(id) {
9475
+ const state = this.#getState();
9476
+ if (!state) return null;
9477
+ const found = state.decorations.find().find((decoration) => decoration.spec.id === id);
9478
+ if (!found) return null;
9479
+ const spec = found.spec;
9480
+ return {
9481
+ id: spec.id,
9482
+ from: found.from,
9483
+ to: found.to,
9484
+ spec
9485
+ };
9486
+ }
9487
+ resolveMany(ids) {
9488
+ const result = /* @__PURE__ */ new Map();
9489
+ for (const id of ids) {
9490
+ result.set(id, null);
9491
+ }
9492
+ const state = this.#getState();
9493
+ if (!state || ids.length === 0) return result;
9494
+ const idSet = new Set(ids);
9495
+ for (const decoration of state.decorations.find()) {
9496
+ const spec = decoration.spec;
9497
+ if (idSet.has(spec.id)) {
9498
+ result.set(spec.id, {
9499
+ id: spec.id,
9500
+ from: decoration.from,
9501
+ to: decoration.to,
9502
+ spec
9503
+ });
9504
+ }
9505
+ }
9506
+ return result;
9507
+ }
9508
+ findByType(type) {
9509
+ const state = this.#getState();
9510
+ if (!state) return [];
9511
+ return state.decorations.find().filter((decoration) => decoration.spec.type === type).map((decoration) => {
9512
+ const spec = decoration.spec;
9513
+ return {
9514
+ id: spec.id,
9515
+ from: decoration.from,
9516
+ to: decoration.to,
9517
+ spec
9518
+ };
9519
+ });
9520
+ }
9521
+ get generation() {
9522
+ return this.#getState()?.generation ?? 0;
9523
+ }
9524
+ }
9373
9525
  class OxmlNode extends Node$1 {
9374
9526
  constructor(config) {
9375
9527
  super(config);
@@ -11730,6 +11882,34 @@ const EditorFocus = Extension.create({
11730
11882
  return [editorFocusPlugin];
11731
11883
  }
11732
11884
  });
11885
+ const PositionTrackerExtension = Extension.create({
11886
+ name: "positionTracker",
11887
+ addStorage() {
11888
+ return {
11889
+ tracker: null
11890
+ };
11891
+ },
11892
+ addPmPlugins() {
11893
+ return [createPositionTrackerPlugin()];
11894
+ },
11895
+ onCreate() {
11896
+ const existing = this.editor?.positionTracker ?? this.storage.tracker;
11897
+ if (existing) {
11898
+ this.storage.tracker = existing;
11899
+ this.editor.positionTracker = existing;
11900
+ return;
11901
+ }
11902
+ const tracker = new PositionTracker(this.editor);
11903
+ this.storage.tracker = tracker;
11904
+ this.editor.positionTracker = tracker;
11905
+ },
11906
+ onDestroy() {
11907
+ if (this.editor?.positionTracker === this.storage.tracker) {
11908
+ this.editor.positionTracker = null;
11909
+ }
11910
+ this.storage.tracker = null;
11911
+ }
11912
+ });
11733
11913
  class EventEmitter {
11734
11914
  #events = /* @__PURE__ */ new Map();
11735
11915
  /**
@@ -15507,7 +15687,7 @@ const canUseDOM = () => {
15507
15687
  return false;
15508
15688
  }
15509
15689
  };
15510
- const summaryVersion = "1.5.0";
15690
+ const summaryVersion = "1.6.0-next.2";
15511
15691
  const nodeKeys = ["group", "content", "marks", "inline", "atom", "defining", "code", "tableRole", "summary"];
15512
15692
  const markKeys = ["group", "inclusive", "excludes", "spanning", "code"];
15513
15693
  function mapAttributes(attrs) {
@@ -17000,7 +17180,7 @@ class Editor extends EventEmitter {
17000
17180
  */
17001
17181
  #createExtensionService() {
17002
17182
  const allowedExtensions = ["extension", "node", "mark"];
17003
- const coreExtensions = [Editable, Commands, EditorFocus, Keymap];
17183
+ const coreExtensions = [Editable, Commands, EditorFocus, Keymap, PositionTrackerExtension];
17004
17184
  const externalExtensions = this.options.externalExtensions || [];
17005
17185
  const allExtensions = [...coreExtensions, ...this.options.extensions].filter((extension) => {
17006
17186
  const extensionType = typeof extension?.type === "string" ? extension.type : void 0;
@@ -17764,6 +17944,10 @@ class Editor extends EventEmitter {
17764
17944
  const hasCustomSettings = !!this.converter.convertedXml["word/settings.xml"]?.elements?.length;
17765
17945
  const customSettings = hasCustomSettings ? this.converter.schemaToXml(this.converter.convertedXml["word/settings.xml"]?.elements?.[0]) : null;
17766
17946
  const rels = this.converter.schemaToXml(this.converter.convertedXml["word/_rels/document.xml.rels"].elements[0]);
17947
+ const footnotesData = this.converter.convertedXml["word/footnotes.xml"];
17948
+ const footnotesXml = footnotesData?.elements?.[0] ? this.converter.schemaToXml(footnotesData.elements[0]) : null;
17949
+ const footnotesRelsData = this.converter.convertedXml["word/_rels/footnotes.xml.rels"];
17950
+ const footnotesRelsXml = footnotesRelsData?.elements?.[0] ? this.converter.schemaToXml(footnotesRelsData.elements[0]) : null;
17767
17951
  const media = this.converter.addedMedia;
17768
17952
  const updatedHeadersFooters = {};
17769
17953
  Object.entries(this.converter.convertedXml).forEach(([name, json2]) => {
@@ -17788,6 +17972,12 @@ class Editor extends EventEmitter {
17788
17972
  if (hasCustomSettings) {
17789
17973
  updatedDocs["word/settings.xml"] = String(customSettings);
17790
17974
  }
17975
+ if (footnotesXml) {
17976
+ updatedDocs["word/footnotes.xml"] = String(footnotesXml);
17977
+ }
17978
+ if (footnotesRelsXml) {
17979
+ updatedDocs["word/_rels/footnotes.xml.rels"] = String(footnotesRelsXml);
17980
+ }
17791
17981
  if (comments.length) {
17792
17982
  const commentsXml = this.converter.schemaToXml(this.converter.convertedXml["word/comments.xml"].elements[0]);
17793
17983
  const commentsExtendedXml = this.converter.schemaToXml(
@@ -18164,7 +18354,7 @@ class Editor extends EventEmitter {
18164
18354
  * Process collaboration migrations
18165
18355
  */
18166
18356
  processCollaborationMigrations() {
18167
- console.debug("[checkVersionMigrations] Current editor version", "1.5.0");
18357
+ console.debug("[checkVersionMigrations] Current editor version", "1.6.0-next.2");
18168
18358
  if (!this.options.ydoc) return;
18169
18359
  const metaMap = this.options.ydoc.getMap("meta");
18170
18360
  let docVersion = metaMap.get("version");
@@ -34045,7 +34235,7 @@ function getParagraphSpacingBefore(block) {
34045
34235
  const value = spacing?.before ?? spacing?.lineSpaceBefore;
34046
34236
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
34047
34237
  }
34048
- function getParagraphSpacingAfter(block) {
34238
+ function getParagraphSpacingAfter$1(block) {
34049
34239
  const spacing = block.attrs?.spacing;
34050
34240
  const value = spacing?.after ?? spacing?.lineSpaceAfter;
34051
34241
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
@@ -34072,7 +34262,7 @@ function getMeasureHeight(block, measure) {
34072
34262
  }
34073
34263
  const DEFAULT_PAGE_SIZE$2 = { w: 612, h: 792 };
34074
34264
  const DEFAULT_MARGINS$2 = { top: 72, right: 72, bottom: 72, left: 72 };
34075
- const COLUMN_EPSILON = 1e-4;
34265
+ const COLUMN_EPSILON$1 = 1e-4;
34076
34266
  const layoutDebugEnabled$1 = typeof process$1 !== "undefined" && typeof process$1.env !== "undefined" && Boolean(process$1.env.SD_DEBUG_LAYOUT);
34077
34267
  const layoutLog = (...args) => {
34078
34268
  if (!layoutDebugEnabled$1) return;
@@ -34383,7 +34573,13 @@ function layoutDocument(blocks, measures, options = {}) {
34383
34573
  const paginator = createPaginator({
34384
34574
  margins: paginatorMargins,
34385
34575
  getActiveTopMargin: () => activeTopMargin,
34386
- getActiveBottomMargin: () => activeBottomMargin,
34576
+ getActiveBottomMargin: () => {
34577
+ const reserves = options.footnoteReservedByPageIndex;
34578
+ const pageIndex = Math.max(0, pageCount - 1);
34579
+ const reserve = Array.isArray(reserves) ? reserves[pageIndex] : 0;
34580
+ const reservePx = typeof reserve === "number" && Number.isFinite(reserve) && reserve > 0 ? reserve : 0;
34581
+ return activeBottomMargin + reservePx;
34582
+ },
34387
34583
  getActiveHeaderDistance: () => activeHeaderDistance,
34388
34584
  getActiveFooterDistance: () => activeFooterDistance,
34389
34585
  getActivePageSize: () => activePageSize,
@@ -34867,7 +35063,7 @@ function layoutDocument(blocks, measures, options = {}) {
34867
35063
  if (!shouldSkipAnchoredTable) {
34868
35064
  let state = paginator.ensurePage();
34869
35065
  const availableHeight = state.contentBottom - state.cursorY;
34870
- const spacingAfter = getParagraphSpacingAfter(paraBlock);
35066
+ const spacingAfter = getParagraphSpacingAfter$1(paraBlock);
34871
35067
  const currentHeight = getMeasureHeight(paraBlock, measure);
34872
35068
  const nextHeight = getMeasureHeight(nextBlock, nextMeasure);
34873
35069
  const nextIsParagraph = nextBlock.kind === "paragraph" && nextMeasure.kind === "paragraph";
@@ -35180,7 +35376,7 @@ function normalizeColumns(input, contentWidth) {
35180
35376
  const gap = Math.max(0, input?.gap ?? 0);
35181
35377
  const totalGap = gap * (count - 1);
35182
35378
  const width = (contentWidth - totalGap) / count;
35183
- if (width <= COLUMN_EPSILON) {
35379
+ if (width <= COLUMN_EPSILON$1) {
35184
35380
  return {
35185
35381
  count: 1,
35186
35382
  gap: 0,
@@ -36943,6 +37139,417 @@ const perfLog = (...args) => {
36943
37139
  if (!layoutDebugEnabled) return;
36944
37140
  console.log(...args);
36945
37141
  };
37142
+ const isFootnotesLayoutInput = (value) => {
37143
+ if (!value || typeof value !== "object") return false;
37144
+ const v = value;
37145
+ if (!Array.isArray(v.refs)) return false;
37146
+ if (!(v.blocksById instanceof Map)) return false;
37147
+ return true;
37148
+ };
37149
+ const findPageIndexForPos = (layout, pos) => {
37150
+ if (!Number.isFinite(pos)) return null;
37151
+ const fallbackRanges = [];
37152
+ for (let pageIndex = 0; pageIndex < layout.pages.length; pageIndex++) {
37153
+ const page = layout.pages[pageIndex];
37154
+ let minStart = null;
37155
+ let maxEnd = null;
37156
+ for (const fragment of page.fragments) {
37157
+ const pmStart = fragment.pmStart;
37158
+ const pmEnd = fragment.pmEnd;
37159
+ if (pmStart == null || pmEnd == null) continue;
37160
+ if (minStart == null || pmStart < minStart) minStart = pmStart;
37161
+ if (maxEnd == null || pmEnd > maxEnd) maxEnd = pmEnd;
37162
+ if (pos >= pmStart && pos <= pmEnd) {
37163
+ return pageIndex;
37164
+ }
37165
+ }
37166
+ fallbackRanges[pageIndex] = minStart != null && maxEnd != null ? { pageIndex, minStart, maxEnd } : null;
37167
+ }
37168
+ let best = null;
37169
+ for (const entry of fallbackRanges) {
37170
+ if (!entry) continue;
37171
+ const distance = pos < entry.minStart ? entry.minStart - pos : pos > entry.maxEnd ? pos - entry.maxEnd : 0;
37172
+ if (!best || distance < best.distance) {
37173
+ best = { pageIndex: entry.pageIndex, distance };
37174
+ }
37175
+ }
37176
+ if (best) return best.pageIndex;
37177
+ if (layout.pages.length > 0) return layout.pages.length - 1;
37178
+ return null;
37179
+ };
37180
+ const footnoteColumnKey = (pageIndex, columnIndex) => `${pageIndex}:${columnIndex}`;
37181
+ const COLUMN_EPSILON = 0.01;
37182
+ const normalizeColumnsForFootnotes = (input, contentWidth) => {
37183
+ const rawCount = Number.isFinite(input?.count) ? Math.floor(input.count) : 1;
37184
+ const count = Math.max(1, rawCount || 1);
37185
+ const gap = Math.max(0, input?.gap ?? 0);
37186
+ const totalGap = gap * (count - 1);
37187
+ const width = (contentWidth - totalGap) / count;
37188
+ if (!Number.isFinite(width) || width <= COLUMN_EPSILON) {
37189
+ return {
37190
+ count: 1,
37191
+ gap: 0,
37192
+ width: Math.max(0, contentWidth)
37193
+ };
37194
+ }
37195
+ return { count, gap, width };
37196
+ };
37197
+ const resolveSectionColumnsByIndex = (options, blocks) => {
37198
+ const result = /* @__PURE__ */ new Map();
37199
+ let activeColumns = options.columns ?? { count: 1, gap: 0 };
37200
+ if (blocks && blocks.length > 0) {
37201
+ for (const block of blocks) {
37202
+ if (block.kind !== "sectionBreak") continue;
37203
+ const sectionIndexRaw = block.attrs?.sectionIndex;
37204
+ const sectionIndex = typeof sectionIndexRaw === "number" && Number.isFinite(sectionIndexRaw) ? sectionIndexRaw : result.size;
37205
+ if (block.columns) {
37206
+ activeColumns = { count: block.columns.count, gap: block.columns.gap };
37207
+ }
37208
+ result.set(sectionIndex, { ...activeColumns });
37209
+ }
37210
+ }
37211
+ if (result.size === 0) {
37212
+ result.set(0, { ...activeColumns });
37213
+ }
37214
+ return result;
37215
+ };
37216
+ const resolvePageColumns = (layout, options, blocks) => {
37217
+ const sectionColumns = resolveSectionColumnsByIndex(options, blocks);
37218
+ const result = /* @__PURE__ */ new Map();
37219
+ for (let pageIndex = 0; pageIndex < layout.pages.length; pageIndex += 1) {
37220
+ const page = layout.pages[pageIndex];
37221
+ const pageSize = page.size ?? layout.pageSize ?? DEFAULT_PAGE_SIZE$1;
37222
+ const marginLeft = normalizeMargin(
37223
+ page.margins?.left,
37224
+ normalizeMargin(options.margins?.left, DEFAULT_MARGINS$1.left)
37225
+ );
37226
+ const marginRight = normalizeMargin(
37227
+ page.margins?.right,
37228
+ normalizeMargin(options.margins?.right, DEFAULT_MARGINS$1.right)
37229
+ );
37230
+ const contentWidth = pageSize.w - (marginLeft + marginRight);
37231
+ const sectionIndex = page.sectionIndex ?? 0;
37232
+ const columnsConfig = sectionColumns.get(sectionIndex) ?? options.columns ?? { count: 1, gap: 0 };
37233
+ const normalized = normalizeColumnsForFootnotes(columnsConfig, contentWidth);
37234
+ result.set(pageIndex, { ...normalized, left: marginLeft, contentWidth });
37235
+ }
37236
+ return result;
37237
+ };
37238
+ const findFragmentForPos = (page, pos) => {
37239
+ for (const fragment of page.fragments) {
37240
+ const pmStart = fragment.pmStart;
37241
+ const pmEnd = fragment.pmEnd;
37242
+ if (pmStart == null || pmEnd == null) continue;
37243
+ if (pos >= pmStart && pos <= pmEnd) {
37244
+ return fragment;
37245
+ }
37246
+ }
37247
+ return null;
37248
+ };
37249
+ const assignFootnotesToColumns = (layout, refs, pageColumns) => {
37250
+ const result = /* @__PURE__ */ new Map();
37251
+ const seenByColumn = /* @__PURE__ */ new Map();
37252
+ for (const ref2 of refs) {
37253
+ const pageIndex = findPageIndexForPos(layout, ref2.pos);
37254
+ if (pageIndex == null) continue;
37255
+ const columns = pageColumns.get(pageIndex);
37256
+ const page = layout.pages[pageIndex];
37257
+ let columnIndex = 0;
37258
+ if (columns && columns.count > 1 && page) {
37259
+ const fragment = findFragmentForPos(page, ref2.pos);
37260
+ if (fragment && typeof fragment.x === "number") {
37261
+ const columnStride = columns.width + columns.gap;
37262
+ const rawIndex = columnStride > 0 ? Math.floor((fragment.x - columns.left) / columnStride) : 0;
37263
+ columnIndex = Math.max(0, Math.min(columns.count - 1, rawIndex));
37264
+ }
37265
+ }
37266
+ const key2 = footnoteColumnKey(pageIndex, columnIndex);
37267
+ let seen = seenByColumn.get(key2);
37268
+ if (!seen) {
37269
+ seen = /* @__PURE__ */ new Set();
37270
+ seenByColumn.set(key2, seen);
37271
+ }
37272
+ if (seen.has(ref2.id)) continue;
37273
+ seen.add(ref2.id);
37274
+ const pageMap = result.get(pageIndex) ?? /* @__PURE__ */ new Map();
37275
+ const list = pageMap.get(columnIndex) ?? [];
37276
+ list.push(ref2.id);
37277
+ pageMap.set(columnIndex, list);
37278
+ result.set(pageIndex, pageMap);
37279
+ }
37280
+ return result;
37281
+ };
37282
+ const resolveFootnoteMeasurementWidth = (options, blocks) => {
37283
+ const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE$1;
37284
+ const margins = {
37285
+ right: normalizeMargin(options.margins?.right, DEFAULT_MARGINS$1.right),
37286
+ left: normalizeMargin(options.margins?.left, DEFAULT_MARGINS$1.left)
37287
+ };
37288
+ let width = pageSize.w - (margins.left + margins.right);
37289
+ let activeColumns = options.columns ?? { count: 1, gap: 0 };
37290
+ let activePageSize = pageSize;
37291
+ let activeMargins = { ...margins };
37292
+ const resolveColumnWidth = () => {
37293
+ const contentWidth = activePageSize.w - (activeMargins.left + activeMargins.right);
37294
+ const normalized = normalizeColumnsForFootnotes(activeColumns, contentWidth);
37295
+ return normalized.width;
37296
+ };
37297
+ width = resolveColumnWidth();
37298
+ if (blocks && blocks.length > 0) {
37299
+ for (const block of blocks) {
37300
+ if (block.kind !== "sectionBreak") continue;
37301
+ activePageSize = block.pageSize ?? activePageSize;
37302
+ activeMargins = {
37303
+ right: normalizeMargin(block.margins?.right, activeMargins.right),
37304
+ left: normalizeMargin(block.margins?.left, activeMargins.left)
37305
+ };
37306
+ if (block.columns) {
37307
+ activeColumns = { count: block.columns.count, gap: block.columns.gap };
37308
+ }
37309
+ const w = resolveColumnWidth();
37310
+ if (w > 0 && w < width) width = w;
37311
+ }
37312
+ }
37313
+ if (!Number.isFinite(width) || width <= 0) return 0;
37314
+ return width;
37315
+ };
37316
+ const MIN_FOOTNOTE_BODY_HEIGHT = 1;
37317
+ const DEFAULT_FOOTNOTE_SEPARATOR_SPACING_BEFORE = 12;
37318
+ const computeMaxFootnoteReserve = (layoutForPages, pageIndex, baseReserve = 0) => {
37319
+ const page = layoutForPages.pages?.[pageIndex];
37320
+ if (!page) return 0;
37321
+ const pageSize = page.size ?? layoutForPages.pageSize ?? DEFAULT_PAGE_SIZE$1;
37322
+ const topMargin = normalizeMargin(page.margins?.top, DEFAULT_MARGINS$1.top);
37323
+ const bottomWithReserve = normalizeMargin(page.margins?.bottom, DEFAULT_MARGINS$1.bottom);
37324
+ const baseReserveSafe = Number.isFinite(baseReserve) ? Math.max(0, baseReserve) : 0;
37325
+ const bottomMargin = Math.max(0, bottomWithReserve - baseReserveSafe);
37326
+ const availableForBody = pageSize.h - topMargin - bottomMargin;
37327
+ if (!Number.isFinite(availableForBody)) return 0;
37328
+ return Math.max(0, availableForBody - MIN_FOOTNOTE_BODY_HEIGHT);
37329
+ };
37330
+ const sumLineHeights$1 = (lines, fromLine, toLine) => {
37331
+ if (!lines || fromLine >= toLine) return 0;
37332
+ let total = 0;
37333
+ for (let i = fromLine; i < toLine; i += 1) {
37334
+ total += lines[i]?.lineHeight ?? 0;
37335
+ }
37336
+ return total;
37337
+ };
37338
+ const getParagraphSpacingAfter = (block) => {
37339
+ const spacing = block.attrs?.spacing;
37340
+ const value = spacing?.after ?? spacing?.lineSpaceAfter;
37341
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
37342
+ };
37343
+ const resolveSeparatorSpacingBefore = (rangesByFootnoteId, measuresById, explicitValue, fallbackValue) => {
37344
+ if (typeof explicitValue === "number" && Number.isFinite(explicitValue)) {
37345
+ return Math.max(0, explicitValue);
37346
+ }
37347
+ for (const ranges of rangesByFootnoteId.values()) {
37348
+ for (const range of ranges) {
37349
+ if (range.kind === "paragraph") {
37350
+ const measure = measuresById.get(range.blockId);
37351
+ if (measure?.kind !== "paragraph") continue;
37352
+ const lineHeight2 = measure.lines?.[range.fromLine]?.lineHeight ?? measure.lines?.[0]?.lineHeight;
37353
+ if (typeof lineHeight2 === "number" && Number.isFinite(lineHeight2) && lineHeight2 > 0) {
37354
+ return lineHeight2;
37355
+ }
37356
+ }
37357
+ if (range.kind === "list-item") {
37358
+ const measure = measuresById.get(range.blockId);
37359
+ if (measure?.kind !== "list") continue;
37360
+ const itemMeasure = measure.items.find((item) => item.itemId === range.itemId);
37361
+ const lineHeight2 = itemMeasure?.paragraph?.lines?.[range.fromLine]?.lineHeight ?? itemMeasure?.paragraph?.lines?.[0]?.lineHeight;
37362
+ if (typeof lineHeight2 === "number" && Number.isFinite(lineHeight2) && lineHeight2 > 0) {
37363
+ return lineHeight2;
37364
+ }
37365
+ }
37366
+ }
37367
+ }
37368
+ return Math.max(0, fallbackValue);
37369
+ };
37370
+ const getRangeRenderHeight = (range) => {
37371
+ if (range.kind === "paragraph" || range.kind === "list-item") {
37372
+ const spacing = range.toLine >= range.totalLines ? range.spacingAfter : 0;
37373
+ return range.height + spacing;
37374
+ }
37375
+ return range.height;
37376
+ };
37377
+ const buildFootnoteRanges = (blocks, measuresById) => {
37378
+ const ranges = [];
37379
+ blocks.forEach((block) => {
37380
+ const measure = measuresById.get(block.id);
37381
+ if (!measure) return;
37382
+ if (block.kind === "paragraph") {
37383
+ if (measure.kind !== "paragraph") return;
37384
+ const lineCount = measure.lines?.length ?? 0;
37385
+ if (lineCount === 0) return;
37386
+ ranges.push({
37387
+ kind: "paragraph",
37388
+ blockId: block.id,
37389
+ fromLine: 0,
37390
+ toLine: lineCount,
37391
+ totalLines: lineCount,
37392
+ height: sumLineHeights$1(measure.lines, 0, lineCount),
37393
+ spacingAfter: getParagraphSpacingAfter(block)
37394
+ });
37395
+ return;
37396
+ }
37397
+ if (block.kind === "list") {
37398
+ if (measure.kind !== "list") return;
37399
+ block.items.forEach((item) => {
37400
+ const itemMeasure = measure.items.find((entry) => entry.itemId === item.id);
37401
+ if (!itemMeasure) return;
37402
+ const lineCount = itemMeasure.paragraph.lines?.length ?? 0;
37403
+ if (lineCount === 0) return;
37404
+ ranges.push({
37405
+ kind: "list-item",
37406
+ blockId: block.id,
37407
+ itemId: item.id,
37408
+ fromLine: 0,
37409
+ toLine: lineCount,
37410
+ totalLines: lineCount,
37411
+ height: sumLineHeights$1(itemMeasure.paragraph.lines, 0, lineCount),
37412
+ spacingAfter: getParagraphSpacingAfter(item.paragraph)
37413
+ });
37414
+ });
37415
+ return;
37416
+ }
37417
+ if (block.kind === "table" && measure.kind === "table") {
37418
+ const height = Math.max(0, measure.totalHeight ?? 0);
37419
+ if (height > 0) {
37420
+ ranges.push({ kind: "table", blockId: block.id, height });
37421
+ }
37422
+ return;
37423
+ }
37424
+ if (block.kind === "image" && measure.kind === "image") {
37425
+ const height = Math.max(0, measure.height ?? 0);
37426
+ if (height > 0) {
37427
+ ranges.push({ kind: "image", blockId: block.id, height });
37428
+ }
37429
+ return;
37430
+ }
37431
+ if (block.kind === "drawing" && measure.kind === "drawing") {
37432
+ const height = Math.max(0, measure.height ?? 0);
37433
+ if (height > 0) {
37434
+ ranges.push({ kind: "drawing", blockId: block.id, height });
37435
+ }
37436
+ }
37437
+ });
37438
+ return ranges;
37439
+ };
37440
+ const splitRangeAtHeight = (range, availableHeight, measuresById) => {
37441
+ if (availableHeight <= 0) return { fitted: null, remaining: range };
37442
+ if (range.kind !== "paragraph") {
37443
+ return getRangeRenderHeight(range) <= availableHeight ? { fitted: range, remaining: null } : { fitted: null, remaining: range };
37444
+ }
37445
+ const measure = measuresById.get(range.blockId);
37446
+ if (!measure || measure.kind !== "paragraph" || !measure.lines) {
37447
+ return getRangeRenderHeight(range) <= availableHeight ? { fitted: range, remaining: null } : { fitted: null, remaining: range };
37448
+ }
37449
+ let accumulatedHeight = 0;
37450
+ let splitLine = range.fromLine;
37451
+ for (let i = range.fromLine; i < range.toLine; i += 1) {
37452
+ const lineHeight2 = measure.lines[i]?.lineHeight ?? 0;
37453
+ if (accumulatedHeight + lineHeight2 > availableHeight) break;
37454
+ accumulatedHeight += lineHeight2;
37455
+ splitLine = i + 1;
37456
+ }
37457
+ if (splitLine === range.fromLine) {
37458
+ return { fitted: null, remaining: range };
37459
+ }
37460
+ const fitted = {
37461
+ ...range,
37462
+ toLine: splitLine,
37463
+ height: sumLineHeights$1(measure.lines, range.fromLine, splitLine)
37464
+ };
37465
+ if (splitLine >= range.toLine) {
37466
+ return getRangeRenderHeight(fitted) <= availableHeight ? { fitted, remaining: null } : { fitted: null, remaining: range };
37467
+ }
37468
+ const remaining = {
37469
+ ...range,
37470
+ fromLine: splitLine,
37471
+ height: sumLineHeights$1(measure.lines, splitLine, range.toLine)
37472
+ };
37473
+ return { fitted, remaining };
37474
+ };
37475
+ const forceFitFirstRange = (range, measuresById) => {
37476
+ if (range.kind !== "paragraph") {
37477
+ return { fitted: range, remaining: null };
37478
+ }
37479
+ const measure = measuresById.get(range.blockId);
37480
+ if (!measure || measure.kind !== "paragraph" || !measure.lines?.length) {
37481
+ return { fitted: range, remaining: null };
37482
+ }
37483
+ const nextLine = Math.min(range.fromLine + 1, range.toLine);
37484
+ const fitted = {
37485
+ ...range,
37486
+ toLine: nextLine,
37487
+ height: sumLineHeights$1(measure.lines, range.fromLine, nextLine)
37488
+ };
37489
+ if (nextLine >= range.toLine) {
37490
+ return { fitted, remaining: null };
37491
+ }
37492
+ const remaining = {
37493
+ ...range,
37494
+ fromLine: nextLine,
37495
+ height: sumLineHeights$1(measure.lines, nextLine, range.toLine)
37496
+ };
37497
+ return { fitted, remaining };
37498
+ };
37499
+ const fitFootnoteContent = (id, inputRanges, availableHeight, pageIndex, columnIndex, isContinuation, measuresById, forceFirstRange) => {
37500
+ const fittedRanges = [];
37501
+ let remainingRanges = [];
37502
+ let usedHeight = 0;
37503
+ const maxHeight = Math.max(0, availableHeight);
37504
+ for (let index2 = 0; index2 < inputRanges.length; index2 += 1) {
37505
+ const range = inputRanges[index2];
37506
+ const remainingSpace = maxHeight - usedHeight;
37507
+ const rangeHeight = getRangeRenderHeight(range);
37508
+ if (rangeHeight <= remainingSpace) {
37509
+ fittedRanges.push(range);
37510
+ usedHeight += rangeHeight;
37511
+ continue;
37512
+ }
37513
+ if (range.kind === "paragraph") {
37514
+ const split = splitRangeAtHeight(range, remainingSpace, measuresById);
37515
+ if (split.fitted && getRangeRenderHeight(split.fitted) <= remainingSpace) {
37516
+ fittedRanges.push(split.fitted);
37517
+ usedHeight += getRangeRenderHeight(split.fitted);
37518
+ }
37519
+ if (split.remaining) {
37520
+ remainingRanges = [split.remaining, ...inputRanges.slice(index2 + 1)];
37521
+ } else {
37522
+ remainingRanges = inputRanges.slice(index2 + 1);
37523
+ }
37524
+ break;
37525
+ }
37526
+ remainingRanges = [range, ...inputRanges.slice(index2 + 1)];
37527
+ break;
37528
+ }
37529
+ if (fittedRanges.length === 0 && forceFirstRange && inputRanges.length > 0) {
37530
+ const forced = forceFitFirstRange(inputRanges[0], measuresById);
37531
+ if (forced.fitted) {
37532
+ fittedRanges.push(forced.fitted);
37533
+ usedHeight = getRangeRenderHeight(forced.fitted);
37534
+ remainingRanges = [];
37535
+ if (forced.remaining) {
37536
+ remainingRanges.push(forced.remaining);
37537
+ }
37538
+ remainingRanges.push(...inputRanges.slice(1));
37539
+ }
37540
+ }
37541
+ return {
37542
+ slice: {
37543
+ id,
37544
+ pageIndex,
37545
+ columnIndex,
37546
+ isContinuation,
37547
+ ranges: fittedRanges,
37548
+ totalHeight: usedHeight
37549
+ },
37550
+ remainingRanges
37551
+ };
37552
+ };
36946
37553
  async function incrementalLayout(previousBlocks, _previousLayout, nextBlocks, options, measureBlock2, headerFooter) {
36947
37554
  performance.now();
36948
37555
  const dirty = computeDirtyRegions(previousBlocks, nextBlocks);
@@ -37206,6 +37813,497 @@ async function incrementalLayout(previousBlocks, _previousLayout, nextBlocks, op
37206
37813
  converged
37207
37814
  });
37208
37815
  }
37816
+ let extraBlocks;
37817
+ let extraMeasures;
37818
+ const footnotesInput = isFootnotesLayoutInput(options.footnotes) ? options.footnotes : null;
37819
+ if (footnotesInput && footnotesInput.refs.length > 0 && footnotesInput.blocksById.size > 0) {
37820
+ const gap = typeof footnotesInput.gap === "number" && Number.isFinite(footnotesInput.gap) ? footnotesInput.gap : 2;
37821
+ const topPadding = typeof footnotesInput.topPadding === "number" && Number.isFinite(footnotesInput.topPadding) ? footnotesInput.topPadding : 6;
37822
+ const dividerHeight = typeof footnotesInput.dividerHeight === "number" && Number.isFinite(footnotesInput.dividerHeight) ? footnotesInput.dividerHeight : 6;
37823
+ const safeGap = Math.max(0, gap);
37824
+ const safeTopPadding = Math.max(0, topPadding);
37825
+ const safeDividerHeight = Math.max(0, dividerHeight);
37826
+ const continuationDividerHeight = safeDividerHeight;
37827
+ const continuationDividerWidthFactor = 0.3;
37828
+ const footnoteWidth = resolveFootnoteMeasurementWidth(options, currentBlocks);
37829
+ if (footnoteWidth > 0) {
37830
+ const footnoteConstraints = { maxWidth: footnoteWidth, maxHeight: measurementHeight };
37831
+ const collectFootnoteIdsByColumn = (idsByColumn2) => {
37832
+ const ids = /* @__PURE__ */ new Set();
37833
+ idsByColumn2.forEach((columns) => {
37834
+ columns.forEach((list) => {
37835
+ list.forEach((id) => ids.add(id));
37836
+ });
37837
+ });
37838
+ return ids;
37839
+ };
37840
+ const measureFootnoteBlocks = async (ids) => {
37841
+ const needed = /* @__PURE__ */ new Map();
37842
+ ids.forEach((id) => {
37843
+ const blocks2 = footnotesInput.blocksById.get(id) ?? [];
37844
+ blocks2.forEach((block) => {
37845
+ if (block?.id && !needed.has(block.id)) {
37846
+ needed.set(block.id, block);
37847
+ }
37848
+ });
37849
+ });
37850
+ const blocks = Array.from(needed.values());
37851
+ const measuresById2 = /* @__PURE__ */ new Map();
37852
+ await Promise.all(
37853
+ blocks.map(async (block) => {
37854
+ const cached = measureCache.get(block, footnoteConstraints.maxWidth, footnoteConstraints.maxHeight);
37855
+ if (cached) {
37856
+ measuresById2.set(block.id, cached);
37857
+ return;
37858
+ }
37859
+ const measurement = await measureBlock2(block, footnoteConstraints);
37860
+ measureCache.set(block, footnoteConstraints.maxWidth, footnoteConstraints.maxHeight, measurement);
37861
+ measuresById2.set(block.id, measurement);
37862
+ })
37863
+ );
37864
+ return { blocks, measuresById: measuresById2 };
37865
+ };
37866
+ const computeFootnoteLayoutPlan = (layoutForPages, idsByColumn2, measuresById2, baseReserves = [], pageColumns2) => {
37867
+ const pageCount = layoutForPages.pages.length;
37868
+ const slicesByPage = /* @__PURE__ */ new Map();
37869
+ const reserves2 = new Array(pageCount).fill(0);
37870
+ const hasContinuationByColumn = /* @__PURE__ */ new Map();
37871
+ const rangesByFootnoteId = /* @__PURE__ */ new Map();
37872
+ const cappedPages = /* @__PURE__ */ new Set();
37873
+ const allIds = collectFootnoteIdsByColumn(idsByColumn2);
37874
+ allIds.forEach((id) => {
37875
+ const blocks = footnotesInput.blocksById.get(id) ?? [];
37876
+ rangesByFootnoteId.set(id, buildFootnoteRanges(blocks, measuresById2));
37877
+ });
37878
+ const separatorSpacingBefore = resolveSeparatorSpacingBefore(
37879
+ rangesByFootnoteId,
37880
+ measuresById2,
37881
+ footnotesInput.separatorSpacingBefore,
37882
+ DEFAULT_FOOTNOTE_SEPARATOR_SPACING_BEFORE
37883
+ );
37884
+ const safeSeparatorSpacingBefore = Math.max(0, separatorSpacingBefore);
37885
+ let pendingByColumn = /* @__PURE__ */ new Map();
37886
+ for (let pageIndex = 0; pageIndex < pageCount; pageIndex += 1) {
37887
+ const baseReserve = Number.isFinite(baseReserves?.[pageIndex]) ? Math.max(0, baseReserves[pageIndex]) : 0;
37888
+ const maxReserve = computeMaxFootnoteReserve(layoutForPages, pageIndex, baseReserve);
37889
+ const columns = pageColumns2.get(pageIndex);
37890
+ const columnCount = Math.max(1, Math.floor(columns?.count ?? 1));
37891
+ const pendingForPage = /* @__PURE__ */ new Map();
37892
+ pendingByColumn.forEach((entries, columnIndex) => {
37893
+ const targetIndex = columnIndex < columnCount ? columnIndex : Math.max(0, columnCount - 1);
37894
+ const list = pendingForPage.get(targetIndex) ?? [];
37895
+ list.push(...entries);
37896
+ pendingForPage.set(targetIndex, list);
37897
+ });
37898
+ pendingByColumn = /* @__PURE__ */ new Map();
37899
+ const pageSlices = [];
37900
+ let pageReserve = 0;
37901
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
37902
+ let usedHeight = 0;
37903
+ const columnSlices = [];
37904
+ const nextPending = [];
37905
+ let stopPlacement = false;
37906
+ const columnKey = footnoteColumnKey(pageIndex, columnIndex);
37907
+ const placeFootnote = (id, ranges, isContinuation) => {
37908
+ if (!ranges || ranges.length === 0) {
37909
+ return { placed: false, remaining: [] };
37910
+ }
37911
+ const isFirstSlice = columnSlices.length === 0;
37912
+ const separatorBefore = isFirstSlice ? safeSeparatorSpacingBefore : 0;
37913
+ const separatorHeight = isFirstSlice ? isContinuation ? continuationDividerHeight : safeDividerHeight : 0;
37914
+ const overhead = isFirstSlice ? separatorBefore + separatorHeight + safeTopPadding : 0;
37915
+ const gapBefore = !isFirstSlice ? safeGap : 0;
37916
+ const availableHeight = Math.max(0, maxReserve - usedHeight - overhead - gapBefore);
37917
+ const { slice: slice2, remainingRanges } = fitFootnoteContent(
37918
+ id,
37919
+ ranges,
37920
+ availableHeight,
37921
+ pageIndex,
37922
+ columnIndex,
37923
+ isContinuation,
37924
+ measuresById2,
37925
+ isFirstSlice && maxReserve > 0
37926
+ );
37927
+ if (slice2.ranges.length === 0) {
37928
+ return { placed: false, remaining: ranges };
37929
+ }
37930
+ if (isFirstSlice) {
37931
+ usedHeight += overhead;
37932
+ if (isContinuation) {
37933
+ hasContinuationByColumn.set(columnKey, true);
37934
+ }
37935
+ }
37936
+ if (gapBefore > 0) {
37937
+ usedHeight += gapBefore;
37938
+ }
37939
+ usedHeight += slice2.totalHeight;
37940
+ columnSlices.push(slice2);
37941
+ return { placed: true, remaining: remainingRanges };
37942
+ };
37943
+ const pending = pendingForPage.get(columnIndex) ?? [];
37944
+ for (const entry of pending) {
37945
+ if (stopPlacement) {
37946
+ nextPending.push(entry);
37947
+ continue;
37948
+ }
37949
+ if (!entry.ranges || entry.ranges.length === 0) continue;
37950
+ const result = placeFootnote(entry.id, entry.ranges, true);
37951
+ if (!result.placed) {
37952
+ nextPending.push(entry);
37953
+ stopPlacement = true;
37954
+ continue;
37955
+ }
37956
+ if (result.remaining.length > 0) {
37957
+ nextPending.push({ id: entry.id, ranges: result.remaining });
37958
+ }
37959
+ }
37960
+ if (!stopPlacement) {
37961
+ const ids = idsByColumn2.get(pageIndex)?.get(columnIndex) ?? [];
37962
+ for (let idIndex = 0; idIndex < ids.length; idIndex += 1) {
37963
+ const id = ids[idIndex];
37964
+ const ranges = rangesByFootnoteId.get(id) ?? [];
37965
+ if (ranges.length === 0) continue;
37966
+ const result = placeFootnote(id, ranges, false);
37967
+ if (!result.placed) {
37968
+ nextPending.push({ id, ranges });
37969
+ for (let remainingIndex = idIndex + 1; remainingIndex < ids.length; remainingIndex += 1) {
37970
+ const remainingId = ids[remainingIndex];
37971
+ const remainingRanges = rangesByFootnoteId.get(remainingId) ?? [];
37972
+ nextPending.push({ id: remainingId, ranges: remainingRanges });
37973
+ }
37974
+ stopPlacement = true;
37975
+ break;
37976
+ }
37977
+ if (result.remaining.length > 0) {
37978
+ nextPending.push({ id, ranges: result.remaining });
37979
+ }
37980
+ }
37981
+ }
37982
+ if (columnSlices.length > 0) {
37983
+ const rawReserve = Math.max(0, Math.ceil(usedHeight));
37984
+ const cappedReserve = Math.min(rawReserve, maxReserve);
37985
+ if (cappedReserve < rawReserve) {
37986
+ cappedPages.add(pageIndex);
37987
+ }
37988
+ pageReserve = Math.max(pageReserve, cappedReserve);
37989
+ pageSlices.push(...columnSlices);
37990
+ }
37991
+ if (nextPending.length > 0) {
37992
+ pendingByColumn.set(columnIndex, nextPending);
37993
+ }
37994
+ }
37995
+ if (pageSlices.length > 0) {
37996
+ slicesByPage.set(pageIndex, pageSlices);
37997
+ }
37998
+ reserves2[pageIndex] = pageReserve;
37999
+ }
38000
+ if (cappedPages.size > 0) {
38001
+ console.warn("[layout] Footnote reserve capped to preserve body area", {
38002
+ pages: Array.from(cappedPages)
38003
+ });
38004
+ }
38005
+ if (pendingByColumn.size > 0) {
38006
+ const pendingIds = /* @__PURE__ */ new Set();
38007
+ pendingByColumn.forEach((entries) => entries.forEach((entry) => pendingIds.add(entry.id)));
38008
+ console.warn("[layout] Footnote content truncated: extends beyond document pages", {
38009
+ ids: Array.from(pendingIds)
38010
+ });
38011
+ }
38012
+ return { slicesByPage, reserves: reserves2, hasContinuationByColumn, separatorSpacingBefore: safeSeparatorSpacingBefore };
38013
+ };
38014
+ const injectFragments = (layoutForPages, plan2, measuresById2, reservesByPageIndex, blockById, pageColumns2) => {
38015
+ const decorativeBlocks = [];
38016
+ const decorativeMeasures = [];
38017
+ for (let pageIndex = 0; pageIndex < layoutForPages.pages.length; pageIndex++) {
38018
+ const page = layoutForPages.pages[pageIndex];
38019
+ page.footnoteReserved = Math.max(0, reservesByPageIndex[pageIndex] ?? plan2.reserves[pageIndex] ?? 0);
38020
+ const slices = plan2.slicesByPage.get(pageIndex) ?? [];
38021
+ if (slices.length === 0) continue;
38022
+ if (!page.margins) continue;
38023
+ const pageSize = page.size ?? layoutForPages.pageSize;
38024
+ const marginLeft = normalizeMargin(
38025
+ page.margins.left,
38026
+ normalizeMargin(options.margins?.left, DEFAULT_MARGINS$1.left)
38027
+ );
38028
+ const marginRight = normalizeMargin(
38029
+ page.margins.right,
38030
+ normalizeMargin(options.margins?.right, DEFAULT_MARGINS$1.right)
38031
+ );
38032
+ const pageContentWidth = pageSize.w - (marginLeft + marginRight);
38033
+ const fallbackColumns = normalizeColumnsForFootnotes(
38034
+ options.columns ?? { count: 1, gap: 0 },
38035
+ pageContentWidth
38036
+ );
38037
+ const columns = pageColumns2.get(pageIndex) ?? {
38038
+ ...fallbackColumns,
38039
+ left: marginLeft
38040
+ };
38041
+ const bandTopY = pageSize.h - (page.margins.bottom ?? 0);
38042
+ const slicesByColumn = /* @__PURE__ */ new Map();
38043
+ slices.forEach((slice2) => {
38044
+ const columnIndex = Number.isFinite(slice2.columnIndex) ? slice2.columnIndex : 0;
38045
+ const list = slicesByColumn.get(columnIndex) ?? [];
38046
+ list.push(slice2);
38047
+ slicesByColumn.set(columnIndex, list);
38048
+ });
38049
+ slicesByColumn.forEach((columnSlices, rawColumnIndex) => {
38050
+ if (columnSlices.length === 0) return;
38051
+ const columnIndex = Math.max(0, Math.min(columns.count - 1, rawColumnIndex));
38052
+ const columnStride = columns.width + columns.gap;
38053
+ const columnX = columns.left + columnIndex * columnStride;
38054
+ const contentWidth = Math.min(columns.width, footnoteWidth);
38055
+ if (!Number.isFinite(contentWidth) || contentWidth <= 0) return;
38056
+ const columnKey = footnoteColumnKey(pageIndex, columnIndex);
38057
+ const isContinuation = plan2.hasContinuationByColumn.get(columnKey) ?? false;
38058
+ let cursorY = bandTopY + Math.max(0, plan2.separatorSpacingBefore);
38059
+ const separatorHeight = isContinuation ? continuationDividerHeight : safeDividerHeight;
38060
+ const separatorWidth = isContinuation ? Math.max(0, contentWidth * continuationDividerWidthFactor) : contentWidth;
38061
+ if (separatorHeight > 0 && separatorWidth > 0) {
38062
+ const separatorId = isContinuation ? `footnote-continuation-separator-page-${page.number}-col-${columnIndex}` : `footnote-separator-page-${page.number}-col-${columnIndex}`;
38063
+ decorativeBlocks.push({
38064
+ kind: "drawing",
38065
+ id: separatorId,
38066
+ drawingKind: "vectorShape",
38067
+ geometry: { width: separatorWidth, height: separatorHeight },
38068
+ shapeKind: "rect",
38069
+ fillColor: "#000000",
38070
+ strokeColor: null,
38071
+ strokeWidth: 0
38072
+ });
38073
+ decorativeMeasures.push({
38074
+ kind: "drawing",
38075
+ drawingKind: "vectorShape",
38076
+ width: separatorWidth,
38077
+ height: separatorHeight,
38078
+ scale: 1,
38079
+ naturalWidth: separatorWidth,
38080
+ naturalHeight: separatorHeight,
38081
+ geometry: { width: separatorWidth, height: separatorHeight }
38082
+ });
38083
+ page.fragments.push({
38084
+ kind: "drawing",
38085
+ blockId: separatorId,
38086
+ drawingKind: "vectorShape",
38087
+ x: columnX,
38088
+ y: cursorY,
38089
+ width: separatorWidth,
38090
+ height: separatorHeight,
38091
+ geometry: { width: separatorWidth, height: separatorHeight },
38092
+ scale: 1
38093
+ });
38094
+ cursorY += separatorHeight;
38095
+ }
38096
+ cursorY += safeTopPadding;
38097
+ columnSlices.forEach((slice2, sliceIndex) => {
38098
+ slice2.ranges.forEach((range) => {
38099
+ if (range.kind === "paragraph") {
38100
+ const measure = measuresById2.get(range.blockId);
38101
+ if (!measure || measure.kind !== "paragraph") return;
38102
+ const marker = measure.marker;
38103
+ page.fragments.push({
38104
+ kind: "para",
38105
+ blockId: range.blockId,
38106
+ fromLine: range.fromLine,
38107
+ toLine: range.toLine,
38108
+ x: columnX,
38109
+ y: cursorY,
38110
+ width: contentWidth,
38111
+ continuesFromPrev: range.fromLine > 0,
38112
+ continuesOnNext: range.toLine < range.totalLines,
38113
+ ...marker?.markerWidth != null ? { markerWidth: marker.markerWidth } : {},
38114
+ ...marker?.markerTextWidth != null ? { markerTextWidth: marker.markerTextWidth } : {},
38115
+ ...marker?.gutterWidth != null ? { markerGutter: marker.gutterWidth } : {}
38116
+ });
38117
+ cursorY += getRangeRenderHeight(range);
38118
+ return;
38119
+ }
38120
+ if (range.kind === "list-item") {
38121
+ const measure = measuresById2.get(range.blockId);
38122
+ const block = blockById.get(range.blockId);
38123
+ if (!measure || measure.kind !== "list") return;
38124
+ if (!block || block.kind !== "list") return;
38125
+ const itemMeasure = measure.items.find((entry) => entry.itemId === range.itemId);
38126
+ if (!itemMeasure) return;
38127
+ const indentLeft = Number.isFinite(itemMeasure.indentLeft) ? itemMeasure.indentLeft : 0;
38128
+ const markerWidth = Number.isFinite(itemMeasure.markerWidth) ? itemMeasure.markerWidth : 0;
38129
+ const itemWidth = Math.max(0, contentWidth - indentLeft - markerWidth);
38130
+ page.fragments.push({
38131
+ kind: "list-item",
38132
+ blockId: range.blockId,
38133
+ itemId: range.itemId,
38134
+ fromLine: range.fromLine,
38135
+ toLine: range.toLine,
38136
+ x: columnX + indentLeft + markerWidth,
38137
+ y: cursorY,
38138
+ width: itemWidth,
38139
+ markerWidth,
38140
+ continuesFromPrev: range.fromLine > 0,
38141
+ continuesOnNext: range.toLine < range.totalLines
38142
+ });
38143
+ cursorY += getRangeRenderHeight(range);
38144
+ return;
38145
+ }
38146
+ if (range.kind === "table") {
38147
+ const measure = measuresById2.get(range.blockId);
38148
+ const block = blockById.get(range.blockId);
38149
+ if (!measure || measure.kind !== "table") return;
38150
+ if (!block || block.kind !== "table") return;
38151
+ const tableWidthRaw = Math.max(0, measure.totalWidth ?? 0);
38152
+ let tableWidth = Math.min(contentWidth, tableWidthRaw);
38153
+ let tableX = columnX;
38154
+ const justification = typeof block.attrs?.justification === "string" ? block.attrs.justification : void 0;
38155
+ if (justification === "center") {
38156
+ tableX = columnX + Math.max(0, (contentWidth - tableWidth) / 2);
38157
+ } else if (justification === "right" || justification === "end") {
38158
+ tableX = columnX + Math.max(0, contentWidth - tableWidth);
38159
+ } else {
38160
+ const indentValue = block.attrs?.tableIndent?.width;
38161
+ const indent = typeof indentValue === "number" && Number.isFinite(indentValue) ? indentValue : 0;
38162
+ tableX += indent;
38163
+ tableWidth = Math.max(0, tableWidth - indent);
38164
+ }
38165
+ page.fragments.push({
38166
+ kind: "table",
38167
+ blockId: range.blockId,
38168
+ fromRow: 0,
38169
+ toRow: block.rows.length,
38170
+ x: tableX,
38171
+ y: cursorY,
38172
+ width: tableWidth,
38173
+ height: Math.max(0, measure.totalHeight ?? 0)
38174
+ });
38175
+ cursorY += getRangeRenderHeight(range);
38176
+ return;
38177
+ }
38178
+ if (range.kind === "image") {
38179
+ const measure = measuresById2.get(range.blockId);
38180
+ if (!measure || measure.kind !== "image") return;
38181
+ page.fragments.push({
38182
+ kind: "image",
38183
+ blockId: range.blockId,
38184
+ x: columnX,
38185
+ y: cursorY,
38186
+ width: Math.min(contentWidth, Math.max(0, measure.width ?? 0)),
38187
+ height: Math.max(0, measure.height ?? 0)
38188
+ });
38189
+ cursorY += getRangeRenderHeight(range);
38190
+ return;
38191
+ }
38192
+ if (range.kind === "drawing") {
38193
+ const measure = measuresById2.get(range.blockId);
38194
+ const block = blockById.get(range.blockId);
38195
+ if (!measure || measure.kind !== "drawing") return;
38196
+ if (!block || block.kind !== "drawing") return;
38197
+ page.fragments.push({
38198
+ kind: "drawing",
38199
+ blockId: range.blockId,
38200
+ drawingKind: block.drawingKind,
38201
+ x: columnX,
38202
+ y: cursorY,
38203
+ width: Math.min(contentWidth, Math.max(0, measure.width ?? 0)),
38204
+ height: Math.max(0, measure.height ?? 0),
38205
+ geometry: measure.geometry,
38206
+ scale: measure.scale
38207
+ });
38208
+ cursorY += getRangeRenderHeight(range);
38209
+ }
38210
+ });
38211
+ if (sliceIndex < columnSlices.length - 1) {
38212
+ cursorY += safeGap;
38213
+ }
38214
+ });
38215
+ });
38216
+ }
38217
+ return { decorativeBlocks, decorativeMeasures };
38218
+ };
38219
+ const resolveFootnoteAssignments = (layoutForPages) => {
38220
+ const columns = resolvePageColumns(layoutForPages, options, currentBlocks);
38221
+ const idsByColumn2 = assignFootnotesToColumns(layoutForPages, footnotesInput.refs, columns);
38222
+ return { columns, idsByColumn: idsByColumn2 };
38223
+ };
38224
+ let { columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout);
38225
+ let { measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn));
38226
+ let plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, [], pageColumns);
38227
+ let reserves = plan.reserves;
38228
+ if (reserves.some((h2) => h2 > 0)) {
38229
+ layout = layoutDocument(currentBlocks, currentMeasures, {
38230
+ ...options,
38231
+ footnoteReservedByPageIndex: reserves,
38232
+ headerContentHeights,
38233
+ footerContentHeights,
38234
+ remeasureParagraph: (block, maxWidth, firstLineIndent) => remeasureParagraph(block, maxWidth, firstLineIndent)
38235
+ });
38236
+ ({ columns: pageColumns, idsByColumn } = resolveFootnoteAssignments(layout));
38237
+ ({ measuresById } = await measureFootnoteBlocks(collectFootnoteIdsByColumn(idsByColumn)));
38238
+ plan = computeFootnoteLayoutPlan(layout, idsByColumn, measuresById, reserves, pageColumns);
38239
+ reserves = plan.reserves;
38240
+ layout = layoutDocument(currentBlocks, currentMeasures, {
38241
+ ...options,
38242
+ footnoteReservedByPageIndex: reserves,
38243
+ headerContentHeights,
38244
+ footerContentHeights,
38245
+ remeasureParagraph: (block, maxWidth, firstLineIndent) => remeasureParagraph(block, maxWidth, firstLineIndent)
38246
+ });
38247
+ let { columns: finalPageColumns, idsByColumn: finalIdsByColumn } = resolveFootnoteAssignments(layout);
38248
+ let { blocks: finalBlocks, measuresById: finalMeasuresById } = await measureFootnoteBlocks(
38249
+ collectFootnoteIdsByColumn(finalIdsByColumn)
38250
+ );
38251
+ let finalPlan = computeFootnoteLayoutPlan(
38252
+ layout,
38253
+ finalIdsByColumn,
38254
+ finalMeasuresById,
38255
+ reserves,
38256
+ finalPageColumns
38257
+ );
38258
+ const finalReserves = finalPlan.reserves;
38259
+ let reservesAppliedToLayout = reserves;
38260
+ const reservesDiffer = finalReserves.length !== reserves.length || finalReserves.some((h2, i) => (reserves[i] ?? 0) !== h2) || reserves.some((h2, i) => (finalReserves[i] ?? 0) !== h2);
38261
+ if (reservesDiffer) {
38262
+ layout = layoutDocument(currentBlocks, currentMeasures, {
38263
+ ...options,
38264
+ footnoteReservedByPageIndex: finalReserves,
38265
+ headerContentHeights,
38266
+ footerContentHeights,
38267
+ remeasureParagraph: (block, maxWidth, firstLineIndent) => remeasureParagraph(block, maxWidth, firstLineIndent)
38268
+ });
38269
+ reservesAppliedToLayout = finalReserves;
38270
+ ({ columns: finalPageColumns, idsByColumn: finalIdsByColumn } = resolveFootnoteAssignments(layout));
38271
+ ({ blocks: finalBlocks, measuresById: finalMeasuresById } = await measureFootnoteBlocks(
38272
+ collectFootnoteIdsByColumn(finalIdsByColumn)
38273
+ ));
38274
+ finalPlan = computeFootnoteLayoutPlan(
38275
+ layout,
38276
+ finalIdsByColumn,
38277
+ finalMeasuresById,
38278
+ reservesAppliedToLayout,
38279
+ finalPageColumns
38280
+ );
38281
+ }
38282
+ const blockById = /* @__PURE__ */ new Map();
38283
+ finalBlocks.forEach((block) => {
38284
+ blockById.set(block.id, block);
38285
+ });
38286
+ const injected = injectFragments(
38287
+ layout,
38288
+ finalPlan,
38289
+ finalMeasuresById,
38290
+ reservesAppliedToLayout,
38291
+ blockById,
38292
+ finalPageColumns
38293
+ );
38294
+ const alignedBlocks = [];
38295
+ const alignedMeasures = [];
38296
+ finalBlocks.forEach((block) => {
38297
+ const measure = finalMeasuresById.get(block.id);
38298
+ if (!measure) return;
38299
+ alignedBlocks.push(block);
38300
+ alignedMeasures.push(measure);
38301
+ });
38302
+ extraBlocks = injected ? alignedBlocks.concat(injected.decorativeBlocks) : alignedBlocks;
38303
+ extraMeasures = injected ? alignedMeasures.concat(injected.decorativeMeasures) : alignedMeasures;
38304
+ }
38305
+ }
38306
+ }
37209
38307
  let headers;
37210
38308
  let footers;
37211
38309
  if (headerFooter?.constraints && (headerFooter.headerBlocks || headerFooter.footerBlocks)) {
@@ -37266,7 +38364,9 @@ async function incrementalLayout(previousBlocks, _previousLayout, nextBlocks, op
37266
38364
  measures: currentMeasures,
37267
38365
  dirty,
37268
38366
  headers,
37269
- footers
38367
+ footers,
38368
+ extraBlocks,
38369
+ extraMeasures
37270
38370
  };
37271
38371
  }
37272
38372
  const DEFAULT_PAGE_SIZE$1 = { w: 612, h: 792 };
@@ -41305,6 +42405,7 @@ const ATOMIC_INLINE_TYPES = /* @__PURE__ */ new Set([
41305
42405
  "lineBreak",
41306
42406
  "page-number",
41307
42407
  "total-page-number",
42408
+ "footnoteReference",
41308
42409
  "passthroughInline",
41309
42410
  "bookmarkEnd"
41310
42411
  ]);
@@ -46329,6 +47430,28 @@ function paragraphToFlowBlocks$1(para, nextBlockId, positions, defaultFont, defa
46329
47430
  let partIndex = 0;
46330
47431
  let tabOrdinal = 0;
46331
47432
  let suppressedByVanish = false;
47433
+ const toSuperscriptDigits2 = (value) => {
47434
+ const map3 = {
47435
+ "0": "⁰",
47436
+ "1": "¹",
47437
+ "2": "²",
47438
+ "3": "³",
47439
+ "4": "⁴",
47440
+ "5": "⁵",
47441
+ "6": "⁶",
47442
+ "7": "⁷",
47443
+ "8": "⁸",
47444
+ "9": "⁹"
47445
+ };
47446
+ return String(value ?? "").split("").map((ch) => map3[ch] ?? ch).join("");
47447
+ };
47448
+ const resolveFootnoteDisplayNumber2 = (id) => {
47449
+ const key2 = id == null ? null : String(id);
47450
+ if (!key2) return null;
47451
+ const mapping = converterContext?.footnoteNumberById;
47452
+ const mapped = mapping && typeof mapping === "object" ? mapping[key2] : void 0;
47453
+ return typeof mapped === "number" && Number.isFinite(mapped) && mapped > 0 ? mapped : null;
47454
+ };
46332
47455
  const nextId = () => partIndex === 0 ? baseBlockId : `${baseBlockId}-${partIndex}`;
46333
47456
  const attachAnchorParagraphId = (block, anchorParagraphId) => {
46334
47457
  const applicableKinds = /* @__PURE__ */ new Set(["drawing", "image", "table"]);
@@ -46374,6 +47497,34 @@ function paragraphToFlowBlocks$1(para, nextBlockId, positions, defaultFont, defa
46374
47497
  });
46375
47498
  };
46376
47499
  const visitNode = (node, inheritedMarks = [], activeSdt, activeRunStyleId = null, activeRunProperties, activeHidden = false) => {
47500
+ if (node.type === "footnoteReference") {
47501
+ const mergedMarks = [...node.marks ?? [], ...inheritedMarks ?? []];
47502
+ const refPos = positions.get(node);
47503
+ const id = node.attrs?.id;
47504
+ const displayId = resolveFootnoteDisplayNumber2(id) ?? id ?? "*";
47505
+ const displayText = toSuperscriptDigits2(displayId);
47506
+ const run = textNodeToRun(
47507
+ { type: "text", text: displayText },
47508
+ positions,
47509
+ defaultFont,
47510
+ defaultSize,
47511
+ [],
47512
+ // marks applied after linked styles/base defaults
47513
+ activeSdt,
47514
+ hyperlinkConfig,
47515
+ themeColors
47516
+ );
47517
+ const inlineStyleId = getInlineStyleId(mergedMarks);
47518
+ applyRunStyles2(run, inlineStyleId, activeRunStyleId);
47519
+ applyBaseRunDefaults(run, baseRunDefaults, defaultFont, defaultSize);
47520
+ applyMarksToRun(run, mergedMarks, hyperlinkConfig, themeColors);
47521
+ if (refPos) {
47522
+ run.pmStart = refPos.start;
47523
+ run.pmEnd = refPos.end;
47524
+ }
47525
+ currentRuns.push(run);
47526
+ return;
47527
+ }
46377
47528
  if (activeHidden && node.type !== "run") {
46378
47529
  suppressedByVanish = true;
46379
47530
  return;
@@ -54774,12 +55925,36 @@ class PresentationEditor extends EventEmitter {
54774
55925
  const sectionMetadata = [];
54775
55926
  let blocks;
54776
55927
  let bookmarks = /* @__PURE__ */ new Map();
55928
+ let converterContext = void 0;
54777
55929
  try {
54778
55930
  const converter2 = this.#editor.converter;
54779
- const converterContext = converter2 ? {
55931
+ const footnoteNumberById = {};
55932
+ try {
55933
+ const seen = /* @__PURE__ */ new Set();
55934
+ let counter = 1;
55935
+ this.#editor?.state?.doc?.descendants?.((node) => {
55936
+ if (node?.type?.name !== "footnoteReference") return;
55937
+ const rawId = node?.attrs?.id;
55938
+ if (rawId == null) return;
55939
+ const key2 = String(rawId);
55940
+ if (!key2 || seen.has(key2)) return;
55941
+ seen.add(key2);
55942
+ footnoteNumberById[key2] = counter;
55943
+ counter += 1;
55944
+ });
55945
+ } catch {
55946
+ }
55947
+ try {
55948
+ if (converter2 && typeof converter2 === "object") {
55949
+ converter2["footnoteNumberById"] = footnoteNumberById;
55950
+ }
55951
+ } catch {
55952
+ }
55953
+ converterContext = converter2 ? {
54780
55954
  docx: converter2.convertedXml,
54781
55955
  numbering: converter2.numbering,
54782
- linkedStyles: converter2.linkedStyles
55956
+ linkedStyles: converter2.linkedStyles,
55957
+ ...Object.keys(footnoteNumberById).length ? { footnoteNumberById } : {}
54783
55958
  } : void 0;
54784
55959
  const atomNodeTypes = getAtomNodeTypes(this.#editor?.schema ?? null);
54785
55960
  const positionMap = this.#editor?.state?.doc && docJson ? buildPositionMapFromPmDoc(this.#editor.state.doc, docJson) : null;
@@ -54807,13 +55982,20 @@ class PresentationEditor extends EventEmitter {
54807
55982
  this.#handleLayoutError("render", new Error("toFlowBlocks returned undefined blocks"));
54808
55983
  return;
54809
55984
  }
54810
- const layoutOptions = this.#resolveLayoutOptions(blocks, sectionMetadata);
55985
+ const baseLayoutOptions = this.#resolveLayoutOptions(blocks, sectionMetadata);
55986
+ const footnotesLayoutInput = this.#buildFootnotesLayoutInput({
55987
+ converterContext,
55988
+ themeColors: this.#editor?.converter?.themeColors ?? void 0
55989
+ });
55990
+ const layoutOptions = footnotesLayoutInput ? { ...baseLayoutOptions, footnotes: footnotesLayoutInput } : baseLayoutOptions;
54811
55991
  const previousBlocks = this.#layoutState.blocks;
54812
55992
  const previousLayout = this.#layoutState.layout;
54813
55993
  let layout;
54814
55994
  let measures;
54815
55995
  let headerLayouts;
54816
55996
  let footerLayouts;
55997
+ let extraBlocks;
55998
+ let extraMeasures;
54817
55999
  const headerFooterInput = this.#buildHeaderFooterInput();
54818
56000
  try {
54819
56001
  const result = await incrementalLayout(
@@ -54837,6 +56019,8 @@ class PresentationEditor extends EventEmitter {
54837
56019
  return;
54838
56020
  }
54839
56021
  ({ layout, measures } = result);
56022
+ extraBlocks = Array.isArray(result.extraBlocks) ? result.extraBlocks : void 0;
56023
+ extraMeasures = Array.isArray(result.extraMeasures) ? result.extraMeasures : void 0;
54840
56024
  layout.pageGap = this.#getEffectivePageGap();
54841
56025
  layout.layoutEpoch = layoutEpoch;
54842
56026
  headerLayouts = result.headers;
@@ -54896,6 +56080,10 @@ class PresentationEditor extends EventEmitter {
54896
56080
  footerBlocks.push(...rIdResult.blocks);
54897
56081
  footerMeasures.push(...rIdResult.measures);
54898
56082
  }
56083
+ if (extraBlocks && extraMeasures && extraBlocks.length === extraMeasures.length && extraBlocks.length > 0) {
56084
+ footerBlocks.push(...extraBlocks);
56085
+ footerMeasures.push(...extraMeasures);
56086
+ }
54899
56087
  painter.setData?.(
54900
56088
  blocks,
54901
56089
  measures,
@@ -55182,6 +56370,123 @@ class PresentationEditor extends EventEmitter {
55182
56370
  sectionMetadata
55183
56371
  };
55184
56372
  }
56373
+ #buildFootnotesLayoutInput({
56374
+ converterContext,
56375
+ themeColors
56376
+ }) {
56377
+ const footnoteNumberById = converterContext?.footnoteNumberById;
56378
+ const toSuperscriptDigits2 = (value) => {
56379
+ const map3 = {
56380
+ "0": "⁰",
56381
+ "1": "¹",
56382
+ "2": "²",
56383
+ "3": "³",
56384
+ "4": "⁴",
56385
+ "5": "⁵",
56386
+ "6": "⁶",
56387
+ "7": "⁷",
56388
+ "8": "⁸",
56389
+ "9": "⁹"
56390
+ };
56391
+ const str = String(value ?? "");
56392
+ return str.split("").map((ch) => map3[ch] ?? ch).join("");
56393
+ };
56394
+ const ensureFootnoteMarker = (blocks, id) => {
56395
+ const displayNumberRaw = footnoteNumberById && typeof footnoteNumberById === "object" ? footnoteNumberById[id] : void 0;
56396
+ const displayNumber = typeof displayNumberRaw === "number" && Number.isFinite(displayNumberRaw) && displayNumberRaw > 0 ? displayNumberRaw : 1;
56397
+ const firstParagraph = blocks.find((b2) => b2?.kind === "paragraph");
56398
+ if (!firstParagraph) return;
56399
+ const runs = Array.isArray(firstParagraph.runs) ? firstParagraph.runs : [];
56400
+ const markerText = toSuperscriptDigits2(displayNumber);
56401
+ const baseRun = runs.find((r2) => {
56402
+ const dataAttrs = r2.dataAttrs;
56403
+ if (dataAttrs?.["data-sd-footnote-number"]) return false;
56404
+ const pmStart = r2.pmStart;
56405
+ const pmEnd = r2.pmEnd;
56406
+ return typeof pmStart === "number" && Number.isFinite(pmStart) && typeof pmEnd === "number" && Number.isFinite(pmEnd);
56407
+ });
56408
+ const markerPmStart = baseRun?.pmStart ?? null;
56409
+ const markerPmEnd = markerPmStart != null ? baseRun?.pmEnd != null ? Math.max(markerPmStart, Math.min(baseRun.pmEnd, markerPmStart + markerText.length)) : markerPmStart + markerText.length : null;
56410
+ const alreadyHasMarker = runs.some((r2) => {
56411
+ const dataAttrs = r2.dataAttrs;
56412
+ return Boolean(dataAttrs?.["data-sd-footnote-number"]);
56413
+ });
56414
+ if (alreadyHasMarker) {
56415
+ if (markerPmStart != null && markerPmEnd != null) {
56416
+ const markerRun2 = runs.find((r2) => {
56417
+ const dataAttrs = r2.dataAttrs;
56418
+ return Boolean(dataAttrs?.["data-sd-footnote-number"]);
56419
+ });
56420
+ if (markerRun2) {
56421
+ if (markerRun2.pmStart == null) markerRun2.pmStart = markerPmStart;
56422
+ if (markerRun2.pmEnd == null) markerRun2.pmEnd = markerPmEnd;
56423
+ }
56424
+ }
56425
+ return;
56426
+ }
56427
+ const firstTextRun = runs.find((r2) => typeof r2.text === "string");
56428
+ const markerRun = {
56429
+ kind: "text",
56430
+ text: markerText,
56431
+ dataAttrs: {
56432
+ "data-sd-footnote-number": "true"
56433
+ },
56434
+ ...markerPmStart != null ? { pmStart: markerPmStart } : {},
56435
+ ...markerPmEnd != null ? { pmEnd: markerPmEnd } : {}
56436
+ };
56437
+ markerRun.fontFamily = typeof firstTextRun?.fontFamily === "string" ? firstTextRun.fontFamily : "Arial";
56438
+ markerRun.fontSize = typeof firstTextRun?.fontSize === "number" && Number.isFinite(firstTextRun.fontSize) ? firstTextRun.fontSize : 12;
56439
+ if (firstTextRun?.color != null) markerRun.color = firstTextRun.color;
56440
+ runs.unshift(markerRun);
56441
+ firstParagraph.runs = runs;
56442
+ };
56443
+ const state = this.#editor?.state;
56444
+ if (!state) return null;
56445
+ const converter = this.#editor?.converter;
56446
+ const importedFootnotes = Array.isArray(converter?.footnotes) ? converter.footnotes : [];
56447
+ if (importedFootnotes.length === 0) return null;
56448
+ const refs = [];
56449
+ const idsInUse = /* @__PURE__ */ new Set();
56450
+ state.doc.descendants((node, pos) => {
56451
+ if (node.type?.name !== "footnoteReference") return;
56452
+ const id = node.attrs?.id;
56453
+ if (id == null) return;
56454
+ const key2 = String(id);
56455
+ const insidePos = Math.min(pos + 1, state.doc.content.size);
56456
+ refs.push({ id: key2, pos: insidePos });
56457
+ idsInUse.add(key2);
56458
+ });
56459
+ if (refs.length === 0) return null;
56460
+ const blocksById = /* @__PURE__ */ new Map();
56461
+ idsInUse.forEach((id) => {
56462
+ const entry = importedFootnotes.find((f) => String(f?.id) === id);
56463
+ const content = entry?.content;
56464
+ if (!Array.isArray(content) || content.length === 0) return;
56465
+ try {
56466
+ const clonedContent = JSON.parse(JSON.stringify(content));
56467
+ const footnoteDoc = { type: "doc", content: clonedContent };
56468
+ const result = toFlowBlocks(footnoteDoc, {
56469
+ blockIdPrefix: `footnote-${id}-`,
56470
+ enableRichHyperlinks: true,
56471
+ themeColors,
56472
+ converterContext
56473
+ });
56474
+ if (result?.blocks?.length) {
56475
+ ensureFootnoteMarker(result.blocks, id);
56476
+ blocksById.set(id, result.blocks);
56477
+ }
56478
+ } catch {
56479
+ }
56480
+ });
56481
+ if (blocksById.size === 0) return null;
56482
+ return {
56483
+ refs,
56484
+ blocksById,
56485
+ gap: 2,
56486
+ topPadding: 4,
56487
+ dividerHeight: 1
56488
+ };
56489
+ }
55185
56490
  #buildHeaderFooterInput() {
55186
56491
  if (!this.#headerFooterAdapter) {
55187
56492
  return null;
@@ -55371,7 +56676,8 @@ class PresentationEditor extends EventEmitter {
55371
56676
  const fragments2 = slotPage2.fragments ?? [];
55372
56677
  const pageHeight2 = page?.size?.h ?? layout.pageSize?.h ?? this.#layoutOptions.pageSize?.h ?? DEFAULT_PAGE_SIZE.h;
55373
56678
  const margins2 = pageMargins ?? layout.pages[0]?.margins ?? this.#layoutOptions.margins ?? DEFAULT_MARGINS;
55374
- const box2 = this.#computeDecorationBox(kind, margins2, pageHeight2);
56679
+ const decorationMargins2 = kind === "footer" ? this.#stripFootnoteReserveFromBottomMargin(margins2, page ?? null) : margins2;
56680
+ const box2 = this.#computeDecorationBox(kind, decorationMargins2, pageHeight2);
55375
56681
  const rawLayoutHeight2 = rIdLayout.layout.height ?? 0;
55376
56682
  const metrics2 = this.#computeHeaderFooterMetrics(
55377
56683
  kind,
@@ -55422,7 +56728,8 @@ class PresentationEditor extends EventEmitter {
55422
56728
  const fragments = slotPage.fragments ?? [];
55423
56729
  const pageHeight = page?.size?.h ?? layout.pageSize?.h ?? this.#layoutOptions.pageSize?.h ?? DEFAULT_PAGE_SIZE.h;
55424
56730
  const margins = pageMargins ?? layout.pages[0]?.margins ?? this.#layoutOptions.margins ?? DEFAULT_MARGINS;
55425
- const box = this.#computeDecorationBox(kind, margins, pageHeight);
56731
+ const decorationMargins = kind === "footer" ? this.#stripFootnoteReserveFromBottomMargin(margins, page ?? null) : margins;
56732
+ const box = this.#computeDecorationBox(kind, decorationMargins, pageHeight);
55426
56733
  const rawLayoutHeight = variant.layout.height ?? 0;
55427
56734
  const metrics = this.#computeHeaderFooterMetrics(kind, rawLayoutHeight, box, pageHeight, margins.footer ?? 0);
55428
56735
  const fallbackId = this.#headerFooterManager?.getVariantId(kind, headerFooterType);
@@ -55509,6 +56816,16 @@ class PresentationEditor extends EventEmitter {
55509
56816
  return { x: left2, width, height, offset: offset2 };
55510
56817
  }
55511
56818
  }
56819
+ #stripFootnoteReserveFromBottomMargin(pageMargins, page) {
56820
+ const reserveRaw = page?.footnoteReserved;
56821
+ const reserve = typeof reserveRaw === "number" && Number.isFinite(reserveRaw) && reserveRaw > 0 ? reserveRaw : 0;
56822
+ if (!reserve) return pageMargins;
56823
+ const bottomRaw = pageMargins.bottom;
56824
+ const bottom2 = typeof bottomRaw === "number" && Number.isFinite(bottomRaw) ? bottomRaw : 0;
56825
+ const nextBottom = Math.max(0, bottom2 - reserve);
56826
+ if (nextBottom === bottom2) return pageMargins;
56827
+ return { ...pageMargins, bottom: nextBottom };
56828
+ }
55512
56829
  /**
55513
56830
  * Computes the expected header/footer section type for a page based on document configuration.
55514
56831
  *
@@ -55573,7 +56890,8 @@ class PresentationEditor extends EventEmitter {
55573
56890
  height: headerPayload?.hitRegion?.height ?? headerBox.height
55574
56891
  });
55575
56892
  const footerPayload = this.#footerDecorationProvider?.(page.number, margins, page);
55576
- const footerBox = this.#computeDecorationBox("footer", margins, actualPageHeight);
56893
+ const footerBoxMargins = this.#stripFootnoteReserveFromBottomMargin(margins, page);
56894
+ const footerBox = this.#computeDecorationBox("footer", footerBoxMargins, actualPageHeight);
55577
56895
  this.#footerRegions.set(pageIndex, {
55578
56896
  kind: "footer",
55579
56897
  headerId: footerPayload?.headerId,
@@ -61474,6 +62792,109 @@ const CommentsMark = Mark.create({
61474
62792
  return [CommentMarkName$1, Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes)];
61475
62793
  }
61476
62794
  });
62795
+ const toSuperscriptDigits = (value) => {
62796
+ const map3 = {
62797
+ 0: "⁰",
62798
+ 1: "¹",
62799
+ 2: "²",
62800
+ 3: "³",
62801
+ 4: "⁴",
62802
+ 5: "⁵",
62803
+ 6: "⁶",
62804
+ 7: "⁷",
62805
+ 8: "⁸",
62806
+ 9: "⁹"
62807
+ };
62808
+ return String(value ?? "").split("").map((ch) => map3[ch] ?? ch).join("");
62809
+ };
62810
+ const resolveFootnoteDisplayNumber = (editor, id) => {
62811
+ const key2 = id == null ? null : String(id);
62812
+ if (!key2) return null;
62813
+ const map3 = editor?.converter?.footnoteNumberById;
62814
+ const mapped = map3 && typeof map3 === "object" ? map3[key2] : void 0;
62815
+ return typeof mapped === "number" && Number.isFinite(mapped) && mapped > 0 ? mapped : null;
62816
+ };
62817
+ class FootnoteReferenceNodeView {
62818
+ constructor(node, getPos, decorations, editor, htmlAttributes = {}) {
62819
+ this.node = node;
62820
+ this.getPos = getPos;
62821
+ this.editor = editor;
62822
+ this.dom = this.#renderDom(node, htmlAttributes);
62823
+ }
62824
+ #renderDom(node, htmlAttributes) {
62825
+ const el = document.createElement("sup");
62826
+ el.className = "sd-footnote-ref";
62827
+ el.setAttribute("contenteditable", "false");
62828
+ el.setAttribute("aria-label", "Footnote reference");
62829
+ Object.entries(htmlAttributes).forEach(([key2, value]) => {
62830
+ if (value != null && value !== false) {
62831
+ el.setAttribute(key2, String(value));
62832
+ }
62833
+ });
62834
+ const id = node?.attrs?.id;
62835
+ if (id != null) {
62836
+ el.setAttribute("data-footnote-id", String(id));
62837
+ const display = resolveFootnoteDisplayNumber(this.editor, id) ?? id;
62838
+ el.textContent = toSuperscriptDigits(display);
62839
+ } else {
62840
+ el.textContent = "*";
62841
+ }
62842
+ return el;
62843
+ }
62844
+ update(node) {
62845
+ const incomingType = node?.type?.name;
62846
+ const currentType = this.node?.type?.name;
62847
+ if (!incomingType || incomingType !== currentType) return false;
62848
+ this.node = node;
62849
+ const id = node?.attrs?.id;
62850
+ if (id != null) {
62851
+ this.dom.setAttribute("data-footnote-id", String(id));
62852
+ const display = resolveFootnoteDisplayNumber(this.editor, id) ?? id;
62853
+ this.dom.textContent = toSuperscriptDigits(display);
62854
+ } else {
62855
+ this.dom.removeAttribute("data-footnote-id");
62856
+ this.dom.textContent = "*";
62857
+ }
62858
+ return true;
62859
+ }
62860
+ }
62861
+ const FootnoteReference = Node$1.create({
62862
+ name: "footnoteReference",
62863
+ group: "inline",
62864
+ inline: true,
62865
+ atom: true,
62866
+ selectable: false,
62867
+ draggable: false,
62868
+ addOptions() {
62869
+ return {
62870
+ htmlAttributes: {
62871
+ "data-footnote-ref": "true"
62872
+ }
62873
+ };
62874
+ },
62875
+ addAttributes() {
62876
+ return {
62877
+ id: {
62878
+ default: null
62879
+ },
62880
+ customMarkFollows: {
62881
+ default: null
62882
+ }
62883
+ };
62884
+ },
62885
+ addNodeView() {
62886
+ return ({ node, editor, getPos, decorations }) => {
62887
+ const htmlAttributes = this.options.htmlAttributes;
62888
+ return new FootnoteReferenceNodeView(node, getPos, decorations, editor, htmlAttributes);
62889
+ };
62890
+ },
62891
+ parseDOM() {
62892
+ return [{ tag: "sup[data-footnote-id]" }];
62893
+ },
62894
+ renderDOM({ htmlAttributes }) {
62895
+ return ["sup", Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes)];
62896
+ }
62897
+ });
61477
62898
  let cache$1 = /* @__PURE__ */ new WeakMap();
61478
62899
  function getParagraphContext(paragraph, startPos, helpers2, revision, compute) {
61479
62900
  const cached = cache$1.get(paragraph);
@@ -74286,14 +75707,317 @@ function getMatchHighlights(state) {
74286
75707
  let search2 = searchKey.getState(state);
74287
75708
  return search2 ? search2.deco : DecorationSet.empty;
74288
75709
  }
74289
- function setSearchState(tr, query, range = null, options = {}) {
74290
- if (options != null && (typeof options !== "object" || Array.isArray(options))) {
74291
- throw new TypeError("setSearchState options must be an object");
75710
+ const BLOCK_SEPARATOR = "\n";
75711
+ const ATOM_PLACEHOLDER = "";
75712
+ class SearchIndex {
75713
+ /** @type {string} */
75714
+ text = "";
75715
+ /** @type {Segment[]} */
75716
+ segments = [];
75717
+ /** @type {boolean} */
75718
+ valid = false;
75719
+ /** @type {number} */
75720
+ docSize = 0;
75721
+ /**
75722
+ * Build the search index from a ProseMirror document.
75723
+ * Uses doc.textBetween for the flattened string and walks
75724
+ * the document to build the segment offset map.
75725
+ *
75726
+ * @param {import('prosemirror-model').Node} doc - The ProseMirror document
75727
+ */
75728
+ build(doc2) {
75729
+ this.text = doc2.textBetween(0, doc2.content.size, BLOCK_SEPARATOR, ATOM_PLACEHOLDER);
75730
+ this.segments = [];
75731
+ this.docSize = doc2.content.size;
75732
+ let offset2 = 0;
75733
+ this.#walkNodeContent(doc2, 0, offset2, (segment) => {
75734
+ this.segments.push(segment);
75735
+ offset2 = segment.offsetEnd;
75736
+ });
75737
+ this.valid = true;
75738
+ }
75739
+ /**
75740
+ * Walk the content of a node to build segments.
75741
+ * This method processes the children of a node, given the position
75742
+ * where the node's content starts.
75743
+ *
75744
+ * @param {import('prosemirror-model').Node} node - Current node
75745
+ * @param {number} contentStart - Document position where this node's content starts
75746
+ * @param {number} offset - Current offset in flattened string
75747
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75748
+ * @returns {number} The new offset after processing this node's content
75749
+ */
75750
+ #walkNodeContent(node, contentStart, offset2, addSegment) {
75751
+ let currentOffset = offset2;
75752
+ let isFirstChild = true;
75753
+ node.forEach((child, childContentOffset) => {
75754
+ const childDocPos = contentStart + childContentOffset;
75755
+ if (child.isBlock && !isFirstChild) {
75756
+ addSegment({
75757
+ offsetStart: currentOffset,
75758
+ offsetEnd: currentOffset + 1,
75759
+ docFrom: childDocPos,
75760
+ docTo: childDocPos,
75761
+ kind: "blockSep"
75762
+ });
75763
+ currentOffset += 1;
75764
+ }
75765
+ currentOffset = this.#walkNode(child, childDocPos, currentOffset, addSegment);
75766
+ isFirstChild = false;
75767
+ });
75768
+ return currentOffset;
75769
+ }
75770
+ /**
75771
+ * Recursively walk a node and its descendants to build segments.
75772
+ *
75773
+ * @param {import('prosemirror-model').Node} node - Current node
75774
+ * @param {number} docPos - Document position at start of this node
75775
+ * @param {number} offset - Current offset in flattened string
75776
+ * @param {(segment: Segment) => void} addSegment - Callback to add a segment
75777
+ * @returns {number} The new offset after processing this node
75778
+ */
75779
+ #walkNode(node, docPos, offset2, addSegment) {
75780
+ if (node.isText) {
75781
+ const text = node.text || "";
75782
+ if (text.length > 0) {
75783
+ addSegment({
75784
+ offsetStart: offset2,
75785
+ offsetEnd: offset2 + text.length,
75786
+ docFrom: docPos,
75787
+ docTo: docPos + text.length,
75788
+ kind: "text"
75789
+ });
75790
+ return offset2 + text.length;
75791
+ }
75792
+ return offset2;
75793
+ }
75794
+ if (node.isLeaf) {
75795
+ if (node.type.name === "hard_break") {
75796
+ addSegment({
75797
+ offsetStart: offset2,
75798
+ offsetEnd: offset2 + 1,
75799
+ docFrom: docPos,
75800
+ docTo: docPos + node.nodeSize,
75801
+ kind: "hardBreak"
75802
+ });
75803
+ return offset2 + 1;
75804
+ }
75805
+ addSegment({
75806
+ offsetStart: offset2,
75807
+ offsetEnd: offset2 + 1,
75808
+ docFrom: docPos,
75809
+ docTo: docPos + node.nodeSize,
75810
+ kind: "atom"
75811
+ });
75812
+ return offset2 + 1;
75813
+ }
75814
+ return this.#walkNodeContent(node, docPos + 1, offset2, addSegment);
75815
+ }
75816
+ /**
75817
+ * Mark the index as stale. It will be rebuilt on next search.
75818
+ */
75819
+ invalidate() {
75820
+ this.valid = false;
75821
+ }
75822
+ /**
75823
+ * Check if the index needs rebuilding for the given document.
75824
+ *
75825
+ * @param {import('prosemirror-model').Node} doc - The document to check against
75826
+ * @returns {boolean} True if index is stale and needs rebuilding
75827
+ */
75828
+ isStale(doc2) {
75829
+ return !this.valid || doc2.content.size !== this.docSize;
75830
+ }
75831
+ /**
75832
+ * Ensure the index is valid for the given document.
75833
+ * Rebuilds if stale.
75834
+ *
75835
+ * @param {import('prosemirror-model').Node} doc - The document
75836
+ */
75837
+ ensureValid(doc2) {
75838
+ if (this.isStale(doc2)) {
75839
+ this.build(doc2);
75840
+ }
75841
+ }
75842
+ /**
75843
+ * Convert an offset range in the flattened string to document ranges.
75844
+ * Skips separator/atom segments and returns only text ranges.
75845
+ *
75846
+ * @param {number} start - Start offset in flattened string
75847
+ * @param {number} end - End offset in flattened string
75848
+ * @returns {DocRange[]} Array of document ranges (text segments only)
75849
+ */
75850
+ offsetRangeToDocRanges(start2, end2) {
75851
+ const ranges = [];
75852
+ for (const segment of this.segments) {
75853
+ if (segment.offsetEnd <= start2) continue;
75854
+ if (segment.offsetStart >= end2) break;
75855
+ if (segment.kind !== "text") continue;
75856
+ const overlapStart = Math.max(start2, segment.offsetStart);
75857
+ const overlapEnd = Math.min(end2, segment.offsetEnd);
75858
+ if (overlapStart < overlapEnd) {
75859
+ const startInSegment = overlapStart - segment.offsetStart;
75860
+ const endInSegment = overlapEnd - segment.offsetStart;
75861
+ ranges.push({
75862
+ from: segment.docFrom + startInSegment,
75863
+ to: segment.docFrom + endInSegment
75864
+ });
75865
+ }
75866
+ }
75867
+ return ranges;
75868
+ }
75869
+ /**
75870
+ * Find the document position for a given offset in the flattened string.
75871
+ *
75872
+ * @param {number} offset - Offset in flattened string
75873
+ * @returns {number|null} Document position, or null if not found
75874
+ */
75875
+ offsetToDocPos(offset2) {
75876
+ for (const segment of this.segments) {
75877
+ if (offset2 >= segment.offsetStart && offset2 < segment.offsetEnd) {
75878
+ if (segment.kind === "text") {
75879
+ return segment.docFrom + (offset2 - segment.offsetStart);
75880
+ }
75881
+ return segment.docFrom;
75882
+ }
75883
+ }
75884
+ if (this.segments.length > 0 && offset2 === this.segments[this.segments.length - 1].offsetEnd) {
75885
+ const lastSeg = this.segments[this.segments.length - 1];
75886
+ return lastSeg.docTo;
75887
+ }
75888
+ return null;
75889
+ }
75890
+ /**
75891
+ * Escape special regex characters in a string.
75892
+ *
75893
+ * @param {string} str - String to escape
75894
+ * @returns {string} Escaped string safe for use in RegExp
75895
+ */
75896
+ static escapeRegex(str) {
75897
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75898
+ }
75899
+ /**
75900
+ * Convert a plain search string to a whitespace-flexible regex pattern.
75901
+ * This allows matching across paragraph boundaries.
75902
+ *
75903
+ * @param {string} searchString - The search string
75904
+ * @returns {string} Regex pattern string
75905
+ */
75906
+ static toFlexiblePattern(searchString) {
75907
+ const parts = searchString.split(/\s+/).filter((part) => part.length > 0);
75908
+ if (parts.length === 0) return "";
75909
+ return parts.map((part) => SearchIndex.escapeRegex(part)).join("\\s+");
75910
+ }
75911
+ /**
75912
+ * Search the index for matches.
75913
+ *
75914
+ * @param {string | RegExp} pattern - Search pattern (string or regex)
75915
+ * @param {Object} options - Search options
75916
+ * @param {boolean} [options.caseSensitive=false] - Case sensitive search
75917
+ * @param {number} [options.maxMatches=1000] - Maximum number of matches to return
75918
+ * @returns {Array<{start: number, end: number, text: string}>} Array of matches with offsets
75919
+ */
75920
+ search(pattern, options = {}) {
75921
+ const { caseSensitive = false, maxMatches = 1e3 } = options;
75922
+ const matches = [];
75923
+ let regex;
75924
+ if (pattern instanceof RegExp) {
75925
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
75926
+ regex = new RegExp(pattern.source, flags);
75927
+ } else if (typeof pattern === "string") {
75928
+ if (pattern.length === 0) return matches;
75929
+ const flexiblePattern = SearchIndex.toFlexiblePattern(pattern);
75930
+ if (flexiblePattern.length === 0) return matches;
75931
+ const flags = caseSensitive ? "g" : "gi";
75932
+ regex = new RegExp(flexiblePattern, flags);
75933
+ } else {
75934
+ return matches;
75935
+ }
75936
+ let match;
75937
+ while ((match = regex.exec(this.text)) !== null && matches.length < maxMatches) {
75938
+ matches.push({
75939
+ start: match.index,
75940
+ end: match.index + match[0].length,
75941
+ text: match[0]
75942
+ });
75943
+ if (match[0].length === 0) {
75944
+ regex.lastIndex++;
75945
+ }
75946
+ }
75947
+ return matches;
74292
75948
  }
74293
- const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
74294
- return tr.setMeta(searchKey, { query, range, highlight });
74295
75949
  }
75950
+ const customSearchHighlightsKey = new PluginKey("customSearchHighlights");
74296
75951
  const isRegExp = (value) => Object.prototype.toString.call(value) === "[object RegExp]";
75952
+ const resolveInlineTextPosition = (doc2, position, direction) => {
75953
+ const docSize = doc2.content.size;
75954
+ if (!Number.isFinite(position) || position < 0 || position > docSize) {
75955
+ return position;
75956
+ }
75957
+ const step = direction === "forward" ? 1 : -1;
75958
+ let current = position;
75959
+ let iterations = 0;
75960
+ while (iterations < 8) {
75961
+ iterations += 1;
75962
+ const resolved = doc2.resolve(current);
75963
+ const boundaryNode = direction === "forward" ? resolved.nodeAfter : resolved.nodeBefore;
75964
+ if (!boundaryNode) break;
75965
+ if (boundaryNode.isText) break;
75966
+ if (!boundaryNode.isInline || boundaryNode.isAtom || boundaryNode.content.size === 0) break;
75967
+ const next = current + step;
75968
+ if (next < 0 || next > docSize) break;
75969
+ current = next;
75970
+ const adjacent = doc2.resolve(current);
75971
+ const checkNode = direction === "forward" ? adjacent.nodeAfter : adjacent.nodeBefore;
75972
+ if (checkNode && checkNode.isText) break;
75973
+ }
75974
+ return current;
75975
+ };
75976
+ const resolveSearchRange = ({ doc: doc2, from: from3, to, expectedText, highlights }) => {
75977
+ const docSize = doc2.content.size;
75978
+ let resolvedFrom = Math.max(0, Math.min(from3, docSize));
75979
+ let resolvedTo = Math.max(0, Math.min(to, docSize));
75980
+ if (highlights) {
75981
+ const windowStart = Math.max(0, resolvedFrom - 4);
75982
+ const windowEnd = Math.min(docSize, resolvedTo + 4);
75983
+ const candidates = highlights.find(windowStart, windowEnd);
75984
+ if (candidates.length > 0) {
75985
+ let chosen = candidates[0];
75986
+ if (expectedText) {
75987
+ const matching = candidates.filter(
75988
+ (decoration) => doc2.textBetween(decoration.from, decoration.to) === expectedText
75989
+ );
75990
+ if (matching.length > 0) {
75991
+ chosen = matching[0];
75992
+ }
75993
+ }
75994
+ resolvedFrom = chosen.from;
75995
+ resolvedTo = chosen.to;
75996
+ }
75997
+ }
75998
+ const normalizedFrom = resolveInlineTextPosition(doc2, resolvedFrom, "forward");
75999
+ const normalizedTo = resolveInlineTextPosition(doc2, resolvedTo, "backward");
76000
+ if (Number.isFinite(normalizedFrom) && Number.isFinite(normalizedTo) && normalizedFrom <= normalizedTo) {
76001
+ resolvedFrom = normalizedFrom;
76002
+ resolvedTo = normalizedTo;
76003
+ }
76004
+ return { from: resolvedFrom, to: resolvedTo };
76005
+ };
76006
+ const getPositionTracker = (editor) => {
76007
+ if (!editor) return null;
76008
+ if (editor.positionTracker) return editor.positionTracker;
76009
+ const storageTracker = editor.storage?.positionTracker?.tracker;
76010
+ if (storageTracker) {
76011
+ editor.positionTracker = storageTracker;
76012
+ return storageTracker;
76013
+ }
76014
+ const tracker = new PositionTracker(editor);
76015
+ if (editor.storage?.positionTracker) {
76016
+ editor.storage.positionTracker.tracker = tracker;
76017
+ }
76018
+ editor.positionTracker = tracker;
76019
+ return tracker;
76020
+ };
74297
76021
  const Search = Extension.create({
74298
76022
  // @ts-expect-error - Storage type mismatch will be fixed in TS migration
74299
76023
  addStorage() {
@@ -74302,29 +76026,58 @@ const Search = Extension.create({
74302
76026
  * @private
74303
76027
  * @type {SearchMatch[]|null}
74304
76028
  */
74305
- searchResults: []
76029
+ searchResults: [],
76030
+ /**
76031
+ * @private
76032
+ * @type {boolean}
76033
+ * Whether to apply CSS highlight classes to matches
76034
+ */
76035
+ highlightEnabled: true,
76036
+ /**
76037
+ * @private
76038
+ * @type {SearchIndex}
76039
+ * Lazily-built search index for cross-paragraph matching
76040
+ */
76041
+ searchIndex: new SearchIndex()
74306
76042
  };
74307
76043
  },
74308
76044
  addPmPlugins() {
74309
76045
  const editor = this.editor;
74310
76046
  const storage = this.storage;
76047
+ const searchIndexInvalidatorPlugin = new Plugin({
76048
+ key: new PluginKey("searchIndexInvalidator"),
76049
+ appendTransaction(transactions, oldState, newState) {
76050
+ const docChanged = transactions.some((tr) => tr.docChanged);
76051
+ if (docChanged && storage?.searchIndex) {
76052
+ storage.searchIndex.invalidate();
76053
+ }
76054
+ return null;
76055
+ }
76056
+ });
74311
76057
  const searchHighlightWithIdPlugin = new Plugin({
74312
- key: new PluginKey("customSearchHighlights"),
76058
+ key: customSearchHighlightsKey,
74313
76059
  props: {
74314
76060
  decorations(state) {
74315
76061
  if (!editor) return null;
74316
76062
  const matches = storage?.searchResults;
74317
76063
  if (!matches?.length) return null;
74318
- const decorations = matches.map(
74319
- (match) => Decoration.inline(match.from, match.to, {
74320
- id: `search-match-${match.id}`
74321
- })
74322
- );
76064
+ const highlightEnabled = storage?.highlightEnabled !== false;
76065
+ const decorations = [];
76066
+ for (const match of matches) {
76067
+ const attrs = highlightEnabled ? { id: `search-match-${match.id}`, class: "ProseMirror-search-match" } : { id: `search-match-${match.id}` };
76068
+ if (match.ranges && match.ranges.length > 0) {
76069
+ for (const range of match.ranges) {
76070
+ decorations.push(Decoration.inline(range.from, range.to, attrs));
76071
+ }
76072
+ } else {
76073
+ decorations.push(Decoration.inline(match.from, match.to, attrs));
76074
+ }
76075
+ }
74323
76076
  return DecorationSet.create(state.doc, decorations);
74324
76077
  }
74325
76078
  }
74326
76079
  });
74327
- return [search(), searchHighlightWithIdPlugin];
76080
+ return [search(), searchIndexInvalidatorPlugin, searchHighlightWithIdPlugin];
74328
76081
  },
74329
76082
  addCommands() {
74330
76083
  return {
@@ -74338,21 +76091,51 @@ const Search = Extension.create({
74338
76091
  goToFirstMatch: () => (
74339
76092
  /** @returns {boolean} */
74340
76093
  ({ state, editor, dispatch }) => {
76094
+ const searchResults = this.storage?.searchResults;
76095
+ if (Array.isArray(searchResults) && searchResults.length > 0) {
76096
+ const firstMatch = searchResults[0];
76097
+ const from3 = firstMatch.ranges?.[0]?.from ?? firstMatch.from;
76098
+ const to = firstMatch.ranges?.[0]?.to ?? firstMatch.to;
76099
+ if (typeof from3 !== "number" || typeof to !== "number") {
76100
+ return false;
76101
+ }
76102
+ editor.view.focus();
76103
+ const tr2 = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
76104
+ if (dispatch) dispatch(tr2);
76105
+ const presentationEditor2 = editor.presentationEditor;
76106
+ if (presentationEditor2 && typeof presentationEditor2.scrollToPosition === "function") {
76107
+ const didScroll = presentationEditor2.scrollToPosition(from3, { block: "center" });
76108
+ if (didScroll) return true;
76109
+ }
76110
+ try {
76111
+ const domPos = editor.view.domAtPos(from3);
76112
+ if (domPos?.node?.scrollIntoView) {
76113
+ domPos.node.scrollIntoView(true);
76114
+ }
76115
+ } catch {
76116
+ }
76117
+ return true;
76118
+ }
74341
76119
  const highlights = getMatchHighlights(state);
74342
76120
  if (!highlights) return false;
74343
76121
  const decorations = highlights.find();
74344
76122
  if (!decorations?.length) return false;
74345
- const firstMatch = decorations[0];
76123
+ const firstDeco = decorations[0];
74346
76124
  editor.view.focus();
74347
- const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstMatch.from, firstMatch.to)).scrollIntoView();
76125
+ const tr = state.tr.setSelection(TextSelection$1.create(state.doc, firstDeco.from, firstDeco.to)).scrollIntoView();
74348
76126
  if (dispatch) dispatch(tr);
74349
76127
  const presentationEditor = editor.presentationEditor;
74350
76128
  if (presentationEditor && typeof presentationEditor.scrollToPosition === "function") {
74351
- const didScroll = presentationEditor.scrollToPosition(firstMatch.from, { block: "center" });
76129
+ const didScroll = presentationEditor.scrollToPosition(firstDeco.from, { block: "center" });
74352
76130
  if (didScroll) return true;
74353
76131
  }
74354
- const domPos = editor.view.domAtPos(firstMatch.from);
74355
- domPos?.node?.scrollIntoView(true);
76132
+ try {
76133
+ const domPos = editor.view.domAtPos(firstDeco.from);
76134
+ if (domPos?.node?.scrollIntoView) {
76135
+ domPos.node.scrollIntoView(true);
76136
+ }
76137
+ } catch {
76138
+ }
74356
76139
  return true;
74357
76140
  }
74358
76141
  ),
@@ -74370,53 +76153,57 @@ const Search = Extension.create({
74370
76153
  *
74371
76154
  * // Search without visual highlighting
74372
76155
  * const silentMatches = editor.commands.search('test', { highlight: false })
74373
- * @note Returns array of SearchMatch objects with positions and IDs
76156
+ *
76157
+ * // Cross-paragraph search (works by default for plain strings)
76158
+ * const crossParagraphMatches = editor.commands.search('end of paragraph start of next')
76159
+ * @note Returns array of SearchMatch objects with positions and IDs.
76160
+ * Plain string searches are whitespace-flexible and match across paragraphs.
76161
+ * Regex searches match exactly as specified.
74374
76162
  */
74375
76163
  search: (patternInput, options = {}) => (
74376
76164
  /** @returns {SearchMatch[]} */
74377
- ({ state, dispatch }) => {
76165
+ ({ state, dispatch, editor }) => {
74378
76166
  if (options != null && (typeof options !== "object" || Array.isArray(options))) {
74379
76167
  throw new TypeError("Search options must be an object");
74380
76168
  }
74381
76169
  const highlight = typeof options?.highlight === "boolean" ? options.highlight : true;
74382
- let pattern;
76170
+ const maxMatches = typeof options?.maxMatches === "number" ? options.maxMatches : 1e3;
74383
76171
  let caseSensitive = false;
74384
- let regexp = false;
74385
- const wholeWord = false;
76172
+ let searchPattern = patternInput;
74386
76173
  if (isRegExp(patternInput)) {
74387
- const regexPattern = (
74388
- /** @type {RegExp} */
74389
- patternInput
74390
- );
74391
- regexp = true;
74392
- pattern = regexPattern.source;
74393
- caseSensitive = !regexPattern.flags.includes("i");
76174
+ caseSensitive = !patternInput.flags.includes("i");
76175
+ searchPattern = patternInput;
74394
76176
  } else if (typeof patternInput === "string" && /^\/(.+)\/([gimsuy]*)$/.test(patternInput)) {
74395
76177
  const [, body, flags] = patternInput.match(/^\/(.+)\/([gimsuy]*)$/);
74396
- regexp = true;
74397
- pattern = body;
74398
76178
  caseSensitive = !flags.includes("i");
76179
+ searchPattern = new RegExp(body, flags.includes("g") ? flags : flags + "g");
74399
76180
  } else {
74400
- pattern = String(patternInput);
76181
+ searchPattern = String(patternInput);
74401
76182
  }
74402
- const query = new SearchQuery({
74403
- search: pattern,
76183
+ const searchIndex = this.storage.searchIndex;
76184
+ searchIndex.ensureValid(state.doc);
76185
+ const indexMatches = searchIndex.search(searchPattern, {
74404
76186
  caseSensitive,
74405
- regexp,
74406
- wholeWord
76187
+ maxMatches
74407
76188
  });
74408
- const tr = setSearchState(state.tr, query, null, { highlight });
74409
- dispatch(tr);
74410
- const newState = state.apply(tr);
74411
- const decoSet = getMatchHighlights(newState);
74412
- const matches = decoSet ? decoSet.find() : [];
74413
- const resultMatches = matches.map((d) => ({
74414
- from: d.from,
74415
- to: d.to,
74416
- text: newState.doc.textBetween(d.from, d.to),
74417
- id: v4()
74418
- }));
76189
+ const resultMatches = [];
76190
+ for (const indexMatch of indexMatches) {
76191
+ const ranges = searchIndex.offsetRangeToDocRanges(indexMatch.start, indexMatch.end);
76192
+ if (ranges.length === 0) continue;
76193
+ const matchTexts = ranges.map((r2) => state.doc.textBetween(r2.from, r2.to));
76194
+ const combinedText = matchTexts.join("");
76195
+ const match = {
76196
+ from: ranges[0].from,
76197
+ to: ranges[ranges.length - 1].to,
76198
+ text: combinedText,
76199
+ id: v4(),
76200
+ ranges,
76201
+ trackerIds: []
76202
+ };
76203
+ resultMatches.push(match);
76204
+ }
74419
76205
  this.storage.searchResults = resultMatches;
76206
+ this.storage.highlightEnabled = highlight;
74420
76207
  return resultMatches;
74421
76208
  }
74422
76209
  ),
@@ -74427,12 +76214,48 @@ const Search = Extension.create({
74427
76214
  * @example
74428
76215
  * const searchResults = editor.commands.search('test string')
74429
76216
  * editor.commands.goToSearchResult(searchResults[3])
74430
- * @note Scrolls to match and selects it
76217
+ * @note Scrolls to match and selects it. For multi-range matches (cross-paragraph),
76218
+ * selects the first range and scrolls to it.
74431
76219
  */
74432
76220
  goToSearchResult: (match) => (
74433
76221
  /** @returns {boolean} */
74434
76222
  ({ state, dispatch, editor }) => {
74435
- const { from: from3, to } = match;
76223
+ const positionTracker = getPositionTracker(editor);
76224
+ const doc2 = state.doc;
76225
+ const highlights = getMatchHighlights(state);
76226
+ let from3, to;
76227
+ if (match?.ranges && match.ranges.length > 0 && match?.trackerIds && match.trackerIds.length > 0) {
76228
+ if (positionTracker?.resolve && match.trackerIds[0]) {
76229
+ const resolved = positionTracker.resolve(match.trackerIds[0]);
76230
+ if (resolved) {
76231
+ from3 = resolved.from;
76232
+ to = resolved.to;
76233
+ }
76234
+ }
76235
+ if (from3 === void 0) {
76236
+ from3 = match.ranges[0].from;
76237
+ to = match.ranges[0].to;
76238
+ }
76239
+ } else {
76240
+ from3 = match.from;
76241
+ to = match.to;
76242
+ if (positionTracker?.resolve && match?.id) {
76243
+ const resolved = positionTracker.resolve(match.id);
76244
+ if (resolved) {
76245
+ from3 = resolved.from;
76246
+ to = resolved.to;
76247
+ }
76248
+ }
76249
+ }
76250
+ const normalized = resolveSearchRange({
76251
+ doc: doc2,
76252
+ from: from3,
76253
+ to,
76254
+ expectedText: match?.text ?? null,
76255
+ highlights
76256
+ });
76257
+ from3 = normalized.from;
76258
+ to = normalized.to;
74436
76259
  editor.view.focus();
74437
76260
  const tr = state.tr.setSelection(TextSelection$1.create(state.doc, from3, to)).scrollIntoView();
74438
76261
  if (dispatch) dispatch(tr);
@@ -75085,6 +76908,7 @@ const getStarterExtensions = () => {
75085
76908
  CommentRangeStart,
75086
76909
  CommentRangeEnd,
75087
76910
  CommentReference,
76911
+ FootnoteReference,
75088
76912
  Document,
75089
76913
  FontFamily,
75090
76914
  FontSize,