postext 1.35.1 → 1.37.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/__bench__/composedPage.bench.test.d.ts +2 -0
- package/dist/__bench__/composedPage.bench.test.d.ts.map +1 -0
- package/dist/__bench__/composedPage.bench.test.js +95 -0
- package/dist/__bench__/composedPage.bench.test.js.map +1 -0
- package/dist/__tests__/composed/continuation.test.d.ts +2 -0
- package/dist/__tests__/composed/continuation.test.d.ts.map +1 -0
- package/dist/__tests__/composed/continuation.test.js +352 -0
- package/dist/__tests__/composed/continuation.test.js.map +1 -0
- package/dist/__tests__/composed/contrast.test.d.ts +2 -0
- package/dist/__tests__/composed/contrast.test.d.ts.map +1 -0
- package/dist/__tests__/composed/contrast.test.js +124 -0
- package/dist/__tests__/composed/contrast.test.js.map +1 -0
- package/dist/__tests__/composed/corpus.test.js +7 -1
- package/dist/__tests__/composed/corpus.test.js.map +1 -1
- package/dist/__tests__/composed/fit.test.d.ts +2 -0
- package/dist/__tests__/composed/fit.test.d.ts.map +1 -0
- package/dist/__tests__/composed/fit.test.js +408 -0
- package/dist/__tests__/composed/fit.test.js.map +1 -0
- package/dist/__tests__/composed/groups.test.js +14 -0
- package/dist/__tests__/composed/groups.test.js.map +1 -1
- package/dist/__tests__/composed/headingDesigns.test.d.ts +2 -0
- package/dist/__tests__/composed/headingDesigns.test.d.ts.map +1 -0
- package/dist/__tests__/composed/headingDesigns.test.js +262 -0
- package/dist/__tests__/composed/headingDesigns.test.js.map +1 -0
- package/dist/__tests__/composed/labels.test.d.ts +2 -0
- package/dist/__tests__/composed/labels.test.d.ts.map +1 -0
- package/dist/__tests__/composed/labels.test.js +326 -0
- package/dist/__tests__/composed/labels.test.js.map +1 -0
- package/dist/__tests__/composed/layout.test.js +2 -1
- package/dist/__tests__/composed/layout.test.js.map +1 -1
- package/dist/__tests__/composed/outsidePrint.test.js +5 -2
- package/dist/__tests__/composed/outsidePrint.test.js.map +1 -1
- package/dist/__tests__/composed/pageMemo.test.d.ts +2 -0
- package/dist/__tests__/composed/pageMemo.test.d.ts.map +1 -0
- package/dist/__tests__/composed/pageMemo.test.js +128 -0
- package/dist/__tests__/composed/pageMemo.test.js.map +1 -0
- package/dist/__tests__/composed/sourceMap.test.d.ts +2 -0
- package/dist/__tests__/composed/sourceMap.test.d.ts.map +1 -0
- package/dist/__tests__/composed/sourceMap.test.js +395 -0
- package/dist/__tests__/composed/sourceMap.test.js.map +1 -0
- package/dist/__tests__/composed/spreadLabels.test.d.ts +2 -0
- package/dist/__tests__/composed/spreadLabels.test.d.ts.map +1 -0
- package/dist/__tests__/composed/spreadLabels.test.js +131 -0
- package/dist/__tests__/composed/spreadLabels.test.js.map +1 -0
- package/dist/__tests__/composed/templateFrameRects.test.d.ts +2 -0
- package/dist/__tests__/composed/templateFrameRects.test.d.ts.map +1 -0
- package/dist/__tests__/composed/templateFrameRects.test.js +37 -0
- package/dist/__tests__/composed/templateFrameRects.test.js.map +1 -0
- package/dist/__tests__/design/imageAltText.test.js +2 -1
- package/dist/__tests__/design/imageAltText.test.js.map +1 -1
- package/dist/__tests__/exports.test.js +33 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/__tests__/migrate/fixtures/textbookPort.d.ts +14 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.d.ts.map +1 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.js +116 -0
- package/dist/__tests__/migrate/fixtures/textbookPort.js.map +1 -0
- package/dist/__tests__/migrate/textToMarkdown.test.d.ts +2 -0
- package/dist/__tests__/migrate/textToMarkdown.test.d.ts.map +1 -0
- package/dist/__tests__/migrate/textToMarkdown.test.js +268 -0
- package/dist/__tests__/migrate/textToMarkdown.test.js.map +1 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts +2 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts.map +1 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.js +89 -0
- package/dist/__tests__/paragraphStyleLetterSpacing.test.js.map +1 -0
- package/dist/__tests__/pictureBelowPrintSize.test.d.ts +2 -0
- package/dist/__tests__/pictureBelowPrintSize.test.d.ts.map +1 -0
- package/dist/__tests__/pictureBelowPrintSize.test.js +82 -0
- package/dist/__tests__/pictureBelowPrintSize.test.js.map +1 -0
- package/dist/__tests__/pipeline/contourWrap.test.d.ts +2 -0
- package/dist/__tests__/pipeline/contourWrap.test.d.ts.map +1 -0
- package/dist/__tests__/pipeline/contourWrap.test.js +412 -0
- package/dist/__tests__/pipeline/contourWrap.test.js.map +1 -0
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
- package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
- package/dist/__tests__/toc.test.js +3 -2
- package/dist/__tests__/toc.test.js.map +1 -1
- package/dist/bundle/codec.d.ts +16 -0
- package/dist/bundle/codec.d.ts.map +1 -1
- package/dist/bundle/codec.js +44 -2
- package/dist/bundle/codec.js.map +1 -1
- package/dist/bundle/index.d.ts +1 -1
- package/dist/bundle/index.d.ts.map +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/index.js.map +1 -1
- package/dist/canvas-backend/composed.d.ts +5 -2
- package/dist/canvas-backend/composed.d.ts.map +1 -1
- package/dist/canvas-backend/composed.js +10 -5
- package/dist/canvas-backend/composed.js.map +1 -1
- package/dist/canvas-backend/index.d.ts.map +1 -1
- package/dist/canvas-backend/index.js +37 -19
- package/dist/canvas-backend/index.js.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts +5 -1
- package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
- package/dist/canvas-backend/renderResourceBlock.js +60 -1
- package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
- package/dist/composed/continuation.d.ts +102 -0
- package/dist/composed/continuation.d.ts.map +1 -0
- package/dist/composed/continuation.js +87 -0
- package/dist/composed/continuation.js.map +1 -0
- package/dist/composed/contour.d.ts +21 -0
- package/dist/composed/contour.d.ts.map +1 -0
- package/dist/composed/contour.js +34 -0
- package/dist/composed/contour.js.map +1 -0
- package/dist/composed/geometry.d.ts +8 -0
- package/dist/composed/geometry.d.ts.map +1 -1
- package/dist/composed/geometry.js +26 -0
- package/dist/composed/geometry.js.map +1 -1
- package/dist/composed/ids.d.ts +22 -0
- package/dist/composed/ids.d.ts.map +1 -0
- package/dist/composed/ids.js +32 -0
- package/dist/composed/ids.js.map +1 -0
- package/dist/composed/labels.d.ts +154 -0
- package/dist/composed/labels.d.ts.map +1 -0
- package/dist/composed/labels.js +524 -0
- package/dist/composed/labels.js.map +1 -0
- package/dist/composed/pageMemo.d.ts +73 -0
- package/dist/composed/pageMemo.d.ts.map +1 -0
- package/dist/composed/pageMemo.js +289 -0
- package/dist/composed/pageMemo.js.map +1 -0
- package/dist/composed/paint.d.ts +29 -9
- package/dist/composed/paint.d.ts.map +1 -1
- package/dist/composed/paint.js +41 -7
- package/dist/composed/paint.js.map +1 -1
- package/dist/composed/reflowLog.d.ts +14 -2
- package/dist/composed/reflowLog.d.ts.map +1 -1
- package/dist/composed/reflowLog.js +64 -7
- package/dist/composed/reflowLog.js.map +1 -1
- package/dist/composed/slotChecks.d.ts.map +1 -1
- package/dist/composed/slotChecks.js +3 -1
- package/dist/composed/slotChecks.js.map +1 -1
- package/dist/composed/sourceMap.d.ts +97 -0
- package/dist/composed/sourceMap.d.ts.map +1 -0
- package/dist/composed/sourceMap.js +748 -0
- package/dist/composed/sourceMap.js.map +1 -0
- package/dist/composed/spread.d.ts +3 -0
- package/dist/composed/spread.d.ts.map +1 -1
- package/dist/composed/spread.js +11 -12
- package/dist/composed/spread.js.map +1 -1
- package/dist/composed/templates.d.ts +1 -0
- package/dist/composed/templates.d.ts.map +1 -1
- package/dist/composed/templates.js +4 -0
- package/dist/composed/templates.js.map +1 -1
- package/dist/composed/types.d.ts +15 -32
- package/dist/composed/types.d.ts.map +1 -1
- package/dist/composed/validate.d.ts.map +1 -1
- package/dist/composed/validate.js +3 -2
- package/dist/composed/validate.js.map +1 -1
- package/dist/composed/wrap.d.ts.map +1 -1
- package/dist/composed/wrap.js +7 -1
- package/dist/composed/wrap.js.map +1 -1
- package/dist/configWarnings.d.ts +15 -0
- package/dist/configWarnings.d.ts.map +1 -1
- package/dist/configWarnings.js +29 -17
- package/dist/configWarnings.js.map +1 -1
- package/dist/contour/index.d.ts +134 -0
- package/dist/contour/index.d.ts.map +1 -0
- package/dist/contour/index.js +354 -0
- package/dist/contour/index.js.map +1 -0
- package/dist/contour/png.d.ts +16 -0
- package/dist/contour/png.d.ts.map +1 -0
- package/dist/contour/png.js +173 -0
- package/dist/contour/png.js.map +1 -0
- package/dist/contrast.d.ts +5 -3
- package/dist/contrast.d.ts.map +1 -1
- package/dist/contrast.js +46 -5
- package/dist/contrast.js.map +1 -1
- package/dist/defaults/paragraphStyles.d.ts +1 -1
- package/dist/defaults/paragraphStyles.d.ts.map +1 -1
- package/dist/defaults/paragraphStyles.js +5 -1
- package/dist/defaults/paragraphStyles.js.map +1 -1
- package/dist/design/slots.d.ts +12 -10
- package/dist/design/slots.d.ts.map +1 -1
- package/dist/design/slots.js +26 -2
- package/dist/design/slots.js.map +1 -1
- package/dist/fonts/faces.d.ts.map +1 -1
- package/dist/fonts/faces.js +6 -0
- package/dist/fonts/faces.js.map +1 -1
- package/dist/highlightMarks.d.ts.map +1 -1
- package/dist/highlightMarks.js +6 -0
- package/dist/highlightMarks.js.map +1 -1
- package/dist/html-backend.d.ts.map +1 -1
- package/dist/html-backend.js +42 -4
- package/dist/html-backend.js.map +1 -1
- package/dist/index.d.ts +18 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/dist/measure/rich.d.ts.map +1 -1
- package/dist/measure/rich.js +2 -0
- package/dist/measure/rich.js.map +1 -1
- package/dist/migrate/markdownRuns.d.ts +70 -0
- package/dist/migrate/markdownRuns.d.ts.map +1 -0
- package/dist/migrate/markdownRuns.js +215 -0
- package/dist/migrate/markdownRuns.js.map +1 -0
- package/dist/migrate/pageDesign.d.ts +119 -0
- package/dist/migrate/pageDesign.d.ts.map +1 -0
- package/dist/migrate/pageDesign.js +446 -0
- package/dist/migrate/pageDesign.js.map +1 -0
- package/dist/migrate/textToMarkdown.d.ts +124 -0
- package/dist/migrate/textToMarkdown.d.ts.map +1 -0
- package/dist/migrate/textToMarkdown.js +716 -0
- package/dist/migrate/textToMarkdown.js.map +1 -0
- package/dist/parse/blockParser.d.ts.map +1 -1
- package/dist/parse/blockParser.js +44 -3
- package/dist/parse/blockParser.js.map +1 -1
- package/dist/parse/types.d.ts +20 -0
- package/dist/parse/types.d.ts.map +1 -1
- package/dist/pipeline/build.d.ts.map +1 -1
- package/dist/pipeline/build.js +298 -30
- package/dist/pipeline/build.js.map +1 -1
- package/dist/pipeline/buildMeasurement.d.ts +7 -2
- package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
- package/dist/pipeline/buildMeasurement.js +21 -5
- package/dist/pipeline/buildMeasurement.js.map +1 -1
- package/dist/pipeline/calloutLayout.d.ts +39 -14
- package/dist/pipeline/calloutLayout.d.ts.map +1 -1
- package/dist/pipeline/calloutLayout.js +50 -4
- package/dist/pipeline/calloutLayout.js.map +1 -1
- package/dist/pipeline/composedLayout.d.ts +51 -6
- package/dist/pipeline/composedLayout.d.ts.map +1 -1
- package/dist/pipeline/composedLayout.js +633 -78
- package/dist/pipeline/composedLayout.js.map +1 -1
- package/dist/pipeline/contentWarnings.d.ts +15 -0
- package/dist/pipeline/contentWarnings.d.ts.map +1 -1
- package/dist/pipeline/contentWarnings.js +98 -4
- package/dist/pipeline/contentWarnings.js.map +1 -1
- package/dist/pipeline/floatPlacement.d.ts +2 -0
- package/dist/pipeline/floatPlacement.d.ts.map +1 -1
- package/dist/pipeline/floatPlacement.js +1 -0
- package/dist/pipeline/floatPlacement.js.map +1 -1
- package/dist/pipeline/frameLayout.d.ts +7 -4
- package/dist/pipeline/frameLayout.d.ts.map +1 -1
- package/dist/pipeline/frameLayout.js +5 -2
- package/dist/pipeline/frameLayout.js.map +1 -1
- package/dist/pipeline/headerFooter.d.ts +20 -1
- package/dist/pipeline/headerFooter.d.ts.map +1 -1
- package/dist/pipeline/headerFooter.js +225 -70
- package/dist/pipeline/headerFooter.js.map +1 -1
- package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
- package/dist/pipeline/measureContentBlock.js +1 -0
- package/dist/pipeline/measureContentBlock.js.map +1 -1
- package/dist/pipeline/mirrorFrame.d.ts.map +1 -1
- package/dist/pipeline/mirrorFrame.js +25 -2
- package/dist/pipeline/mirrorFrame.js.map +1 -1
- package/dist/pipeline/outline.d.ts.map +1 -1
- package/dist/pipeline/outline.js +2 -0
- package/dist/pipeline/outline.js.map +1 -1
- package/dist/pipeline/resourceLayout.d.ts +28 -0
- package/dist/pipeline/resourceLayout.d.ts.map +1 -1
- package/dist/pipeline/resourceLayout.js +21 -0
- package/dist/pipeline/resourceLayout.js.map +1 -1
- package/dist/pipeline/styles.d.ts +1 -1
- package/dist/pipeline/styles.d.ts.map +1 -1
- package/dist/pipeline/styles.js +4 -0
- package/dist/pipeline/styles.js.map +1 -1
- package/dist/pipeline/textWrap.d.ts +4 -1
- package/dist/pipeline/textWrap.d.ts.map +1 -1
- package/dist/pipeline/textWrap.js +17 -3
- package/dist/pipeline/textWrap.js.map +1 -1
- package/dist/pipeline/toc.d.ts.map +1 -1
- package/dist/pipeline/toc.js +1 -0
- package/dist/pipeline/toc.js.map +1 -1
- package/dist/types.d.ts +77 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/vdt.d.ts +290 -15
- package/dist/vdt.d.ts.map +1 -1
- package/dist/vdt.js.map +1 -1
- package/dist/word/toDocx.d.ts.map +1 -1
- package/dist/word/toDocx.js +14 -0
- package/dist/word/toDocx.js.map +1 -1
- package/dist/word/word.test.js +11 -1
- package/dist/word/word.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { pageIsMirrored } from '../vdt';
|
|
2
|
+
import { contourBounds, scaleContourAbout, shiftContour } from '../contour';
|
|
3
|
+
import { frameContour } from '../composed/contour';
|
|
1
4
|
import { dimensionToPx } from '../units';
|
|
2
5
|
import { frameBounds } from '../composed/paint';
|
|
3
6
|
import { FRAME_ATTR } from '../composed/wrap';
|
|
@@ -5,21 +8,455 @@ import { framePlaceable, nextFrameOf, groupPlaceable, placeBox, unionRect } from
|
|
|
5
8
|
import { composedOrder, groupOfFrames, layoutGroup } from '../composed/groups';
|
|
6
9
|
import { reflowEvents } from '../composed/reflowLog';
|
|
7
10
|
import { anchorTarget } from '../composed/templates';
|
|
11
|
+
import { continuationTemplate, continuesOffPage } from '../composed/continuation';
|
|
8
12
|
import { resolvePageTemplate } from '../composed/templates';
|
|
13
|
+
import { frameElementId } from '../composed/ids';
|
|
9
14
|
import { offsetCalloutToAbsolute } from './calloutLayout';
|
|
15
|
+
import { isMarkerBlock } from './buildHelpers';
|
|
10
16
|
import { CUT_START, fragmentRange, makeCalloutLayouter, splitCalloutFragment } from './frameLayout';
|
|
11
17
|
const MM_PER_IN = 25.4;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const NO_FIT = { patches: new Map(), text: new Map(), pictures: new Map(), yieldAll: new Set(), goOn: new Set() };
|
|
19
|
+
/** Step of the text and picture scales the ladder tries (DESIGN §5.4). */
|
|
20
|
+
const FIT_STEP = 0.025;
|
|
21
|
+
/** Lowest text scale an `allowShrink` proposal goes to. */
|
|
22
|
+
const PROPOSAL_MIN_SCALE = 0.5;
|
|
23
|
+
/** Most compositions of one page the ladder and its proposals may run. */
|
|
24
|
+
const COMPOSE_BUDGET = 96;
|
|
25
|
+
/** The scales from just under 1 (or under `from`) down to `min`, in
|
|
26
|
+
* {@link FIT_STEP} steps, `min` last. */
|
|
27
|
+
function scaleSteps(min, from = 1) {
|
|
28
|
+
const out = [];
|
|
29
|
+
for (let k = 1;; k++) {
|
|
30
|
+
const s = Math.round((from - k * FIT_STEP) * 1e4) / 1e4;
|
|
31
|
+
if (s <= min + 1e-6)
|
|
32
|
+
break;
|
|
33
|
+
out.push(s);
|
|
34
|
+
}
|
|
35
|
+
if (min < from - 1e-6)
|
|
36
|
+
out.push(Math.round(min * 1e4) / 1e4);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The template with the ladder's patches on its frames. */
|
|
40
|
+
function patchTemplate(template, patches) {
|
|
41
|
+
if (patches.size === 0)
|
|
42
|
+
return template;
|
|
43
|
+
return { ...template, frames: template.frames.map((f) => (patches.has(f.name) ? { ...f, ...patches.get(f.name) } : f)) };
|
|
44
|
+
}
|
|
45
|
+
/** A fixed height (a length, not a percentage). */
|
|
46
|
+
const fixedHeight = (f) => {
|
|
47
|
+
const h = f.height;
|
|
48
|
+
return h !== null && typeof h === 'object' && 'value' in h && 'unit' in h ? f.height : undefined;
|
|
49
|
+
};
|
|
50
|
+
/** The frame before each frame of a chain on the page. On a page added by
|
|
51
|
+
* continuation (#859) a frame that receives content starts its chain. */
|
|
52
|
+
function chainPrev(template, receivers) {
|
|
53
|
+
const names = new Set(template.frames.map((f) => f.name));
|
|
17
54
|
const prev = new Map();
|
|
18
55
|
for (const f of template.frames) {
|
|
19
56
|
const n = nextFrameOf(f);
|
|
20
|
-
if (n &&
|
|
57
|
+
if (n && names.has(n) && !prev.has(n) && !receivers?.has(n))
|
|
21
58
|
prev.set(n, f.name);
|
|
22
59
|
}
|
|
60
|
+
return prev;
|
|
61
|
+
}
|
|
62
|
+
const sameCut = (a, b) => a.child === b.child && a.line === b.line && JSON.stringify(a.group ?? null) === JSON.stringify(b.group ?? null);
|
|
63
|
+
export function layoutComposedPage(given) {
|
|
64
|
+
// A page added by continuation (#859) lays its template with the
|
|
65
|
+
// continuation's rules (and, for the template laid again, its other
|
|
66
|
+
// frames collapsed).
|
|
67
|
+
const input = given.continuing ? { ...given, template: continuationTemplate(given.template, given.continuing) } : given;
|
|
68
|
+
const { template, fence, dpi } = input;
|
|
69
|
+
const fi = fence.fenceIndex;
|
|
70
|
+
const mmPx = dpi / MM_PER_IN;
|
|
71
|
+
const byName = new Map(template.frames.map((f) => [f.name, f]));
|
|
72
|
+
const groupOf = groupOfFrames(template);
|
|
73
|
+
const prev = chainPrev(template, input.continuing?.receivers);
|
|
74
|
+
const headOf = (name) => {
|
|
75
|
+
let at = name;
|
|
76
|
+
const seen = new Set([at]);
|
|
77
|
+
for (let p = prev.get(at); p !== undefined && !seen.has(p); p = prev.get(at)) {
|
|
78
|
+
seen.add(p);
|
|
79
|
+
at = p;
|
|
80
|
+
}
|
|
81
|
+
return at;
|
|
82
|
+
};
|
|
83
|
+
// Compositions by fit state: the ladder and its proposals ask for some
|
|
84
|
+
// twice.
|
|
85
|
+
const memo = new Map();
|
|
86
|
+
let runs = 0;
|
|
87
|
+
const keyOf = (tpl, fit) => JSON.stringify([tpl.id, [...fit.patches], [...fit.text], [...fit.pictures], [...fit.yieldAll], [...fit.goOn]]);
|
|
88
|
+
const compose = (fit, tpl = template) => {
|
|
89
|
+
const key = keyOf(tpl, fit);
|
|
90
|
+
const known = memo.get(key);
|
|
91
|
+
if (known)
|
|
92
|
+
return known;
|
|
93
|
+
runs++;
|
|
94
|
+
const c = composePage(input, patchTemplate(tpl, fit.patches), fit);
|
|
95
|
+
memo.set(key, c);
|
|
96
|
+
return c;
|
|
97
|
+
};
|
|
98
|
+
const frameIn = (c, name) => c.frames.find((f) => f.name === name);
|
|
99
|
+
const overflows = (c, name) => (frameIn(c, name)?.overflowPx ?? 0) > 0.5;
|
|
100
|
+
/** The ladder's last step (#859): the frame is cut at its foot and the
|
|
101
|
+
* rest goes on on another page, its text and pictures back at full
|
|
102
|
+
* size (they go on anyway). */
|
|
103
|
+
const goingOn = (f, frame) => {
|
|
104
|
+
const text = new Map(f.text);
|
|
105
|
+
text.delete(headOf(frame.name));
|
|
106
|
+
const pictures = new Map([...f.pictures].filter(([, p]) => p.by !== frame.name));
|
|
107
|
+
return { ...f, text, pictures, goOn: new Set([...f.goOn, frame.name]) };
|
|
108
|
+
};
|
|
109
|
+
// The ladder, frame by frame in template order (ties break there).
|
|
110
|
+
let fit = NO_FIT;
|
|
111
|
+
let cur = compose(fit);
|
|
112
|
+
const tried = new Map();
|
|
113
|
+
const patched = (f, name, patch) => ({ ...f, patches: new Map([...f.patches, [name, { ...f.patches.get(name), ...patch }]]) });
|
|
114
|
+
const withText = (f, head, s) => ({ ...f, text: new Map([...f.text, [head, s]]) });
|
|
115
|
+
const withPictures = (f, heads, s, by) => {
|
|
116
|
+
const pictures = new Map(f.pictures);
|
|
117
|
+
for (const h of heads)
|
|
118
|
+
pictures.set(h, { scale: s, by });
|
|
119
|
+
return { ...f, pictures };
|
|
120
|
+
};
|
|
121
|
+
/** Take `next` when it makes `name` fit; else keep it anyway (each step
|
|
122
|
+
* builds on the ones before it at their limit) and say so. */
|
|
123
|
+
const attempt = (next, name) => {
|
|
124
|
+
fit = next;
|
|
125
|
+
cur = compose(fit);
|
|
126
|
+
return !overflows(cur, name);
|
|
127
|
+
};
|
|
128
|
+
/** The largest scale among `steps` (descending) at which `name` fits,
|
|
129
|
+
* checked at the lowest first, then by bisection; adopted. */
|
|
130
|
+
const searchScale = (steps, make, name) => {
|
|
131
|
+
if (steps.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
if (!attempt(make(steps[steps.length - 1]), name))
|
|
134
|
+
return false;
|
|
135
|
+
let lo = 0;
|
|
136
|
+
let hi = steps.length - 1; // fits at hi
|
|
137
|
+
while (lo < hi && runs < COMPOSE_BUDGET) {
|
|
138
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
139
|
+
if (!overflows(compose(make(steps[mid])), name))
|
|
140
|
+
hi = mid;
|
|
141
|
+
else
|
|
142
|
+
lo = mid + 1;
|
|
143
|
+
}
|
|
144
|
+
fit = make(steps[hi]);
|
|
145
|
+
cur = compose(fit);
|
|
146
|
+
return true;
|
|
147
|
+
};
|
|
148
|
+
/** Story heads of the frame and of its group's frames whose content holds
|
|
149
|
+
* a picture. */
|
|
150
|
+
const pictureHeads = (frame) => {
|
|
151
|
+
const g = groupOf.get(frame.name);
|
|
152
|
+
const names = g ? [frame.name, ...g.frames.filter((m) => m !== frame.name)] : [frame.name];
|
|
153
|
+
const heads = [];
|
|
154
|
+
for (const n of names) {
|
|
155
|
+
const head = headOf(n);
|
|
156
|
+
const story = input.stories.get(head);
|
|
157
|
+
if (!story || heads.includes(head))
|
|
158
|
+
continue;
|
|
159
|
+
for (let i = story.startIdx + 1; i < story.plan.endIdx; i++) {
|
|
160
|
+
if (input.pass.contentBlocks[i]?.type === 'resourceBlock') {
|
|
161
|
+
heads.push(head);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return heads;
|
|
167
|
+
};
|
|
168
|
+
/** The frame's story sets text (not only pictures): its text can
|
|
169
|
+
* shrink. */
|
|
170
|
+
const holdsText = (frame) => {
|
|
171
|
+
const story = input.stories.get(headOf(frame.name));
|
|
172
|
+
if (!story)
|
|
173
|
+
return false;
|
|
174
|
+
for (let i = story.startIdx + 1; i < story.plan.endIdx; i++) {
|
|
175
|
+
const b = input.pass.contentBlocks[i];
|
|
176
|
+
if (b && b.type !== 'resourceBlock' && b.type !== 'directive' && !isMarkerBlock(b))
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
};
|
|
181
|
+
const movesOthers = (frame) => {
|
|
182
|
+
const g = groupOf.get(frame.name);
|
|
183
|
+
if (g && g.frames.filter((m) => byName.has(m)).length > 1)
|
|
184
|
+
return true;
|
|
185
|
+
return template.frames.some((f) => f.name !== frame.name && !f.grid && anchorTarget(f.placement) === frame.name);
|
|
186
|
+
};
|
|
187
|
+
/** `free` applies: a fixed height with room up to a higher `maxHeight`. */
|
|
188
|
+
const growPatch = (frame) => {
|
|
189
|
+
const h = fixedHeight(frame);
|
|
190
|
+
if (!h || !frame.maxHeight)
|
|
191
|
+
return undefined;
|
|
192
|
+
if (dimensionToPx(frame.maxHeight, dpi, input.emPx) <= dimensionToPx(h, dpi, input.emPx) + 0.5)
|
|
193
|
+
return undefined;
|
|
194
|
+
const min = frame.minHeight && dimensionToPx(frame.minHeight, dpi, input.emPx) > dimensionToPx(h, dpi, input.emPx) ? frame.minHeight : h;
|
|
195
|
+
return { height: 'auto', minHeight: min };
|
|
196
|
+
};
|
|
197
|
+
for (const frame of template.frames) {
|
|
198
|
+
const name = frame.name;
|
|
199
|
+
if (!overflows(cur, name) || runs >= COMPOSE_BUDGET)
|
|
200
|
+
continue;
|
|
201
|
+
const steps = [];
|
|
202
|
+
tried.set(name, steps);
|
|
203
|
+
// 1. Free space: a fixed height grows to its maximum ('auto' heights
|
|
204
|
+
// already take the room their area and group leave).
|
|
205
|
+
steps.push('free');
|
|
206
|
+
const grow = growPatch(frame);
|
|
207
|
+
if (grow && attempt(patched(fit, name, grow), name))
|
|
208
|
+
continue;
|
|
209
|
+
// 2. Push siblings: the group moved the frames after it and the frames
|
|
210
|
+
// that shrink gave space back when it was laid.
|
|
211
|
+
if (movesOthers(frame))
|
|
212
|
+
steps.push('push');
|
|
213
|
+
// 3. Pictures of the frame and its group, down to imageMinScale.
|
|
214
|
+
const heads = frame.fit.scaleImages ? pictureHeads(frame) : [];
|
|
215
|
+
if (heads.length > 0 && frame.fit.imageMinScale < 1) {
|
|
216
|
+
steps.push('scaleImages');
|
|
217
|
+
if (searchScale(scaleSteps(frame.fit.imageMinScale), (s) => withPictures(fit, heads, s, name), name))
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
// 4. Redistribute: every other frame of the group gives space (or
|
|
221
|
+
// width) back, down to its minimum or its content.
|
|
222
|
+
const g = groupOf.get(name);
|
|
223
|
+
if (g && g.frames.filter((m) => byName.has(m)).length > 1) {
|
|
224
|
+
steps.push('redistribute');
|
|
225
|
+
if (attempt({ ...fit, yieldAll: new Set([...fit.yieldAll, g.name]) }, name))
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
// 5. The frame's text (its whole story), down to fit.minScale.
|
|
229
|
+
if (frame.fit.shrinkText && frame.fit.minScale < 1 && holdsText(frame)) {
|
|
230
|
+
steps.push('shrinkText');
|
|
231
|
+
const head = headOf(name);
|
|
232
|
+
if (searchScale(scaleSteps(frame.fit.minScale), (s) => withText(fit, head, s), name))
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// 6. Continuation on another page (`overflow.continueIn: 'next-page'`
|
|
236
|
+
// or `'template:<id>'`, #859). On a page added by continuation, a
|
|
237
|
+
// frame that would take none of what it receives sends nothing on
|
|
238
|
+
// (it would go on for ever): it warns.
|
|
239
|
+
if (continuesOffPage(frame.overflow.continueIn)) {
|
|
240
|
+
steps.push('continue');
|
|
241
|
+
const before = { fit, cur };
|
|
242
|
+
if (attempt(goingOn(fit, frame), name) && (!input.continuing || cur.spills.get(name)?.advanced !== false))
|
|
243
|
+
continue;
|
|
244
|
+
({ fit, cur } = before);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// 7. Warn, with what would make each frame that still overflows fit.
|
|
248
|
+
const warnings = [...(cur.notes ?? [])];
|
|
249
|
+
const templateAttr = fence.attrSources.template;
|
|
250
|
+
for (const frame of template.frames) {
|
|
251
|
+
const vdt = frameIn(cur, frame.name);
|
|
252
|
+
if (!vdt || vdt.overflowPx <= 0.5)
|
|
253
|
+
continue;
|
|
254
|
+
const overflowMm = Math.round((vdt.overflowPx / mmPx) * 10) / 10;
|
|
255
|
+
const proposals = overflowProposals({
|
|
256
|
+
frame,
|
|
257
|
+
vdt,
|
|
258
|
+
cur,
|
|
259
|
+
fit,
|
|
260
|
+
compose,
|
|
261
|
+
overflows,
|
|
262
|
+
headOf,
|
|
263
|
+
withText,
|
|
264
|
+
patched,
|
|
265
|
+
groupOf,
|
|
266
|
+
byName,
|
|
267
|
+
mmPx,
|
|
268
|
+
dpi,
|
|
269
|
+
emPx: input.emPx,
|
|
270
|
+
alternatives: input.alternatives,
|
|
271
|
+
templateSource: templateAttr ? { sourceStart: templateAttr.sourceStart + input.bodyOffset, sourceEnd: templateAttr.sourceEnd + input.bodyOffset } : undefined,
|
|
272
|
+
budgetLeft: () => runs < COMPOSE_BUDGET,
|
|
273
|
+
holdsText: holdsText(frame),
|
|
274
|
+
});
|
|
275
|
+
warnings.push({
|
|
276
|
+
kind: 'frameOverflow',
|
|
277
|
+
template: template.id,
|
|
278
|
+
frame: frame.name,
|
|
279
|
+
fenceIndex: fi,
|
|
280
|
+
overflowMm,
|
|
281
|
+
configPath: frame.configPath,
|
|
282
|
+
...(input.locale ? { locale: input.locale } : {}),
|
|
283
|
+
stepsTried: tried.get(frame.name) ?? ['free'],
|
|
284
|
+
scale: vdt.scale,
|
|
285
|
+
...(vdt.imageScale !== undefined ? { imageScale: vdt.imageScale } : {}),
|
|
286
|
+
proposals,
|
|
287
|
+
...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}),
|
|
288
|
+
pageIndex: input.page.index,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const { frames, blocks, log, laid, fitted } = cur;
|
|
292
|
+
// Template order on the page record.
|
|
293
|
+
const order = new Map(template.frames.map((f, i) => [f.name, i]));
|
|
294
|
+
frames.sort((a, b) => order.get(a.name) - order.get(b.name));
|
|
295
|
+
const contentBlocks = input.pass.contentBlocks;
|
|
296
|
+
const composed = {
|
|
297
|
+
template: template.id,
|
|
298
|
+
...(template.variant ? { variant: template.variant } : {}),
|
|
299
|
+
fenceIndex: fi,
|
|
300
|
+
role: template.role,
|
|
301
|
+
attrs: { ...fence.attrs },
|
|
302
|
+
attrSources: Object.fromEntries(Object.entries(fence.attrSources).map(([k, v]) => [k, { sourceStart: v.sourceStart + input.bodyOffset, sourceEnd: v.sourceEnd + input.bodyOffset }])),
|
|
303
|
+
frames,
|
|
304
|
+
log,
|
|
305
|
+
configPath: template.configPath,
|
|
306
|
+
sourceStart: contentBlocks[input.startIdx].sourceStart + input.bodyOffset,
|
|
307
|
+
sourceEnd: contentBlocks[input.endIdx].sourceEnd + input.bodyOffset,
|
|
308
|
+
};
|
|
309
|
+
// What goes on on other pages (#859), in template order.
|
|
310
|
+
const continuations = [];
|
|
311
|
+
for (const frame of template.frames) {
|
|
312
|
+
const spill = cur.spills.get(frame.name);
|
|
313
|
+
const story = spill ? input.stories.get(spill.head) : undefined;
|
|
314
|
+
if (!spill || !story)
|
|
315
|
+
continue;
|
|
316
|
+
const came = input.continuing?.receivers.get(spill.head);
|
|
317
|
+
continuations.push({
|
|
318
|
+
frame: frame.name,
|
|
319
|
+
fromFrameId: spill.frameId,
|
|
320
|
+
rule: frame.overflow.continueIn,
|
|
321
|
+
template: template.id,
|
|
322
|
+
collapsed: input.continuing?.collapse ?? false,
|
|
323
|
+
story,
|
|
324
|
+
cut: spill.cut,
|
|
325
|
+
pieces: spill.pieces,
|
|
326
|
+
headBlockId: spill.headBlockId,
|
|
327
|
+
slot: came?.slot ?? frame.name,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return { composed, blocks, warnings, laid, ...(fitted.size > 0 ? { fitted } : {}), ...(continuations.length > 0 ? { continuations } : {}) };
|
|
331
|
+
}
|
|
332
|
+
const mmDim = (value) => ({ value, unit: 'mm' });
|
|
333
|
+
/** What would make `frame`'s content fit, each value worked out from the
|
|
334
|
+
* shortfall and, where the page can tell, tried on it (DESIGN §5.7). */
|
|
335
|
+
function overflowProposals(c) {
|
|
336
|
+
const { frame, vdt, cur, fit, mmPx } = c;
|
|
337
|
+
const name = frame.name;
|
|
338
|
+
const out = [];
|
|
339
|
+
const path = (key) => frame.fieldPaths[key] ?? `${frame.configPath}/${key}`;
|
|
340
|
+
const fits = (next, tpl) => c.budgetLeft() && !c.overflows(c.compose(next, tpl), name);
|
|
341
|
+
// More shrink: the largest text scale, in 2.5 % steps, at which it fits.
|
|
342
|
+
const head = c.headOf(name);
|
|
343
|
+
const from = frame.fit.shrinkText ? Math.min(frame.fit.minScale, vdt.scale) : 1;
|
|
344
|
+
const steps = scaleSteps(PROPOSAL_MIN_SCALE, from);
|
|
345
|
+
if (c.holdsText && steps.length > 0 && fits(c.withText(fit, head, steps[steps.length - 1]))) {
|
|
346
|
+
let lo = 0;
|
|
347
|
+
let hi = steps.length - 1;
|
|
348
|
+
while (lo < hi && c.budgetLeft()) {
|
|
349
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
350
|
+
if (fits(c.withText(fit, head, steps[mid])))
|
|
351
|
+
hi = mid;
|
|
352
|
+
else
|
|
353
|
+
lo = mid + 1;
|
|
354
|
+
}
|
|
355
|
+
out.push({
|
|
356
|
+
kind: 'allowShrink',
|
|
357
|
+
path: path('fit/minScale'),
|
|
358
|
+
value: steps[hi],
|
|
359
|
+
...(frame.fit.shrinkText ? {} : { also: [{ path: path('fit/shrinkText'), value: true }] }),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// A higher maximum: the frame's height plus the shortfall, in whole mm.
|
|
363
|
+
const fixed = fixedHeight(frame);
|
|
364
|
+
if (frame.maxHeight || fixed) {
|
|
365
|
+
const want = Math.ceil((vdt.rect.height + vdt.overflowPx) / mmPx - 1e-6);
|
|
366
|
+
const patch = { maxHeight: mmDim(want), ...(fixed ? { height: 'auto', minHeight: fixed } : {}) };
|
|
367
|
+
if (fits(c.patched(fit, name, patch)))
|
|
368
|
+
out.push({ kind: 'raiseMax', path: path('maxHeight'), value: mmDim(want) });
|
|
369
|
+
}
|
|
370
|
+
// A lower minimum on a frame of its group that stands at its minimum
|
|
371
|
+
// with less content than that: lowered by the shortfall, not below its
|
|
372
|
+
// content.
|
|
373
|
+
const g = c.groupOf.get(name);
|
|
374
|
+
if (g) {
|
|
375
|
+
for (const other of g.frames) {
|
|
376
|
+
const f = c.byName.get(other);
|
|
377
|
+
if (!f || other === name || f.pin || !f.minHeight)
|
|
378
|
+
continue;
|
|
379
|
+
const box = cur.frames.find((x) => x.name === other);
|
|
380
|
+
if (!box)
|
|
381
|
+
continue;
|
|
382
|
+
const minPx = dimensionToPx(f.minHeight, c.dpi, c.emPx);
|
|
383
|
+
const need = cur.needs.get(other)?.() ?? 0;
|
|
384
|
+
if (Math.abs(box.rect.height - minPx) > 0.5 || need >= minPx - mmPx)
|
|
385
|
+
continue;
|
|
386
|
+
const lowered = Math.max(Math.ceil(need / mmPx - 1e-6), Math.floor((minPx - vdt.overflowPx) / mmPx + 1e-6));
|
|
387
|
+
if (lowered * mmPx >= minPx - 0.5)
|
|
388
|
+
continue;
|
|
389
|
+
if (fits(c.patched(fit, other, { minHeight: mmDim(lowered) }))) {
|
|
390
|
+
out.push({ kind: 'lowerMin', path: f.fieldPaths.minHeight ?? `${f.configPath}/minHeight`, value: mmDim(lowered), frame: other });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Continue on the next page (#859 lays it out).
|
|
395
|
+
if (!frame.overflow.continueIn)
|
|
396
|
+
out.push({ kind: 'continueNextPage', path: path('overflow/continueIn'), value: 'next-page' });
|
|
397
|
+
// Another template of the book with a frame of this name, where the
|
|
398
|
+
// content fits as it is.
|
|
399
|
+
for (const alt of c.alternatives?.() ?? []) {
|
|
400
|
+
if (!c.budgetLeft())
|
|
401
|
+
break;
|
|
402
|
+
if (!alt.frames.some((f) => f.name === name))
|
|
403
|
+
continue;
|
|
404
|
+
const trial = c.compose(NO_FIT, alt);
|
|
405
|
+
const laidThere = trial.frames.find((f) => f.name === name);
|
|
406
|
+
if (laidThere && laidThere.blockIds.length > 0 && laidThere.overflowPx <= 0.5) {
|
|
407
|
+
out.push({ kind: 'useTemplate', value: alt.id, ...(c.templateSource ?? {}) });
|
|
408
|
+
break;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Shorter text: the lines that do not fit, at the frame's characters a
|
|
412
|
+
// line.
|
|
413
|
+
const lines = cur.blocks.filter((b) => vdt.blockIds.includes(b.id) && b.type !== 'resource').flatMap((b) => b.lines.filter((l) => l.text.length > 0));
|
|
414
|
+
if (lines.length > 0) {
|
|
415
|
+
// Characters of a full line: the lines that do not end their paragraph.
|
|
416
|
+
const full = lines.filter((l) => !l.isLastLine);
|
|
417
|
+
const sample = full.length > 0 ? full : lines;
|
|
418
|
+
const perLine = sample.reduce((n, l) => n + l.text.length, 0) / sample.length;
|
|
419
|
+
const lineH = (lines.reduce((h, l) => h + l.bbox.height, 0) / lines.length) * vdt.scale;
|
|
420
|
+
if (lineH > 0) {
|
|
421
|
+
const over = Math.ceil(vdt.overflowPx / lineH - 1e-6);
|
|
422
|
+
out.push({ kind: 'shortenText', value: Math.ceil(over * perLine), ...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}) });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
/** A frame style whose box keeps its drawn size when the frame's text is
|
|
428
|
+
* set at `scale` (#858): the frame is laid out `1 / scale` as large and
|
|
429
|
+
* painted at `scale`, so its padding, border, corner radius, stripe and
|
|
430
|
+
* tab are laid out `1 / scale` as large to come out as drawn. Only the
|
|
431
|
+
* text and what it holds shrink: padding that keeps room for the photos
|
|
432
|
+
* set over a box stays that room. */
|
|
433
|
+
export function keepChromeAt(style, scale) {
|
|
434
|
+
if (scale === 1)
|
|
435
|
+
return style;
|
|
436
|
+
const d = (v) => ({ ...v, value: v.value / scale });
|
|
437
|
+
const p = style.padding;
|
|
438
|
+
return {
|
|
439
|
+
...style,
|
|
440
|
+
padding: { top: d(p.top), right: d(p.right), bottom: d(p.bottom), left: d(p.left) },
|
|
441
|
+
border: { ...style.border, width: d(style.border.width) },
|
|
442
|
+
borderRadius: d(style.borderRadius),
|
|
443
|
+
stripe: { ...style.stripe, width: d(style.stripe.width) },
|
|
444
|
+
...(style.label
|
|
445
|
+
? { label: { ...style.label, height: d(style.label.height), paddingX: d(style.label.paddingX), offset: d(style.label.offset), inset: d(style.label.inset), rule: { ...style.label.rule, width: d(style.label.rule.width) } } }
|
|
446
|
+
: {}),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/** Lay out every frame of the page once, with the reflow ladder's `fit`. */
|
|
450
|
+
function composePage(input, template, fit) {
|
|
451
|
+
const { pass, fence, dpi } = input;
|
|
452
|
+
const fi = fence.fenceIndex;
|
|
453
|
+
const cont = input.continuing;
|
|
454
|
+
/** A frame's id: `cp<fence>:<name>`; on a page added by continuation
|
|
455
|
+
* (#859) `~<k>`, a frame that receives content named by the frame of
|
|
456
|
+
* the fence's page that first sent it on. */
|
|
457
|
+
const frameIdOf = (name) => (cont ? frameElementId(fi, cont.receivers.get(name)?.slot ?? name, cont.generation) : frameElementId(fi, name));
|
|
458
|
+
const byName = new Map(template.frames.map((f) => [f.name, f]));
|
|
459
|
+
const prev = chainPrev(template, cont?.receivers);
|
|
23
460
|
/** The head of the chain a frame belongs to. */
|
|
24
461
|
const headOf = (name) => {
|
|
25
462
|
let at = name;
|
|
@@ -30,11 +467,34 @@ export function layoutComposedPage(input) {
|
|
|
30
467
|
}
|
|
31
468
|
return at;
|
|
32
469
|
};
|
|
470
|
+
/** The text scale of a frame's story (#858). */
|
|
471
|
+
const textScale = (frame) => fit.text.get(headOf(frame.name)) ?? 1;
|
|
472
|
+
/** The style a frame is laid out in: its box as drawn at any text scale. */
|
|
473
|
+
const styleOf = (frame) => {
|
|
474
|
+
const style = keepChromeAt(input.styleOf(frame), textScale(frame));
|
|
475
|
+
if (!fit.goOn.has(frame.name))
|
|
476
|
+
return style;
|
|
477
|
+
// A frame cut for another page (#859) ends with its mark: its own
|
|
478
|
+
// words, none (`false`), or its style's localized ones.
|
|
479
|
+
const mark = frame.overflow.mark;
|
|
480
|
+
return { ...style, continuesMarkerEnabled: mark !== false, continuesMarker: typeof mark === 'string' ? mark : style.continuesMarker };
|
|
481
|
+
};
|
|
482
|
+
/** The scale of a frame's pictures (#858), below 1, or undefined. */
|
|
483
|
+
const pictureScale = (frame) => fit.pictures.get(headOf(frame.name))?.scale;
|
|
484
|
+
/** Rectangles in a frame's box coordinates, in its laid-out (scaled)
|
|
485
|
+
* coordinates. */
|
|
486
|
+
const unscale = (rs, s) => rs && s !== 1
|
|
487
|
+
? rs.map((r) => ({ x: r.x / s, y: r.y / s, width: r.width / s, height: r.height / s, ...(r.contour ? { contour: scaleContourAbout(r.contour, 0, 0, 1 / s) } : {}) }))
|
|
488
|
+
: rs;
|
|
33
489
|
/** Place and fill every frame. `seed`: the boxes of the frames that push
|
|
34
490
|
* text, from a first run, so a frame placed before one that pushes it
|
|
35
491
|
* (the photo anchored to its box) still runs round it. */
|
|
36
|
-
const run = (seed) => {
|
|
492
|
+
const run = (seed, seedShapes) => {
|
|
37
493
|
const pushRects = new Map();
|
|
494
|
+
/** The shapes of the frames that push with their pictures' contour (#883). */
|
|
495
|
+
const pushShapes = new Map();
|
|
496
|
+
/** Pictures of a contour push whose shape is not known (#883). */
|
|
497
|
+
const notes = [];
|
|
38
498
|
const states = new Map();
|
|
39
499
|
const stateOf = (frame) => {
|
|
40
500
|
const head = headOf(frame.name);
|
|
@@ -45,13 +505,16 @@ export function layoutComposedPage(input) {
|
|
|
45
505
|
const headFrame = byName.get(head);
|
|
46
506
|
if (!story || !headFrame)
|
|
47
507
|
return undefined;
|
|
508
|
+
// Content from the page before (#859) goes on where it was cut, its
|
|
509
|
+
// blocks numbered on from its pieces there.
|
|
510
|
+
const came = cont?.receivers.get(head);
|
|
48
511
|
const st = {
|
|
49
512
|
story,
|
|
50
|
-
L: makeCalloutLayouter(pass, story.startIdx, story.plan,
|
|
51
|
-
cut: CUT_START,
|
|
52
|
-
pieces: 0,
|
|
53
|
-
headBlockId: frameIdOf(head),
|
|
54
|
-
headFrameId: frameIdOf(head),
|
|
513
|
+
L: makeCalloutLayouter(pass, story.startIdx, story.plan, styleOf(frame)),
|
|
514
|
+
cut: came?.cut ?? CUT_START,
|
|
515
|
+
pieces: came?.pieces ?? 0,
|
|
516
|
+
headBlockId: came?.headBlockId ?? frameIdOf(head),
|
|
517
|
+
headFrameId: came?.fromFrameId ?? frameIdOf(head),
|
|
55
518
|
};
|
|
56
519
|
states.set(head, st);
|
|
57
520
|
return st;
|
|
@@ -82,10 +545,16 @@ export function layoutComposedPage(input) {
|
|
|
82
545
|
const frames = [];
|
|
83
546
|
const vdtByName = new Map();
|
|
84
547
|
const blocks = [];
|
|
85
|
-
const warnings = [];
|
|
86
548
|
const overflowLog = [];
|
|
549
|
+
const fitLog = [];
|
|
550
|
+
const needs = new Map();
|
|
551
|
+
/** What `fitBox` did to each frame (#861), last placement wins. */
|
|
552
|
+
const fitted = new Map();
|
|
87
553
|
const laid = new Set();
|
|
88
|
-
const
|
|
554
|
+
const spills = new Map();
|
|
555
|
+
const pushed = seed
|
|
556
|
+
? [...seed].map(([name, box]) => ({ name, box, ...(seedShapes?.get(name) ? { contour: seedShapes.get(name) } : {}) }))
|
|
557
|
+
: [];
|
|
89
558
|
/** The rectangles of the pushing frames placed so far that `rect`
|
|
90
559
|
* meets, in the frame block's coordinates. */
|
|
91
560
|
const exclusionsFor = (frame, rect, open) => {
|
|
@@ -94,11 +563,17 @@ export function layoutComposedPage(input) {
|
|
|
94
563
|
for (const p of pushed) {
|
|
95
564
|
if (p.name === frame.name)
|
|
96
565
|
continue;
|
|
97
|
-
|
|
566
|
+
// A contour (#883) pushes with its shape, its margin round it.
|
|
567
|
+
const cb = p.contour ? contourBounds(p.contour) : undefined;
|
|
568
|
+
const b = cb ?? p.box;
|
|
569
|
+
const e = cb ? 0 : g;
|
|
98
570
|
const below = open ? true : b.y < rect.y + rect.height;
|
|
99
|
-
if (b.x -
|
|
571
|
+
if (b.x - e >= rect.x + rect.width || b.x + b.width + e <= rect.x || !below || b.y + b.height + e <= rect.y)
|
|
100
572
|
continue;
|
|
101
|
-
out.push({
|
|
573
|
+
out.push({
|
|
574
|
+
x: b.x - e - rect.x, y: b.y - e - rect.y, width: b.width + 2 * e, height: b.height + 2 * e,
|
|
575
|
+
...(cb && p.contour ? { contour: shiftContour(p.contour, -rect.x, -rect.y) } : {}),
|
|
576
|
+
});
|
|
102
577
|
}
|
|
103
578
|
return out.length > 0 ? out : undefined;
|
|
104
579
|
};
|
|
@@ -107,7 +582,7 @@ export function layoutComposedPage(input) {
|
|
|
107
582
|
// Each frame of a chain sets the story in its own style: the cuts are
|
|
108
583
|
// positions in the story's blocks, the same in any.
|
|
109
584
|
if (st)
|
|
110
|
-
st.L = makeCalloutLayouter(pass, st.story.startIdx, st.story.plan,
|
|
585
|
+
st.L = makeCalloutLayouter(pass, st.story.startIdx, st.story.plan, styleOf(frame));
|
|
111
586
|
// A frame continues its story only once the frame before it in the
|
|
112
587
|
// chain was filled (a frame placed before it, through an anchor, starts
|
|
113
588
|
// empty).
|
|
@@ -119,9 +594,13 @@ export function layoutComposedPage(input) {
|
|
|
119
594
|
return { frame, st, before, holds, continuation, blockId };
|
|
120
595
|
};
|
|
121
596
|
/** The content's height at `width`, running round the pushing frames
|
|
122
|
-
* below the top of `at` (the frame's box as first placed).
|
|
597
|
+
* below the top of `at` (the frame's box as first placed). A frame
|
|
598
|
+
* whose text the ladder scales (#858) is laid out `1 / scale` as
|
|
599
|
+
* wide and is `scale` as tall as that. */
|
|
123
600
|
const naturalOf = (p, at) => {
|
|
124
601
|
const memo = new Map();
|
|
602
|
+
const s = textScale(p.frame);
|
|
603
|
+
const pic = pictureScale(p.frame);
|
|
125
604
|
return (width) => {
|
|
126
605
|
if (!p.holds)
|
|
127
606
|
return 0;
|
|
@@ -129,10 +608,12 @@ export function layoutComposedPage(input) {
|
|
|
129
608
|
const known = memo.get(key);
|
|
130
609
|
if (known !== undefined)
|
|
131
610
|
return known;
|
|
132
|
-
const exclusions = at ? exclusionsFor(p.frame, { ...at, width }, true) : undefined;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
611
|
+
const exclusions = at ? unscale(exclusionsFor(p.frame, { ...at, width }, true), s) : undefined;
|
|
612
|
+
const opts = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}) };
|
|
613
|
+
const r = p.st.L.layoutRange(p.st.cut, p.st.L.end, width / s, p.blockId, p.continuation, input.mirrored, opts.exclusions || opts.pictureScale !== undefined ? opts : undefined);
|
|
614
|
+
const h = r.totalHeight * s;
|
|
615
|
+
memo.set(key, h);
|
|
616
|
+
return h;
|
|
136
617
|
};
|
|
137
618
|
};
|
|
138
619
|
/** `placeBox`, then the box the page lets the frame take (`fitBox`:
|
|
@@ -143,10 +624,18 @@ export function layoutComposedPage(input) {
|
|
|
143
624
|
const r = placeBox(placeable, template, ctx, refs, nat);
|
|
144
625
|
if (!input.fitBox)
|
|
145
626
|
return r;
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
627
|
+
const keep = (placed) => {
|
|
628
|
+
const kept = input.fitBox(frame, placed);
|
|
629
|
+
if (sameRect(kept, placed))
|
|
630
|
+
fitted.delete(frame.name);
|
|
631
|
+
else
|
|
632
|
+
fitted.set(frame.name, { placed, kept });
|
|
633
|
+
return kept;
|
|
634
|
+
};
|
|
635
|
+
const first = keep(r);
|
|
636
|
+
if (Math.abs(first.width - r.width) < 0.01)
|
|
637
|
+
return first;
|
|
638
|
+
return keep(placeBox(placeable, template, ctx, refs, () => nat(first.width)));
|
|
150
639
|
};
|
|
151
640
|
/** A frame's height as its template declares it: its `minHeight` where
|
|
152
641
|
* the height follows the content, else the content's (`natural`). */
|
|
@@ -177,10 +666,13 @@ export function layoutComposedPage(input) {
|
|
|
177
666
|
return rect;
|
|
178
667
|
};
|
|
179
668
|
/** Lay a group: its frames' boxes as the template declares them and as
|
|
180
|
-
* their content needs (`composed/groups.ts`).
|
|
669
|
+
* their content needs (`composed/groups.ts`). With the ladder's
|
|
670
|
+
* `redistribute` (#858) every frame gives space back, down to its
|
|
671
|
+
* minimum or its content, whether it `shrink`s or not. */
|
|
181
672
|
const placeGroup = (group, members) => {
|
|
182
673
|
const own = groupPlaceable(group);
|
|
183
674
|
const mmPx = dpi / MM_PER_IN;
|
|
675
|
+
const yieldAll = fit.yieldAll.has(group.name);
|
|
184
676
|
const preps = members.map(prepare);
|
|
185
677
|
const solve = (mode, naturals) => {
|
|
186
678
|
const refs = mode === 'final' ? refOf : declaredRefOf;
|
|
@@ -200,12 +692,13 @@ export function layoutComposedPage(input) {
|
|
|
200
692
|
const fill = f.height === 'fill';
|
|
201
693
|
const minH = f.minHeight ? dimensionToPx(f.minHeight, dpi, input.emPx) : 0;
|
|
202
694
|
const want = fill ? minH : auto ? (mode === 'final' ? natural(box.width) : declaredNatural(f, natural)(box.width)) : box.height;
|
|
203
|
-
|
|
695
|
+
const max = f.maxHeight ? dimensionToPx(f.maxHeight, dpi, input.emPx) : Infinity;
|
|
696
|
+
const item = {
|
|
204
697
|
name: f.name,
|
|
205
698
|
own: box,
|
|
206
699
|
want,
|
|
207
700
|
min: minH,
|
|
208
|
-
max
|
|
701
|
+
max,
|
|
209
702
|
minWidth: f.minWidth ? dimensionToPx(f.minWidth, dpi, input.emPx) : 0,
|
|
210
703
|
maxWidth: f.maxWidth ? dimensionToPx(f.maxWidth, dpi, input.emPx) : Infinity,
|
|
211
704
|
grow: fill ? Math.max(1, f.grow) : f.grow,
|
|
@@ -215,6 +708,17 @@ export function layoutComposedPage(input) {
|
|
|
215
708
|
fixedHeight: !auto && !fill,
|
|
216
709
|
need: natural,
|
|
217
710
|
};
|
|
711
|
+
if (yieldAll && mode === 'final' && !f.pin) {
|
|
712
|
+
// Down to its minimum, never under what its content needs, never
|
|
713
|
+
// over the height it has.
|
|
714
|
+
const height = Math.min(Math.max(want, minH), max);
|
|
715
|
+
item.shrink = f.shrink > 0 ? f.shrink : 1;
|
|
716
|
+
// (A row's frames give width, each down to the narrowest width
|
|
717
|
+
// its content still fits: their heights stay the row's.)
|
|
718
|
+
if (group.kind === 'stack')
|
|
719
|
+
item.min = Math.min(height, Math.max(minH, natural(box.width)));
|
|
720
|
+
}
|
|
721
|
+
return item;
|
|
218
722
|
});
|
|
219
723
|
const firstFree = items.find((it) => !it.pin) ?? items[0];
|
|
220
724
|
const start = firstFree?.own ?? input.areas.area;
|
|
@@ -257,41 +761,59 @@ export function layoutComposedPage(input) {
|
|
|
257
761
|
groupDeclared.set(group.name, decl.box);
|
|
258
762
|
return fin.rects;
|
|
259
763
|
};
|
|
260
|
-
/** Pour a frame's content into its box and record the frame.
|
|
764
|
+
/** Pour a frame's content into its box and record the frame. A frame
|
|
765
|
+
* whose text the ladder scales (#858) is laid out in a box `1 / scale`
|
|
766
|
+
* as large, painted at `scale` (`frameMatrix`). */
|
|
261
767
|
const fill = (p, rect) => {
|
|
262
768
|
const { frame, st, holds, continuation, blockId, before } = p;
|
|
263
|
-
const style =
|
|
769
|
+
const style = styleOf(frame);
|
|
770
|
+
const s = textScale(frame);
|
|
771
|
+
const pic = pictureScale(frame);
|
|
772
|
+
const laidW = rect.width / s;
|
|
773
|
+
const laidH = rect.height / s;
|
|
264
774
|
let result;
|
|
265
775
|
let overflowPx = 0;
|
|
776
|
+
let goesOn = false;
|
|
266
777
|
const from = st?.cut ?? CUT_START;
|
|
267
778
|
if (holds) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
779
|
+
// What its content needs at its width, measured only when a
|
|
780
|
+
// proposal asks (`lowerMin`), from where the frame starts.
|
|
781
|
+
const width = rect.width;
|
|
782
|
+
const startAt = { ...p, st: { ...st, cut: from } };
|
|
783
|
+
let known;
|
|
784
|
+
needs.set(frame.name, () => (known ??= naturalOf(startAt)(width)));
|
|
785
|
+
const exclusions = unscale(exclusionsFor(frame, rect, false), s);
|
|
786
|
+
const extra = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}) };
|
|
787
|
+
const opts = { boxHeight: laidH, verticalAlign: frame.verticalAlign, ...extra };
|
|
788
|
+
const whole = st.L.layoutRange(from, st.L.end, laidW, blockId, continuation, input.mirrored, opts);
|
|
274
789
|
const nextName = nextFrameOf(frame);
|
|
275
790
|
const nextFrame = nextName !== undefined ? byName.get(nextName) : undefined;
|
|
276
|
-
|
|
277
|
-
|
|
791
|
+
const chained = nextFrame !== undefined && !vdtByName.has(nextFrame.name);
|
|
792
|
+
// Cut for another page (#859) when the chain ends here.
|
|
793
|
+
goesOn = !chained && fit.goOn.has(frame.name);
|
|
794
|
+
if ((whole.overflowPx ?? 0) > 0.5 && (chained || goesOn)) {
|
|
795
|
+
// The rest goes on in the next frame of the chain, or on another
|
|
796
|
+
// page.
|
|
278
797
|
const minLines = Math.max(1, style.splitMinLines);
|
|
279
|
-
const frag = splitCalloutFragment(pass, st.L, from,
|
|
798
|
+
const frag = splitCalloutFragment(pass, st.L, from, laidW, laidH, blockId, continuation, minLines, input.mirrored, extra.exclusions || extra.pictureScale !== undefined ? extra : undefined);
|
|
280
799
|
const to = frag?.to ?? from;
|
|
281
800
|
result = frag
|
|
282
|
-
? st.L.layoutRange(from, to,
|
|
283
|
-
|
|
801
|
+
? st.L.layoutRange(from, to, laidW, blockId, continuation, input.mirrored, opts)
|
|
802
|
+
// A styled frame none of whose content fits still draws its box
|
|
803
|
+
// (and its mark) when the content goes on elsewhere.
|
|
804
|
+
: goesOn && frame.style ? st.L.layoutRange(from, from, laidW, blockId, continuation, input.mirrored, { boxHeight: laidH }) : undefined;
|
|
284
805
|
st.cut = to;
|
|
285
806
|
}
|
|
286
807
|
else {
|
|
808
|
+
goesOn = false;
|
|
287
809
|
result = whole;
|
|
288
|
-
overflowPx = whole.overflowPx ?? 0;
|
|
810
|
+
overflowPx = (whole.overflowPx ?? 0) * s;
|
|
289
811
|
st.cut = st.L.end;
|
|
290
812
|
}
|
|
291
813
|
}
|
|
292
814
|
else if (st && frame.style) {
|
|
293
815
|
// A styled frame with nothing (left) to hold still draws its box.
|
|
294
|
-
result = st.L.layoutRange(st.L.end, st.L.end,
|
|
816
|
+
result = st.L.layoutRange(st.L.end, st.L.end, laidW, blockId, true, input.mirrored, { boxHeight: laidH });
|
|
295
817
|
}
|
|
296
818
|
const vdt = {
|
|
297
819
|
id: frameIdOf(frame.name),
|
|
@@ -300,7 +822,8 @@ export function layoutComposedPage(input) {
|
|
|
300
822
|
rotate: frame.rotate,
|
|
301
823
|
layer: frame.layer,
|
|
302
824
|
...(frame.style ? { styleId: frame.style } : {}),
|
|
303
|
-
scale:
|
|
825
|
+
scale: s,
|
|
826
|
+
...(pic !== undefined && holds ? { imageScale: pic } : {}),
|
|
304
827
|
overflowPx,
|
|
305
828
|
blockIds: [],
|
|
306
829
|
configPath: frame.configPath,
|
|
@@ -315,32 +838,49 @@ export function layoutComposedPage(input) {
|
|
|
315
838
|
if (q)
|
|
316
839
|
q.continuesTo = vdt.id;
|
|
317
840
|
}
|
|
841
|
+
else if (holds && cont?.receivers.has(frame.name)) {
|
|
842
|
+
// Content from the page before (#859).
|
|
843
|
+
vdt.continuedFrom = cont.receivers.get(frame.name).fromFrameId;
|
|
844
|
+
}
|
|
318
845
|
if (result && st) {
|
|
319
846
|
const range = fragmentRange(st.L, from, st.cut);
|
|
320
847
|
commitFrame(result, rect, st.story, range, input, vdt, blocks);
|
|
321
848
|
st.pieces++;
|
|
322
849
|
st.lastFrameId = vdt.id;
|
|
323
850
|
laid.add(headOf(frame.name));
|
|
851
|
+
// What the ladder changed in the frame's content (#858).
|
|
852
|
+
const pictures = fit.pictures.get(headOf(frame.name));
|
|
853
|
+
if (pictures && result.children.some((b) => b.type === 'resource')) {
|
|
854
|
+
fitLog.push({ frame: frame.name, kind: 'imageScaled', scale: pictures.scale, ...(pictures.by !== frame.name ? { cause: pictures.by } : {}) });
|
|
855
|
+
}
|
|
856
|
+
if (s !== 1 && holds)
|
|
857
|
+
fitLog.push({ frame: frame.name, kind: 'textScaled', scale: s });
|
|
858
|
+
}
|
|
859
|
+
if (goesOn && st) {
|
|
860
|
+
spills.set(frame.name, { head: headOf(frame.name), cut: st.cut, advanced: !sameCut(from, st.cut), pieces: st.pieces, headBlockId: st.headBlockId, frameId: vdt.id });
|
|
324
861
|
}
|
|
325
862
|
if (overflowPx > 0.5) {
|
|
326
863
|
const overflowMm = Math.round((overflowPx / dpi) * MM_PER_IN * 10) / 10;
|
|
327
864
|
overflowLog.push({ frame: frame.name, kind: 'overflow', toMm: overflowMm });
|
|
328
|
-
warnings.push({
|
|
329
|
-
kind: 'frameOverflow',
|
|
330
|
-
template: template.id,
|
|
331
|
-
frame: frame.name,
|
|
332
|
-
fenceIndex: fi,
|
|
333
|
-
overflowMm,
|
|
334
|
-
configPath: frame.configPath,
|
|
335
|
-
...(vdt.sourceStart !== undefined ? { sourceStart: vdt.sourceStart, sourceEnd: vdt.sourceEnd } : {}),
|
|
336
|
-
pageIndex: page.index,
|
|
337
|
-
});
|
|
338
865
|
}
|
|
339
866
|
if (frame.exclude === 'push') {
|
|
340
867
|
const box = frameBounds({ rect, rotate: frame.rotate });
|
|
341
868
|
pushRects.set(frame.name, box);
|
|
869
|
+
// `wrap: 'contour'` (#883): the shape of the pictures it holds; a
|
|
870
|
+
// turned frame pushes with its box.
|
|
871
|
+
const shape = frame.wrap === 'contour' && frame.rotate === 0 && result
|
|
872
|
+
? frameContour(result.children, { gapPx: input.pushGapPx, dpi, emPx: input.emPx, mirrored: pageIsMirrored(input.page), pageIndex: input.page.index })
|
|
873
|
+
: undefined;
|
|
874
|
+
if (shape)
|
|
875
|
+
notes.push(...shape.notes);
|
|
876
|
+
// A frame whose text the ladder scaled (#858) holds its pictures in
|
|
877
|
+
// its laid-out coordinates: on the page they are `s` as large, from
|
|
878
|
+
// its box's corner (`frameMatrix`).
|
|
879
|
+
const contour = shape?.contour && s !== 1 ? scaleContourAbout(shape.contour, rect.x, rect.y, s, false) : shape?.contour;
|
|
880
|
+
if (contour)
|
|
881
|
+
pushShapes.set(frame.name, contour);
|
|
342
882
|
if (!seed)
|
|
343
|
-
pushed.push({ name: frame.name, box });
|
|
883
|
+
pushed.push({ name: frame.name, box, ...(contour ? { contour } : {}) });
|
|
344
884
|
}
|
|
345
885
|
frames.push(vdt);
|
|
346
886
|
vdtByName.set(frame.name, vdt);
|
|
@@ -357,29 +897,12 @@ export function layoutComposedPage(input) {
|
|
|
357
897
|
for (const f of step.frames)
|
|
358
898
|
fill(prepare(f), solved.get(f.name));
|
|
359
899
|
}
|
|
360
|
-
const log = mergeLog(reflowEvents(motionsOf(template, rects, declared), dpi / MM_PER_IN), overflowLog, template);
|
|
361
|
-
return { frames, blocks,
|
|
900
|
+
const log = mergeLog(reflowEvents(motionsOf(template, rects, declared), dpi / MM_PER_IN), [...fitLog, ...overflowLog], template);
|
|
901
|
+
return { frames, blocks, log, laid, needs, pushRects, pushShapes, fitted, notes, spills };
|
|
362
902
|
};
|
|
363
903
|
const first = run();
|
|
364
|
-
const { frames, blocks,
|
|
365
|
-
|
|
366
|
-
const order = new Map(template.frames.map((f, i) => [f.name, i]));
|
|
367
|
-
frames.sort((a, b) => order.get(a.name) - order.get(b.name));
|
|
368
|
-
const contentBlocks = pass.contentBlocks;
|
|
369
|
-
const composed = {
|
|
370
|
-
template: template.id,
|
|
371
|
-
...(template.variant ? { variant: template.variant } : {}),
|
|
372
|
-
fenceIndex: fi,
|
|
373
|
-
role: template.role,
|
|
374
|
-
attrs: { ...fence.attrs },
|
|
375
|
-
attrSources: Object.fromEntries(Object.entries(fence.attrSources).map(([k, v]) => [k, { sourceStart: v.sourceStart + input.bodyOffset, sourceEnd: v.sourceEnd + input.bodyOffset }])),
|
|
376
|
-
frames,
|
|
377
|
-
log,
|
|
378
|
-
configPath: template.configPath,
|
|
379
|
-
sourceStart: contentBlocks[input.startIdx].sourceStart + input.bodyOffset,
|
|
380
|
-
sourceEnd: contentBlocks[input.endIdx].sourceEnd + input.bodyOffset,
|
|
381
|
-
};
|
|
382
|
-
return { composed, blocks, warnings, laid };
|
|
904
|
+
const { frames, blocks, log, laid, needs, fitted, notes, spills } = first.pushRects.size > 0 ? run(first.pushRects, first.pushShapes) : first;
|
|
905
|
+
return { frames, blocks, log, laid, needs, template, fitted, spills, ...(notes.length > 0 ? { notes } : {}) };
|
|
383
906
|
}
|
|
384
907
|
const sameRect = (a, b) => Math.abs(a.x - b.x) < 0.01 && Math.abs(a.y - b.y) < 0.01 && Math.abs(a.width - b.width) < 0.01 && Math.abs(a.height - b.height) < 0.01;
|
|
385
908
|
/** Each laid frame's declared and final boxes, in template order, with
|
|
@@ -439,6 +962,7 @@ function commitFrame(result, rect, story, range, input, vdt, out) {
|
|
|
439
962
|
}
|
|
440
963
|
box.pageIndex = page.index;
|
|
441
964
|
box.columnIndex = 0;
|
|
965
|
+
stampFrameAttrText(box, blocks[story.startIdx]?.containerAttrSources, story.plan.attrs, bodyOffset);
|
|
442
966
|
offsetCalloutToAbsolute(result, rect.x, rect.y);
|
|
443
967
|
out.push(box);
|
|
444
968
|
vdt.blockIds.push(box.id);
|
|
@@ -449,6 +973,37 @@ function commitFrame(result, rect, story, range, input, vdt, out) {
|
|
|
449
973
|
vdt.blockIds.push(child.id);
|
|
450
974
|
}
|
|
451
975
|
}
|
|
976
|
+
/** The frame's tab (`label`) and title (`title`) as the box draws them,
|
|
977
|
+
* stamped with where the `:::frame` fence writes them (#857): the
|
|
978
|
+
* attribute's value range, and a per-character map when the text is
|
|
979
|
+
* printed as written. */
|
|
980
|
+
function stampFrameAttrText(box, sources, attrs, bodyOffset) {
|
|
981
|
+
if (!sources || !box.designOverlay)
|
|
982
|
+
return;
|
|
983
|
+
for (const name of ['label', 'title']) {
|
|
984
|
+
const src = sources[name];
|
|
985
|
+
const value = attrs[name];
|
|
986
|
+
if (!src || value === undefined || value.trim() === '')
|
|
987
|
+
continue;
|
|
988
|
+
const start = src.sourceStart + bodyOffset;
|
|
989
|
+
const end = src.sourceEnd + bodyOffset;
|
|
990
|
+
for (const b of box.designOverlay.blocks) {
|
|
991
|
+
if (b.kind !== 'text' || b.attr !== undefined)
|
|
992
|
+
continue;
|
|
993
|
+
const shown = b.lines.map((l) => l.text).join(' ');
|
|
994
|
+
if (shown.trim() !== value.trim() && shown.trim().toLocaleLowerCase() !== value.trim().toLocaleLowerCase())
|
|
995
|
+
continue;
|
|
996
|
+
b.sourceStart = start;
|
|
997
|
+
b.sourceEnd = end;
|
|
998
|
+
b.attr = name;
|
|
999
|
+
if (shown === value && end - start === value.length) {
|
|
1000
|
+
b.sourceText = value;
|
|
1001
|
+
b.sourceMap = Array.from({ length: value.length }, (_, i) => start + i);
|
|
1002
|
+
}
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
452
1007
|
/** The callout style a frame is drawn with: `base` (its `style`, or the
|
|
453
1008
|
* frameless frame style) with what a frame sets of its own: its padding
|
|
454
1009
|
* (side by side) and a measure that is the frame's. A style's placement,
|