postext 1.58.3 → 1.59.1

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 (208) hide show
  1. package/dist/__tests__/defaults/headingsBalancing.test.js +8 -0
  2. package/dist/__tests__/defaults/headingsBalancing.test.js.map +1 -1
  3. package/dist/__tests__/design/titleTextWrapped.test.js +13 -10
  4. package/dist/__tests__/design/titleTextWrapped.test.js.map +1 -1
  5. package/dist/__tests__/exercises/config.test.d.ts +2 -0
  6. package/dist/__tests__/exercises/config.test.d.ts.map +1 -0
  7. package/dist/__tests__/exercises/config.test.js +63 -0
  8. package/dist/__tests__/exercises/config.test.js.map +1 -0
  9. package/dist/__tests__/exercises/digital.test.d.ts +2 -0
  10. package/dist/__tests__/exercises/digital.test.d.ts.map +1 -0
  11. package/dist/__tests__/exercises/digital.test.js +163 -0
  12. package/dist/__tests__/exercises/digital.test.js.map +1 -0
  13. package/dist/__tests__/exercises/languages.test.d.ts +2 -0
  14. package/dist/__tests__/exercises/languages.test.d.ts.map +1 -0
  15. package/dist/__tests__/exercises/languages.test.js +109 -0
  16. package/dist/__tests__/exercises/languages.test.js.map +1 -0
  17. package/dist/__tests__/exercises/layout.test.d.ts +2 -0
  18. package/dist/__tests__/exercises/layout.test.d.ts.map +1 -0
  19. package/dist/__tests__/exercises/layout.test.js +77 -0
  20. package/dist/__tests__/exercises/layout.test.js.map +1 -0
  21. package/dist/__tests__/exercises/model.test.d.ts +2 -0
  22. package/dist/__tests__/exercises/model.test.d.ts.map +1 -0
  23. package/dist/__tests__/exercises/model.test.js +90 -0
  24. package/dist/__tests__/exercises/model.test.js.map +1 -0
  25. package/dist/__tests__/exercises/parse.test.d.ts +2 -0
  26. package/dist/__tests__/exercises/parse.test.d.ts.map +1 -0
  27. package/dist/__tests__/exercises/parse.test.js +37 -0
  28. package/dist/__tests__/exercises/parse.test.js.map +1 -0
  29. package/dist/__tests__/exercises/refs.test.d.ts +2 -0
  30. package/dist/__tests__/exercises/refs.test.d.ts.map +1 -0
  31. package/dist/__tests__/exercises/refs.test.js +46 -0
  32. package/dist/__tests__/exercises/refs.test.js.map +1 -0
  33. package/dist/__tests__/exercises/solutions.test.d.ts +2 -0
  34. package/dist/__tests__/exercises/solutions.test.d.ts.map +1 -0
  35. package/dist/__tests__/exercises/solutions.test.js +53 -0
  36. package/dist/__tests__/exercises/solutions.test.js.map +1 -0
  37. package/dist/__tests__/exports.test.js +14 -0
  38. package/dist/__tests__/exports.test.js.map +1 -1
  39. package/dist/__tests__/parse/containers.test.js +2 -2
  40. package/dist/__tests__/parse/containers.test.js.map +1 -1
  41. package/dist/__tests__/pipeline/fillShortColumns.test.d.ts +2 -0
  42. package/dist/__tests__/pipeline/fillShortColumns.test.d.ts.map +1 -0
  43. package/dist/__tests__/pipeline/fillShortColumns.test.js +163 -0
  44. package/dist/__tests__/pipeline/fillShortColumns.test.js.map +1 -0
  45. package/dist/__tests__/pipeline/spanHeadingMidPage.test.js +52 -0
  46. package/dist/__tests__/pipeline/spanHeadingMidPage.test.js.map +1 -1
  47. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  48. package/dist/canvas-backend/blockRender.js +18 -0
  49. package/dist/canvas-backend/blockRender.js.map +1 -1
  50. package/dist/canvas-backend/chip.d.ts +3 -0
  51. package/dist/canvas-backend/chip.d.ts.map +1 -1
  52. package/dist/canvas-backend/chip.js +31 -1
  53. package/dist/canvas-backend/chip.js.map +1 -1
  54. package/dist/configWarnings.d.ts +3 -1
  55. package/dist/configWarnings.d.ts.map +1 -1
  56. package/dist/configWarnings.js +6 -2
  57. package/dist/configWarnings.js.map +1 -1
  58. package/dist/defaults/chipStyles.d.ts.map +1 -1
  59. package/dist/defaults/chipStyles.js +7 -0
  60. package/dist/defaults/chipStyles.js.map +1 -1
  61. package/dist/defaults/exercises.d.ts +111 -0
  62. package/dist/defaults/exercises.d.ts.map +1 -0
  63. package/dist/defaults/exercises.js +255 -0
  64. package/dist/defaults/exercises.js.map +1 -0
  65. package/dist/defaults/headings.d.ts +1 -0
  66. package/dist/defaults/headings.d.ts.map +1 -1
  67. package/dist/defaults/headings.js +7 -0
  68. package/dist/defaults/headings.js.map +1 -1
  69. package/dist/defaults/index.d.ts +2 -0
  70. package/dist/defaults/index.d.ts.map +1 -1
  71. package/dist/defaults/index.js +16 -0
  72. package/dist/defaults/index.js.map +1 -1
  73. package/dist/defaults/shared.d.ts +4 -0
  74. package/dist/defaults/shared.d.ts.map +1 -1
  75. package/dist/defaults/shared.js +6 -0
  76. package/dist/defaults/shared.js.map +1 -1
  77. package/dist/design/elementKeys.d.ts +2 -0
  78. package/dist/design/elementKeys.d.ts.map +1 -1
  79. package/dist/design/elementKeys.js +1 -1
  80. package/dist/design/elementKeys.js.map +1 -1
  81. package/dist/exercises/build.d.ts +60 -0
  82. package/dist/exercises/build.d.ts.map +1 -0
  83. package/dist/exercises/build.js +177 -0
  84. package/dist/exercises/build.js.map +1 -0
  85. package/dist/exercises/configWarnings.d.ts +11 -0
  86. package/dist/exercises/configWarnings.d.ts.map +1 -0
  87. package/dist/exercises/configWarnings.js +175 -0
  88. package/dist/exercises/configWarnings.js.map +1 -0
  89. package/dist/exercises/designs.d.ts +38 -0
  90. package/dist/exercises/designs.d.ts.map +1 -0
  91. package/dist/exercises/designs.js +143 -0
  92. package/dist/exercises/designs.js.map +1 -0
  93. package/dist/exercises/digital.d.ts +69 -0
  94. package/dist/exercises/digital.d.ts.map +1 -0
  95. package/dist/exercises/digital.js +58 -0
  96. package/dist/exercises/digital.js.map +1 -0
  97. package/dist/exercises/expand.d.ts +74 -0
  98. package/dist/exercises/expand.d.ts.map +1 -0
  99. package/dist/exercises/expand.js +1089 -0
  100. package/dist/exercises/expand.js.map +1 -0
  101. package/dist/exercises/grids.d.ts +62 -0
  102. package/dist/exercises/grids.d.ts.map +1 -0
  103. package/dist/exercises/grids.js +535 -0
  104. package/dist/exercises/grids.js.map +1 -0
  105. package/dist/exercises/model.d.ts +24 -0
  106. package/dist/exercises/model.d.ts.map +1 -0
  107. package/dist/exercises/model.js +360 -0
  108. package/dist/exercises/model.js.map +1 -0
  109. package/dist/exercises/record.d.ts +94 -0
  110. package/dist/exercises/record.d.ts.map +1 -0
  111. package/dist/exercises/record.js +9 -0
  112. package/dist/exercises/record.js.map +1 -0
  113. package/dist/exercises/runtime.d.ts +65 -0
  114. package/dist/exercises/runtime.d.ts.map +1 -0
  115. package/dist/exercises/runtime.js +1261 -0
  116. package/dist/exercises/runtime.js.map +1 -0
  117. package/dist/exercises/shuffle.d.ts +13 -0
  118. package/dist/exercises/shuffle.d.ts.map +1 -0
  119. package/dist/exercises/shuffle.js +56 -0
  120. package/dist/exercises/shuffle.js.map +1 -0
  121. package/dist/exercises/styles.d.ts +51 -0
  122. package/dist/exercises/styles.d.ts.map +1 -0
  123. package/dist/exercises/styles.js +221 -0
  124. package/dist/exercises/styles.js.map +1 -0
  125. package/dist/exercises/text.d.ts +31 -0
  126. package/dist/exercises/text.d.ts.map +1 -0
  127. package/dist/exercises/text.js +82 -0
  128. package/dist/exercises/text.js.map +1 -0
  129. package/dist/exercises/types.d.ts +125 -0
  130. package/dist/exercises/types.d.ts.map +1 -0
  131. package/dist/exercises/types.js +9 -0
  132. package/dist/exercises/types.js.map +1 -0
  133. package/dist/exercises/warnings.d.ts +16 -0
  134. package/dist/exercises/warnings.d.ts.map +1 -0
  135. package/dist/exercises/warnings.js +55 -0
  136. package/dist/exercises/warnings.js.map +1 -0
  137. package/dist/html-backend.d.ts +6 -0
  138. package/dist/html-backend.d.ts.map +1 -1
  139. package/dist/html-backend.js +67 -6
  140. package/dist/html-backend.js.map +1 -1
  141. package/dist/index.d.ts +6 -0
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +4 -0
  144. package/dist/index.js.map +1 -1
  145. package/dist/measure/cache.d.ts.map +1 -1
  146. package/dist/measure/cache.js +6 -1
  147. package/dist/measure/cache.js.map +1 -1
  148. package/dist/measure/rich.d.ts.map +1 -1
  149. package/dist/measure/rich.js +11 -1
  150. package/dist/measure/rich.js.map +1 -1
  151. package/dist/migrate/markdownRuns.js +1 -1
  152. package/dist/migrate/markdownRuns.js.map +1 -1
  153. package/dist/numbering.d.ts +3 -0
  154. package/dist/numbering.d.ts.map +1 -1
  155. package/dist/numbering.js +6 -0
  156. package/dist/numbering.js.map +1 -1
  157. package/dist/parse/blockParser.d.ts.map +1 -1
  158. package/dist/parse/blockParser.js +47 -4
  159. package/dist/parse/blockParser.js.map +1 -1
  160. package/dist/parse/exerciseGaps.d.ts +50 -0
  161. package/dist/parse/exerciseGaps.d.ts.map +1 -0
  162. package/dist/parse/exerciseGaps.js +151 -0
  163. package/dist/parse/exerciseGaps.js.map +1 -0
  164. package/dist/parse/index.d.ts +1 -1
  165. package/dist/parse/index.d.ts.map +1 -1
  166. package/dist/parse/index.js.map +1 -1
  167. package/dist/parse/sourceMapping.d.ts.map +1 -1
  168. package/dist/parse/sourceMapping.js +21 -0
  169. package/dist/parse/sourceMapping.js.map +1 -1
  170. package/dist/parse/types.d.ts +49 -1
  171. package/dist/parse/types.d.ts.map +1 -1
  172. package/dist/pipeline/build.d.ts.map +1 -1
  173. package/dist/pipeline/build.js +124 -29
  174. package/dist/pipeline/build.js.map +1 -1
  175. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  176. package/dist/pipeline/calloutLayout.js +1 -0
  177. package/dist/pipeline/calloutLayout.js.map +1 -1
  178. package/dist/pipeline/chips.d.ts.map +1 -1
  179. package/dist/pipeline/chips.js +2 -0
  180. package/dist/pipeline/chips.js.map +1 -1
  181. package/dist/pipeline/columnBalancing.d.ts +19 -4
  182. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  183. package/dist/pipeline/columnBalancing.js +31 -3
  184. package/dist/pipeline/columnBalancing.js.map +1 -1
  185. package/dist/pipeline/config.d.ts.map +1 -1
  186. package/dist/pipeline/config.js +6 -0
  187. package/dist/pipeline/config.js.map +1 -1
  188. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  189. package/dist/pipeline/contentWarnings.js +29 -1
  190. package/dist/pipeline/contentWarnings.js.map +1 -1
  191. package/dist/pipeline/continuation.d.ts.map +1 -1
  192. package/dist/pipeline/continuation.js +21 -6
  193. package/dist/pipeline/continuation.js.map +1 -1
  194. package/dist/pipeline/measureContentBlock.d.ts +9 -1
  195. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  196. package/dist/pipeline/measureContentBlock.js +4 -3
  197. package/dist/pipeline/measureContentBlock.js.map +1 -1
  198. package/dist/pipeline/outline.d.ts.map +1 -1
  199. package/dist/pipeline/outline.js +1 -0
  200. package/dist/pipeline/outline.js.map +1 -1
  201. package/dist/pipeline/textWrap.js +1 -1
  202. package/dist/pipeline/textWrap.js.map +1 -1
  203. package/dist/types.d.ts +331 -2
  204. package/dist/types.d.ts.map +1 -1
  205. package/dist/vdt.d.ts +74 -4
  206. package/dist/vdt.d.ts.map +1 -1
  207. package/dist/vdt.js.map +1 -1
  208. package/package.json +1 -1
@@ -22,6 +22,9 @@ import { stampCentralBaselines } from './verticalMetrics.js';
22
22
  import { orientLinesToFrames } from './mirrorFrame.js';
23
23
  import { verseMarginTopPx } from './buildBlockKind.js';
24
24
  import { verseLinesSettings, versesLineByLine } from './verseLines.js';
25
+ import { exerciseOutline, exerciseSettingsFor, expandDocumentExercises, markTurnedBoxes, nameExerciseAnchors, needsExerciseExpansion, withExerciseStyles } from '../exercises/build.js';
26
+ import { hasExercises } from '../exercises/model.js';
27
+ import { exerciseDigitalOf } from '../exercises/digital.js';
25
28
  import { arabicNumbersOf, resolveAllConfig, computeBaselineGrid, computeColumnBboxes, resolvedDirection, resolvedLocale, validateConfigCache } from './config.js';
26
29
  import { resolveDebugConfig } from '../defaults/debug.js';
27
30
  import { defaultFontSet, documentFontFaces, faceRequestKey, fontFallbacks } from '../fonts/faces.js';
@@ -63,7 +66,7 @@ import { comicStripExtent, comicStripOffset, comicStripPlacement, comicStripWidt
63
66
  import { comicSpreadGutter, isComicSpread, layoutComicSpread } from '../comics/spread.js';
64
67
  import { comicTextPanelsSupported, layoutComicTextFrames, layoutStripTextFrames, planComicTextPanels, wrapComicTextPanels } from '../comics/textPanels.js';
65
68
  import { applyStyleAttrs, computePageMetrics, composedLeafConfig, flowPageMirrored, pageMirrored, sheetRectToFlow, isMarkerBlock, nextNonMarkerBlock, spaceLinesAfter, prevNonMarkerBlock, rollbackTrailingBlocks, } from './buildHelpers.js';
66
- import { measureContentBlock } from './measureContentBlock.js';
69
+ import { measureContentBlock, wordSpacingProfile } from './measureContentBlock.js';
67
70
  import { containerStyle, paragraphStyleIdOf, planParagraphContainers } from './paragraphContainers.js';
68
71
  import { appendChapterEndNotes, footnoteIdsOfLines, footnoteParagraphStyle, noteContentBlock, noteNumberPositionOf, numberFootnotes, footnoteFormatOf, numberFootnotesByPlacement, sameFootnoteNumbers, splitFootnoteDefinitions, withFootnoteStyle, } from './footnotes.js';
69
72
  import { planParts, derivePartMeasureContext } from './parts.js';
@@ -75,6 +78,7 @@ import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, floatGapBel
75
78
  import { computeHeadingContext, computeResourceNumberingState, } from './resourceNumbering.js';
76
79
  import { documentLocale, effectiveResourceTypes } from '../defaults/resourceTypes.js';
77
80
  import { resolveCalloutStylesConfig } from '../defaults/calloutStyles.js';
81
+ import { relinkToPalette } from '../defaults/shared.js';
78
82
  import { codeBoxStyle, codeParseOptions, wrapCodeBlocks } from './codeBlocks.js';
79
83
  import { expandScoreBlocks, scoreInfoOf, scoreKeepHeight } from './scoreBlocks.js';
80
84
  import { buildCodeLineNumbers, codeOverflowWarnings } from './codeLines.js';
@@ -185,6 +189,11 @@ function chapterNumbersOfContent(blocks, resolved, prefixes, values, ordinalOffs
185
189
  * looseness counts from that optimum, so such a paragraph asks it for one
186
190
  * line less (EF-129: asked for one more, it could only match the target by
187
191
  * falling back to its optimum, unchecked).
192
+ *
193
+ * With `headings.balancing.fillShortColumns` (#1236) `extraLines` may be
194
+ * -1: the paragraph is set a line short, its tracking rungs taken in the
195
+ * other direction (tighter letters), and the outcome recorded as negative
196
+ * tracking.
188
197
  */
189
198
  function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome, offGrid) {
190
199
  const base = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride });
@@ -211,23 +220,32 @@ function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverri
211
220
  const onGrid = ctx.resolved.cjk.grid.enabled && composesAsCjk(rawBlock.text);
212
221
  const ladder = trackingLadder.length > 1 && (onGrid || mostlyJoiningScript(rawBlock.text)) ? trackingLadder.slice(0, 1) : trackingLadder;
213
222
  const gridCap = onGrid ? { gridTrackingEm: ctx.resolved.headings.balancing.maxTracking / 1000 } : {};
214
- const maxWordSpacing = ctx.resolved.bodyText.maxWordSpacing + 1e-9;
223
+ // With `fillShortColumns` (#1236) the limit is the one the runt fix
224
+ // keeps (EF-65): no justified line looser than `maxWordSpacing` or than
225
+ // the paragraph's own loosest one, and no more lines set ragged past 3x
226
+ // than it had. A narrow column of long words often passes the absolute
227
+ // limit in every setting of a paragraph, the breaker's own included.
228
+ const natural = ctx.resolved.headings.balancing.fillShortColumns ? wordSpacingProfile(base.measured.lines) : undefined;
229
+ const maxWordSpacing = Math.max(ctx.resolved.bodyText.maxWordSpacing, natural?.loosest ?? 0) + 1e-9;
215
230
  // A CJK line spread past its tracking cap (`cjkLoose`) is past the
216
231
  // limit too.
217
- const withinLimit = (lines) => lines.every((l) => l.isLastLine || ((l.justifiedSpaceRatio === undefined || l.justifiedSpaceRatio <= maxWordSpacing) && !l.cjkLoose));
232
+ const withinLimit = (lines) => natural
233
+ ? lines.every((l) => !l.cjkLoose) && ((p) => p.loosest <= maxWordSpacing && p.ragged <= natural.ragged)(wordSpacingProfile(lines))
234
+ : lines.every((l) => l.isLastLine || ((l.justifiedSpaceRatio === undefined || l.justifiedSpaceRatio <= maxWordSpacing) && !l.cjkLoose));
218
235
  const gains = (loose) => loose !== null
219
236
  && loose.measured.lines.length === target
220
237
  && !loose.measured.lastLineRunt
221
238
  && withinLimit(loose.measured.lines);
239
+ const sign = extraLines < 0 ? -1 : 1;
222
240
  for (const tracking of ladder) {
223
241
  const loose = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, {
224
242
  styleOverride,
225
243
  looseness,
226
- trackingEm: tracking > 0 ? tracking / 1000 : undefined,
244
+ trackingEm: tracking > 0 ? (sign * tracking) / 1000 : undefined,
227
245
  ...gridCap,
228
246
  });
229
247
  if (gains(loose)) {
230
- looseOutcome.set(blockIdx, tracking);
248
+ looseOutcome.set(blockIdx, sign * tracking);
231
249
  return loose;
232
250
  }
233
251
  }
@@ -288,7 +306,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
288
306
  * band above (a page-span box, a top float band) or a caption below has
289
307
  * shortened it, so what does not fit it may still fit a full column. */
290
308
  const shortSideColumn = (page, col) => col.bbox.height < page.contentArea.height - 0.5;
291
- const resolved = resolveAllConfig(config);
309
+ // Rebound once the content is read when it holds exercises (#1227): the
310
+ // config then also carries their chip and paragraph styles.
311
+ let resolved = resolveAllConfig(config);
292
312
  // Bitmaps sized by the document's `layout.bitmapResolution` carry it as
293
313
  // their own `resolution` from here on (#631); under the default policy
294
314
  // the content is untouched.
@@ -506,7 +526,21 @@ function placeDocumentPass(content, config, cache, options, hints) {
506
526
  // The citations of resource captions and notes (#529), formatted with
507
527
  // the text's.
508
528
  const captionCitations = citationsApplied && citationsApplied.captions.size > 0 ? citationsApplied.captions : undefined;
509
- const footnoteSplit = splitFootnoteDefinitions(citationsApplied?.blocks ?? parsedBody);
529
+ // Exercises (#1227): each set as a box of ordinary blocks, its solutions
530
+ // where the book prints them.
531
+ const exerciseExpansion = needsExerciseExpansion(citationsApplied?.blocks ?? parsedBody)
532
+ ? expandDocumentExercises(citationsApplied?.blocks ?? parsedBody, resolved, computeColumnBboxes(pageMetrics.contentArea, resolved)[0]?.width ?? pageMetrics.contentArea.width, continuation, content.outline)
533
+ : undefined;
534
+ if (exerciseExpansion) {
535
+ resolved = withExerciseStyles(resolved);
536
+ // The document's config names the exercises' chip and paragraph
537
+ // styles too: an output that styles its text from the config (a
538
+ // reflowable EPUB, #1231) finds them there.
539
+ doc.config = resolved;
540
+ doc.exercises = exerciseExpansion.records;
541
+ doc.exerciseDigital = exerciseDigitalOf(exerciseSettingsFor(resolved));
542
+ }
543
+ const footnoteSplit = splitFootnoteDefinitions(exerciseExpansion?.blocks ?? citationsApplied?.blocks ?? parsedBody);
510
544
  const footnoteDefs = footnoteSplit.defs;
511
545
  // Numbered by page or column (`numbering: 'page'`), a note takes the
512
546
  // number the previous build gave it where it landed; the first build
@@ -534,7 +568,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
534
568
  const resourceIds = new Set((content.resources ?? []).map((r) => r.id));
535
569
  const anchorRefs = hasAnchorRefs(parsedBlocks, resourceIds);
536
570
  const outline = content.outline
537
- ?? (hasTocDirective(parsedBlocks) || hasIndexDirective(parsedBlocks) || anchorRefs ? computeOutline(parsedBlocks, resolved, headingStart, continuation?.statementCounters) : undefined);
571
+ ?? (hasTocDirective(parsedBlocks) || hasIndexDirective(parsedBlocks) || anchorRefs
572
+ // A reference to an exercise prints its name (#1225).
573
+ ? nameExerciseAnchors(computeOutline(parsedBlocks, resolved, headingStart, continuation?.statementCounters), exerciseExpansion?.records ?? [])
574
+ : undefined);
538
575
  // A story carried in from another chapter (#943) may refer to anchors too.
539
576
  const carriedRefs = continuation?.jumps?.some((j) => j.outline) === true;
540
577
  const anchorRefContext = anchorRefs || carriedRefs
@@ -630,6 +667,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
630
667
  const syntheticBoxes = [
631
668
  ...(spanEmbeds.wrapped ? [spanEmbedStyle(bodyStyle.lineHeightPx, resolved.layout.inlineResourceGap !== 'above')] : []),
632
669
  ...(codeBoxes.wrapped ? [codeBoxStyle(resolved)] : []),
670
+ // The boxes exercises are set in (#1227).
671
+ ...(exerciseExpansion?.wrapped ? exerciseExpansion.boxes : []),
633
672
  // The frameless box of a composed page's story (#855).
634
673
  ...(composedWrap.wrapped || comicText.wrapped ? [frameBoxStyle(Math.max(resolved.bodyText.avoidOrphans ? resolved.bodyText.orphanMinLines : 1, resolved.bodyText.avoidWidows ? resolved.bodyText.widowMinLines : 1))] : []),
635
674
  // The boxes of main-flow groups: sub-columns a body line apart in the
@@ -638,12 +677,16 @@ function placeDocumentPass(content, config, cache, options, hints) {
638
677
  ? flowColumnsStyles(bodyStyle.lineHeightPx, dimensionToPx(resolved.layout.gutterWidth, resolved.page.dpi, bodyStyle.fontSizePx), resolved.bodyText.paragraphSpacing === false ? { value: 0, unit: 'px' } : { value: bodyStyle.marginBottomPx, unit: 'px' }, Math.max(resolved.bodyText.avoidOrphans ? resolved.bodyText.orphanMinLines : 1, resolved.bodyText.avoidWidows ? resolved.bodyText.widowMinLines : 1))
639
678
  : []),
640
679
  ];
680
+ const exerciseBoxIds = new Set(exerciseExpansion?.wrapped ? exerciseExpansion.boxes.map((b) => b.id) : []);
641
681
  const calloutStyles = syntheticBoxes.length > 0
642
- ? [...resolved.calloutStyles, ...resolveCalloutStylesConfig(syntheticBoxes, resolved.bodyText, resolved.headings, resolved.unorderedLists, config?.locale)]
682
+ ? [...resolved.calloutStyles, ...resolveCalloutStylesConfig(syntheticBoxes, resolved.bodyText, resolved.headings, resolved.unorderedLists, config?.locale)
683
+ // The exercise boxes' defaults (a solutions title, a bank's frame)
684
+ // link to `main-color`: set them to the document's palette.
685
+ .map((s) => (exerciseBoxIds.has(s.id) ? relinkToPalette(s, config?.colorPalette) : s))]
643
686
  : resolved.calloutStyles;
644
687
  // A box nested in another looks its style up in the configuration it is
645
688
  // laid out with: the listings' box among them.
646
- const calloutResolved = codeBoxes.wrapped || flowGroups.wrapped || composedWrap.wrapped || comicText.wrapped ? { ...resolved, calloutStyles } : resolved;
689
+ const calloutResolved = codeBoxes.wrapped || flowGroups.wrapped || composedWrap.wrapped || comicText.wrapped || exerciseExpansion?.wrapped ? { ...resolved, calloutStyles } : resolved;
647
690
  const blockquoteStyle = resolveBlockquoteStyle(resolved);
648
691
  const listLevelIndentsPx = computeLevelIndentsPx(resolved, bodyStyle.fontSizePx);
649
692
  const orderedMetrics = computeOrderedListRunMetrics(contentBlocks, resolved, bodyStyle.fontSizePx);
@@ -3336,6 +3379,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
3336
3379
  && !(lvl.advancedDesign.enabled && lvl.advancedDesign.slot.elements.length > 0)
3337
3380
  && !headingIsHidden(raw, lvl);
3338
3381
  };
3382
+ /** Whether a heading is measured across the content area: a `span:
3383
+ * 'page'` level (or style) that prints, painted by the default opener or
3384
+ * by a design of its own, which lays out across the page as well. At the
3385
+ * column width a designed opener's title broke into more lines than the
3386
+ * design paints, and those lines set its band: taller than the same
3387
+ * heading without a design, so that it no longer fitted under the text
3388
+ * of a page the plain one opens on (#1224). */
3389
+ const measuredAcrossPage = (raw) => {
3390
+ const lvl = headingLevels.forBlock(raw);
3391
+ return lvl?.span === 'page' && !headingIsHidden(raw, lvl);
3392
+ };
3339
3393
  /** Page-spanning heading placed in `headingCol`, reserving `h` px from the
3340
3394
  * top of the page: the same vertical band is reserved in every other
3341
3395
  * column of the page, so body text under the opener band starts below it
@@ -3522,6 +3576,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
3522
3576
  /** The band (`page:band`) a keep-with-next rollback last took each
3523
3577
  * heading out of, by content index. */
3524
3578
  const headingLeftBand = new Map();
3579
+ /** The band (`page:band`) each page-span heading opened under the text
3580
+ * of its page was set at the head of (#1238), by content index. */
3581
+ const spanHeadingOpenedHere = new Map();
3525
3582
  /**
3526
3583
  * A `span: 'page'` heading with `spanBreak: false` (#539) opens where the
3527
3584
  * text reaches: the band the flow is in closes level under its text (a
@@ -3556,7 +3613,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
3556
3613
  // opener paints it) or its design's band, with its margins.
3557
3614
  const width = page.contentArea.width;
3558
3615
  const measureCtx = partPlan.byBlock[blockIdx] ? partMeasureCtx : sectionMeasureCtx(sectionPlan.byBlock[blockIdx]);
3559
- const measured = measureContentBlock(rawBlock, blockIdx, opensDefaultOpener(rawBlock) ? width : cols[0].bbox.width, measureCtx);
3616
+ const measured = measureContentBlock(rawBlock, blockIdx, measuredAcrossPage(rawBlock) ? width : cols[0].bbox.width, measureCtx);
3560
3617
  if (!measured)
3561
3618
  return false;
3562
3619
  const style = measured.kind.style;
@@ -3586,6 +3643,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
3586
3643
  uncapBand(cols, uncappedBottoms);
3587
3644
  if (!closeBandAt(page, cols, cutY, top, cursor))
3588
3645
  return false;
3646
+ spanHeadingOpenedHere.set(blockIdx, `${page.index}:${currentBand(page, cursor)}`);
3589
3647
  pendingSpacing = 0;
3590
3648
  return true;
3591
3649
  };
@@ -3976,6 +4034,21 @@ function placeDocumentPass(content, config, cache, options, hints) {
3976
4034
  n++;
3977
4035
  return n;
3978
4036
  };
4037
+ /** Whether a block moving on whole may take the `run` headings closing
4038
+ * `col` along. A run that is all the column holds would open the next
4039
+ * column just as it opens this one, and is rolled back no further —
4040
+ * unless it opens a band a page-span heading cut under the text of the
4041
+ * page (`spanBreak: false`, #1238): rolled back, that heading opens the
4042
+ * next page instead (`headingLeftBand`), with the block it is kept with. */
4043
+ const headingRunMayLeave = (col, run) => {
4044
+ if (run <= 0)
4045
+ return false;
4046
+ if (run < col.blocks.length)
4047
+ return true;
4048
+ const head = col.blocks[0];
4049
+ return head.contentIndex !== undefined
4050
+ && spanHeadingOpenedHere.get(head.contentIndex) === `${cursor.pageIndex}:${col.band ?? 0}`;
4051
+ };
3979
4052
  /**
3980
4053
  * An index group divided between two columns keeps at least
3981
4054
  * `index.groups.splitMinEntries` entries on each side (#712). `entry` is
@@ -5482,7 +5555,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5482
5555
  break;
5483
5556
  }
5484
5557
  pendingSpacing = 0;
5485
- if (part === 0 && resolved.headings.keepWithNext && run > 0 && run < curCol.blocks.length) {
5558
+ if (part === 0 && resolved.headings.keepWithNext && headingRunMayLeave(curCol, run)) {
5486
5559
  const rolledBack = rollbackHeadings(curCol);
5487
5560
  advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
5488
5561
  return (rolledBack[0].contentIndex ?? startIdx - rolledBack.length) - 1;
@@ -5874,7 +5947,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5874
5947
  const spacingBefore = Math.max(pendingSpacing, floatGapPx);
5875
5948
  if (col.blocks.length > 0 && height > col.availableHeight - spacingBefore + FIT_EPS) {
5876
5949
  const run = trailingHeadingRun(col);
5877
- if (run > 0 && run < col.blocks.length) {
5950
+ if (headingRunMayLeave(col, run)) {
5878
5951
  const rolled = rollbackHeadings(col);
5879
5952
  pendingSpacing = 0;
5880
5953
  advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(height));
@@ -7118,11 +7191,11 @@ function placeDocumentPass(content, config, cache, options, hints) {
7118
7191
  && knownOutcome === undefined
7119
7192
  && (looseGained.get(budget.group) ?? 0) >= budget.need;
7120
7193
  const tryLoose = looseLines !== undefined && !budgetMet && knownOutcome !== null;
7121
- // A page-span heading without a design of its own is painted by the
7122
- // default opener across the content area: measured at that width, its
7123
- // band holds the title as it is painted (EF-100). A designed opener
7124
- // keeps the column-width measure: its design sets the band.
7125
- const measureWidth = rawBlock.type === 'heading' && opensDefaultOpener(rawBlock)
7194
+ // A page-span heading is painted across the content area, by the
7195
+ // default opener or by its design: measured at that width, its band
7196
+ // holds the title as it is painted (EF-100), and a design sets it no
7197
+ // taller than the lines it paints (#1224).
7198
+ const measureWidth = rawBlock.type === 'heading' && measuredAcrossPage(rawBlock)
7126
7199
  ? doc.pages[cursor.pageIndex].contentArea.width
7127
7200
  : col.bbox.width;
7128
7201
  // A resource on a vertical page stands upright, its frame at most as
@@ -7598,8 +7671,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
7598
7671
  * layout with text in both columns, a styled section with its own
7599
7672
  * margins — is broken again there: its placed lines stay, the rest is
7600
7673
  * broken for the new column. Not the loose paragraphs balancing runs
7601
- * long (placed whole where they were measured), nor a heading the
7602
- * default opener paints across the page. */
7674
+ * long (placed whole where they were measured), nor a page-span
7675
+ * heading, painted across the page. */
7603
7676
  let brokenForWidth = measureWidth;
7604
7677
  let laterWidths = [];
7605
7678
  /** Text wrap (#627): the lines of the block set short beside what text
@@ -7607,7 +7680,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
7607
7680
  let wrapInsets = [];
7608
7681
  const wrapsBeside = canSplit && runsBesideWrap(rawBlock);
7609
7682
  const rebreaksForColumn = !tryLoose && rawBlock.toc === undefined && rawBlock.index === undefined
7610
- && (canSplit || (vdtType === 'heading' && !opensDefaultOpener(rawBlock)));
7683
+ && (canSplit || (vdtType === 'heading' && !measuredAcrossPage(rawBlock)));
7611
7684
  /** Measure the block again for the columns it is set in (see
7612
7685
  * `laterWidths`), with `extra` options. */
7613
7686
  const measureForColumns = (extra) => measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
@@ -8157,7 +8230,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
8157
8230
  if (partIndex === 0 && curCol.blocks.length > 0) {
8158
8231
  // Moving on whole must not strand a heading closing the column.
8159
8232
  const headingRun = trailingHeadingRun(curCol);
8160
- if (resolved.headings.keepWithNext && headingRun > 0 && headingRun < curCol.blocks.length) {
8233
+ if (resolved.headings.keepWithNext && headingRunMayLeave(curCol, headingRun)) {
8161
8234
  const rolledBack = rollbackHeadings(curCol);
8162
8235
  if (rolledBack.length > 0) {
8163
8236
  blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
@@ -8728,7 +8801,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
8728
8801
  // Only a short column (under a float, a page-span box) lets the run
8729
8802
  // move on to a taller one.
8730
8803
  const headingRun = trailingHeadingRun(curCol);
8731
- const runFillsColumn = headingRun > 0 && headingRun === curCol.blocks.length;
8804
+ const runFillsColumn = headingRun > 0 && !headingRunMayLeave(curCol, headingRun);
8732
8805
  const strands = partIndex === 0 && vdtType !== 'heading'
8733
8806
  && headingRun > 0 && !runFillsColumn;
8734
8807
  const pullsRun = vdtType === 'heading' && (!runFillsColumn || shortColumn);
@@ -9076,6 +9149,9 @@ export function* buildDocumentGen(content, config, cache, options) {
9076
9149
  syncFontSet(fontSet);
9077
9150
  validateConfigCache(config);
9078
9151
  const doc = yield* buildDocumentRounds(content, config, cache, options);
9152
+ // Boxes printed upside down (#1229): their blocks learn the point they
9153
+ // turn about.
9154
+ markTurnedBoxes(doc);
9079
9155
  // References and markup the source names wrongly: read from the source
9080
9156
  // once, located on the pages of the finished layout. Kept apart from
9081
9157
  // `doc.warnings`, whose entries keep their postext 1.4 shape.
@@ -9391,10 +9467,10 @@ function* buildDocumentRounds(content, config, cache, options) {
9391
9467
  if (!hasAnchorRefs(parsed, new Set((content.resources ?? []).map((r) => r.id))))
9392
9468
  return false;
9393
9469
  const resolved = resolveAllConfig(config);
9394
- return printsAnchorPages(parsed, anchorTargetsOf(computeOutline(parsed, resolved, content.continuation?.headings, content.continuation?.statementCounters)), resolved.crossRefs.defaultStyle);
9470
+ return printsAnchorPages(parsed, anchorTargetsOf(ownOutline(parsed, resolved, content.continuation)), resolved.crossRefs.defaultStyle);
9395
9471
  };
9396
9472
  if (hasTocDirective(parsed) || hasIndexDirective(parsed) || pageRefs()) {
9397
- let outline = computeOutline(parsed, resolveAllConfig(config), content.continuation?.headings, content.continuation?.statementCounters);
9473
+ let outline = ownOutline(parsed, resolveAllConfig(config), content.continuation);
9398
9474
  let doc = withIndexMarks(yield* buildDocumentNumbered({ ...content, outline }, config, cache, options, 0, citing), content);
9399
9475
  for (let round = 0; round < MAX_TOC_ROUNDS; round++) {
9400
9476
  const after = outlineFromDoc(doc, outline);
@@ -9408,6 +9484,12 @@ function* buildDocumentRounds(content, config, cache, options) {
9408
9484
  }
9409
9485
  return withIndexMarks(yield* buildDocumentNumbered(content, config, cache, options, 0, citing), content);
9410
9486
  }
9487
+ /** A document's own outline, its exercises' anchors named as a reference
9488
+ * prints them ("Exercise 3", #1225). */
9489
+ function ownOutline(parsed, resolved, continuation) {
9490
+ const outline = computeOutline(parsed, resolved, continuation?.headings, continuation?.statementCounters);
9491
+ return hasExercises(parsed) ? nameExerciseAnchors(outline, exerciseOutline(parsed, resolved, continuation).records) : outline;
9492
+ }
9411
9493
  /** The number each note's marker prints in `doc` (its first marker). */
9412
9494
  function printedFootnoteNumbers(doc) {
9413
9495
  const out = new Map();
@@ -9620,6 +9702,7 @@ citing = { hints: new Map(), refused: new Set() }) {
9620
9702
  stable: score === 0,
9621
9703
  failedLoose: old?.failedLoose ?? new Set(),
9622
9704
  failedLines: old?.failedLines ?? new Set(),
9705
+ failedTight: old?.failedTight ?? new Set(),
9623
9706
  };
9624
9707
  });
9625
9708
  };
@@ -9798,7 +9881,7 @@ citing = { hints: new Map(), refused: new Set() }) {
9798
9881
  let hit = false;
9799
9882
  for (const k of newLoose)
9800
9883
  if (!cands || cands.has(k)) {
9801
- s.failedLoose.add(k);
9884
+ failLoose(s, k);
9802
9885
  hit = true;
9803
9886
  }
9804
9887
  if (hit && looseFirst)
@@ -9817,6 +9900,13 @@ citing = { hints: new Map(), refused: new Set() }) {
9817
9900
  return true;
9818
9901
  return blacklist(null);
9819
9902
  };
9903
+ /** The paragraphs the current proposal sets a line short (#1236): their
9904
+ * failures go to the segment's `failedTight`, the others' to
9905
+ * `failedLoose`. */
9906
+ let tightKeys = new Set();
9907
+ const failLoose = (s, k) => {
9908
+ (tightKeys.has(k) ? s.failedTight : s.failedLoose).add(k);
9909
+ };
9820
9910
  let balancingPasses = 0;
9821
9911
  function* balance() {
9822
9912
  while (balancingPasses < MAX_BALANCING_PASSES_PER_DOCUMENT) {
@@ -9827,11 +9917,14 @@ citing = { hints: new Map(), refused: new Set() }) {
9827
9917
  const owner = keyOwners(gaps);
9828
9918
  const failedLoose = new Set();
9829
9919
  const failedLines = new Set();
9920
+ const failedTight = new Set();
9830
9921
  for (const s of segments) {
9831
9922
  for (const k of s.failedLoose)
9832
9923
  failedLoose.add(k);
9833
9924
  for (const k of s.failedLines)
9834
9925
  failedLines.add(k);
9926
+ for (const k of s.failedTight)
9927
+ failedTight.add(k);
9835
9928
  }
9836
9929
  const proposal = proposeBalanceLines(best.doc, best.forcedBreakPages, applied, {
9837
9930
  maxLinesPerHeading: balancing.maxLinesPerHeading,
@@ -9847,6 +9940,7 @@ citing = { hints: new Map(), refused: new Set() }) {
9847
9940
  optimalLineBreaking: best.doc.config.bodyText.optimalLineBreaking,
9848
9941
  failedLoose,
9849
9942
  failedLines,
9943
+ ...(balancing.fillShortColumns ? { fillShortColumns: true, failedTight } : {}),
9850
9944
  closingBox: balancing.closingBox,
9851
9945
  ...(onGrid && balancing.gridLines === 'off' ? { gridLines: false } : {}),
9852
9946
  });
@@ -9879,6 +9973,7 @@ citing = { hints: new Map(), refused: new Set() }) {
9879
9973
  }
9880
9974
  newLoose.push(k);
9881
9975
  }
9976
+ tightKeys = new Set(newLoose.filter((k) => proposal.loose.get(k) < 0));
9882
9977
  const trying = new Set([...newLines, ...newLoose].map((k) => owner.get(k)));
9883
9978
  if (trying.size === 0) {
9884
9979
  // No stretch point can absorb the remaining gaps — stable.
@@ -9926,7 +10021,7 @@ citing = { hints: new Map(), refused: new Set() }) {
9926
10021
  // first) stay eligible for a later proposal.
9927
10022
  const looseFailed = keysLoose.filter((k) => next.looseOutcome.get(k) === null);
9928
10023
  for (const k of looseFailed)
9929
- s.failedLoose.add(k);
10024
+ failLoose(s, k);
9930
10025
  const looseWon = keysLoose.filter((k) => typeof next.looseOutcome.get(k) === 'number');
9931
10026
  // A box closing its column that moves down onto the column's last
9932
10027
  // grid slot (or level with the column beside it) closes less than a
@@ -9969,11 +10064,11 @@ citing = { hints: new Map(), refused: new Set() }) {
9969
10064
  if (!boxKeys.has(k))
9970
10065
  s.failedLines.add(k);
9971
10066
  for (const k of keysLoose)
9972
- s.failedLoose.add(k);
10067
+ failLoose(s, k);
9973
10068
  }
9974
10069
  else if (looseFailed.length > 0 || looseWon.length > 0) {
9975
10070
  for (const k of looseWon)
9976
- s.failedLoose.add(k);
10071
+ failLoose(s, k);
9977
10072
  }
9978
10073
  else {
9979
10074
  s.done = true;