postext 1.37.0 → 1.38.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 (194) hide show
  1. package/dist/__tests__/comics/frames.test.d.ts +2 -0
  2. package/dist/__tests__/comics/frames.test.d.ts.map +1 -0
  3. package/dist/__tests__/comics/frames.test.js +251 -0
  4. package/dist/__tests__/comics/frames.test.js.map +1 -0
  5. package/dist/__tests__/composed/continuationRules.test.d.ts +2 -0
  6. package/dist/__tests__/composed/continuationRules.test.d.ts.map +1 -0
  7. package/dist/__tests__/composed/continuationRules.test.js +389 -0
  8. package/dist/__tests__/composed/continuationRules.test.js.map +1 -0
  9. package/dist/__tests__/composed/coverImages.test.d.ts +2 -0
  10. package/dist/__tests__/composed/coverImages.test.d.ts.map +1 -0
  11. package/dist/__tests__/composed/coverImages.test.js +75 -0
  12. package/dist/__tests__/composed/coverImages.test.js.map +1 -0
  13. package/dist/__tests__/composed/decorationSize.test.d.ts +2 -0
  14. package/dist/__tests__/composed/decorationSize.test.d.ts.map +1 -0
  15. package/dist/__tests__/composed/decorationSize.test.js +79 -0
  16. package/dist/__tests__/composed/decorationSize.test.js.map +1 -0
  17. package/dist/__tests__/composed/emptyFrames.test.d.ts +2 -0
  18. package/dist/__tests__/composed/emptyFrames.test.d.ts.map +1 -0
  19. package/dist/__tests__/composed/emptyFrames.test.js +129 -0
  20. package/dist/__tests__/composed/emptyFrames.test.js.map +1 -0
  21. package/dist/__tests__/composed/fit.test.js +10 -4
  22. package/dist/__tests__/composed/fit.test.js.map +1 -1
  23. package/dist/__tests__/composed/groups.test.js +20 -0
  24. package/dist/__tests__/composed/groups.test.js.map +1 -1
  25. package/dist/__tests__/composed/ruledFrames.test.d.ts +2 -0
  26. package/dist/__tests__/composed/ruledFrames.test.d.ts.map +1 -0
  27. package/dist/__tests__/composed/ruledFrames.test.js +93 -0
  28. package/dist/__tests__/composed/ruledFrames.test.js.map +1 -0
  29. package/dist/__tests__/composed/spread.test.js +82 -0
  30. package/dist/__tests__/composed/spread.test.js.map +1 -1
  31. package/dist/__tests__/exports.test.js +12 -0
  32. package/dist/__tests__/exports.test.js.map +1 -1
  33. package/dist/__tests__/migrate/labelsOnPictures.test.d.ts +2 -0
  34. package/dist/__tests__/migrate/labelsOnPictures.test.d.ts.map +1 -0
  35. package/dist/__tests__/migrate/labelsOnPictures.test.js +114 -0
  36. package/dist/__tests__/migrate/labelsOnPictures.test.js.map +1 -0
  37. package/dist/__tests__/paragraphDropCap.test.js +38 -0
  38. package/dist/__tests__/paragraphDropCap.test.js.map +1 -1
  39. package/dist/__tests__/resourceTypePartial.test.d.ts +2 -0
  40. package/dist/__tests__/resourceTypePartial.test.d.ts.map +1 -0
  41. package/dist/__tests__/resourceTypePartial.test.js +60 -0
  42. package/dist/__tests__/resourceTypePartial.test.js.map +1 -0
  43. package/dist/canvas-backend/composed.d.ts +3 -0
  44. package/dist/canvas-backend/composed.d.ts.map +1 -1
  45. package/dist/canvas-backend/composed.js +47 -26
  46. package/dist/canvas-backend/composed.js.map +1 -1
  47. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  48. package/dist/canvas-backend/headerFooter.js +13 -0
  49. package/dist/canvas-backend/headerFooter.js.map +1 -1
  50. package/dist/canvas-backend/index.d.ts.map +1 -1
  51. package/dist/canvas-backend/index.js +16 -5
  52. package/dist/canvas-backend/index.js.map +1 -1
  53. package/dist/comics/frames.d.ts +82 -0
  54. package/dist/comics/frames.d.ts.map +1 -0
  55. package/dist/comics/frames.js +182 -0
  56. package/dist/comics/frames.js.map +1 -0
  57. package/dist/comics/index.d.ts +4 -1
  58. package/dist/comics/index.d.ts.map +1 -1
  59. package/dist/comics/index.js +3 -1
  60. package/dist/comics/index.js.map +1 -1
  61. package/dist/comics/layoutPage.d.ts +9 -0
  62. package/dist/comics/layoutPage.d.ts.map +1 -1
  63. package/dist/comics/layoutPage.js +53 -68
  64. package/dist/comics/layoutPage.js.map +1 -1
  65. package/dist/comics/page.d.ts.map +1 -1
  66. package/dist/comics/page.js +10 -1
  67. package/dist/comics/page.js.map +1 -1
  68. package/dist/comics/sourceMap.d.ts +60 -0
  69. package/dist/comics/sourceMap.d.ts.map +1 -0
  70. package/dist/comics/sourceMap.js +166 -0
  71. package/dist/comics/sourceMap.js.map +1 -0
  72. package/dist/comics/spread.d.ts.map +1 -1
  73. package/dist/comics/spread.js +6 -0
  74. package/dist/comics/spread.js.map +1 -1
  75. package/dist/comics/textPanels.d.ts +113 -0
  76. package/dist/comics/textPanels.d.ts.map +1 -0
  77. package/dist/comics/textPanels.js +327 -0
  78. package/dist/comics/textPanels.js.map +1 -0
  79. package/dist/comics/transform.d.ts +6 -2
  80. package/dist/comics/transform.d.ts.map +1 -1
  81. package/dist/comics/transform.js +14 -1
  82. package/dist/comics/transform.js.map +1 -1
  83. package/dist/comics/types.d.ts +10 -0
  84. package/dist/comics/types.d.ts.map +1 -1
  85. package/dist/composed/continuation.d.ts +55 -5
  86. package/dist/composed/continuation.d.ts.map +1 -1
  87. package/dist/composed/continuation.js +118 -16
  88. package/dist/composed/continuation.js.map +1 -1
  89. package/dist/composed/groups.d.ts +3 -0
  90. package/dist/composed/groups.d.ts.map +1 -1
  91. package/dist/composed/groups.js +34 -4
  92. package/dist/composed/groups.js.map +1 -1
  93. package/dist/composed/paint.d.ts +20 -1
  94. package/dist/composed/paint.d.ts.map +1 -1
  95. package/dist/composed/paint.js +40 -0
  96. package/dist/composed/paint.js.map +1 -1
  97. package/dist/composed/rules.d.ts +31 -0
  98. package/dist/composed/rules.d.ts.map +1 -0
  99. package/dist/composed/rules.js +45 -0
  100. package/dist/composed/rules.js.map +1 -0
  101. package/dist/composed/slotChecks.d.ts +2 -1
  102. package/dist/composed/slotChecks.d.ts.map +1 -1
  103. package/dist/composed/slotChecks.js +56 -2
  104. package/dist/composed/slotChecks.js.map +1 -1
  105. package/dist/composed/sourceMap.d.ts.map +1 -1
  106. package/dist/composed/sourceMap.js +15 -1
  107. package/dist/composed/sourceMap.js.map +1 -1
  108. package/dist/composed/spread.d.ts +20 -5
  109. package/dist/composed/spread.d.ts.map +1 -1
  110. package/dist/composed/spread.js +51 -26
  111. package/dist/composed/spread.js.map +1 -1
  112. package/dist/composed/templates.d.ts +11 -0
  113. package/dist/composed/templates.d.ts.map +1 -1
  114. package/dist/composed/templates.js +51 -2
  115. package/dist/composed/templates.js.map +1 -1
  116. package/dist/composed/types.d.ts +43 -3
  117. package/dist/composed/types.d.ts.map +1 -1
  118. package/dist/composed/validate.d.ts.map +1 -1
  119. package/dist/composed/validate.js +5 -3
  120. package/dist/composed/validate.js.map +1 -1
  121. package/dist/configWarnings.d.ts.map +1 -1
  122. package/dist/configWarnings.js +11 -1
  123. package/dist/configWarnings.js.map +1 -1
  124. package/dist/defaults/headerFooter.d.ts.map +1 -1
  125. package/dist/defaults/headerFooter.js +4 -0
  126. package/dist/defaults/headerFooter.js.map +1 -1
  127. package/dist/defaults/resourceTypes.d.ts +15 -0
  128. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  129. package/dist/defaults/resourceTypes.js +39 -1
  130. package/dist/defaults/resourceTypes.js.map +1 -1
  131. package/dist/design/layout.d.ts +7 -0
  132. package/dist/design/layout.d.ts.map +1 -1
  133. package/dist/design/layout.js +109 -16
  134. package/dist/design/layout.js.map +1 -1
  135. package/dist/html-backend.d.ts.map +1 -1
  136. package/dist/html-backend.js +43 -5
  137. package/dist/html-backend.js.map +1 -1
  138. package/dist/htmlComic.d.ts +3 -0
  139. package/dist/htmlComic.d.ts.map +1 -1
  140. package/dist/htmlComic.js +2 -0
  141. package/dist/htmlComic.js.map +1 -1
  142. package/dist/index.d.ts +5 -5
  143. package/dist/index.d.ts.map +1 -1
  144. package/dist/index.js +2 -2
  145. package/dist/index.js.map +1 -1
  146. package/dist/migrate/pageDesign.d.ts +35 -2
  147. package/dist/migrate/pageDesign.d.ts.map +1 -1
  148. package/dist/migrate/pageDesign.js +134 -7
  149. package/dist/migrate/pageDesign.js.map +1 -1
  150. package/dist/migrate/textToMarkdown.d.ts +33 -1
  151. package/dist/migrate/textToMarkdown.d.ts.map +1 -1
  152. package/dist/migrate/textToMarkdown.js +206 -9
  153. package/dist/migrate/textToMarkdown.js.map +1 -1
  154. package/dist/parse/blockParser.js +26 -0
  155. package/dist/parse/blockParser.js.map +1 -1
  156. package/dist/pipeline/build.d.ts.map +1 -1
  157. package/dist/pipeline/build.js +219 -40
  158. package/dist/pipeline/build.js.map +1 -1
  159. package/dist/pipeline/calloutLayout.d.ts +5 -0
  160. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  161. package/dist/pipeline/calloutLayout.js +70 -0
  162. package/dist/pipeline/calloutLayout.js.map +1 -1
  163. package/dist/pipeline/composedLayout.d.ts.map +1 -1
  164. package/dist/pipeline/composedLayout.js +246 -49
  165. package/dist/pipeline/composedLayout.js.map +1 -1
  166. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  167. package/dist/pipeline/contentWarnings.js +21 -1
  168. package/dist/pipeline/contentWarnings.js.map +1 -1
  169. package/dist/pipeline/dropCap.d.ts +5 -0
  170. package/dist/pipeline/dropCap.d.ts.map +1 -1
  171. package/dist/pipeline/dropCap.js +23 -2
  172. package/dist/pipeline/dropCap.js.map +1 -1
  173. package/dist/pipeline/frameLayout.d.ts +17 -2
  174. package/dist/pipeline/frameLayout.d.ts.map +1 -1
  175. package/dist/pipeline/frameLayout.js +47 -2
  176. package/dist/pipeline/frameLayout.js.map +1 -1
  177. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  178. package/dist/pipeline/headerFooter.js +41 -2
  179. package/dist/pipeline/headerFooter.js.map +1 -1
  180. package/dist/pipeline/measureContentBlock.d.ts +1 -1
  181. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  182. package/dist/pipeline/measureContentBlock.js +4 -1
  183. package/dist/pipeline/measureContentBlock.js.map +1 -1
  184. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  185. package/dist/pipeline/resourceNumbering.js +13 -4
  186. package/dist/pipeline/resourceNumbering.js.map +1 -1
  187. package/dist/preflight.js +5 -1
  188. package/dist/preflight.js.map +1 -1
  189. package/dist/types.d.ts +32 -2
  190. package/dist/types.d.ts.map +1 -1
  191. package/dist/vdt.d.ts +90 -2
  192. package/dist/vdt.d.ts.map +1 -1
  193. package/dist/vdt.js.map +1 -1
  194. package/package.json +1 -1
@@ -7,13 +7,14 @@ import { FRAME_ATTR } from '../composed/wrap';
7
7
  import { framePlaceable, nextFrameOf, groupPlaceable, placeBox, unionRect } from '../composed/geometry';
8
8
  import { composedOrder, groupOfFrames, layoutGroup } from '../composed/groups';
9
9
  import { reflowEvents } from '../composed/reflowLog';
10
- import { anchorTarget } from '../composed/templates';
11
- import { continuationTemplate, continuesOffPage } from '../composed/continuation';
10
+ import { anchorTarget, resolveContinueIn } from '../composed/templates';
11
+ import { collapsedOnAddedPage, collapseFrames, continuationTemplate, continuesCollapsed, continuesOffPage, trailingFrames } from '../composed/continuation';
12
12
  import { resolvePageTemplate } from '../composed/templates';
13
13
  import { frameElementId } from '../composed/ids';
14
+ import { frameLinePx, frameRuleBlocks } from '../composed/rules';
14
15
  import { offsetCalloutToAbsolute } from './calloutLayout';
15
16
  import { isMarkerBlock } from './buildHelpers';
16
- import { CUT_START, fragmentRange, makeCalloutLayouter, splitCalloutFragment } from './frameLayout';
17
+ import { CUT_START, fragmentRange, frameCutRules, makeCalloutLayouter, splitCalloutFragment } from './frameLayout';
17
18
  const MM_PER_IN = 25.4;
18
19
  const NO_FIT = { patches: new Map(), text: new Map(), pictures: new Map(), yieldAll: new Set(), goOn: new Set() };
19
20
  /** Step of the text and picture scales the ladder tries (DESIGN §5.4). */
@@ -85,18 +86,87 @@ export function layoutComposedPage(given) {
85
86
  const memo = new Map();
86
87
  let runs = 0;
87
88
  const keyOf = (tpl, fit) => JSON.stringify([tpl.id, [...fit.patches], [...fit.text], [...fit.pictures], [...fit.yieldAll], [...fit.goOn]]);
89
+ /** Frames a page added by continuation lays with no room (#859). */
90
+ const collapsedHere = input.continuing ? collapsedOnAddedPage(given.template, input.continuing) : new Set();
91
+ const holdsStory = (name) => input.stories.has(headOf(name));
92
+ /** The empty frames that go with the rest of a frame cut for a page
93
+ * laid from this template again (#912), by that frame. */
94
+ const travelling = (tpl, fit) => {
95
+ const out = new Map();
96
+ for (const f of tpl.frames) {
97
+ if (!fit.goOn.has(f.name) || !continuesCollapsed(tpl, f, input.continuing?.collapse ?? false))
98
+ continue;
99
+ const t = trailingFrames(tpl, f.name, holdsStory);
100
+ if (t.length > 0)
101
+ out.set(f.name, t);
102
+ }
103
+ return out;
104
+ };
88
105
  const compose = (fit, tpl = template) => {
89
106
  const key = keyOf(tpl, fit);
90
107
  const known = memo.get(key);
91
108
  if (known)
92
109
  return known;
93
110
  runs++;
94
- const c = composePage(input, patchTemplate(tpl, fit.patches), fit);
111
+ const travel = travelling(tpl, fit);
112
+ const lay = (leaving, whole = new Set()) => composePage(input, collapseFrames(patchTemplate(tpl, fit.patches), leaving, whole), fit, leaving.size > 0 ? new Set([...collapsedHere, ...leaving]) : collapsedHere);
113
+ const all = new Map(travel);
114
+ let c = lay(new Set([...travel.values()].flat()));
115
+ // The empty frames go with the rest only when there is a rest: a frame
116
+ // that holds all of it once they leave keeps them after it…
117
+ const staying = [...travel.keys()].filter((f) => !c.spills.has(f));
118
+ if (staying.length > 0) {
119
+ for (const f of staying)
120
+ travel.delete(f);
121
+ c = lay(new Set([...travel.values()].flat()));
122
+ // …unless they then do not fit under it at their size: the whole box
123
+ // goes on with them, and takes no room here.
124
+ const whole = staying.filter((f) => squeezes(c, f));
125
+ if (whole.length > 0) {
126
+ for (const f of whole)
127
+ travel.set(f, all.get(f));
128
+ c = lay(new Set([...whole, ...[...travel.values()].flat()]), new Set(whole));
129
+ }
130
+ }
131
+ // A box none of whose content fits goes on whole and takes no room
132
+ // here either.
133
+ const empty = [...fit.goOn].filter((f) => { const sp = c.spills.get(f); return sp !== undefined && !sp.advanced && sp.pieces === 0 && !frameIn(c, f)?.collapsed; });
134
+ if (empty.length > 0) {
135
+ for (const f of empty)
136
+ if (all.has(f))
137
+ travel.set(f, all.get(f));
138
+ c = lay(new Set([...empty, ...[...travel.values()].flat()]), new Set(empty));
139
+ }
95
140
  memo.set(key, c);
96
141
  return c;
97
142
  };
98
143
  const frameIn = (c, name) => c.frames.find((f) => f.name === name);
99
144
  const overflows = (c, name) => (frameIn(c, name)?.overflowPx ?? 0) > 0.5;
145
+ /** A frame that can go on to another page squeezes the frames after it
146
+ * in its stack below their declared size (#912): the stack reached the
147
+ * foot of its area and the last frames were cut to fit, an answer space
148
+ * under the box lost its lines. The ladder treats it as content that
149
+ * does not fit, so the box goes on and they keep their size. */
150
+ const squeezes = (c, name) => {
151
+ const frame = byName.get(name);
152
+ if (!frame || !continuesOffPage(frame.overflow.continueIn))
153
+ return false;
154
+ const g = groupOf.get(name);
155
+ if (!g || g.kind !== 'stack')
156
+ return false;
157
+ for (const other of g.frames.slice(g.frames.indexOf(name) + 1)) {
158
+ const f = byName.get(other);
159
+ const v = frameIn(c, other);
160
+ if (!f || !v || v.collapsed)
161
+ continue;
162
+ const min = f.minHeight ? dimensionToPx(f.minHeight, dpi, input.emPx) : fixedHeight(f) ? dimensionToPx(fixedHeight(f), dpi, input.emPx) : 0;
163
+ if (v.rect.height < min - 0.5)
164
+ return true;
165
+ }
166
+ return false;
167
+ };
168
+ /** The frame's content does not fit, or it squeezes the frames after it. */
169
+ const short = (c, name) => overflows(c, name) || squeezes(c, name);
100
170
  /** The ladder's last step (#859): the frame is cut at its foot and the
101
171
  * rest goes on on another page, its text and pictures back at full
102
172
  * size (they go on anyway). */
@@ -123,7 +193,7 @@ export function layoutComposedPage(given) {
123
193
  const attempt = (next, name) => {
124
194
  fit = next;
125
195
  cur = compose(fit);
126
- return !overflows(cur, name);
196
+ return !short(cur, name);
127
197
  };
128
198
  /** The largest scale among `steps` (descending) at which `name` fits,
129
199
  * checked at the lowest first, then by bisection; adopted. */
@@ -136,7 +206,7 @@ export function layoutComposedPage(given) {
136
206
  let hi = steps.length - 1; // fits at hi
137
207
  while (lo < hi && runs < COMPOSE_BUDGET) {
138
208
  const mid = Math.floor((lo + hi) / 2);
139
- if (!overflows(compose(make(steps[mid])), name))
209
+ if (!short(compose(make(steps[mid])), name))
140
210
  hi = mid;
141
211
  else
142
212
  lo = mid + 1;
@@ -196,7 +266,7 @@ export function layoutComposedPage(given) {
196
266
  };
197
267
  for (const frame of template.frames) {
198
268
  const name = frame.name;
199
- if (!overflows(cur, name) || runs >= COMPOSE_BUDGET)
269
+ if (!short(cur, name) || runs >= COMPOSE_BUDGET)
200
270
  continue;
201
271
  const steps = [];
202
272
  tried.set(name, steps);
@@ -211,38 +281,57 @@ export function layoutComposedPage(given) {
211
281
  if (movesOthers(frame))
212
282
  steps.push('push');
213
283
  // 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) {
284
+ const scaleImages = () => {
285
+ const heads = frame.fit.scaleImages ? pictureHeads(frame) : [];
286
+ if (heads.length === 0 || frame.fit.imageMinScale >= 1)
287
+ return false;
216
288
  steps.push('scaleImages');
217
- if (searchScale(scaleSteps(frame.fit.imageMinScale), (s) => withPictures(fit, heads, s, name), name))
218
- continue;
219
- }
289
+ return searchScale(scaleSteps(frame.fit.imageMinScale), (s) => withPictures(fit, heads, s, name), name);
290
+ };
220
291
  // 4. Redistribute: every other frame of the group gives space (or
221
292
  // 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) {
293
+ const redistribute = () => {
294
+ const g = groupOf.get(name);
295
+ if (!g || g.frames.filter((m) => byName.has(m)).length <= 1)
296
+ return false;
224
297
  steps.push('redistribute');
225
- if (attempt({ ...fit, yieldAll: new Set([...fit.yieldAll, g.name]) }, name))
226
- continue;
227
- }
298
+ return attempt({ ...fit, yieldAll: new Set([...fit.yieldAll, g.name]) }, name);
299
+ };
228
300
  // 5. The frame's text (its whole story), down to fit.minScale.
229
- if (frame.fit.shrinkText && frame.fit.minScale < 1 && holdsText(frame)) {
301
+ const shrinkText = () => {
302
+ if (!frame.fit.shrinkText || frame.fit.minScale >= 1 || !holdsText(frame))
303
+ return false;
230
304
  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)) {
305
+ return searchScale(scaleSteps(frame.fit.minScale), (s) => withText(fit, headOf(name), s), name);
306
+ };
307
+ // 6. Continuation on another page (`overflow.continueIn: 'next-page'`,
308
+ // `'template:<id>'` or `'page:<id>'`, #859). On a page added by
309
+ // continuation, a frame that would take none of what it receives
310
+ // sends nothing on (it would go on for ever): it warns.
311
+ const goOn = () => {
312
+ if (!continuesOffPage(frame.overflow.continueIn))
313
+ return false;
240
314
  steps.push('continue');
241
315
  const before = { fit, cur };
242
- if (attempt(goingOn(fit, frame), name) && (!input.continuing || cur.spills.get(name)?.advanced !== false))
243
- continue;
316
+ // (Only for a story that came from another page: a page's own story
317
+ // may go on whole.)
318
+ const received = input.continuing?.receivers.has(headOf(name)) ?? false;
319
+ if (attempt(goingOn(fit, frame), name) && (!received || cur.spills.get(name)?.advanced !== false))
320
+ return true;
244
321
  ({ fit, cur } = before);
245
- }
322
+ return false;
323
+ };
324
+ // A frame whose content can go on to another page goes on before its
325
+ // pictures and text shrink (`fit.preferContinue`, #920): a story keeps
326
+ // one size from page to page. The pictures and the text shrink only
327
+ // when it cannot go on (the last page continuation may add, or a
328
+ // receiver that takes none of it).
329
+ const order = continuesOffPage(frame.overflow.continueIn) && frame.fit.preferContinue
330
+ ? [redistribute, goOn, scaleImages, shrinkText]
331
+ : [scaleImages, redistribute, shrinkText, goOn];
332
+ for (const step of order)
333
+ if (step())
334
+ break;
246
335
  }
247
336
  // 7. Warn, with what would make each frame that still overflows fit.
248
337
  const warnings = [...(cur.notes ?? [])];
@@ -308,6 +397,7 @@ export function layoutComposedPage(given) {
308
397
  };
309
398
  // What goes on on other pages (#859), in template order.
310
399
  const continuations = [];
400
+ const trailing = new Map([...travelling(template, fit)].filter(([f, names]) => cur.spills.has(f) && names.every((n) => frameIn(cur, n)?.collapsed)));
311
401
  for (const frame of template.frames) {
312
402
  const spill = cur.spills.get(frame.name);
313
403
  const story = spill ? input.stories.get(spill.head) : undefined;
@@ -317,14 +407,19 @@ export function layoutComposedPage(given) {
317
407
  continuations.push({
318
408
  frame: frame.name,
319
409
  fromFrameId: spill.frameId,
320
- rule: frame.overflow.continueIn,
410
+ head: spill.head,
411
+ // `{attr.x}` read from the fence (#917).
412
+ rule: resolveContinueIn(frame.overflow.continueIn, fence.attrs),
321
413
  template: template.id,
322
414
  collapsed: input.continuing?.collapse ?? false,
323
415
  story,
324
416
  cut: spill.cut,
325
417
  pieces: spill.pieces,
326
418
  headBlockId: spill.headBlockId,
327
- slot: came?.slot ?? frame.name,
419
+ // A story that jumped here (#917) goes on named by this page's frame.
420
+ slot: (input.continuing?.jump ? undefined : came?.slot) ?? frame.name,
421
+ ...(trailing.has(frame.name) ? { trailing: trailing.get(frame.name) } : {}),
422
+ ...(frame.overflow.fromMark !== undefined ? { fromMark: frame.overflow.fromMark } : {}),
328
423
  });
329
424
  }
330
425
  return { composed, blocks, warnings, laid, ...(fitted.size > 0 ? { fitted } : {}), ...(continuations.length > 0 ? { continuations } : {}) };
@@ -359,9 +454,12 @@ function overflowProposals(c) {
359
454
  ...(frame.fit.shrinkText ? {} : { also: [{ path: path('fit/shrinkText'), value: true }] }),
360
455
  });
361
456
  }
457
+ // A frame that holds content (an empty one is short of its own box, #911:
458
+ // more height, text or pages do not help it).
459
+ const holds = cur.needs.has(name);
362
460
  // A higher maximum: the frame's height plus the shortfall, in whole mm.
363
461
  const fixed = fixedHeight(frame);
364
- if (frame.maxHeight || fixed) {
462
+ if (holds && (frame.maxHeight || fixed)) {
365
463
  const want = Math.ceil((vdt.rect.height + vdt.overflowPx) / mmPx - 1e-6);
366
464
  const patch = { maxHeight: mmDim(want), ...(fixed ? { height: 'auto', minHeight: fixed } : {}) };
367
465
  if (fits(c.patched(fit, name, patch)))
@@ -374,7 +472,9 @@ function overflowProposals(c) {
374
472
  if (g) {
375
473
  for (const other of g.frames) {
376
474
  const f = c.byName.get(other);
377
- if (!f || other === name || f.pin || !f.minHeight)
475
+ // An empty frame's minimum is the room it stands for (an answer
476
+ // space, a picture slot), not room to take (#911).
477
+ if (!f || other === name || f.pin || !f.minHeight || !cur.needs.has(other))
378
478
  continue;
379
479
  const box = cur.frames.find((x) => x.name === other);
380
480
  if (!box)
@@ -391,8 +491,16 @@ function overflowProposals(c) {
391
491
  }
392
492
  }
393
493
  }
494
+ // An empty frame cut short by its group: a lower minimum of its own, the
495
+ // height it was left (#911).
496
+ if (!holds && g && frame.minHeight) {
497
+ const lowered = Math.floor(vdt.rect.height / mmPx + 1e-6);
498
+ if (lowered * mmPx < dimensionToPx(frame.minHeight, c.dpi, c.emPx) - 0.5 && fits(c.patched(fit, name, { minHeight: mmDim(lowered) }))) {
499
+ out.push({ kind: 'lowerMin', path: path('minHeight'), value: mmDim(lowered), frame: name });
500
+ }
501
+ }
394
502
  // Continue on the next page (#859 lays it out).
395
- if (!frame.overflow.continueIn)
503
+ if (holds && !frame.overflow.continueIn)
396
504
  out.push({ kind: 'continueNextPage', path: path('overflow/continueIn'), value: 'next-page' });
397
505
  // Another template of the book with a frame of this name, where the
398
506
  // content fits as it is.
@@ -447,14 +555,15 @@ export function keepChromeAt(style, scale) {
447
555
  };
448
556
  }
449
557
  /** Lay out every frame of the page once, with the reflow ladder's `fit`. */
450
- function composePage(input, template, fit) {
558
+ function composePage(input, template, fit, collapsed) {
451
559
  const { pass, fence, dpi } = input;
452
560
  const fi = fence.fenceIndex;
453
561
  const cont = input.continuing;
562
+ const cutRules = frameCutRules(pass.resolved);
454
563
  /** A frame's id: `cp<fence>:<name>`; on a page added by continuation
455
564
  * (#859) `~<k>`, a frame that receives content named by the frame of
456
565
  * 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));
566
+ const frameIdOf = (name) => (cont && !cont.jump ? frameElementId(fi, cont.receivers.get(name)?.slot ?? name, cont.generation) : frameElementId(fi, name));
458
567
  const byName = new Map(template.frames.map((f) => [f.name, f]));
459
568
  const prev = chainPrev(template, cont?.receivers);
460
569
  /** The head of the chain a frame belongs to. */
@@ -469,6 +578,11 @@ function composePage(input, template, fit) {
469
578
  };
470
579
  /** The text scale of a frame's story (#858). */
471
580
  const textScale = (frame) => fit.text.get(headOf(frame.name)) ?? 1;
581
+ /** A page number a mark's `{page}` is laid out with (#917): a guess of
582
+ * the right width, the build writing the true one once pages are
583
+ * numbered (`VDTDesignTextBlock.pageMark`). */
584
+ const toGuess = String(input.page.index + 2);
585
+ const fromGuess = String(Math.max(1, input.page.index));
472
586
  /** The style a frame is laid out in: its box as drawn at any text scale. */
473
587
  const styleOf = (frame) => {
474
588
  const style = keepChromeAt(input.styleOf(frame), textScale(frame));
@@ -477,7 +591,15 @@ function composePage(input, template, fit) {
477
591
  // A frame cut for another page (#859) ends with its mark: its own
478
592
  // words, none (`false`), or its style's localized ones.
479
593
  const mark = frame.overflow.mark;
480
- return { ...style, continuesMarkerEnabled: mark !== false, continuesMarker: typeof mark === 'string' ? mark : style.continuesMarker };
594
+ return { ...style, continuesMarkerEnabled: mark !== false, continuesMarker: typeof mark === 'string' ? withPage(mark, toGuess) : style.continuesMarker };
595
+ };
596
+ /** The mark of a frame that receives a story from another page (#917):
597
+ * its own `overflow.fromMark`, else the sending frame's, set at the
598
+ * head of the first piece it lays here. */
599
+ const fromMarkOf = (frame) => {
600
+ const came = cont?.receivers.get(headOf(frame.name));
601
+ const words = frame.overflow.fromMark ?? came?.fromMark;
602
+ return came && words !== undefined ? withPage(words, fromGuess) : undefined;
481
603
  };
482
604
  /** The scale of a frame's pictures (#858), below 1, or undefined. */
483
605
  const pictureScale = (frame) => fit.pictures.get(headOf(frame.name))?.scale;
@@ -542,6 +664,8 @@ function composePage(input, template, fit) {
542
664
  const declaredRefOf = (name) => declared.get(name) ?? groupRect(name, declared, groupDeclared, declaredRefOf);
543
665
  /** Pinned frames laid so far: a group's frames flow round them. */
544
666
  const pinned = [];
667
+ /** How far a stack cut each frame below its least height (#911). */
668
+ const boxShort = new Map();
545
669
  const frames = [];
546
670
  const vdtByName = new Map();
547
671
  const blocks = [];
@@ -591,7 +715,10 @@ function composePage(input, template, fit) {
591
715
  const holds = reached && !atEnd(st);
592
716
  const continuation = st !== undefined && st.pieces > 0;
593
717
  const blockId = st && st.pieces > 0 ? `${st.headBlockId}-cont-${st.pieces}` : frameIdOf(frame.name);
594
- return { frame, st, before, holds, continuation, blockId };
718
+ // The first piece here of a story that came from another page.
719
+ const came = cont?.receivers.get(headOf(frame.name));
720
+ const fromMark = st && came && continuation && st.pieces === came.pieces ? fromMarkOf(frame) : undefined;
721
+ return { frame, st, before, holds, continuation, blockId, ...(fromMark !== undefined ? { fromMark } : {}) };
595
722
  };
596
723
  /** The content's height at `width`, running round the pushing frames
597
724
  * below the top of `at` (the frame's box as first placed). A frame
@@ -609,8 +736,8 @@ function composePage(input, template, fit) {
609
736
  if (known !== undefined)
610
737
  return known;
611
738
  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);
739
+ const opts = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}), ...(p.fromMark !== undefined ? { continuedMark: p.fromMark } : {}) };
740
+ 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.continuedMark !== undefined ? opts : undefined);
614
741
  const h = r.totalHeight * s;
615
742
  memo.set(key, h);
616
743
  return h;
@@ -707,6 +834,7 @@ function composePage(input, template, fit) {
707
834
  pin: f.pin,
708
835
  fixedHeight: !auto && !fill,
709
836
  need: natural,
837
+ floor: minH,
710
838
  };
711
839
  if (yieldAll && mode === 'final' && !f.pin) {
712
840
  // Down to its minimum, never under what its content needs, never
@@ -718,6 +846,14 @@ function composePage(input, template, fit) {
718
846
  if (group.kind === 'stack')
719
847
  item.min = Math.min(height, Math.max(minH, natural(box.width)));
720
848
  }
849
+ if (fit.goOn.has(f.name) && mode === 'final' && !f.pin) {
850
+ // A frame cut for another page (#859) gives the room first,
851
+ // down to its minimum: the frames after it in the group keep
852
+ // their size (#912), and its rest goes on.
853
+ item.shrink = Math.max(f.shrink, 1);
854
+ item.priority = Number.NEGATIVE_INFINITY;
855
+ item.min = minH;
856
+ }
721
857
  return item;
722
858
  });
723
859
  const firstFree = items.find((it) => !it.pin) ?? items[0];
@@ -739,7 +875,7 @@ function composePage(input, template, fit) {
739
875
  for (const it of items)
740
876
  if (it.pin)
741
877
  out.set(it.name, it.own);
742
- return { rects: out, box: ownBox ?? unionRect([...out.values()]) ?? box };
878
+ return { rects: out, box: ownBox ?? unionRect([...out.values()]) ?? box, items };
743
879
  };
744
880
  let naturals = preps.map((p) => naturalOf(p));
745
881
  let fin = solve('final', naturals);
@@ -751,6 +887,20 @@ function composePage(input, template, fit) {
751
887
  fin = solve('final', naturals);
752
888
  }
753
889
  const decl = solve('declared', naturals);
890
+ // A frame the stack cut below the least it may have (its
891
+ // `minHeight`, a fixed height its own) is short of its box, content
892
+ // or none (#911): an empty answer space or picture slot overflows
893
+ // too.
894
+ for (const it of fin.items) {
895
+ if (it.pin || group.kind !== 'stack')
896
+ continue;
897
+ const least = it.fixedHeight && (it.floor ?? 0) <= 0.5 ? it.own.height : it.floor ?? 0;
898
+ const got = fin.rects.get(it.name).height;
899
+ if (least - got > 0.5)
900
+ boxShort.set(it.name, least - got);
901
+ else
902
+ boxShort.delete(it.name);
903
+ }
754
904
  for (const m of members) {
755
905
  rects.set(m.name, fin.rects.get(m.name));
756
906
  declared.set(m.name, decl.rects.get(m.name));
@@ -783,7 +933,7 @@ function composePage(input, template, fit) {
783
933
  let known;
784
934
  needs.set(frame.name, () => (known ??= naturalOf(startAt)(width)));
785
935
  const exclusions = unscale(exclusionsFor(frame, rect, false), s);
786
- const extra = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}) };
936
+ const extra = { ...(exclusions ? { exclusions } : {}), ...(pic !== undefined ? { pictureScale: pic } : {}), ...(p.fromMark !== undefined ? { continuedMark: p.fromMark } : {}) };
787
937
  const opts = { boxHeight: laidH, verticalAlign: frame.verticalAlign, ...extra };
788
938
  const whole = st.L.layoutRange(from, st.L.end, laidW, blockId, continuation, input.mirrored, opts);
789
939
  const nextName = nextFrameOf(frame);
@@ -795,13 +945,13 @@ function composePage(input, template, fit) {
795
945
  // The rest goes on in the next frame of the chain, or on another
796
946
  // page.
797
947
  const minLines = Math.max(1, style.splitMinLines);
798
- const frag = splitCalloutFragment(pass, st.L, from, laidW, laidH, blockId, continuation, minLines, input.mirrored, extra.exclusions || extra.pictureScale !== undefined ? extra : undefined);
948
+ // A heading stays with what follows it, a list keeps two items
949
+ // on each side of the cut (#913).
950
+ const frag = splitCalloutFragment(pass, st.L, from, laidW, laidH, blockId, continuation, minLines, input.mirrored, extra.exclusions || extra.pictureScale !== undefined || extra.continuedMark !== undefined ? extra : undefined, cutRules);
799
951
  const to = frag?.to ?? from;
800
- result = frag
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;
952
+ // A frame none of whose content fits draws nothing: the whole box
953
+ // goes on (#912), not a tab and a mark over an empty piece.
954
+ result = frag ? st.L.layoutRange(from, to, laidW, blockId, continuation, input.mirrored, opts) : undefined;
805
955
  st.cut = to;
806
956
  }
807
957
  else {
@@ -815,6 +965,26 @@ function composePage(input, template, fit) {
815
965
  // A styled frame with nothing (left) to hold still draws its box.
816
966
  result = st.L.layoutRange(st.L.end, st.L.end, laidW, blockId, true, input.mirrored, { boxHeight: laidH });
817
967
  }
968
+ // A frame cut below its least height overflows by the cut, holding
969
+ // content or not (#911).
970
+ overflowPx = Math.max(overflowPx, boxShort.get(frame.name) ?? 0);
971
+ // Ruled lines (#914): on the box, over its background, in its
972
+ // laid-out coordinates; an empty frame keeps them for the decoration.
973
+ let emptyRules;
974
+ if (frame.rules) {
975
+ const linePx = frameLinePx(style, dpi);
976
+ if (result?.frame.designOverlay && result.frame.callout) {
977
+ result.frame.designOverlay.blocks.push(...frameRuleBlocks(frame.rules, result.frame.callout.innerRect, linePx, s, dpi, input.emPx));
978
+ }
979
+ else if (!result) {
980
+ const pad = (d) => dimensionToPx(d, dpi, input.emPx);
981
+ const p = style.padding;
982
+ const inner = { x: rect.x + pad(p.left), y: rect.y + pad(p.top), width: rect.width - pad(p.left) - pad(p.right), height: rect.height - pad(p.top) - pad(p.bottom) };
983
+ const blocks = frameRuleBlocks(frame.rules, inner, linePx, 1, dpi, input.emPx);
984
+ if (blocks.length > 0)
985
+ emptyRules = blocks;
986
+ }
987
+ }
818
988
  const vdt = {
819
989
  id: frameIdOf(frame.name),
820
990
  name: frame.name,
@@ -828,6 +998,8 @@ function composePage(input, template, fit) {
828
998
  blockIds: [],
829
999
  configPath: frame.configPath,
830
1000
  ...(frame.exclude === 'push' ? { push: true } : {}),
1001
+ ...(collapsed.has(frame.name) ? { collapsed: true } : {}),
1002
+ ...(emptyRules ? { rules: emptyRules } : {}),
831
1003
  };
832
1004
  const decl = declared.get(frame.name);
833
1005
  if (decl && !sameRect(decl, rect))
@@ -842,6 +1014,14 @@ function composePage(input, template, fit) {
842
1014
  // Content from the page before (#859).
843
1015
  vdt.continuedFrom = cont.receivers.get(frame.name).fromFrameId;
844
1016
  }
1017
+ if (result) {
1018
+ // The marks whose `{page}` the build fills in (#917).
1019
+ if (goesOn && typeof frame.overflow.mark === 'string' && frame.overflow.mark.includes('{page}'))
1020
+ tagPageMark(result.frame, withPage(frame.overflow.mark, toGuess), frame.overflow.mark, 'to');
1021
+ const fromWords = frame.overflow.fromMark ?? cont?.receivers.get(headOf(frame.name))?.fromMark;
1022
+ if (p.fromMark !== undefined && fromWords?.includes('{page}'))
1023
+ tagPageMark(result.frame, p.fromMark, fromWords, 'from');
1024
+ }
845
1025
  if (result && st) {
846
1026
  const range = fragmentRange(st.L, from, st.cut);
847
1027
  commitFrame(result, rect, st.story, range, input, vdt, blocks);
@@ -857,6 +1037,8 @@ function composePage(input, template, fit) {
857
1037
  fitLog.push({ frame: frame.name, kind: 'textScaled', scale: s });
858
1038
  }
859
1039
  if (goesOn && st) {
1040
+ // Laid here even when none of it fits: the rest goes on (#912).
1041
+ laid.add(headOf(frame.name));
860
1042
  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 });
861
1043
  }
862
1044
  if (overflowPx > 0.5) {
@@ -1072,4 +1254,19 @@ export function composedPageAt(pages, idx) {
1072
1254
  return p;
1073
1255
  return undefined;
1074
1256
  }
1257
+ /** `words` with `{page}` as `page` (#917). */
1258
+ const withPage = (words, page) => words.split('{page}').join(page);
1259
+ /** Mark the design text of `box` that sets `shown` as a continuation mark
1260
+ * whose `{page}` the build fills in once pages are numbered (#917). */
1261
+ function tagPageMark(box, shown, text, page) {
1262
+ const want = shown.trim();
1263
+ for (const b of box.designOverlay?.blocks ?? []) {
1264
+ if (b.kind !== 'text' || !b.artifact || b.pageMark)
1265
+ continue;
1266
+ if (b.lines.map((l) => l.text).join(' ').trim() !== want && b.lines.map((l) => l.text).join('').trim() !== want)
1267
+ continue;
1268
+ b.pageMark = { text: text.trim(), page };
1269
+ return;
1270
+ }
1271
+ }
1075
1272
  //# sourceMappingURL=composedLayout.js.map