postext 1.5.1 → 1.6.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/dist/__tests__/exports.test.js +4 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/pipeline/footnotes.test.d.ts +2 -0
- package/dist/__tests__/pipeline/footnotes.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/footnotes.test.js +203 -0
- package/dist/__tests__/pipeline/footnotes.test.js.map +1 -0
- package/dist/canvas-backend/decorations.d.ts +3 -1
- package/dist/canvas-backend/decorations.d.ts.map +1 -1
- package/dist/canvas-backend/decorations.js +19 -3
- package/dist/canvas-backend/decorations.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +3 -2
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/columnRule.d.ts +16 -2
- package/dist/columnRule.d.ts.map +1 -1
- package/dist/columnRule.js +21 -2
- package/dist/columnRule.js.map +1 -1
- package/dist/defaults/footnotes.d.ts +5 -0
- package/dist/defaults/footnotes.d.ts.map +1 -0
- package/dist/defaults/footnotes.js +87 -0
- package/dist/defaults/footnotes.js.map +1 -0
- package/dist/defaults/index.d.ts +1 -0
- package/dist/defaults/index.d.ts.map +1 -1
- package/dist/defaults/index.js +9 -0
- package/dist/defaults/index.js.map +1 -1
- package/dist/defaults/shared.d.ts.map +1 -1
- package/dist/defaults/shared.js +17 -0
- package/dist/defaults/shared.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +5 -2
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/knuthPlass/richAdapter.d.ts +1 -0
- package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
- package/dist/knuthPlass/richAdapter.js +2 -1
- package/dist/knuthPlass/richAdapter.js.map +1 -1
- package/dist/measure/cache.js +1 -1
- package/dist/measure/cache.js.map +1 -1
- package/dist/measure/rich.d.ts +3 -0
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +7 -5
- package/dist/measure/rich.js.map +1 -1
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +25 -9
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/inlineFormatting.d.ts +22 -0
- package/dist/parse/inlineFormatting.d.ts.map +1 -1
- package/dist/parse/inlineFormatting.js +42 -2
- package/dist/parse/inlineFormatting.js.map +1 -1
- package/dist/parse/sourceMapping.d.ts.map +1 -1
- package/dist/parse/sourceMapping.js +19 -1
- package/dist/parse/sourceMapping.js.map +1 -1
- package/dist/parse/types.d.ts +14 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +320 -5
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/config.d.ts.map +1 -1
- package/dist/pipeline/config.js +2 -1
- package/dist/pipeline/config.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +27 -1
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/continuation.d.ts.map +1 -1
- package/dist/pipeline/continuation.js +15 -1
- package/dist/pipeline/continuation.js.map +1 -1
- package/dist/pipeline/footnotes.d.ts +56 -0
- package/dist/pipeline/footnotes.d.ts.map +1 -0
- package/dist/pipeline/footnotes.js +202 -0
- package/dist/pipeline/footnotes.js.map +1 -0
- package/dist/pipeline/measureContentBlock.d.ts +2 -0
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +10 -0
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +44 -4
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/package.json +1 -1
package/dist/pipeline/build.js
CHANGED
|
@@ -11,13 +11,14 @@ import { resolveAllConfig, computeBaselineGrid, resolvedLocale } from './config'
|
|
|
11
11
|
import { createHeadingLevelResolver, deriveSectionGeometryConfig, deriveSectionMeasureContext, headingIsHidden, headingIsNumbered, headingMarksFor, headingStyleOf, planHeadingSections, } from './headingStyles';
|
|
12
12
|
import { computeOutline, hasTocDirective, headingNumberingOptions, headingTemplatesOf, outlineFromDoc, sameOutline } from './outline';
|
|
13
13
|
import { expandTocDirectives } from './toc';
|
|
14
|
-
import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
|
|
14
|
+
import { resolveBodyStyle, resolveBlockquoteStyle, resolveParagraphStyle } from './styles';
|
|
15
15
|
import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, listBulletPosition, listItemGapPx, listItemSpacingPx, } from './lists';
|
|
16
16
|
import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, pageLayoutOf, } from './placement';
|
|
17
17
|
import { chooseParagraphSplit } from './orphanWidow';
|
|
18
18
|
import { applyStyleAttrs, computePageMetrics, isMarkerBlock, nextNonMarkerBlock, spaceLinesAfter, prevNonMarkerBlock, rollbackTrailingBlocks, } from './buildHelpers';
|
|
19
19
|
import { measureContentBlock } from './measureContentBlock';
|
|
20
20
|
import { planParagraphContainers } from './paragraphContainers';
|
|
21
|
+
import { appendChapterEndNotes, footnoteIdsOfLines, footnoteParagraphStyle, noteContentBlock, numberFootnotes, splitFootnoteDefinitions, withFootnoteStyle, } from './footnotes';
|
|
21
22
|
import { planParts, derivePartMeasureContext } from './parts';
|
|
22
23
|
import { layoutCallout, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
|
|
23
24
|
import { layoutResourceBlock, planTableSlice } from './resourceLayout';
|
|
@@ -214,7 +215,16 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
214
215
|
doc.metadataSources = fieldSources;
|
|
215
216
|
// A configuration that leaves headings' inline marks off sets their
|
|
216
217
|
// spans plain here, for the outline and the layout alike (EF-122).
|
|
217
|
-
|
|
218
|
+
// Footnote definitions (`[^id]: …`) leave the flow; the markers are
|
|
219
|
+
// numbered in citation order. With `placement: 'chapterEnd'` the notes
|
|
220
|
+
// come back as paragraphs after each chapter's last block.
|
|
221
|
+
const footnoteSplit = splitFootnoteDefinitions(headingMarksFor(parseMarkdownMemo(markdownBody), resolved));
|
|
222
|
+
const footnoteDefs = footnoteSplit.defs;
|
|
223
|
+
const footnoteNumbering = numberFootnotes(footnoteSplit.blocks, resolved.footnotes.numbering, resolved.footnotes.numbering === 'document' ? Math.max(0, Math.floor(continuation?.footnoteNumber ?? 0)) : 0);
|
|
224
|
+
const chapterEndNotes = resolved.footnotes.placement === 'chapterEnd' && footnoteNumbering.numbers.size > 0;
|
|
225
|
+
const parsedBlocks = chapterEndNotes
|
|
226
|
+
? appendChapterEndNotes(footnoteSplit.blocks, footnoteNumbering, footnoteDefs)
|
|
227
|
+
: footnoteSplit.blocks;
|
|
218
228
|
const headingStart = continuation?.headings;
|
|
219
229
|
// `:::toc` expands into the entries of the book's outline — the one the
|
|
220
230
|
// host supplied, else this document's own (page labels unknown on the
|
|
@@ -255,7 +265,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
255
265
|
const orderedMetrics = computeOrderedListRunMetrics(contentBlocks, resolved, bodyStyle.fontSizePx);
|
|
256
266
|
const orderedLevelIndentsPx = computeOrderedLevelIndentsPx(resolved, bodyStyle.fontSizePx, orderedMetrics.maxWidthByDepth);
|
|
257
267
|
// `:::paragraphs{style="…"}` containers, resolved per content-block index.
|
|
258
|
-
const paragraphContainers = planParagraphContainers(contentBlocks, resolved);
|
|
268
|
+
const paragraphContainers = planParagraphContainers(contentBlocks, chapterEndNotes ? withFootnoteStyle(resolved) : resolved);
|
|
259
269
|
// `:::callout` ranges keyed by their start marker index (same rationale).
|
|
260
270
|
const calloutPlan = planCallouts(contentBlocks);
|
|
261
271
|
// `:::part` ranges: start/end marker indices and the enclosed blocks.
|
|
@@ -427,6 +437,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
427
437
|
* past it does), and a rollback that replays the heading gates it on the
|
|
428
438
|
* heading again (`gateCitedFloats`). */
|
|
429
439
|
const citedLinesPlaced = (blockIdx, placed) => {
|
|
440
|
+
reserveCitedNotes(placed);
|
|
430
441
|
// Side boxes waiting for the text after their fence stand beside it
|
|
431
442
|
// before the floats this text cites take the side column.
|
|
432
443
|
settleAwaitingSideBoxes(false);
|
|
@@ -1472,6 +1483,289 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1472
1483
|
resourceNumbering,
|
|
1473
1484
|
floatedIds,
|
|
1474
1485
|
leftFlow: calloutsOutOfFlow,
|
|
1486
|
+
...(footnoteNumbering.numbers.size > 0 ? { footnoteNumbers: footnoteNumbering.numbers } : {}),
|
|
1487
|
+
};
|
|
1488
|
+
// --- Footnotes at the column foot (`footnotes.placement: 'column'`) ------
|
|
1489
|
+
// A text block placed in a column reserves, at the column's foot, the
|
|
1490
|
+
// notes its lines cite for the first time: the column's box shrinks by
|
|
1491
|
+
// their height (as a bottom float band shrinks it), so the text above
|
|
1492
|
+
// keeps its positions and the grid. The flow counts that height before it
|
|
1493
|
+
// places lines (`notesPrefixCost`), so the line citing a note and the note
|
|
1494
|
+
// share a column. The notes are set in their reserved room once the pass
|
|
1495
|
+
// is placed (`setColumnNotes`).
|
|
1496
|
+
const columnNotes = resolved.footnotes.placement === 'column' && footnoteNumbering.numbers.size > 0;
|
|
1497
|
+
const noteStyle = columnNotes ? resolveParagraphStyle(footnoteParagraphStyle(resolved), resolved) : bodyStyle;
|
|
1498
|
+
const noteSpaceAbovePx = dimensionToPx(resolved.footnotes.spaceAbove, dpi, bodyStyle.fontSizePx);
|
|
1499
|
+
const noteSpaceBelowRulePx = dimensionToPx(resolved.footnotes.spaceBelowRule, dpi, bodyStyle.fontSizePx);
|
|
1500
|
+
const noteRulePx = resolved.footnotes.separator.enabled ? dimensionToPx(resolved.footnotes.separator.lineWidth, dpi) : 0;
|
|
1501
|
+
/** The separator zone above a column's first note. */
|
|
1502
|
+
const noteHeadPx = noteSpaceAbovePx + noteRulePx + noteSpaceBelowRulePx;
|
|
1503
|
+
/** Space between two notes. */
|
|
1504
|
+
const noteGapPx = Math.max(0, noteStyle.marginBottomPx);
|
|
1505
|
+
/** Notes set (reserved) in this pass: a note is set once, where it is
|
|
1506
|
+
* first cited. */
|
|
1507
|
+
const placedNotes = new Set();
|
|
1508
|
+
const noteMeasures = new Map();
|
|
1509
|
+
const measureNote = (id, width) => {
|
|
1510
|
+
const key = `${id}@${width.toFixed(2)}`;
|
|
1511
|
+
let m = noteMeasures.get(key);
|
|
1512
|
+
if (m === undefined) {
|
|
1513
|
+
const number = footnoteNumbering.numbers.get(id) ?? '?';
|
|
1514
|
+
m = measureContentBlock(noteContentBlock(footnoteDefs.get(id), id, number), 0, width, measureCtx, { styleOverride: noteStyle });
|
|
1515
|
+
noteMeasures.set(key, m);
|
|
1516
|
+
}
|
|
1517
|
+
return m;
|
|
1518
|
+
};
|
|
1519
|
+
const noteHeight = (id, width) => Math.max(1, measureNote(id, width)?.measured.lines.length ?? 1) * noteStyle.lineHeightPx;
|
|
1520
|
+
/** The notes of a column: ids in order, and the room reserved for them,
|
|
1521
|
+
* one slot per reservation (page coordinates). */
|
|
1522
|
+
const columnNotesOf = new Map();
|
|
1523
|
+
const columnOfNotes = new Map();
|
|
1524
|
+
/** Height `ids` (not yet set) add to `col`'s foot: the separator zone
|
|
1525
|
+
* when the column has no note yet, the notes and the gaps between. */
|
|
1526
|
+
const notesCost = (col, ids) => {
|
|
1527
|
+
if (ids.length === 0)
|
|
1528
|
+
return 0;
|
|
1529
|
+
const first = !columnNotesOf.has(col);
|
|
1530
|
+
let h = first ? noteHeadPx : 0;
|
|
1531
|
+
ids.forEach((id, i) => {
|
|
1532
|
+
if (!first || i > 0)
|
|
1533
|
+
h += noteGapPx;
|
|
1534
|
+
h += noteHeight(id, col.bbox.width);
|
|
1535
|
+
});
|
|
1536
|
+
return h;
|
|
1537
|
+
};
|
|
1538
|
+
/** Notes cited by blocks set outside the flow (the paragraphs of a box,
|
|
1539
|
+
* inline, floated or fixed): they wait for the next text the flow
|
|
1540
|
+
* places, and go to the foot of its column, in citation order. */
|
|
1541
|
+
const pendingNotes = [];
|
|
1542
|
+
/** How far `doc.blocks` was read for such citations. */
|
|
1543
|
+
let notesScanned = 0;
|
|
1544
|
+
const collectPendingNotes = () => {
|
|
1545
|
+
for (; notesScanned < doc.blocks.length; notesScanned++) {
|
|
1546
|
+
for (const id of footnoteIdsOfLines(doc.blocks[notesScanned].lines)) {
|
|
1547
|
+
if (!placedNotes.has(id) && !pendingNotes.includes(id))
|
|
1548
|
+
pendingNotes.push(id);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
/** `cost[k]`: the notes the first `k` of `lines` cite for the first time
|
|
1553
|
+
* (after the pending ones) would add to `col`'s foot. Undefined when
|
|
1554
|
+
* there are none. */
|
|
1555
|
+
const notesPrefixCost = (col, lines) => {
|
|
1556
|
+
if (!columnNotes)
|
|
1557
|
+
return undefined;
|
|
1558
|
+
collectPendingNotes();
|
|
1559
|
+
const ids = [...pendingNotes];
|
|
1560
|
+
let any = ids.length > 0;
|
|
1561
|
+
const cost = [notesCost(col, ids)];
|
|
1562
|
+
for (const line of lines) {
|
|
1563
|
+
for (const seg of line.segments ?? []) {
|
|
1564
|
+
const id = seg.footnoteId;
|
|
1565
|
+
if (id !== undefined && !placedNotes.has(id) && !ids.includes(id)) {
|
|
1566
|
+
ids.push(id);
|
|
1567
|
+
any = true;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
cost.push(notesCost(col, ids));
|
|
1571
|
+
}
|
|
1572
|
+
return any ? cost : undefined;
|
|
1573
|
+
};
|
|
1574
|
+
/** Reserve at the placed block's column foot the pending notes and the
|
|
1575
|
+
* ones the block cites first. */
|
|
1576
|
+
const reserveCitedNotes = (placed) => {
|
|
1577
|
+
if (!columnNotes || placed.pageIndex < 0)
|
|
1578
|
+
return;
|
|
1579
|
+
collectPendingNotes();
|
|
1580
|
+
const cited = footnoteIdsOfLines(placed.lines).filter((id) => !placedNotes.has(id) && !pendingNotes.includes(id));
|
|
1581
|
+
if (cited.length === 0 && pendingNotes.length === 0)
|
|
1582
|
+
return;
|
|
1583
|
+
const page = doc.pages[placed.pageIndex];
|
|
1584
|
+
const col = page?.columns[placed.columnIndex];
|
|
1585
|
+
if (!page || !col)
|
|
1586
|
+
return;
|
|
1587
|
+
reserveNotes(page, col, [...pendingNotes.splice(0), ...cited], placed.contentIndex ?? 0);
|
|
1588
|
+
};
|
|
1589
|
+
/** Reserve `ids` at `col`'s foot. */
|
|
1590
|
+
const reserveNotes = (page, col, ids, anchor) => {
|
|
1591
|
+
const h = notesCost(col, ids);
|
|
1592
|
+
let bottom;
|
|
1593
|
+
const capped = uncappedBottoms.get(col);
|
|
1594
|
+
if (capped !== undefined) {
|
|
1595
|
+
// A column a band cap cut: the notes go under the cut, at the true
|
|
1596
|
+
// foot, as a bottom float does; the cut moves up only when they reach
|
|
1597
|
+
// above it.
|
|
1598
|
+
bottom = capped;
|
|
1599
|
+
uncappedBottoms.set(col, capped - h);
|
|
1600
|
+
const overlap = col.bbox.y + col.bbox.height - (capped - h);
|
|
1601
|
+
if (overlap > 0) {
|
|
1602
|
+
col.bbox.height = Math.max(0, col.bbox.height - overlap);
|
|
1603
|
+
col.availableHeight = Math.max(0, col.availableHeight - overlap);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
else {
|
|
1607
|
+
bottom = col.bbox.y + col.bbox.height;
|
|
1608
|
+
col.bbox.height = Math.max(0, col.bbox.height - h);
|
|
1609
|
+
col.availableHeight = Math.max(0, col.availableHeight - h);
|
|
1610
|
+
}
|
|
1611
|
+
let entry = columnNotesOf.get(col);
|
|
1612
|
+
if (!entry) {
|
|
1613
|
+
entry = { ids: [], slots: [] };
|
|
1614
|
+
columnNotesOf.set(col, entry);
|
|
1615
|
+
columnOfNotes.set(col, page);
|
|
1616
|
+
}
|
|
1617
|
+
entry.ids.push(...ids);
|
|
1618
|
+
entry.slots.push({ top: bottom - h, height: h, ids });
|
|
1619
|
+
for (const id of ids)
|
|
1620
|
+
placedNotes.add(id);
|
|
1621
|
+
for (const id of ids)
|
|
1622
|
+
noteAnchor.set(id, anchor);
|
|
1623
|
+
};
|
|
1624
|
+
/** Notes still pending when the flow ends (a box closes the document):
|
|
1625
|
+
* the foot of the column the flow ended in takes them. */
|
|
1626
|
+
const reserveLeftoverNotes = () => {
|
|
1627
|
+
if (!columnNotes)
|
|
1628
|
+
return;
|
|
1629
|
+
collectPendingNotes();
|
|
1630
|
+
if (pendingNotes.length === 0)
|
|
1631
|
+
return;
|
|
1632
|
+
const page = doc.pages[cursor.pageIndex];
|
|
1633
|
+
const col = page?.columns[cursor.columnIndex];
|
|
1634
|
+
if (page && col)
|
|
1635
|
+
reserveNotes(page, col, pendingNotes.splice(0), contentBlocks.length - 1);
|
|
1636
|
+
};
|
|
1637
|
+
/** `chapterEnd`: move the run of note paragraphs that closes a column
|
|
1638
|
+
* down to the column's true foot (under a band cap's cut, above any
|
|
1639
|
+
* bottom float band). */
|
|
1640
|
+
const dropChapterEndNotes = () => {
|
|
1641
|
+
for (const page of doc.pages) {
|
|
1642
|
+
for (const col of page.columns) {
|
|
1643
|
+
const blocks = col.blocks;
|
|
1644
|
+
let first = blocks.length;
|
|
1645
|
+
while (first > 0 && blocks[first - 1].footnoteNote !== undefined)
|
|
1646
|
+
first--;
|
|
1647
|
+
if (first === blocks.length)
|
|
1648
|
+
continue;
|
|
1649
|
+
const run = blocks.slice(first);
|
|
1650
|
+
const last = run[run.length - 1];
|
|
1651
|
+
const lastLine = last.lines[last.lines.length - 1];
|
|
1652
|
+
const foot = lastLine ? lastLine.bbox.y + lastLine.bbox.height : last.bbox.y + last.bbox.height;
|
|
1653
|
+
const d = trueBottom(col, uncappedBottoms) - foot;
|
|
1654
|
+
if (d < 0.5)
|
|
1655
|
+
continue;
|
|
1656
|
+
for (const b of run) {
|
|
1657
|
+
b.bbox = createBoundingBox(b.bbox.x, b.bbox.y + d, b.bbox.width, b.bbox.height);
|
|
1658
|
+
for (const line of b.lines) {
|
|
1659
|
+
line.bbox = createBoundingBox(line.bbox.x, line.bbox.y + d, line.bbox.width, line.bbox.height);
|
|
1660
|
+
line.baseline += d;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
/** `chapterEnd`: the separator rule over the notes of each column, set
|
|
1667
|
+
* in the space above them (`spaceBelowRule` over the first note). */
|
|
1668
|
+
const ruleChapterEndNotes = () => {
|
|
1669
|
+
const sep = resolved.footnotes.separator;
|
|
1670
|
+
if (!sep.enabled || sep.width <= 0 || noteRulePx <= 0)
|
|
1671
|
+
return;
|
|
1672
|
+
const color = sep.color?.hex ?? resolved.footnotes.color?.hex ?? bodyStyle.color;
|
|
1673
|
+
for (const page of doc.pages) {
|
|
1674
|
+
for (const col of page.columns) {
|
|
1675
|
+
const at = col.blocks.findIndex((b, i) => b.footnoteNote !== undefined && col.blocks[i - 1]?.footnoteNote === undefined);
|
|
1676
|
+
if (at < 0)
|
|
1677
|
+
continue;
|
|
1678
|
+
const notes = col.blocks.slice(at).filter((b) => b.footnoteNote !== undefined);
|
|
1679
|
+
const firstLine = notes[0].lines[0];
|
|
1680
|
+
if (!firstLine)
|
|
1681
|
+
continue;
|
|
1682
|
+
const top = firstLine.bbox.y - noteSpaceBelowRulePx - noteRulePx;
|
|
1683
|
+
const last = notes[notes.length - 1];
|
|
1684
|
+
(page.footnoteAreas ??= []).push({
|
|
1685
|
+
columnIndex: col.index,
|
|
1686
|
+
bbox: createBoundingBox(col.bbox.x, top, col.bbox.width, last.bbox.y + last.bbox.height - top),
|
|
1687
|
+
noteIds: notes.map((b) => b.footnoteNote),
|
|
1688
|
+
rule: { x: col.bbox.x, y: top + noteRulePx / 2, width: col.bbox.width * sep.width, lineWidthPx: noteRulePx, color },
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
/** Content index of the block citing each note (reading order). */
|
|
1694
|
+
const noteAnchor = new Map();
|
|
1695
|
+
/** Set the notes in the room reserved for them: the reservations of a
|
|
1696
|
+
* column that touch (the usual case) make one stack in citation order
|
|
1697
|
+
* under the separator; a float band set between two leaves each its own
|
|
1698
|
+
* room. */
|
|
1699
|
+
const setColumnNotes = () => {
|
|
1700
|
+
for (const [col, entry] of columnNotesOf) {
|
|
1701
|
+
const page = columnOfNotes.get(col);
|
|
1702
|
+
const regions = [];
|
|
1703
|
+
for (const slot of entry.slots) {
|
|
1704
|
+
const last = regions[regions.length - 1];
|
|
1705
|
+
if (last && Math.abs(slot.top + slot.height - last.top) < 0.5) {
|
|
1706
|
+
last.top = slot.top;
|
|
1707
|
+
last.ids.push(...slot.ids);
|
|
1708
|
+
}
|
|
1709
|
+
else {
|
|
1710
|
+
regions.push({ top: slot.top, bottom: slot.top + slot.height, ids: [...slot.ids] });
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
const x = col.bbox.x;
|
|
1714
|
+
const width = col.bbox.width;
|
|
1715
|
+
regions.forEach((region, r) => {
|
|
1716
|
+
let y = region.top;
|
|
1717
|
+
if (r === 0) {
|
|
1718
|
+
const sep = resolved.footnotes.separator;
|
|
1719
|
+
const rule = sep.enabled && sep.width > 0 && noteRulePx > 0
|
|
1720
|
+
? {
|
|
1721
|
+
x,
|
|
1722
|
+
y: y + noteSpaceAbovePx + noteRulePx / 2,
|
|
1723
|
+
width: width * sep.width,
|
|
1724
|
+
lineWidthPx: noteRulePx,
|
|
1725
|
+
color: sep.color?.hex ?? noteStyle.color,
|
|
1726
|
+
}
|
|
1727
|
+
: undefined;
|
|
1728
|
+
(page.footnoteAreas ??= []).push({
|
|
1729
|
+
columnIndex: col.index,
|
|
1730
|
+
bbox: createBoundingBox(x, region.top, width, region.bottom - region.top),
|
|
1731
|
+
noteIds: entry.ids,
|
|
1732
|
+
...(rule ? { rule } : {}),
|
|
1733
|
+
});
|
|
1734
|
+
y += noteHeadPx;
|
|
1735
|
+
}
|
|
1736
|
+
region.ids.forEach((id, i) => {
|
|
1737
|
+
if (r > 0 || i > 0)
|
|
1738
|
+
y += noteGapPx;
|
|
1739
|
+
const m = measureNote(id, width);
|
|
1740
|
+
if (!m)
|
|
1741
|
+
return;
|
|
1742
|
+
const blk = createVDTBlock(`fn-${id}`, 'paragraph', noteStyle.fontString, noteStyle.color, noteStyle.textAlign);
|
|
1743
|
+
applyStyleAttrs(blk, noteStyle);
|
|
1744
|
+
blk.lines = resetLinePositions(raggedLooseLines(m.measured.lines, noteStyle.textAlign), noteStyle.lineHeightPx).map((line) => ({
|
|
1745
|
+
...line,
|
|
1746
|
+
bbox: createBoundingBox(line.bbox.x + x, line.bbox.y + y, line.bbox.width, line.bbox.height),
|
|
1747
|
+
baseline: line.baseline + y,
|
|
1748
|
+
}));
|
|
1749
|
+
const h = blk.lines.length * noteStyle.lineHeightPx;
|
|
1750
|
+
blk.bbox = createBoundingBox(x, y, width, h);
|
|
1751
|
+
blk.pageIndex = page.index;
|
|
1752
|
+
blk.columnIndex = col.index;
|
|
1753
|
+
blk.contentIndex = noteAnchor.get(id) ?? 0;
|
|
1754
|
+
blk.footnoteNote = id;
|
|
1755
|
+
blk.dirty = false;
|
|
1756
|
+
blk.snappedToGrid = false;
|
|
1757
|
+
if (blk.lines.length > 0) {
|
|
1758
|
+
blk.sourceStart = blk.lines[0].sourceStart;
|
|
1759
|
+
blk.sourceEnd = blk.lines[blk.lines.length - 1].sourceEnd;
|
|
1760
|
+
}
|
|
1761
|
+
blk.sourceMap = m.absoluteSourceMap;
|
|
1762
|
+
blk.plainPrefixLen = m.prefixLen;
|
|
1763
|
+
(page.floats ??= []).push(blk);
|
|
1764
|
+
doc.blocks.push(blk);
|
|
1765
|
+
y += h;
|
|
1766
|
+
});
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1475
1769
|
};
|
|
1476
1770
|
// Blocks inside a `:::part` measure with the part body typography.
|
|
1477
1771
|
const partMeasureCtx = partPlan.byStart.size > 0
|
|
@@ -1530,6 +1824,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
1530
1824
|
blk.sourceTitle = flattenTitleBreaks(raw.text);
|
|
1531
1825
|
}
|
|
1532
1826
|
}
|
|
1827
|
+
if (raw.footnoteNote !== undefined)
|
|
1828
|
+
blk.footnoteNote = raw.footnoteNote;
|
|
1533
1829
|
if (raw.toc?.kind === 'entry') {
|
|
1534
1830
|
blk.tocEntry = raw.toc.pageIndex !== undefined ? { pageIndex: raw.toc.pageIndex } : {};
|
|
1535
1831
|
}
|
|
@@ -3975,11 +4271,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
3975
4271
|
tracking: looseTracking,
|
|
3976
4272
|
};
|
|
3977
4273
|
}
|
|
3978
|
-
|
|
4274
|
+
// Footnotes the lines cite take room at the column's foot: the whole
|
|
4275
|
+
// block fits when its lines and all their notes do, and a split
|
|
4276
|
+
// keeps the lines whose notes fit under them.
|
|
4277
|
+
const noteCosts = vdtType === 'mathDisplay' ? undefined : notesPrefixCost(curCol, remainingLines);
|
|
4278
|
+
const effectiveAvailable = curCol.availableHeight - spacingBefore - (noteCosts ? noteCosts[noteCosts.length - 1] : 0);
|
|
3979
4279
|
// A hair of tolerance: room of exactly N lines (grid arithmetic in
|
|
3980
4280
|
// floats — a column cut by a band cap, a balancing extra line) must
|
|
3981
4281
|
// hold N lines, not N - 1.
|
|
3982
|
-
|
|
4282
|
+
let linesPerAvailable = Math.floor((curCol.availableHeight - spacingBefore + 0.01) / style.lineHeightPx);
|
|
4283
|
+
if (noteCosts) {
|
|
4284
|
+
const costOf = (k) => noteCosts[Math.min(k, noteCosts.length - 1)];
|
|
4285
|
+
while (linesPerAvailable > 0 && linesPerAvailable * style.lineHeightPx + costOf(linesPerAvailable) > curCol.availableHeight - spacingBefore + 0.01) {
|
|
4286
|
+
linesPerAvailable--;
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
3983
4289
|
// Math display blocks carry their natural pixel height on the single
|
|
3984
4290
|
// VDTLine; text blocks use the uniform body lineHeightPx per line.
|
|
3985
4291
|
const totalRemainHeight = vdtType === 'mathDisplay'
|
|
@@ -4709,6 +5015,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
|
|
|
4709
5015
|
// End of the document: level the closing band and place any floats still
|
|
4710
5016
|
// pending (referenced on the last page) on pages appended after it.
|
|
4711
5017
|
closeFlowSegment(contentBlocks.length);
|
|
5018
|
+
// The notes, in the room the flow reserved for them.
|
|
5019
|
+
reserveLeftoverNotes();
|
|
5020
|
+
setColumnNotes();
|
|
5021
|
+
// After the chapter (`chapterEnd`), the notes that close a column drop
|
|
5022
|
+
// to its foot: the room left over stays between the text and them.
|
|
5023
|
+
if (chapterEndNotes && resolved.footnotes.chapterEndAlign === 'foot')
|
|
5024
|
+
dropChapterEndNotes();
|
|
5025
|
+
if (chapterEndNotes)
|
|
5026
|
+
ruleChapterEndNotes();
|
|
4712
5027
|
// A band that is still cut at the end of the pass (a trailing cap: the
|
|
4713
5028
|
// closing band of a chapter or of the document) must hold what it took,
|
|
4714
5029
|
// under the same rules as an uncut one. Two ways a cut too low breaks
|