docxodus 9.8.0 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/dist/canonical.d.ts +7 -0
- package/dist/canonical.d.ts.map +1 -0
- package/dist/canonical.js +56 -0
- package/dist/canonical.js.map +1 -0
- package/dist/docxodus.worker.d.ts +2 -1
- package/dist/docxodus.worker.d.ts.map +1 -1
- package/dist/docxodus.worker.js +244 -3
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor.bundle.js +2024 -388
- package/dist/editor.d.ts +22 -2
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +94 -9
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +2912 -437
- package/dist/embed.iife.js +2911 -436
- package/dist/export-assets.json +283 -0
- package/dist/export-browser.bundle.js +7990 -0
- package/dist/export-browser.d.ts +266 -0
- package/dist/export-browser.d.ts.map +1 -0
- package/dist/export-browser.js +2915 -0
- package/dist/export-browser.js.map +1 -0
- package/dist/export-resource-limits-v1.json +57 -0
- package/dist/font-contract.d.ts +150 -0
- package/dist/font-contract.d.ts.map +1 -0
- package/dist/font-contract.js +59 -0
- package/dist/font-contract.js.map +1 -0
- package/dist/font-runtime.d.ts +33 -0
- package/dist/font-runtime.d.ts.map +1 -0
- package/dist/font-runtime.js +1100 -0
- package/dist/font-runtime.js.map +1 -0
- package/dist/index.d.ts +62 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +130 -16
- package/dist/index.js.map +1 -1
- package/dist/page-geometry.d.ts +8 -1
- package/dist/page-geometry.d.ts.map +1 -1
- package/dist/page-geometry.js +37 -11
- package/dist/page-geometry.js.map +1 -1
- package/dist/pagination.bundle.js +1937 -380
- package/dist/pagination.d.ts +277 -15
- package/dist/pagination.d.ts.map +1 -1
- package/dist/pagination.js +2055 -438
- package/dist/pagination.js.map +1 -1
- package/dist/react.d.ts +11 -4
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +38 -5
- package/dist/react.js.map +1 -1
- package/dist/render-report-v2.schema.json +1581 -0
- package/dist/ribbon-chrome.d.ts +1 -1
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +46 -0
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon.js +70 -0
- package/dist/ribbon.js.map +1 -1
- package/dist/session.bundle.js +695 -29
- package/dist/session.d.ts +134 -19
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +562 -25
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +1207 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.Framework.wasm.br +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm +0 -0
- package/dist/wasm/_framework/DocumentFormat.OpenXml.wasm.br +0 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm.br +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm +0 -0
- package/dist/wasm/_framework/System.Diagnostics.Process.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Packaging.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/System.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +38 -38
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
- package/dist/worker-proxy.bundle.js +196 -29
- package/dist/worker-proxy.d.ts +36 -3
- package/dist/worker-proxy.d.ts.map +1 -1
- package/dist/worker-proxy.js +186 -33
- package/dist/worker-proxy.js.map +1 -1
- package/package.json +24 -7
package/dist/editor.bundle.js
CHANGED
|
@@ -172,17 +172,41 @@ var DocxodusEditor = (() => {
|
|
|
172
172
|
function ptToPx(pt) {
|
|
173
173
|
return pt / 0.75;
|
|
174
174
|
}
|
|
175
|
+
function finiteDataNumber(value, fallback) {
|
|
176
|
+
if (value === void 0 || value.trim() === "") return fallback;
|
|
177
|
+
const parsed = Number(value);
|
|
178
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
179
|
+
}
|
|
180
|
+
function positiveDataNumber(value, fallback) {
|
|
181
|
+
const parsed = finiteDataNumber(value, fallback);
|
|
182
|
+
return parsed > 0 ? parsed : fallback;
|
|
183
|
+
}
|
|
184
|
+
function nonNegativeDataNumber(value, fallback) {
|
|
185
|
+
const parsed = finiteDataNumber(value, fallback);
|
|
186
|
+
return parsed >= 0 ? parsed : fallback;
|
|
187
|
+
}
|
|
188
|
+
function positiveFinite(value, fallback) {
|
|
189
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
190
|
+
}
|
|
175
191
|
function parseSectionDimensions(section) {
|
|
176
|
-
const pageWidth =
|
|
177
|
-
const pageHeight =
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const
|
|
192
|
+
const pageWidth = positiveDataNumber(section.dataset.pageWidth, DEFAULT_PAGE_WIDTH);
|
|
193
|
+
const pageHeight = positiveDataNumber(section.dataset.pageHeight, DEFAULT_PAGE_HEIGHT);
|
|
194
|
+
const marginTop = finiteDataNumber(section.dataset.marginTop, DEFAULT_MARGIN);
|
|
195
|
+
const marginRight = finiteDataNumber(section.dataset.marginRight, DEFAULT_MARGIN);
|
|
196
|
+
const marginBottom = finiteDataNumber(section.dataset.marginBottom, DEFAULT_MARGIN);
|
|
197
|
+
const marginLeft = finiteDataNumber(section.dataset.marginLeft, DEFAULT_MARGIN);
|
|
198
|
+
const derivedContentWidth = positiveFinite(pageWidth - marginLeft - marginRight, pageWidth);
|
|
199
|
+
const derivedContentHeight = positiveFinite(pageHeight - marginTop - marginBottom, pageHeight);
|
|
200
|
+
const contentWidth = positiveDataNumber(section.dataset.contentWidth, derivedContentWidth);
|
|
201
|
+
const contentHeight = positiveDataNumber(section.dataset.contentHeight, derivedContentHeight);
|
|
202
|
+
const headerDistance = nonNegativeDataNumber(
|
|
203
|
+
section.dataset.headerHeight,
|
|
204
|
+
DEFAULT_HEADER_FOOTER_DISTANCE
|
|
205
|
+
);
|
|
206
|
+
const footerDistance = nonNegativeDataNumber(
|
|
207
|
+
section.dataset.footerHeight,
|
|
208
|
+
DEFAULT_HEADER_FOOTER_DISTANCE
|
|
209
|
+
);
|
|
186
210
|
return {
|
|
187
211
|
pageWidth,
|
|
188
212
|
pageHeight,
|
|
@@ -259,8 +283,12 @@ var DocxodusEditor = (() => {
|
|
|
259
283
|
}
|
|
260
284
|
|
|
261
285
|
// src/pagination.ts
|
|
286
|
+
function samePageBox(a, b) {
|
|
287
|
+
return a.pageWidth === b.pageWidth && a.pageHeight === b.pageHeight && a.marginTop === b.marginTop && a.marginRight === b.marginRight && a.marginBottom === b.marginBottom && a.marginLeft === b.marginLeft;
|
|
288
|
+
}
|
|
262
289
|
var MAX_FOOTNOTE_AREA_RATIO = 0.6;
|
|
263
|
-
var
|
|
290
|
+
var FOOTNOTE_MEASUREMENT_GUARD_PT = 2;
|
|
291
|
+
var FOOTNOTE_SOURCE_POSITION_ATTR = "data-pagination-footnote-source-position";
|
|
264
292
|
var PaginationEngine = class {
|
|
265
293
|
/**
|
|
266
294
|
* Creates a new pagination engine.
|
|
@@ -270,24 +298,46 @@ var DocxodusEditor = (() => {
|
|
|
270
298
|
* @param options - Pagination options
|
|
271
299
|
*/
|
|
272
300
|
constructor(staging, container, options = {}) {
|
|
301
|
+
this.createdPageCount = 0;
|
|
302
|
+
this.footnoteSeparator = null;
|
|
303
|
+
this.footnoteContinuationSeparator = null;
|
|
304
|
+
this.footnoteLayoutLikeWord8 = false;
|
|
273
305
|
this.pendingFootnoteContinuation = null;
|
|
274
306
|
/** Per-section `w:pgNumType` (start / format), read off the section wrappers. */
|
|
275
307
|
this.pageNumbering = /* @__PURE__ */ new Map();
|
|
276
|
-
this.
|
|
277
|
-
this.
|
|
308
|
+
this.lastPages = [];
|
|
309
|
+
this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
|
|
310
|
+
this.state = "ready";
|
|
311
|
+
const ownerDocument = typeof staging !== "string" ? staging.ownerDocument : typeof container !== "string" ? container.ownerDocument : globalThis.document;
|
|
312
|
+
this.stagingElement = typeof staging === "string" ? ownerDocument.getElementById(staging) : staging;
|
|
313
|
+
this.containerElement = typeof container === "string" ? ownerDocument.getElementById(container) : container;
|
|
278
314
|
if (!this.stagingElement) {
|
|
279
315
|
throw new Error("Staging element not found");
|
|
280
316
|
}
|
|
281
317
|
if (!this.containerElement) {
|
|
282
318
|
throw new Error("Container element not found");
|
|
283
319
|
}
|
|
320
|
+
if (this.stagingElement.ownerDocument !== this.containerElement.ownerDocument) {
|
|
321
|
+
throw new Error("Staging and container elements must belong to the same document");
|
|
322
|
+
}
|
|
323
|
+
const view = ownerDocument.defaultView;
|
|
324
|
+
if (!view) {
|
|
325
|
+
throw new Error("Pagination requires an attached document with a defaultView");
|
|
326
|
+
}
|
|
327
|
+
this.document = ownerDocument;
|
|
328
|
+
this.view = view;
|
|
284
329
|
this.scale = options.scale ?? 1;
|
|
285
330
|
this.cssPrefix = options.cssPrefix ?? "page-";
|
|
286
331
|
this.showPageNumbers = options.showPageNumbers ?? true;
|
|
287
332
|
this.pageGap = options.pageGap ?? 20;
|
|
288
333
|
this.fragmentParagraphs = options.fragmentParagraphs ?? false;
|
|
334
|
+
this.deferFragmentIdentities = options.deferFragmentIdentities ?? false;
|
|
335
|
+
this.cancellationCheckpoint = options.checkCancellation;
|
|
336
|
+
this.pageCountCheckpoint = options.checkPageCount;
|
|
337
|
+
this.layoutToken = options.layoutToken;
|
|
289
338
|
this.hfRegistry = /* @__PURE__ */ new Map();
|
|
290
339
|
this.footnoteRegistry = /* @__PURE__ */ new Map();
|
|
340
|
+
this.commentMarginRegistry = /* @__PURE__ */ new Map();
|
|
291
341
|
}
|
|
292
342
|
/**
|
|
293
343
|
* Runs the pagination process.
|
|
@@ -295,31 +345,469 @@ var DocxodusEditor = (() => {
|
|
|
295
345
|
* @returns PaginationResult with page information
|
|
296
346
|
*/
|
|
297
347
|
paginate() {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
"
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.
|
|
313
|
-
this.
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
348
|
+
this.checkpoint();
|
|
349
|
+
if (this.state !== "ready") {
|
|
350
|
+
throw new Error(`PaginationEngine is one-shot and is already ${this.state}`);
|
|
351
|
+
}
|
|
352
|
+
if (!this.stagingElement.isConnected || !this.containerElement.isConnected) {
|
|
353
|
+
throw new Error("Pagination requires staging and container elements attached to their document");
|
|
354
|
+
}
|
|
355
|
+
if (this.document.documentElement.getBoundingClientRect().width <= 0) {
|
|
356
|
+
throw new Error("Pagination requires a browsing context with non-zero layout");
|
|
357
|
+
}
|
|
358
|
+
this.state = "running";
|
|
359
|
+
try {
|
|
360
|
+
const pages = [];
|
|
361
|
+
let pageNumber = 1;
|
|
362
|
+
this.hfRegistry = this.parseHeaderFooterRegistry();
|
|
363
|
+
this.footnoteRegistry = this.parseFootnoteRegistry();
|
|
364
|
+
({
|
|
365
|
+
normal: this.footnoteSeparator,
|
|
366
|
+
continuation: this.footnoteContinuationSeparator
|
|
367
|
+
} = this.parseFootnoteSeparators());
|
|
368
|
+
this.commentMarginRegistry = this.parseCommentMarginRegistry();
|
|
369
|
+
this.footnoteLayoutLikeWord8 = this.stagingElement.dataset.footnoteLayoutLikeWord8 === "true";
|
|
370
|
+
const sections = this.stagingElement.querySelectorAll(
|
|
371
|
+
"[data-section-index]"
|
|
372
|
+
);
|
|
373
|
+
this.pageNumbering = this.parsePageNumbering(sections);
|
|
374
|
+
const sectionsToProcess = sections.length > 0 ? Array.from(sections) : [this.stagingElement];
|
|
375
|
+
this.expectedPageMapAnchorIds = /* @__PURE__ */ new Set();
|
|
376
|
+
const referencedFootnoteIds = /* @__PURE__ */ new Set();
|
|
377
|
+
const referencedCommentIds = /* @__PURE__ */ new Set();
|
|
378
|
+
for (const section of sectionsToProcess) {
|
|
379
|
+
this.checkpoint();
|
|
380
|
+
this.collectExpectedSourceAnchors(section, this.expectedPageMapAnchorIds, true);
|
|
381
|
+
for (const reference of Array.from(section.querySelectorAll("[data-footnote-id]"))) {
|
|
382
|
+
this.checkpoint();
|
|
383
|
+
if (reference.closest("#pagination-footnote-registry, #pagination-hf-registry")) continue;
|
|
384
|
+
const id = reference.dataset.footnoteId;
|
|
385
|
+
if (id) referencedFootnoteIds.add(id);
|
|
386
|
+
}
|
|
387
|
+
for (const reference of Array.from(section.querySelectorAll("[data-comment-id]"))) {
|
|
388
|
+
this.checkpoint();
|
|
389
|
+
if (reference.closest(
|
|
390
|
+
"#pagination-comment-margin-registry, #pagination-footnote-registry, #pagination-hf-registry"
|
|
391
|
+
)) continue;
|
|
392
|
+
const id = reference.dataset.commentId;
|
|
393
|
+
if (id) referencedCommentIds.add(id);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
for (const id of referencedFootnoteIds) {
|
|
397
|
+
this.checkpoint();
|
|
398
|
+
const source = this.footnoteRegistry.get(id);
|
|
399
|
+
if (!source) continue;
|
|
400
|
+
this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
|
|
401
|
+
for (const reference of Array.from(
|
|
402
|
+
source.querySelectorAll("[data-comment-id]")
|
|
403
|
+
)) {
|
|
404
|
+
this.checkpoint();
|
|
405
|
+
const commentId = reference.dataset.commentId;
|
|
406
|
+
if (commentId) referencedCommentIds.add(commentId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const id of referencedCommentIds) {
|
|
410
|
+
this.checkpoint();
|
|
411
|
+
const source = this.commentMarginRegistry.get(id);
|
|
412
|
+
if (source) this.collectExpectedSourceAnchors(source, this.expectedPageMapAnchorIds);
|
|
413
|
+
}
|
|
414
|
+
const runs = [];
|
|
415
|
+
for (const section of sectionsToProcess) {
|
|
416
|
+
this.checkpoint();
|
|
417
|
+
const dims = parseSectionDimensions(section);
|
|
418
|
+
const previous = runs[runs.length - 1];
|
|
419
|
+
if (previous && section.dataset.sectionType === "continuous" && samePageBox(previous.dims, dims)) {
|
|
420
|
+
previous.sections.push(section);
|
|
421
|
+
previous.sectionDimensions.set(
|
|
422
|
+
parseInt(section.dataset.sectionIndex || "0", 10),
|
|
423
|
+
dims
|
|
424
|
+
);
|
|
425
|
+
} else {
|
|
426
|
+
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
427
|
+
runs.push({
|
|
428
|
+
sections: [section],
|
|
429
|
+
sectionIndex,
|
|
430
|
+
sectionType: section.dataset.sectionType ?? "nextPage",
|
|
431
|
+
dims,
|
|
432
|
+
sectionDimensions: /* @__PURE__ */ new Map([[sectionIndex, dims]])
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const run of runs) {
|
|
437
|
+
this.checkpoint();
|
|
438
|
+
const needsParityFiller = pages.length > 0 && (run.sectionType === "oddPage" && pageNumber % 2 === 0 || run.sectionType === "evenPage" && pageNumber % 2 === 1);
|
|
439
|
+
if (needsParityFiller) {
|
|
440
|
+
const precedingPage = pages[pages.length - 1];
|
|
441
|
+
const precedingPageInSection = parseInt(
|
|
442
|
+
precedingPage.element.dataset.pageInSection ?? "1",
|
|
443
|
+
10
|
|
444
|
+
);
|
|
445
|
+
const precedingDisplayedPageNumber = parseInt(
|
|
446
|
+
precedingPage.element.dataset.displayedPageNumber ?? String(precedingPage.pageNumber),
|
|
447
|
+
10
|
|
448
|
+
);
|
|
449
|
+
const filler = this.createPage(
|
|
450
|
+
precedingPage.dimensions,
|
|
451
|
+
pageNumber,
|
|
452
|
+
precedingPage.sectionIndex,
|
|
453
|
+
precedingDisplayedPageNumber + 1,
|
|
454
|
+
[],
|
|
455
|
+
precedingPageInSection + 1,
|
|
456
|
+
[],
|
|
457
|
+
0,
|
|
458
|
+
null,
|
|
459
|
+
void 0,
|
|
460
|
+
true
|
|
461
|
+
);
|
|
462
|
+
pages.push(filler);
|
|
463
|
+
pageNumber++;
|
|
464
|
+
}
|
|
465
|
+
this.stagingElement.style.visibility = "hidden";
|
|
466
|
+
this.stagingElement.style.position = "absolute";
|
|
467
|
+
this.stagingElement.style.left = "-9999px";
|
|
468
|
+
this.stagingElement.style.display = "block";
|
|
469
|
+
const blocks = [];
|
|
470
|
+
for (const section of run.sections) {
|
|
471
|
+
this.checkpoint();
|
|
472
|
+
const sectionIndex = parseInt(section.dataset.sectionIndex || "0", 10);
|
|
473
|
+
const sectionDims = run.sectionDimensions.get(sectionIndex) ?? run.dims;
|
|
474
|
+
section.style.width = `${sectionDims.contentWidth}pt`;
|
|
475
|
+
const columnCount = parseInt(section.dataset.cols || "1", 10);
|
|
476
|
+
if (columnCount > 1) {
|
|
477
|
+
const gap = parseFloat(section.dataset.colGap || "");
|
|
478
|
+
blocks.push(...this.buildColumnBlocks(
|
|
479
|
+
section,
|
|
480
|
+
sectionDims,
|
|
481
|
+
sectionIndex,
|
|
482
|
+
columnCount,
|
|
483
|
+
Number.isFinite(gap) ? gap : 36
|
|
484
|
+
));
|
|
485
|
+
} else {
|
|
486
|
+
blocks.push(...this.measureBlocks(section, sectionDims, sectionIndex));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const sectionPages = this.flowToPages(
|
|
490
|
+
blocks,
|
|
491
|
+
run.dims,
|
|
492
|
+
pageNumber,
|
|
493
|
+
run.sectionIndex,
|
|
494
|
+
run.sectionDimensions
|
|
495
|
+
);
|
|
496
|
+
this.checkpoint();
|
|
497
|
+
pages.push(...sectionPages);
|
|
498
|
+
pageNumber += sectionPages.length;
|
|
499
|
+
}
|
|
500
|
+
this.stagingElement.style.display = "none";
|
|
501
|
+
this.substitutePageNumberFields(pages.length);
|
|
502
|
+
for (const page of pages) {
|
|
503
|
+
this.checkpoint();
|
|
504
|
+
if (page.element.dataset.sectionFiller === "true") continue;
|
|
505
|
+
const pageInSection = parseInt(page.element.dataset.pageInSection || "1", 10);
|
|
506
|
+
const displayedPageNumber = parseInt(
|
|
507
|
+
page.element.dataset.displayedPageNumber || String(page.pageNumber),
|
|
508
|
+
10
|
|
509
|
+
);
|
|
510
|
+
const header = this.selectHeader(page.sectionIndex, pageInSection, displayedPageNumber);
|
|
511
|
+
const footer = this.selectFooter(page.sectionIndex, pageInSection, displayedPageNumber);
|
|
512
|
+
if (header) this.collectExpectedSourceAnchors(header, this.expectedPageMapAnchorIds);
|
|
513
|
+
if (footer) this.collectExpectedSourceAnchors(footer, this.expectedPageMapAnchorIds);
|
|
514
|
+
}
|
|
515
|
+
this.qualifyPageFragments(pages);
|
|
516
|
+
this.transferVisibleFragmentTargets();
|
|
517
|
+
if (!this.deferFragmentIdentities) this.normalizeVisiblePageFragments(pages);
|
|
518
|
+
this.lastPages = pages;
|
|
519
|
+
const result = {
|
|
520
|
+
totalPages: pages.length,
|
|
521
|
+
pages,
|
|
522
|
+
pageMap: this.layoutToken ? this.materializePageMap(
|
|
523
|
+
this.layoutToken.documentVersion,
|
|
524
|
+
this.layoutToken.rendererFingerprint
|
|
525
|
+
) : void 0
|
|
526
|
+
};
|
|
527
|
+
this.state = "complete";
|
|
528
|
+
return result;
|
|
529
|
+
} catch (error) {
|
|
530
|
+
this.state = "failed";
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
checkpoint() {
|
|
535
|
+
this.cancellationCheckpoint?.();
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Normalize visible fragment identities after a caller applies final standalone styles. This
|
|
539
|
+
* deliberately runs before the stability barrier; materializePageMap is read-only so PageMap
|
|
540
|
+
* measurement cannot mutate a tree after it was declared stable.
|
|
541
|
+
*/
|
|
542
|
+
normalizePageMapFragmentIdentities() {
|
|
543
|
+
if (this.lastPages.length === 0) {
|
|
544
|
+
throw new Error("paginate() must complete before fragment identities can be normalized");
|
|
545
|
+
}
|
|
546
|
+
this.normalizeVisiblePageFragments(this.lastPages);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Materialize the last completed browser layout as portable page-relative point geometry.
|
|
550
|
+
* The caller supplies both invalidation tokens; this engine never guesses a document version
|
|
551
|
+
* or renderer fingerprint.
|
|
552
|
+
*/
|
|
553
|
+
materializePageMap(documentVersion, rendererFingerprint) {
|
|
554
|
+
if (!Number.isSafeInteger(documentVersion) || documentVersion < 0) {
|
|
555
|
+
throw new Error("documentVersion must be a non-negative safe integer");
|
|
556
|
+
}
|
|
557
|
+
if (!rendererFingerprint) throw new Error("rendererFingerprint must be non-empty");
|
|
558
|
+
if (this.lastPages.length === 0) throw new Error("paginate() must complete before materializePageMap()");
|
|
559
|
+
const pages = this.lastPages.map((page) => ({
|
|
560
|
+
pageNumber: page.pageNumber,
|
|
561
|
+
pageInSection: parseInt(page.element.dataset.pageInSection || "1", 10),
|
|
562
|
+
width: page.dimensions.pageWidth,
|
|
563
|
+
height: page.dimensions.pageHeight,
|
|
564
|
+
sectionIndex: page.sectionIndex,
|
|
565
|
+
pageName: `docxodus-section-${page.sectionIndex}`
|
|
566
|
+
}));
|
|
567
|
+
const fragments = [];
|
|
568
|
+
const requiredAnchorIds = new Set(this.expectedPageMapAnchorIds);
|
|
569
|
+
if (requiredAnchorIds.size === 0) {
|
|
570
|
+
throw new Error("cannot publish an available PageMap without canonical source inventory");
|
|
571
|
+
}
|
|
572
|
+
const measuredAnchorIds = /* @__PURE__ */ new Set();
|
|
573
|
+
const emittedFragmentCounts = /* @__PURE__ */ new Map();
|
|
574
|
+
for (const page of this.lastPages) {
|
|
575
|
+
this.checkpoint();
|
|
576
|
+
const pageRect = page.element.getBoundingClientRect();
|
|
577
|
+
if (pageRect.width <= 0 || pageRect.height <= 0) {
|
|
578
|
+
throw new Error(`page ${page.pageNumber} has no measurable geometry`);
|
|
579
|
+
}
|
|
580
|
+
const pointPerRenderedX = page.dimensions.pageWidth / pageRect.width;
|
|
581
|
+
const pointPerRenderedY = page.dimensions.pageHeight / pageRect.height;
|
|
582
|
+
const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
|
|
583
|
+
for (const element of Array.from(nodes)) {
|
|
584
|
+
this.checkpoint();
|
|
585
|
+
if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
|
|
586
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
587
|
+
if (!anchorId || !element.dataset.pageFragmentId || !Number.isInteger(parseInt(element.dataset.fragmentIndex || "", 10))) {
|
|
588
|
+
throw new Error(`page ${page.pageNumber} contains an unqualified source anchor`);
|
|
589
|
+
}
|
|
590
|
+
const rect = element.getBoundingClientRect();
|
|
591
|
+
const style = this.view.getComputedStyle(element);
|
|
592
|
+
const deliberatelyHidden = style.display === "none" || style.visibility === "hidden";
|
|
593
|
+
if (deliberatelyHidden) continue;
|
|
594
|
+
requiredAnchorIds.add(anchorId);
|
|
595
|
+
const visibleRect = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
|
|
596
|
+
const left2 = visibleRect.left;
|
|
597
|
+
const top2 = visibleRect.top;
|
|
598
|
+
const right2 = visibleRect.right;
|
|
599
|
+
const bottom2 = visibleRect.bottom;
|
|
600
|
+
if (rect.width <= 0 || rect.height <= 0 || right2 <= left2 || bottom2 <= top2) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
measuredAnchorIds.add(anchorId);
|
|
604
|
+
const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
|
|
605
|
+
emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
606
|
+
const fragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
607
|
+
if (element.dataset.fragmentIndex !== String(fragmentIndex) || element.dataset.pageFragmentId !== fragmentId || element.dataset.pageNumber !== String(page.pageNumber)) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
`page ${page.pageNumber} fragment identity changed after final-tree normalization`
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
fragments.push({
|
|
613
|
+
fragmentId,
|
|
614
|
+
anchorId,
|
|
615
|
+
fragmentIndex,
|
|
616
|
+
pageNumber: page.pageNumber,
|
|
617
|
+
geometry: {
|
|
618
|
+
x: (left2 - pageRect.left) * pointPerRenderedX,
|
|
619
|
+
y: (top2 - pageRect.top) * pointPerRenderedY,
|
|
620
|
+
width: (right2 - left2) * pointPerRenderedX,
|
|
621
|
+
height: (bottom2 - top2) * pointPerRenderedY
|
|
622
|
+
},
|
|
623
|
+
story: this.storyForCanonicalAnchor(anchorId),
|
|
624
|
+
inTableCell: element.matches("td,th") || element.closest("td,th") !== null
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const missingAnchor = Array.from(requiredAnchorIds).find((id) => !measuredAnchorIds.has(id));
|
|
629
|
+
if (missingAnchor) {
|
|
630
|
+
throw new Error(`source anchor ${missingAnchor} has no measurable fragment in the paginated layout`);
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
schemaVersion: 1,
|
|
634
|
+
mode: "paginated",
|
|
635
|
+
availability: "available",
|
|
636
|
+
documentVersion,
|
|
637
|
+
rendererFingerprint,
|
|
638
|
+
pages,
|
|
639
|
+
fragments
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
normalizeVisiblePageFragments(pages) {
|
|
643
|
+
const emittedFragmentCounts = /* @__PURE__ */ new Map();
|
|
644
|
+
for (const page of pages) {
|
|
645
|
+
this.checkpoint();
|
|
646
|
+
const pageRect = page.element.getBoundingClientRect();
|
|
647
|
+
for (const element of Array.from(
|
|
648
|
+
page.element.querySelectorAll("[data-source-anchor-id]")
|
|
649
|
+
)) {
|
|
650
|
+
this.checkpoint();
|
|
651
|
+
if (this.isDeliberatelyUnrenderedSource(element, page.element)) continue;
|
|
652
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
653
|
+
if (!anchorId) continue;
|
|
654
|
+
const style = this.view.getComputedStyle(element);
|
|
655
|
+
if (style.display === "none" || style.visibility === "hidden") continue;
|
|
656
|
+
const rect = element.getBoundingClientRect();
|
|
657
|
+
const visible = this.intersectWithClippingAncestors(element, page.element, pageRect, rect);
|
|
658
|
+
if (rect.width <= 0 || rect.height <= 0 || visible.right <= visible.left || visible.bottom <= visible.top) continue;
|
|
659
|
+
const fragmentIndex = emittedFragmentCounts.get(anchorId) ?? 0;
|
|
660
|
+
emittedFragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
661
|
+
element.dataset.pageNumber = String(page.pageNumber);
|
|
662
|
+
element.dataset.fragmentIndex = String(fragmentIndex);
|
|
663
|
+
element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Intersect an element with every ancestor that establishes an overflow clip before the page
|
|
669
|
+
* root. getBoundingClientRect() reports layout outside those clips, which is not rendered and
|
|
670
|
+
* therefore must not satisfy PageMap completeness or inflate portable geometry.
|
|
671
|
+
*/
|
|
672
|
+
intersectWithClippingAncestors(element, page, pageRect, rect) {
|
|
673
|
+
let left2 = Math.max(rect.left, pageRect.left);
|
|
674
|
+
let top2 = Math.max(rect.top, pageRect.top);
|
|
675
|
+
let right2 = Math.min(rect.right, pageRect.right);
|
|
676
|
+
let bottom2 = Math.min(rect.bottom, pageRect.bottom);
|
|
677
|
+
const clips = (value) => value === "hidden" || value === "clip" || value === "scroll" || value === "auto";
|
|
678
|
+
for (let ancestor = element.parentElement; ancestor && ancestor !== page; ancestor = ancestor.parentElement) {
|
|
679
|
+
const style = this.view.getComputedStyle(ancestor);
|
|
680
|
+
const clipsX = clips(style.overflowX);
|
|
681
|
+
const clipsY = clips(style.overflowY);
|
|
682
|
+
if (!clipsX && !clipsY) continue;
|
|
683
|
+
const ancestorRect = ancestor.getBoundingClientRect();
|
|
684
|
+
if (clipsX) {
|
|
685
|
+
left2 = Math.max(left2, ancestorRect.left);
|
|
686
|
+
right2 = Math.min(right2, ancestorRect.right);
|
|
687
|
+
}
|
|
688
|
+
if (clipsY) {
|
|
689
|
+
top2 = Math.max(top2, ancestorRect.top);
|
|
690
|
+
bottom2 = Math.min(bottom2, ancestorRect.bottom);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return { left: left2, top: top2, right: right2, bottom: bottom2 };
|
|
694
|
+
}
|
|
695
|
+
storyForCanonicalAnchor(anchorId) {
|
|
696
|
+
const first = anchorId.indexOf(":");
|
|
697
|
+
const second = first < 0 ? -1 : anchorId.indexOf(":", first + 1);
|
|
698
|
+
const scope = first >= 0 && second > first ? anchorId.slice(first + 1, second) : "body";
|
|
699
|
+
if (scope.startsWith("hdr")) return "header";
|
|
700
|
+
if (scope.startsWith("ftr")) return "footer";
|
|
701
|
+
if (scope === "fn") return "footnote";
|
|
702
|
+
if (scope === "en") return "endnote";
|
|
703
|
+
if (scope === "cmt") return "comment";
|
|
704
|
+
return "body";
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Add canonical IDs from an addressable source subtree to the pre-pagination inventory.
|
|
708
|
+
* Registry wrappers are excluded when scanning staging because selectable registry contents are
|
|
709
|
+
* inventoried separately. Producers may explicitly mark content that has no visual substrate
|
|
710
|
+
* with `data-page-map-exclude="true"`; native hidden semantics carry the same signal.
|
|
711
|
+
*/
|
|
712
|
+
collectExpectedSourceAnchors(source, destination, excludeRegistries = false) {
|
|
713
|
+
const candidates = source.matches("[data-source-anchor-id]") ? [source] : [];
|
|
714
|
+
candidates.push(...Array.from(source.querySelectorAll("[data-source-anchor-id]")));
|
|
715
|
+
for (const element of candidates) {
|
|
716
|
+
if (excludeRegistries && element.closest(
|
|
717
|
+
"#pagination-hf-registry, #pagination-footnote-registry, #pagination-comment-margin-registry"
|
|
718
|
+
)) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (this.isZeroHeightExplicitBreakCarrier(element)) {
|
|
722
|
+
element.dataset.pageMapExclude = "true";
|
|
723
|
+
}
|
|
724
|
+
if (this.isDeliberatelyUnrenderedSource(element, source)) continue;
|
|
725
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
726
|
+
if (anchorId) destination.add(anchorId);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
isZeroHeightExplicitBreakCarrier(element) {
|
|
730
|
+
const next = element.nextElementSibling;
|
|
731
|
+
const bounds = element.getBoundingClientRect();
|
|
732
|
+
return element.hasAttribute("data-source-anchor-id") && (element.textContent ?? "").replace(/\u00a0/g, "").trim() === "" && bounds.height <= 0.01 && !element.querySelector("[data-source-anchor-id]") && !element.querySelector("img,svg,canvas,table,hr,input,textarea,select") && (next?.dataset.pageBreak === "true" || next?.classList.contains(`${this.cssPrefix}break`) === true);
|
|
733
|
+
}
|
|
734
|
+
isDeliberatelyUnrenderedSource(element, sourceRoot) {
|
|
735
|
+
for (let current = element; current; current = current.parentElement) {
|
|
736
|
+
const zeroHeightExplicitBreakCarrier = current === element && this.isZeroHeightExplicitBreakCarrier(current);
|
|
737
|
+
if (current.dataset.pageMapExclude === "true" || current.dataset.pageBreak === "true" || current.classList.contains(`${this.cssPrefix}break`) || zeroHeightExplicitBreakCarrier || current.hidden || current.getAttribute("aria-hidden") === "true" || current.style.display === "none" || current.style.visibility === "hidden") {
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
if (current === sourceRoot) break;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Keep exactly one active bare-Unid editor anchor per source block. Presentation clones use
|
|
746
|
+
* canonical source identity plus page/fragment qualification instead.
|
|
747
|
+
*/
|
|
748
|
+
qualifyPageFragments(pages) {
|
|
749
|
+
const fragmentCounts = /* @__PURE__ */ new Map();
|
|
750
|
+
const activeCanonicalIds = /* @__PURE__ */ new Set();
|
|
751
|
+
const activeBareAnchorIds = /* @__PURE__ */ new Set();
|
|
752
|
+
const makeInactive = (element) => {
|
|
753
|
+
element.removeAttribute("data-anchor");
|
|
754
|
+
element.removeAttribute("data-committed-text");
|
|
755
|
+
if (element.hasAttribute("contenteditable")) element.setAttribute("contenteditable", "false");
|
|
756
|
+
};
|
|
757
|
+
for (const page of pages) {
|
|
758
|
+
const nodes = page.element.querySelectorAll("[data-source-anchor-id]");
|
|
759
|
+
for (const element of Array.from(nodes)) {
|
|
760
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
761
|
+
if (!anchorId) continue;
|
|
762
|
+
const fragmentIndex = fragmentCounts.get(anchorId) ?? 0;
|
|
763
|
+
fragmentCounts.set(anchorId, fragmentIndex + 1);
|
|
764
|
+
element.dataset.pageNumber = String(page.pageNumber);
|
|
765
|
+
element.dataset.fragmentIndex = String(fragmentIndex);
|
|
766
|
+
element.dataset.pageFragmentId = `p${page.pageNumber}-f${fragmentIndex}-${anchorId}`;
|
|
767
|
+
const story = this.storyForCanonicalAnchor(anchorId);
|
|
768
|
+
const mayOwnActiveEditorAnchor = story === "body" || story === "comment" || story === "footnote" || story === "endnote";
|
|
769
|
+
if (element.hasAttribute("data-anchor")) {
|
|
770
|
+
const bareAnchorId = element.dataset.anchor;
|
|
771
|
+
if (mayOwnActiveEditorAnchor && !activeCanonicalIds.has(anchorId) && !activeBareAnchorIds.has(bareAnchorId)) {
|
|
772
|
+
activeCanonicalIds.add(anchorId);
|
|
773
|
+
activeBareAnchorIds.add(bareAnchorId);
|
|
774
|
+
} else {
|
|
775
|
+
makeInactive(element);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const activeStagingCanonicalIds = /* @__PURE__ */ new Set();
|
|
781
|
+
for (const element of Array.from(
|
|
782
|
+
this.stagingElement.querySelectorAll("[data-source-anchor-id][data-anchor]")
|
|
783
|
+
)) {
|
|
784
|
+
const anchorId = element.dataset.sourceAnchorId;
|
|
785
|
+
if (!anchorId) continue;
|
|
786
|
+
const bareAnchorId = element.dataset.anchor;
|
|
787
|
+
if (activeCanonicalIds.has(anchorId) || activeStagingCanonicalIds.has(anchorId) || activeBareAnchorIds.has(bareAnchorId)) {
|
|
788
|
+
makeInactive(element);
|
|
789
|
+
} else {
|
|
790
|
+
activeStagingCanonicalIds.add(anchorId);
|
|
791
|
+
activeBareAnchorIds.add(bareAnchorId);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Page flow clones source blocks while the hidden staging tree stays in the document. Any HTML
|
|
797
|
+
* fragment target copied into a visible page would therefore resolve to its earlier hidden
|
|
798
|
+
* source. Transfer target ownership to the page presentation after flow is complete; registry
|
|
799
|
+
* and wrapper IDs that have no visible counterpart remain available to pagination internals.
|
|
800
|
+
*/
|
|
801
|
+
transferVisibleFragmentTargets() {
|
|
802
|
+
const visibleIds = new Set(Array.from(
|
|
803
|
+
this.containerElement.querySelectorAll("[id]")
|
|
804
|
+
).map((element) => element.id).filter(Boolean));
|
|
805
|
+
if (visibleIds.size === 0) return;
|
|
806
|
+
for (const source of Array.from(
|
|
807
|
+
this.stagingElement.querySelectorAll("[id]")
|
|
808
|
+
)) {
|
|
809
|
+
if (visibleIds.has(source.id)) source.removeAttribute("id");
|
|
319
810
|
}
|
|
320
|
-
this.stagingElement.style.display = "none";
|
|
321
|
-
this.substitutePageNumberFields(pages.length);
|
|
322
|
-
return { totalPages: pages.length, pages };
|
|
323
811
|
}
|
|
324
812
|
/** Read each section's `w:pgNumType` off its wrapper (see {@link SectionPageNumbering}). */
|
|
325
813
|
parsePageNumbering(sections) {
|
|
@@ -363,9 +851,8 @@ var DocxodusEditor = (() => {
|
|
|
363
851
|
if (markers.length === 0) continue;
|
|
364
852
|
const sectionIndex = parseInt(box.dataset.sectionIndex || "0", 10);
|
|
365
853
|
const pageNumber = parseInt(box.dataset.pageNumber || "1", 10);
|
|
366
|
-
const pageInSection = parseInt(box.dataset.pageInSection || "1", 10);
|
|
367
854
|
const numbering = this.pageNumbering.get(sectionIndex) ?? {};
|
|
368
|
-
const displayed =
|
|
855
|
+
const displayed = parseInt(box.dataset.displayedPageNumber || String(pageNumber), 10);
|
|
369
856
|
for (const marker of Array.from(markers)) {
|
|
370
857
|
const kind = marker.dataset.field;
|
|
371
858
|
if (kind !== "PAGE" && kind !== "NUMPAGES") continue;
|
|
@@ -377,17 +864,26 @@ var DocxodusEditor = (() => {
|
|
|
377
864
|
/**
|
|
378
865
|
* Measures all content blocks in a section.
|
|
379
866
|
*/
|
|
380
|
-
measureBlocks(section, dims) {
|
|
867
|
+
measureBlocks(section, dims, sectionIndex) {
|
|
381
868
|
const blocks = [];
|
|
382
869
|
const children = Array.from(section.children);
|
|
383
870
|
for (const child of children) {
|
|
871
|
+
this.checkpoint();
|
|
384
872
|
if (child.dataset.sectionIndex !== void 0) {
|
|
385
|
-
const
|
|
873
|
+
const nestedSectionIndex = parseInt(child.dataset.sectionIndex || String(sectionIndex), 10);
|
|
874
|
+
const nestedBlocks = this.measureBlocks(child, dims, nestedSectionIndex);
|
|
386
875
|
blocks.push(...nestedBlocks);
|
|
387
876
|
continue;
|
|
388
877
|
}
|
|
878
|
+
if (child.matches("section.endnotes")) {
|
|
879
|
+
const endnoteBlocks = this.measureSafeEndnoteBlocks(child, dims, sectionIndex);
|
|
880
|
+
if (endnoteBlocks) {
|
|
881
|
+
blocks.push(...endnoteBlocks);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
389
885
|
const rect = child.getBoundingClientRect();
|
|
390
|
-
const style =
|
|
886
|
+
const style = this.view.getComputedStyle(child);
|
|
391
887
|
const marginTopPx = parseFloat(style.marginTop) || 0;
|
|
392
888
|
const marginBottomPx = parseFloat(style.marginBottom) || 0;
|
|
393
889
|
const heightPt = pxToPt(rect.height);
|
|
@@ -396,14 +892,166 @@ var DocxodusEditor = (() => {
|
|
|
396
892
|
const isPageBreak = child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
|
|
397
893
|
blocks.push({
|
|
398
894
|
element: child,
|
|
895
|
+
sectionIndex,
|
|
399
896
|
heightPt,
|
|
400
897
|
marginTopPt,
|
|
401
898
|
marginBottomPt,
|
|
402
899
|
keepWithNext: child.dataset.keepWithNext === "true",
|
|
403
900
|
keepLines: child.dataset.keepLines === "true",
|
|
404
901
|
pageBreakBefore: child.dataset.pageBreakBefore === "true",
|
|
405
|
-
isPageBreak
|
|
902
|
+
isPageBreak,
|
|
903
|
+
isWordParagraph: this.isWordParagraphElement(child)
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
return blocks;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Flatten the converter's `section.endnotes > ol > li > p` presentation into
|
|
910
|
+
* ordinary paragraph blocks. This preserves paragraph formatting and canonical
|
|
911
|
+
* p:en/en:en identities while allowing the existing paragraph fragmenter to
|
|
912
|
+
* split a long endnote across page boundaries.
|
|
913
|
+
*/
|
|
914
|
+
measureSafeEndnoteBlocks(section, dims, sectionIndex) {
|
|
915
|
+
const sectionChildren = Array.from(section.children);
|
|
916
|
+
const list = sectionChildren.find((child) => child.tagName === "OL");
|
|
917
|
+
if (!list || sectionChildren.some((child) => child.tagName !== "HR" && child !== list) || Array.from(list.children).some((child) => child.tagName !== "LI")) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
const items = Array.from(list.children);
|
|
921
|
+
if (items.length === 0 || items.some(
|
|
922
|
+
(item) => item.children.length === 0 || Array.from(item.children).some((child) => child.tagName !== "P")
|
|
923
|
+
)) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
const blocks = [];
|
|
927
|
+
const sectionStyle = this.view.getComputedStyle(section);
|
|
928
|
+
for (const rule of sectionChildren.filter((child) => child.tagName === "HR")) {
|
|
929
|
+
this.checkpoint();
|
|
930
|
+
const clonedRule = rule.cloneNode(true);
|
|
931
|
+
if (blocks.length === 0) clonedRule.style.marginTop = sectionStyle.marginTop;
|
|
932
|
+
blocks.push(this.measureElement(clonedRule, dims, sectionIndex));
|
|
933
|
+
}
|
|
934
|
+
const listStyle = this.view.getComputedStyle(list);
|
|
935
|
+
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
936
|
+
this.checkpoint();
|
|
937
|
+
const item = items[itemIndex];
|
|
938
|
+
const ownerAnchorId = item.dataset.sourceAnchorId;
|
|
939
|
+
const paragraphs = Array.from(item.children);
|
|
940
|
+
for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
|
|
941
|
+
this.checkpoint();
|
|
942
|
+
const paragraph = paragraphs[paragraphIndex];
|
|
943
|
+
if (!this.hasRangeFragmentSafeLayout(paragraph)) {
|
|
944
|
+
return null;
|
|
945
|
+
}
|
|
946
|
+
const clone = paragraph.cloneNode(true);
|
|
947
|
+
clone.dataset.paginationSafeEndnote = "true";
|
|
948
|
+
clone.style.fontSize ||= sectionStyle.fontSize;
|
|
949
|
+
clone.style.lineHeight ||= sectionStyle.lineHeight;
|
|
950
|
+
clone.style.paddingLeft ||= listStyle.paddingLeft;
|
|
951
|
+
if (ownerAnchorId && !Array.from(
|
|
952
|
+
clone.querySelectorAll("[data-source-anchor-id]")
|
|
953
|
+
).some((node) => node.dataset.sourceAnchorId === ownerAnchorId)) {
|
|
954
|
+
const owner = this.document.createElement("span");
|
|
955
|
+
owner.dataset.sourceAnchorId = ownerAnchorId;
|
|
956
|
+
while (clone.firstChild) owner.appendChild(clone.firstChild);
|
|
957
|
+
clone.appendChild(owner);
|
|
958
|
+
}
|
|
959
|
+
if (paragraphIndex === 0) {
|
|
960
|
+
const itemId = item.id;
|
|
961
|
+
if (itemId) {
|
|
962
|
+
if (clone.id && clone.id !== itemId) return null;
|
|
963
|
+
clone.id = itemId;
|
|
964
|
+
}
|
|
965
|
+
const value = parseInt(item.getAttribute("value") || String(itemIndex + 1), 10);
|
|
966
|
+
const marker = this.formatOrderedListMarker(
|
|
967
|
+
Number.isFinite(value) ? value : itemIndex + 1,
|
|
968
|
+
listStyle.listStyleType
|
|
969
|
+
);
|
|
970
|
+
clone.insertBefore(this.document.createTextNode(`${marker}. `), clone.firstChild);
|
|
971
|
+
}
|
|
972
|
+
blocks.push(this.measureElement(clone, dims, sectionIndex));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return blocks;
|
|
976
|
+
}
|
|
977
|
+
/** Render the CSS ordered-list formats emitted by the converter after an endnote is flattened. */
|
|
978
|
+
formatOrderedListMarker(value, listStyleType) {
|
|
979
|
+
const format = (() => {
|
|
980
|
+
switch (listStyleType) {
|
|
981
|
+
case "lower-roman":
|
|
982
|
+
return "lowerRoman";
|
|
983
|
+
case "upper-roman":
|
|
984
|
+
return "upperRoman";
|
|
985
|
+
case "lower-alpha":
|
|
986
|
+
case "lower-latin":
|
|
987
|
+
return "lowerLetter";
|
|
988
|
+
case "upper-alpha":
|
|
989
|
+
case "upper-latin":
|
|
990
|
+
return "upperLetter";
|
|
991
|
+
default:
|
|
992
|
+
return "decimal";
|
|
993
|
+
}
|
|
994
|
+
})();
|
|
995
|
+
return formatPageNumber(value, format);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Flows a multi-column (`w:cols`) section's children into CSS-multicol container
|
|
999
|
+
* blocks. Word lays such a section out as N columns inside the same body extent;
|
|
1000
|
+
* a balanced `column-count` container reproduces that geometry, and the paginator
|
|
1001
|
+
* then places each container as one ordinary measured block. A container grows
|
|
1002
|
+
* greedily until its balanced height would exceed the smallest page body available
|
|
1003
|
+
* to the section, so a long columned section still splits across pages at block
|
|
1004
|
+
* boundaries. Each child lands in exactly one container, so anchors never
|
|
1005
|
+
* duplicate. An explicit page break child passes through as its own block, which
|
|
1006
|
+
* ends the current container and lets the normal flow logic turn the page.
|
|
1007
|
+
*/
|
|
1008
|
+
buildColumnBlocks(section, dims, sectionIndex, columnCount, columnGapPt) {
|
|
1009
|
+
const children = Array.from(section.children);
|
|
1010
|
+
const blocks = [];
|
|
1011
|
+
const maxFragmentHeight = this.smallestEffectiveContentHeight(dims, sectionIndex);
|
|
1012
|
+
const isBreak = (child) => child.dataset.pageBreak === "true" || child.classList.contains(`${this.cssPrefix}break`);
|
|
1013
|
+
const makeContainer = (slice) => {
|
|
1014
|
+
const container = this.document.createElement("div");
|
|
1015
|
+
container.style.columnCount = String(columnCount);
|
|
1016
|
+
container.style.columnGap = `${columnGapPt}pt`;
|
|
1017
|
+
for (const child of slice) {
|
|
1018
|
+
container.appendChild(child.cloneNode(true));
|
|
1019
|
+
}
|
|
1020
|
+
return container;
|
|
1021
|
+
};
|
|
1022
|
+
let start2 = 0;
|
|
1023
|
+
while (start2 < children.length) {
|
|
1024
|
+
this.checkpoint();
|
|
1025
|
+
if (isBreak(children[start2])) {
|
|
1026
|
+
blocks.push(this.measureElement(children[start2], dims, sectionIndex));
|
|
1027
|
+
start2++;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
let end = start2 + 1;
|
|
1031
|
+
let container = makeContainer(children.slice(start2, end));
|
|
1032
|
+
let measured = this.measureElement(container, dims, sectionIndex);
|
|
1033
|
+
while (end < children.length && !isBreak(children[end])) {
|
|
1034
|
+
this.checkpoint();
|
|
1035
|
+
const candidate = makeContainer(children.slice(start2, end + 1));
|
|
1036
|
+
const candidateMeasured = this.measureElement(candidate, dims, sectionIndex);
|
|
1037
|
+
if (candidateMeasured.heightPt > maxFragmentHeight) break;
|
|
1038
|
+
container = candidate;
|
|
1039
|
+
measured = candidateMeasured;
|
|
1040
|
+
end++;
|
|
1041
|
+
}
|
|
1042
|
+
blocks.push({
|
|
1043
|
+
element: container,
|
|
1044
|
+
sectionIndex,
|
|
1045
|
+
heightPt: measured.heightPt,
|
|
1046
|
+
marginTopPt: measured.marginTopPt,
|
|
1047
|
+
marginBottomPt: measured.marginBottomPt,
|
|
1048
|
+
keepWithNext: false,
|
|
1049
|
+
keepLines: false,
|
|
1050
|
+
pageBreakBefore: false,
|
|
1051
|
+
isPageBreak: false,
|
|
1052
|
+
isWordParagraph: false
|
|
406
1053
|
});
|
|
1054
|
+
start2 = end;
|
|
407
1055
|
}
|
|
408
1056
|
return blocks;
|
|
409
1057
|
}
|
|
@@ -412,8 +1060,8 @@ var DocxodusEditor = (() => {
|
|
|
412
1060
|
* This is intentionally DOM-based: table row heights cannot be inferred from individual
|
|
413
1061
|
* rows because wrapping and collapsed borders change the height of a fragment.
|
|
414
1062
|
*/
|
|
415
|
-
measureElement(element, dims) {
|
|
416
|
-
const measurementHost = document.createElement("div");
|
|
1063
|
+
measureElement(element, dims, sectionIndex) {
|
|
1064
|
+
const measurementHost = this.document.createElement("div");
|
|
417
1065
|
measurementHost.style.position = "absolute";
|
|
418
1066
|
measurementHost.style.visibility = "hidden";
|
|
419
1067
|
measurementHost.style.left = "-9999px";
|
|
@@ -422,16 +1070,18 @@ var DocxodusEditor = (() => {
|
|
|
422
1070
|
measurementHost.appendChild(measuredElement);
|
|
423
1071
|
this.stagingElement.appendChild(measurementHost);
|
|
424
1072
|
const rect = measuredElement.getBoundingClientRect();
|
|
425
|
-
const style =
|
|
1073
|
+
const style = this.view.getComputedStyle(measuredElement);
|
|
426
1074
|
const measured = {
|
|
427
1075
|
element,
|
|
1076
|
+
sectionIndex,
|
|
428
1077
|
heightPt: pxToPt(rect.height),
|
|
429
1078
|
marginTopPt: pxToPt(parseFloat(style.marginTop) || 0),
|
|
430
1079
|
marginBottomPt: pxToPt(parseFloat(style.marginBottom) || 0),
|
|
431
1080
|
keepWithNext: element.dataset.keepWithNext === "true",
|
|
432
1081
|
keepLines: element.dataset.keepLines === "true",
|
|
433
1082
|
pageBreakBefore: element.dataset.pageBreakBefore === "true",
|
|
434
|
-
isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`)
|
|
1083
|
+
isPageBreak: element.dataset.pageBreak === "true" || element.classList.contains(`${this.cssPrefix}break`),
|
|
1084
|
+
isWordParagraph: this.isWordParagraphElement(element)
|
|
435
1085
|
};
|
|
436
1086
|
this.stagingElement.removeChild(measurementHost);
|
|
437
1087
|
return measured;
|
|
@@ -463,10 +1113,15 @@ var DocxodusEditor = (() => {
|
|
|
463
1113
|
* collapsed-margin rules as normal block placement. The trailing margin is
|
|
464
1114
|
* intentionally excluded, matching the individual block fit check.
|
|
465
1115
|
*/
|
|
466
|
-
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage) {
|
|
1116
|
+
measureKeepWithNextChainBodyHeight(chain, previousMarginBottomPt, isFirstOnPage, pageInSection) {
|
|
467
1117
|
const firstBlock = chain[0];
|
|
468
1118
|
if (!firstBlock) return 0;
|
|
469
|
-
const firstMarginTop =
|
|
1119
|
+
const firstMarginTop = this.effectiveBlockMarginTop(
|
|
1120
|
+
firstBlock,
|
|
1121
|
+
previousMarginBottomPt,
|
|
1122
|
+
isFirstOnPage,
|
|
1123
|
+
pageInSection
|
|
1124
|
+
);
|
|
470
1125
|
let bodyHeight = firstMarginTop + firstBlock.heightPt;
|
|
471
1126
|
for (let index = 1; index < chain.length; index++) {
|
|
472
1127
|
const previousBlock = chain[index - 1];
|
|
@@ -475,6 +1130,35 @@ var DocxodusEditor = (() => {
|
|
|
475
1130
|
}
|
|
476
1131
|
return bodyHeight;
|
|
477
1132
|
}
|
|
1133
|
+
/** Word paragraphs render as `p`, or as `h1`–`h6` when their style has an outline level. */
|
|
1134
|
+
isWordParagraphElement(element) {
|
|
1135
|
+
return element.tagName === "P" || /^H[1-6]$/.test(element.tagName);
|
|
1136
|
+
}
|
|
1137
|
+
shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection) {
|
|
1138
|
+
return isFirstOnPage && pageInSection > 1 && block.isWordParagraph === true;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Resolves the part of a block's top margin that consumes the current page.
|
|
1142
|
+
*
|
|
1143
|
+
* In Word's native DOCX layout, paragraph space-before is suppressed when a paragraph is the
|
|
1144
|
+
* first body block on a later page of the SAME section. The first page of a document/section is
|
|
1145
|
+
* the exception and keeps its spacing. Tables and other block margins are not paragraph spacing,
|
|
1146
|
+
* so they continue to use the ordinary CSS collapsing rule.
|
|
1147
|
+
*/
|
|
1148
|
+
effectiveBlockMarginTop(block, previousMarginBottomPt, isFirstOnPage, pageInSection) {
|
|
1149
|
+
if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
|
|
1150
|
+
return 0;
|
|
1151
|
+
}
|
|
1152
|
+
return isFirstOnPage ? block.marginTopPt : Math.max(block.marginTopPt, previousMarginBottomPt) - previousMarginBottomPt;
|
|
1153
|
+
}
|
|
1154
|
+
/** Clone a source block with the same page-top spacing decision used by the height budget. */
|
|
1155
|
+
cloneBlockForPage(block, isFirstOnPage, pageInSection) {
|
|
1156
|
+
const clone = block.element.cloneNode(true);
|
|
1157
|
+
if (this.shouldSuppressPageTopSpacing(block, isFirstOnPage, pageInSection)) {
|
|
1158
|
+
clone.style.setProperty("margin-top", "0", "important");
|
|
1159
|
+
}
|
|
1160
|
+
return clone;
|
|
1161
|
+
}
|
|
478
1162
|
/**
|
|
479
1163
|
* Finds footnote references introduced by a sequence of blocks, preserving
|
|
480
1164
|
* document order and excluding references already assigned to the page.
|
|
@@ -483,6 +1167,7 @@ var DocxodusEditor = (() => {
|
|
|
483
1167
|
const knownIds = new Set(existingFootnoteIds);
|
|
484
1168
|
const newIds = [];
|
|
485
1169
|
for (const block of blocks) {
|
|
1170
|
+
this.checkpoint();
|
|
486
1171
|
for (const id of this.extractFootnoteRefs(block.element)) {
|
|
487
1172
|
if (!knownIds.has(id)) {
|
|
488
1173
|
knownIds.add(id);
|
|
@@ -500,8 +1185,8 @@ var DocxodusEditor = (() => {
|
|
|
500
1185
|
smallestEffectiveContentHeight(dims, sectionIndex) {
|
|
501
1186
|
return Math.min(
|
|
502
1187
|
this.getPageBands(dims, sectionIndex, 1, 1).bodyHeight,
|
|
503
|
-
this.getPageBands(dims, sectionIndex, 2,
|
|
504
|
-
this.getPageBands(dims, sectionIndex,
|
|
1188
|
+
this.getPageBands(dims, sectionIndex, 2, 2).bodyHeight,
|
|
1189
|
+
this.getPageBands(dims, sectionIndex, 3, 3).bodyHeight
|
|
505
1190
|
);
|
|
506
1191
|
}
|
|
507
1192
|
/**
|
|
@@ -539,10 +1224,11 @@ var DocxodusEditor = (() => {
|
|
|
539
1224
|
if (wrapper.tagName !== "DIV" || wrapper.children.length !== 1 || block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak) {
|
|
540
1225
|
return null;
|
|
541
1226
|
}
|
|
542
|
-
const
|
|
543
|
-
if (!
|
|
1227
|
+
const tableElement = wrapper.firstElementChild;
|
|
1228
|
+
if (!tableElement || tableElement.localName !== "table") {
|
|
544
1229
|
return null;
|
|
545
1230
|
}
|
|
1231
|
+
const table = tableElement;
|
|
546
1232
|
const body = table.tBodies.length === 1 ? table.tBodies[0] : null;
|
|
547
1233
|
if (!body || table.tHead || table.tFoot || body.rows.length < 2 || Array.from(table.children).some((child) => child !== body && child.tagName !== "COLGROUP") || table.querySelector("table, [rowspan], [colspan], [data-footnote-id]") || wrapper.querySelector("[data-footnote-id]")) {
|
|
548
1234
|
return null;
|
|
@@ -556,8 +1242,10 @@ var DocxodusEditor = (() => {
|
|
|
556
1242
|
const groups = [];
|
|
557
1243
|
let start2 = 0;
|
|
558
1244
|
while (start2 < rows.length) {
|
|
1245
|
+
this.checkpoint();
|
|
559
1246
|
let end = start2;
|
|
560
1247
|
while (end < rows.length) {
|
|
1248
|
+
this.checkpoint();
|
|
561
1249
|
const candidate = this.createSimpleTableFragment(
|
|
562
1250
|
wrapper,
|
|
563
1251
|
table,
|
|
@@ -565,7 +1253,7 @@ var DocxodusEditor = (() => {
|
|
|
565
1253
|
rows.slice(start2, end + 1),
|
|
566
1254
|
start2 === 0
|
|
567
1255
|
);
|
|
568
|
-
const measured = this.measureElement(candidate, dims);
|
|
1256
|
+
const measured = this.measureElement(candidate, dims, block.sectionIndex);
|
|
569
1257
|
if (measured.heightPt > maximumFragmentHeight) {
|
|
570
1258
|
break;
|
|
571
1259
|
}
|
|
@@ -597,7 +1285,7 @@ var DocxodusEditor = (() => {
|
|
|
597
1285
|
if (!isLast) {
|
|
598
1286
|
fragment.style.setProperty("margin-bottom", "0", "important");
|
|
599
1287
|
}
|
|
600
|
-
const measured = this.measureElement(fragment, dims);
|
|
1288
|
+
const measured = this.measureElement(fragment, dims, block.sectionIndex);
|
|
601
1289
|
if (measured.heightPt + measured.marginTopPt + measured.marginBottomPt > minimumContentHeight) {
|
|
602
1290
|
return null;
|
|
603
1291
|
}
|
|
@@ -612,35 +1300,198 @@ var DocxodusEditor = (() => {
|
|
|
612
1300
|
return fragments;
|
|
613
1301
|
}
|
|
614
1302
|
/**
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
1303
|
+
* DOM endpoints that can finish a paragraph fragment. The flattened UTF-16
|
|
1304
|
+
* offsets are checked against the browser's Unicode grapheme segmenter, so a
|
|
1305
|
+
* formatting-run boundary can never bisect a surrogate pair, combining
|
|
1306
|
+
* sequence, or joined emoji. NBSP/word-joiner boundaries remain indivisible.
|
|
1307
|
+
* PAGE/NUMPAGES field results are atomic because splitting their marker would
|
|
1308
|
+
* make later substitution duplicate or replace only half of the field.
|
|
618
1309
|
*/
|
|
619
|
-
paragraphFragmentEndpoints(paragraph) {
|
|
620
|
-
const
|
|
621
|
-
const
|
|
1310
|
+
paragraphFragmentEndpoints(paragraph, emergencyGraphemeBreaks = false) {
|
|
1311
|
+
const textNodes = [];
|
|
1312
|
+
const textStarts = /* @__PURE__ */ new Map();
|
|
1313
|
+
const lastTextByField = /* @__PURE__ */ new Map();
|
|
1314
|
+
const walker = this.document.createTreeWalker(paragraph, this.view.NodeFilter.SHOW_TEXT);
|
|
1315
|
+
const flattenedChunks = [];
|
|
1316
|
+
let flattenedLength = 0;
|
|
622
1317
|
let textNode;
|
|
623
1318
|
while (textNode = walker.nextNode()) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1319
|
+
this.checkpoint();
|
|
1320
|
+
textStarts.set(textNode, flattenedLength);
|
|
1321
|
+
textNodes.push(textNode);
|
|
1322
|
+
flattenedChunks.push(textNode.data);
|
|
1323
|
+
flattenedLength += textNode.data.length;
|
|
1324
|
+
let field = textNode.parentElement?.closest("[data-field]") ?? null;
|
|
1325
|
+
while (field?.parentElement?.closest("[data-field]")) {
|
|
1326
|
+
field = field.parentElement.closest("[data-field]");
|
|
1327
|
+
}
|
|
1328
|
+
if (field && paragraph.contains(field)) lastTextByField.set(field, textNode);
|
|
1329
|
+
}
|
|
1330
|
+
const flattenedText = flattenedChunks.join("");
|
|
1331
|
+
if (!this.hasVisibleText(flattenedText)) return [];
|
|
1332
|
+
const invisible = /[\s\u200B-\u200F\uFEFF]/;
|
|
1333
|
+
let firstVisibleOffset = -1;
|
|
1334
|
+
let lastVisibleOffset = -1;
|
|
1335
|
+
for (let index = 0; index < flattenedText.length; index++) {
|
|
1336
|
+
if (index % 4096 === 0) this.checkpoint();
|
|
1337
|
+
if (invisible.test(flattenedText[index])) continue;
|
|
1338
|
+
if (firstVisibleOffset < 0) firstVisibleOffset = index;
|
|
1339
|
+
lastVisibleOffset = index;
|
|
1340
|
+
}
|
|
1341
|
+
const graphemeBoundaries = this.graphemeBoundaryOffsets(flattenedText);
|
|
1342
|
+
const candidates = [];
|
|
1343
|
+
const addCandidate = (candidate) => {
|
|
1344
|
+
const { textOffset } = candidate;
|
|
1345
|
+
if (textOffset <= 0 || textOffset >= flattenedText.length) return;
|
|
1346
|
+
if (textOffset <= firstVisibleOffset || textOffset > lastVisibleOffset) return;
|
|
1347
|
+
if (this.isNonBreakingTextBoundary(flattenedText, textOffset)) return;
|
|
1348
|
+
if (!this.isLegalParagraphLineBoundary(paragraph, flattenedText, textOffset)) return;
|
|
1349
|
+
if (graphemeBoundaries) {
|
|
1350
|
+
if (!graphemeBoundaries.has(textOffset)) return;
|
|
1351
|
+
} else if (!this.isConservativeFallbackTextBoundary(flattenedText, textOffset)) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
candidates.push(candidate);
|
|
1355
|
+
};
|
|
1356
|
+
for (const node of textNodes) {
|
|
1357
|
+
this.checkpoint();
|
|
1358
|
+
const start2 = textStarts.get(node);
|
|
1359
|
+
const atomic = node.parentElement?.closest(
|
|
1360
|
+
"[data-field], [data-list-marker], a[data-comment-id]"
|
|
1361
|
+
);
|
|
1362
|
+
if (atomic && paragraph.contains(atomic)) continue;
|
|
1363
|
+
const whitespace = /[\u0009-\u000D\u0020]+/g;
|
|
627
1364
|
let match;
|
|
628
|
-
|
|
629
|
-
|
|
1365
|
+
let matchesSinceCheckpoint = 0;
|
|
1366
|
+
while ((match = whitespace.exec(node.data)) !== null) {
|
|
1367
|
+
if (++matchesSinceCheckpoint % 256 === 0) this.checkpoint();
|
|
1368
|
+
const offset = match.index + match[0].length;
|
|
1369
|
+
addCandidate({ node, offset, textOffset: start2 + offset, priority: 0 });
|
|
630
1370
|
}
|
|
631
|
-
|
|
632
|
-
|
|
1371
|
+
addCandidate({
|
|
1372
|
+
node,
|
|
1373
|
+
offset: node.data.length,
|
|
1374
|
+
textOffset: start2 + node.data.length,
|
|
1375
|
+
priority: 0
|
|
1376
|
+
});
|
|
1377
|
+
if (emergencyGraphemeBreaks) {
|
|
1378
|
+
for (let offset = 1; offset < node.data.length; offset++) {
|
|
1379
|
+
if (offset % 256 === 0) this.checkpoint();
|
|
1380
|
+
addCandidate({
|
|
1381
|
+
node,
|
|
1382
|
+
offset,
|
|
1383
|
+
textOffset: start2 + offset,
|
|
1384
|
+
priority: -1
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
for (const field of Array.from(paragraph.querySelectorAll("[data-field]"))) {
|
|
1390
|
+
this.checkpoint();
|
|
1391
|
+
if (field.parentElement?.closest("[data-field]")) continue;
|
|
1392
|
+
const last = lastTextByField.get(field);
|
|
1393
|
+
const parent = field.parentNode;
|
|
1394
|
+
if (!last || !parent) continue;
|
|
1395
|
+
const childIndex = Array.prototype.indexOf.call(parent.childNodes, field);
|
|
1396
|
+
if (childIndex < 0) continue;
|
|
1397
|
+
addCandidate({
|
|
1398
|
+
node: parent,
|
|
1399
|
+
offset: childIndex + 1,
|
|
1400
|
+
textOffset: textStarts.get(last) + last.data.length,
|
|
1401
|
+
priority: 1
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
candidates.sort((left2, right2) => left2.textOffset - right2.textOffset || right2.priority - left2.priority);
|
|
1405
|
+
const endpoints = [];
|
|
1406
|
+
for (const candidate of candidates) {
|
|
1407
|
+
if (endpoints[endpoints.length - 1]?.textOffset !== candidate.textOffset) {
|
|
1408
|
+
endpoints.push(candidate);
|
|
633
1409
|
}
|
|
634
1410
|
}
|
|
635
1411
|
return endpoints;
|
|
636
1412
|
}
|
|
1413
|
+
/** Browser-native UAX #29 boundaries; null keeps older runtimes conservative. */
|
|
1414
|
+
graphemeBoundaryOffsets(text) {
|
|
1415
|
+
const SegmenterConstructor = this.view.Intl.Segmenter;
|
|
1416
|
+
if (!SegmenterConstructor) return null;
|
|
1417
|
+
const boundaries = /* @__PURE__ */ new Set([0, text.length]);
|
|
1418
|
+
let segmentCount = 0;
|
|
1419
|
+
for (const part of new SegmenterConstructor("en", { granularity: "grapheme" }).segment(text)) {
|
|
1420
|
+
if (++segmentCount % 256 === 0) this.checkpoint();
|
|
1421
|
+
boundaries.add(part.index);
|
|
1422
|
+
boundaries.add(part.index + part.segment.length);
|
|
1423
|
+
}
|
|
1424
|
+
return boundaries;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Conservative UAX #14/CSS wrapping opportunities used for synthetic page
|
|
1428
|
+
* boundaries. In particular, a formatting-run boundary is not itself a word
|
|
1429
|
+
* boundary, and Japanese opening/closing punctuation stays with its pair.
|
|
1430
|
+
* Arbitrary grapheme breaks are admitted only when the paragraph's CSS asks
|
|
1431
|
+
* the browser to wrap anywhere.
|
|
1432
|
+
*/
|
|
1433
|
+
isLegalParagraphLineBoundary(paragraph, text, offset) {
|
|
1434
|
+
const lastCodeUnit = text.charCodeAt(offset - 1);
|
|
1435
|
+
const beforeOffset = lastCodeUnit >= 56320 && lastCodeUnit <= 57343 ? Math.max(0, offset - 2) : offset - 1;
|
|
1436
|
+
const beforeCodePoint = text.codePointAt(beforeOffset);
|
|
1437
|
+
const afterCodePoint = text.codePointAt(offset);
|
|
1438
|
+
const before = beforeCodePoint === void 0 ? "" : String.fromCodePoint(beforeCodePoint);
|
|
1439
|
+
const after = afterCodePoint === void 0 ? "" : String.fromCodePoint(afterCodePoint);
|
|
1440
|
+
if (/^[\u0009-\u000D\u0020]$/.test(before) || /^[\u0009-\u000D\u0020]$/.test(after)) return true;
|
|
1441
|
+
if (/[-\u00AD\u2010]$/u.test(before)) return true;
|
|
1442
|
+
const style = this.view.getComputedStyle(paragraph);
|
|
1443
|
+
if (style.wordBreak === "break-all" || style.overflowWrap === "anywhere" || style.overflowWrap === "break-word") return true;
|
|
1444
|
+
const eastAsian = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
1445
|
+
if (!eastAsian.test(before) && !eastAsian.test(after)) return false;
|
|
1446
|
+
const opening = "([{<\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\u3016\u3018\u301A\uFF08\uFF3B\uFF5B";
|
|
1447
|
+
const closing = ")]}>\u2019\u201D\u3001\u3002\u3009\u300B\u300D\u300F\u3011\u3015\u3017\u3019\u301B\uFF01\uFF05\uFF09\uFF0C\uFF0E\uFF1A\uFF1B\uFF1F\uFF3D\uFF5D";
|
|
1448
|
+
return !opening.includes(before) && !closing.includes(after);
|
|
1449
|
+
}
|
|
1450
|
+
/** Never fragment across characters whose line-break meaning is explicitly non-breaking. */
|
|
1451
|
+
isNonBreakingTextBoundary(text, offset) {
|
|
1452
|
+
const codePointBefore = (index) => {
|
|
1453
|
+
if (index <= 0) return void 0;
|
|
1454
|
+
const last = text.charCodeAt(index - 1);
|
|
1455
|
+
const start2 = last >= 56320 && last <= 57343 ? index - 2 : index - 1;
|
|
1456
|
+
return text.codePointAt(Math.max(0, start2));
|
|
1457
|
+
};
|
|
1458
|
+
const nonBreaking = /* @__PURE__ */ new Set([
|
|
1459
|
+
160,
|
|
1460
|
+
8206,
|
|
1461
|
+
8207,
|
|
1462
|
+
8209,
|
|
1463
|
+
8234,
|
|
1464
|
+
8235,
|
|
1465
|
+
8236,
|
|
1466
|
+
8237,
|
|
1467
|
+
8238,
|
|
1468
|
+
8239,
|
|
1469
|
+
8288,
|
|
1470
|
+
8294,
|
|
1471
|
+
8295,
|
|
1472
|
+
8296,
|
|
1473
|
+
8297,
|
|
1474
|
+
65279
|
|
1475
|
+
]);
|
|
1476
|
+
return nonBreaking.has(codePointBefore(offset) ?? -1) || nonBreaking.has(text.codePointAt(offset) ?? -1);
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Without Intl.Segmenter, admit only an all-ASCII boundary. This still
|
|
1480
|
+
* fragments ordinary prose while refusing to guess about Unicode clusters.
|
|
1481
|
+
*/
|
|
1482
|
+
isConservativeFallbackTextBoundary(text, offset) {
|
|
1483
|
+
return text.charCodeAt(offset - 1) <= 127 && text.charCodeAt(offset) <= 127;
|
|
1484
|
+
}
|
|
637
1485
|
/**
|
|
638
1486
|
* Whether a range contains visible text after ignoring bidi/zero-width marks.
|
|
639
1487
|
* Paragraph fragmentation deliberately excludes non-textual descendants, so
|
|
640
1488
|
* this is enough to reject empty head or tail fragments.
|
|
641
1489
|
*/
|
|
642
1490
|
hasVisibleFragmentText(fragment) {
|
|
643
|
-
return (fragment.textContent || "")
|
|
1491
|
+
return this.hasVisibleText(fragment.textContent || "");
|
|
1492
|
+
}
|
|
1493
|
+
hasVisibleText(text) {
|
|
1494
|
+
return text.replace(/[\u200B-\u200F\uFEFF]/g, "").trim().length > 0;
|
|
644
1495
|
}
|
|
645
1496
|
/**
|
|
646
1497
|
* Phase one only handles text-like paragraph descendants. Objects, explicit
|
|
@@ -649,13 +1500,25 @@ var DocxodusEditor = (() => {
|
|
|
649
1500
|
* fallback instead of risking broken content or duplicate anchors.
|
|
650
1501
|
*/
|
|
651
1502
|
canFragmentParagraph(block) {
|
|
652
|
-
if (!this.fragmentParagraphs
|
|
1503
|
+
if (!this.fragmentParagraphs) {
|
|
653
1504
|
return false;
|
|
654
1505
|
}
|
|
655
1506
|
const paragraph = block.element;
|
|
656
1507
|
if (block.keepWithNext || block.keepLines || block.pageBreakBefore || block.isPageBreak || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
657
1508
|
return false;
|
|
658
1509
|
}
|
|
1510
|
+
return this.canRangeFragmentParagraph(paragraph);
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Shared structural gate for body and note paragraph fragmentation. Footnote
|
|
1514
|
+
* first paragraphs are inline beside their marker, so that one known layout
|
|
1515
|
+
* context may opt into an inline root while retaining every descendant and
|
|
1516
|
+
* break-safety restriction used for body text.
|
|
1517
|
+
*/
|
|
1518
|
+
canRangeFragmentParagraph(paragraph, allowInlineRoot = false) {
|
|
1519
|
+
if (paragraph.tagName !== "P" || paragraph.dataset.keepWithNext === "true" || paragraph.dataset.keepLines === "true" || paragraph.dataset.pageBreakBefore === "true" || paragraph.dataset.widowControl === "true" || paragraph.hasAttribute("contenteditable")) {
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
659
1522
|
const unsupportedDescendants = [
|
|
660
1523
|
"br",
|
|
661
1524
|
"img",
|
|
@@ -686,21 +1549,27 @@ var DocxodusEditor = (() => {
|
|
|
686
1549
|
"fieldset",
|
|
687
1550
|
"details",
|
|
688
1551
|
"[data-footnote-id]",
|
|
689
|
-
"[data-list-marker]",
|
|
690
|
-
"[data-anchor]",
|
|
691
|
-
"[id]",
|
|
692
1552
|
"[contenteditable]"
|
|
693
1553
|
].join(", ");
|
|
694
1554
|
if (paragraph.querySelector(unsupportedDescendants)) {
|
|
695
1555
|
return false;
|
|
696
1556
|
}
|
|
697
|
-
const
|
|
698
|
-
|
|
1557
|
+
const isValidatedEndnote = paragraph.dataset.paginationSafeEndnote === "true";
|
|
1558
|
+
return isValidatedEndnote || this.hasRangeFragmentSafeLayout(paragraph, allowInlineRoot);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* A range clone preserves nested inline formatting exactly. Anything that establishes its own
|
|
1562
|
+
* box/layout context is deferred until a future fragmenter can model it accurately. Callers
|
|
1563
|
+
* must invoke this while the paragraph is attached to the styled document.
|
|
1564
|
+
*/
|
|
1565
|
+
hasRangeFragmentSafeLayout(paragraph, allowInlineRoot = false) {
|
|
1566
|
+
const paragraphStyle = this.view.getComputedStyle(paragraph);
|
|
1567
|
+
if (paragraphStyle.display !== "block" && !(allowInlineRoot && paragraphStyle.display === "inline") || paragraphStyle.position !== "static" || paragraphStyle.float !== "none" || paragraphStyle.whiteSpace !== "normal" && paragraphStyle.whiteSpace !== "pre-wrap" || paragraphStyle.breakBefore !== "auto" || paragraphStyle.breakAfter !== "auto" || paragraphStyle.breakInside === "avoid" || paragraphStyle.pageBreakBefore !== "auto" || paragraphStyle.pageBreakAfter !== "auto" || paragraphStyle.pageBreakInside === "avoid") {
|
|
699
1568
|
return false;
|
|
700
1569
|
}
|
|
701
1570
|
for (const descendant of Array.from(paragraph.querySelectorAll("*"))) {
|
|
702
|
-
const style =
|
|
703
|
-
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal") {
|
|
1571
|
+
const style = this.view.getComputedStyle(descendant);
|
|
1572
|
+
if (style.display !== "inline" || style.position !== "static" || style.float !== "none" || style.whiteSpace !== "normal" && style.whiteSpace !== "pre-wrap") {
|
|
704
1573
|
return false;
|
|
705
1574
|
}
|
|
706
1575
|
}
|
|
@@ -727,58 +1596,122 @@ var DocxodusEditor = (() => {
|
|
|
727
1596
|
return fragment;
|
|
728
1597
|
}
|
|
729
1598
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
1599
|
+
* Range clones repeat an inline ancestor when the split lands inside it. Keep
|
|
1600
|
+
* semantic wrappers (links, comments, formatting) on both sides, but retain a
|
|
1601
|
+
* duplicated HTML/editor identity only on the leading fragment. Targets that
|
|
1602
|
+
* occur wholly after the split are absent from `head` and remain on `tail`.
|
|
733
1603
|
*/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
1604
|
+
reconcileParagraphFragmentIdentities(head, tail) {
|
|
1605
|
+
const elements = (root, selector2) => [
|
|
1606
|
+
...root.matches(selector2) ? [root] : [],
|
|
1607
|
+
...Array.from(root.querySelectorAll(selector2))
|
|
1608
|
+
];
|
|
1609
|
+
const headIds = new Set(elements(head, "[id]").map((element) => element.id));
|
|
1610
|
+
for (const element of elements(tail, "[id]")) {
|
|
1611
|
+
if (headIds.has(element.id)) element.removeAttribute("id");
|
|
737
1612
|
}
|
|
738
|
-
const
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
1613
|
+
const headAnchors = new Set(elements(head, "[data-anchor]").map((element) => element.dataset.anchor).filter((anchor) => Boolean(anchor)));
|
|
1614
|
+
for (const element of elements(tail, "[data-anchor]")) {
|
|
1615
|
+
if (!element.dataset.anchor || !headAnchors.has(element.dataset.anchor)) continue;
|
|
1616
|
+
element.removeAttribute("data-anchor");
|
|
1617
|
+
element.removeAttribute("data-committed-text");
|
|
742
1618
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Range-clone the largest safe prefix accepted by `fits`. The measurement
|
|
1622
|
+
* policy stays with the caller, allowing body blocks and note bands to share
|
|
1623
|
+
* one DOM fragmenter while measuring in their respective layout contexts.
|
|
1624
|
+
*/
|
|
1625
|
+
splitParagraphAtLargestFit(paragraph, fits, options = {}) {
|
|
1626
|
+
const largestFit = (endpoints2) => {
|
|
1627
|
+
const lastEndpoint = endpoints2[endpoints2.length - 1];
|
|
1628
|
+
if (lastEndpoint) {
|
|
1629
|
+
const lastRange = this.document.createRange();
|
|
1630
|
+
lastRange.setStart(paragraph, 0);
|
|
1631
|
+
lastRange.setEnd(lastEndpoint.node, lastEndpoint.offset);
|
|
1632
|
+
const lastContents = lastRange.cloneContents();
|
|
1633
|
+
if (this.hasVisibleFragmentText(lastContents)) {
|
|
1634
|
+
const lastHead = this.createParagraphFragment(paragraph, lastRange, true, false);
|
|
1635
|
+
if (fits(lastHead)) return { endpoint: lastEndpoint, head: lastHead };
|
|
1636
|
+
}
|
|
756
1637
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1638
|
+
let low = 0;
|
|
1639
|
+
let high = endpoints2.length - 2;
|
|
1640
|
+
let best2 = null;
|
|
1641
|
+
while (low <= high) {
|
|
1642
|
+
this.checkpoint();
|
|
1643
|
+
const middle = Math.floor((low + high) / 2);
|
|
1644
|
+
const endpoint = endpoints2[middle];
|
|
1645
|
+
const headRange = this.document.createRange();
|
|
1646
|
+
headRange.setStart(paragraph, 0);
|
|
1647
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
1648
|
+
const headContents = headRange.cloneContents();
|
|
1649
|
+
if (!this.hasVisibleFragmentText(headContents)) {
|
|
1650
|
+
low = middle + 1;
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
const head = this.createParagraphFragment(paragraph, headRange, true, false);
|
|
1654
|
+
if (fits(head)) {
|
|
1655
|
+
best2 = { endpoint, head };
|
|
1656
|
+
low = middle + 1;
|
|
1657
|
+
} else {
|
|
1658
|
+
high = middle - 1;
|
|
1659
|
+
}
|
|
764
1660
|
}
|
|
1661
|
+
return best2;
|
|
1662
|
+
};
|
|
1663
|
+
let endpoints = this.paragraphFragmentEndpoints(paragraph);
|
|
1664
|
+
let best = largestFit(endpoints);
|
|
1665
|
+
if (!best && options.emergencyGraphemeBreaks) {
|
|
1666
|
+
endpoints = this.paragraphFragmentEndpoints(paragraph, true);
|
|
1667
|
+
best = largestFit(endpoints);
|
|
1668
|
+
}
|
|
1669
|
+
if (!best && options.forceFirstOnNoFit && endpoints.length > 0) {
|
|
1670
|
+
const endpoint = endpoints[0];
|
|
1671
|
+
const headRange = this.document.createRange();
|
|
1672
|
+
headRange.setStart(paragraph, 0);
|
|
1673
|
+
headRange.setEnd(endpoint.node, endpoint.offset);
|
|
1674
|
+
best = {
|
|
1675
|
+
endpoint,
|
|
1676
|
+
head: this.createParagraphFragment(paragraph, headRange, true, false)
|
|
1677
|
+
};
|
|
765
1678
|
}
|
|
766
1679
|
if (!best) {
|
|
767
|
-
return
|
|
1680
|
+
return endpoints.length > 0 ? { kind: "no-fit" } : { kind: "indivisible" };
|
|
768
1681
|
}
|
|
769
|
-
const tailRange = document.createRange();
|
|
1682
|
+
const tailRange = this.document.createRange();
|
|
770
1683
|
tailRange.setStart(best.endpoint.node, best.endpoint.offset);
|
|
771
1684
|
tailRange.setEnd(paragraph, paragraph.childNodes.length);
|
|
772
1685
|
const tailContents = tailRange.cloneContents();
|
|
773
|
-
if (!this.hasVisibleFragmentText(tailContents)) {
|
|
1686
|
+
if (!this.hasVisibleFragmentText(tailContents)) return { kind: "indivisible" };
|
|
1687
|
+
const tail = this.createParagraphFragment(paragraph, tailRange, false, true);
|
|
1688
|
+
this.reconcileParagraphFragmentIdentities(best.head, tail);
|
|
1689
|
+
return {
|
|
1690
|
+
kind: "split",
|
|
1691
|
+
head: best.head,
|
|
1692
|
+
tail
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Splits a simple paragraph at the largest DOM Range endpoint that fits the
|
|
1697
|
+
* currently available body space. The caller then processes the tail normally,
|
|
1698
|
+
* allowing it to fragment again on later pages when necessary.
|
|
1699
|
+
*/
|
|
1700
|
+
tryFragmentParagraph(block, dims, availableHeightPt, effectiveMarginTopPt) {
|
|
1701
|
+
if (!this.canFragmentParagraph(block) || availableHeightPt <= effectiveMarginTopPt) {
|
|
774
1702
|
return null;
|
|
775
1703
|
}
|
|
776
|
-
const
|
|
777
|
-
|
|
1704
|
+
const split = this.splitParagraphAtLargestFit(block.element, (head) => {
|
|
1705
|
+
const measured = this.measureElement(head, dims, block.sectionIndex);
|
|
1706
|
+
return effectiveMarginTopPt + measured.heightPt <= availableHeightPt;
|
|
1707
|
+
});
|
|
1708
|
+
if (split.kind !== "split") return null;
|
|
1709
|
+
const headMeasured = this.measureElement(split.head, dims, block.sectionIndex);
|
|
1710
|
+
const tailMeasured = this.measureElement(split.tail, dims, block.sectionIndex);
|
|
778
1711
|
return [
|
|
779
1712
|
{
|
|
780
|
-
...
|
|
781
|
-
element:
|
|
1713
|
+
...headMeasured,
|
|
1714
|
+
element: split.head,
|
|
782
1715
|
keepWithNext: false,
|
|
783
1716
|
keepLines: false,
|
|
784
1717
|
pageBreakBefore: false,
|
|
@@ -786,7 +1719,7 @@ var DocxodusEditor = (() => {
|
|
|
786
1719
|
},
|
|
787
1720
|
{
|
|
788
1721
|
...tailMeasured,
|
|
789
|
-
element: tail,
|
|
1722
|
+
element: split.tail,
|
|
790
1723
|
keepWithNext: false,
|
|
791
1724
|
keepLines: false,
|
|
792
1725
|
pageBreakBefore: false,
|
|
@@ -857,7 +1790,7 @@ var DocxodusEditor = (() => {
|
|
|
857
1790
|
const registry = /* @__PURE__ */ new Map();
|
|
858
1791
|
const registryEl = this.stagingElement.querySelector("#pagination-footnote-registry");
|
|
859
1792
|
if (!registryEl) return registry;
|
|
860
|
-
const entries = Array.from(registryEl.
|
|
1793
|
+
const entries = Array.from(registryEl.children).filter((entry) => entry instanceof this.view.HTMLElement && entry.hasAttribute("data-footnote-id"));
|
|
861
1794
|
for (const entry of entries) {
|
|
862
1795
|
const footnoteId = entry.dataset.footnoteId;
|
|
863
1796
|
if (footnoteId) {
|
|
@@ -866,6 +1799,68 @@ var DocxodusEditor = (() => {
|
|
|
866
1799
|
}
|
|
867
1800
|
return registry;
|
|
868
1801
|
}
|
|
1802
|
+
/** Read Word's optional normal and continuation separator stories. */
|
|
1803
|
+
parseFootnoteSeparators() {
|
|
1804
|
+
const registry = this.stagingElement.querySelector("#pagination-footnote-registry");
|
|
1805
|
+
const clone = (kind) => {
|
|
1806
|
+
const source = Array.from(registry?.children ?? []).find(
|
|
1807
|
+
(entry) => entry instanceof this.view.HTMLElement && entry.getAttribute("data-footnote-separator") === kind
|
|
1808
|
+
);
|
|
1809
|
+
return source?.cloneNode(true);
|
|
1810
|
+
};
|
|
1811
|
+
return {
|
|
1812
|
+
normal: clone("normal") ?? null,
|
|
1813
|
+
continuation: clone("continuation") ?? null
|
|
1814
|
+
};
|
|
1815
|
+
}
|
|
1816
|
+
/** Append the exact separator story selected for this initial/continued note band. */
|
|
1817
|
+
appendFootnoteSeparator(container, continuation) {
|
|
1818
|
+
const kind = continuation ? "continuation" : "normal";
|
|
1819
|
+
const source = continuation ? this.footnoteContinuationSeparator : this.footnoteSeparator;
|
|
1820
|
+
if (!source) {
|
|
1821
|
+
const fallback = this.document.createElement("hr");
|
|
1822
|
+
fallback.dataset.footnoteSeparator = kind;
|
|
1823
|
+
container.appendChild(fallback);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const clone = source.cloneNode(true);
|
|
1827
|
+
for (const element of [clone, ...Array.from(clone.querySelectorAll("*"))]) {
|
|
1828
|
+
element.removeAttribute("id");
|
|
1829
|
+
element.removeAttribute("data-anchor");
|
|
1830
|
+
element.removeAttribute("data-committed-text");
|
|
1831
|
+
element.removeAttribute("data-source-anchor-id");
|
|
1832
|
+
element.removeAttribute("data-page-fragment-id");
|
|
1833
|
+
element.removeAttribute("data-fragment-index");
|
|
1834
|
+
element.removeAttribute("data-footnote-id");
|
|
1835
|
+
element.removeAttribute("data-comment-id");
|
|
1836
|
+
element.removeAttribute("name");
|
|
1837
|
+
if (element instanceof this.view.HTMLAnchorElement && element.getAttribute("href")?.startsWith("#")) {
|
|
1838
|
+
element.removeAttribute("href");
|
|
1839
|
+
}
|
|
1840
|
+
element.setAttribute("contenteditable", "false");
|
|
1841
|
+
}
|
|
1842
|
+
container.appendChild(clone);
|
|
1843
|
+
}
|
|
1844
|
+
/** Parses the hidden source notes used to render paginated margin comments. */
|
|
1845
|
+
parseCommentMarginRegistry() {
|
|
1846
|
+
const registry = /* @__PURE__ */ new Map();
|
|
1847
|
+
const registryEl = this.stagingElement.querySelector(
|
|
1848
|
+
"#pagination-comment-margin-registry"
|
|
1849
|
+
);
|
|
1850
|
+
if (!registryEl) return registry;
|
|
1851
|
+
for (const entry of Array.from(
|
|
1852
|
+
registryEl.querySelectorAll("[data-comment-id]")
|
|
1853
|
+
)) {
|
|
1854
|
+
const commentId = entry.dataset.commentId;
|
|
1855
|
+
if (!commentId) continue;
|
|
1856
|
+
let root = entry;
|
|
1857
|
+
for (let parent = root.parentElement?.closest("[data-comment-id]"); parent && registryEl.contains(parent); parent = root.parentElement?.closest("[data-comment-id]")) {
|
|
1858
|
+
root = parent;
|
|
1859
|
+
}
|
|
1860
|
+
registry.set(commentId, root.cloneNode(true));
|
|
1861
|
+
}
|
|
1862
|
+
return registry;
|
|
1863
|
+
}
|
|
869
1864
|
/**
|
|
870
1865
|
* Extracts footnote reference IDs from an element.
|
|
871
1866
|
*/
|
|
@@ -880,6 +1875,92 @@ var DocxodusEditor = (() => {
|
|
|
880
1875
|
}
|
|
881
1876
|
return ids;
|
|
882
1877
|
}
|
|
1878
|
+
/** Clone a note child for the continuation queue with its original selector position. */
|
|
1879
|
+
cloneFootnoteElementForContinuation(element, sourceIndex) {
|
|
1880
|
+
const clone = element.cloneNode(true);
|
|
1881
|
+
clone.setAttribute(
|
|
1882
|
+
FOOTNOTE_SOURCE_POSITION_ATTR,
|
|
1883
|
+
sourceIndex === 0 ? "first" : "later"
|
|
1884
|
+
);
|
|
1885
|
+
return clone;
|
|
1886
|
+
}
|
|
1887
|
+
/** Remove identities that belong only to the initial registry presentation shell. */
|
|
1888
|
+
makeContinuationShellInert(element) {
|
|
1889
|
+
element.removeAttribute("id");
|
|
1890
|
+
element.removeAttribute("data-anchor");
|
|
1891
|
+
element.removeAttribute("data-committed-text");
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Build the exact continuation shape shared by measurement and paint.
|
|
1895
|
+
*
|
|
1896
|
+
* Keep the source `.footnote-item > .footnote-content` ancestry: document
|
|
1897
|
+
* CSS, inherited direction/language, and custom classes frequently target
|
|
1898
|
+
* those shells. Only the number and initial HTML/editor identities are
|
|
1899
|
+
* omitted. A hidden sentinel preserves `p:not(:first-of-type)` for a page
|
|
1900
|
+
* whose first carried element was a later source paragraph.
|
|
1901
|
+
*/
|
|
1902
|
+
createFootnoteContinuationWrapper(continuation, elements = continuation.remainingElements) {
|
|
1903
|
+
const source = this.footnoteRegistry.get(continuation.footnoteId);
|
|
1904
|
+
const wrapper = source ? source.cloneNode(false) : this.document.createElement("div");
|
|
1905
|
+
this.makeContinuationShellInert(wrapper);
|
|
1906
|
+
wrapper.classList.add("footnote-continuation");
|
|
1907
|
+
wrapper.dataset.footnoteId = continuation.footnoteId;
|
|
1908
|
+
if (continuation.sourceAnchorId) {
|
|
1909
|
+
wrapper.dataset.sourceAnchorId = continuation.sourceAnchorId;
|
|
1910
|
+
}
|
|
1911
|
+
const sourceContent = source?.querySelector(".footnote-content");
|
|
1912
|
+
const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
|
|
1913
|
+
this.makeContinuationShellInert(content);
|
|
1914
|
+
if (!sourceContent) content.className = "footnote-content";
|
|
1915
|
+
const first = elements[0];
|
|
1916
|
+
if (first?.tagName === "P" && first.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
|
|
1917
|
+
const sentinel = this.document.createElement("p");
|
|
1918
|
+
sentinel.className = "footnote-continuation-position-sentinel";
|
|
1919
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
1920
|
+
sentinel.style.setProperty("display", "none", "important");
|
|
1921
|
+
content.appendChild(sentinel);
|
|
1922
|
+
}
|
|
1923
|
+
for (let index = 0; index < elements.length; index++) {
|
|
1924
|
+
this.checkpoint();
|
|
1925
|
+
const element = elements[index];
|
|
1926
|
+
const clone = element.cloneNode(true);
|
|
1927
|
+
clone.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
1928
|
+
if (index === 0 && clone.tagName === "P") {
|
|
1929
|
+
clone.style.setProperty("display", "block", "important");
|
|
1930
|
+
}
|
|
1931
|
+
content.appendChild(clone);
|
|
1932
|
+
}
|
|
1933
|
+
wrapper.appendChild(content);
|
|
1934
|
+
return wrapper;
|
|
1935
|
+
}
|
|
1936
|
+
/** Build the exact partial-note item shape shared by measurement and paint. */
|
|
1937
|
+
createPartialFootnoteItem(footnote, fittingElements) {
|
|
1938
|
+
const item = footnote.cloneNode(false);
|
|
1939
|
+
const number = footnote.querySelector(".footnote-number");
|
|
1940
|
+
if (number) item.appendChild(number.cloneNode(true));
|
|
1941
|
+
const sourceContent = footnote.querySelector(".footnote-content");
|
|
1942
|
+
const content = sourceContent ? sourceContent.cloneNode(false) : this.document.createElement("span");
|
|
1943
|
+
if (!sourceContent) content.className = "footnote-content";
|
|
1944
|
+
for (const element of fittingElements) {
|
|
1945
|
+
this.checkpoint();
|
|
1946
|
+
content.appendChild(element.cloneNode(true));
|
|
1947
|
+
}
|
|
1948
|
+
item.appendChild(content);
|
|
1949
|
+
return item;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Where a note measurement tree is attached.
|
|
1953
|
+
*
|
|
1954
|
+
* A note band is reserved from a measurement and then painted; if the two
|
|
1955
|
+
* happen under different inherited typography the painted notes overflow the
|
|
1956
|
+
* reserve, which is the invisible clipping this engine exists to avoid. The
|
|
1957
|
+
* registry lives in the staging tree but a band paints inside the output
|
|
1958
|
+
* container, so measure there. The host is never a page box, so a page's
|
|
1959
|
+
* `zoom` cannot scale a reserve that is accounted for in unscaled points.
|
|
1960
|
+
*/
|
|
1961
|
+
noteMeasurementHost() {
|
|
1962
|
+
return this.containerElement.isConnected ? this.containerElement : this.stagingElement;
|
|
1963
|
+
}
|
|
883
1964
|
/**
|
|
884
1965
|
* Measures the height of footnotes for given IDs (in points).
|
|
885
1966
|
* Creates a temporary container to measure the footnotes.
|
|
@@ -887,140 +1968,246 @@ var DocxodusEditor = (() => {
|
|
|
887
1968
|
* @param contentWidth - Width for measurement
|
|
888
1969
|
* @param continuation - Optional continuation content to include first
|
|
889
1970
|
*/
|
|
890
|
-
measureFootnotesHeight(footnoteIds, contentWidth, continuation) {
|
|
1971
|
+
measureFootnotesHeight(footnoteIds, contentWidth, continuation, partialFootnotes) {
|
|
891
1972
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
892
|
-
if (footnoteIds.length === 0 && !hasContinuation
|
|
1973
|
+
if (footnoteIds.length === 0 && !hasContinuation) {
|
|
1974
|
+
return 0;
|
|
1975
|
+
}
|
|
1976
|
+
if (this.footnoteRegistry.size === 0 && !hasContinuation) {
|
|
893
1977
|
return 0;
|
|
894
1978
|
}
|
|
895
|
-
const measureContainer = document.createElement("div");
|
|
1979
|
+
const measureContainer = this.document.createElement("div");
|
|
896
1980
|
measureContainer.style.position = "absolute";
|
|
897
1981
|
measureContainer.style.visibility = "hidden";
|
|
898
1982
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
899
1983
|
measureContainer.style.left = "-9999px";
|
|
900
1984
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
901
|
-
|
|
902
|
-
measureContainer.appendChild(hr);
|
|
1985
|
+
this.appendFootnoteSeparator(measureContainer, Boolean(hasContinuation));
|
|
903
1986
|
if (hasContinuation) {
|
|
904
|
-
|
|
905
|
-
contWrapper.className = "footnote-continuation";
|
|
906
|
-
for (const el of continuation.remainingElements) {
|
|
907
|
-
contWrapper.appendChild(el.cloneNode(true));
|
|
908
|
-
}
|
|
909
|
-
measureContainer.appendChild(contWrapper);
|
|
1987
|
+
measureContainer.appendChild(this.createFootnoteContinuationWrapper(continuation));
|
|
910
1988
|
}
|
|
1989
|
+
const partialById = new Map(
|
|
1990
|
+
partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
|
|
1991
|
+
);
|
|
911
1992
|
for (const id of footnoteIds) {
|
|
1993
|
+
this.checkpoint();
|
|
912
1994
|
const footnote = this.footnoteRegistry.get(id);
|
|
913
1995
|
if (footnote) {
|
|
914
|
-
|
|
1996
|
+
const partial = partialById.get(id);
|
|
1997
|
+
measureContainer.appendChild(partial ? this.createPartialFootnoteItem(footnote, partial.fittingElements) : footnote.cloneNode(true));
|
|
915
1998
|
}
|
|
916
1999
|
}
|
|
917
|
-
this.
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
2000
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2001
|
+
try {
|
|
2002
|
+
return pxToPt(measureContainer.getBoundingClientRect().height);
|
|
2003
|
+
} finally {
|
|
2004
|
+
measureContainer.remove();
|
|
2005
|
+
}
|
|
922
2006
|
}
|
|
923
2007
|
/**
|
|
924
|
-
*
|
|
2008
|
+
* Partition a continuation for one page's note band, preferring complete children
|
|
2009
|
+
* and range-fragmenting an eligible paragraph when necessary. Always advances by
|
|
2010
|
+
* at least one element so an indivisible oversized paragraph follows the established
|
|
2011
|
+
* clipped fallback without trapping pagination in a loop.
|
|
925
2012
|
*/
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const measureContainer = document.createElement("div");
|
|
2013
|
+
splitContinuationForPage(continuation, availableHeightPt, contentWidth) {
|
|
2014
|
+
const fitting = [];
|
|
2015
|
+
let remaining = [];
|
|
2016
|
+
const measureContainer = this.document.createElement("div");
|
|
931
2017
|
measureContainer.style.position = "absolute";
|
|
932
2018
|
measureContainer.style.visibility = "hidden";
|
|
933
2019
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
934
2020
|
measureContainer.style.left = "-9999px";
|
|
935
2021
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
2022
|
+
this.appendFootnoteSeparator(measureContainer, true);
|
|
2023
|
+
const wrapper = this.createFootnoteContinuationWrapper(continuation, []);
|
|
2024
|
+
const content = wrapper.querySelector(":scope > .footnote-content");
|
|
2025
|
+
if (!content) {
|
|
2026
|
+
throw new Error("Footnote continuation is missing its content shell");
|
|
2027
|
+
}
|
|
2028
|
+
measureContainer.appendChild(wrapper);
|
|
2029
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2030
|
+
try {
|
|
2031
|
+
for (let index = 0; index < continuation.remainingElements.length; index++) {
|
|
2032
|
+
this.checkpoint();
|
|
2033
|
+
const element = continuation.remainingElements[index];
|
|
2034
|
+
const sourcePosition = element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
2035
|
+
if (fitting.length === 0 && element.tagName === "P" && element.getAttribute(FOOTNOTE_SOURCE_POSITION_ATTR) === "later") {
|
|
2036
|
+
const sentinel = this.document.createElement("p");
|
|
2037
|
+
sentinel.className = "footnote-continuation-position-sentinel";
|
|
2038
|
+
sentinel.setAttribute("aria-hidden", "true");
|
|
2039
|
+
sentinel.style.setProperty("display", "none", "important");
|
|
2040
|
+
content.appendChild(sentinel);
|
|
2041
|
+
}
|
|
2042
|
+
const candidate = element.cloneNode(true);
|
|
2043
|
+
candidate.removeAttribute(FOOTNOTE_SOURCE_POSITION_ATTR);
|
|
2044
|
+
if (fitting.length === 0 && candidate.tagName === "P") {
|
|
2045
|
+
candidate.style.setProperty("display", "block", "important");
|
|
2046
|
+
}
|
|
2047
|
+
content.appendChild(candidate);
|
|
2048
|
+
const candidateHeight = pxToPt(measureContainer.getBoundingClientRect().height);
|
|
2049
|
+
if (candidateHeight <= availableHeightPt) {
|
|
2050
|
+
fitting.push(element);
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
const canSplitCandidate = candidate.tagName === "P" && this.canRangeFragmentParagraph(candidate);
|
|
2054
|
+
candidate.remove();
|
|
2055
|
+
let split = null;
|
|
2056
|
+
if (canSplitCandidate) {
|
|
2057
|
+
split = this.splitParagraphAtLargestFit(candidate, (head) => {
|
|
2058
|
+
content.appendChild(head);
|
|
2059
|
+
try {
|
|
2060
|
+
return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2061
|
+
} finally {
|
|
2062
|
+
head.remove();
|
|
2063
|
+
}
|
|
2064
|
+
}, {
|
|
2065
|
+
// Both fallbacks below cut where ordinary line breaking would not,
|
|
2066
|
+
// so they are only ever right for an element that owns the whole
|
|
2067
|
+
// band: if this page already carries earlier content, a paragraph
|
|
2068
|
+
// that will not start here belongs intact in the next note band.
|
|
2069
|
+
emergencyGraphemeBreaks: fitting.length === 0,
|
|
2070
|
+
// A continuation page owns the full note band. If even one legal
|
|
2071
|
+
// grapheme is taller than it, clip only that unit and keep draining.
|
|
2072
|
+
forceFirstOnNoFit: fitting.length === 0
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
if (split?.kind === "split") {
|
|
2076
|
+
if (sourcePosition) {
|
|
2077
|
+
split.head.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
|
|
2078
|
+
split.tail.setAttribute(FOOTNOTE_SOURCE_POSITION_ATTR, sourcePosition);
|
|
2079
|
+
}
|
|
2080
|
+
fitting.push(split.head);
|
|
2081
|
+
remaining = [split.tail, ...continuation.remainingElements.slice(index + 1)];
|
|
2082
|
+
} else if (fitting.length === 0) {
|
|
2083
|
+
fitting.push(element);
|
|
2084
|
+
remaining = continuation.remainingElements.slice(index + 1);
|
|
2085
|
+
} else {
|
|
2086
|
+
remaining = continuation.remainingElements.slice(index);
|
|
1000
2087
|
}
|
|
1001
2088
|
break;
|
|
1002
2089
|
}
|
|
2090
|
+
} finally {
|
|
2091
|
+
measureContainer.remove();
|
|
1003
2092
|
}
|
|
1004
|
-
return {
|
|
2093
|
+
return {
|
|
2094
|
+
current: {
|
|
2095
|
+
footnoteId: continuation.footnoteId,
|
|
2096
|
+
sourceAnchorId: continuation.sourceAnchorId,
|
|
2097
|
+
remainingElements: fitting
|
|
2098
|
+
},
|
|
2099
|
+
overflow: remaining.length > 0 ? {
|
|
2100
|
+
footnoteId: continuation.footnoteId,
|
|
2101
|
+
sourceAnchorId: continuation.sourceAnchorId,
|
|
2102
|
+
remainingElements: remaining
|
|
2103
|
+
} : null
|
|
2104
|
+
};
|
|
1005
2105
|
}
|
|
1006
2106
|
/**
|
|
1007
|
-
*
|
|
2107
|
+
* Splits a footnote element into parts that fit within the available height.
|
|
2108
|
+
* Returns the elements that fit and the elements that need to continue.
|
|
1008
2109
|
*/
|
|
1009
|
-
|
|
1010
|
-
const
|
|
1011
|
-
|
|
1012
|
-
|
|
2110
|
+
splitFootnoteToFit(footnoteElement, availableHeightPt, contentWidth, forceProgress = false, existingPayload) {
|
|
2111
|
+
const layoutContext = this.document.createElement("div");
|
|
2112
|
+
layoutContext.style.position = "absolute";
|
|
2113
|
+
layoutContext.style.visibility = "hidden";
|
|
2114
|
+
layoutContext.style.width = `${contentWidth}pt`;
|
|
2115
|
+
layoutContext.style.left = "-9999px";
|
|
2116
|
+
layoutContext.className = this.cssPrefix + "footnotes";
|
|
2117
|
+
this.appendFootnoteSeparator(layoutContext, false);
|
|
2118
|
+
const attachedFootnote = footnoteElement.cloneNode(true);
|
|
2119
|
+
layoutContext.appendChild(attachedFootnote);
|
|
2120
|
+
this.noteMeasurementHost().appendChild(layoutContext);
|
|
2121
|
+
const measureContainer = this.document.createElement("div");
|
|
1013
2122
|
measureContainer.style.position = "absolute";
|
|
1014
2123
|
measureContainer.style.visibility = "hidden";
|
|
1015
2124
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
1016
2125
|
measureContainer.style.left = "-9999px";
|
|
1017
2126
|
measureContainer.className = this.cssPrefix + "footnotes";
|
|
1018
|
-
|
|
1019
|
-
this.
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
2127
|
+
const hasContinuation = Boolean(existingPayload?.continuation?.remainingElements.length);
|
|
2128
|
+
this.appendFootnoteSeparator(measureContainer, hasContinuation);
|
|
2129
|
+
if (hasContinuation) {
|
|
2130
|
+
measureContainer.appendChild(this.createFootnoteContinuationWrapper(
|
|
2131
|
+
existingPayload.continuation
|
|
2132
|
+
));
|
|
2133
|
+
}
|
|
2134
|
+
const partialById = new Map(
|
|
2135
|
+
existingPayload?.partialFootnotes?.map((partial) => [partial.footnoteId, partial]) ?? []
|
|
2136
|
+
);
|
|
2137
|
+
for (const id of existingPayload?.footnoteIds ?? []) {
|
|
2138
|
+
this.checkpoint();
|
|
2139
|
+
const source = this.footnoteRegistry.get(id);
|
|
2140
|
+
if (!source) continue;
|
|
2141
|
+
const partial = partialById.get(id);
|
|
2142
|
+
measureContainer.appendChild(partial ? this.createPartialFootnoteItem(source, partial.fittingElements) : source.cloneNode(true));
|
|
2143
|
+
}
|
|
2144
|
+
const measuredPartial = this.createPartialFootnoteItem(attachedFootnote, []);
|
|
2145
|
+
const measuredContent = measuredPartial.querySelector(":scope > .footnote-content");
|
|
2146
|
+
if (!measuredContent) {
|
|
2147
|
+
layoutContext.remove();
|
|
2148
|
+
throw new Error("Footnote is missing its content shell");
|
|
2149
|
+
}
|
|
2150
|
+
measureContainer.appendChild(measuredPartial);
|
|
2151
|
+
this.noteMeasurementHost().appendChild(measureContainer);
|
|
2152
|
+
try {
|
|
2153
|
+
const footnoteContent = attachedFootnote.querySelector(".footnote-content");
|
|
2154
|
+
if (!footnoteContent) {
|
|
2155
|
+
return {
|
|
2156
|
+
fits: Array.from(attachedFootnote.children).map((el) => el.cloneNode(true)),
|
|
2157
|
+
overflow: []
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
const children = Array.from(footnoteContent.children);
|
|
2161
|
+
const fits = [];
|
|
2162
|
+
let overflow = [];
|
|
2163
|
+
for (let i = 0; i < children.length; i++) {
|
|
2164
|
+
this.checkpoint();
|
|
2165
|
+
const child = children[i];
|
|
2166
|
+
const measuredCandidate = child.cloneNode(true);
|
|
2167
|
+
measuredContent.appendChild(measuredCandidate);
|
|
2168
|
+
const candidateFits = pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2169
|
+
if (candidateFits) {
|
|
2170
|
+
fits.push(child.cloneNode(true));
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
measuredCandidate.remove();
|
|
2174
|
+
const split = this.canRangeFragmentParagraph(child, true) ? this.splitParagraphAtLargestFit(child, (head) => {
|
|
2175
|
+
measuredContent.appendChild(head);
|
|
2176
|
+
try {
|
|
2177
|
+
return pxToPt(measureContainer.getBoundingClientRect().height) <= availableHeightPt;
|
|
2178
|
+
} finally {
|
|
2179
|
+
head.remove();
|
|
2180
|
+
}
|
|
2181
|
+
}, {
|
|
2182
|
+
// Both fallbacks cut where ordinary line breaking would not, so they
|
|
2183
|
+
// are only ever right for an element that owns the whole band: with
|
|
2184
|
+
// earlier siblings already packed here, a paragraph that will not
|
|
2185
|
+
// start belongs intact in the next note band.
|
|
2186
|
+
emergencyGraphemeBreaks: fits.length === 0 && forceProgress,
|
|
2187
|
+
forceFirstOnNoFit: fits.length === 0 && forceProgress
|
|
2188
|
+
}) : null;
|
|
2189
|
+
if (split?.kind === "split") {
|
|
2190
|
+
fits.push(split.head);
|
|
2191
|
+
split.tail.setAttribute(
|
|
2192
|
+
FOOTNOTE_SOURCE_POSITION_ATTR,
|
|
2193
|
+
i === 0 ? "first" : "later"
|
|
2194
|
+
);
|
|
2195
|
+
overflow = [split.tail, ...children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset))];
|
|
2196
|
+
} else if (split?.kind === "no-fit") {
|
|
2197
|
+
overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
|
|
2198
|
+
} else if (fits.length === 0 && forceProgress) {
|
|
2199
|
+
fits.push(child.cloneNode(true));
|
|
2200
|
+
overflow = children.slice(i + 1).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + 1 + offset));
|
|
2201
|
+
} else {
|
|
2202
|
+
overflow = children.slice(i).map((element, offset) => this.cloneFootnoteElementForContinuation(element, i + offset));
|
|
2203
|
+
}
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
return { fits, overflow };
|
|
2207
|
+
} finally {
|
|
2208
|
+
measureContainer.remove();
|
|
2209
|
+
layoutContext.remove();
|
|
2210
|
+
}
|
|
1024
2211
|
}
|
|
1025
2212
|
/**
|
|
1026
2213
|
* Adds footnotes to a page container, including continuation content.
|
|
@@ -1033,12 +2220,11 @@ var DocxodusEditor = (() => {
|
|
|
1033
2220
|
if (this.footnoteRegistry.size === 0 && !hasContinuation) {
|
|
1034
2221
|
return;
|
|
1035
2222
|
}
|
|
1036
|
-
const partialFootnoteIds = new Set(partialFootnotes?.map((p) => p.footnoteId) || []);
|
|
1037
2223
|
const maxFootnoteHeight = Math.min(
|
|
1038
2224
|
footnoteHeight,
|
|
1039
2225
|
bands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO
|
|
1040
2226
|
);
|
|
1041
|
-
const footnotesDiv = document.createElement("div");
|
|
2227
|
+
const footnotesDiv = this.document.createElement("div");
|
|
1042
2228
|
footnotesDiv.className = `${this.cssPrefix}footnotes`;
|
|
1043
2229
|
footnotesDiv.style.position = "absolute";
|
|
1044
2230
|
footnotesDiv.style.bottom = `${dims.pageHeight - (bands.bodyTop + bands.bodyHeight)}pt`;
|
|
@@ -1047,35 +2233,18 @@ var DocxodusEditor = (() => {
|
|
|
1047
2233
|
footnotesDiv.style.boxSizing = "border-box";
|
|
1048
2234
|
footnotesDiv.style.maxHeight = `${maxFootnoteHeight}pt`;
|
|
1049
2235
|
footnotesDiv.style.overflow = "hidden";
|
|
1050
|
-
|
|
1051
|
-
footnotesDiv.appendChild(hr);
|
|
2236
|
+
this.appendFootnoteSeparator(footnotesDiv, Boolean(hasContinuation));
|
|
1052
2237
|
if (hasContinuation) {
|
|
1053
|
-
|
|
1054
|
-
contWrapper.className = "footnote-continuation";
|
|
1055
|
-
for (const el of continuation.remainingElements) {
|
|
1056
|
-
contWrapper.appendChild(el.cloneNode(true));
|
|
1057
|
-
}
|
|
1058
|
-
footnotesDiv.appendChild(contWrapper);
|
|
2238
|
+
footnotesDiv.appendChild(this.createFootnoteContinuationWrapper(continuation));
|
|
1059
2239
|
}
|
|
1060
2240
|
for (const id of footnoteIds) {
|
|
1061
2241
|
const partial = partialFootnotes?.find((p) => p.footnoteId === id);
|
|
1062
2242
|
if (partial) {
|
|
1063
2243
|
const footnote = this.footnoteRegistry.get(id);
|
|
1064
2244
|
if (footnote) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
const numberSpan = footnote.querySelector(".footnote-number");
|
|
1069
|
-
if (numberSpan) {
|
|
1070
|
-
partialDiv.appendChild(numberSpan.cloneNode(true));
|
|
1071
|
-
}
|
|
1072
|
-
const contentSpan = document.createElement("span");
|
|
1073
|
-
contentSpan.className = "footnote-content";
|
|
1074
|
-
for (const el of partial.fittingElements) {
|
|
1075
|
-
contentSpan.appendChild(el.cloneNode(true));
|
|
1076
|
-
}
|
|
1077
|
-
partialDiv.appendChild(contentSpan);
|
|
1078
|
-
footnotesDiv.appendChild(partialDiv);
|
|
2245
|
+
footnotesDiv.appendChild(
|
|
2246
|
+
this.createPartialFootnoteItem(footnote, partial.fittingElements)
|
|
2247
|
+
);
|
|
1079
2248
|
}
|
|
1080
2249
|
} else {
|
|
1081
2250
|
const footnote = this.footnoteRegistry.get(id);
|
|
@@ -1086,30 +2255,26 @@ var DocxodusEditor = (() => {
|
|
|
1086
2255
|
}
|
|
1087
2256
|
pageBox.appendChild(footnotesDiv);
|
|
1088
2257
|
}
|
|
1089
|
-
/**
|
|
1090
|
-
|
|
1091
|
-
*/
|
|
1092
|
-
selectHeader(sectionIndex, pageInSection, globalPageNumber) {
|
|
2258
|
+
/** Select the section's first/odd/even header from its one-based page position. */
|
|
2259
|
+
selectHeader(sectionIndex, pageInSection, displayedPageNumber) {
|
|
1093
2260
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1094
2261
|
if (!sectionHf) return void 0;
|
|
1095
2262
|
if (pageInSection === 1 && sectionHf.headerFirst) {
|
|
1096
2263
|
return sectionHf.headerFirst;
|
|
1097
2264
|
}
|
|
1098
|
-
if (
|
|
2265
|
+
if (displayedPageNumber % 2 === 0 && sectionHf.headerEven) {
|
|
1099
2266
|
return sectionHf.headerEven;
|
|
1100
2267
|
}
|
|
1101
2268
|
return sectionHf.headerDefault;
|
|
1102
2269
|
}
|
|
1103
|
-
/**
|
|
1104
|
-
|
|
1105
|
-
*/
|
|
1106
|
-
selectFooter(sectionIndex, pageInSection, globalPageNumber) {
|
|
2270
|
+
/** Select the section's first/odd/even footer from its one-based page position. */
|
|
2271
|
+
selectFooter(sectionIndex, pageInSection, displayedPageNumber) {
|
|
1107
2272
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1108
2273
|
if (!sectionHf) return void 0;
|
|
1109
2274
|
if (pageInSection === 1 && sectionHf.footerFirst) {
|
|
1110
2275
|
return sectionHf.footerFirst;
|
|
1111
2276
|
}
|
|
1112
|
-
if (
|
|
2277
|
+
if (displayedPageNumber % 2 === 0 && sectionHf.footerEven) {
|
|
1113
2278
|
return sectionHf.footerEven;
|
|
1114
2279
|
}
|
|
1115
2280
|
return sectionHf.footerDefault;
|
|
@@ -1124,7 +2289,7 @@ var DocxodusEditor = (() => {
|
|
|
1124
2289
|
* Deterministic: it depends only on the section's page setup and the registry's pre-measured
|
|
1125
2290
|
* story heights, never on the page's content, which is what keeps it lazy-loading compatible.
|
|
1126
2291
|
*/
|
|
1127
|
-
getPageBands(dims, sectionIndex, pageInSection,
|
|
2292
|
+
getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber) {
|
|
1128
2293
|
const sectionHf = this.hfRegistry.get(sectionIndex);
|
|
1129
2294
|
return resolvePageBands(
|
|
1130
2295
|
dims,
|
|
@@ -1133,14 +2298,14 @@ var DocxodusEditor = (() => {
|
|
|
1133
2298
|
sectionHf?.headerEvenHeight,
|
|
1134
2299
|
sectionHf?.headerDefaultHeight,
|
|
1135
2300
|
pageInSection,
|
|
1136
|
-
|
|
2301
|
+
displayedPageNumber
|
|
1137
2302
|
),
|
|
1138
2303
|
this.selectStoryHeight(
|
|
1139
2304
|
sectionHf?.footerFirstHeight,
|
|
1140
2305
|
sectionHf?.footerEvenHeight,
|
|
1141
2306
|
sectionHf?.footerDefaultHeight,
|
|
1142
2307
|
pageInSection,
|
|
1143
|
-
|
|
2308
|
+
displayedPageNumber
|
|
1144
2309
|
)
|
|
1145
2310
|
);
|
|
1146
2311
|
}
|
|
@@ -1148,9 +2313,9 @@ var DocxodusEditor = (() => {
|
|
|
1148
2313
|
* The measured height of the running story this page position selects, mirroring
|
|
1149
2314
|
* {@link selectHeader}/{@link selectFooter}. Zero when the page has no such story.
|
|
1150
2315
|
*/
|
|
1151
|
-
selectStoryHeight(first, even, fallback, pageInSection,
|
|
2316
|
+
selectStoryHeight(first, even, fallback, pageInSection, displayedPageNumber) {
|
|
1152
2317
|
if (pageInSection === 1 && first != null) return first;
|
|
1153
|
-
if (
|
|
2318
|
+
if (displayedPageNumber % 2 === 0 && even != null) return even;
|
|
1154
2319
|
return fallback ?? 0;
|
|
1155
2320
|
}
|
|
1156
2321
|
/**
|
|
@@ -1162,7 +2327,7 @@ var DocxodusEditor = (() => {
|
|
|
1162
2327
|
* silently starts overlapping body text.
|
|
1163
2328
|
*/
|
|
1164
2329
|
measureHeaderFooterHeight(source, contentWidth) {
|
|
1165
|
-
const measureContainer = document.createElement("div");
|
|
2330
|
+
const measureContainer = this.document.createElement("div");
|
|
1166
2331
|
measureContainer.style.position = "absolute";
|
|
1167
2332
|
measureContainer.style.visibility = "hidden";
|
|
1168
2333
|
measureContainer.style.width = `${contentWidth}pt`;
|
|
@@ -1181,79 +2346,275 @@ var DocxodusEditor = (() => {
|
|
|
1181
2346
|
* Implements a single-pass, forward-only algorithm that is compatible with future lazy loading.
|
|
1182
2347
|
* Supports footnote continuation - long footnotes can split across pages.
|
|
1183
2348
|
*/
|
|
1184
|
-
flowToPages(blocks, dims, startPageNumber, sectionIndex) {
|
|
2349
|
+
flowToPages(blocks, dims, startPageNumber, sectionIndex, sectionDimensions) {
|
|
1185
2350
|
const pages = [];
|
|
1186
2351
|
let currentContent = [];
|
|
1187
2352
|
let pageNumber = startPageNumber;
|
|
1188
|
-
let
|
|
2353
|
+
let pageSectionIndex = sectionIndex;
|
|
2354
|
+
const sectionStartPages = /* @__PURE__ */ new Map([[sectionIndex, startPageNumber]]);
|
|
2355
|
+
const pageInSection = (owner = pageSectionIndex, physicalPage = pageNumber) => physicalPage - (sectionStartPages.get(owner) ?? physicalPage) + 1;
|
|
2356
|
+
const dimensionsFor = (owner = pageSectionIndex) => sectionDimensions.get(owner) ?? dims;
|
|
2357
|
+
const markSectionPlaced = (owner) => {
|
|
2358
|
+
if (!sectionStartPages.has(owner)) sectionStartPages.set(owner, pageNumber);
|
|
2359
|
+
};
|
|
2360
|
+
const previousBox = this.containerElement.querySelector(
|
|
2361
|
+
`.${this.cssPrefix}box:last-of-type`
|
|
2362
|
+
);
|
|
2363
|
+
let precedingDisplayedPageNumber = parseInt(
|
|
2364
|
+
previousBox?.dataset.displayedPageNumber ?? "0",
|
|
2365
|
+
10
|
|
2366
|
+
);
|
|
2367
|
+
const displayedPageNumber = (owner = pageSectionIndex, ownerPageInSection = pageInSection(owner), physicalPage = pageNumber) => {
|
|
2368
|
+
const numbering = this.pageNumbering.get(owner) ?? {};
|
|
2369
|
+
if (numbering.start !== void 0) {
|
|
2370
|
+
return numbering.start + ownerPageInSection - 1;
|
|
2371
|
+
}
|
|
2372
|
+
const ownerNumbering = this.pageNumbering.get(pageSectionIndex) ?? {};
|
|
2373
|
+
const currentPhysicalNumber = ownerNumbering.start !== void 0 ? ownerNumbering.start + pageInSection(pageSectionIndex) - 1 : precedingDisplayedPageNumber + 1;
|
|
2374
|
+
return currentPhysicalNumber + physicalPage - pageNumber;
|
|
2375
|
+
};
|
|
1189
2376
|
let { bodyHeight: effectiveContentHeight } = this.getPageBands(
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
pageInSection,
|
|
1193
|
-
|
|
2377
|
+
dimensionsFor(),
|
|
2378
|
+
pageSectionIndex,
|
|
2379
|
+
pageInSection(),
|
|
2380
|
+
displayedPageNumber()
|
|
1194
2381
|
);
|
|
1195
2382
|
let remainingHeight = effectiveContentHeight;
|
|
1196
2383
|
let prevMarginBottomPt = 0;
|
|
1197
2384
|
let currentFootnoteIds = [];
|
|
2385
|
+
let currentPageHasFootnoteReference = false;
|
|
1198
2386
|
let currentFootnoteHeight = 0;
|
|
1199
2387
|
let currentContinuation = this.pendingFootnoteContinuation;
|
|
1200
2388
|
let nextPageContinuation = null;
|
|
2389
|
+
let currentContinuationPartitioned = false;
|
|
2390
|
+
let currentPageAdmitted = false;
|
|
1201
2391
|
let deferredFootnoteIds = [];
|
|
1202
2392
|
let currentPartialFootnotes = [];
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
2393
|
+
const adoptEmptyPageOwner = (owner) => {
|
|
2394
|
+
pageSectionIndex = owner;
|
|
2395
|
+
const bands = this.getPageBands(
|
|
2396
|
+
dimensionsFor(owner),
|
|
2397
|
+
owner,
|
|
2398
|
+
pageInSection(owner),
|
|
2399
|
+
displayedPageNumber(owner)
|
|
2400
|
+
);
|
|
2401
|
+
effectiveContentHeight = bands.bodyHeight;
|
|
2402
|
+
remainingHeight = effectiveContentHeight;
|
|
2403
|
+
};
|
|
2404
|
+
const admitCurrentPage = () => {
|
|
2405
|
+
if (currentPageAdmitted) return;
|
|
2406
|
+
this.admitPageAllocation();
|
|
2407
|
+
currentPageAdmitted = true;
|
|
2408
|
+
};
|
|
2409
|
+
const prepareCurrentContinuation = () => {
|
|
2410
|
+
if (currentContinuationPartitioned || (currentContinuation?.remainingElements.length ?? 0) === 0) return;
|
|
2411
|
+
admitCurrentPage();
|
|
2412
|
+
const ownedDimensions = dimensionsFor();
|
|
2413
|
+
const pageBands = this.getPageBands(
|
|
2414
|
+
ownedDimensions,
|
|
2415
|
+
pageSectionIndex,
|
|
2416
|
+
pageInSection(),
|
|
2417
|
+
displayedPageNumber()
|
|
2418
|
+
);
|
|
2419
|
+
const partition = this.splitContinuationForPage(
|
|
2420
|
+
currentContinuation,
|
|
2421
|
+
pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO,
|
|
2422
|
+
ownedDimensions.contentWidth
|
|
2423
|
+
);
|
|
2424
|
+
currentContinuation = partition.current;
|
|
2425
|
+
if (partition.overflow) nextPageContinuation = partition.overflow;
|
|
2426
|
+
currentContinuationPartitioned = true;
|
|
2427
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2428
|
+
currentFootnoteIds,
|
|
2429
|
+
ownedDimensions.contentWidth,
|
|
2430
|
+
currentContinuation,
|
|
2431
|
+
currentPartialFootnotes
|
|
2432
|
+
);
|
|
2433
|
+
};
|
|
2434
|
+
const finishPage = (nextPageSectionIndex = pageSectionIndex, forceEmptyPage = false) => {
|
|
1207
2435
|
const hasCurrentContinuation = (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1208
|
-
if (currentContent.length === 0 && !hasCurrentContinuation)
|
|
2436
|
+
if (!forceEmptyPage && currentContent.length === 0 && currentFootnoteIds.length === 0 && !hasCurrentContinuation) {
|
|
2437
|
+
adoptEmptyPageOwner(nextPageSectionIndex);
|
|
2438
|
+
return;
|
|
2439
|
+
}
|
|
2440
|
+
prepareCurrentContinuation();
|
|
2441
|
+
admitCurrentPage();
|
|
2442
|
+
let pageContinuation = currentContinuation;
|
|
2443
|
+
const ownedPageInSection = pageInSection();
|
|
2444
|
+
const ownedDimensions = dimensionsFor();
|
|
2445
|
+
const ownedDisplayedPageNumber = displayedPageNumber(
|
|
2446
|
+
pageSectionIndex,
|
|
2447
|
+
ownedPageInSection
|
|
2448
|
+
);
|
|
2449
|
+
const pageBands = this.getPageBands(
|
|
2450
|
+
ownedDimensions,
|
|
2451
|
+
pageSectionIndex,
|
|
2452
|
+
ownedPageInSection,
|
|
2453
|
+
ownedDisplayedPageNumber
|
|
2454
|
+
);
|
|
2455
|
+
const maxFootnoteHeight = pageBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2456
|
+
pageContinuation = currentContinuation;
|
|
2457
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2458
|
+
currentFootnoteIds,
|
|
2459
|
+
ownedDimensions.contentWidth,
|
|
2460
|
+
pageContinuation,
|
|
2461
|
+
currentPartialFootnotes
|
|
2462
|
+
);
|
|
2463
|
+
if (currentContent.length === 0 && currentFootnoteIds.length > 0 && currentPartialFootnotes.length === 0) {
|
|
2464
|
+
const fittingIds = [];
|
|
2465
|
+
for (let index = 0; index < currentFootnoteIds.length; index++) {
|
|
2466
|
+
const footnoteId = currentFootnoteIds[index];
|
|
2467
|
+
const candidateIds = [...fittingIds, footnoteId];
|
|
2468
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2469
|
+
candidateIds,
|
|
2470
|
+
ownedDimensions.contentWidth,
|
|
2471
|
+
pageContinuation
|
|
2472
|
+
);
|
|
2473
|
+
const guardedCandidateHeight = candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT;
|
|
2474
|
+
if (guardedCandidateHeight <= maxFootnoteHeight) {
|
|
2475
|
+
fittingIds.push(footnoteId);
|
|
2476
|
+
currentFootnoteHeight = guardedCandidateHeight;
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
const hasPageContinuation = (pageContinuation?.remainingElements.length ?? 0) > 0;
|
|
2480
|
+
if (fittingIds.length === 0 && !hasPageContinuation) {
|
|
2481
|
+
const source = this.footnoteRegistry.get(footnoteId);
|
|
2482
|
+
const split = source ? this.splitFootnoteToFit(
|
|
2483
|
+
source,
|
|
2484
|
+
maxFootnoteHeight - FOOTNOTE_MEASUREMENT_GUARD_PT,
|
|
2485
|
+
ownedDimensions.contentWidth,
|
|
2486
|
+
true
|
|
2487
|
+
) : null;
|
|
2488
|
+
fittingIds.push(footnoteId);
|
|
2489
|
+
if (source && split && split.fits.length > 0 && split.overflow.length > 0) {
|
|
2490
|
+
currentPartialFootnotes.push({ footnoteId, fittingElements: split.fits });
|
|
2491
|
+
nextPageContinuation = {
|
|
2492
|
+
footnoteId,
|
|
2493
|
+
sourceAnchorId: source.dataset.sourceAnchorId,
|
|
2494
|
+
remainingElements: split.overflow
|
|
2495
|
+
};
|
|
2496
|
+
currentFootnoteHeight = maxFootnoteHeight;
|
|
2497
|
+
} else {
|
|
2498
|
+
currentFootnoteHeight = guardedCandidateHeight;
|
|
2499
|
+
}
|
|
2500
|
+
deferredFootnoteIds.push(...currentFootnoteIds.slice(index + 1));
|
|
2501
|
+
} else {
|
|
2502
|
+
deferredFootnoteIds.push(...currentFootnoteIds.slice(index));
|
|
2503
|
+
}
|
|
2504
|
+
break;
|
|
2505
|
+
}
|
|
2506
|
+
currentFootnoteIds = fittingIds;
|
|
2507
|
+
}
|
|
1209
2508
|
const page = this.createPage(
|
|
1210
|
-
|
|
2509
|
+
ownedDimensions,
|
|
1211
2510
|
pageNumber,
|
|
1212
|
-
|
|
2511
|
+
pageSectionIndex,
|
|
2512
|
+
ownedDisplayedPageNumber,
|
|
1213
2513
|
currentContent,
|
|
1214
|
-
|
|
2514
|
+
ownedPageInSection,
|
|
1215
2515
|
currentFootnoteIds,
|
|
1216
2516
|
currentFootnoteHeight,
|
|
1217
|
-
|
|
1218
|
-
currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0
|
|
2517
|
+
pageContinuation,
|
|
2518
|
+
currentPartialFootnotes.length > 0 ? currentPartialFootnotes : void 0,
|
|
2519
|
+
false,
|
|
2520
|
+
currentPageAdmitted
|
|
1219
2521
|
);
|
|
1220
2522
|
pages.push(page);
|
|
2523
|
+
precedingDisplayedPageNumber = ownedDisplayedPageNumber;
|
|
1221
2524
|
pageNumber++;
|
|
1222
|
-
pageInSection++;
|
|
1223
2525
|
currentContent = [];
|
|
1224
|
-
|
|
2526
|
+
pageSectionIndex = nextPageSectionIndex;
|
|
2527
|
+
if (!sectionStartPages.has(pageSectionIndex)) {
|
|
2528
|
+
sectionStartPages.set(pageSectionIndex, pageNumber);
|
|
2529
|
+
}
|
|
2530
|
+
const newBands = this.getPageBands(
|
|
2531
|
+
dimensionsFor(),
|
|
2532
|
+
pageSectionIndex,
|
|
2533
|
+
pageInSection(),
|
|
2534
|
+
displayedPageNumber()
|
|
2535
|
+
);
|
|
1225
2536
|
effectiveContentHeight = newBands.bodyHeight;
|
|
1226
2537
|
remainingHeight = effectiveContentHeight;
|
|
1227
2538
|
prevMarginBottomPt = 0;
|
|
1228
2539
|
currentFootnoteIds = [];
|
|
2540
|
+
currentPageHasFootnoteReference = false;
|
|
1229
2541
|
currentPartialFootnotes = [];
|
|
1230
2542
|
currentContinuation = nextPageContinuation;
|
|
1231
2543
|
nextPageContinuation = null;
|
|
2544
|
+
currentContinuationPartitioned = false;
|
|
2545
|
+
currentPageAdmitted = false;
|
|
1232
2546
|
if (deferredFootnoteIds.length > 0) {
|
|
1233
2547
|
currentFootnoteIds = [...deferredFootnoteIds];
|
|
1234
2548
|
deferredFootnoteIds = [];
|
|
1235
2549
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
} else {
|
|
1239
|
-
currentFootnoteHeight = 0;
|
|
1240
|
-
}
|
|
1241
|
-
if (currentFootnoteIds.length > 0) {
|
|
1242
|
-
currentFootnoteHeight += this.measureFootnotesHeight(
|
|
1243
|
-
currentFootnoteIds,
|
|
1244
|
-
dims.contentWidth,
|
|
1245
|
-
null
|
|
1246
|
-
);
|
|
1247
|
-
}
|
|
2550
|
+
const nextDimensions = dimensionsFor();
|
|
2551
|
+
currentFootnoteHeight = currentContinuation ? 0 : this.measureFootnotesHeight(currentFootnoteIds, nextDimensions.contentWidth);
|
|
1248
2552
|
};
|
|
2553
|
+
const pageHasPayload = () => currentContent.length > 0 || currentFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0;
|
|
1249
2554
|
for (let i = 0; i < blocks.length; i++) {
|
|
2555
|
+
this.checkpoint();
|
|
1250
2556
|
const block = blocks[i];
|
|
2557
|
+
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
2558
|
+
if (block.sectionIndex !== pageSectionIndex && currentPageHasFootnoteReference && (!this.footnoteLayoutLikeWord8 || allBlockFootnoteIds.length > 0)) {
|
|
2559
|
+
finishPage(block.sectionIndex);
|
|
2560
|
+
}
|
|
2561
|
+
const pageIsEmpty = currentContent.length === 0 && currentFootnoteIds.length === 0 && (currentContinuation?.remainingElements.length ?? 0) === 0;
|
|
2562
|
+
if (pageIsEmpty && block.sectionIndex !== pageSectionIndex) {
|
|
2563
|
+
adoptEmptyPageOwner(block.sectionIndex);
|
|
2564
|
+
}
|
|
2565
|
+
prepareCurrentContinuation();
|
|
2566
|
+
if (currentFootnoteHeight > 0) {
|
|
2567
|
+
const ownerDimensions = dimensionsFor();
|
|
2568
|
+
const ownerBands = this.getPageBands(
|
|
2569
|
+
ownerDimensions,
|
|
2570
|
+
pageSectionIndex,
|
|
2571
|
+
pageInSection(),
|
|
2572
|
+
displayedPageNumber()
|
|
2573
|
+
);
|
|
2574
|
+
const ownerMaxFootnoteArea = ownerBands.bodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2575
|
+
if (currentFootnoteHeight > ownerMaxFootnoteArea) {
|
|
2576
|
+
const fittingIds = [];
|
|
2577
|
+
let fittingHeight = 0;
|
|
2578
|
+
if ((currentContinuation?.remainingElements.length ?? 0) === 0 && currentPartialFootnotes.length === 0) {
|
|
2579
|
+
for (const footnoteId of currentFootnoteIds) {
|
|
2580
|
+
this.checkpoint();
|
|
2581
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2582
|
+
[...fittingIds, footnoteId],
|
|
2583
|
+
ownerDimensions.contentWidth
|
|
2584
|
+
);
|
|
2585
|
+
if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT > ownerMaxFootnoteArea) break;
|
|
2586
|
+
fittingIds.push(footnoteId);
|
|
2587
|
+
fittingHeight = candidateHeight;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
if (fittingIds.length > 0) {
|
|
2591
|
+
deferredFootnoteIds = [
|
|
2592
|
+
...currentFootnoteIds.slice(fittingIds.length),
|
|
2593
|
+
...deferredFootnoteIds
|
|
2594
|
+
];
|
|
2595
|
+
currentFootnoteIds = fittingIds;
|
|
2596
|
+
currentFootnoteHeight = fittingHeight;
|
|
2597
|
+
} else {
|
|
2598
|
+
finishPage(block.sectionIndex);
|
|
2599
|
+
i--;
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const blockDimensions = dimensionsFor(block.sectionIndex);
|
|
2605
|
+
const nextBlockPageInSection = pageInSection(block.sectionIndex, pageNumber + 1);
|
|
2606
|
+
const freshBlockPageBodyHeight = this.getPageBands(
|
|
2607
|
+
blockDimensions,
|
|
2608
|
+
block.sectionIndex,
|
|
2609
|
+
nextBlockPageInSection,
|
|
2610
|
+
displayedPageNumber(block.sectionIndex, nextBlockPageInSection, pageNumber + 1)
|
|
2611
|
+
).bodyHeight;
|
|
1251
2612
|
if (block.isPageBreak) {
|
|
1252
|
-
finishPage();
|
|
2613
|
+
finishPage(block.sectionIndex);
|
|
1253
2614
|
continue;
|
|
1254
2615
|
}
|
|
1255
2616
|
if (block.pageBreakBefore && currentContent.length > 0) {
|
|
1256
|
-
finishPage();
|
|
2617
|
+
finishPage(block.sectionIndex);
|
|
1257
2618
|
}
|
|
1258
2619
|
const previousBlock = blocks[i - 1];
|
|
1259
2620
|
const startsKeepChain = !previousBlock || !previousBlock.keepWithNext || previousBlock.isPageBreak || block.pageBreakBefore;
|
|
@@ -1269,7 +2630,7 @@ var DocxodusEditor = (() => {
|
|
|
1269
2630
|
if (newChainFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1270
2631
|
const totalChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1271
2632
|
[...currentFootnoteIds, ...newChainFootnoteIds],
|
|
1272
|
-
|
|
2633
|
+
blockDimensions.contentWidth,
|
|
1273
2634
|
currentContinuation
|
|
1274
2635
|
);
|
|
1275
2636
|
additionalChainFootnoteHeight = Math.max(
|
|
@@ -1280,58 +2641,69 @@ var DocxodusEditor = (() => {
|
|
|
1280
2641
|
const currentChainHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1281
2642
|
keepChain,
|
|
1282
2643
|
prevMarginBottomPt,
|
|
1283
|
-
currentContent.length === 0
|
|
2644
|
+
currentContent.length === 0,
|
|
2645
|
+
pageInSection(block.sectionIndex)
|
|
1284
2646
|
) + additionalChainFootnoteHeight;
|
|
1285
2647
|
const currentAvailableHeight = remainingHeight - currentFootnoteHeight;
|
|
1286
2648
|
if (currentChainHeight > currentAvailableHeight) {
|
|
1287
2649
|
const nextPageBands = this.getPageBands(
|
|
1288
|
-
|
|
1289
|
-
sectionIndex,
|
|
1290
|
-
pageInSection + 1,
|
|
1291
|
-
|
|
2650
|
+
dimensionsFor(block.sectionIndex),
|
|
2651
|
+
block.sectionIndex,
|
|
2652
|
+
pageInSection(block.sectionIndex, pageNumber + 1),
|
|
2653
|
+
displayedPageNumber(
|
|
2654
|
+
block.sectionIndex,
|
|
2655
|
+
pageInSection(block.sectionIndex, pageNumber + 1),
|
|
2656
|
+
pageNumber + 1
|
|
2657
|
+
)
|
|
1292
2658
|
);
|
|
1293
2659
|
const freshChainBodyHeight = this.measureKeepWithNextChainBodyHeight(
|
|
1294
2660
|
keepChain,
|
|
1295
2661
|
0,
|
|
1296
|
-
true
|
|
2662
|
+
true,
|
|
2663
|
+
pageInSection(block.sectionIndex, pageNumber + 1)
|
|
1297
2664
|
);
|
|
1298
2665
|
const freshChainFootnoteHeight = this.measureFootnotesHeight(
|
|
1299
2666
|
newChainFootnoteIds,
|
|
1300
|
-
|
|
2667
|
+
blockDimensions.contentWidth,
|
|
1301
2668
|
nextPageContinuation
|
|
1302
2669
|
);
|
|
1303
2670
|
if (freshChainBodyHeight + freshChainFootnoteHeight <= nextPageBands.bodyHeight) {
|
|
1304
|
-
finishPage();
|
|
2671
|
+
finishPage(block.sectionIndex);
|
|
1305
2672
|
}
|
|
1306
2673
|
}
|
|
1307
2674
|
}
|
|
1308
2675
|
}
|
|
1309
|
-
const allBlockFootnoteIds = this.extractFootnoteRefs(block.element);
|
|
1310
2676
|
const newFootnoteIds = this.collectNewFootnoteIds([block], currentFootnoteIds);
|
|
2677
|
+
let combinedFootnoteHeight = currentFootnoteHeight;
|
|
1311
2678
|
let additionalFootnoteHeight = 0;
|
|
1312
2679
|
if (newFootnoteIds.length > 0 && this.footnoteRegistry.size > 0) {
|
|
1313
2680
|
const combinedFootnoteIds = [...currentFootnoteIds, ...newFootnoteIds];
|
|
1314
|
-
|
|
2681
|
+
combinedFootnoteHeight = this.measureFootnotesHeight(
|
|
1315
2682
|
combinedFootnoteIds,
|
|
1316
|
-
|
|
1317
|
-
currentContinuation
|
|
2683
|
+
blockDimensions.contentWidth,
|
|
2684
|
+
currentContinuation,
|
|
2685
|
+
currentPartialFootnotes
|
|
2686
|
+
);
|
|
2687
|
+
additionalFootnoteHeight = Math.max(
|
|
2688
|
+
0,
|
|
2689
|
+
combinedFootnoteHeight - currentFootnoteHeight
|
|
1318
2690
|
);
|
|
1319
|
-
additionalFootnoteHeight = totalFootnoteHeight - currentFootnoteHeight;
|
|
1320
2691
|
}
|
|
1321
2692
|
const isFirstOnPage = currentContent.length === 0;
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
2693
|
+
const effectiveMarginTop = this.effectiveBlockMarginTop(
|
|
2694
|
+
block,
|
|
2695
|
+
prevMarginBottomPt,
|
|
2696
|
+
isFirstOnPage,
|
|
2697
|
+
pageInSection(block.sectionIndex)
|
|
2698
|
+
);
|
|
1326
2699
|
const blockSpace = effectiveMarginTop + block.heightPt + additionalFootnoteHeight;
|
|
1327
2700
|
const effectiveRemainingHeight = remainingHeight - currentFootnoteHeight;
|
|
1328
2701
|
const bodyContentUsed = effectiveContentHeight - remainingHeight;
|
|
1329
2702
|
const maxFootnoteArea = effectiveContentHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
1330
|
-
const maxFootnoteExpansion = Math.max(0, maxFootnoteArea - currentFootnoteHeight);
|
|
1331
2703
|
if (blockSpace > effectiveRemainingHeight) {
|
|
1332
2704
|
const paragraphFragments = this.tryFragmentParagraph(
|
|
1333
2705
|
block,
|
|
1334
|
-
|
|
2706
|
+
blockDimensions,
|
|
1335
2707
|
effectiveRemainingHeight,
|
|
1336
2708
|
effectiveMarginTop
|
|
1337
2709
|
);
|
|
@@ -1341,40 +2713,74 @@ var DocxodusEditor = (() => {
|
|
|
1341
2713
|
continue;
|
|
1342
2714
|
}
|
|
1343
2715
|
}
|
|
1344
|
-
if (blockSpace <= effectiveRemainingHeight) {
|
|
1345
|
-
|
|
2716
|
+
if (blockSpace <= effectiveRemainingHeight && (combinedFootnoteHeight === 0 || combinedFootnoteHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= maxFootnoteArea)) {
|
|
2717
|
+
markSectionPlaced(block.sectionIndex);
|
|
2718
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2719
|
+
block,
|
|
2720
|
+
isFirstOnPage,
|
|
2721
|
+
pageInSection(block.sectionIndex)
|
|
2722
|
+
));
|
|
1346
2723
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
1347
2724
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1348
2725
|
if (newFootnoteIds.length > 0) {
|
|
1349
2726
|
currentFootnoteIds.push(...newFootnoteIds);
|
|
1350
|
-
currentFootnoteHeight
|
|
2727
|
+
currentFootnoteHeight = combinedFootnoteHeight;
|
|
1351
2728
|
}
|
|
1352
|
-
|
|
2729
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
2730
|
+
} else if (block.heightPt + this.effectiveBlockMarginTop(
|
|
2731
|
+
block,
|
|
2732
|
+
0,
|
|
2733
|
+
true,
|
|
2734
|
+
pageInSection(block.sectionIndex, pageNumber + 1)
|
|
2735
|
+
) <= freshBlockPageBodyHeight) {
|
|
1353
2736
|
const blockSpaceWithoutFootnotes = effectiveMarginTop + block.heightPt;
|
|
1354
|
-
const minBodySpaceNeeded = bodyContentUsed + blockSpaceWithoutFootnotes + MIN_BODY_CONTENT_HEIGHT;
|
|
1355
|
-
const canExpandFootnotes = minBodySpaceNeeded <= effectiveContentHeight;
|
|
1356
2737
|
if (newFootnoteIds.length > 0 && blockSpaceWithoutFootnotes <= effectiveRemainingHeight) {
|
|
1357
|
-
|
|
2738
|
+
markSectionPlaced(block.sectionIndex);
|
|
2739
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2740
|
+
block,
|
|
2741
|
+
isFirstOnPage,
|
|
2742
|
+
pageInSection(block.sectionIndex)
|
|
2743
|
+
));
|
|
1358
2744
|
remainingHeight -= effectiveMarginTop + block.heightPt + block.marginBottomPt;
|
|
1359
2745
|
prevMarginBottomPt = block.marginBottomPt;
|
|
1360
2746
|
const availableForFootnotes = Math.min(
|
|
1361
2747
|
maxFootnoteArea,
|
|
1362
2748
|
effectiveContentHeight - bodyContentUsed - blockSpaceWithoutFootnotes
|
|
1363
2749
|
);
|
|
1364
|
-
|
|
2750
|
+
const nextPageMaxFootnoteArea = freshBlockPageBodyHeight * MAX_FOOTNOTE_AREA_RATIO;
|
|
2751
|
+
const deferralCannotHelp = availableForFootnotes >= nextPageMaxFootnoteArea;
|
|
2752
|
+
for (let noteIndex = 0; noteIndex < newFootnoteIds.length; noteIndex++) {
|
|
2753
|
+
this.checkpoint();
|
|
2754
|
+
const footnoteId = newFootnoteIds[noteIndex];
|
|
1365
2755
|
const footnote = this.footnoteRegistry.get(footnoteId);
|
|
1366
2756
|
if (!footnote) continue;
|
|
1367
|
-
|
|
2757
|
+
if (nextPageContinuation) {
|
|
2758
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2759
|
+
break;
|
|
2760
|
+
}
|
|
2761
|
+
const candidateIds = [...currentFootnoteIds, footnoteId];
|
|
2762
|
+
const candidateHeight = this.measureFootnotesHeight(
|
|
2763
|
+
candidateIds,
|
|
2764
|
+
blockDimensions.contentWidth,
|
|
2765
|
+
currentContinuation,
|
|
2766
|
+
currentPartialFootnotes
|
|
2767
|
+
);
|
|
1368
2768
|
const spaceLeftForFootnotes = availableForFootnotes - currentFootnoteHeight;
|
|
1369
|
-
if (
|
|
2769
|
+
if (candidateHeight + FOOTNOTE_MEASUREMENT_GUARD_PT <= availableForFootnotes) {
|
|
1370
2770
|
currentFootnoteIds.push(footnoteId);
|
|
1371
|
-
currentFootnoteHeight
|
|
2771
|
+
currentFootnoteHeight = candidateHeight;
|
|
1372
2772
|
} else {
|
|
1373
2773
|
if (spaceLeftForFootnotes > 20) {
|
|
1374
2774
|
const { fits, overflow } = this.splitFootnoteToFit(
|
|
1375
2775
|
footnote,
|
|
1376
|
-
|
|
1377
|
-
|
|
2776
|
+
availableForFootnotes,
|
|
2777
|
+
blockDimensions.contentWidth,
|
|
2778
|
+
deferralCannotHelp,
|
|
2779
|
+
{
|
|
2780
|
+
footnoteIds: currentFootnoteIds,
|
|
2781
|
+
continuation: currentContinuation,
|
|
2782
|
+
partialFootnotes: currentPartialFootnotes
|
|
2783
|
+
}
|
|
1378
2784
|
);
|
|
1379
2785
|
if (fits.length > 0) {
|
|
1380
2786
|
currentFootnoteIds.push(footnoteId);
|
|
@@ -1385,66 +2791,70 @@ var DocxodusEditor = (() => {
|
|
|
1385
2791
|
if (overflow.length > 0) {
|
|
1386
2792
|
nextPageContinuation = {
|
|
1387
2793
|
footnoteId,
|
|
2794
|
+
sourceAnchorId: footnote.dataset.sourceAnchorId,
|
|
1388
2795
|
remainingElements: overflow
|
|
1389
2796
|
};
|
|
1390
2797
|
}
|
|
1391
|
-
currentFootnoteHeight =
|
|
2798
|
+
currentFootnoteHeight = this.measureFootnotesHeight(
|
|
2799
|
+
currentFootnoteIds,
|
|
2800
|
+
blockDimensions.contentWidth,
|
|
2801
|
+
currentContinuation,
|
|
2802
|
+
currentPartialFootnotes
|
|
2803
|
+
);
|
|
2804
|
+
if (overflow.length > 0) {
|
|
2805
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex + 1));
|
|
2806
|
+
break;
|
|
2807
|
+
}
|
|
1392
2808
|
} else {
|
|
1393
|
-
deferredFootnoteIds.push(
|
|
2809
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2810
|
+
break;
|
|
1394
2811
|
}
|
|
1395
2812
|
} else {
|
|
1396
|
-
deferredFootnoteIds.push(
|
|
2813
|
+
deferredFootnoteIds.push(...newFootnoteIds.slice(noteIndex));
|
|
2814
|
+
break;
|
|
1397
2815
|
}
|
|
1398
2816
|
}
|
|
1399
2817
|
}
|
|
1400
|
-
|
|
1401
|
-
const expandedFootnoteSpace = Math.min(maxFootnoteArea, additionalFootnoteHeight + currentFootnoteHeight);
|
|
1402
|
-
const bodySpaceAfterExpansion = effectiveContentHeight - expandedFootnoteSpace;
|
|
1403
|
-
if (blockSpaceWithoutFootnotes <= bodySpaceAfterExpansion - bodyContentUsed) {
|
|
1404
|
-
currentContent.push(block.element.cloneNode(true));
|
|
1405
|
-
remainingHeight = bodySpaceAfterExpansion - bodyContentUsed - blockSpaceWithoutFootnotes;
|
|
1406
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1407
|
-
currentFootnoteIds.push(...newFootnoteIds);
|
|
1408
|
-
currentFootnoteHeight = expandedFootnoteSpace;
|
|
1409
|
-
} else {
|
|
1410
|
-
finishPage();
|
|
1411
|
-
const newPageFootnoteHeight = allBlockFootnoteIds.length > 0 ? this.measureFootnotesHeight(allBlockFootnoteIds, dims.contentWidth, currentContinuation) : currentContinuation ? this.measureContinuationHeight(currentContinuation, dims.contentWidth) : 0;
|
|
1412
|
-
const newPageSpace = block.marginTopPt + block.heightPt + block.marginBottomPt;
|
|
1413
|
-
currentContent.push(block.element.cloneNode(true));
|
|
1414
|
-
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1415
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1416
|
-
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1417
|
-
currentFootnoteHeight = newPageFootnoteHeight;
|
|
1418
|
-
}
|
|
2818
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
1419
2819
|
} else {
|
|
1420
|
-
finishPage();
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
currentContent.push(block.element.cloneNode(true));
|
|
1424
|
-
remainingHeight = effectiveContentHeight - newPageSpace;
|
|
1425
|
-
prevMarginBottomPt = block.marginBottomPt;
|
|
1426
|
-
currentFootnoteIds = [...currentFootnoteIds, ...allBlockFootnoteIds];
|
|
1427
|
-
currentFootnoteHeight = newPageFootnoteHeight;
|
|
2820
|
+
finishPage(block.sectionIndex, !pageHasPayload());
|
|
2821
|
+
i--;
|
|
2822
|
+
continue;
|
|
1428
2823
|
}
|
|
1429
2824
|
} else {
|
|
1430
|
-
const tableFragments = this.trySplitSimpleOversizedTable(
|
|
2825
|
+
const tableFragments = this.trySplitSimpleOversizedTable(
|
|
2826
|
+
block,
|
|
2827
|
+
dimensionsFor(block.sectionIndex),
|
|
2828
|
+
block.sectionIndex
|
|
2829
|
+
);
|
|
1431
2830
|
if (tableFragments) {
|
|
1432
2831
|
if (currentContent.length > 0 || currentContinuation) {
|
|
1433
|
-
finishPage();
|
|
2832
|
+
finishPage(block.sectionIndex);
|
|
1434
2833
|
}
|
|
1435
2834
|
blocks.splice(i, 1, ...tableFragments);
|
|
1436
2835
|
i--;
|
|
1437
2836
|
continue;
|
|
1438
2837
|
}
|
|
1439
|
-
if (
|
|
1440
|
-
finishPage();
|
|
2838
|
+
if (pageHasPayload()) {
|
|
2839
|
+
finishPage(block.sectionIndex);
|
|
2840
|
+
i--;
|
|
2841
|
+
continue;
|
|
1441
2842
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
2843
|
+
markSectionPlaced(block.sectionIndex);
|
|
2844
|
+
currentContent.push(this.cloneBlockForPage(
|
|
2845
|
+
block,
|
|
2846
|
+
true,
|
|
2847
|
+
pageInSection(block.sectionIndex)
|
|
2848
|
+
));
|
|
2849
|
+
deferredFootnoteIds.push(...newFootnoteIds);
|
|
2850
|
+
currentPageHasFootnoteReference ||= allBlockFootnoteIds.length > 0;
|
|
2851
|
+
finishPage(block.sectionIndex);
|
|
1445
2852
|
}
|
|
1446
2853
|
}
|
|
1447
2854
|
finishPage();
|
|
2855
|
+
while (currentFootnoteIds.length > 0 || deferredFootnoteIds.length > 0 || (currentContinuation?.remainingElements.length ?? 0) > 0) {
|
|
2856
|
+
finishPage();
|
|
2857
|
+
}
|
|
1448
2858
|
this.pendingFootnoteContinuation = nextPageContinuation;
|
|
1449
2859
|
return pages;
|
|
1450
2860
|
}
|
|
@@ -1469,6 +2879,19 @@ var DocxodusEditor = (() => {
|
|
|
1469
2879
|
if (el.getAttribute("contenteditable") !== null) el.setAttribute("contenteditable", "false");
|
|
1470
2880
|
}
|
|
1471
2881
|
}
|
|
2882
|
+
/** A repeated margin note is presentation, not a second bookmark/link target. */
|
|
2883
|
+
makeClonedMarginCommentInert(root) {
|
|
2884
|
+
this.makeClonedStoryInert(root);
|
|
2885
|
+
const nodes = [root, ...Array.from(root.querySelectorAll("*"))];
|
|
2886
|
+
for (const element of nodes) {
|
|
2887
|
+
element.removeAttribute("id");
|
|
2888
|
+
if (element.localName === "a" && element.getAttribute("href")?.startsWith("#")) {
|
|
2889
|
+
element.removeAttribute("href");
|
|
2890
|
+
element.setAttribute("aria-disabled", "true");
|
|
2891
|
+
element.tabIndex = -1;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
1472
2895
|
/**
|
|
1473
2896
|
* Resolves floating DrawingML objects after their anchor paragraphs have landed on a page.
|
|
1474
2897
|
*
|
|
@@ -1494,7 +2917,7 @@ var DocxodusEditor = (() => {
|
|
|
1494
2917
|
const context = {
|
|
1495
2918
|
staticLeft: toPageX(staticRect.left),
|
|
1496
2919
|
staticTop: toPageY(staticRect.top),
|
|
1497
|
-
lineHeight: paragraph ? (parseFloat(getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
|
|
2920
|
+
lineHeight: paragraph ? (parseFloat(this.view.getComputedStyle(paragraph).lineHeight) || staticRect.height) / pixelsPerPoint : staticRect.height / pixelsPerPoint,
|
|
1498
2921
|
paragraphLeft: toPageX(paragraphRect.left),
|
|
1499
2922
|
paragraphTop: toPageY(paragraphRect.top),
|
|
1500
2923
|
paragraphWidth: paragraphRect.width / pixelsPerPoint,
|
|
@@ -1624,36 +3047,48 @@ var DocxodusEditor = (() => {
|
|
|
1624
3047
|
else if (alignment === "outside") fraction = pageNumber % 2 === 1 ? 1 : 0;
|
|
1625
3048
|
return reference.start + (reference.size - objectSize) * fraction;
|
|
1626
3049
|
}
|
|
3050
|
+
/** Charge a physical page before any page-owned partitioning or DOM allocation. */
|
|
3051
|
+
admitPageAllocation() {
|
|
3052
|
+
const prospectivePageCount = this.createdPageCount + 1;
|
|
3053
|
+
this.pageCountCheckpoint?.(prospectivePageCount);
|
|
3054
|
+
this.createdPageCount = prospectivePageCount;
|
|
3055
|
+
}
|
|
1627
3056
|
/**
|
|
1628
3057
|
* Creates a page container element.
|
|
1629
3058
|
*/
|
|
1630
|
-
createPage(dims, pageNumber, sectionIndex, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes) {
|
|
1631
|
-
|
|
3059
|
+
createPage(dims, pageNumber, sectionIndex, displayedPageNumber, content, pageInSection, footnoteIds = [], footnoteHeight = 0, continuation, partialFootnotes, isSectionFiller = false, pageAlreadyAdmitted = false) {
|
|
3060
|
+
if (!pageAlreadyAdmitted) this.admitPageAllocation();
|
|
3061
|
+
const pageBox = this.document.createElement("div");
|
|
1632
3062
|
pageBox.className = `${this.cssPrefix}box`;
|
|
1633
3063
|
pageBox.style.width = `${dims.pageWidth}pt`;
|
|
1634
3064
|
pageBox.style.height = `${dims.pageHeight}pt`;
|
|
1635
3065
|
pageBox.style.overflow = "hidden";
|
|
1636
3066
|
pageBox.style.position = "relative";
|
|
1637
3067
|
if (this.scale !== 1) {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
3068
|
+
if (this.view.CSS?.supports("zoom", "1")) {
|
|
3069
|
+
pageBox.style.zoom = String(this.scale);
|
|
3070
|
+
} else {
|
|
3071
|
+
pageBox.style.transform = `scale(${this.scale})`;
|
|
3072
|
+
pageBox.style.transformOrigin = "top left";
|
|
3073
|
+
const heightReductionPt = dims.pageHeight * (1 - this.scale);
|
|
3074
|
+
const widthReductionPt = dims.pageWidth * (1 - this.scale);
|
|
3075
|
+
const heightReductionPx = ptToPx(heightReductionPt);
|
|
3076
|
+
const widthReductionPx = ptToPx(widthReductionPt);
|
|
3077
|
+
pageBox.style.marginRight = `-${widthReductionPx}px`;
|
|
3078
|
+
pageBox.style.marginBottom = `${this.pageGap - heightReductionPx}px`;
|
|
3079
|
+
}
|
|
1647
3080
|
}
|
|
1648
3081
|
pageBox.style.willChange = "transform";
|
|
1649
3082
|
pageBox.style.contain = "layout paint";
|
|
1650
3083
|
pageBox.dataset.pageNumber = String(pageNumber);
|
|
1651
3084
|
pageBox.dataset.sectionIndex = String(sectionIndex);
|
|
3085
|
+
pageBox.dataset.displayedPageNumber = String(displayedPageNumber);
|
|
3086
|
+
if (isSectionFiller) pageBox.dataset.sectionFiller = "true";
|
|
1652
3087
|
pageBox.dataset.pageInSection = String(pageInSection);
|
|
1653
|
-
const bands = this.getPageBands(dims, sectionIndex, pageInSection,
|
|
1654
|
-
const headerSource = this.selectHeader(sectionIndex, pageInSection,
|
|
3088
|
+
const bands = this.getPageBands(dims, sectionIndex, pageInSection, displayedPageNumber);
|
|
3089
|
+
const headerSource = isSectionFiller ? void 0 : this.selectHeader(sectionIndex, pageInSection, displayedPageNumber);
|
|
1655
3090
|
if (headerSource) {
|
|
1656
|
-
const headerDiv = document.createElement("div");
|
|
3091
|
+
const headerDiv = this.document.createElement("div");
|
|
1657
3092
|
headerDiv.className = `${this.cssPrefix}header`;
|
|
1658
3093
|
headerDiv.style.position = "absolute";
|
|
1659
3094
|
headerDiv.style.top = `${bands.headerTop}pt`;
|
|
@@ -1675,7 +3110,7 @@ var DocxodusEditor = (() => {
|
|
|
1675
3110
|
}
|
|
1676
3111
|
const contentAreaTop = bands.bodyTop;
|
|
1677
3112
|
const contentAreaHeight = bands.bodyHeight;
|
|
1678
|
-
const contentArea = document.createElement("div");
|
|
3113
|
+
const contentArea = this.document.createElement("div");
|
|
1679
3114
|
contentArea.className = `${this.cssPrefix}content`;
|
|
1680
3115
|
contentArea.style.position = "absolute";
|
|
1681
3116
|
contentArea.style.top = `${contentAreaTop}pt`;
|
|
@@ -1688,12 +3123,46 @@ var DocxodusEditor = (() => {
|
|
|
1688
3123
|
}
|
|
1689
3124
|
pageBox.appendChild(contentArea);
|
|
1690
3125
|
const hasContinuation = continuation && continuation.remainingElements.length > 0;
|
|
1691
|
-
if (footnoteIds.length > 0 || hasContinuation) {
|
|
3126
|
+
if (!isSectionFiller && (footnoteIds.length > 0 || hasContinuation)) {
|
|
1692
3127
|
this.addPageFootnotes(pageBox, footnoteIds, dims, bands, footnoteHeight, continuation, partialFootnotes);
|
|
1693
3128
|
}
|
|
1694
|
-
const
|
|
3129
|
+
const pageCommentIds = [];
|
|
3130
|
+
for (const marker of Array.from(
|
|
3131
|
+
pageBox.querySelectorAll("[data-comment-id]")
|
|
3132
|
+
)) {
|
|
3133
|
+
if (marker.closest(`.${this.cssPrefix}comment-margin`)) continue;
|
|
3134
|
+
const id = marker.dataset.commentId;
|
|
3135
|
+
if (id && this.commentMarginRegistry.has(id) && !pageCommentIds.includes(id)) {
|
|
3136
|
+
pageCommentIds.push(id);
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
if (pageCommentIds.length > 0) {
|
|
3140
|
+
const marginColumn = this.document.createElement("aside");
|
|
3141
|
+
marginColumn.className = `${this.cssPrefix}comment-margin`;
|
|
3142
|
+
marginColumn.style.position = "absolute";
|
|
3143
|
+
marginColumn.style.top = `${contentAreaTop}pt`;
|
|
3144
|
+
marginColumn.style.left = `${dims.marginLeft + dims.contentWidth + 3}pt`;
|
|
3145
|
+
marginColumn.style.width = `${Math.max(12, dims.marginRight - 6)}pt`;
|
|
3146
|
+
marginColumn.style.maxHeight = `${contentAreaHeight}pt`;
|
|
3147
|
+
marginColumn.style.overflow = "hidden";
|
|
3148
|
+
marginColumn.style.boxSizing = "border-box";
|
|
3149
|
+
const placedThreadRoots = /* @__PURE__ */ new Set();
|
|
3150
|
+
for (const id of pageCommentIds) {
|
|
3151
|
+
const source = this.commentMarginRegistry.get(id);
|
|
3152
|
+
if (source) {
|
|
3153
|
+
const rootId = source.dataset.commentId ?? id;
|
|
3154
|
+
if (placedThreadRoots.has(rootId)) continue;
|
|
3155
|
+
placedThreadRoots.add(rootId);
|
|
3156
|
+
const clone = source.cloneNode(true);
|
|
3157
|
+
this.makeClonedMarginCommentInert(clone);
|
|
3158
|
+
marginColumn.appendChild(clone);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
pageBox.appendChild(marginColumn);
|
|
3162
|
+
}
|
|
3163
|
+
const footerSource = isSectionFiller ? void 0 : this.selectFooter(sectionIndex, pageInSection, displayedPageNumber);
|
|
1695
3164
|
if (footerSource) {
|
|
1696
|
-
const footerDiv = document.createElement("div");
|
|
3165
|
+
const footerDiv = this.document.createElement("div");
|
|
1697
3166
|
footerDiv.className = `${this.cssPrefix}footer`;
|
|
1698
3167
|
footerDiv.style.position = "absolute";
|
|
1699
3168
|
footerDiv.style.top = "auto";
|
|
@@ -1713,8 +3182,8 @@ var DocxodusEditor = (() => {
|
|
|
1713
3182
|
}
|
|
1714
3183
|
pageBox.appendChild(footerDiv);
|
|
1715
3184
|
}
|
|
1716
|
-
if (this.showPageNumbers) {
|
|
1717
|
-
const pageNum = document.createElement("div");
|
|
3185
|
+
if (this.showPageNumbers && !isSectionFiller) {
|
|
3186
|
+
const pageNum = this.document.createElement("div");
|
|
1718
3187
|
pageNum.className = `${this.cssPrefix}number`;
|
|
1719
3188
|
pageNum.textContent = String(pageNumber);
|
|
1720
3189
|
pageBox.appendChild(pageNum);
|
|
@@ -1737,7 +3206,8 @@ var DocxodusEditor = (() => {
|
|
|
1737
3206
|
}
|
|
1738
3207
|
};
|
|
1739
3208
|
function paginateHtml(html, container, options = {}) {
|
|
1740
|
-
const
|
|
3209
|
+
const ownerDocument = typeof container === "string" ? globalThis.document : container.ownerDocument;
|
|
3210
|
+
const containerEl = typeof container === "string" ? ownerDocument.getElementById(container) : container;
|
|
1741
3211
|
if (!containerEl) {
|
|
1742
3212
|
throw new Error("Container element not found");
|
|
1743
3213
|
}
|
|
@@ -5039,6 +6509,13 @@ var DocxodusEditor = (() => {
|
|
|
5039
6509
|
function escapeInlineMarkdown(text) {
|
|
5040
6510
|
return text.replace(/([\\`*_[\]])/g, "\\$1");
|
|
5041
6511
|
}
|
|
6512
|
+
function escapeLeadingBlockMarkers(md) {
|
|
6513
|
+
return md.split("\n").map((line) => {
|
|
6514
|
+
if (/^[#>|]/.test(line) || /^[-+] /.test(line)) return "\\" + line;
|
|
6515
|
+
if (/^\d+\. /.test(line)) return line.replace(".", "\\.");
|
|
6516
|
+
return line;
|
|
6517
|
+
}).join("\n");
|
|
6518
|
+
}
|
|
5042
6519
|
function collectInlineSegments(node, out) {
|
|
5043
6520
|
node.childNodes.forEach((child) => {
|
|
5044
6521
|
if (isGeneratedChrome(child)) return;
|
|
@@ -5090,7 +6567,7 @@ var DocxodusEditor = (() => {
|
|
|
5090
6567
|
merged.push({ ...s });
|
|
5091
6568
|
}
|
|
5092
6569
|
}
|
|
5093
|
-
return merged.map(segToMarkdown).join("").trim();
|
|
6570
|
+
return escapeLeadingBlockMarkers(merged.map(segToMarkdown).join("").trim());
|
|
5094
6571
|
}
|
|
5095
6572
|
function isListBlock(block) {
|
|
5096
6573
|
return !!block.querySelector(":scope > [data-list-marker]");
|
|
@@ -7125,17 +8602,19 @@ var DocxodusEditor = (() => {
|
|
|
7125
8602
|
this.refreshAfter(block, idx, false);
|
|
7126
8603
|
}
|
|
7127
8604
|
// ─── Table row / column editing (active block must be inside a table cell) ──────────
|
|
7128
|
-
/** Run a table-structure op on the active cell
|
|
8605
|
+
/** Run a table-structure op on the active cell's canonical tc anchor and re-render. */
|
|
7129
8606
|
tableEdit(run) {
|
|
7130
8607
|
const block = this.activeBlock;
|
|
7131
|
-
|
|
8608
|
+
const cell = block?.closest("td, th");
|
|
8609
|
+
if (this.closed || !block || !cell || !block.closest("table")) return;
|
|
7132
8610
|
const unid = block.getAttribute("data-anchor");
|
|
7133
8611
|
if (!unid) return;
|
|
7134
|
-
|
|
7135
|
-
|
|
8612
|
+
const paragraphId = this.anchorIdOf(block);
|
|
8613
|
+
const cellId = this.anchorIdOf(cell);
|
|
8614
|
+
if (!paragraphId || !cellId) return;
|
|
7136
8615
|
const idx = this.blockIndex(block);
|
|
7137
|
-
|
|
7138
|
-
const res = this.parseEdit(run(
|
|
8616
|
+
this.syncBlock(block, paragraphId);
|
|
8617
|
+
const res = this.parseEdit(run(cellId));
|
|
7139
8618
|
if (!res.success) return;
|
|
7140
8619
|
this.refreshAfter(block, idx, false);
|
|
7141
8620
|
}
|
|
@@ -7288,6 +8767,59 @@ var DocxodusEditor = (() => {
|
|
|
7288
8767
|
if (!res.success) return;
|
|
7289
8768
|
this.reconcile(idx, false);
|
|
7290
8769
|
}
|
|
8770
|
+
/**
|
|
8771
|
+
* Add a native Word comment on the current selection in the active block — or on the whole
|
|
8772
|
+
* block when the selection is collapsed (a null span comments the paragraph). The definition,
|
|
8773
|
+
* threading parts, and body-side range markers are written by the session's `AddComment`
|
|
8774
|
+
* (issue #300); the re-render shows the marker chrome the converter already draws. The
|
|
8775
|
+
* annotation type legal review runs on, finally authorable from the shipped surface
|
|
8776
|
+
* (issue #580).
|
|
8777
|
+
*/
|
|
8778
|
+
addComment(markdown = "New comment.", author = "Reviewer") {
|
|
8779
|
+
const block = this.activeBlock;
|
|
8780
|
+
if (this.closed || !block) return;
|
|
8781
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8782
|
+
if (!bridge.AddComment) return;
|
|
8783
|
+
let fullId = this.anchorIdOf(block);
|
|
8784
|
+
if (!fullId) return;
|
|
8785
|
+
const idx = this.blockIndex(block);
|
|
8786
|
+
const span = selectionSpanIn(block);
|
|
8787
|
+
fullId = this.syncBlock(block, fullId);
|
|
8788
|
+
const res = this.parseEdit(
|
|
8789
|
+
bridge.AddComment(
|
|
8790
|
+
this.handle,
|
|
8791
|
+
fullId,
|
|
8792
|
+
span ? JSON.stringify(span) : "",
|
|
8793
|
+
author,
|
|
8794
|
+
"",
|
|
8795
|
+
"",
|
|
8796
|
+
markdown
|
|
8797
|
+
)
|
|
8798
|
+
);
|
|
8799
|
+
if (!res.success) return;
|
|
8800
|
+
this.reconcile(idx, false);
|
|
8801
|
+
}
|
|
8802
|
+
/** The document's native comment threads (session truth), for review UIs — flat entries
|
|
8803
|
+
* with `parentAnchorId` linking replies and `resolved` carrying thread state. */
|
|
8804
|
+
listComments() {
|
|
8805
|
+
if (this.closed) return [];
|
|
8806
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8807
|
+
if (!bridge.ListComments) return [];
|
|
8808
|
+
try {
|
|
8809
|
+
return JSON.parse(bridge.ListComments(this.handle));
|
|
8810
|
+
} catch {
|
|
8811
|
+
return [];
|
|
8812
|
+
}
|
|
8813
|
+
}
|
|
8814
|
+
/** Resolve or reopen a comment thread by its `cmt` anchor id (from {@link listComments}).
|
|
8815
|
+
* Resolution is thread metadata (`commentsExtended`), not body markup, so no re-render is
|
|
8816
|
+
* needed — a review UI re-reads {@link listComments} for the new state. */
|
|
8817
|
+
setCommentResolved(commentAnchorId, resolved) {
|
|
8818
|
+
if (this.closed) return;
|
|
8819
|
+
const bridge = this.exports.DocxSessionBridge;
|
|
8820
|
+
if (!bridge.SetCommentResolved) return;
|
|
8821
|
+
this.parseEdit(bridge.SetCommentResolved(this.handle, commentAnchorId, resolved));
|
|
8822
|
+
}
|
|
7291
8823
|
applyParagraphFormat(op) {
|
|
7292
8824
|
const block = this.activeBlock;
|
|
7293
8825
|
if (this.closed || !block) return;
|
|
@@ -7815,8 +9347,8 @@ var DocxodusEditor = (() => {
|
|
|
7815
9347
|
const backref = document.createElement("a");
|
|
7816
9348
|
backref.setAttribute("class", `${prefix}-backref`);
|
|
7817
9349
|
backref.setAttribute("contenteditable", "false");
|
|
7818
|
-
backref.textContent = "\u21A9";
|
|
7819
|
-
last.append(
|
|
9350
|
+
backref.textContent = " \u21A9";
|
|
9351
|
+
last.append(backref);
|
|
7820
9352
|
}
|
|
7821
9353
|
li.querySelectorAll("[data-anchor]").forEach((b) => this.wireBlock(b));
|
|
7822
9354
|
return li;
|
|
@@ -7969,6 +9501,7 @@ var DocxodusEditor = (() => {
|
|
|
7969
9501
|
color: var(--dxr-ink);
|
|
7970
9502
|
background: var(--dxr-desk);
|
|
7971
9503
|
-webkit-text-size-adjust: 100%;
|
|
9504
|
+
text-size-adjust: 100%;
|
|
7972
9505
|
}
|
|
7973
9506
|
.dxr *, .dxr *::before, .dxr *::after { box-sizing: border-box; }
|
|
7974
9507
|
|
|
@@ -8372,6 +9905,25 @@ var DocxodusEditor = (() => {
|
|
|
8372
9905
|
box-shadow: 0 1px 3px rgba(15, 23, 42, .08), 0 12px 24px rgba(15, 23, 42, .06);
|
|
8373
9906
|
}
|
|
8374
9907
|
|
|
9908
|
+
/* \u2500\u2500 Review: comment threads \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
9909
|
+
.dxr-threads {
|
|
9910
|
+
display: flex; flex-direction: column; gap: 4px;
|
|
9911
|
+
max-height: 92px; overflow-y: auto; min-width: 260px; padding-right: 2px;
|
|
9912
|
+
}
|
|
9913
|
+
.dxr-thread {
|
|
9914
|
+
display: flex; align-items: center; gap: 6px;
|
|
9915
|
+
font-size: 11px; line-height: 1.3; padding: 3px 6px;
|
|
9916
|
+
border: 1px solid var(--dxr-rule); border-radius: 4px; background: var(--dxr-chrome-sunk);
|
|
9917
|
+
}
|
|
9918
|
+
.dxr-thread[data-resolved] { opacity: 0.55; }
|
|
9919
|
+
.dxr-thread[data-reply] { margin-left: 14px; }
|
|
9920
|
+
.dxr-thread .dxr-tauthor { font-weight: 600; white-space: nowrap; }
|
|
9921
|
+
.dxr-thread .dxr-ttext {
|
|
9922
|
+
flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px;
|
|
9923
|
+
}
|
|
9924
|
+
.dxr-thread button { flex: none; }
|
|
9925
|
+
.dxr-group.dxr-grow { flex: 1; min-width: 0; }
|
|
9926
|
+
|
|
8375
9927
|
/* \u2500\u2500 Table size picker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
8376
9928
|
.dxr-pop {
|
|
8377
9929
|
display: none;
|
|
@@ -8690,6 +10242,7 @@ var DocxodusEditor = (() => {
|
|
|
8690
10242
|
<button type="button" class="dxr-tab" role="tab" data-tab="home" aria-selected="true">Home</button>
|
|
8691
10243
|
<button type="button" class="dxr-tab" role="tab" data-tab="insert" aria-selected="false">Insert</button>
|
|
8692
10244
|
<button type="button" class="dxr-tab" role="tab" data-tab="layout" aria-selected="false">Layout</button>
|
|
10245
|
+
<button type="button" class="dxr-tab" role="tab" data-tab="review" aria-selected="false">Review</button>
|
|
8693
10246
|
<button type="button" class="dxr-tab" role="tab" data-tab="table" data-contextual aria-selected="false" hidden>Table</button>
|
|
8694
10247
|
</div>
|
|
8695
10248
|
|
|
@@ -8855,6 +10408,31 @@ var DocxodusEditor = (() => {
|
|
|
8855
10408
|
</div>
|
|
8856
10409
|
</div>
|
|
8857
10410
|
|
|
10411
|
+
<!-- REVIEW \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
10412
|
+
Comment authoring over the session's native comment family (issue #580):
|
|
10413
|
+
the button comments the selection (the whole block when collapsed), and
|
|
10414
|
+
the thread list is live session truth with resolve/reopen per thread. -->
|
|
10415
|
+
<div class="dxr-panel" data-panel="review">
|
|
10416
|
+
<div class="dxr-group">
|
|
10417
|
+
<span class="dxr-glabel">Comments</span>
|
|
10418
|
+
<div class="dxr-row">
|
|
10419
|
+
<input data-dxr="commenttext" type="text" placeholder="Comment text…"
|
|
10420
|
+
title="The comment body; markdown is honored" />
|
|
10421
|
+
<button type="button" data-dxr="comment"
|
|
10422
|
+
title="Comment on the selection, or on the whole block when nothing is selected">💬 Comment</button>
|
|
10423
|
+
</div>
|
|
10424
|
+
<div class="dxr-row">
|
|
10425
|
+
<span class="dxr-note">Comments the selected text; the whole block when the caret is collapsed.</span>
|
|
10426
|
+
</div>
|
|
10427
|
+
</div>
|
|
10428
|
+
<div class="dxr-group dxr-grow">
|
|
10429
|
+
<span class="dxr-glabel">Threads</span>
|
|
10430
|
+
<div class="dxr-threads" data-dxr="threads" aria-live="polite">
|
|
10431
|
+
<span class="dxr-note">No comments yet.</span>
|
|
10432
|
+
</div>
|
|
10433
|
+
</div>
|
|
10434
|
+
</div>
|
|
10435
|
+
|
|
8858
10436
|
<!-- TABLE (contextual) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
8859
10437
|
Row/column editing lives here rather than in a floating toolbar: a docked
|
|
8860
10438
|
contextual tab cannot overlap the cell you are editing. -->
|
|
@@ -9362,6 +10940,18 @@ var DocxodusEditor = (() => {
|
|
|
9362
10940
|
if (this.live && style.value) this.run("style", () => this.live.setParagraphStyle(style.value));
|
|
9363
10941
|
style.value = "";
|
|
9364
10942
|
});
|
|
10943
|
+
const commentButton = this.control("comment");
|
|
10944
|
+
if (commentButton) {
|
|
10945
|
+
this.keepSelection(commentButton);
|
|
10946
|
+
commentButton.addEventListener("click", () => {
|
|
10947
|
+
if (!this.live) return;
|
|
10948
|
+
const input = this.control("commenttext");
|
|
10949
|
+
const text = input?.value.trim() || "New comment.";
|
|
10950
|
+
this.run("comment", () => this.live.addComment(text, this.options.revisionAuthor ?? "Reviewer"));
|
|
10951
|
+
if (input) input.value = "";
|
|
10952
|
+
this.renderThreads();
|
|
10953
|
+
});
|
|
10954
|
+
}
|
|
9365
10955
|
this.wireFileActions();
|
|
9366
10956
|
this.wireLayout();
|
|
9367
10957
|
this.wirePicker();
|
|
@@ -9484,6 +11074,52 @@ var DocxodusEditor = (() => {
|
|
|
9484
11074
|
else panel.removeAttribute("data-active");
|
|
9485
11075
|
}
|
|
9486
11076
|
if (name === "layout") this.syncPageNumbering();
|
|
11077
|
+
if (name === "review") this.renderThreads();
|
|
11078
|
+
}
|
|
11079
|
+
/** Re-render the Review tab's thread list from session truth ({@link DocxEditor.listComments}):
|
|
11080
|
+
* one row per comment, replies indented under their thread root, resolve/reopen per root. */
|
|
11081
|
+
renderThreads() {
|
|
11082
|
+
const host = this.control("threads");
|
|
11083
|
+
if (!host || !this.live) return;
|
|
11084
|
+
const doc = this.element.ownerDocument ?? document;
|
|
11085
|
+
host.textContent = "";
|
|
11086
|
+
const comments = this.live.listComments();
|
|
11087
|
+
if (comments.length === 0) {
|
|
11088
|
+
const empty = doc.createElement("span");
|
|
11089
|
+
empty.className = "dxr-note";
|
|
11090
|
+
empty.textContent = "No comments yet.";
|
|
11091
|
+
host.appendChild(empty);
|
|
11092
|
+
return;
|
|
11093
|
+
}
|
|
11094
|
+
for (const entry of comments) {
|
|
11095
|
+
const row = doc.createElement("div");
|
|
11096
|
+
row.className = "dxr-thread";
|
|
11097
|
+
row.setAttribute("data-thread", entry.anchorId);
|
|
11098
|
+
if (entry.resolved) row.setAttribute("data-resolved", "");
|
|
11099
|
+
if (entry.parentAnchorId) row.setAttribute("data-reply", "");
|
|
11100
|
+
const author = doc.createElement("span");
|
|
11101
|
+
author.className = "dxr-tauthor";
|
|
11102
|
+
author.textContent = entry.author || "\u2014";
|
|
11103
|
+
row.appendChild(author);
|
|
11104
|
+
const text = doc.createElement("span");
|
|
11105
|
+
text.className = "dxr-ttext";
|
|
11106
|
+
text.textContent = entry.text;
|
|
11107
|
+
text.title = entry.text;
|
|
11108
|
+
row.appendChild(text);
|
|
11109
|
+
if (!entry.parentAnchorId) {
|
|
11110
|
+
const toggle = doc.createElement("button");
|
|
11111
|
+
toggle.type = "button";
|
|
11112
|
+
toggle.textContent = entry.resolved ? "Reopen" : "Resolve";
|
|
11113
|
+
toggle.title = entry.resolved ? "Reopen this comment thread" : "Mark this comment thread resolved";
|
|
11114
|
+
toggle.addEventListener("click", () => {
|
|
11115
|
+
if (!this.live) return;
|
|
11116
|
+
this.run(entry.resolved ? "reopen comment" : "resolve comment", () => this.live.setCommentResolved(entry.anchorId, !entry.resolved));
|
|
11117
|
+
this.renderThreads();
|
|
11118
|
+
});
|
|
11119
|
+
row.appendChild(toggle);
|
|
11120
|
+
}
|
|
11121
|
+
host.appendChild(row);
|
|
11122
|
+
}
|
|
9487
11123
|
}
|
|
9488
11124
|
// ── selection-driven state ──────────────────────────────────────────────────
|
|
9489
11125
|
selectionElement() {
|