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.
Files changed (273) hide show
  1. package/dist/__bench__/composedPage.bench.test.d.ts +2 -0
  2. package/dist/__bench__/composedPage.bench.test.d.ts.map +1 -0
  3. package/dist/__bench__/composedPage.bench.test.js +95 -0
  4. package/dist/__bench__/composedPage.bench.test.js.map +1 -0
  5. package/dist/__tests__/composed/continuation.test.d.ts +2 -0
  6. package/dist/__tests__/composed/continuation.test.d.ts.map +1 -0
  7. package/dist/__tests__/composed/continuation.test.js +352 -0
  8. package/dist/__tests__/composed/continuation.test.js.map +1 -0
  9. package/dist/__tests__/composed/contrast.test.d.ts +2 -0
  10. package/dist/__tests__/composed/contrast.test.d.ts.map +1 -0
  11. package/dist/__tests__/composed/contrast.test.js +124 -0
  12. package/dist/__tests__/composed/contrast.test.js.map +1 -0
  13. package/dist/__tests__/composed/corpus.test.js +7 -1
  14. package/dist/__tests__/composed/corpus.test.js.map +1 -1
  15. package/dist/__tests__/composed/fit.test.d.ts +2 -0
  16. package/dist/__tests__/composed/fit.test.d.ts.map +1 -0
  17. package/dist/__tests__/composed/fit.test.js +408 -0
  18. package/dist/__tests__/composed/fit.test.js.map +1 -0
  19. package/dist/__tests__/composed/groups.test.js +14 -0
  20. package/dist/__tests__/composed/groups.test.js.map +1 -1
  21. package/dist/__tests__/composed/headingDesigns.test.d.ts +2 -0
  22. package/dist/__tests__/composed/headingDesigns.test.d.ts.map +1 -0
  23. package/dist/__tests__/composed/headingDesigns.test.js +262 -0
  24. package/dist/__tests__/composed/headingDesigns.test.js.map +1 -0
  25. package/dist/__tests__/composed/labels.test.d.ts +2 -0
  26. package/dist/__tests__/composed/labels.test.d.ts.map +1 -0
  27. package/dist/__tests__/composed/labels.test.js +326 -0
  28. package/dist/__tests__/composed/labels.test.js.map +1 -0
  29. package/dist/__tests__/composed/layout.test.js +2 -1
  30. package/dist/__tests__/composed/layout.test.js.map +1 -1
  31. package/dist/__tests__/composed/outsidePrint.test.js +5 -2
  32. package/dist/__tests__/composed/outsidePrint.test.js.map +1 -1
  33. package/dist/__tests__/composed/pageMemo.test.d.ts +2 -0
  34. package/dist/__tests__/composed/pageMemo.test.d.ts.map +1 -0
  35. package/dist/__tests__/composed/pageMemo.test.js +128 -0
  36. package/dist/__tests__/composed/pageMemo.test.js.map +1 -0
  37. package/dist/__tests__/composed/sourceMap.test.d.ts +2 -0
  38. package/dist/__tests__/composed/sourceMap.test.d.ts.map +1 -0
  39. package/dist/__tests__/composed/sourceMap.test.js +395 -0
  40. package/dist/__tests__/composed/sourceMap.test.js.map +1 -0
  41. package/dist/__tests__/composed/spreadLabels.test.d.ts +2 -0
  42. package/dist/__tests__/composed/spreadLabels.test.d.ts.map +1 -0
  43. package/dist/__tests__/composed/spreadLabels.test.js +131 -0
  44. package/dist/__tests__/composed/spreadLabels.test.js.map +1 -0
  45. package/dist/__tests__/composed/templateFrameRects.test.d.ts +2 -0
  46. package/dist/__tests__/composed/templateFrameRects.test.d.ts.map +1 -0
  47. package/dist/__tests__/composed/templateFrameRects.test.js +37 -0
  48. package/dist/__tests__/composed/templateFrameRects.test.js.map +1 -0
  49. package/dist/__tests__/design/imageAltText.test.js +2 -1
  50. package/dist/__tests__/design/imageAltText.test.js.map +1 -1
  51. package/dist/__tests__/exports.test.js +33 -0
  52. package/dist/__tests__/exports.test.js.map +1 -1
  53. package/dist/__tests__/migrate/fixtures/textbookPort.d.ts +14 -0
  54. package/dist/__tests__/migrate/fixtures/textbookPort.d.ts.map +1 -0
  55. package/dist/__tests__/migrate/fixtures/textbookPort.js +116 -0
  56. package/dist/__tests__/migrate/fixtures/textbookPort.js.map +1 -0
  57. package/dist/__tests__/migrate/textToMarkdown.test.d.ts +2 -0
  58. package/dist/__tests__/migrate/textToMarkdown.test.d.ts.map +1 -0
  59. package/dist/__tests__/migrate/textToMarkdown.test.js +268 -0
  60. package/dist/__tests__/migrate/textToMarkdown.test.js.map +1 -0
  61. package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts +2 -0
  62. package/dist/__tests__/paragraphStyleLetterSpacing.test.d.ts.map +1 -0
  63. package/dist/__tests__/paragraphStyleLetterSpacing.test.js +89 -0
  64. package/dist/__tests__/paragraphStyleLetterSpacing.test.js.map +1 -0
  65. package/dist/__tests__/pictureBelowPrintSize.test.d.ts +2 -0
  66. package/dist/__tests__/pictureBelowPrintSize.test.d.ts.map +1 -0
  67. package/dist/__tests__/pictureBelowPrintSize.test.js +82 -0
  68. package/dist/__tests__/pictureBelowPrintSize.test.js.map +1 -0
  69. package/dist/__tests__/pipeline/contourWrap.test.d.ts +2 -0
  70. package/dist/__tests__/pipeline/contourWrap.test.d.ts.map +1 -0
  71. package/dist/__tests__/pipeline/contourWrap.test.js +412 -0
  72. package/dist/__tests__/pipeline/contourWrap.test.js.map +1 -0
  73. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
  74. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  75. package/dist/__tests__/toc.test.js +3 -2
  76. package/dist/__tests__/toc.test.js.map +1 -1
  77. package/dist/bundle/codec.d.ts +16 -0
  78. package/dist/bundle/codec.d.ts.map +1 -1
  79. package/dist/bundle/codec.js +44 -2
  80. package/dist/bundle/codec.js.map +1 -1
  81. package/dist/bundle/index.d.ts +1 -1
  82. package/dist/bundle/index.d.ts.map +1 -1
  83. package/dist/bundle/index.js +1 -1
  84. package/dist/bundle/index.js.map +1 -1
  85. package/dist/canvas-backend/composed.d.ts +5 -2
  86. package/dist/canvas-backend/composed.d.ts.map +1 -1
  87. package/dist/canvas-backend/composed.js +10 -5
  88. package/dist/canvas-backend/composed.js.map +1 -1
  89. package/dist/canvas-backend/index.d.ts.map +1 -1
  90. package/dist/canvas-backend/index.js +37 -19
  91. package/dist/canvas-backend/index.js.map +1 -1
  92. package/dist/canvas-backend/renderResourceBlock.d.ts +5 -1
  93. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  94. package/dist/canvas-backend/renderResourceBlock.js +60 -1
  95. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  96. package/dist/composed/continuation.d.ts +102 -0
  97. package/dist/composed/continuation.d.ts.map +1 -0
  98. package/dist/composed/continuation.js +87 -0
  99. package/dist/composed/continuation.js.map +1 -0
  100. package/dist/composed/contour.d.ts +21 -0
  101. package/dist/composed/contour.d.ts.map +1 -0
  102. package/dist/composed/contour.js +34 -0
  103. package/dist/composed/contour.js.map +1 -0
  104. package/dist/composed/geometry.d.ts +8 -0
  105. package/dist/composed/geometry.d.ts.map +1 -1
  106. package/dist/composed/geometry.js +26 -0
  107. package/dist/composed/geometry.js.map +1 -1
  108. package/dist/composed/ids.d.ts +22 -0
  109. package/dist/composed/ids.d.ts.map +1 -0
  110. package/dist/composed/ids.js +32 -0
  111. package/dist/composed/ids.js.map +1 -0
  112. package/dist/composed/labels.d.ts +154 -0
  113. package/dist/composed/labels.d.ts.map +1 -0
  114. package/dist/composed/labels.js +524 -0
  115. package/dist/composed/labels.js.map +1 -0
  116. package/dist/composed/pageMemo.d.ts +73 -0
  117. package/dist/composed/pageMemo.d.ts.map +1 -0
  118. package/dist/composed/pageMemo.js +289 -0
  119. package/dist/composed/pageMemo.js.map +1 -0
  120. package/dist/composed/paint.d.ts +29 -9
  121. package/dist/composed/paint.d.ts.map +1 -1
  122. package/dist/composed/paint.js +41 -7
  123. package/dist/composed/paint.js.map +1 -1
  124. package/dist/composed/reflowLog.d.ts +14 -2
  125. package/dist/composed/reflowLog.d.ts.map +1 -1
  126. package/dist/composed/reflowLog.js +64 -7
  127. package/dist/composed/reflowLog.js.map +1 -1
  128. package/dist/composed/slotChecks.d.ts.map +1 -1
  129. package/dist/composed/slotChecks.js +3 -1
  130. package/dist/composed/slotChecks.js.map +1 -1
  131. package/dist/composed/sourceMap.d.ts +97 -0
  132. package/dist/composed/sourceMap.d.ts.map +1 -0
  133. package/dist/composed/sourceMap.js +748 -0
  134. package/dist/composed/sourceMap.js.map +1 -0
  135. package/dist/composed/spread.d.ts +3 -0
  136. package/dist/composed/spread.d.ts.map +1 -1
  137. package/dist/composed/spread.js +11 -12
  138. package/dist/composed/spread.js.map +1 -1
  139. package/dist/composed/templates.d.ts +1 -0
  140. package/dist/composed/templates.d.ts.map +1 -1
  141. package/dist/composed/templates.js +4 -0
  142. package/dist/composed/templates.js.map +1 -1
  143. package/dist/composed/types.d.ts +15 -32
  144. package/dist/composed/types.d.ts.map +1 -1
  145. package/dist/composed/validate.d.ts.map +1 -1
  146. package/dist/composed/validate.js +3 -2
  147. package/dist/composed/validate.js.map +1 -1
  148. package/dist/composed/wrap.d.ts.map +1 -1
  149. package/dist/composed/wrap.js +7 -1
  150. package/dist/composed/wrap.js.map +1 -1
  151. package/dist/configWarnings.d.ts +15 -0
  152. package/dist/configWarnings.d.ts.map +1 -1
  153. package/dist/configWarnings.js +29 -17
  154. package/dist/configWarnings.js.map +1 -1
  155. package/dist/contour/index.d.ts +134 -0
  156. package/dist/contour/index.d.ts.map +1 -0
  157. package/dist/contour/index.js +354 -0
  158. package/dist/contour/index.js.map +1 -0
  159. package/dist/contour/png.d.ts +16 -0
  160. package/dist/contour/png.d.ts.map +1 -0
  161. package/dist/contour/png.js +173 -0
  162. package/dist/contour/png.js.map +1 -0
  163. package/dist/contrast.d.ts +5 -3
  164. package/dist/contrast.d.ts.map +1 -1
  165. package/dist/contrast.js +46 -5
  166. package/dist/contrast.js.map +1 -1
  167. package/dist/defaults/paragraphStyles.d.ts +1 -1
  168. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  169. package/dist/defaults/paragraphStyles.js +5 -1
  170. package/dist/defaults/paragraphStyles.js.map +1 -1
  171. package/dist/design/slots.d.ts +12 -10
  172. package/dist/design/slots.d.ts.map +1 -1
  173. package/dist/design/slots.js +26 -2
  174. package/dist/design/slots.js.map +1 -1
  175. package/dist/fonts/faces.d.ts.map +1 -1
  176. package/dist/fonts/faces.js +6 -0
  177. package/dist/fonts/faces.js.map +1 -1
  178. package/dist/highlightMarks.d.ts.map +1 -1
  179. package/dist/highlightMarks.js +6 -0
  180. package/dist/highlightMarks.js.map +1 -1
  181. package/dist/html-backend.d.ts.map +1 -1
  182. package/dist/html-backend.js +42 -4
  183. package/dist/html-backend.js.map +1 -1
  184. package/dist/index.d.ts +18 -7
  185. package/dist/index.d.ts.map +1 -1
  186. package/dist/index.js +18 -4
  187. package/dist/index.js.map +1 -1
  188. package/dist/measure/rich.d.ts.map +1 -1
  189. package/dist/measure/rich.js +2 -0
  190. package/dist/measure/rich.js.map +1 -1
  191. package/dist/migrate/markdownRuns.d.ts +70 -0
  192. package/dist/migrate/markdownRuns.d.ts.map +1 -0
  193. package/dist/migrate/markdownRuns.js +215 -0
  194. package/dist/migrate/markdownRuns.js.map +1 -0
  195. package/dist/migrate/pageDesign.d.ts +119 -0
  196. package/dist/migrate/pageDesign.d.ts.map +1 -0
  197. package/dist/migrate/pageDesign.js +446 -0
  198. package/dist/migrate/pageDesign.js.map +1 -0
  199. package/dist/migrate/textToMarkdown.d.ts +124 -0
  200. package/dist/migrate/textToMarkdown.d.ts.map +1 -0
  201. package/dist/migrate/textToMarkdown.js +716 -0
  202. package/dist/migrate/textToMarkdown.js.map +1 -0
  203. package/dist/parse/blockParser.d.ts.map +1 -1
  204. package/dist/parse/blockParser.js +44 -3
  205. package/dist/parse/blockParser.js.map +1 -1
  206. package/dist/parse/types.d.ts +20 -0
  207. package/dist/parse/types.d.ts.map +1 -1
  208. package/dist/pipeline/build.d.ts.map +1 -1
  209. package/dist/pipeline/build.js +298 -30
  210. package/dist/pipeline/build.js.map +1 -1
  211. package/dist/pipeline/buildMeasurement.d.ts +7 -2
  212. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  213. package/dist/pipeline/buildMeasurement.js +21 -5
  214. package/dist/pipeline/buildMeasurement.js.map +1 -1
  215. package/dist/pipeline/calloutLayout.d.ts +39 -14
  216. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  217. package/dist/pipeline/calloutLayout.js +50 -4
  218. package/dist/pipeline/calloutLayout.js.map +1 -1
  219. package/dist/pipeline/composedLayout.d.ts +51 -6
  220. package/dist/pipeline/composedLayout.d.ts.map +1 -1
  221. package/dist/pipeline/composedLayout.js +633 -78
  222. package/dist/pipeline/composedLayout.js.map +1 -1
  223. package/dist/pipeline/contentWarnings.d.ts +15 -0
  224. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  225. package/dist/pipeline/contentWarnings.js +98 -4
  226. package/dist/pipeline/contentWarnings.js.map +1 -1
  227. package/dist/pipeline/floatPlacement.d.ts +2 -0
  228. package/dist/pipeline/floatPlacement.d.ts.map +1 -1
  229. package/dist/pipeline/floatPlacement.js +1 -0
  230. package/dist/pipeline/floatPlacement.js.map +1 -1
  231. package/dist/pipeline/frameLayout.d.ts +7 -4
  232. package/dist/pipeline/frameLayout.d.ts.map +1 -1
  233. package/dist/pipeline/frameLayout.js +5 -2
  234. package/dist/pipeline/frameLayout.js.map +1 -1
  235. package/dist/pipeline/headerFooter.d.ts +20 -1
  236. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  237. package/dist/pipeline/headerFooter.js +225 -70
  238. package/dist/pipeline/headerFooter.js.map +1 -1
  239. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  240. package/dist/pipeline/measureContentBlock.js +1 -0
  241. package/dist/pipeline/measureContentBlock.js.map +1 -1
  242. package/dist/pipeline/mirrorFrame.d.ts.map +1 -1
  243. package/dist/pipeline/mirrorFrame.js +25 -2
  244. package/dist/pipeline/mirrorFrame.js.map +1 -1
  245. package/dist/pipeline/outline.d.ts.map +1 -1
  246. package/dist/pipeline/outline.js +2 -0
  247. package/dist/pipeline/outline.js.map +1 -1
  248. package/dist/pipeline/resourceLayout.d.ts +28 -0
  249. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  250. package/dist/pipeline/resourceLayout.js +21 -0
  251. package/dist/pipeline/resourceLayout.js.map +1 -1
  252. package/dist/pipeline/styles.d.ts +1 -1
  253. package/dist/pipeline/styles.d.ts.map +1 -1
  254. package/dist/pipeline/styles.js +4 -0
  255. package/dist/pipeline/styles.js.map +1 -1
  256. package/dist/pipeline/textWrap.d.ts +4 -1
  257. package/dist/pipeline/textWrap.d.ts.map +1 -1
  258. package/dist/pipeline/textWrap.js +17 -3
  259. package/dist/pipeline/textWrap.js.map +1 -1
  260. package/dist/pipeline/toc.d.ts.map +1 -1
  261. package/dist/pipeline/toc.js +1 -0
  262. package/dist/pipeline/toc.js.map +1 -1
  263. package/dist/types.d.ts +77 -2
  264. package/dist/types.d.ts.map +1 -1
  265. package/dist/vdt.d.ts +290 -15
  266. package/dist/vdt.d.ts.map +1 -1
  267. package/dist/vdt.js.map +1 -1
  268. package/dist/word/toDocx.d.ts.map +1 -1
  269. package/dist/word/toDocx.js +14 -0
  270. package/dist/word/toDocx.js.map +1 -1
  271. package/dist/word/word.test.js +11 -1
  272. package/dist/word/word.test.js.map +1 -1
  273. 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
- export function layoutComposedPage(input) {
13
- const { pass, page, template, fence, dpi } = input;
14
- const fi = fence.fenceIndex;
15
- const frameIdOf = (name) => `cp${fi}:${name}`;
16
- const byName = new Map(template.frames.map((f) => [f.name, f]));
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 && byName.has(n) && !prev.has(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, input.styleOf(frame)),
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 pushed = seed ? [...seed].map(([name, box]) => ({ name, box })) : [];
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
- const b = p.box;
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 - g >= rect.x + rect.width || b.x + b.width + g <= rect.x || !below || b.y + b.height + g <= rect.y)
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({ x: b.x - g - rect.x, y: b.y - g - rect.y, width: b.width + 2 * g, height: b.height + 2 * g });
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, input.styleOf(frame));
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 r = p.st.L.layoutRange(p.st.cut, p.st.L.end, width, p.blockId, p.continuation, input.mirrored, exclusions ? { exclusions } : undefined);
134
- memo.set(key, r.totalHeight);
135
- return r.totalHeight;
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 fitted = input.fitBox(frame, r);
147
- if (Math.abs(fitted.width - r.width) < 0.01)
148
- return fitted;
149
- return input.fitBox(frame, placeBox(placeable, template, ctx, refs, () => nat(fitted.width)));
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
- return {
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: f.maxHeight ? dimensionToPx(f.maxHeight, dpi, input.emPx) : Infinity,
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 = input.styleOf(frame);
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
- const opts = {
269
- boxHeight: rect.height,
270
- verticalAlign: frame.verticalAlign,
271
- ...(exclusionsFor(frame, rect, false) ? { exclusions: exclusionsFor(frame, rect, false) } : {}),
272
- };
273
- const whole = st.L.layoutRange(from, st.L.end, rect.width, blockId, continuation, input.mirrored, opts);
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
- if ((whole.overflowPx ?? 0) > 0.5 && nextFrame && !vdtByName.has(nextFrame.name)) {
277
- // The rest goes on in the next frame of the chain.
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, rect.width, rect.height, blockId, continuation, minLines, input.mirrored, opts.exclusions ? { exclusions: opts.exclusions } : undefined);
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, rect.width, blockId, continuation, input.mirrored, opts)
283
- : undefined;
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, rect.width, blockId, true, input.mirrored, { boxHeight: rect.height });
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: 1,
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, warnings, log, laid, pushRects };
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, warnings, log, laid } = first.pushRects.size > 0 ? run(first.pushRects) : first;
365
- // Template order on the page record.
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,