postext 1.4.2 → 1.5.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 (1072) hide show
  1. package/README.md +76 -202
  2. package/dist/__tests__/breakOpportunities.test.d.ts +2 -0
  3. package/dist/__tests__/breakOpportunities.test.d.ts.map +1 -0
  4. package/dist/__tests__/breakOpportunities.test.js +204 -0
  5. package/dist/__tests__/breakOpportunities.test.js.map +1 -0
  6. package/dist/__tests__/bundle.test.js +399 -1
  7. package/dist/__tests__/bundle.test.js.map +1 -1
  8. package/dist/__tests__/bundleLocaleAndBytes.test.d.ts +2 -0
  9. package/dist/__tests__/bundleLocaleAndBytes.test.d.ts.map +1 -0
  10. package/dist/__tests__/bundleLocaleAndBytes.test.js +177 -0
  11. package/dist/__tests__/bundleLocaleAndBytes.test.js.map +1 -0
  12. package/dist/__tests__/colorAlpha.test.d.ts +2 -0
  13. package/dist/__tests__/colorAlpha.test.d.ts.map +1 -0
  14. package/dist/__tests__/colorAlpha.test.js +87 -0
  15. package/dist/__tests__/colorAlpha.test.js.map +1 -0
  16. package/dist/__tests__/columnBalancing.test.js +88 -0
  17. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  18. package/dist/__tests__/columnClip.test.js +20 -0
  19. package/dist/__tests__/columnClip.test.js.map +1 -1
  20. package/dist/__tests__/columnRuleBands.test.d.ts +2 -0
  21. package/dist/__tests__/columnRuleBands.test.d.ts.map +1 -0
  22. package/dist/__tests__/columnRuleBands.test.js +146 -0
  23. package/dist/__tests__/columnRuleBands.test.js.map +1 -0
  24. package/dist/__tests__/contentWarnings.test.d.ts +2 -0
  25. package/dist/__tests__/contentWarnings.test.d.ts.map +1 -0
  26. package/dist/__tests__/contentWarnings.test.js +219 -0
  27. package/dist/__tests__/contentWarnings.test.js.map +1 -0
  28. package/dist/__tests__/continuation.test.js +2 -1
  29. package/dist/__tests__/continuation.test.js.map +1 -1
  30. package/dist/__tests__/createLayout.test.js +18 -1
  31. package/dist/__tests__/createLayout.test.js.map +1 -1
  32. package/dist/__tests__/cropMarks.test.d.ts +2 -0
  33. package/dist/__tests__/cropMarks.test.d.ts.map +1 -0
  34. package/dist/__tests__/cropMarks.test.js +138 -0
  35. package/dist/__tests__/cropMarks.test.js.map +1 -0
  36. package/dist/__tests__/defaults/calloutStyles.test.js +30 -0
  37. package/dist/__tests__/defaults/calloutStyles.test.js.map +1 -1
  38. package/dist/__tests__/defaults/headingsBreakBefore.test.d.ts +2 -0
  39. package/dist/__tests__/defaults/headingsBreakBefore.test.d.ts.map +1 -0
  40. package/dist/__tests__/defaults/headingsBreakBefore.test.js +78 -0
  41. package/dist/__tests__/defaults/headingsBreakBefore.test.js.map +1 -0
  42. package/dist/__tests__/defaults/paragraphStyles.test.js +22 -0
  43. package/dist/__tests__/defaults/paragraphStyles.test.js.map +1 -1
  44. package/dist/__tests__/design/bleedClip.test.d.ts +2 -0
  45. package/dist/__tests__/design/bleedClip.test.d.ts.map +1 -0
  46. package/dist/__tests__/design/bleedClip.test.js +91 -0
  47. package/dist/__tests__/design/bleedClip.test.js.map +1 -0
  48. package/dist/__tests__/design/boxBorderInside.test.d.ts +2 -0
  49. package/dist/__tests__/design/boxBorderInside.test.d.ts.map +1 -0
  50. package/dist/__tests__/design/boxBorderInside.test.js +102 -0
  51. package/dist/__tests__/design/boxBorderInside.test.js.map +1 -0
  52. package/dist/__tests__/design/designLineHeight.test.d.ts +2 -0
  53. package/dist/__tests__/design/designLineHeight.test.d.ts.map +1 -0
  54. package/dist/__tests__/design/designLineHeight.test.js +108 -0
  55. package/dist/__tests__/design/designLineHeight.test.js.map +1 -0
  56. package/dist/__tests__/design/designText.test.d.ts +2 -0
  57. package/dist/__tests__/design/designText.test.d.ts.map +1 -0
  58. package/dist/__tests__/design/designText.test.js +331 -0
  59. package/dist/__tests__/design/designText.test.js.map +1 -0
  60. package/dist/__tests__/design/designTextJustify.test.d.ts +2 -0
  61. package/dist/__tests__/design/designTextJustify.test.d.ts.map +1 -0
  62. package/dist/__tests__/design/designTextJustify.test.js +119 -0
  63. package/dist/__tests__/design/designTextJustify.test.js.map +1 -0
  64. package/dist/__tests__/design/designTextNegativeTracking.test.d.ts +2 -0
  65. package/dist/__tests__/design/designTextNegativeTracking.test.d.ts.map +1 -0
  66. package/dist/__tests__/design/designTextNegativeTracking.test.js +95 -0
  67. package/dist/__tests__/design/designTextNegativeTracking.test.js.map +1 -0
  68. package/dist/__tests__/design/designTextRender.test.d.ts +2 -0
  69. package/dist/__tests__/design/designTextRender.test.d.ts.map +1 -0
  70. package/dist/__tests__/design/designTextRender.test.js +106 -0
  71. package/dist/__tests__/design/designTextRender.test.js.map +1 -0
  72. package/dist/__tests__/design/dropCap.test.d.ts +2 -0
  73. package/dist/__tests__/design/dropCap.test.d.ts.map +1 -0
  74. package/dist/__tests__/design/dropCap.test.js +205 -0
  75. package/dist/__tests__/design/dropCap.test.js.map +1 -0
  76. package/dist/__tests__/design/ellipsis.test.d.ts +2 -0
  77. package/dist/__tests__/design/ellipsis.test.d.ts.map +1 -0
  78. package/dist/__tests__/design/ellipsis.test.js +113 -0
  79. package/dist/__tests__/design/ellipsis.test.js.map +1 -0
  80. package/dist/__tests__/design/headingDesignClip.test.d.ts +2 -0
  81. package/dist/__tests__/design/headingDesignClip.test.d.ts.map +1 -0
  82. package/dist/__tests__/design/headingDesignClip.test.js +60 -0
  83. package/dist/__tests__/design/headingDesignClip.test.js.map +1 -0
  84. package/dist/__tests__/design/headingDesignSideChannel.test.d.ts +2 -0
  85. package/dist/__tests__/design/headingDesignSideChannel.test.d.ts.map +1 -0
  86. package/dist/__tests__/design/headingDesignSideChannel.test.js +187 -0
  87. package/dist/__tests__/design/headingDesignSideChannel.test.js.map +1 -0
  88. package/dist/__tests__/design/inColumnTitleBreak.test.d.ts +2 -0
  89. package/dist/__tests__/design/inColumnTitleBreak.test.d.ts.map +1 -0
  90. package/dist/__tests__/design/inColumnTitleBreak.test.js +60 -0
  91. package/dist/__tests__/design/inColumnTitleBreak.test.js.map +1 -0
  92. package/dist/__tests__/design/openerBottomAnchor.test.d.ts +2 -0
  93. package/dist/__tests__/design/openerBottomAnchor.test.d.ts.map +1 -0
  94. package/dist/__tests__/design/openerBottomAnchor.test.js +106 -0
  95. package/dist/__tests__/design/openerBottomAnchor.test.js.map +1 -0
  96. package/dist/__tests__/design/openerChapterNumber.test.d.ts +2 -0
  97. package/dist/__tests__/design/openerChapterNumber.test.d.ts.map +1 -0
  98. package/dist/__tests__/design/openerChapterNumber.test.js +163 -0
  99. package/dist/__tests__/design/openerChapterNumber.test.js.map +1 -0
  100. package/dist/__tests__/design/openerImageReserve.test.d.ts +2 -0
  101. package/dist/__tests__/design/openerImageReserve.test.d.ts.map +1 -0
  102. package/dist/__tests__/design/openerImageReserve.test.js +83 -0
  103. package/dist/__tests__/design/openerImageReserve.test.js.map +1 -0
  104. package/dist/__tests__/design/openerReserve.test.d.ts +2 -0
  105. package/dist/__tests__/design/openerReserve.test.d.ts.map +1 -0
  106. package/dist/__tests__/design/openerReserve.test.js +147 -0
  107. package/dist/__tests__/design/openerReserve.test.js.map +1 -0
  108. package/dist/__tests__/design/ruleDefaults.test.d.ts +2 -0
  109. package/dist/__tests__/design/ruleDefaults.test.d.ts.map +1 -0
  110. package/dist/__tests__/design/ruleDefaults.test.js +59 -0
  111. package/dist/__tests__/design/ruleDefaults.test.js.map +1 -0
  112. package/dist/__tests__/design/runningHeads.test.js +4 -2
  113. package/dist/__tests__/design/runningHeads.test.js.map +1 -1
  114. package/dist/__tests__/design/titleTextWrapped.test.d.ts +2 -0
  115. package/dist/__tests__/design/titleTextWrapped.test.d.ts.map +1 -0
  116. package/dist/__tests__/design/titleTextWrapped.test.js +258 -0
  117. package/dist/__tests__/design/titleTextWrapped.test.js.map +1 -0
  118. package/dist/__tests__/design/trackingAlignment.test.d.ts +2 -0
  119. package/dist/__tests__/design/trackingAlignment.test.d.ts.map +1 -0
  120. package/dist/__tests__/design/trackingAlignment.test.js +101 -0
  121. package/dist/__tests__/design/trackingAlignment.test.js.map +1 -0
  122. package/dist/__tests__/design/verticalAlignOverflow.test.d.ts +2 -0
  123. package/dist/__tests__/design/verticalAlignOverflow.test.d.ts.map +1 -0
  124. package/dist/__tests__/design/verticalAlignOverflow.test.js +68 -0
  125. package/dist/__tests__/design/verticalAlignOverflow.test.js.map +1 -0
  126. package/dist/__tests__/exports.test.js +24 -0
  127. package/dist/__tests__/exports.test.js.map +1 -1
  128. package/dist/__tests__/fontStack.test.d.ts +2 -0
  129. package/dist/__tests__/fontStack.test.d.ts.map +1 -0
  130. package/dist/__tests__/fontStack.test.js +100 -0
  131. package/dist/__tests__/fontStack.test.js.map +1 -0
  132. package/dist/__tests__/frontmatterValues.test.d.ts +2 -0
  133. package/dist/__tests__/frontmatterValues.test.d.ts.map +1 -0
  134. package/dist/__tests__/frontmatterValues.test.js +119 -0
  135. package/dist/__tests__/frontmatterValues.test.js.map +1 -0
  136. package/dist/__tests__/gradedRunt.test.d.ts +2 -0
  137. package/dist/__tests__/gradedRunt.test.d.ts.map +1 -0
  138. package/dist/__tests__/gradedRunt.test.js +66 -0
  139. package/dist/__tests__/gradedRunt.test.js.map +1 -0
  140. package/dist/__tests__/headingAlign.test.d.ts +2 -0
  141. package/dist/__tests__/headingAlign.test.d.ts.map +1 -0
  142. package/dist/__tests__/headingAlign.test.js +92 -0
  143. package/dist/__tests__/headingAlign.test.js.map +1 -0
  144. package/dist/__tests__/headingInlineMarks.test.d.ts +2 -0
  145. package/dist/__tests__/headingInlineMarks.test.d.ts.map +1 -0
  146. package/dist/__tests__/headingInlineMarks.test.js +126 -0
  147. package/dist/__tests__/headingInlineMarks.test.js.map +1 -0
  148. package/dist/__tests__/headingLetterSpacing.test.d.ts +2 -0
  149. package/dist/__tests__/headingLetterSpacing.test.d.ts.map +1 -0
  150. package/dist/__tests__/headingLetterSpacing.test.js +135 -0
  151. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -0
  152. package/dist/__tests__/headingNumberPlaceholders.test.d.ts +2 -0
  153. package/dist/__tests__/headingNumberPlaceholders.test.d.ts.map +1 -0
  154. package/dist/__tests__/headingNumberPlaceholders.test.js +104 -0
  155. package/dist/__tests__/headingNumberPlaceholders.test.js.map +1 -0
  156. package/dist/__tests__/headingNumberingStyles.test.d.ts +2 -0
  157. package/dist/__tests__/headingNumberingStyles.test.d.ts.map +1 -0
  158. package/dist/__tests__/headingNumberingStyles.test.js +151 -0
  159. package/dist/__tests__/headingNumberingStyles.test.js.map +1 -0
  160. package/dist/__tests__/headingSourceTitle.test.d.ts +2 -0
  161. package/dist/__tests__/headingSourceTitle.test.d.ts.map +1 -0
  162. package/dist/__tests__/headingSourceTitle.test.js +35 -0
  163. package/dist/__tests__/headingSourceTitle.test.js.map +1 -0
  164. package/dist/__tests__/headingStyleBreakAfterPagebreak.test.d.ts +2 -0
  165. package/dist/__tests__/headingStyleBreakAfterPagebreak.test.d.ts.map +1 -0
  166. package/dist/__tests__/headingStyleBreakAfterPagebreak.test.js +52 -0
  167. package/dist/__tests__/headingStyleBreakAfterPagebreak.test.js.map +1 -0
  168. package/dist/__tests__/hiddenHeadings.test.d.ts +2 -0
  169. package/dist/__tests__/hiddenHeadings.test.d.ts.map +1 -0
  170. package/dist/__tests__/hiddenHeadings.test.js +137 -0
  171. package/dist/__tests__/hiddenHeadings.test.js.map +1 -0
  172. package/dist/__tests__/htmlHostReset.test.d.ts +2 -0
  173. package/dist/__tests__/htmlHostReset.test.d.ts.map +1 -0
  174. package/dist/__tests__/htmlHostReset.test.js +63 -0
  175. package/dist/__tests__/htmlHostReset.test.js.map +1 -0
  176. package/dist/__tests__/hyphenationLocale.test.d.ts +2 -0
  177. package/dist/__tests__/hyphenationLocale.test.d.ts.map +1 -0
  178. package/dist/__tests__/hyphenationLocale.test.js +143 -0
  179. package/dist/__tests__/hyphenationLocale.test.js.map +1 -0
  180. package/dist/__tests__/justifyTracking.test.d.ts +2 -0
  181. package/dist/__tests__/justifyTracking.test.d.ts.map +1 -0
  182. package/dist/__tests__/justifyTracking.test.js +217 -0
  183. package/dist/__tests__/justifyTracking.test.js.map +1 -0
  184. package/dist/__tests__/layoutWorkerClient.test.d.ts +2 -0
  185. package/dist/__tests__/layoutWorkerClient.test.d.ts.map +1 -0
  186. package/dist/__tests__/layoutWorkerClient.test.js +140 -0
  187. package/dist/__tests__/layoutWorkerClient.test.js.map +1 -0
  188. package/dist/__tests__/links.test.d.ts +2 -0
  189. package/dist/__tests__/links.test.d.ts.map +1 -0
  190. package/dist/__tests__/links.test.js +227 -0
  191. package/dist/__tests__/links.test.js.map +1 -0
  192. package/dist/__tests__/localisedStrings.test.d.ts +2 -0
  193. package/dist/__tests__/localisedStrings.test.d.ts.map +1 -0
  194. package/dist/__tests__/localisedStrings.test.js +118 -0
  195. package/dist/__tests__/localisedStrings.test.js.map +1 -0
  196. package/dist/__tests__/looseLines.test.d.ts +2 -0
  197. package/dist/__tests__/looseLines.test.d.ts.map +1 -0
  198. package/dist/__tests__/looseLines.test.js +72 -0
  199. package/dist/__tests__/looseLines.test.js.map +1 -0
  200. package/dist/__tests__/mainEntryReact.test.d.ts +2 -0
  201. package/dist/__tests__/mainEntryReact.test.d.ts.map +1 -0
  202. package/dist/__tests__/mainEntryReact.test.js +13 -0
  203. package/dist/__tests__/mainEntryReact.test.js.map +1 -0
  204. package/dist/__tests__/mathBundle.test.d.ts +2 -0
  205. package/dist/__tests__/mathBundle.test.d.ts.map +1 -0
  206. package/dist/__tests__/mathBundle.test.js +80 -0
  207. package/dist/__tests__/mathBundle.test.js.map +1 -0
  208. package/dist/__tests__/mathEngine.test.d.ts +2 -0
  209. package/dist/__tests__/mathEngine.test.d.ts.map +1 -0
  210. package/dist/__tests__/mathEngine.test.js +69 -0
  211. package/dist/__tests__/mathEngine.test.js.map +1 -0
  212. package/dist/__tests__/mathEngineInit.test.d.ts +2 -0
  213. package/dist/__tests__/mathEngineInit.test.d.ts.map +1 -0
  214. package/dist/__tests__/mathEngineInit.test.js +33 -0
  215. package/dist/__tests__/mathEngineInit.test.js.map +1 -0
  216. package/dist/__tests__/mathSize.test.d.ts +2 -0
  217. package/dist/__tests__/mathSize.test.d.ts.map +1 -0
  218. package/dist/__tests__/mathSize.test.js +168 -0
  219. package/dist/__tests__/mathSize.test.js.map +1 -0
  220. package/dist/__tests__/mathSizeMigration.test.d.ts +2 -0
  221. package/dist/__tests__/mathSizeMigration.test.d.ts.map +1 -0
  222. package/dist/__tests__/mathSizeMigration.test.js +188 -0
  223. package/dist/__tests__/mathSizeMigration.test.js.map +1 -0
  224. package/dist/__tests__/measureCache.test.js +148 -4
  225. package/dist/__tests__/measureCache.test.js.map +1 -1
  226. package/dist/__tests__/measureCacheKeepBreaks.test.d.ts +2 -0
  227. package/dist/__tests__/measureCacheKeepBreaks.test.d.ts.map +1 -0
  228. package/dist/__tests__/measureCacheKeepBreaks.test.js +59 -0
  229. package/dist/__tests__/measureCacheKeepBreaks.test.js.map +1 -0
  230. package/dist/__tests__/noBreakSpaces.test.d.ts +2 -0
  231. package/dist/__tests__/noBreakSpaces.test.d.ts.map +1 -0
  232. package/dist/__tests__/noBreakSpaces.test.js +208 -0
  233. package/dist/__tests__/noBreakSpaces.test.js.map +1 -0
  234. package/dist/__tests__/numberFormats.test.d.ts +2 -0
  235. package/dist/__tests__/numberFormats.test.d.ts.map +1 -0
  236. package/dist/__tests__/numberFormats.test.js +142 -0
  237. package/dist/__tests__/numberFormats.test.js.map +1 -0
  238. package/dist/__tests__/paletteEverywhere.test.d.ts +2 -0
  239. package/dist/__tests__/paletteEverywhere.test.d.ts.map +1 -0
  240. package/dist/__tests__/paletteEverywhere.test.js +228 -0
  241. package/dist/__tests__/paletteEverywhere.test.js.map +1 -0
  242. package/dist/__tests__/paragraphStyleIndent.test.d.ts +2 -0
  243. package/dist/__tests__/paragraphStyleIndent.test.d.ts.map +1 -0
  244. package/dist/__tests__/paragraphStyleIndent.test.js +91 -0
  245. package/dist/__tests__/paragraphStyleIndent.test.js.map +1 -0
  246. package/dist/__tests__/parse/attrQuoting.test.d.ts +2 -0
  247. package/dist/__tests__/parse/attrQuoting.test.d.ts.map +1 -0
  248. package/dist/__tests__/parse/attrQuoting.test.js +87 -0
  249. package/dist/__tests__/parse/attrQuoting.test.js.map +1 -0
  250. package/dist/__tests__/parse/codeSpans.test.d.ts +2 -0
  251. package/dist/__tests__/parse/codeSpans.test.d.ts.map +1 -0
  252. package/dist/__tests__/parse/codeSpans.test.js +31 -0
  253. package/dist/__tests__/parse/codeSpans.test.js.map +1 -0
  254. package/dist/__tests__/parse/intrawordUnderscore.test.d.ts +2 -0
  255. package/dist/__tests__/parse/intrawordUnderscore.test.d.ts.map +1 -0
  256. package/dist/__tests__/parse/intrawordUnderscore.test.js +69 -0
  257. package/dist/__tests__/parse/intrawordUnderscore.test.js.map +1 -0
  258. package/dist/__tests__/parse/scripts.test.js +4 -1
  259. package/dist/__tests__/parse/scripts.test.js.map +1 -1
  260. package/dist/__tests__/parts.test.js +103 -0
  261. package/dist/__tests__/parts.test.js.map +1 -1
  262. package/dist/__tests__/pipeline/balancingRecord.test.d.ts +2 -0
  263. package/dist/__tests__/pipeline/balancingRecord.test.d.ts.map +1 -0
  264. package/dist/__tests__/pipeline/balancingRecord.test.js +176 -0
  265. package/dist/__tests__/pipeline/balancingRecord.test.js.map +1 -0
  266. package/dist/__tests__/pipeline/blockquoteStyle.test.d.ts +2 -0
  267. package/dist/__tests__/pipeline/blockquoteStyle.test.d.ts.map +1 -0
  268. package/dist/__tests__/pipeline/blockquoteStyle.test.js +99 -0
  269. package/dist/__tests__/pipeline/blockquoteStyle.test.js.map +1 -0
  270. package/dist/__tests__/pipeline/calloutContinuation.test.d.ts +2 -0
  271. package/dist/__tests__/pipeline/calloutContinuation.test.d.ts.map +1 -0
  272. package/dist/__tests__/pipeline/calloutContinuation.test.js +133 -0
  273. package/dist/__tests__/pipeline/calloutContinuation.test.js.map +1 -0
  274. package/dist/__tests__/pipeline/calloutContinuationInset.test.d.ts +2 -0
  275. package/dist/__tests__/pipeline/calloutContinuationInset.test.d.ts.map +1 -0
  276. package/dist/__tests__/pipeline/calloutContinuationInset.test.js +116 -0
  277. package/dist/__tests__/pipeline/calloutContinuationInset.test.js.map +1 -0
  278. package/dist/__tests__/pipeline/calloutCornerWideIcon.test.d.ts +2 -0
  279. package/dist/__tests__/pipeline/calloutCornerWideIcon.test.d.ts.map +1 -0
  280. package/dist/__tests__/pipeline/calloutCornerWideIcon.test.js +69 -0
  281. package/dist/__tests__/pipeline/calloutCornerWideIcon.test.js.map +1 -0
  282. package/dist/__tests__/pipeline/calloutFloatMargins.test.d.ts +2 -0
  283. package/dist/__tests__/pipeline/calloutFloatMargins.test.d.ts.map +1 -0
  284. package/dist/__tests__/pipeline/calloutFloatMargins.test.js +104 -0
  285. package/dist/__tests__/pipeline/calloutFloatMargins.test.js.map +1 -0
  286. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.d.ts +2 -0
  287. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.d.ts.map +1 -0
  288. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +148 -0
  289. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js.map +1 -0
  290. package/dist/__tests__/pipeline/calloutListColor.test.d.ts +2 -0
  291. package/dist/__tests__/pipeline/calloutListColor.test.d.ts.map +1 -0
  292. package/dist/__tests__/pipeline/calloutListColor.test.js +86 -0
  293. package/dist/__tests__/pipeline/calloutListColor.test.js.map +1 -0
  294. package/dist/__tests__/pipeline/calloutRaggedLines.test.d.ts +2 -0
  295. package/dist/__tests__/pipeline/calloutRaggedLines.test.d.ts.map +1 -0
  296. package/dist/__tests__/pipeline/calloutRaggedLines.test.js +65 -0
  297. package/dist/__tests__/pipeline/calloutRaggedLines.test.js.map +1 -0
  298. package/dist/__tests__/pipeline/calloutRuntTracking.test.d.ts +2 -0
  299. package/dist/__tests__/pipeline/calloutRuntTracking.test.d.ts.map +1 -0
  300. package/dist/__tests__/pipeline/calloutRuntTracking.test.js +84 -0
  301. package/dist/__tests__/pipeline/calloutRuntTracking.test.js.map +1 -0
  302. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.d.ts +2 -0
  303. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.d.ts.map +1 -0
  304. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +187 -0
  305. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js.map +1 -0
  306. package/dist/__tests__/pipeline/calloutStripeRadius.test.d.ts +2 -0
  307. package/dist/__tests__/pipeline/calloutStripeRadius.test.d.ts.map +1 -0
  308. package/dist/__tests__/pipeline/calloutStripeRadius.test.js +127 -0
  309. package/dist/__tests__/pipeline/calloutStripeRadius.test.js.map +1 -0
  310. package/dist/__tests__/pipeline/calloutTitleLineHeight.test.d.ts +2 -0
  311. package/dist/__tests__/pipeline/calloutTitleLineHeight.test.d.ts.map +1 -0
  312. package/dist/__tests__/pipeline/calloutTitleLineHeight.test.js +81 -0
  313. package/dist/__tests__/pipeline/calloutTitleLineHeight.test.js.map +1 -0
  314. package/dist/__tests__/pipeline/calloutTypography.test.d.ts +2 -0
  315. package/dist/__tests__/pipeline/calloutTypography.test.d.ts.map +1 -0
  316. package/dist/__tests__/pipeline/calloutTypography.test.js +97 -0
  317. package/dist/__tests__/pipeline/calloutTypography.test.js.map +1 -0
  318. package/dist/__tests__/pipeline/capUnderHeadFloats.test.d.ts +2 -0
  319. package/dist/__tests__/pipeline/capUnderHeadFloats.test.d.ts.map +1 -0
  320. package/dist/__tests__/pipeline/capUnderHeadFloats.test.js +68 -0
  321. package/dist/__tests__/pipeline/capUnderHeadFloats.test.js.map +1 -0
  322. package/dist/__tests__/pipeline/captionAlign.test.d.ts +2 -0
  323. package/dist/__tests__/pipeline/captionAlign.test.d.ts.map +1 -0
  324. package/dist/__tests__/pipeline/captionAlign.test.js +99 -0
  325. package/dist/__tests__/pipeline/captionAlign.test.js.map +1 -0
  326. package/dist/__tests__/pipeline/chipPalette.test.d.ts +2 -0
  327. package/dist/__tests__/pipeline/chipPalette.test.d.ts.map +1 -0
  328. package/dist/__tests__/pipeline/chipPalette.test.js +80 -0
  329. package/dist/__tests__/pipeline/chipPalette.test.js.map +1 -0
  330. package/dist/__tests__/pipeline/chipVerticalPadding.test.d.ts +2 -0
  331. package/dist/__tests__/pipeline/chipVerticalPadding.test.d.ts.map +1 -0
  332. package/dist/__tests__/pipeline/chipVerticalPadding.test.js +66 -0
  333. package/dist/__tests__/pipeline/chipVerticalPadding.test.js.map +1 -0
  334. package/dist/__tests__/pipeline/chips.test.js +32 -0
  335. package/dist/__tests__/pipeline/chips.test.js.map +1 -1
  336. package/dist/__tests__/pipeline/closingBoxLever.test.d.ts +2 -0
  337. package/dist/__tests__/pipeline/closingBoxLever.test.d.ts.map +1 -0
  338. package/dist/__tests__/pipeline/closingBoxLever.test.js +68 -0
  339. package/dist/__tests__/pipeline/closingBoxLever.test.js.map +1 -0
  340. package/dist/__tests__/pipeline/closingBoxOrder.test.d.ts +2 -0
  341. package/dist/__tests__/pipeline/closingBoxOrder.test.d.ts.map +1 -0
  342. package/dist/__tests__/pipeline/closingBoxOrder.test.js +87 -0
  343. package/dist/__tests__/pipeline/closingBoxOrder.test.js.map +1 -0
  344. package/dist/__tests__/pipeline/closingPageAfterFloat.test.d.ts +2 -0
  345. package/dist/__tests__/pipeline/closingPageAfterFloat.test.d.ts.map +1 -0
  346. package/dist/__tests__/pipeline/closingPageAfterFloat.test.js +159 -0
  347. package/dist/__tests__/pipeline/closingPageAfterFloat.test.js.map +1 -0
  348. package/dist/__tests__/pipeline/closingPageAfterFloatPage.test.d.ts +2 -0
  349. package/dist/__tests__/pipeline/closingPageAfterFloatPage.test.d.ts.map +1 -0
  350. package/dist/__tests__/pipeline/closingPageAfterFloatPage.test.js +90 -0
  351. package/dist/__tests__/pipeline/closingPageAfterFloatPage.test.js.map +1 -0
  352. package/dist/__tests__/pipeline/colonBalancing.test.js +19 -15
  353. package/dist/__tests__/pipeline/colonBalancing.test.js.map +1 -1
  354. package/dist/__tests__/pipeline/colonListRoom.test.d.ts +2 -0
  355. package/dist/__tests__/pipeline/colonListRoom.test.d.ts.map +1 -0
  356. package/dist/__tests__/pipeline/colonListRoom.test.js +127 -0
  357. package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -0
  358. package/dist/__tests__/pipeline/columnWidthRebreak.test.d.ts +2 -0
  359. package/dist/__tests__/pipeline/columnWidthRebreak.test.d.ts.map +1 -0
  360. package/dist/__tests__/pipeline/columnWidthRebreak.test.js +124 -0
  361. package/dist/__tests__/pipeline/columnWidthRebreak.test.js.map +1 -0
  362. package/dist/__tests__/pipeline/compoundBreaks.test.d.ts +2 -0
  363. package/dist/__tests__/pipeline/compoundBreaks.test.d.ts.map +1 -0
  364. package/dist/__tests__/pipeline/compoundBreaks.test.js +368 -0
  365. package/dist/__tests__/pipeline/compoundBreaks.test.js.map +1 -0
  366. package/dist/__tests__/pipeline/continuationUnderFloat.test.d.ts +2 -0
  367. package/dist/__tests__/pipeline/continuationUnderFloat.test.d.ts.map +1 -0
  368. package/dist/__tests__/pipeline/continuationUnderFloat.test.js +64 -0
  369. package/dist/__tests__/pipeline/continuationUnderFloat.test.js.map +1 -0
  370. package/dist/__tests__/pipeline/crossChapterRef.test.d.ts +2 -0
  371. package/dist/__tests__/pipeline/crossChapterRef.test.d.ts.map +1 -0
  372. package/dist/__tests__/pipeline/crossChapterRef.test.js +124 -0
  373. package/dist/__tests__/pipeline/crossChapterRef.test.js.map +1 -0
  374. package/dist/__tests__/pipeline/dashBreaks.test.d.ts +2 -0
  375. package/dist/__tests__/pipeline/dashBreaks.test.d.ts.map +1 -0
  376. package/dist/__tests__/pipeline/dashBreaks.test.js +358 -0
  377. package/dist/__tests__/pipeline/dashBreaks.test.js.map +1 -0
  378. package/dist/__tests__/pipeline/defaultOpenerPaint.test.d.ts +2 -0
  379. package/dist/__tests__/pipeline/defaultOpenerPaint.test.d.ts.map +1 -0
  380. package/dist/__tests__/pipeline/defaultOpenerPaint.test.js +128 -0
  381. package/dist/__tests__/pipeline/defaultOpenerPaint.test.js.map +1 -0
  382. package/dist/__tests__/pipeline/displayMathFlow.test.d.ts +2 -0
  383. package/dist/__tests__/pipeline/displayMathFlow.test.d.ts.map +1 -0
  384. package/dist/__tests__/pipeline/displayMathFlow.test.js +156 -0
  385. package/dist/__tests__/pipeline/displayMathFlow.test.js.map +1 -0
  386. package/dist/__tests__/pipeline/emptyResourceTemplate.test.d.ts +2 -0
  387. package/dist/__tests__/pipeline/emptyResourceTemplate.test.d.ts.map +1 -0
  388. package/dist/__tests__/pipeline/emptyResourceTemplate.test.js +66 -0
  389. package/dist/__tests__/pipeline/emptyResourceTemplate.test.js.map +1 -0
  390. package/dist/__tests__/pipeline/figureBodyAlign.test.d.ts +2 -0
  391. package/dist/__tests__/pipeline/figureBodyAlign.test.d.ts.map +1 -0
  392. package/dist/__tests__/pipeline/figureBodyAlign.test.js +92 -0
  393. package/dist/__tests__/pipeline/figureBodyAlign.test.js.map +1 -0
  394. package/dist/__tests__/pipeline/floatAfterCitingLine.test.d.ts +2 -0
  395. package/dist/__tests__/pipeline/floatAfterCitingLine.test.d.ts.map +1 -0
  396. package/dist/__tests__/pipeline/floatAfterCitingLine.test.js +217 -0
  397. package/dist/__tests__/pipeline/floatAfterCitingLine.test.js.map +1 -0
  398. package/dist/__tests__/pipeline/floatFirstSlot.test.js +15 -0
  399. package/dist/__tests__/pipeline/floatFirstSlot.test.js.map +1 -1
  400. package/dist/__tests__/pipeline/floatOverInlineResource.test.d.ts +2 -0
  401. package/dist/__tests__/pipeline/floatOverInlineResource.test.d.ts.map +1 -0
  402. package/dist/__tests__/pipeline/floatOverInlineResource.test.js +75 -0
  403. package/dist/__tests__/pipeline/floatOverInlineResource.test.js.map +1 -0
  404. package/dist/__tests__/pipeline/floatPlacementRules.test.d.ts +2 -0
  405. package/dist/__tests__/pipeline/floatPlacementRules.test.d.ts.map +1 -0
  406. package/dist/__tests__/pipeline/floatPlacementRules.test.js +177 -0
  407. package/dist/__tests__/pipeline/floatPlacementRules.test.js.map +1 -0
  408. package/dist/__tests__/pipeline/floatSlots.test.js +35 -1
  409. package/dist/__tests__/pipeline/floatSlots.test.js.map +1 -1
  410. package/dist/__tests__/pipeline/hardHyphenBreaks.test.d.ts +2 -0
  411. package/dist/__tests__/pipeline/hardHyphenBreaks.test.d.ts.map +1 -0
  412. package/dist/__tests__/pipeline/hardHyphenBreaks.test.js +165 -0
  413. package/dist/__tests__/pipeline/hardHyphenBreaks.test.js.map +1 -0
  414. package/dist/__tests__/pipeline/headingDesignCuts.test.d.ts +2 -0
  415. package/dist/__tests__/pipeline/headingDesignCuts.test.d.ts.map +1 -0
  416. package/dist/__tests__/pipeline/headingDesignCuts.test.js +96 -0
  417. package/dist/__tests__/pipeline/headingDesignCuts.test.js.map +1 -0
  418. package/dist/__tests__/pipeline/headingSnapPerLevel.test.d.ts +2 -0
  419. package/dist/__tests__/pipeline/headingSnapPerLevel.test.d.ts.map +1 -0
  420. package/dist/__tests__/pipeline/headingSnapPerLevel.test.js +97 -0
  421. package/dist/__tests__/pipeline/headingSnapPerLevel.test.js.map +1 -0
  422. package/dist/__tests__/pipeline/headingSplitOrphans.test.d.ts +2 -0
  423. package/dist/__tests__/pipeline/headingSplitOrphans.test.d.ts.map +1 -0
  424. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +134 -0
  425. package/dist/__tests__/pipeline/headingSplitOrphans.test.js.map +1 -0
  426. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.d.ts +2 -0
  427. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.d.ts.map +1 -0
  428. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js +232 -0
  429. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js.map +1 -0
  430. package/dist/__tests__/pipeline/indentAfterOutOfFlow.test.d.ts +2 -0
  431. package/dist/__tests__/pipeline/indentAfterOutOfFlow.test.d.ts.map +1 -0
  432. package/dist/__tests__/pipeline/indentAfterOutOfFlow.test.js +93 -0
  433. package/dist/__tests__/pipeline/indentAfterOutOfFlow.test.js.map +1 -0
  434. package/dist/__tests__/pipeline/inlineFigureSpace.test.d.ts +2 -0
  435. package/dist/__tests__/pipeline/inlineFigureSpace.test.d.ts.map +1 -0
  436. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +195 -0
  437. package/dist/__tests__/pipeline/inlineFigureSpace.test.js.map +1 -0
  438. package/dist/__tests__/pipeline/lineSourceEdges.test.d.ts +2 -0
  439. package/dist/__tests__/pipeline/lineSourceEdges.test.d.ts.map +1 -0
  440. package/dist/__tests__/pipeline/lineSourceEdges.test.js +143 -0
  441. package/dist/__tests__/pipeline/lineSourceEdges.test.js.map +1 -0
  442. package/dist/__tests__/pipeline/lineSourceRanges.test.d.ts +2 -0
  443. package/dist/__tests__/pipeline/lineSourceRanges.test.d.ts.map +1 -0
  444. package/dist/__tests__/pipeline/lineSourceRanges.test.js +96 -0
  445. package/dist/__tests__/pipeline/lineSourceRanges.test.js.map +1 -0
  446. package/dist/__tests__/pipeline/listTopSnap.test.d.ts +2 -0
  447. package/dist/__tests__/pipeline/listTopSnap.test.d.ts.map +1 -0
  448. package/dist/__tests__/pipeline/listTopSnap.test.js +90 -0
  449. package/dist/__tests__/pipeline/listTopSnap.test.js.map +1 -0
  450. package/dist/__tests__/pipeline/looseParagraphLimit.test.d.ts +2 -0
  451. package/dist/__tests__/pipeline/looseParagraphLimit.test.d.ts.map +1 -0
  452. package/dist/__tests__/pipeline/looseParagraphLimit.test.js +63 -0
  453. package/dist/__tests__/pipeline/looseParagraphLimit.test.js.map +1 -0
  454. package/dist/__tests__/pipeline/nestedListSpacing.test.d.ts +2 -0
  455. package/dist/__tests__/pipeline/nestedListSpacing.test.d.ts.map +1 -0
  456. package/dist/__tests__/pipeline/nestedListSpacing.test.js +62 -0
  457. package/dist/__tests__/pipeline/nestedListSpacing.test.js.map +1 -0
  458. package/dist/__tests__/pipeline/openerColumnStarts.test.d.ts +2 -0
  459. package/dist/__tests__/pipeline/openerColumnStarts.test.d.ts.map +1 -0
  460. package/dist/__tests__/pipeline/openerColumnStarts.test.js +189 -0
  461. package/dist/__tests__/pipeline/openerColumnStarts.test.js.map +1 -0
  462. package/dist/__tests__/pipeline/openerFillsPage.test.d.ts +2 -0
  463. package/dist/__tests__/pipeline/openerFillsPage.test.d.ts.map +1 -0
  464. package/dist/__tests__/pipeline/openerFillsPage.test.js +133 -0
  465. package/dist/__tests__/pipeline/openerFillsPage.test.js.map +1 -0
  466. package/dist/__tests__/pipeline/openerTallerThanColumn.test.d.ts +2 -0
  467. package/dist/__tests__/pipeline/openerTallerThanColumn.test.d.ts.map +1 -0
  468. package/dist/__tests__/pipeline/openerTallerThanColumn.test.js +139 -0
  469. package/dist/__tests__/pipeline/openerTallerThanColumn.test.js.map +1 -0
  470. package/dist/__tests__/pipeline/orderedNumberWidth.test.d.ts +2 -0
  471. package/dist/__tests__/pipeline/orderedNumberWidth.test.d.ts.map +1 -0
  472. package/dist/__tests__/pipeline/orderedNumberWidth.test.js +69 -0
  473. package/dist/__tests__/pipeline/orderedNumberWidth.test.js.map +1 -0
  474. package/dist/__tests__/pipeline/pageSpanHeadingWidth.test.d.ts +2 -0
  475. package/dist/__tests__/pipeline/pageSpanHeadingWidth.test.d.ts.map +1 -0
  476. package/dist/__tests__/pipeline/pageSpanHeadingWidth.test.js +74 -0
  477. package/dist/__tests__/pipeline/pageSpanHeadingWidth.test.js.map +1 -0
  478. package/dist/__tests__/pipeline/paragraphContainerTail.test.d.ts +2 -0
  479. package/dist/__tests__/pipeline/paragraphContainerTail.test.d.ts.map +1 -0
  480. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +218 -0
  481. package/dist/__tests__/pipeline/paragraphContainerTail.test.js.map +1 -0
  482. package/dist/__tests__/pipeline/paragraphStyleFaces.test.d.ts +2 -0
  483. package/dist/__tests__/pipeline/paragraphStyleFaces.test.d.ts.map +1 -0
  484. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +121 -0
  485. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -0
  486. package/dist/__tests__/pipeline/paragraphStyles.test.js +85 -0
  487. package/dist/__tests__/pipeline/paragraphStyles.test.js.map +1 -1
  488. package/dist/__tests__/pipeline/raggedOptimal.test.d.ts +2 -0
  489. package/dist/__tests__/pipeline/raggedOptimal.test.d.ts.map +1 -0
  490. package/dist/__tests__/pipeline/raggedOptimal.test.js +231 -0
  491. package/dist/__tests__/pipeline/raggedOptimal.test.js.map +1 -0
  492. package/dist/__tests__/pipeline/resolvedPaletteCopy.test.d.ts +2 -0
  493. package/dist/__tests__/pipeline/resolvedPaletteCopy.test.d.ts.map +1 -0
  494. package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js +68 -0
  495. package/dist/__tests__/pipeline/resolvedPaletteCopy.test.js.map +1 -0
  496. package/dist/__tests__/pipeline/resourceNumberingRules.test.d.ts +2 -0
  497. package/dist/__tests__/pipeline/resourceNumberingRules.test.d.ts.map +1 -0
  498. package/dist/__tests__/pipeline/resourceNumberingRules.test.js +103 -0
  499. package/dist/__tests__/pipeline/resourceNumberingRules.test.js.map +1 -0
  500. package/dist/__tests__/pipeline/sectionPageOwnership.test.d.ts +2 -0
  501. package/dist/__tests__/pipeline/sectionPageOwnership.test.d.ts.map +1 -0
  502. package/dist/__tests__/pipeline/sectionPageOwnership.test.js +89 -0
  503. package/dist/__tests__/pipeline/sectionPageOwnership.test.js.map +1 -0
  504. package/dist/__tests__/pipeline/sectionPaletteSharedBase.test.d.ts +2 -0
  505. package/dist/__tests__/pipeline/sectionPaletteSharedBase.test.d.ts.map +1 -0
  506. package/dist/__tests__/pipeline/sectionPaletteSharedBase.test.js +278 -0
  507. package/dist/__tests__/pipeline/sectionPaletteSharedBase.test.js.map +1 -0
  508. package/dist/__tests__/pipeline/sectionPaletteTables.test.d.ts +2 -0
  509. package/dist/__tests__/pipeline/sectionPaletteTables.test.d.ts.map +1 -0
  510. package/dist/__tests__/pipeline/sectionPaletteTables.test.js +104 -0
  511. package/dist/__tests__/pipeline/sectionPaletteTables.test.js.map +1 -0
  512. package/dist/__tests__/pipeline/sideBoxAtColumnEnd.test.d.ts +2 -0
  513. package/dist/__tests__/pipeline/sideBoxAtColumnEnd.test.d.ts.map +1 -0
  514. package/dist/__tests__/pipeline/sideBoxAtColumnEnd.test.js +176 -0
  515. package/dist/__tests__/pipeline/sideBoxAtColumnEnd.test.js.map +1 -0
  516. package/dist/__tests__/pipeline/sideColumnPercent.test.d.ts +2 -0
  517. package/dist/__tests__/pipeline/sideColumnPercent.test.d.ts.map +1 -0
  518. package/dist/__tests__/pipeline/sideColumnPercent.test.js +114 -0
  519. package/dist/__tests__/pipeline/sideColumnPercent.test.js.map +1 -0
  520. package/dist/__tests__/pipeline/snippetDollarEscape.test.d.ts +2 -0
  521. package/dist/__tests__/pipeline/snippetDollarEscape.test.d.ts.map +1 -0
  522. package/dist/__tests__/pipeline/snippetDollarEscape.test.js +112 -0
  523. package/dist/__tests__/pipeline/snippetDollarEscape.test.js.map +1 -0
  524. package/dist/__tests__/pipeline/snippetLineBreaks.test.d.ts +2 -0
  525. package/dist/__tests__/pipeline/snippetLineBreaks.test.d.ts.map +1 -0
  526. package/dist/__tests__/pipeline/snippetLineBreaks.test.js +178 -0
  527. package/dist/__tests__/pipeline/snippetLineBreaks.test.js.map +1 -0
  528. package/dist/__tests__/pipeline/spanCutUnderColumnFloat.test.d.ts +2 -0
  529. package/dist/__tests__/pipeline/spanCutUnderColumnFloat.test.d.ts.map +1 -0
  530. package/dist/__tests__/pipeline/spanCutUnderColumnFloat.test.js +90 -0
  531. package/dist/__tests__/pipeline/spanCutUnderColumnFloat.test.js.map +1 -0
  532. package/dist/__tests__/pipeline/splitBoxCappedBand.test.d.ts +2 -0
  533. package/dist/__tests__/pipeline/splitBoxCappedBand.test.d.ts.map +1 -0
  534. package/dist/__tests__/pipeline/splitBoxCappedBand.test.js +64 -0
  535. package/dist/__tests__/pipeline/splitBoxCappedBand.test.js.map +1 -0
  536. package/dist/__tests__/pipeline/tableCellNoBreakSpaces.test.d.ts +2 -0
  537. package/dist/__tests__/pipeline/tableCellNoBreakSpaces.test.d.ts.map +1 -0
  538. package/dist/__tests__/pipeline/tableCellNoBreakSpaces.test.js +97 -0
  539. package/dist/__tests__/pipeline/tableCellNoBreakSpaces.test.js.map +1 -0
  540. package/dist/__tests__/pipeline/tableHeaderTracking.test.d.ts +2 -0
  541. package/dist/__tests__/pipeline/tableHeaderTracking.test.d.ts.map +1 -0
  542. package/dist/__tests__/pipeline/tableHeaderTracking.test.js +139 -0
  543. package/dist/__tests__/pipeline/tableHeaderTracking.test.js.map +1 -0
  544. package/dist/__tests__/pipeline/tableSplitTextRoom.test.d.ts +2 -0
  545. package/dist/__tests__/pipeline/tableSplitTextRoom.test.d.ts.map +1 -0
  546. package/dist/__tests__/pipeline/tableSplitTextRoom.test.js +90 -0
  547. package/dist/__tests__/pipeline/tableSplitTextRoom.test.js.map +1 -0
  548. package/dist/__tests__/pipeline/tableStyles.test.js +94 -1
  549. package/dist/__tests__/pipeline/tableStyles.test.js.map +1 -1
  550. package/dist/__tests__/pipeline/tallParagraphShortColumn.test.d.ts +2 -0
  551. package/dist/__tests__/pipeline/tallParagraphShortColumn.test.d.ts.map +1 -0
  552. package/dist/__tests__/pipeline/tallParagraphShortColumn.test.js +79 -0
  553. package/dist/__tests__/pipeline/tallParagraphShortColumn.test.js.map +1 -0
  554. package/dist/__tests__/pipeline/textFaces.test.d.ts +2 -0
  555. package/dist/__tests__/pipeline/textFaces.test.d.ts.map +1 -0
  556. package/dist/__tests__/pipeline/textFaces.test.js +258 -0
  557. package/dist/__tests__/pipeline/textFaces.test.js.map +1 -0
  558. package/dist/__tests__/pipeline/trailingBand.test.js +29 -0
  559. package/dist/__tests__/pipeline/trailingBand.test.js.map +1 -1
  560. package/dist/__tests__/pipeline/trailingCapHeading.test.d.ts +2 -0
  561. package/dist/__tests__/pipeline/trailingCapHeading.test.d.ts.map +1 -0
  562. package/dist/__tests__/pipeline/trailingCapHeading.test.js +69 -0
  563. package/dist/__tests__/pipeline/trailingCapHeading.test.js.map +1 -0
  564. package/dist/__tests__/pipeline/trailingCapOverflow.test.d.ts +2 -0
  565. package/dist/__tests__/pipeline/trailingCapOverflow.test.d.ts.map +1 -0
  566. package/dist/__tests__/pipeline/trailingCapOverflow.test.js +116 -0
  567. package/dist/__tests__/pipeline/trailingCapOverflow.test.js.map +1 -0
  568. package/dist/__tests__/placeholders.test.js +100 -2
  569. package/dist/__tests__/placeholders.test.js.map +1 -1
  570. package/dist/__tests__/punctuationGlue.test.d.ts +2 -0
  571. package/dist/__tests__/punctuationGlue.test.d.ts.map +1 -0
  572. package/dist/__tests__/punctuationGlue.test.js +165 -0
  573. package/dist/__tests__/punctuationGlue.test.js.map +1 -0
  574. package/dist/__tests__/raggedHyphenation.test.d.ts +2 -0
  575. package/dist/__tests__/raggedHyphenation.test.d.ts.map +1 -0
  576. package/dist/__tests__/raggedHyphenation.test.js +184 -0
  577. package/dist/__tests__/raggedHyphenation.test.js.map +1 -0
  578. package/dist/__tests__/renderWarnings.test.d.ts +2 -0
  579. package/dist/__tests__/renderWarnings.test.d.ts.map +1 -0
  580. package/dist/__tests__/renderWarnings.test.js +72 -0
  581. package/dist/__tests__/renderWarnings.test.js.map +1 -0
  582. package/dist/__tests__/resolvedLocale.test.d.ts +2 -0
  583. package/dist/__tests__/resolvedLocale.test.d.ts.map +1 -0
  584. package/dist/__tests__/resolvedLocale.test.js +51 -0
  585. package/dist/__tests__/resolvedLocale.test.js.map +1 -0
  586. package/dist/__tests__/runningChapter.test.d.ts +2 -0
  587. package/dist/__tests__/runningChapter.test.d.ts.map +1 -0
  588. package/dist/__tests__/runningChapter.test.js +161 -0
  589. package/dist/__tests__/runningChapter.test.js.map +1 -0
  590. package/dist/__tests__/runtFixStretchLimit.test.d.ts +2 -0
  591. package/dist/__tests__/runtFixStretchLimit.test.d.ts.map +1 -0
  592. package/dist/__tests__/runtFixStretchLimit.test.js +145 -0
  593. package/dist/__tests__/runtFixStretchLimit.test.js.map +1 -0
  594. package/dist/__tests__/scriptStacking.test.d.ts +2 -0
  595. package/dist/__tests__/scriptStacking.test.d.ts.map +1 -0
  596. package/dist/__tests__/scriptStacking.test.js +331 -0
  597. package/dist/__tests__/scriptStacking.test.js.map +1 -0
  598. package/dist/__tests__/sectionPaletteFlow.test.d.ts +2 -0
  599. package/dist/__tests__/sectionPaletteFlow.test.d.ts.map +1 -0
  600. package/dist/__tests__/sectionPaletteFlow.test.js +51 -0
  601. package/dist/__tests__/sectionPaletteFlow.test.js.map +1 -0
  602. package/dist/__tests__/singleInk.test.js +64 -2
  603. package/dist/__tests__/singleInk.test.js.map +1 -1
  604. package/dist/__tests__/singleInkRender.test.d.ts +2 -0
  605. package/dist/__tests__/singleInkRender.test.d.ts.map +1 -0
  606. package/dist/__tests__/singleInkRender.test.js +396 -0
  607. package/dist/__tests__/singleInkRender.test.js.map +1 -0
  608. package/dist/__tests__/spaceDirective.test.js +75 -0
  609. package/dist/__tests__/spaceDirective.test.js.map +1 -1
  610. package/dist/__tests__/table/model.test.js +124 -1
  611. package/dist/__tests__/table/model.test.js.map +1 -1
  612. package/dist/__tests__/tableFillSeams.test.d.ts +2 -0
  613. package/dist/__tests__/tableFillSeams.test.d.ts.map +1 -0
  614. package/dist/__tests__/tableFillSeams.test.js +180 -0
  615. package/dist/__tests__/tableFillSeams.test.js.map +1 -0
  616. package/dist/__tests__/toc.test.js +23 -2
  617. package/dist/__tests__/toc.test.js.map +1 -1
  618. package/dist/__tests__/tocLeaderKerning.test.d.ts +2 -0
  619. package/dist/__tests__/tocLeaderKerning.test.d.ts.map +1 -0
  620. package/dist/__tests__/tocLeaderKerning.test.js +83 -0
  621. package/dist/__tests__/tocLeaderKerning.test.js.map +1 -0
  622. package/dist/__tests__/tocNumberBaseline.test.d.ts +2 -0
  623. package/dist/__tests__/tocNumberBaseline.test.d.ts.map +1 -0
  624. package/dist/__tests__/tocNumberBaseline.test.js +90 -0
  625. package/dist/__tests__/tocNumberBaseline.test.js.map +1 -0
  626. package/dist/__tests__/tocPartsHeight.test.d.ts +2 -0
  627. package/dist/__tests__/tocPartsHeight.test.d.ts.map +1 -0
  628. package/dist/__tests__/tocPartsHeight.test.js +41 -0
  629. package/dist/__tests__/tocPartsHeight.test.js.map +1 -0
  630. package/dist/__tests__/trackedLineAlign.test.d.ts +2 -0
  631. package/dist/__tests__/trackedLineAlign.test.d.ts.map +1 -0
  632. package/dist/__tests__/trackedLineAlign.test.js +96 -0
  633. package/dist/__tests__/trackedLineAlign.test.js.map +1 -0
  634. package/dist/bundle/adapters.d.ts +10 -3
  635. package/dist/bundle/adapters.d.ts.map +1 -1
  636. package/dist/bundle/adapters.js +13 -5
  637. package/dist/bundle/adapters.js.map +1 -1
  638. package/dist/bundle/api.d.ts +32 -0
  639. package/dist/bundle/api.d.ts.map +1 -1
  640. package/dist/bundle/api.js +12 -2
  641. package/dist/bundle/api.js.map +1 -1
  642. package/dist/bundle/book.d.ts +6 -1
  643. package/dist/bundle/book.d.ts.map +1 -1
  644. package/dist/bundle/book.js +89 -17
  645. package/dist/bundle/book.js.map +1 -1
  646. package/dist/bundle/codec.d.ts +87 -3
  647. package/dist/bundle/codec.d.ts.map +1 -1
  648. package/dist/bundle/codec.js +193 -13
  649. package/dist/bundle/codec.js.map +1 -1
  650. package/dist/bundle/configVersion.d.ts +295 -0
  651. package/dist/bundle/configVersion.d.ts.map +1 -0
  652. package/dist/bundle/configVersion.js +788 -0
  653. package/dist/bundle/configVersion.js.map +1 -0
  654. package/dist/bundle/index.d.ts +6 -4
  655. package/dist/bundle/index.d.ts.map +1 -1
  656. package/dist/bundle/index.js +2 -1
  657. package/dist/bundle/index.js.map +1 -1
  658. package/dist/bundle/manifest.d.ts +19 -5
  659. package/dist/bundle/manifest.d.ts.map +1 -1
  660. package/dist/bundle/manifest.js +53 -21
  661. package/dist/bundle/manifest.js.map +1 -1
  662. package/dist/bundle/types.d.ts +19 -0
  663. package/dist/bundle/types.d.ts.map +1 -1
  664. package/dist/bundle/zip.d.ts +14 -1
  665. package/dist/bundle/zip.d.ts.map +1 -1
  666. package/dist/bundle/zip.js +13 -2
  667. package/dist/bundle/zip.js.map +1 -1
  668. package/dist/canvas-backend/blockRender.d.ts +3 -1
  669. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  670. package/dist/canvas-backend/blockRender.js +27 -17
  671. package/dist/canvas-backend/blockRender.js.map +1 -1
  672. package/dist/canvas-backend/decorations.d.ts.map +1 -1
  673. package/dist/canvas-backend/decorations.js +6 -36
  674. package/dist/canvas-backend/decorations.js.map +1 -1
  675. package/dist/canvas-backend/headerFooter.d.ts +3 -1
  676. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  677. package/dist/canvas-backend/headerFooter.js +85 -22
  678. package/dist/canvas-backend/headerFooter.js.map +1 -1
  679. package/dist/canvas-backend/index.d.ts +17 -3
  680. package/dist/canvas-backend/index.d.ts.map +1 -1
  681. package/dist/canvas-backend/index.js +75 -14
  682. package/dist/canvas-backend/index.js.map +1 -1
  683. package/dist/canvas-backend/renderResourceBlock.d.ts +45 -5
  684. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  685. package/dist/canvas-backend/renderResourceBlock.js +133 -22
  686. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  687. package/dist/columnClip.d.ts +13 -2
  688. package/dist/columnClip.d.ts.map +1 -1
  689. package/dist/columnClip.js +37 -4
  690. package/dist/columnClip.js.map +1 -1
  691. package/dist/columnRule.d.ts +7 -2
  692. package/dist/columnRule.d.ts.map +1 -1
  693. package/dist/columnRule.js +27 -2
  694. package/dist/columnRule.js.map +1 -1
  695. package/dist/configWarnings.d.ts +26 -0
  696. package/dist/configWarnings.d.ts.map +1 -0
  697. package/dist/configWarnings.js +223 -0
  698. package/dist/configWarnings.js.map +1 -0
  699. package/dist/createLayout.d.ts +16 -1
  700. package/dist/createLayout.d.ts.map +1 -1
  701. package/dist/createLayout.js +25 -22
  702. package/dist/createLayout.js.map +1 -1
  703. package/dist/cropMarks.d.ts +28 -0
  704. package/dist/cropMarks.d.ts.map +1 -0
  705. package/dist/cropMarks.js +40 -0
  706. package/dist/cropMarks.js.map +1 -0
  707. package/dist/defaults/bodyText.d.ts +12 -2
  708. package/dist/defaults/bodyText.d.ts.map +1 -1
  709. package/dist/defaults/bodyText.js +132 -7
  710. package/dist/defaults/bodyText.js.map +1 -1
  711. package/dist/defaults/calloutStyles.d.ts +13 -2
  712. package/dist/defaults/calloutStyles.d.ts.map +1 -1
  713. package/dist/defaults/calloutStyles.js +64 -4
  714. package/dist/defaults/calloutStyles.js.map +1 -1
  715. package/dist/defaults/chipStyles.d.ts +4 -3
  716. package/dist/defaults/chipStyles.d.ts.map +1 -1
  717. package/dist/defaults/chipStyles.js +11 -3
  718. package/dist/defaults/chipStyles.js.map +1 -1
  719. package/dist/defaults/headerFooter.d.ts +11 -1
  720. package/dist/defaults/headerFooter.d.ts.map +1 -1
  721. package/dist/defaults/headerFooter.js +52 -4
  722. package/dist/defaults/headerFooter.js.map +1 -1
  723. package/dist/defaults/headingStyles.d.ts +7 -3
  724. package/dist/defaults/headingStyles.d.ts.map +1 -1
  725. package/dist/defaults/headingStyles.js +34 -6
  726. package/dist/defaults/headingStyles.js.map +1 -1
  727. package/dist/defaults/headings.d.ts +10 -2
  728. package/dist/defaults/headings.d.ts.map +1 -1
  729. package/dist/defaults/headings.js +79 -16
  730. package/dist/defaults/headings.js.map +1 -1
  731. package/dist/defaults/index.d.ts +1 -1
  732. package/dist/defaults/index.d.ts.map +1 -1
  733. package/dist/defaults/index.js +1 -1
  734. package/dist/defaults/index.js.map +1 -1
  735. package/dist/defaults/layout.d.ts.map +1 -1
  736. package/dist/defaults/layout.js +27 -0
  737. package/dist/defaults/layout.js.map +1 -1
  738. package/dist/defaults/lists-shared.d.ts +2 -0
  739. package/dist/defaults/lists-shared.d.ts.map +1 -1
  740. package/dist/defaults/lists-shared.js +2 -0
  741. package/dist/defaults/lists-shared.js.map +1 -1
  742. package/dist/defaults/math.d.ts.map +1 -1
  743. package/dist/defaults/math.js +12 -0
  744. package/dist/defaults/math.js.map +1 -1
  745. package/dist/defaults/orderedLists.d.ts +7 -1
  746. package/dist/defaults/orderedLists.d.ts.map +1 -1
  747. package/dist/defaults/orderedLists.js +30 -4
  748. package/dist/defaults/orderedLists.js.map +1 -1
  749. package/dist/defaults/page.d.ts.map +1 -1
  750. package/dist/defaults/page.js +7 -2
  751. package/dist/defaults/page.js.map +1 -1
  752. package/dist/defaults/paragraphStyles.d.ts +2 -1
  753. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  754. package/dist/defaults/paragraphStyles.js +26 -1
  755. package/dist/defaults/paragraphStyles.js.map +1 -1
  756. package/dist/defaults/resourceTypes.d.ts +5 -1
  757. package/dist/defaults/resourceTypes.d.ts.map +1 -1
  758. package/dist/defaults/resourceTypes.js +37 -6
  759. package/dist/defaults/resourceTypes.js.map +1 -1
  760. package/dist/defaults/shared.d.ts +10 -0
  761. package/dist/defaults/shared.d.ts.map +1 -1
  762. package/dist/defaults/shared.js +79 -0
  763. package/dist/defaults/shared.js.map +1 -1
  764. package/dist/defaults/tableStyle.d.ts +1 -1
  765. package/dist/defaults/tableStyle.d.ts.map +1 -1
  766. package/dist/defaults/tableStyle.js +40 -6
  767. package/dist/defaults/tableStyle.js.map +1 -1
  768. package/dist/defaults/toc.d.ts +2 -1
  769. package/dist/defaults/toc.d.ts.map +1 -1
  770. package/dist/defaults/toc.js +2 -1
  771. package/dist/defaults/toc.js.map +1 -1
  772. package/dist/defaults/unorderedLists.d.ts +1 -0
  773. package/dist/defaults/unorderedLists.d.ts.map +1 -1
  774. package/dist/defaults/unorderedLists.js +7 -1
  775. package/dist/defaults/unorderedLists.js.map +1 -1
  776. package/dist/design/layout.d.ts +41 -3
  777. package/dist/design/layout.d.ts.map +1 -1
  778. package/dist/design/layout.js +322 -90
  779. package/dist/design/layout.js.map +1 -1
  780. package/dist/design/placeholders.d.ts +39 -4
  781. package/dist/design/placeholders.d.ts.map +1 -1
  782. package/dist/design/placeholders.js +61 -11
  783. package/dist/design/placeholders.js.map +1 -1
  784. package/dist/design/richText.d.ts +110 -0
  785. package/dist/design/richText.d.ts.map +1 -0
  786. package/dist/design/richText.js +458 -0
  787. package/dist/design/richText.js.map +1 -0
  788. package/dist/frontmatter.d.ts +14 -0
  789. package/dist/frontmatter.d.ts.map +1 -1
  790. package/dist/frontmatter.js +61 -0
  791. package/dist/frontmatter.js.map +1 -1
  792. package/dist/html-backend.d.ts +60 -2
  793. package/dist/html-backend.d.ts.map +1 -1
  794. package/dist/html-backend.js +317 -62
  795. package/dist/html-backend.js.map +1 -1
  796. package/dist/hyphenate.d.ts +30 -4
  797. package/dist/hyphenate.d.ts.map +1 -1
  798. package/dist/hyphenate.js +71 -5
  799. package/dist/hyphenate.js.map +1 -1
  800. package/dist/index.d.ts +28 -19
  801. package/dist/index.d.ts.map +1 -1
  802. package/dist/index.js +17 -11
  803. package/dist/index.js.map +1 -1
  804. package/dist/knuthPlass/breakpoints.d.ts +21 -0
  805. package/dist/knuthPlass/breakpoints.d.ts.map +1 -1
  806. package/dist/knuthPlass/breakpoints.js +205 -14
  807. package/dist/knuthPlass/breakpoints.js.map +1 -1
  808. package/dist/knuthPlass/constants.d.ts +15 -0
  809. package/dist/knuthPlass/constants.d.ts.map +1 -1
  810. package/dist/knuthPlass/constants.js +15 -0
  811. package/dist/knuthPlass/constants.js.map +1 -1
  812. package/dist/knuthPlass/pretextAdapter.d.ts +23 -2
  813. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
  814. package/dist/knuthPlass/pretextAdapter.js +95 -9
  815. package/dist/knuthPlass/pretextAdapter.js.map +1 -1
  816. package/dist/knuthPlass/richAdapter.d.ts +26 -2
  817. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  818. package/dist/knuthPlass/richAdapter.js +79 -18
  819. package/dist/knuthPlass/richAdapter.js.map +1 -1
  820. package/dist/knuthPlass/tracking.d.ts +24 -0
  821. package/dist/knuthPlass/tracking.d.ts.map +1 -0
  822. package/dist/knuthPlass/tracking.js +77 -0
  823. package/dist/knuthPlass/tracking.js.map +1 -0
  824. package/dist/knuthPlass/types.d.ts +66 -4
  825. package/dist/knuthPlass/types.d.ts.map +1 -1
  826. package/dist/lineInk.d.ts +12 -0
  827. package/dist/lineInk.d.ts.map +1 -0
  828. package/dist/lineInk.js +20 -0
  829. package/dist/lineInk.js.map +1 -0
  830. package/dist/locale.d.ts +26 -0
  831. package/dist/locale.d.ts.map +1 -0
  832. package/dist/locale.js +63 -0
  833. package/dist/locale.js.map +1 -0
  834. package/dist/looseLines.d.ts +44 -0
  835. package/dist/looseLines.d.ts.map +1 -0
  836. package/dist/looseLines.js +46 -0
  837. package/dist/looseLines.js.map +1 -0
  838. package/dist/math/THIRD_PARTY_LICENSES.txt +221 -0
  839. package/dist/math/engine.d.ts +24 -0
  840. package/dist/math/engine.d.ts.map +1 -1
  841. package/dist/math/engine.js +252 -83
  842. package/dist/math/engine.js.map +1 -1
  843. package/dist/math/index.d.ts +1 -1
  844. package/dist/math/index.d.ts.map +1 -1
  845. package/dist/math/index.js +1 -1
  846. package/dist/math/index.js.map +1 -1
  847. package/dist/math/mathjax.d.ts +20 -0
  848. package/dist/math/mathjax.d.ts.map +1 -0
  849. package/dist/math/mathjax.js +29 -0
  850. package/dist/measure/breakRules.d.ts +50 -0
  851. package/dist/measure/breakRules.d.ts.map +1 -0
  852. package/dist/measure/breakRules.js +116 -0
  853. package/dist/measure/breakRules.js.map +1 -0
  854. package/dist/measure/cache.d.ts.map +1 -1
  855. package/dist/measure/cache.js +26 -5
  856. package/dist/measure/cache.js.map +1 -1
  857. package/dist/measure/cjk.d.ts +19 -0
  858. package/dist/measure/cjk.d.ts.map +1 -0
  859. package/dist/measure/cjk.js +70 -0
  860. package/dist/measure/cjk.js.map +1 -0
  861. package/dist/measure/font.d.ts +21 -5
  862. package/dist/measure/font.d.ts.map +1 -1
  863. package/dist/measure/font.js +61 -4
  864. package/dist/measure/font.js.map +1 -1
  865. package/dist/measure/index.d.ts +16 -2
  866. package/dist/measure/index.d.ts.map +1 -1
  867. package/dist/measure/index.js +61 -1
  868. package/dist/measure/index.js.map +1 -1
  869. package/dist/measure/links.d.ts +15 -0
  870. package/dist/measure/links.d.ts.map +1 -0
  871. package/dist/measure/links.js +115 -0
  872. package/dist/measure/links.js.map +1 -0
  873. package/dist/measure/plain.d.ts +10 -2
  874. package/dist/measure/plain.d.ts.map +1 -1
  875. package/dist/measure/plain.js +175 -25
  876. package/dist/measure/plain.js.map +1 -1
  877. package/dist/measure/rich.d.ts +86 -6
  878. package/dist/measure/rich.d.ts.map +1 -1
  879. package/dist/measure/rich.js +808 -131
  880. package/dist/measure/rich.js.map +1 -1
  881. package/dist/measure/spaces.d.ts +34 -0
  882. package/dist/measure/spaces.d.ts.map +1 -0
  883. package/dist/measure/spaces.js +42 -0
  884. package/dist/measure/spaces.js.map +1 -0
  885. package/dist/measure/types.d.ts +99 -0
  886. package/dist/measure/types.d.ts.map +1 -1
  887. package/dist/measure/types.js +19 -0
  888. package/dist/measure/types.js.map +1 -1
  889. package/dist/numberWords.d.ts +22 -0
  890. package/dist/numberWords.d.ts.map +1 -0
  891. package/dist/numberWords.js +155 -0
  892. package/dist/numberWords.js.map +1 -0
  893. package/dist/numbering.d.ts +52 -3
  894. package/dist/numbering.d.ts.map +1 -1
  895. package/dist/numbering.js +116 -16
  896. package/dist/numbering.js.map +1 -1
  897. package/dist/parse/blockParser.d.ts.map +1 -1
  898. package/dist/parse/blockParser.js +54 -7
  899. package/dist/parse/blockParser.js.map +1 -1
  900. package/dist/parse/index.d.ts +1 -1
  901. package/dist/parse/index.d.ts.map +1 -1
  902. package/dist/parse/index.js.map +1 -1
  903. package/dist/parse/injectSpans.d.ts +3 -1
  904. package/dist/parse/injectSpans.d.ts.map +1 -1
  905. package/dist/parse/injectSpans.js +8 -4
  906. package/dist/parse/injectSpans.js.map +1 -1
  907. package/dist/parse/inlineFormatting.d.ts +50 -2
  908. package/dist/parse/inlineFormatting.d.ts.map +1 -1
  909. package/dist/parse/inlineFormatting.js +441 -26
  910. package/dist/parse/inlineFormatting.js.map +1 -1
  911. package/dist/parse/inlineSnippet.d.ts +12 -3
  912. package/dist/parse/inlineSnippet.d.ts.map +1 -1
  913. package/dist/parse/inlineSnippet.js +10 -3
  914. package/dist/parse/inlineSnippet.js.map +1 -1
  915. package/dist/parse/links.d.ts +9 -0
  916. package/dist/parse/links.d.ts.map +1 -0
  917. package/dist/parse/links.js +23 -0
  918. package/dist/parse/links.js.map +1 -0
  919. package/dist/parse/sourceMapping.d.ts.map +1 -1
  920. package/dist/parse/sourceMapping.js +68 -7
  921. package/dist/parse/sourceMapping.js.map +1 -1
  922. package/dist/parse/types.d.ts +32 -0
  923. package/dist/parse/types.d.ts.map +1 -1
  924. package/dist/pipeline/bandCaps.d.ts +26 -5
  925. package/dist/pipeline/bandCaps.d.ts.map +1 -1
  926. package/dist/pipeline/bandCaps.js +47 -20
  927. package/dist/pipeline/bandCaps.js.map +1 -1
  928. package/dist/pipeline/build.d.ts.map +1 -1
  929. package/dist/pipeline/build.js +1417 -172
  930. package/dist/pipeline/build.js.map +1 -1
  931. package/dist/pipeline/buildBlockKind.d.ts +4 -0
  932. package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
  933. package/dist/pipeline/buildBlockKind.js +38 -7
  934. package/dist/pipeline/buildBlockKind.js.map +1 -1
  935. package/dist/pipeline/buildHelpers.d.ts +5 -2
  936. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  937. package/dist/pipeline/buildHelpers.js +101 -25
  938. package/dist/pipeline/buildHelpers.js.map +1 -1
  939. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  940. package/dist/pipeline/buildMeasurement.js +6 -2
  941. package/dist/pipeline/buildMeasurement.js.map +1 -1
  942. package/dist/pipeline/calloutLayout.d.ts +34 -7
  943. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  944. package/dist/pipeline/calloutLayout.js +331 -44
  945. package/dist/pipeline/calloutLayout.js.map +1 -1
  946. package/dist/pipeline/chips.d.ts.map +1 -1
  947. package/dist/pipeline/chips.js +3 -0
  948. package/dist/pipeline/chips.js.map +1 -1
  949. package/dist/pipeline/columnBalancing.d.ts +26 -6
  950. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  951. package/dist/pipeline/columnBalancing.js +82 -21
  952. package/dist/pipeline/columnBalancing.js.map +1 -1
  953. package/dist/pipeline/config.d.ts +18 -0
  954. package/dist/pipeline/config.d.ts.map +1 -1
  955. package/dist/pipeline/config.js +44 -7
  956. package/dist/pipeline/config.js.map +1 -1
  957. package/dist/pipeline/contentWarnings.d.ts +50 -0
  958. package/dist/pipeline/contentWarnings.d.ts.map +1 -0
  959. package/dist/pipeline/contentWarnings.js +362 -0
  960. package/dist/pipeline/contentWarnings.js.map +1 -0
  961. package/dist/pipeline/continuation.d.ts.map +1 -1
  962. package/dist/pipeline/continuation.js +25 -5
  963. package/dist/pipeline/continuation.js.map +1 -1
  964. package/dist/pipeline/floatPlacement.d.ts +16 -7
  965. package/dist/pipeline/floatPlacement.d.ts.map +1 -1
  966. package/dist/pipeline/floatPlacement.js +22 -5
  967. package/dist/pipeline/floatPlacement.js.map +1 -1
  968. package/dist/pipeline/floatSlots.d.ts +30 -4
  969. package/dist/pipeline/floatSlots.d.ts.map +1 -1
  970. package/dist/pipeline/floatSlots.js +42 -7
  971. package/dist/pipeline/floatSlots.js.map +1 -1
  972. package/dist/pipeline/headerFooter.d.ts +125 -11
  973. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  974. package/dist/pipeline/headerFooter.js +444 -69
  975. package/dist/pipeline/headerFooter.js.map +1 -1
  976. package/dist/pipeline/headingDesignCuts.d.ts +40 -0
  977. package/dist/pipeline/headingDesignCuts.d.ts.map +1 -0
  978. package/dist/pipeline/headingDesignCuts.js +72 -0
  979. package/dist/pipeline/headingDesignCuts.js.map +1 -0
  980. package/dist/pipeline/headingStyles.d.ts +12 -0
  981. package/dist/pipeline/headingStyles.d.ts.map +1 -1
  982. package/dist/pipeline/headingStyles.js +45 -1
  983. package/dist/pipeline/headingStyles.js.map +1 -1
  984. package/dist/pipeline/lists.d.ts +33 -1
  985. package/dist/pipeline/lists.d.ts.map +1 -1
  986. package/dist/pipeline/lists.js +53 -2
  987. package/dist/pipeline/lists.js.map +1 -1
  988. package/dist/pipeline/measureContentBlock.d.ts +25 -1
  989. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  990. package/dist/pipeline/measureContentBlock.js +137 -17
  991. package/dist/pipeline/measureContentBlock.js.map +1 -1
  992. package/dist/pipeline/outline.d.ts +10 -5
  993. package/dist/pipeline/outline.d.ts.map +1 -1
  994. package/dist/pipeline/outline.js +38 -14
  995. package/dist/pipeline/outline.js.map +1 -1
  996. package/dist/pipeline/paragraphContainers.d.ts +13 -4
  997. package/dist/pipeline/paragraphContainers.d.ts.map +1 -1
  998. package/dist/pipeline/paragraphContainers.js +4 -3
  999. package/dist/pipeline/paragraphContainers.js.map +1 -1
  1000. package/dist/pipeline/partPalette.d.ts +49 -3
  1001. package/dist/pipeline/partPalette.d.ts.map +1 -1
  1002. package/dist/pipeline/partPalette.js +414 -46
  1003. package/dist/pipeline/partPalette.js.map +1 -1
  1004. package/dist/pipeline/parts.d.ts +5 -1
  1005. package/dist/pipeline/parts.d.ts.map +1 -1
  1006. package/dist/pipeline/parts.js +16 -0
  1007. package/dist/pipeline/parts.js.map +1 -1
  1008. package/dist/pipeline/placeholders.d.ts +152 -3
  1009. package/dist/pipeline/placeholders.d.ts.map +1 -1
  1010. package/dist/pipeline/placeholders.js +259 -33
  1011. package/dist/pipeline/placeholders.js.map +1 -1
  1012. package/dist/pipeline/placement.d.ts +6 -1
  1013. package/dist/pipeline/placement.d.ts.map +1 -1
  1014. package/dist/pipeline/placement.js +13 -2
  1015. package/dist/pipeline/placement.js.map +1 -1
  1016. package/dist/pipeline/raggedLines.d.ts +5 -0
  1017. package/dist/pipeline/raggedLines.d.ts.map +1 -1
  1018. package/dist/pipeline/raggedLines.js +24 -3
  1019. package/dist/pipeline/raggedLines.js.map +1 -1
  1020. package/dist/pipeline/resourceLayout.d.ts +6 -4
  1021. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  1022. package/dist/pipeline/resourceLayout.js +137 -36
  1023. package/dist/pipeline/resourceLayout.js.map +1 -1
  1024. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  1025. package/dist/pipeline/resourceNumbering.js +5 -15
  1026. package/dist/pipeline/resourceNumbering.js.map +1 -1
  1027. package/dist/pipeline/styles.d.ts +38 -5
  1028. package/dist/pipeline/styles.d.ts.map +1 -1
  1029. package/dist/pipeline/styles.js +99 -46
  1030. package/dist/pipeline/styles.js.map +1 -1
  1031. package/dist/pipeline/toc.d.ts.map +1 -1
  1032. package/dist/pipeline/toc.js +41 -5
  1033. package/dist/pipeline/toc.js.map +1 -1
  1034. package/dist/react/createLayout.d.ts +20 -0
  1035. package/dist/react/createLayout.d.ts.map +1 -0
  1036. package/dist/react/createLayout.js +50 -0
  1037. package/dist/react/createLayout.js.map +1 -0
  1038. package/dist/react/index.d.ts +3 -0
  1039. package/dist/react/index.d.ts.map +1 -0
  1040. package/dist/react/index.js +4 -0
  1041. package/dist/react/index.js.map +1 -0
  1042. package/dist/svg/singleInk.d.ts +46 -1
  1043. package/dist/svg/singleInk.d.ts.map +1 -1
  1044. package/dist/svg/singleInk.js +180 -10
  1045. package/dist/svg/singleInk.js.map +1 -1
  1046. package/dist/table/model.d.ts +36 -1
  1047. package/dist/table/model.d.ts.map +1 -1
  1048. package/dist/table/model.js +163 -20
  1049. package/dist/table/model.js.map +1 -1
  1050. package/dist/types.d.ts +979 -68
  1051. package/dist/types.d.ts.map +1 -1
  1052. package/dist/vdt.d.ts +439 -13
  1053. package/dist/vdt.d.ts.map +1 -1
  1054. package/dist/vdt.js +91 -0
  1055. package/dist/vdt.js.map +1 -1
  1056. package/dist/worker/client.d.ts +15 -1
  1057. package/dist/worker/client.d.ts.map +1 -1
  1058. package/dist/worker/client.js +52 -3
  1059. package/dist/worker/client.js.map +1 -1
  1060. package/dist/worker/entryUrl.d.ts +15 -0
  1061. package/dist/worker/entryUrl.d.ts.map +1 -0
  1062. package/dist/worker/entryUrl.js +33 -0
  1063. package/dist/worker/entryUrl.js.map +1 -0
  1064. package/dist/worker/fonts.d.ts +18 -0
  1065. package/dist/worker/fonts.d.ts.map +1 -0
  1066. package/dist/worker/fonts.js +53 -0
  1067. package/dist/worker/fonts.js.map +1 -0
  1068. package/dist/worker/layout.worker.js +41 -6
  1069. package/dist/worker/layout.worker.js.map +1 -1
  1070. package/dist/worker/protocol.d.ts +4 -0
  1071. package/dist/worker/protocol.d.ts.map +1 -1
  1072. package/package.json +12 -5
@@ -1,18 +1,19 @@
1
- import { applyTitleBreaks } from '../parse/inlineFormatting';
1
+ import { flattenTitleBreaks } from '../parse/inlineFormatting';
2
2
  import { dimensionToPx } from '../units';
3
3
  import { createVDTDocument, createVDTBlock, createBoundingBox, } from '../vdt';
4
4
  import { anchorBox } from '../design/layout';
5
5
  import { parseMarkdownMemo, spaceDirectiveLines } from '../parse';
6
- import { buildPageLabels, computeHeadingNumbers, } from '../numbering';
7
- import { extractFrontmatter } from '../frontmatter';
6
+ import { buildPageLabels, computeHeadingNumbering, parseNumberFormat, } from '../numbering';
7
+ import { extractFrontmatter, normalizeMetadata } from '../frontmatter';
8
+ import { collectConfigWarnings } from '../configWarnings';
8
9
  import { initHyphenator } from '../measure';
9
- import { resolveAllConfig, computeBaselineGrid } from './config';
10
- import { createHeadingLevelResolver, deriveSectionGeometryConfig, deriveSectionMeasureContext, headingIsNumbered, headingStyleOf, planHeadingSections, } from './headingStyles';
11
- import { computeOutline, hasTocDirective, outlineFromDoc, sameOutline } from './outline';
10
+ import { resolveAllConfig, computeBaselineGrid, resolvedLocale } from './config';
11
+ import { createHeadingLevelResolver, deriveSectionGeometryConfig, deriveSectionMeasureContext, headingIsHidden, headingIsNumbered, headingMarksFor, headingStyleOf, planHeadingSections, } from './headingStyles';
12
+ import { computeOutline, hasTocDirective, headingNumberingOptions, headingTemplatesOf, outlineFromDoc, sameOutline } from './outline';
12
13
  import { expandTocDirectives } from './toc';
13
14
  import { resolveBodyStyle, resolveBlockquoteStyle } from './styles';
14
- import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, } from './lists';
15
- import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, } from './placement';
15
+ import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, listBulletPosition, listItemGapPx, listItemSpacingPx, } from './lists';
16
+ import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, pageLayoutOf, } from './placement';
16
17
  import { chooseParagraphSplit } from './orphanWidow';
17
18
  import { applyStyleAttrs, computePageMetrics, isMarkerBlock, nextNonMarkerBlock, spaceLinesAfter, prevNonMarkerBlock, rollbackTrailingBlocks, } from './buildHelpers';
18
19
  import { measureContentBlock } from './measureContentBlock';
@@ -21,17 +22,26 @@ import { planParts, derivePartMeasureContext } from './parts';
21
22
  import { layoutCallout, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
22
23
  import { layoutResourceBlock, planTableSlice } from './resourceLayout';
23
24
  import { computeFloatPlan, floatedResourceIds, } from './floatPlacement';
24
- import { enumerateCurrentPageSlots, measureFloatBand, measureSideStack, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
25
+ import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
25
26
  import { computeHeadingContext, computeResourceNumbering, } from './resourceNumbering';
26
- import { defaultResourceTypes } from '../defaults/resourceTypes';
27
+ import { defaultResourceTypes, documentLocale } from '../defaults/resourceTypes';
27
28
  import { pickTableStyle } from '../defaults/tableStyle';
28
- import { buildHeadersAndFooters, measureHeadingAdvancedDesignHeight } from './headerFooter';
29
- import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey } from './columnBalancing';
30
- import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
29
+ import { buildHeadersAndFooters, defaultOpenerTitle, headingDesignBoxes, headingTitleText, measureDefaultOpenerHeight, measureHeadingDesign } from './headerFooter';
30
+ import { flowColorValues } from './partPalette';
31
+ import { chapterNumberCounter, leadingBoldText } from './placeholders';
32
+ import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, boxRoomIn, boxLeverKeys, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey } from './columnBalancing';
33
+ import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
31
34
  import { raggedLooseLines } from './raggedLines';
35
+ import { collectContentWarnings, locateContentWarnings } from './contentWarnings';
32
36
  /** Tolerance for "does this block fit" checks against a column's free
33
37
  * height, absorbing floating-point drift between grid multiples. */
34
38
  const FIT_EPS = 0.01;
39
+ /** Room (px) under closing boxes a balancing pass must close to be kept on
40
+ * that ground alone (see `boxRoomIn`). */
41
+ const BOX_ROOM_EPS_PX = 0.5;
42
+ /** Overflow past a capped column's foot (px) that counts as content the
43
+ * cut did not hold — a clipped line, not floating-point drift. */
44
+ const CAPPED_OVERFLOW_PX = 0.5;
35
45
  /** Smallest share of its width an inline figure is set at to stay in the
36
46
  * room left in its column (`layout.fitFiguresToPage`). */
37
47
  const MIN_INLINE_FIGURE_SCALE = 0.5;
@@ -41,14 +51,33 @@ export class BuildCancelledError extends Error {
41
51
  this.name = 'BuildCancelledError';
42
52
  }
43
53
  }
44
- /** Page-number formats accepted by the `::numbering` directive. */
45
- const ALLOWED_PAGE_FORMATS = new Set([
46
- 'decimal',
47
- 'lower-roman',
48
- 'upper-roman',
49
- 'lower-alpha',
50
- 'upper-alpha',
51
- ]);
54
+ /** `{chapterNumber}` at each content block: the value of the last level-1
55
+ * heading at or before it ('' before the first), by the counter the
56
+ * running heads use over the placed blocks (`computeChapterNumbers`) —
57
+ * fed the same fields the placed heading blocks will carry. */
58
+ function chapterNumbersOfContent(blocks, resolved, prefixes, values, ordinalOffset) {
59
+ const numberless = new Set(resolved.headingStyles.filter((s) => s.numberingTemplate === '').map((s) => s.id));
60
+ const next = chapterNumberCounter(ordinalOffset, (b) => b.styleId !== undefined && numberless.has(b.styleId));
61
+ let current = '';
62
+ return blocks.map((b, i) => {
63
+ if (b.type === 'heading' && (b.level ?? 1) === 1) {
64
+ const style = headingStyleOf(b, resolved);
65
+ const value = next({
66
+ contentIndex: i,
67
+ unnumbered: style ? !style.numbered : false,
68
+ headingNumber: values[i],
69
+ numberPrefix: prefixes[i],
70
+ styleId: style?.id,
71
+ });
72
+ // A plate (`runningChapter: false`) reads its own value; the blocks
73
+ // after it stay in the chapter it interrupts.
74
+ if (style && !style.runningChapter)
75
+ return value;
76
+ current = value;
77
+ }
78
+ return current;
79
+ });
80
+ }
52
81
  /**
53
82
  * Column balancing: measure a paragraph asked to run `extraLines` long.
54
83
  * Walks the tracking ladder — no tracking first, then a little positive
@@ -57,21 +86,38 @@ const ALLOWED_PAGE_FORMATS = new Set([
57
86
  * gains the line on its own, without needing the looseness target, counts
58
87
  * too). A solution whose extra line is a runt is refused: filling a
59
88
  * column's foot is no reason to leave a syllable alone at the end of a
60
- * paragraph. Falls back to the plain measurement when no rung works,
61
- * recording the outcome either way.
89
+ * paragraph. So is one with a justified line past `bodyText.maxWordSpacing`
90
+ * (EF-129): the breaker keeps a looser line only when no break set stays
91
+ * within the limit, and filling a column is no reason to print one. Falls
92
+ * back to the plain measurement when no rung works, recording the outcome
93
+ * either way.
94
+ *
95
+ * The target counts from the paragraph as placed, which the runt fix may
96
+ * have set a line shorter than the breaker's own optimum; the breaker's
97
+ * looseness counts from that optimum, so such a paragraph asks it for one
98
+ * line less (EF-129: asked for one more, it could only match the target by
99
+ * falling back to its optimum, unchecked).
62
100
  */
63
101
  function measureLooseParagraph(rawBlock, blockIdx, columnWidth, ctx, styleOverride, extraLines, trackingLadder, looseOutcome) {
64
102
  const base = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride });
65
103
  if (!base)
66
104
  return null;
67
105
  const target = base.measured.lines.length + extraLines;
106
+ // The breaker's optimum, before any runt fix (a looseness of 0 skips it).
107
+ const optimum = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, { styleOverride, looseness: 0 });
108
+ const looseness = optimum ? target - optimum.measured.lines.length : extraLines;
109
+ const maxWordSpacing = ctx.resolved.bodyText.maxWordSpacing + 1e-9;
110
+ const withinLimit = (lines) => lines.every((l) => l.isLastLine || l.justifiedSpaceRatio === undefined || l.justifiedSpaceRatio <= maxWordSpacing);
68
111
  for (const tracking of trackingLadder) {
69
112
  const loose = measureContentBlock(rawBlock, blockIdx, columnWidth, ctx, {
70
113
  styleOverride,
71
- looseness: extraLines,
114
+ looseness,
72
115
  trackingEm: tracking > 0 ? tracking / 1000 : undefined,
73
116
  });
74
- if (loose && loose.measured.lines.length === target && !loose.measured.lastLineRunt) {
117
+ if (loose
118
+ && loose.measured.lines.length === target
119
+ && !loose.measured.lastLineRunt
120
+ && withinLimit(loose.measured.lines)) {
75
121
  looseOutcome.set(blockIdx, tracking);
76
122
  return loose;
77
123
  }
@@ -116,13 +162,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
116
162
  // Level configs with heading-style overrides merged in (`{style="…"}`).
117
163
  const headingLevels = createHeadingLevelResolver(resolved);
118
164
  const dpi = resolved.page.dpi;
119
- // Initialize hyphenator if needed (body text, or any justified paragraph
120
- // style that hyphenates — they share the document locale).
121
- const needsHyphenator = (resolved.bodyText.hyphenation.enabled && resolved.bodyText.textAlign === 'justify')
122
- || resolved.paragraphStyles.some((s) => s.hyphenation && s.textAlign === 'justify');
123
- if (needsHyphenator) {
124
- initHyphenator(resolved.bodyText.hyphenation.locale);
125
- }
165
+ // The document's hyphenation dictionary, set whatever the alignment: every
166
+ // hyphenating text shares it — body text, paragraph styles, callout bodies,
167
+ // design text — and so do the words wider than their measure, divided at a
168
+ // syllable even in ragged text. Left unset, a build would hyphenate with
169
+ // whichever dictionary the previous one (or the default, en-us) chose.
170
+ initHyphenator(resolved.bodyText.hyphenation.locale);
126
171
  // Compute baseline grid
127
172
  const baselineGrid = computeBaselineGrid(resolved);
128
173
  // Create document
@@ -139,6 +184,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
139
184
  // overrides apply from the first page until this document opens its own.
140
185
  if (continuation?.part)
141
186
  doc.partStart = continuation.part;
187
+ // A part that closed the preceding content: its break is still owed
188
+ // (`pendingPartBreak` below) and a first page left blank is its verso.
189
+ const afterPartPage = continuation?.afterPartPage === true && resolved.parts.page;
190
+ if (afterPartPage)
191
+ doc.afterPartPage = true;
192
+ // The book's page count, when the host knows it (`{bookTotalPages}`).
193
+ const bookPageCount = Math.floor(continuation?.bookPageCount ?? 0);
194
+ if (bookPageCount > 0)
195
+ doc.bookPageCount = bookPageCount;
142
196
  const pageMetrics = computePageMetrics(resolved);
143
197
  const { pageWidthPx, pageHeightPx, trimOffset } = pageMetrics;
144
198
  // The geometry pages are opened with: the document's, or — inside a
@@ -153,10 +207,14 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
153
207
  doc.pages.push(firstPage);
154
208
  // Extract frontmatter, then parse the remaining markdown body
155
209
  const { metadata: frontmatterMeta, content: markdownBody, contentOffset: bodyOffset, fieldSources } = extractFrontmatter(content.markdown);
156
- doc.metadata = { ...(content.metadata ?? {}), ...frontmatterMeta };
210
+ // Typed YAML values (`title: 1984`, `publishDate: 2026-09-24`) print as
211
+ // text; dates in the document language (a blank `locale` is unset).
212
+ doc.metadata = normalizeMetadata({ ...(content.metadata ?? {}), ...frontmatterMeta }, resolvedLocale(resolved));
157
213
  if (fieldSources)
158
214
  doc.metadataSources = fieldSources;
159
- const parsedBlocks = parseMarkdownMemo(markdownBody);
215
+ // A configuration that leaves headings' inline marks off sets their
216
+ // spans plain here, for the outline and the layout alike (EF-122).
217
+ const parsedBlocks = headingMarksFor(parseMarkdownMemo(markdownBody), resolved);
160
218
  const headingStart = continuation?.headings;
161
219
  // `:::toc` expands into the entries of the book's outline — the one the
162
220
  // host supplied, else this document's own (page labels unknown on the
@@ -165,18 +223,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
165
223
  ?? (hasTocDirective(parsedBlocks) ? computeOutline(parsedBlocks, resolved, headingStart) : undefined);
166
224
  const contentBlocks = expandTocDirectives(parsedBlocks, outline, resolved);
167
225
  const isNumbered = (b) => headingIsNumbered(b, resolved);
168
- const headingTemplates = {};
169
- for (const lvl of resolved.headings.levels) {
170
- if (lvl.numberingTemplate && lvl.numberingTemplate.length > 0) {
171
- headingTemplates[lvl.level] = lvl.numberingTemplate;
172
- }
173
- }
174
- const headingPrefixes = computeHeadingNumbers(contentBlocks, headingTemplates, headingStart ? [headingStart.h1, headingStart.h2, headingStart.h3, headingStart.h4, headingStart.h5, headingStart.h6] : undefined, isNumbered);
226
+ const { prefixes: headingPrefixes, values: headingNumbers } = computeHeadingNumbering(contentBlocks, headingTemplatesOf(resolved), headingStart ? [headingStart.h1, headingStart.h2, headingStart.h3, headingStart.h4, headingStart.h5, headingStart.h6] : undefined, isNumbered, headingNumberingOptions(resolved));
227
+ // `{chapterNumber}` at each content block — the current chapter's, as the
228
+ // running heads print it on the block's page (`computeChapterNumbers`
229
+ // runs the same counter over the placed blocks) — so an opener design is
230
+ // measured with the number it is painted with.
231
+ const chapterNumberByBlock = chapterNumbersOfContent(contentBlocks, resolved, headingPrefixes, headingNumbers, doc.chapterOrdinalOffset ?? 0);
175
232
  // Resource numbering — computed up front (before the placement loop) so that
176
233
  // captions and inline `:ref`s can resolve their rendered number strings
177
234
  // before measurement. Numbering follows order of first reference in the
178
235
  // document.
179
- const resourceTypes = config?.resourceTypes ?? defaultResourceTypes();
236
+ const resourceTypes = config?.resourceTypes ?? defaultResourceTypes(documentLocale(config));
180
237
  const resources = content.resources ?? [];
181
238
  const headingContext = computeHeadingContext(contentBlocks, headingStart, isNumbered);
182
239
  const resourceNumbering = computeResourceNumbering(contentBlocks, resourceTypes, resources, headingContext, continuation ? { counters: continuation.resourceCounters, numbered: continuation.resourceNumbers } : undefined);
@@ -212,9 +269,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
212
269
  // resources detach from the running text and take the first free slot
213
270
  // after that reference (`floatSlots.ts`); the text flows past the
214
271
  // reference uninterrupted. `position: 'here'` resources keep inline
215
- // `::resource` placement and are not floated.
216
- const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes);
217
- const floatedIds = floatedResourceIds(floatPlan);
272
+ // `::resource` placement and are not floated. A resource an earlier
273
+ // chapter already numbered (`continuation.resourceNumbers`) was placed
274
+ // there: here it is only referred to, never floated again.
275
+ const incorporated = new Set(Object.keys(continuation?.resourceNumbers ?? {}));
276
+ const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated);
277
+ const floatedIds = floatedResourceIds(floatPlan, incorporated, resources, resourceTypes);
218
278
  const floatsByFirstBlock = new Map();
219
279
  for (const f of floatPlan) {
220
280
  const list = floatsByFirstBlock.get(f.firstBlockIdx);
@@ -230,6 +290,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
230
290
  * page: they take the side column of the next page the flow opens, in
231
291
  * order (see `placeCalloutSide`). */
232
292
  const pendingSideBoxes = [];
293
+ /** `span: 'side'` boxes of a style with `sideAtColumnEnd: 'after'`
294
+ * (EF-161), and the side boxes fenced behind them, waiting for the text
295
+ * after their fence to land (see `settleAwaitingSideBoxes`). Each keeps
296
+ * where its fence was: its page, band, text column and the y the text
297
+ * would have gone on at. */
298
+ const awaitingSideBoxes = [];
233
299
  /** Whether the band the cursor is in lays out as a multi-column band:
234
300
  * two or more text columns, or one beside a float-only side column — a
235
301
  * page-span block then cuts the band across every column. */
@@ -258,6 +324,133 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
258
324
  pendingFloats.push(stamped);
259
325
  }
260
326
  };
327
+ /** Floats first cited in a text block that is still being placed, keyed
328
+ * by resource id: the block and the index, among its measured lines, of
329
+ * the line that holds the citation. A float is queued when its citing
330
+ * block starts, but until that line is placed it takes no slot — not the
331
+ * head of a page the block's earlier lines open, nor that of the page
332
+ * the whole block moves on to — so it never lands above the line that
333
+ * cites it (EF-69). Side floats sit beside their text and are not held. */
334
+ const citationGates = new Map();
335
+ /** The text block whose lines are being placed, and how many are. */
336
+ const citedLines = { blockIdx: -1, placed: 0 };
337
+ const awaitsCitation = (f) => citationGates.has(f.resourceId);
338
+ /** Drop the gates of blocks before `blockIdx` (a block always places all
339
+ * its lines before the flow moves past it; this only guards against a
340
+ * gate outliving its block), or every gate at a chapter barrier. */
341
+ const releaseStaleCitationGates = (blockIdx = Infinity) => {
342
+ for (const [id, gate] of citationGates)
343
+ if (gate.blockIdx < blockIdx)
344
+ citationGates.delete(id);
345
+ };
346
+ /** Gate the pending floats first cited in `lines` of text block
347
+ * `blockIdx`, before its first line is placed. A keep-with-next rewind
348
+ * that places the headings before it again leaves its gates standing:
349
+ * they belong to a later block. */
350
+ const gateCitedFloats = (blockIdx, lines) => {
351
+ citedLines.blockIdx = blockIdx;
352
+ citedLines.placed = 0;
353
+ const fl = floatsByFirstBlock.get(blockIdx);
354
+ if (!fl)
355
+ return;
356
+ for (const f of fl) {
357
+ if (f.span === 'side' || !pendingFloats.some((p) => p.resourceId === f.resourceId))
358
+ continue;
359
+ const line = lines.findIndex((l) => l.segments?.some((s) => s.refResourceId === f.resourceId));
360
+ if (line >= 0)
361
+ citationGates.set(f.resourceId, { blockIdx, line });
362
+ }
363
+ };
364
+ /** A paragraph re-broken after some of its lines were placed (the lines
365
+ * placed are unchanged): the floats it still holds back wait for the
366
+ * line of the new setting that cites them. */
367
+ const regateCitedFloats = (blockIdx, lines) => {
368
+ for (const [id, gate] of citationGates) {
369
+ if (gate.blockIdx !== blockIdx || gate.line === Infinity)
370
+ continue;
371
+ const line = lines.findIndex((l) => l.segments?.some((s) => s.refResourceId === id));
372
+ if (line >= 0)
373
+ citationGates.set(id, { blockIdx, line });
374
+ }
375
+ };
376
+ /** Gate the pending floats first cited by the children of a box (content
377
+ * indices `from`…`to`) until the fragment that holds the citing child
378
+ * is committed: a box split across pages must not let a figure its
379
+ * second part cites head the page that part opens (above it). */
380
+ const gateBoxCitations = (from, to) => {
381
+ for (let i = from; i <= to; i++) {
382
+ for (const f of floatsByFirstBlock.get(i) ?? []) {
383
+ if (f.span === 'side' || !pendingFloats.some((p) => p.resourceId === f.resourceId))
384
+ continue;
385
+ citationGates.set(f.resourceId, { blockIdx: i, line: Infinity });
386
+ }
387
+ }
388
+ };
389
+ /** A box fragment was committed: every child before content index
390
+ * `placedBefore` is set whole, so the floats they cite may take slots
391
+ * from here on. */
392
+ const releaseBoxCitations = (placedBefore) => {
393
+ for (const [id, gate] of citationGates) {
394
+ if (gate.line !== Infinity || gate.blockIdx >= placedBefore)
395
+ continue;
396
+ citationGates.delete(id);
397
+ const at = pendingFloats.findIndex((p) => p.resourceId === id);
398
+ if (at >= 0)
399
+ pendingFloats[at] = { ...pendingFloats[at], refPageIndex: cursor.pageIndex };
400
+ }
401
+ };
402
+ /** The block a free heading at `blockIdx` keeps with: the first one after
403
+ * it that is not a heading, a directive or a container marker (a box's
404
+ * opening fence counts: the box is placed there). `undefined` without
405
+ * keep-with-next, or when nothing follows. */
406
+ const keepWithNextTarget = (blockIdx) => {
407
+ if (!resolved.headings.keepWithNext)
408
+ return undefined;
409
+ for (let j = blockIdx + 1; j < contentBlocks.length; j++) {
410
+ const b = contentBlocks[j];
411
+ if (b.type === 'containerStart' && b.containerName === 'callout')
412
+ return j;
413
+ if (b.type === 'heading' || b.type === 'directive' || isMarkerBlock(b))
414
+ continue;
415
+ return j;
416
+ }
417
+ return undefined;
418
+ };
419
+ /** `placed`, the part of the block just set in its column, holds its
420
+ * next lines: release the floats they cite, stamped with the page and
421
+ * height of the citing line. A free heading's floats wait instead for
422
+ * the block it keeps with: until that block is placed, a keep-with-next
423
+ * rollback can still carry the heading to the next column or page, and
424
+ * a float placed meanwhile would sit above the line that cites it. The
425
+ * gate moves to that block's first line (`releaseStaleCitationGates`
426
+ * keeps it; its first `citedLinesPlaced` releases it, or the flow moving
427
+ * past it does), and a rollback that replays the heading gates it on the
428
+ * heading again (`gateCitedFloats`). */
429
+ const citedLinesPlaced = (blockIdx, placed) => {
430
+ // Side boxes waiting for the text after their fence stand beside it
431
+ // before the floats this text cites take the side column.
432
+ settleAwaitingSideBoxes(false);
433
+ if (citedLines.blockIdx !== blockIdx)
434
+ return;
435
+ const from = citedLines.placed;
436
+ citedLines.placed += placed.lines.length;
437
+ const keeper = placed.type === 'heading' && placed.containerId === undefined
438
+ ? keepWithNextTarget(blockIdx)
439
+ : undefined;
440
+ for (const [id, gate] of citationGates) {
441
+ if (gate.blockIdx !== blockIdx || gate.line >= citedLines.placed)
442
+ continue;
443
+ if (keeper !== undefined) {
444
+ citationGates.set(id, { blockIdx: keeper, line: 0 });
445
+ continue;
446
+ }
447
+ citationGates.delete(id);
448
+ const at = pendingFloats.findIndex((p) => p.resourceId === id);
449
+ const line = placed.lines[Math.max(0, gate.line - from)];
450
+ if (at >= 0 && line)
451
+ pendingFloats[at] = { ...pendingFloats[at], refPageIndex: cursor.pageIndex, refY: line.bbox.y };
452
+ }
453
+ };
261
454
  /** Floated boxes (`placement: 'auto' | 'top' | 'bottom'`, `span` column / page):
262
455
  * a `:::callout` that leaves the flow like a resource and takes the
263
456
  * first free band after the point it occurs at — the foot of the
@@ -404,9 +597,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
404
597
  const cf = calloutFloatOf(resourceId);
405
598
  if (cf) {
406
599
  // A floated box: its frame at the band's width, title and icon
407
- // included; no caption baseline to align.
600
+ // included; no caption baseline to align. Its margins, when wider
601
+ // than the float gap, are the space the band keeps next to it
602
+ // (EF-144), as a box set in the side column keeps its `marginBottom`.
408
603
  const r = cf.L.layoutRange(CUT_START, cf.L.end, width, 'float-probe', false);
409
- const mc = { height: r.totalHeight };
604
+ const mc = {
605
+ height: r.totalHeight,
606
+ ...(r.marginBottomPx > floatGapPx ? { gapBelow: r.marginBottomPx } : {}),
607
+ ...(r.marginTopPx > floatGapPx ? { gapAbove: r.marginTopPx } : {}),
608
+ };
410
609
  floatMeasureMemo.set(key, mc);
411
610
  return mc;
412
611
  }
@@ -488,6 +687,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
488
687
  * sends single-column floats to the least reserved column). */
489
688
  const floatReserved = new Map();
490
689
  const reservedOf = (col) => floatReserved.get(col) ?? { top: 0, bottom: 0 };
690
+ const floatHeadOf = (col) => reservedOf(col).top;
691
+ /** `BandCap.headPx` for a cap proposed on `cols`: how far the float bands
692
+ * every column holds at its head put the band's top below its bare one. */
693
+ const capHeadPx = (cols) => {
694
+ const head = bandTop(cols) - bareBandTop(cols, floatHeadOf);
695
+ return head > 0.5 ? { headPx: head } : {};
696
+ };
491
697
  /** Content index of the block that first referenced the latest float
492
698
  * reserved at the head of each column. */
493
699
  const topFloatRefOf = new Map();
@@ -593,6 +799,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
593
799
  const rest = slice.continues ? { ...f, startRow: end } : undefined;
594
800
  return rest ? { slice, rest } : { slice };
595
801
  };
802
+ /** The heading-design elements standing in each float-only side column
803
+ * (EF-78), with the heading block whose design paints them: the side
804
+ * stack sets its figures, tables and boxes clear of them
805
+ * (`clearSideObstacles`), above them when they fit there. Recorded by
806
+ * `markSideObstacles`, dropped when their heading is rolled back. */
807
+ const sideObstacles = new WeakMap();
596
808
  /**
597
809
  * Reserve a float band on `targetCols` (one column, or every text column
598
810
  * of the band for a page-span float) and position the float there.
@@ -634,7 +846,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
634
846
  return null;
635
847
  // A side float stacks in the side column beside the citing text.
636
848
  if (side) {
637
- const { need, y } = measureSideStack(measure, first, refY, page.contentArea, baselineGrid, floatGapPx);
849
+ const { need, y } = measureSideStack(measure, first, refY, page.contentArea, baselineGrid, floatGapPx, sideObstacles.get(first));
638
850
  return { need, y, measure, slice, width, xLeft, rotated };
639
851
  }
640
852
  // A bottom band normally anchors to the column's true foot (under a
@@ -659,10 +871,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
659
871
  }
660
872
  // A bottom float's caption cuts the side column's foot; when the side
661
873
  // stack already reaches into that band (a box set beside the text
662
- // above), the caption goes under the figure instead of behind the box.
874
+ // above), or a heading design stands there, the caption goes under the
875
+ // figure instead of behind the box.
663
876
  if (aside && sideCol && position === 'bottom' && measure.asideHeight !== undefined) {
664
877
  const captionTop = y + measure.height - measure.asideHeight - floatGapPx;
665
- if (sideUsedBottom(sideCol) > captionTop + 0.5) {
878
+ const standsInBand = (sideObstacles.get(sideCol) ?? []).some((o) => o.bottom > captionTop + 0.5);
879
+ if (sideUsedBottom(sideCol) > captionTop + 0.5 || standsInBand) {
666
880
  const plain = measureFloat(f.resourceId, width, slice, rotated);
667
881
  if (plain) {
668
882
  ({ need, y } = measureFloatBand(position, plain, targetCols, page.contentArea, baselineGrid, floatGapPx, bottomOf));
@@ -681,13 +895,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
681
895
  return Math.max(0, col.availableHeight - probe.need);
682
896
  if (uncappedBottoms.has(col) && !anchorToCap)
683
897
  return col.availableHeight;
684
- const newHeight = Math.max(0, probe.y - floatGapPx - col.bbox.y);
898
+ const newHeight = Math.max(0, probe.y - floatGapAbove(probe.measure, floatGapPx) - col.bbox.y);
685
899
  return Math.max(0, col.availableHeight - (col.bbox.height - newHeight));
686
900
  };
687
901
  /** Set a built float at its band position and hand it to the page: a
688
- * resource block into `page.floats`; a floated box's frame and children
689
- * into `page.floats` and `doc.blocks`, the way a fixed box goes. */
690
- const commitFloatBlock = (page, col, built, x, y, width) => {
902
+ * resource block into `page.floats`, with the content index of the block
903
+ * that first cites or embeds it (`anchorIdx`, where a tagged PDF reads
904
+ * it); a floated box's frame and children into `page.floats` and
905
+ * `doc.blocks`, the way a fixed box goes. */
906
+ const commitFloatBlock = (page, col, built, x, y, width, anchorIdx) => {
691
907
  const cf = calloutFloatResults.get(built.block);
692
908
  if (cf) {
693
909
  calloutFloatResults.delete(built.block);
@@ -713,6 +929,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
713
929
  built.block.bbox = createBoundingBox(x, y, width, built.height);
714
930
  built.block.pageIndex = page.index;
715
931
  built.block.columnIndex = col.index;
932
+ built.block.contentIndex = anchorIdx;
716
933
  (page.floats ??= []).push(built.block);
717
934
  };
718
935
  const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false, side = false, refY) => {
@@ -733,7 +950,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
733
950
  // side column of the next page — where it is set anyway, overflowing,
734
951
  // when even an empty column cannot hold it (a dominating figure).
735
952
  if (need > first.availableHeight + 0.01) {
736
- if (mode === 'strict' || sideUsedBottom(first) > first.bbox.y + 0.5)
953
+ // A column that already holds something — a float, a box, or a
954
+ // heading design standing in it — is no empty column to overflow.
955
+ const holds = sideUsedBottom(first) > first.bbox.y + 0.5 || (sideObstacles.get(first)?.length ?? 0) > 0;
956
+ if (mode === 'strict' || holds)
737
957
  return 'defer';
738
958
  // An empty column shortened by a band above it (a page-span box,
739
959
  // a top float) is no measure of the float: when a full column
@@ -750,7 +970,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
750
970
  if (!built)
751
971
  return 'skip';
752
972
  first.availableHeight = Math.max(0, first.availableHeight - need);
753
- commitFloatBlock(page, first, built, xLeft, y, width);
973
+ commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
754
974
  floatsPlaced++;
755
975
  return 'placed';
756
976
  }
@@ -805,7 +1025,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
805
1025
  for (const c of targetCols) {
806
1026
  if (position === 'bottom' && uncappedBottoms.has(c) && !anchorToCap) {
807
1027
  // Trailing cap: the band must lie entirely below the level cut.
808
- if (y - floatGapPx < c.bbox.y + c.bbox.height - 0.01)
1028
+ if (y - floatGapAbove(measure, floatGapPx) < c.bbox.y + c.bbox.height - 0.01)
809
1029
  return 'defer';
810
1030
  continue;
811
1031
  }
@@ -843,6 +1063,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
843
1063
  if (minAvail - need < minTextPx)
844
1064
  need = Math.max(need, minAvail);
845
1065
  }
1066
+ // A page opened for a block that cannot split (an inline figure or
1067
+ // table, see `onAtomicNewPage`): a float takes its head or foot only
1068
+ // when the block still fits the column beside it; otherwise it waits
1069
+ // for the next page, as it did before the block took the flush.
1070
+ if (mode === 'fresh' && freshPageHold && targetCols.includes(freshPageHold.col)) {
1071
+ const after = galleryFill ? 0 : availableAfterBand(freshPageHold.col, position, { need, y, measure }, false);
1072
+ if (after < freshPageHold.px - FIT_EPS)
1073
+ return 'defer';
1074
+ }
846
1075
  // The rest goes on after this slice in reading order, never before.
847
1076
  if (rest) {
848
1077
  rest = { ...rest, notBefore: { pageIndex: page.index, columnIndex: targetCols[targetCols.length - 1].index } };
@@ -884,7 +1113,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
884
1113
  uncappedBottoms.set(col, capped - need);
885
1114
  }
886
1115
  else {
887
- const newHeight = Math.max(0, y - floatGapPx - col.bbox.y);
1116
+ const newHeight = Math.max(0, y - floatGapAbove(measure, floatGapPx) - col.bbox.y);
888
1117
  const reserved = col.bbox.height - newHeight;
889
1118
  col.bbox.height = newHeight;
890
1119
  col.availableHeight = Math.max(0, col.availableHeight - reserved);
@@ -902,7 +1131,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
902
1131
  if (galleryFill)
903
1132
  for (const col of targetCols)
904
1133
  col.availableHeight = 0;
905
- commitFloatBlock(page, first, built, xLeft, y, width);
1134
+ commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
906
1135
  floatsPlaced++;
907
1136
  return rest ? { rest } : 'placed';
908
1137
  };
@@ -925,8 +1154,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
925
1154
  * tables are numbered in first-reference order, and the reader must
926
1155
  * meet them in that order too — table 3 never lands after table 4, even
927
1156
  * when 4 would fit a slot 3 does not. Sequences do not hold each other
928
- * up: a waiting table lets a later figure through. */
1157
+ * up: a waiting table lets a later figure through. A float whose citing
1158
+ * line is not placed yet waits too (see `citationGates`), and holds back
1159
+ * its sequence. */
929
1160
  const heldBack = (i) => {
1161
+ if (awaitsCitation(pendingFloats[i]))
1162
+ return true;
930
1163
  const typeId = resourceById.get(pendingFloats[i].resourceId)?.typeId;
931
1164
  for (let j = 0; j < i; j++) {
932
1165
  if (resourceById.get(pendingFloats[j].resourceId)?.typeId === typeId)
@@ -1157,6 +1390,24 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1157
1390
  /** Reserve floats on each freshly opened content page. Passed only to the
1158
1391
  * content-flow column advances — parity / force-blank pages never get it. */
1159
1392
  const onNewPage = (page) => flushFloatsIntoPage(page);
1393
+ /** The block a page is being opened for, when it cannot split: the
1394
+ * column it goes into and the height it needs there (see
1395
+ * `placeFloatInColumns`). Set only while that page's floats are flushed. */
1396
+ let freshPageHold = null;
1397
+ /** `onNewPage` for a page an inline figure or table opens (EF-160): the
1398
+ * floats pending take its bands as on any page the flow opens — up to
1399
+ * postext 1.4 they skipped it and waited for the page after — but never
1400
+ * the room the block needs under or over them. */
1401
+ const onAtomicNewPage = (heightPx) => (page) => {
1402
+ const col = page.columns.find((c) => c.kind !== 'span' && c.kind !== 'side');
1403
+ freshPageHold = col ? { col, px: heightPx } : null;
1404
+ try {
1405
+ flushFloatsIntoPage(page);
1406
+ }
1407
+ finally {
1408
+ freshPageHold = null;
1409
+ }
1410
+ };
1160
1411
  /** Whether content block `idx` opens a callout that will span the page
1161
1412
  * in the current (multi-column) band — the floats placed right before
1162
1413
  * it prefer the head of an empty column. */
@@ -1195,6 +1446,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1195
1446
  break; // safety: no progress
1196
1447
  }
1197
1448
  };
1449
+ /** Boxes that left the flow this pass (side column, float band, fixed),
1450
+ * by the content index of their opening marker (see `leftFlow`). */
1451
+ const calloutsOutOfFlow = new Set();
1198
1452
  // Everything per-block measurement needs that is constant for this pass.
1199
1453
  const measureCtx = {
1200
1454
  resolved,
@@ -1202,6 +1456,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1202
1456
  bodyStyle,
1203
1457
  blockquoteStyle,
1204
1458
  headingPrefixes,
1459
+ headingNumbers,
1205
1460
  listLevelIndentsPx,
1206
1461
  orderedLevelIndentsPx,
1207
1462
  orderedMetrics,
@@ -1211,10 +1466,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1211
1466
  contentBlocks,
1212
1467
  cache,
1213
1468
  bodyOffset,
1469
+ source: markdownBody,
1214
1470
  resources,
1215
1471
  resourceTypes,
1216
1472
  resourceNumbering,
1217
1473
  floatedIds,
1474
+ leftFlow: calloutsOutOfFlow,
1218
1475
  };
1219
1476
  // Blocks inside a `:::part` measure with the part body typography.
1220
1477
  const partMeasureCtx = partPlan.byStart.size > 0
@@ -1262,6 +1519,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1262
1519
  blk.headingStyleId = style.id;
1263
1520
  if (!style.numbered)
1264
1521
  blk.unnumbered = true;
1522
+ if (!style.runningChapter && (raw.level ?? 1) === 1)
1523
+ blk.notRunningChapter = true;
1524
+ }
1525
+ // A letter-case transform changes what the lines print, not the
1526
+ // title: keep it as written for the bookmarks (EF-81). A title citing
1527
+ // a resource keeps the printed text, whose `:ref` label the source
1528
+ // does not hold.
1529
+ if (headingLevels.forBlock(raw)?.textTransform === 'uppercase' && !raw.spans.some((s) => s.ref)) {
1530
+ blk.sourceTitle = flattenTitleBreaks(raw.text);
1265
1531
  }
1266
1532
  }
1267
1533
  if (raw.toc?.kind === 'entry') {
@@ -1279,6 +1545,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1279
1545
  };
1280
1546
  let blockIdCounter = 0;
1281
1547
  let pendingSpacing = 0;
1548
+ /** The float gap an inline resource still owes the block right after it
1549
+ * (EF-93), or the space a `:::paragraphs` container still owes the block
1550
+ * after its closing marker (EF-159; `container`, carried through the
1551
+ * closing markers that follow, an enclosing container's included),
1552
+ * measured from the grid line its foot snapped to: `pending` is what it
1553
+ * left in `pendingSpacing`, in whole grid lines so text lands back on the
1554
+ * grid; `exact` is the gap itself, which a block that sets itself off
1555
+ * the grid and snaps the flow after it (a heading, a display formula, a
1556
+ * box) collapses its own space above with instead. */
1557
+ let inlineGapOwed = null;
1558
+ /** The pending spacing a heading, a display formula or a box at `idx`
1559
+ * collapses its top margin with (see `inlineGapOwed`). */
1560
+ const pendingBeforeSelfSnapping = (idx) => inlineGapOwed && inlineGapOwed.afterIdx === idx - 1 && pendingSpacing === inlineGapOwed.pending
1561
+ ? inlineGapOwed.exact
1562
+ : pendingSpacing;
1282
1563
  // Pages whose break into the next page is explicit rather than natural
1283
1564
  // overflow. Column balancing leaves their last column short (a chapter's
1284
1565
  // closing page legitimately ends early).
@@ -1301,6 +1582,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1301
1582
  // after landing (the cap then trims whatever the block left).
1302
1583
  let registeredBand = null;
1303
1584
  let bandStart = null;
1585
+ /** Bands registered so far in this pass per opening block and part
1586
+ * (`contentIndex:part`): the `visit` of the next one (see `enterBand`). */
1587
+ const bandVisits = new Map();
1304
1588
  let activeCap = null;
1305
1589
  /** True bottoms of capped columns (restored when the span block cuts). */
1306
1590
  const uncappedBottoms = new Map();
@@ -1317,6 +1601,157 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1317
1601
  if (px > 0)
1318
1602
  balanceExtraInColumn.set(col, (balanceExtraInColumn.get(col) ?? 0) + px);
1319
1603
  };
1604
+ /** Whether a heading is painted by the default opener: a `span: 'page'`
1605
+ * level (or style) without design elements of its own, that prints
1606
+ * (EF-100). */
1607
+ const opensDefaultOpener = (raw) => {
1608
+ const lvl = headingLevels.forBlock(raw);
1609
+ return lvl?.span === 'page'
1610
+ && !(lvl.advancedDesign.enabled && lvl.advancedDesign.slot.elements.length > 0)
1611
+ && !headingIsHidden(raw, lvl);
1612
+ };
1613
+ /** Page-spanning heading placed in `headingCol`, reserving `h` px from the
1614
+ * top of the page: the same vertical band is reserved in every other
1615
+ * column of the page, so body text under the opener band starts below it
1616
+ * in ALL columns, not just the one it was placed in. A column still
1617
+ * untouched moves its head below the band (as a top float does), so a
1618
+ * float offered that column's head lands under the opener instead of
1619
+ * over it. A text column's head goes to the first grid line at or under
1620
+ * the band's foot, so what it snaps lands on the page's grid, as in the
1621
+ * opener's own column (EF-139); its first block is then placed as
1622
+ * `openerHeadSpacing` says. An `h` at least as tall as the columns
1623
+ * spends them whole. `pending` is the space the opener leaves under it:
1624
+ * its `marginBottom` when it did not snap, 0 when the snap took it in;
1625
+ * `gridText` whether the text under it lands on the page's grid (its
1626
+ * level snaps, or would but for the heading after it). */
1627
+ const reserveOpenerBand = (headingCol, h, opener, pending = 0, gridText = true) => {
1628
+ const page = doc.pages[cursor.pageIndex];
1629
+ for (const otherCol of page.columns) {
1630
+ if (otherCol === headingCol)
1631
+ continue;
1632
+ if (otherCol.blocks.length === 0 && otherCol.availableHeight >= otherCol.bbox.height - 0.01) {
1633
+ const foot = otherCol.bbox.y + h;
1634
+ const text = otherCol.kind !== 'side' && otherCol.kind !== 'span';
1635
+ const reach = (text ? gridUpOnPage(page, foot) : foot) - otherCol.bbox.y;
1636
+ const shift = Math.min(reach, otherCol.bbox.height);
1637
+ otherCol.bbox.y += shift;
1638
+ otherCol.bbox.height -= shift;
1639
+ otherCol.availableHeight = Math.max(0, otherCol.availableHeight - reach);
1640
+ if (opener && text) {
1641
+ const textTop = gridText ? gridUpOnPage(page, foot + pending) : foot + pending;
1642
+ openerBandHeads.set(otherCol, { openerCol: headingCol, opener, foot, textTop, floatTop: reservedOf(otherCol).top });
1643
+ }
1644
+ continue;
1645
+ }
1646
+ otherCol.availableHeight = Math.max(0, otherCol.availableHeight - h);
1647
+ }
1648
+ };
1649
+ /** The page grid line at or below `y` (absolute px). */
1650
+ const gridUpOnPage = (page, y) => page.contentArea.y + Math.ceil((y - page.contentArea.y - 0.01) / baselineGrid) * baselineGrid;
1651
+ /** Columns headed by a page-span opener's band (see `reserveOpenerBand`):
1652
+ * the opener, its column, the band's foot, where text right under the
1653
+ * opener starts and the float band the column held at its head then. */
1654
+ const openerBandHeads = new Map();
1655
+ /**
1656
+ * The room above the first block of a column headed by an opener band,
1657
+ * relative to the column's head, or undefined for any other column (or
1658
+ * one the flow reached after a float took its head). Every column under
1659
+ * the band starts as the opener's own column does (EF-139):
1660
+ * - text starts where text right under the opener starts: the band's
1661
+ * foot plus the space the opener leaves, taken to the next grid line
1662
+ * when the opener's level snaps (its text lands on the page's grid, as
1663
+ * it does when the opener did not snap only because a heading follows);
1664
+ * - a heading (or another block that keeps its top margin, `level`) is
1665
+ * set level with the first visible block under the opener when that
1666
+ * is a heading or a display formula, margins included as there (a
1667
+ * balancing lever above it excepted), else level with the text there.
1668
+ * A hidden heading (`hidden: true`) under the opener does not count:
1669
+ * it prints nothing to line up with. Its own snap then puts the text
1670
+ * after it on the page's grid.
1671
+ * The block may sit up to a line above the column's head, which is on
1672
+ * the grid; the renderers' column clip takes it in (`columnClipRect`).
1673
+ * Up to postext 1.4 the first block of such a column started at the
1674
+ * band's foot, off the grid when the band ended between two lines, and a
1675
+ * heading there dropped the top margin the first column's kept.
1676
+ */
1677
+ const openerHeadSpacing = (col, level) => {
1678
+ const head = openerBandHeads.get(col);
1679
+ if (!head || col.blocks.length > 0 || reservedOf(col).top !== head.floatTop)
1680
+ return undefined;
1681
+ let target = head.textTop;
1682
+ if (level) {
1683
+ const at = head.openerCol.blocks.indexOf(head.opener);
1684
+ const next = at >= 0 ? head.openerCol.blocks.slice(at + 1).find((b) => !b.hidden) : undefined;
1685
+ if (next && (next.type === 'heading' || next.type === 'mathDisplay')) {
1686
+ target = Math.max(head.foot, next.bbox.y - (next.balancing?.spaceAbove ?? 0));
1687
+ }
1688
+ }
1689
+ return target - col.bbox.y;
1690
+ };
1691
+ /** The side column each in-column heading marked obstacles in (see
1692
+ * `markSideObstacles`), to drop them when the heading is rolled back. */
1693
+ const sideObstacleColumn = new WeakMap();
1694
+ /**
1695
+ * Keep the float-only side column's stack off the heading design placed
1696
+ * as `blk` (EF-78). A design element may be anchored outside the heading's
1697
+ * column — a chapter numeral set in the outer margin column of a textbook
1698
+ * opener, a section number hung in the margin — where the side stack
1699
+ * (`span: 'side'` figures, tables and boxes) would paint over it. Each
1700
+ * such element is recorded as an obstacle of the side column: what the
1701
+ * stack sets later goes above it when it fits there, else one float gap
1702
+ * under it (`clearSideObstacles`). A numeral at the head of the channel
1703
+ * thus holds the whole stack under it, while a number hung beside a
1704
+ * heading further down the page leaves the head of the channel to the
1705
+ * figures the page cites. What the stack already holds is not moved.
1706
+ * A page-spanning opener needs none of this: its band is reserved in
1707
+ * every column, the side column included (`reserveOpenerBand`).
1708
+ */
1709
+ const markSideObstacles = (blk, design) => {
1710
+ if (design.lvl.span === 'page')
1711
+ return;
1712
+ const page = doc.pages[cursor.pageIndex];
1713
+ const side = sideColumnOf(page, currentBand(page, cursor));
1714
+ if (!side || side.bbox.height <= 0.5)
1715
+ return;
1716
+ const boxes = headingDesignBoxes(design.lvl, design.info, blk.bbox, resolved.page.dpi, doc.metadata, cursor.pageIndex, designFrames, resourceById);
1717
+ const top = side.bbox.y;
1718
+ const foot = side.bbox.y + side.bbox.height;
1719
+ const left = side.bbox.x;
1720
+ const right = side.bbox.x + side.bbox.width;
1721
+ const marks = [];
1722
+ for (const b of boxes) {
1723
+ if (b.width <= 0 || b.height <= 0)
1724
+ continue;
1725
+ if (b.x + b.width <= left + 0.5 || b.x >= right - 0.5)
1726
+ continue;
1727
+ if (b.y + b.height <= top + 0.5 || b.y >= foot - 0.5)
1728
+ continue;
1729
+ marks.push({ owner: blk, top: b.y, bottom: b.y + b.height });
1730
+ }
1731
+ if (marks.length === 0)
1732
+ return;
1733
+ const list = sideObstacles.get(side);
1734
+ if (list)
1735
+ list.push(...marks);
1736
+ else
1737
+ sideObstacles.set(side, marks);
1738
+ sideObstacleColumn.set(blk, side);
1739
+ };
1740
+ /** `rollbackTrailingBlocks` that also drops a rolled-back heading's
1741
+ * side-column obstacles: the heading is set again further on. */
1742
+ const rollbackHeadings = (col) => {
1743
+ const popped = rollbackTrailingBlocks(col, doc.blocks, isFreeHeading);
1744
+ for (const p of popped) {
1745
+ const side = sideObstacleColumn.get(p);
1746
+ if (!side)
1747
+ continue;
1748
+ sideObstacleColumn.delete(p);
1749
+ const list = sideObstacles.get(side);
1750
+ if (list)
1751
+ sideObstacles.set(side, list.filter((o) => o.owner !== p));
1752
+ }
1753
+ return popped;
1754
+ };
1320
1755
  const bandCapProposals = new Map();
1321
1756
  const spanPlacedInBand = new Set();
1322
1757
  const bandCapsApplied = new Set();
@@ -1328,12 +1763,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1328
1763
  if (registeredBand && registeredBand.pageIndex === page.index && registeredBand.band === band)
1329
1764
  return;
1330
1765
  registeredBand = { pageIndex: page.index, band };
1331
- bandStart = { contentIndex, part };
1766
+ // The same block (or part) can be offered to several bands before one
1767
+ // takes it: a page a float fills leaves it no room, and it moves on to
1768
+ // the next page. Each of those bands is told apart by how many came
1769
+ // before it with the same opening block, so a cap lands on the band it
1770
+ // was measured in and not on the first one the block passed through,
1771
+ // which took nothing (EF-167, EF-189).
1772
+ const key = `${contentIndex}:${part}`;
1773
+ const visit = bandVisits.get(key) ?? 0;
1774
+ bandVisits.set(key, visit + 1);
1775
+ bandStart = { contentIndex, part, visit };
1332
1776
  activeCap = null;
1333
1777
  if (!bandCaps)
1334
1778
  return;
1335
1779
  for (const [spanIndex, cap] of bandCaps) {
1336
- if (cap.startContentIndex !== contentIndex || cap.startPart !== part)
1780
+ if (cap.startContentIndex !== contentIndex || cap.startPart !== part || (cap.startVisit ?? 0) !== visit)
1337
1781
  continue;
1338
1782
  // A cap whose last band took nothing (its opening block is taller
1339
1783
  // than the cut — a heading's leading over a one-line cap) would cut
@@ -1344,7 +1788,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1344
1788
  if (prev && prev.every((c) => c.blocks.length === 0))
1345
1789
  continue;
1346
1790
  const cols = bandColumns(page, band);
1347
- applyBandCap(cols, cap.lines * baselineGrid, uncappedBottoms, cap.zone, cap.kind === 'trailing');
1791
+ // The cut is measured from under the float bands the proposing pass
1792
+ // had at the band's head, which may not have landed yet.
1793
+ const top = Math.max(bandTop(cols), bareBandTop(cols, floatHeadOf) + (cap.headPx ?? 0));
1794
+ applyBandCap(cols, cap.lines * baselineGrid, uncappedBottoms, cap.zone, cap.kind === 'trailing', top);
1348
1795
  cappedBandColumns.set(spanIndex, cols);
1349
1796
  activeCap = { spanIndex, pageIndex: page.index, band };
1350
1797
  bandCapsApplied.add(spanIndex);
@@ -1422,29 +1869,55 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1422
1869
  kind: 'trailing',
1423
1870
  startContentIndex: bandStart.contentIndex,
1424
1871
  startPart: bandStart.part,
1872
+ ...(bandStart.visit > 0 ? { startVisit: bandStart.visit } : {}),
1425
1873
  lines: aroundZone ?? Math.max(bandCapLines(cols, baselineGrid, footBands), ...floatHeads),
1426
1874
  retries: 0,
1427
1875
  ...(aroundZone !== null ? { zone } : {}),
1876
+ ...capHeadPx(cols),
1428
1877
  });
1429
1878
  };
1430
1879
  /** Close the flow at a chapter-level boundary (block `boundaryIndex`):
1431
1880
  * floats take the page's free slots, the closing band is levelled, the
1432
1881
  * page is marked as an explicit break, and every float still pending is
1433
1882
  * drained onto pages opened BEFORE the boundary. The caller then opens
1434
- * the boundary's own page. */
1883
+ * the boundary's own page.
1884
+ *
1885
+ * The floats the boundary block itself cites first — a `breakBefore`
1886
+ * heading that names a figure, the first paragraph after a part — were
1887
+ * queued at the top of its iteration, but their citation is set on the
1888
+ * page the boundary opens: they sit the drain out and stay pending (with
1889
+ * no reference stamp, which the citing line sets), so they land after
1890
+ * the line that cites them, in the new segment (EF-69). */
1435
1891
  const closeFlowSegment = (boundaryIndex) => {
1892
+ settleAwaitingSideBoxes(true);
1893
+ releaseStaleCitationGates();
1894
+ const ownFloats = [];
1895
+ for (let i = pendingFloats.length - 1; i >= 0; i--) {
1896
+ const f = pendingFloats[i];
1897
+ if (f.firstBlockIdx !== boundaryIndex || f.callout)
1898
+ continue;
1899
+ const unstamped = { ...f };
1900
+ delete unstamped.refPageIndex;
1901
+ delete unstamped.refY;
1902
+ ownFloats.unshift(unstamped);
1903
+ pendingFloats.splice(i, 1);
1904
+ }
1436
1905
  tryPlacePendingFloatsOnCurrentPage();
1437
1906
  proposeTrailingCap(boundaryIndex);
1438
1907
  markForcedBreak();
1439
1908
  hugClosingText();
1440
1909
  drainPendingFloats();
1910
+ pendingFloats.push(...ownFloats);
1441
1911
  };
1442
1912
  /** On the closing page of a chapter nothing follows the page-span floats
1443
1913
  * set at the foot of its last band: they move up to sit right under the
1444
1914
  * band's text (one float gap below its grid-rounded bottom, stacked in
1445
1915
  * their order) instead of leaving a gap between the text and a table at
1446
- * the page foot. */
1916
+ * the page foot. `layout.hugClosingFloats: false` leaves them where their
1917
+ * placement put them (EF-94). */
1447
1918
  const hugClosingText = () => {
1919
+ if (!resolved.layout.hugClosingFloats)
1920
+ return;
1448
1921
  const page = doc.pages[cursor.pageIndex];
1449
1922
  const floats = page.floats ?? [];
1450
1923
  // A side column holds its own band under a page-span float: leave it.
@@ -1472,8 +1945,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1472
1945
  }
1473
1946
  };
1474
1947
  /** Parity of the page break a closed `:::part` still owes (applied before
1475
- * the next placed block). */
1476
- let pendingPartBreak = null;
1948
+ * the next placed block) — from the start when the part closed the
1949
+ * preceding content (`continuation.afterPartPage`). */
1950
+ let pendingPartBreak = afterPartPage && resolved.parts.breakAfter.enabled
1951
+ ? resolved.parts.breakAfter.parity
1952
+ : null;
1477
1953
  /** Content index of the closing fence of a part set without a page
1478
1954
  * (`parts.page: false`): blocks up to it are skipped. */
1479
1955
  let skipPartUntil = null;
@@ -1575,6 +2051,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1575
2051
  doc.blocks.push(child);
1576
2052
  }
1577
2053
  };
2054
+ /** The widths a child's lines are counted at in a fragment `width` wide
2055
+ * that opens `from` (see `CalloutCut.widths`). */
2056
+ const cutWidths = (from, width) => {
2057
+ const before = from.line > 0 ? from.widths : undefined;
2058
+ if (!before || before.length === 0)
2059
+ return [{ fromLine: 0, width }];
2060
+ const last = before[before.length - 1];
2061
+ return Math.abs(last.width - width) <= 0.5 ? before : [...before, { fromLine: from.line, width }];
2062
+ };
1578
2063
  const CUT_START = { child: 0, line: 0 };
1579
2064
  const makeCalloutLayouter = (startIdx, plan, style) => {
1580
2065
  const children = contentBlocks.slice(startIdx + 1, plan.endIdx);
@@ -1621,6 +2106,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1621
2106
  style,
1622
2107
  attrs: plan.attrs,
1623
2108
  continuation,
2109
+ // Any range short of the box's end is a fragment that goes on.
2110
+ ...(to.child < children.length || to.line > 0 ? { continues: true } : {}),
1624
2111
  children: children.slice(from.child, toChild),
1625
2112
  childStartIdx: startIdx + 1 + from.child,
1626
2113
  width,
@@ -1629,8 +2116,9 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1629
2116
  containerId: plan.containerId,
1630
2117
  frameId,
1631
2118
  nextChildId: () => `${frameId}-c${n++}`,
1632
- paragraphStyleFor: (idx) => paragraphContainers.byBlock[idx]?.style,
2119
+ paragraphContainers,
1633
2120
  ...(from.line > 0 ? { lineFrom: from.line } : {}),
2121
+ ...(from.line > 0 && from.widths ? { lineWidths: from.widths } : {}),
1634
2122
  ...(to.line > 0 ? { lineTo: to.line } : {}),
1635
2123
  ...(open.length > 0 ? { openNested: open } : {}),
1636
2124
  mirrored,
@@ -1641,6 +2129,18 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1641
2129
  /** Whether a page is a verso of mirrored margins (an `'outer'` corner
1642
2130
  * icon hangs on the left there). */
1643
2131
  const mirroredOf = (page) => resolved.page.margins.mirror === true && (page.index + pageIndexOffset + 1) % 2 === 0;
2132
+ /** Where a block's paint ends: its last line's foot (a list tail's box
2133
+ * bakes its bottom margin in and may reach past a cut with every line
2134
+ * inside it), else its box's. */
2135
+ const paintedBottom = (b) => {
2136
+ const last = b.lines[b.lines.length - 1];
2137
+ return last ? last.bbox.y + last.bbox.height : b.bbox.y + b.bbox.height;
2138
+ };
2139
+ /** Whether a band cut by a cap holds a block painted past the cut: one
2140
+ * that could not split across it (a paragraph tail the orphan and widow
2141
+ * minimums keep whole, a keep-together box) was force-placed into an
2142
+ * empty capped column and ran past its foot. */
2143
+ const capClipsLines = (cols) => cols.some((c) => c.bandCapped && c.blocks.some((b) => !b.hidden && paintedBottom(b) > c.bbox.y + c.bbox.height + CAPPED_OVERFLOW_PX));
1644
2144
  /** Used bottom of a band, the float-only side column's stack included:
1645
2145
  * a span block cuts under the side boxes already set there. */
1646
2146
  const bandUsedBottomWithSide = (page, cols) => {
@@ -1654,8 +2154,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1654
2154
  * children leaves on each side at least that many text lines or at least
1655
2155
  * one indivisible block (a figure, table, display formula or nested
1656
2156
  * box); a cut inside a text child counts every line on its side, a block
1657
- * as one line, as before. A nested box that may split itself
1658
- * (`keepTogether: false`, or taller than a full column) offers the cuts
2157
+ * as one line, and leaves at least `layout.boxChildSplitMinLines` lines
2158
+ * of that child on each side (two by default, `splitMinLines` when that
2159
+ * is lower; EF-115). A nested box that may split
2160
+ * itself (`keepTogether: false`, or taller than a full column) offers the cuts
1659
2161
  * inside it too, by its own `splitMinLines` over its own children; the
1660
2162
  * fragments on both sides redraw its frame. The full layout's geometry
1661
2163
  * ranks the candidates (box bottom = content bottom at the cut + the
@@ -1677,7 +2179,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1677
2179
  };
1678
2180
  if (full.units.length === 0)
1679
2181
  return null;
1680
- const tail = tailOf(full);
2182
+ // A head that goes on also carries the style's continuation marker.
2183
+ const tail = tailOf(full) + full.continuesMarkerPx;
1681
2184
  /** Candidate cuts with the head's content bottom. */
1682
2185
  const candidates = [];
1683
2186
  /** Collect the cuts among `units` (one box's items; `below` = the tails
@@ -1707,12 +2210,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1707
2210
  if (!indivisible(c) && c.lines.length > 1) {
1708
2211
  // The first laid-out child of a continuation opens after
1709
2212
  // `from.line` lines: cuts inside it are counted from the
1710
- // child's own head.
2213
+ // child's own head. Each side of the box still counts all its
2214
+ // lines against `min`, and a cut inside a child leaves at least
2215
+ // `layout.boxChildSplitMinLines` lines of it on each side (2 by
2216
+ // default, so no lone line: EF-115), or `min` when that is
2217
+ // lower. The count is of this fragment's part of the child,
2218
+ // since a part left behind already passed the test.
2219
+ const childMin = Math.min(min, resolved.layout.boxChildSplitMinLines);
1711
2220
  const lineBase = k === from.child ? from.line : 0;
1712
- for (let l = 1; l < c.lines.length; l++) {
2221
+ for (let l = childMin; l <= c.lines.length - childMin; l++) {
1713
2222
  const line = c.lines[l - 1];
1714
2223
  if (bothHold(linesBefore + l, blocksBefore, false)) {
1715
- candidates.push({ cut: { child: k, line: lineBase + l }, bottom: line.bbox.y + line.bbox.height + below });
2224
+ candidates.push({
2225
+ cut: { child: k, line: lineBase + l, widths: k === from.child ? cutWidths(from, width) : [{ fromLine: 0, width }] },
2226
+ bottom: line.bbox.y + line.bbox.height + below,
2227
+ });
1716
2228
  }
1717
2229
  }
1718
2230
  }
@@ -1866,13 +2378,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1866
2378
  * the band can be cut for the box (level, or capped for it) and the
1867
2379
  * figure fits between the cut and the band bottom; otherwise the figure
1868
2380
  * stays pending for the ordinary slots. Returns whether any was set.
2381
+ * A floated box (`placement` on a `:::callout`) is no figure: it keeps
2382
+ * to the head or foot of a page, the bands it was floated to.
1869
2383
  */
1870
2384
  const placeSpanFloatsAtCut = (page, capActiveHere) => {
1871
2385
  let placedAny = false;
1872
2386
  for (let i = 0; i < pendingFloats.length;) {
1873
2387
  const f = pendingFloats[i];
1874
2388
  const cols = bandColumns(page, currentBand(page, cursor));
1875
- if (f.span !== 'page' || cols.length < 2 || heldBack(i) || !((capActiveHere && !placedAny) || levelForBox(cols))) {
2389
+ if (f.span !== 'page' || f.callout || cols.length < 2 || heldBack(i) || !((capActiveHere && !placedAny) || levelForBox(cols))) {
1876
2390
  i++;
1877
2391
  continue;
1878
2392
  }
@@ -1897,8 +2411,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1897
2411
  continue;
1898
2412
  }
1899
2413
  if (capActiveHere && !placedAny) {
2414
+ // A band whose content ran past the cut did not deliver its cap
2415
+ // (EF-116): the driver takes the cut lower instead.
2416
+ const overran = capClipsLines(cols);
1900
2417
  uncapBand(cols, uncappedBottoms);
1901
- spanPlacedInBand.add(startIdx);
2418
+ if (!overran)
2419
+ spanPlacedInBand.add(startIdx);
1902
2420
  }
1903
2421
  closeBandAndInsertSpan(page, cols, cutY, built.block, need, cursor, spacing, built.height);
1904
2422
  // The float was built at (x, 0): move its inner geometry down to
@@ -1980,15 +2498,19 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
1980
2498
  const lines = bandCapLines(cols, baselineGrid);
1981
2499
  const capBottom = bandTop(cols) + lines * baselineGrid;
1982
2500
  const bandBottom = Math.min(...cols.map((c) => columnBottom(c, uncappedBottoms)));
1983
- const m = measureFloat(first.resourceId, page.contentArea.width, sliceOf(first));
2501
+ // A floated box is never set at the cut (see
2502
+ // `placeSpanFloatsAtCut`): it only gets the trailing cap.
2503
+ const m = first.callout ? null : measureFloat(first.resourceId, page.contentArea.width, sliceOf(first));
1984
2504
  const figureFits = m !== null && capBottom + needFor(floatGapPx, m.height, floatGapPx) <= bandBottom + 0.01;
1985
2505
  if (figureFits) {
1986
2506
  bandCapProposals.set(startIdx, {
1987
2507
  kind: 'span',
1988
2508
  startContentIndex: bandStart.contentIndex,
1989
2509
  startPart: bandStart.part,
2510
+ ...(bandStart.visit > 0 ? { startVisit: bandStart.visit } : {}),
1990
2511
  lines,
1991
2512
  retries: 0,
2513
+ ...capHeadPx(cols),
1992
2514
  });
1993
2515
  }
1994
2516
  else {
@@ -2056,8 +2578,15 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2056
2578
  }
2057
2579
  if (action) {
2058
2580
  if (capActive) {
2581
+ // The box cuts where the band's content ends. When that content
2582
+ // ran past the cap's cut (a paragraph tail that could not split
2583
+ // over it, force-placed into an empty capped column), the cap is
2584
+ // not delivered: the driver takes the cut a line lower rather than
2585
+ // keep a cut as low as that one column (EF-116).
2586
+ const overran = capClipsLines(action.fit.cols);
2059
2587
  uncapBand(action.fit.cols, uncappedBottoms);
2060
- spanPlacedInBand.add(startIdx);
2588
+ if (!overran)
2589
+ spanPlacedInBand.add(startIdx);
2061
2590
  }
2062
2591
  const placed = action.kind === 'whole' ? action.result : action.fragment.result;
2063
2592
  const to = action.kind === 'whole' ? L.end : action.fragment.to;
@@ -2066,10 +2595,14 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2066
2595
  const spanCol = closeBandAndInsertSpan(page, action.fit.cols, action.fit.cutY, placed.frame, action.need, cursor, action.fit.spacing, placed.totalHeight);
2067
2596
  commitCallout(placed, startIdx, plan, spanCol, part > 0 || action.kind === 'split' ? fragmentRange(L, from, to) : undefined);
2068
2597
  // Floats first-referenced inside the box enqueue once its head is
2069
- // committed, in reading order (same as the inline path).
2070
- if (part === 0)
2598
+ // committed, in reading order (same as the inline path); those its
2599
+ // later parts cite wait for them.
2600
+ if (part === 0) {
2071
2601
  for (let i = startIdx + 1; i <= plan.endIdx; i++)
2072
2602
  enqueueFloatsFor(i);
2603
+ gateBoxCitations(startIdx + 1, plan.endIdx);
2604
+ }
2605
+ releaseBoxCitations(action.kind === 'whole' ? plan.endIdx + 1 : L.childBase + to.child);
2073
2606
  // The new band starts on the grid right below the span column; nothing
2074
2607
  // to snap — `need` already bakes in `marginBottom`.
2075
2608
  pendingSpacing = 0;
@@ -2119,8 +2652,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2119
2652
  kind: 'span',
2120
2653
  startContentIndex: bandStart.contentIndex,
2121
2654
  startPart: bandStart.part,
2655
+ ...(bandStart.visit > 0 ? { startVisit: bandStart.visit } : {}),
2122
2656
  lines,
2123
2657
  retries: 0,
2658
+ ...capHeadPx(cols),
2124
2659
  });
2125
2660
  proposedSpan = true;
2126
2661
  }
@@ -2353,8 +2888,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2353
2888
  const used = sideUsedBottom(side);
2354
2889
  const raw = Math.max(used, refY ?? used);
2355
2890
  const gridUpSide = (v) => page.contentArea.y + Math.ceil((v - page.contentArea.y - 0.01) / baselineGrid) * baselineGrid;
2356
- let y = gridUpSide(raw);
2357
2891
  const below = Math.max(result.marginBottomPx, floatGapPx);
2892
+ // Clear of the heading-design elements standing in the column (EF-78).
2893
+ const obstacles = sideObstacles.get(side);
2894
+ let y = clearSideObstacles(gridUpSide(raw), result.totalHeight, obstacles, below, gridUpSide);
2358
2895
  let need = y - used + result.totalHeight + below;
2359
2896
  // The gap under the box is owed only to what follows: a box whose foot
2360
2897
  // lands on the column's foot needs none.
@@ -2362,16 +2899,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2362
2899
  if (!fits() && refY !== undefined && raw > used + 0.5) {
2363
2900
  // Beside its text the box runs off the column: it slides up — as far
2364
2901
  // as the stack above allows — to the lowest position that fits, its
2365
- // foot on the column's foot (the bottom-aligned marginal box).
2902
+ // foot on the column's foot (the bottom-aligned marginal box), unless
2903
+ // a heading design stands there.
2366
2904
  const foot = side.bbox.y + side.bbox.height;
2367
2905
  const fit = page.contentArea.y + Math.floor((foot - result.totalHeight - page.contentArea.y + 0.01) / baselineGrid) * baselineGrid;
2368
- if (fit >= used - 0.01) {
2906
+ if (fit >= used - 0.01 && clearSideObstacles(fit, result.totalHeight, obstacles, below, gridUpSide) === fit) {
2369
2907
  y = fit;
2370
2908
  need = y - used + result.totalHeight + below;
2371
2909
  }
2372
2910
  }
2373
2911
  if (!fits()) {
2374
- if (mode === 'strict' || used > side.bbox.y + 0.5)
2912
+ if (mode === 'strict' || used > side.bbox.y + 0.5 || (obstacles?.length ?? 0) > 0)
2375
2913
  return false;
2376
2914
  // A box that a full side column would hold waits for one rather than
2377
2915
  // overflowing an empty column a band above has shortened.
@@ -2409,11 +2947,93 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2409
2947
  return false;
2410
2948
  const curCol = currentColumn(doc, cursor);
2411
2949
  const refY = curCol.bbox.y + (curCol.bbox.height - curCol.availableHeight) + (curCol.blocks.length > 0 ? pendingSpacing : 0);
2950
+ // `sideAtColumnEnd: 'after'` (EF-161): the box waits for the text after
2951
+ // its fence, and stands beside it when the break rules send that text
2952
+ // on to the next page (a column with no room left, a paragraph the
2953
+ // widow and orphan rules move whole, a heading kept with its text). A
2954
+ // side box fenced while one waits waits too, so the side column keeps
2955
+ // their order. A box nothing follows in its chapter stays with the
2956
+ // text before it, like any box with `'before'` (postext 1.4's rule).
2957
+ // A box fenced again after a keep-with-next rewind is already waiting.
2958
+ if (awaitingSideBoxes.some((w) => w.box.startIdx === startIdx))
2959
+ return true;
2960
+ if (awaitingSideBoxes.length > 0 || (style.sideAtColumnEnd === 'after' && !nextIsBarrier(plan.endIdx + 1))) {
2961
+ awaitingSideBoxes.push({ box: { startIdx, plan, style }, pageIndex: page.index, band: currentBand(page, cursor), column: curCol, refY });
2962
+ return true;
2963
+ }
2412
2964
  if (!trySideBox(page, side, { startIdx, plan, style }, refY, 'strict')) {
2413
2965
  pendingSideBoxes.push({ startIdx, plan, style });
2414
2966
  }
2415
2967
  return true;
2416
2968
  };
2969
+ /** The first block after content index `endIdx` that the flow holds in a
2970
+ * column: the first part of the block that follows a side box's fence. */
2971
+ const firstFlowBlockAfter = (endIdx) => {
2972
+ let first;
2973
+ for (const b of doc.blocks) {
2974
+ const ci = b.contentIndex;
2975
+ if (ci === undefined || ci <= endIdx || (first !== undefined && first.contentIndex <= ci))
2976
+ continue;
2977
+ const col = doc.pages[b.pageIndex]?.columns[b.columnIndex];
2978
+ if (!col || col.kind === 'side' || !col.blocks.includes(b))
2979
+ continue;
2980
+ first = b;
2981
+ }
2982
+ return first;
2983
+ };
2984
+ /**
2985
+ * Set the side boxes waiting for the text after their fence
2986
+ * (`awaitingSideBoxes`), in order. Called once the flow holds a block
2987
+ * after the last of them that it will not take back — a heading kept
2988
+ * with its text can still move on with it — and with `force` where the
2989
+ * flow segment ends. A box with `sideAtColumnEnd: 'after'` whose text
2990
+ * went on in another column or on a later page stands level with that
2991
+ * text's first block, in the side column there; every other box stands
2992
+ * at its fence, as it would have been set there. One the side column
2993
+ * cannot hold takes the side column of a later page.
2994
+ */
2995
+ const settleAwaitingSideBoxes = (force) => {
2996
+ if (awaitingSideBoxes.length === 0)
2997
+ return;
2998
+ const lastEnd = awaitingSideBoxes[awaitingSideBoxes.length - 1].box.plan.endIdx;
2999
+ if (!force) {
3000
+ const held = doc.blocks.some((b) => {
3001
+ if ((b.contentIndex ?? -1) <= lastEnd || isFreeHeading(b))
3002
+ return false;
3003
+ const col = doc.pages[b.pageIndex]?.columns[b.columnIndex];
3004
+ return col !== undefined && col.kind !== 'side' && col.blocks.includes(b);
3005
+ });
3006
+ if (!held)
3007
+ return;
3008
+ }
3009
+ for (const w of awaitingSideBoxes.splice(0)) {
3010
+ let { pageIndex, band, refY } = w;
3011
+ if (w.box.style.sideAtColumnEnd === 'after') {
3012
+ const anchor = firstFlowBlockAfter(w.box.plan.endIdx);
3013
+ const anchorPage = anchor ? doc.pages[anchor.pageIndex] : undefined;
3014
+ const col = anchorPage?.columns[anchor.columnIndex];
3015
+ if (anchorPage && col && col !== w.column && col.kind !== 'span' && sideColumnOf(anchorPage, col.band ?? 0)) {
3016
+ pageIndex = anchorPage.index;
3017
+ band = col.band ?? 0;
3018
+ refY = anchor.bbox.y;
3019
+ }
3020
+ }
3021
+ const page = doc.pages[pageIndex];
3022
+ const side = sideColumnOf(page, band);
3023
+ if (side && trySideBox(page, side, w.box, refY, 'strict'))
3024
+ continue;
3025
+ // The pages the flow has opened since: their side columns, from the
3026
+ // top, as `flushSideBoxesIntoPage` would have set it there.
3027
+ let placed = false;
3028
+ for (let p = pageIndex + 1; p <= cursor.pageIndex && !placed; p++) {
3029
+ const later = doc.pages[p];
3030
+ const laterSide = sideColumnOf(later, 0);
3031
+ placed = laterSide !== undefined && trySideBox(later, laterSide, w.box, undefined, 'fresh');
3032
+ }
3033
+ if (!placed)
3034
+ pendingSideBoxes.push(w.box);
3035
+ }
3036
+ };
2417
3037
  /** Set the waiting side boxes in the side column of a freshly opened
2418
3038
  * page, in order; one that does not fit an empty column is set anyway. */
2419
3039
  const flushSideBoxesIntoPage = (page) => {
@@ -2458,6 +3078,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2458
3078
  // Fixed boxes leave the flow entirely.
2459
3079
  if (placement === 'fixed') {
2460
3080
  placeCalloutFixed(startIdx, plan, style, layoutAt);
3081
+ calloutsOutOfFlow.add(startIdx);
2461
3082
  return undefined;
2462
3083
  }
2463
3084
  // Floated boxes leave it too: they take the first free band after
@@ -2475,6 +3096,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2475
3096
  && splitCalloutFragment(L, CUT_START, width, contentArea.height, 'float-probe', false, style.splitMinLines) !== null;
2476
3097
  if (!inFlow) {
2477
3098
  enqueueCalloutFloat(startIdx, plan, style, L, placement, span);
3099
+ calloutsOutOfFlow.add(startIdx);
2478
3100
  return undefined;
2479
3101
  }
2480
3102
  }
@@ -2491,6 +3113,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2491
3113
  // Side boxes stack in the float-only side column beside the text
2492
3114
  // (whatever their placement: a side box never floats to a band).
2493
3115
  if (span === 'side' && placeCalloutSide(startIdx, plan, style)) {
3116
+ calloutsOutOfFlow.add(startIdx);
2494
3117
  return undefined;
2495
3118
  }
2496
3119
  }
@@ -2500,6 +3123,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2500
3123
  /** Times the box left an EMPTY short column (see below) — bounded. */
2501
3124
  let shortColumnMoves = 0;
2502
3125
  for (;;) {
3126
+ // A box (or the rest of a split one) that opens a band applies the
3127
+ // band's cap before it measures its room, as a paragraph does: sized
3128
+ // against the uncut column, a continuation ran past the cut of a
3129
+ // band levelled for a page-span box after it (EF-126).
3130
+ enterBand(startIdx, part);
2503
3131
  let curCol = currentColumn(doc, cursor);
2504
3132
  const continuation = part > 0;
2505
3133
  const result = L.layoutRange(from, L.end, curCol.bbox.width, frameId, continuation, mirroredOf(doc.pages[cursor.pageIndex]));
@@ -2511,7 +3139,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2511
3139
  const balanceBefore = curCol.blocks.length > 0 || reservedOf(curCol).top > 0
2512
3140
  ? (balanceExtraPx?.get(balanceKey(startIdx, part)) ?? 0)
2513
3141
  : 0;
2514
- const spacing = (curCol.blocks.length === 0 ? 0 : Math.max(pendingSpacing, result.marginTopPx)) + balanceBefore;
3142
+ const spacing = (curCol.blocks.length === 0 ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), result.marginTopPx)) + balanceBefore;
2515
3143
  const roomPx = curCol.availableHeight - spacing;
2516
3144
  let fragment = null;
2517
3145
  if (result.totalHeight > roomPx + 0.01) {
@@ -2559,7 +3187,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2559
3187
  }
2560
3188
  pendingSpacing = 0;
2561
3189
  if (part === 0 && resolved.headings.keepWithNext && run > 0 && run < curCol.blocks.length) {
2562
- const rolledBack = rollbackTrailingBlocks(curCol, doc.blocks, isFreeHeading);
3190
+ const rolledBack = rollbackHeadings(curCol);
2563
3191
  advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
2564
3192
  return (rolledBack[0].contentIndex ?? startIdx - rolledBack.length) - 1;
2565
3193
  }
@@ -2582,27 +3210,33 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2582
3210
  }
2583
3211
  // Floats first-referenced inside the box still enqueue in reading order
2584
3212
  // (only once the box is committed, so a keep-with-next replay does not
2585
- // enqueue them twice).
2586
- if (part === 0)
3213
+ // enqueue them twice); those a later part of a split box cites wait
3214
+ // for that part.
3215
+ if (part === 0) {
2587
3216
  for (let i = startIdx + 1; i <= plan.endIdx; i++)
2588
3217
  enqueueFloatsFor(i);
3218
+ gateBoxCitations(startIdx + 1, plan.endIdx);
3219
+ }
2589
3220
  const placed = fragment ? fragment.result : result;
2590
3221
  const to = fragment ? fragment.to : L.end;
2591
3222
  if (part > 0 || fragment)
2592
3223
  markFragment(placed, part, fragment !== null);
2593
- const spacingBefore = (curCol.blocks.length === 0 ? 0 : Math.max(pendingSpacing, placed.marginTopPx)) + balanceBefore;
2594
- if (balanceBefore > 0)
3224
+ const spacingBefore = (curCol.blocks.length === 0 ? 0 : Math.max(pendingBeforeSelfSnapping(startIdx), placed.marginTopPx)) + balanceBefore;
3225
+ if (balanceBefore > 0) {
2595
3226
  addBalanceExtra(curCol, balanceBefore);
3227
+ placed.frame.balancing = { levers: ['trailingCallout'], spaceAbove: balanceBefore };
3228
+ }
2596
3229
  // Spacing collapses at a column top, so the lever's push under a
2597
3230
  // float band is consumed here: the box then opens that far down.
2598
3231
  if (balanceBefore > 0 && curCol.blocks.length === 0) {
2599
3232
  curCol.availableHeight = Math.max(0, curCol.availableHeight - balanceBefore);
2600
3233
  }
2601
- enterBand(startIdx, 0);
3234
+ enterBand(startIdx, part);
2602
3235
  placeAtomicBlock(placed.frame, placed.totalHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx);
2603
- enterBand(startIdx, 0);
3236
+ enterBand(startIdx, part);
2604
3237
  curCol = currentColumn(doc, cursor);
2605
3238
  commitCallout(placed, startIdx, plan, curCol, part > 0 || fragment ? fragmentRange(L, from, to) : undefined);
3239
+ releaseBoxCitations(fragment ? L.childBase + to.child : plan.endIdx + 1);
2606
3240
  // Snap the flow after the box to the baseline grid, baking in at least
2607
3241
  // `marginBottom` (grid wins, margin is a minimum — the resource rule).
2608
3242
  // An off-grid style (`snapToGrid: false`) keeps its exact margin
@@ -2625,6 +3259,40 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2625
3259
  advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
2626
3260
  }
2627
3261
  };
3262
+ /**
3263
+ * `bodyText.keepColonWithList` with `colonListRoom: 'item'` (EF-110):
3264
+ * whether the first item of the list after the lead-in paragraph
3265
+ * `leadIdx` can start in `roomPx` of column under it — it fits whole, or
3266
+ * the orphan and widow rules for lists would leave some of its lines
3267
+ * there, as they do when it is set.
3268
+ */
3269
+ const firstListItemStarts = (leadIdx, roomPx, width, ctx) => {
3270
+ let idx = leadIdx + 1;
3271
+ while (idx < contentBlocks.length && isMarkerBlock(contentBlocks[idx]))
3272
+ idx++;
3273
+ const raw = contentBlocks[idx];
3274
+ if (!raw || raw.type !== 'listItem')
3275
+ return true;
3276
+ const item = measureContentBlock(raw, idx, width, ctx);
3277
+ if (!item)
3278
+ return true;
3279
+ const lines = item.measured.lines.length;
3280
+ const fit = Math.floor((roomPx + FIT_EPS) / item.kind.style.lineHeightPx);
3281
+ if (lines <= fit)
3282
+ return true;
3283
+ if (fit < 1)
3284
+ return false;
3285
+ const body = resolved.bodyText;
3286
+ return chooseParagraphSplit(lines, fit, {
3287
+ avoidOrphans: body.avoidOrphans && body.avoidOrphansInLists,
3288
+ orphanMinLines: body.orphanMinLines,
3289
+ orphanPenalty: body.orphanPenalty,
3290
+ avoidWidows: body.avoidWidows && body.avoidWidowsInLists,
3291
+ widowMinLines: body.widowMinLines,
3292
+ widowPenalty: body.widowPenalty,
3293
+ slackWeight: body.slackWeight,
3294
+ }).splitAt > 0;
3295
+ };
2628
3296
  for (let blockIdx = 0; blockIdx < contentBlocks.length; blockIdx++) {
2629
3297
  if (options?.shouldCancel?.())
2630
3298
  throw new BuildCancelledError();
@@ -2639,7 +3307,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2639
3307
  // free slot of the current page now — after their reference in reading
2640
3308
  // order. Then enqueue the floats first-referenced in this block, so the
2641
3309
  // next page opened while placing it (or any later block) reserves their
2642
- // band and the next iteration offers them the slots that follow.
3310
+ // band and the next iteration offers them the slots that follow. A text
3311
+ // block holds its floats back until the line citing them is placed
3312
+ // (`gateCitedFloats`).
3313
+ settleAwaitingSideBoxes(false);
3314
+ releaseStaleCitationGates(blockIdx);
2643
3315
  tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx), blockIdx);
2644
3316
  enqueueFloatsFor(blockIdx);
2645
3317
  // --- Directives ----------------------------------------------------
@@ -2693,8 +3365,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2693
3365
  }
2694
3366
  else if (name === 'numbering') {
2695
3367
  const change = {};
2696
- const fmt = attrs.format;
2697
- if (fmt && ALLOWED_PAGE_FORMATS.has(fmt))
3368
+ // Any spelling of a format (`roman-lower`, `arabic`, `i`…); an
3369
+ // unknown one keeps the current format.
3370
+ const fmt = parseNumberFormat(attrs.format);
3371
+ if (fmt)
2698
3372
  change.format = fmt;
2699
3373
  if (attrs.startAt !== undefined) {
2700
3374
  const n = Number(attrs.startAt);
@@ -2711,9 +3385,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2711
3385
  // --- Container markers ---------------------------------------------
2712
3386
  // `:::paragraphs` applies its style's top margin on entry through the
2713
3387
  // pending-spacing mechanism (collapses like any margin, vanishes at a
2714
- // column top). Its bottom margin is normally baked into the last
2715
- // paragraph's grid snap; the pending-spacing fallback covers containers
2716
- // that end with a non-paragraph block. Replaying a marker after a keep-with-next rewind is
3388
+ // column top). Its bottom margin is normally set by the last
3389
+ // paragraph (baked into its grid snap, or carried past it with
3390
+ // `paragraphContainerSpacing: 'collapse'`); the pending-spacing
3391
+ // fallback covers containers that end with a non-paragraph block. Replaying a marker after a keep-with-next rewind is
2717
3392
  // harmless: the container plan is index-based, and `max` is idempotent.
2718
3393
  // `:::part`: the opener lives on a dedicated single-column page. On
2719
3394
  // entry, break to a fresh page of the configured parity and convert it
@@ -2813,6 +3488,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2813
3488
  const pc = rawBlock.containerId !== undefined
2814
3489
  ? paragraphContainers.byId.get(rawBlock.containerId)
2815
3490
  : undefined;
3491
+ // What a container tail still owes passes through the closing
3492
+ // markers right after it (see `inlineGapOwed`).
3493
+ const owed = rawBlock.type === 'containerEnd' && inlineGapOwed?.container
3494
+ && inlineGapOwed.afterIdx === blockIdx - 1 && pendingSpacing === inlineGapOwed.pending
3495
+ ? inlineGapOwed
3496
+ : null;
3497
+ let owedExact = owed?.exact ?? 0;
2816
3498
  if (pc) {
2817
3499
  // Margins collapse with the pending spacing; a negative one pulls
2818
3500
  // the flow up past it instead (the container starts inside the
@@ -2823,9 +3505,13 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2823
3505
  pendingSpacing = collapse(pc.marginTopPx);
2824
3506
  }
2825
3507
  else if (contentBlocks[blockIdx - 1]?.type !== 'paragraph') {
2826
- pendingSpacing = collapse(pc.marginBottomPx);
3508
+ const margin = pc.marginBottomPx;
3509
+ owedExact = margin < 0 ? owedExact + margin : Math.max(owedExact, margin);
3510
+ pendingSpacing = collapse(margin);
2827
3511
  }
2828
3512
  }
3513
+ if (owed)
3514
+ inlineGapOwed = { afterIdx: blockIdx, pending: pendingSpacing, exact: owedExact, container: true };
2829
3515
  continue;
2830
3516
  }
2831
3517
  // --- Heading `breakBefore` ----------------------------------------
@@ -2874,8 +3560,24 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2874
3560
  // to place inline (empty text, unknown resource id, floated resource).
2875
3561
  const col = currentColumn(doc, cursor);
2876
3562
  const blockMeasureCtx = partPlan.byBlock[blockIdx] ? partMeasureCtx : sectionMeasureCtx(sectionPlan.byBlock[blockIdx]);
3563
+ /** The space under a container that closes on a paragraph merges with
3564
+ * the next block's own (`bodyText.paragraphContainerSpacing:
3565
+ * 'collapse'`, EF-159 / EF-181): it is at least the paragraph spacing
3566
+ * of the text around the container, and a snapped tail carries it as
3567
+ * pending spacing instead of baking it into the snap. A negative
3568
+ * `marginBottom` pulls the flow up as in 1.4. A container that closes
3569
+ * on anything else (a list) is set as in 1.4 under either rule: the
3570
+ * block keeps its own space in its snap, and the container's
3571
+ * `marginBottom` follows as pending spacing from the closing marker. */
3572
+ const containerTail = isContainerTail && rawBlock.type === 'paragraph' ? paragraphContainer : undefined;
3573
+ const collapseTail = containerTail !== undefined
3574
+ && resolved.bodyText.paragraphContainerSpacing === 'collapse'
3575
+ && containerTail.tailStyle.marginBottomPx >= 0;
3576
+ const tailStyle = collapseTail && blockMeasureCtx.bodyStyle.marginBottomPx > containerTail.tailStyle.marginBottomPx
3577
+ ? { ...containerTail.tailStyle, marginBottomPx: blockMeasureCtx.bodyStyle.marginBottomPx }
3578
+ : containerTail?.tailStyle;
2877
3579
  const styleOverride = paragraphContainer
2878
- ? (isContainerTail ? paragraphContainer.tailStyle : paragraphContainer.style)
3580
+ ? (isContainerTail ? tailStyle ?? paragraphContainer.tailStyle : paragraphContainer.style)
2879
3581
  : undefined;
2880
3582
  // Column balancing "run a paragraph long": the loose path is taken only
2881
3583
  // for the paragraphs the driver asked for, so the common case keeps its
@@ -2890,9 +3592,16 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2890
3592
  && knownOutcome === undefined
2891
3593
  && (looseGained.get(budget.group) ?? 0) >= budget.need;
2892
3594
  const tryLoose = looseLines !== undefined && !budgetMet && knownOutcome !== null;
3595
+ // A page-span heading without a design of its own is painted by the
3596
+ // default opener across the content area: measured at that width, its
3597
+ // band holds the title as it is painted (EF-100). A designed opener
3598
+ // keeps the column-width measure: its design sets the band.
3599
+ const measureWidth = rawBlock.type === 'heading' && opensDefaultOpener(rawBlock)
3600
+ ? doc.pages[cursor.pageIndex].contentArea.width
3601
+ : col.bbox.width;
2893
3602
  let measuredBlock = tryLoose
2894
3603
  ? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome)
2895
- : measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride });
3604
+ : measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, { styleOverride });
2896
3605
  // Screen pages (`layout.fitFiguresToPage`): an inline figure a little
2897
3606
  // too tall for the room left in its column — under an opener band, say
2898
3607
  // — is set smaller to stay with its text rather than leave the rest of
@@ -2930,8 +3639,16 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2930
3639
  }
2931
3640
  if (!measuredBlock)
2932
3641
  continue;
2933
- const { kind, contentBlock, measured, prefixLen, absoluteSourceMap, mathDisplayRender, letterSpacingPx } = measuredBlock;
2934
- const { style, vdtType, headingLevel, numberPrefix, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
3642
+ const { kind, contentBlock, measured, prefixLen, absoluteSourceMap, mathDisplayRender } = measuredBlock;
3643
+ /** Tracking the block's lines are set with; a paragraph moved whole into
3644
+ * a column of another width is measured again, and may change it. */
3645
+ let { letterSpacingPx } = measuredBlock;
3646
+ const { vdtType, headingLevel, numberPrefix, headingNumber, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
3647
+ // A structural heading (`hidden`) is placed like any heading — its
3648
+ // break, keep-with-next and grid snap apply — but with no line height
3649
+ // and no margins: it takes no room, and renderers skip it.
3650
+ const hiddenHeading = vdtType === 'heading' && headingIsHidden(rawBlock, headingLevels.forBlock(rawBlock));
3651
+ const style = hiddenHeading ? { ...kind.style, lineHeightPx: 0, marginTopPx: 0, marginBottomPx: 0 } : kind.style;
2935
3652
  // --- Resource blocks (image / svg / table + caption) -----------------
2936
3653
  // Placed atomically (kept-together) — no mid-content split for v1.
2937
3654
  if (vdtType === 'resource') {
@@ -2959,7 +3676,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2959
3676
  // float would, unless the block before asked for more.
2960
3677
  const spacingBefore = Math.max(pendingSpacing, floatGapPx);
2961
3678
  enterBand(blockIdx, 0);
2962
- placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx);
3679
+ placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(groupHeight));
2963
3680
  enterBand(blockIdx, 0);
2964
3681
  // `placeBlockInColumn` (inside placeAtomicBlock) shifts `blk.lines`; the
2965
3682
  // resource's own caption/table lines live on `resourceBlock` and must be
@@ -2970,14 +3687,25 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
2970
3687
  // group height is arbitrary), baking in at least marginBottom — same
2971
3688
  // convention as snapped headings — so the following text lands back on
2972
3689
  // the global grid instead of inheriting the resource's offset.
3690
+ // The white under it then makes up the float gap, as above it (EF-93):
3691
+ // what the snap left short of that gap, in whole grid lines, is carried
3692
+ // as pending spacing, so it collapses with the next block's own space
3693
+ // above — a heading's, a list's or a box's top margin, the next inline
3694
+ // resource's float gap — and vanishes at a column foot, where the
3695
+ // column is full either way. `layout.inlineResourceGap: 'above'` keeps
3696
+ // 1.4's rule, the snap alone.
2973
3697
  {
2974
3698
  const rCol = currentColumn(doc, cursor);
2975
3699
  const usedHeight = rCol.bbox.height - rCol.availableHeight;
2976
3700
  const naturalBottom = usedHeight + style.marginBottomPx;
2977
3701
  const snappedBottom = Math.ceil((naturalBottom - 0.01) / baselineGrid) * baselineGrid;
2978
3702
  rCol.availableHeight = Math.max(0, rCol.bbox.height - snappedBottom);
3703
+ const owed = resolved.layout.inlineResourceGap === 'above'
3704
+ ? 0
3705
+ : floatGapPx - (snappedBottom - usedHeight);
3706
+ pendingSpacing = owed > 0.01 ? Math.ceil((owed - 0.01) / baselineGrid) * baselineGrid : 0;
3707
+ inlineGapOwed = pendingSpacing > 0 ? { afterIdx: blockIdx, pending: pendingSpacing, exact: owed } : null;
2979
3708
  }
2980
- pendingSpacing = 0;
2981
3709
  flushPendingNumberingAtBoundary();
2982
3710
  continue;
2983
3711
  }
@@ -3007,15 +3735,25 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3007
3735
  blk.strikethroughText = true;
3008
3736
  // Bullet Y = x-height midpoint of the item's first text line.
3009
3737
  // Pairs with `textBaseline='middle'` at render so the bullet stays
3010
- // visually centered on the text regardless of its own font size.
3738
+ // visually centered on the text regardless of its own font size. A
3739
+ // contents number sits on the line's baseline instead.
3011
3740
  const firstLine = blk.lines[0];
3012
3741
  if (firstLine) {
3013
- blk.bulletY = firstLine.baseline - listBullet.textFontSizePx * 0.3 + listBullet.verticalOffsetPx;
3742
+ const at = listBulletPosition(listBullet, firstLine.baseline);
3743
+ blk.bulletY = at.bulletY;
3744
+ if (at.bulletBaselineY !== undefined)
3745
+ blk.bulletBaselineY = at.bulletBaselineY;
3014
3746
  }
3015
3747
  };
3016
3748
  // Neighbour lookaheads see through container markers.
3017
3749
  const nextBlock = nextNonMarkerBlock(contentBlocks, blockIdx) ?? null;
3018
3750
  const nextIsListItem = nextBlock?.type === 'listItem';
3751
+ /** The space before the next item of the list (EF-165): the item
3752
+ * spacing of the shallower list, so the item after a nested list
3753
+ * takes its own list's spacing, not the nested one's. */
3754
+ const itemGapPx = (bullet) => (nextBlock?.type === 'listItem'
3755
+ ? listItemGapPx({ spacingPx: bullet.itemSpacingPx, depth: listDepth ?? 1 }, { spacingPx: listItemSpacingPx(nextBlock, blockMeasureCtx.resolved), depth: nextBlock.depth ?? 1 })
3756
+ : bullet.itemSpacingPx);
3019
3757
  // For headings, only snap to baseline grid if the next block is NOT a heading.
3020
3758
  // Consecutive headings flow without grid snapping; the last heading in the
3021
3759
  // group snaps so that the following body text realigns with the grid.
@@ -3026,10 +3764,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3026
3764
  // are off-grid by design.
3027
3765
  const nextIsHeading = nextBlock?.type === 'heading';
3028
3766
  // The contents (`:::toc`) keep their own rhythm: an entry set as a list
3029
- // item is not a list tail to realign the text after it.
3030
- const shouldSnapToGrid = rawBlock.toc === undefined && ((vdtType === 'heading' && !nextIsHeading && resolved.headings.snapToGrid) ||
3767
+ // item is not a list tail to realign the text after it. A heading snaps
3768
+ // per its level (or style), which inherits `headings.snapToGrid`.
3769
+ const shouldSnapToGrid = rawBlock.toc === undefined && ((vdtType === 'heading' && !nextIsHeading
3770
+ && (headingLevels.forBlock(rawBlock)?.snapToGrid ?? resolved.headings.snapToGrid)) ||
3031
3771
  (vdtType === 'listItem' && !nextIsListItem) ||
3032
- (vdtType === 'paragraph' && isContainerTail) ||
3772
+ (vdtType === 'paragraph' && isContainerTail && paragraphContainer?.snapToGrid !== false) ||
3033
3773
  vdtType === 'mathDisplay');
3034
3774
  // Place block, splitting across columns/pages if needed.
3035
3775
  // List items may split too — orphan/widow protection per-list is gated by
@@ -3039,10 +3779,42 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3039
3779
  // joints, a last word that cannot come up) is set ragged, not stretched.
3040
3780
  let remainingLines = [...raggedLooseLines(measured.lines, style.textAlign)];
3041
3781
  let partIndex = 0;
3782
+ // The floats this block cites wait for the line that cites them.
3783
+ gateCitedFloats(blockIdx, remainingLines);
3042
3784
  /** Times this block left an EMPTY short column (see `shortColumn`) —
3043
3785
  * bounded so a page whose columns are all short (footnotes, design
3044
3786
  * bands) cannot make it wander forever. */
3045
3787
  let shortColumnMoves = 0;
3788
+ /** Whether the paragraph was already re-broken to keep a hyphen off
3789
+ * the last line of a column (`bodyText.hyphenateAcrossColumns`). */
3790
+ let columnEndHyphenRetried = false;
3791
+ /** `hyphenateAcrossColumns: false`: the paragraph's lines as set now
3792
+ * (a re-break replaces them) and where the breaker broke them, the
3793
+ * lines (1-based) its last re-break kept a hyphen off, and the part
3794
+ * last re-broken from. */
3795
+ let paragraphLines = remainingLines;
3796
+ let paragraphBreaks = measured.breaks;
3797
+ let guardedEnds = [];
3798
+ let columnEndHyphenPart = -1;
3799
+ /** The column width the block was broken for, and the columns of other
3800
+ * widths its later lines were broken for since (EF-157). A text block
3801
+ * that goes on into a column of another width — a one-and-a-half
3802
+ * layout with text in both columns, a styled section with its own
3803
+ * margins — is broken again there: its placed lines stay, the rest is
3804
+ * broken for the new column. Not the loose paragraphs balancing runs
3805
+ * long (placed whole where they were measured), nor a heading the
3806
+ * default opener paints across the page. */
3807
+ let brokenForWidth = measureWidth;
3808
+ let laterWidths = [];
3809
+ const rebreaksForColumn = !tryLoose && rawBlock.toc === undefined
3810
+ && (canSplit || (vdtType === 'heading' && !opensDefaultOpener(rawBlock)));
3811
+ /** Measure the block again for the columns it is set in (see
3812
+ * `laterWidths`), with `extra` options. */
3813
+ const measureForColumns = (extra) => measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
3814
+ styleOverride,
3815
+ ...(laterWidths.length > 0 ? { restColumnWidths: laterWidths } : {}),
3816
+ ...extra,
3817
+ });
3046
3818
  // "Keep with next" for colon-introduced lists: a paragraph ending in `:`
3047
3819
  // followed directly by a list acts as a lead-in title — the colon-bearing
3048
3820
  // line must share a column with the first list item. Only checked for the
@@ -3055,14 +3827,70 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3055
3827
  while (remainingLines.length > 0) {
3056
3828
  enterBand(blockIdx, partIndex);
3057
3829
  const curCol = currentColumn(doc, cursor);
3830
+ // The rest of the block goes into a column of another width than the
3831
+ // one it was broken for (EF-157): break it again for this one. Up to
3832
+ // postext 1.4 it kept the lines of the column it started in, and a
3833
+ // line set for the wide column ran past the narrow one (clipped).
3834
+ const restWidth = laterWidths.length > 0 ? laterWidths[laterWidths.length - 1].columnWidth : brokenForWidth;
3835
+ if (rebreaksForColumn && Math.abs(curCol.bbox.width - restWidth) > 0.5) {
3836
+ const placed = paragraphLines.length - remainingLines.length;
3837
+ if (placed === 0) {
3838
+ // Nothing placed yet: the whole block, for this column.
3839
+ const again = measureContentBlock(rawBlock, blockIdx, curCol.bbox.width, blockMeasureCtx, { styleOverride });
3840
+ if (again && again.measured.lines.length > 0) {
3841
+ brokenForWidth = curCol.bbox.width;
3842
+ laterWidths = [];
3843
+ letterSpacingPx = again.letterSpacingPx;
3844
+ remainingLines = [...raggedLooseLines(again.measured.lines, style.textAlign)];
3845
+ paragraphLines = remainingLines;
3846
+ paragraphBreaks = again.measured.breaks;
3847
+ guardedEnds = [];
3848
+ columnEndHyphenRetried = false;
3849
+ regateCitedFloats(blockIdx, remainingLines);
3850
+ }
3851
+ }
3852
+ else {
3853
+ // The placed lines keep their breaks; the rest is broken for
3854
+ // this column. Taken only when the placed lines come out as they
3855
+ // were (a break trace from the other measuring path cannot keep
3856
+ // them) and with the same tracking.
3857
+ const steps = [...laterWidths, { fromLine: placed, columnWidth: curCol.bbox.width }];
3858
+ const keepBreaks = paragraphBreaks && paragraphBreaks.at.length >= placed
3859
+ ? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
3860
+ : undefined;
3861
+ const again = measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
3862
+ styleOverride, restColumnWidths: steps, ...(keepBreaks ? { keepBreaks } : {}),
3863
+ });
3864
+ const lines = again && (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0)
3865
+ ? [...raggedLooseLines(again.measured.lines, style.textAlign)]
3866
+ : undefined;
3867
+ if (lines && lines.length > placed
3868
+ && paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated)) {
3869
+ laterWidths = steps;
3870
+ paragraphLines = lines;
3871
+ paragraphBreaks = again.measured.breaks;
3872
+ remainingLines = lines.slice(placed);
3873
+ regateCitedFloats(blockIdx, lines);
3874
+ }
3875
+ }
3876
+ }
3058
3877
  const isFirstInColumn = curCol.blocks.length === 0;
3878
+ /** Under a page-span opener: where this column's first block goes. A
3879
+ * heading, a display formula or a contents row keeps level with the
3880
+ * first block under the opener; text starts on the grid. */
3881
+ const openerHead = isFirstInColumn
3882
+ ? openerHeadSpacing(curCol, vdtType === 'heading' || vdtType === 'mathDisplay' || rawBlock.toc !== undefined)
3883
+ : undefined;
3059
3884
  // Compute spacing before this block — margin collapsing between
3060
3885
  // consecutive headings: only the larger of marginBottom / marginTop applies
3061
3886
  let spacingBefore = 0;
3887
+ /** Column balancing: the levers applied to this placement, stamped
3888
+ * on the block it places (`VDTBlock.balancing`). */
3889
+ let balancing;
3062
3890
  if (!isFirstInColumn) {
3063
3891
  spacingBefore = pendingSpacing;
3064
3892
  if (vdtType === 'heading' || vdtType === 'mathDisplay') {
3065
- spacingBefore = Math.max(spacingBefore, style.marginTopPx);
3893
+ spacingBefore = Math.max(partIndex === 0 ? pendingBeforeSelfSnapping(blockIdx) : spacingBefore, style.marginTopPx);
3066
3894
  // Column balancing: extra whole grid lines above this heading so
3067
3895
  // the column it sits in ends flush with the page bottom. Applied
3068
3896
  // after margin collapsing — the heading's effective top margin
@@ -3074,6 +3902,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3074
3902
  if (extraPx) {
3075
3903
  spacingBefore += extraPx;
3076
3904
  addBalanceExtra(curCol, extraPx);
3905
+ balancing = { levers: ['heading'], spaceAbove: extraPx };
3077
3906
  }
3078
3907
  }
3079
3908
  }
@@ -3081,6 +3910,17 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3081
3910
  const prevWasList = prevNonMarkerBlock(contentBlocks, blockIdx)?.type === 'listItem';
3082
3911
  if (!prevWasList) {
3083
3912
  spacingBefore = Math.max(spacingBefore, style.marginTopPx);
3913
+ // `snapTopToGrid` (EF-103): the space above rounds up so the
3914
+ // first item lands on the grid, the margin a minimum. Read from
3915
+ // the block's own context, since a part or a styled section may
3916
+ // override its lists.
3917
+ const listsCfg = blockMeasureCtx.resolved;
3918
+ const lists = listKind === 'ordered' ? listsCfg.orderedLists : listsCfg.unorderedLists;
3919
+ if (partIndex === 0 && lists.snapTopToGrid) {
3920
+ const usedHeight = curCol.bbox.height - curCol.availableHeight;
3921
+ const top = Math.ceil((usedHeight + spacingBefore - 0.01) / baselineGrid) * baselineGrid;
3922
+ spacingBefore = top - usedHeight;
3923
+ }
3084
3924
  }
3085
3925
  }
3086
3926
  else if (rawBlock.toc) {
@@ -3097,25 +3937,43 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3097
3937
  if (extraPx) {
3098
3938
  spacingBefore += extraPx;
3099
3939
  addBalanceExtra(curCol, extraPx);
3940
+ // The block the lever pushes down follows a list's end, or a
3941
+ // display formula / box (the two candidates the gap collector
3942
+ // offers a non-heading block that does not open its column).
3943
+ const prev = curCol.blocks[curCol.blocks.length - 1];
3944
+ const afterList = prev?.type === 'listItem' && prev.containerId === undefined;
3945
+ balancing = { levers: [afterList ? 'listEnd' : 'afterDisplay'], spaceAbove: extraPx };
3100
3946
  }
3101
3947
  }
3102
3948
  }
3949
+ else if (openerHead !== undefined) {
3950
+ // The head of a column under a page-span opener (EF-139).
3951
+ spacingBefore = openerHead;
3952
+ }
3103
3953
  else if (reservedOf(curCol).top > 0) {
3104
3954
  // Column balancing: extra grid lines between the float band at the
3105
3955
  // head of this column and its first block (the after-float lever).
3106
3956
  // A paragraph resuming from the column before is levered on its own
3107
3957
  // fragment, so the room lands under the figure and not back at the
3108
- // paragraph's start.
3958
+ // paragraph's start. A heading there is a heading lever.
3109
3959
  const extraPx = balanceExtraPx?.get(balanceKey(blockIdx, partIndex));
3110
3960
  if (extraPx) {
3111
3961
  spacingBefore += extraPx;
3112
3962
  addBalanceExtra(curCol, extraPx);
3963
+ balancing = { levers: [vdtType === 'heading' ? 'heading' : 'afterFloat'], spaceAbove: extraPx };
3113
3964
  }
3114
3965
  }
3115
3966
  // A loose paragraph's extra line is balancing height too (it lands
3116
3967
  // whole in this column — loose candidates are never split parts).
3117
- if (partIndex === 0 && tryLoose && looseLines !== undefined && typeof looseOutcome.get(blockIdx) === 'number') {
3968
+ const looseTracking = looseOutcome.get(blockIdx);
3969
+ if (partIndex === 0 && tryLoose && looseLines !== undefined && typeof looseTracking === 'number') {
3118
3970
  addBalanceExtra(curCol, looseLines * style.lineHeightPx);
3971
+ balancing = {
3972
+ levers: [...(balancing?.levers ?? []), 'looseParagraph'],
3973
+ spaceAbove: balancing?.spaceAbove ?? 0,
3974
+ extraLines: looseLines,
3975
+ tracking: looseTracking,
3976
+ };
3119
3977
  }
3120
3978
  const effectiveAvailable = curCol.availableHeight - spacingBefore;
3121
3979
  // A hair of tolerance: room of exactly N lines (grid arithmetic in
@@ -3132,24 +3990,44 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3132
3990
  // actual content bottom so the reserved block height (and the
3133
3991
  // subsequent marginBottom + grid snap) starts from there.
3134
3992
  let effectiveRemainHeight = totalRemainHeight;
3135
- if (vdtType === 'heading' && headingLevel !== undefined && partIndex === 0) {
3993
+ /** The heading's design and the values it is laid out with, kept for
3994
+ * the side-column obstacles once the block is placed (EF-78). */
3995
+ let headingDesign;
3996
+ if (vdtType === 'heading' && headingLevel !== undefined && partIndex === 0 && !hiddenHeading) {
3136
3997
  const lvl = headingLevels.forBlock(rawBlock);
3137
3998
  if (lvl) {
3138
- const full = remainingLines
3139
- .map((ln) => (ln.segments ?? []).map((s) => s.text).join(''))
3140
- .join(' ');
3141
3999
  const pref = numberPrefix ?? '';
3142
- const title = applyTitleBreaks(pref && full.startsWith(`${pref} `) ? full.slice(pref.length + 1) : full, rawBlock.titleBreaks, rawBlock.text.length);
4000
+ // The lines joined back into the title (EF-162), as the design is
4001
+ // painted (`buildHeadersAndFooters`).
4002
+ const title = headingTitleText(remainingLines, pref, rawBlock.titleBreaks, rawBlock.text.length);
3143
4003
  // Span-page openers lay out across the full content area (both
3144
4004
  // columns); in-column headings use just the column width.
3145
4005
  const pageArea = doc.pages[cursor.pageIndex].contentArea;
3146
4006
  const measureWidth = lvl.span === 'page' ? pageArea.width : curCol.bbox.width;
3147
- const designBottom = measureHeadingAdvancedDesignHeight(lvl, { titleText: title, formattedNumber: pref, chapterNumber: pref, attrs: rawBlock.attrs }, measureWidth, resolved.page.dpi, doc.metadata, cursor.pageIndex, designFrames, {
4007
+ const info = { titleText: title, formattedNumber: pref, numericValue: headingNumber, locale: resolvedLocale(resolved), chapterNumber: chapterNumberByBlock[blockIdx] ?? '', attrs: rawBlock.attrs };
4008
+ const design = measureHeadingDesign(lvl, info, measureWidth, resolved.page.dpi, doc.metadata, cursor.pageIndex, designFrames, {
3148
4009
  x: lvl.span === 'page' ? pageArea.x : curCol.bbox.x,
3149
4010
  y: curCol.bbox.y + (curCol.bbox.height - curCol.availableHeight) + spacingBefore,
3150
- });
3151
- if (designBottom > effectiveRemainHeight)
3152
- effectiveRemainHeight = designBottom;
4011
+ }, resourceById);
4012
+ if (design.height > effectiveRemainHeight)
4013
+ effectiveRemainHeight = design.height;
4014
+ // A text anchored to the foot or the middle of the band keeps the
4015
+ // box the design is painted in at least as tall as it needs, so it
4016
+ // never rises above the heading's top (EF-176). The box of a
4017
+ // heading that snaps holds its margin too.
4018
+ const textFloor = design.textFloor - (shouldSnapToGrid ? style.marginBottomPx : 0);
4019
+ if (textFloor > effectiveRemainHeight)
4020
+ effectiveRemainHeight = textFloor;
4021
+ // The default opener sets the title with the design painter, which
4022
+ // can take more lines than the heading's own measure (a justified
4023
+ // line whose spaces shrink, a forced break, a run it prints plain):
4024
+ // the band holds every line it paints (EF-100).
4025
+ if (opensDefaultOpener(rawBlock)) {
4026
+ const painted = measureDefaultOpenerHeight(lvl, resolved.headings.textAlign, defaultOpenerTitle(remainingLines, pref, rawBlock.titleBreaks, rawBlock.text.length), pref, pageArea.width, resolved.page.dpi, doc.metadata, cursor.pageIndex);
4027
+ if (painted > effectiveRemainHeight)
4028
+ effectiveRemainHeight = painted;
4029
+ }
4030
+ headingDesign = { lvl, info };
3153
4031
  }
3154
4032
  }
3155
4033
  // Keep-with-list: if this colon-paragraph would fit but would leave no
@@ -3171,9 +4049,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3171
4049
  ? resolved.orderedLists.marginTop
3172
4050
  : resolved.unorderedLists.marginTop;
3173
4051
  const nextListMarginTopPx = dimensionToPx(nextListMarginDim, dpi, bodyStyle.fontSizePx);
3174
- const effectiveGap = Math.max(style.marginBottomPx, nextListMarginTopPx);
4052
+ let effectiveGap = Math.max(style.marginBottomPx, nextListMarginTopPx);
4053
+ // A list that snaps its top (EF-103) opens on the next grid line.
4054
+ const nextLists = blockMeasureCtx.resolved;
4055
+ if ((nextListKind === 'ordered' ? nextLists.orderedLists : nextLists.unorderedLists).snapTopToGrid) {
4056
+ effectiveGap = Math.ceil((paragraphBottom + effectiveGap - 0.01) / baselineGrid) * baselineGrid - paragraphBottom;
4057
+ }
3175
4058
  const minSpaceForList = effectiveGap + bodyStyle.lineHeightPx;
3176
- if (availableAfter < minSpaceForList) {
4059
+ // A line of room is not always enough (EF-110): with
4060
+ // `colonListRoom: 'item'` the first item has to be able to start
4061
+ // there, by the rules that split it — a two-line item the orphan
4062
+ // and widow rules keep whole needs both. `'line'` keeps the 1.4
4063
+ // check, one line.
4064
+ if (availableAfter < minSpaceForList
4065
+ || (resolved.bodyText.colonListRoom === 'item'
4066
+ && !firstListItemStarts(blockIdx, availableAfter - effectiveGap, curCol.bbox.width, blockMeasureCtx))) {
3177
4067
  const effectiveWidowMin = resolved.bodyText.avoidWidows
3178
4068
  ? Math.max(1, resolved.bodyText.widowMinLines)
3179
4069
  : 1;
@@ -3198,7 +4088,10 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3198
4088
  blk.sourceEnd = splitLines[splitLines.length - 1].sourceEnd;
3199
4089
  blk.sourceMap = absoluteSourceMap;
3200
4090
  blk.plainPrefixLen = prefixLen;
4091
+ if (balancing)
4092
+ blk.balancing = balancing;
3201
4093
  placeBlockInColumn(blk, splitAt * style.lineHeightPx, curCol, cursor);
4094
+ citedLinesPlaced(blockIdx, blk);
3202
4095
  doc.blocks.push(blk);
3203
4096
  remainingLines = remainingLines.slice(splitAt);
3204
4097
  partIndex++;
@@ -3224,13 +4117,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3224
4117
  }
3225
4118
  }
3226
4119
  if (headingRunCount > 0 && headingRunCount < curCol.blocks.length) {
3227
- const popped = curCol.blocks.splice(curCol.blocks.length - headingRunCount);
3228
- for (const p of popped) {
3229
- const idx = doc.blocks.indexOf(p);
3230
- if (idx !== -1)
3231
- doc.blocks.splice(idx, 1);
3232
- curCol.availableHeight += p.bbox.height;
3233
- }
4120
+ const popped = rollbackHeadings(curCol);
3234
4121
  // Rewind so the for-loop's blockIdx++ lands on the first
3235
4122
  // rolled-back heading (marker blocks in between are replayed).
3236
4123
  blockIdx = (popped[0].contentIndex ?? blockIdx - headingRunCount) - 1;
@@ -3246,6 +4133,76 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3246
4133
  // headingRunCount === curCol.blocks.length: fall through to place.
3247
4134
  }
3248
4135
  }
4136
+ // Keep a display formula with its lead-in (`math.keepWithLeadIn`,
4137
+ // TeX's predisplay penalty, EF-84): when this paragraph — or the part
4138
+ // of it left to set — fits the column but the formula after it does
4139
+ // not fit under its last line, that line goes on with the formula.
4140
+ // The lines left here keep the widow minimum; with fewer, a paragraph
4141
+ // that opens here moves on whole, and one already running on from
4142
+ // the column before stays (it cannot do better anywhere).
4143
+ if (resolved.math.keepWithLeadIn
4144
+ && vdtType === 'paragraph'
4145
+ && contentBlocks[blockIdx + 1]?.type === 'mathDisplay'
4146
+ && totalRemainHeight <= effectiveAvailable + FIT_EPS) {
4147
+ const formula = measureContentBlock(contentBlocks[blockIdx + 1], blockIdx + 1, curCol.bbox.width, blockMeasureCtx);
4148
+ const formulaHeight = formula ? (formula.measured.lines[0]?.bbox.height ?? formula.measured.totalHeight) : 0;
4149
+ const gap = Math.max(style.marginBottomPx, formula?.kind.style.marginTopPx ?? 0);
4150
+ const usedHeight = (curCol.bbox.height - curCol.availableHeight) + spacingBefore;
4151
+ // As the plain pass saw it: room balancing added above is not room
4152
+ // the formula lost.
4153
+ const availableAfter = curCol.bbox.height - (usedHeight + totalRemainHeight) + (balanceExtraInColumn.get(curCol) ?? 0);
4154
+ if (formula && formulaHeight > 0 && availableAfter + FIT_EPS < gap + formulaHeight) {
4155
+ const minKeep = resolved.bodyText.avoidWidows ? Math.max(1, resolved.bodyText.widowMinLines) : 1;
4156
+ const splitAt = remainingLines.length - 1;
4157
+ if (splitAt >= minKeep) {
4158
+ if (spacingBefore !== 0)
4159
+ curCol.availableHeight -= spacingBefore;
4160
+ const partId = partIndex === 0 ? id : `${id}-cont-${partIndex}`;
4161
+ const splitLines = remainingLines.slice(0, splitAt);
4162
+ const blk = createVDTBlock(partId, vdtType, style.fontString, style.color, style.textAlign);
4163
+ applyStyleAttrs(blk, style);
4164
+ if (letterSpacingPx !== undefined)
4165
+ blk.letterSpacing = letterSpacingPx;
4166
+ blk.contentIndex = blockIdx;
4167
+ stampBlockExtras(blk, rawBlock);
4168
+ blk.lines = resetLinePositions(splitLines, style.lineHeightPx);
4169
+ blk.dirty = false;
4170
+ blk.snappedToGrid = false;
4171
+ blk.sourceStart = splitLines[0].sourceStart;
4172
+ blk.sourceEnd = splitLines[splitLines.length - 1].sourceEnd;
4173
+ blk.sourceMap = absoluteSourceMap;
4174
+ blk.plainPrefixLen = prefixLen;
4175
+ if (balancing)
4176
+ blk.balancing = balancing;
4177
+ placeBlockInColumn(blk, splitAt * style.lineHeightPx, curCol, cursor);
4178
+ citedLinesPlaced(blockIdx, blk);
4179
+ doc.blocks.push(blk);
4180
+ remainingLines = remainingLines.slice(splitAt);
4181
+ partIndex++;
4182
+ pendingSpacing = 0;
4183
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
4184
+ continue;
4185
+ }
4186
+ if (partIndex === 0 && curCol.blocks.length > 0) {
4187
+ // Moving on whole must not strand a heading closing the column.
4188
+ const headingRun = trailingHeadingRun(curCol);
4189
+ if (resolved.headings.keepWithNext && headingRun > 0 && headingRun < curCol.blocks.length) {
4190
+ const rolledBack = rollbackHeadings(curCol);
4191
+ if (rolledBack.length > 0) {
4192
+ blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
4193
+ pendingSpacing = 0;
4194
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
4195
+ break;
4196
+ }
4197
+ }
4198
+ if (headingRun < curCol.blocks.length) {
4199
+ pendingSpacing = 0;
4200
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
4201
+ continue;
4202
+ }
4203
+ }
4204
+ }
4205
+ }
3249
4206
  // A block opening a column normally stays (no column can take it any
3250
4207
  // better), except in a column at least a line shorter than the content
3251
4208
  // area — the band under a page-span box, or a column cut by a float:
@@ -3294,7 +4251,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3294
4251
  shortColumnMoves++;
3295
4252
  // Roll back any immediately-preceding heading blocks in this
3296
4253
  // column so they travel with this one.
3297
- const rolledBack = rollbackTrailingBlocks(curCol, doc.blocks, isFreeHeading);
4254
+ const rolledBack = rollbackHeadings(curCol);
3298
4255
  if (rolledBack.length > 0) {
3299
4256
  // Rewind so the for-loop's blockIdx++ lands on the first
3300
4257
  // rolled-back heading (marker blocks in between are replayed).
@@ -3323,7 +4280,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3323
4280
  blk.headingLevel = headingLevel;
3324
4281
  if (numberPrefix)
3325
4282
  blk.numberPrefix = numberPrefix;
4283
+ if (headingNumber !== undefined)
4284
+ blk.headingNumber = headingNumber;
3326
4285
  }
4286
+ if (hiddenHeading)
4287
+ blk.hidden = true;
3327
4288
  if (partIndex === 0 && vdtType === 'heading' && rawBlock.attrs)
3328
4289
  blk.attrs = rawBlock.attrs;
3329
4290
  if (partIndex === 0 && vdtType === 'heading' && rawBlock.attrSources) {
@@ -3334,10 +4295,6 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3334
4295
  blk.titleBreaks = rawBlock.titleBreaks;
3335
4296
  blk.titleLength = rawBlock.text.length;
3336
4297
  }
3337
- if (partIndex === 0 && vdtType === 'heading' && rawBlock.titleBreaks) {
3338
- blk.titleBreaks = rawBlock.titleBreaks;
3339
- blk.titleLength = rawBlock.text.length;
3340
- }
3341
4298
  if (vdtType === 'mathDisplay' && mathDisplayRender) {
3342
4299
  blk.mathRender = mathDisplayRender;
3343
4300
  blk.tex = rawBlock.tex;
@@ -3360,15 +4317,21 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3360
4317
  }
3361
4318
  blk.sourceMap = absoluteSourceMap;
3362
4319
  blk.plainPrefixLen = prefixLen;
4320
+ if (balancing)
4321
+ blk.balancing = balancing;
3363
4322
  let h = effectiveRemainHeight;
4323
+ /** What a collapsing container tail still owes the next block,
4324
+ * from the grid line its text snapped to (see `collapseTail`). */
4325
+ let tailOwed = 0;
3364
4326
  if (shouldSnapToGrid && partIndex === 0) {
3365
4327
  // Snap using the absolute position in the column so that the block
3366
4328
  // bottom lands on a baseline grid line. This accounts for off-grid
3367
4329
  // starts (e.g. after consecutive unsnapped headings) and bakes in
3368
4330
  // the minimum marginBottom — the grid always wins, but the margin
3369
- // below is guaranteed to be at least marginBottomPx.
4331
+ // below is guaranteed to be at least marginBottomPx. A collapsing
4332
+ // container tail snaps under its text alone and carries the rest.
3370
4333
  const usedHeight = curCol.bbox.height - curCol.availableHeight;
3371
- const naturalBottom = usedHeight + effectiveRemainHeight + style.marginBottomPx;
4334
+ const naturalBottom = usedHeight + effectiveRemainHeight + (collapseTail ? 0 : style.marginBottomPx);
3372
4335
  // Tolerance guards against FP drift: if naturalBottom is already on
3373
4336
  // the grid (e.g. marginBottom is an exact multiple of baselineGrid),
3374
4337
  // don't round up to the next line.
@@ -3376,39 +4339,44 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3376
4339
  // A negative margin below a container tail may snap the flow back
3377
4340
  // above the text's own bottom; never below the block's top.
3378
4341
  h = Math.max(0, snappedBottom - usedHeight);
4342
+ if (collapseTail)
4343
+ tailOwed = style.marginBottomPx - (snappedBottom - (usedHeight + effectiveRemainHeight));
4344
+ }
4345
+ // A design that ends within a line of the column's foot fits, but
4346
+ // its margin and grid snap would carry the block past the foot: it
4347
+ // takes the rest of the column instead, as a design reaching past
4348
+ // the foot does (the text after it opens the next column or page).
4349
+ if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight) {
4350
+ h = Math.max(effectiveRemainHeight, curCol.availableHeight);
3379
4351
  }
3380
4352
  placeBlockInColumn(blk, h, curCol, cursor);
4353
+ citedLinesPlaced(blockIdx, blk);
3381
4354
  finalizeListItem(blk, partIndex === 0);
3382
4355
  doc.blocks.push(blk);
3383
- // Page-spanning heading: reserve the same vertical band in every
3384
- // other column on this page so body text under the opener band
3385
- // starts below it in ALL columns, not just the one it was placed in.
3386
- // A column still untouched moves its head below the band (as a top
3387
- // float does), so a float offered that column's head lands under
3388
- // the opener instead of over it; the text starts where it did.
4356
+ // Page-spanning heading: reserve its band in every other column.
3389
4357
  if (vdtType === 'heading' && headingLevel !== undefined) {
3390
4358
  const lvl = headingLevels.forBlock(rawBlock);
3391
4359
  if (lvl?.span === 'page') {
3392
- const page = doc.pages[cursor.pageIndex];
3393
- for (const otherCol of page.columns) {
3394
- if (otherCol === curCol)
3395
- continue;
3396
- if (otherCol.blocks.length === 0 && otherCol.availableHeight >= otherCol.bbox.height - 0.01) {
3397
- const shift = Math.min(h, otherCol.bbox.height);
3398
- otherCol.bbox.y += shift;
3399
- otherCol.bbox.height -= shift;
3400
- }
3401
- otherCol.availableHeight = Math.max(0, otherCol.availableHeight - h);
3402
- }
4360
+ reserveOpenerBand(curCol, h, blk, shouldSnapToGrid && partIndex === 0 ? 0 : style.marginBottomPx, lvl.snapToGrid ?? resolved.headings.snapToGrid);
3403
4361
  }
3404
4362
  }
4363
+ // Its design's elements in the side column hold the side stack off.
4364
+ if (headingDesign)
4365
+ markSideObstacles(blk, headingDesign);
3405
4366
  // For snapped headings/list-tails the margin is baked into the snap;
3406
4367
  // for unsnapped ones (consecutive) track it for collapsing
3407
4368
  if (vdtType === 'listItem' && nextIsListItem) {
3408
- pendingSpacing = listBullet.itemSpacingPx;
4369
+ pendingSpacing = itemGapPx(listBullet);
3409
4370
  }
3410
4371
  else {
3411
4372
  pendingSpacing = (shouldSnapToGrid && partIndex === 0) ? 0 : style.marginBottomPx;
4373
+ // A collapsing container tail: the space the snap did not cover,
4374
+ // in whole grid lines, merges with the next block's own space
4375
+ // above (a heading's margin collapses with the exact rest).
4376
+ if (collapseTail && shouldSnapToGrid && partIndex === 0 && tailOwed > 0.01) {
4377
+ pendingSpacing = Math.ceil((tailOwed - 0.01) / baselineGrid) * baselineGrid;
4378
+ inlineGapOwed = { afterIdx: blockIdx, pending: pendingSpacing, exact: tailOwed, container: true };
4379
+ }
3412
4380
  }
3413
4381
  break;
3414
4382
  }
@@ -3429,17 +4397,136 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3429
4397
  slackWeight: resolved.bodyText.slackWeight,
3430
4398
  });
3431
4399
  // The block sits right under a heading: pushing it whole would
3432
- // strand the heading. Keep as many lines as fit under it — at
3433
- // least the widow minimum — even when that leaves a short tail;
3434
- // with fewer than that, the heading moves along with the block
4400
+ // strand the heading. Keep the most lines that fit under it — at
4401
+ // least the widow minimum — that also leave the orphan minimum for
4402
+ // the next column (`headings.keepWithNextSplit: 'rules'`, EF-185;
4403
+ // `'fill'`, postext 1.4's rule, keeps as many as fit, however few
4404
+ // go on). With no such split the heading moves along with the block
3435
4405
  // (the no-fit branch below rolls it back).
3436
4406
  const headingRun = partIndex === 0 ? trailingHeadingRun(curCol) : 0;
3437
4407
  if (choice.splitAt === 0 && headingRun > 0 && headingRun < curCol.blocks.length) {
3438
4408
  const minKeep = effectiveAvoidWidows ? Math.max(1, resolved.bodyText.widowMinLines) : 1;
3439
- const maxFit = Math.min(linesPerAvailable, remainingLines.length);
3440
- if (maxFit >= minKeep)
4409
+ const minTail = effectiveAvoidOrphans && resolved.headings.keepWithNextSplit === 'rules'
4410
+ ? Math.max(1, resolved.bodyText.orphanMinLines)
4411
+ : 1;
4412
+ let keep = Math.min(linesPerAvailable, remainingLines.length);
4413
+ while (keep >= minKeep && keep < remainingLines.length && remainingLines.length - keep < minTail)
4414
+ keep--;
4415
+ if (keep >= minKeep)
4416
+ choice = { splitAt: keep, demerit: choice.demerit };
4417
+ }
4418
+ // An empty column the block cannot leave (a full-height one, or a
4419
+ // short one once the moves below are spent) holds as many lines as
4420
+ // it can: a paragraph never runs past its column's foot, whatever
4421
+ // the orphan and widow rules would rather do (EF-97). Not a column
4422
+ // a band cap cut short: there the block runs past the cut, and the
4423
+ // driver takes the cut lower, or drops it, so the rules still hold
4424
+ // (under a figure heading such a column it moves on instead, see
4425
+ // `capShortUnderFloat` below).
4426
+ if (choice.splitAt === 0 && curCol.blocks.length === 0 && !shortColumn && !uncappedBottoms.has(curCol)) {
4427
+ const maxFit = Math.min(linesPerAvailable, remainingLines.length - 1);
4428
+ if (maxFit >= 1)
3441
4429
  choice = { splitAt: maxFit, demerit: choice.demerit };
3442
4430
  }
4431
+ // `bodyText.hyphenateAcrossColumns: false` (EF-86): the lines that
4432
+ // close a column must not end on a hyphen. Those are the line that
4433
+ // closes this column and the lines later full columns would end on.
4434
+ // When one of them ends on a hyphen, the paragraph is broken again,
4435
+ // once, with a hyphen at each of them priced out of the breaker; the
4436
+ // placement then starts over with its lines. Only a paragraph set
4437
+ // from its first line here — its earlier lines are not placed yet —
4438
+ // and never one the balancing is running long.
4439
+ if (choice.splitAt > 0
4440
+ && !resolved.bodyText.hyphenateAcrossColumns
4441
+ && !columnEndHyphenRetried
4442
+ && partIndex === 0
4443
+ && looseLines === undefined) {
4444
+ const fullColumn = Math.max(1, Math.floor((contentArea.height + 0.01) / style.lineHeightPx));
4445
+ const ends = [];
4446
+ for (let n = choice.splitAt; n < remainingLines.length; n += fullColumn)
4447
+ ends.push(n);
4448
+ /** How a setting ends its guarded lines: whether this column's
4449
+ * last line is hyphenated, and how many guarded lines are. */
4450
+ const columnEndHyphens = (lines) => ({
4451
+ first: lines[choice.splitAt - 1]?.hyphenated === true,
4452
+ count: ends.filter((n) => lines[n - 1]?.hyphenated).length,
4453
+ });
4454
+ const natural = columnEndHyphens(remainingLines);
4455
+ columnEndHyphenRetried = natural.count > 0;
4456
+ const again = natural.count > 0
4457
+ ? measureForColumns({ avoidHyphenAtLines: ends })
4458
+ : undefined;
4459
+ // The re-break is kept when it hyphenates fewer guarded lines. The
4460
+ // line closing this column counts first: it is the one certain
4461
+ // break, while the later ends assume full columns follow.
4462
+ const retried = again && again.measured.lines.length > 0 ? columnEndHyphens(again.measured.lines) : undefined;
4463
+ if (again
4464
+ && retried
4465
+ && (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0)
4466
+ && (natural.first !== retried.first ? natural.first : retried.count < natural.count)) {
4467
+ remainingLines = [...raggedLooseLines(again.measured.lines, style.textAlign)];
4468
+ paragraphLines = remainingLines;
4469
+ paragraphBreaks = again.measured.breaks;
4470
+ guardedEnds = ends;
4471
+ columnEndHyphenPart = 0;
4472
+ gateCitedFloats(blockIdx, remainingLines);
4473
+ // The pass over this column starts again: the balancing room it
4474
+ // recorded above is recorded again then.
4475
+ if (balancing && balancing.spaceAbove > 0) {
4476
+ balanceExtraInColumn.set(curCol, (balanceExtraInColumn.get(curCol) ?? 0) - balancing.spaceAbove);
4477
+ }
4478
+ continue;
4479
+ }
4480
+ }
4481
+ // …and again from a later part: the first re-break assumed full
4482
+ // columns after this one, and a column that ends elsewhere (a widow
4483
+ // kept, a float, a shorter page) can still end on a hyphen. The
4484
+ // paragraph is broken again with that end guarded too, the lines
4485
+ // already placed kept at their breaks (only the rest is broken
4486
+ // again), and the new setting is taken when it puts no hyphen at
4487
+ // this column's end (or fewer at the ends still to come). The
4488
+ // placed lines are checked again all the same: a break trace from
4489
+ // the other measuring path cannot keep them.
4490
+ if (choice.splitAt > 0
4491
+ && !resolved.bodyText.hyphenateAcrossColumns
4492
+ && partIndex > 0
4493
+ && columnEndHyphenPart !== partIndex
4494
+ && looseLines === undefined
4495
+ && remainingLines[choice.splitAt - 1]?.hyphenated === true) {
4496
+ columnEndHyphenPart = partIndex;
4497
+ const placed = paragraphLines.length - remainingLines.length;
4498
+ const fullColumn = Math.max(1, Math.floor((contentArea.height + 0.01) / style.lineHeightPx));
4499
+ const later = [];
4500
+ for (let n = choice.splitAt; n < remainingLines.length; n += fullColumn)
4501
+ later.push(placed + n);
4502
+ // The ends already placed keep their guard; the later ones the
4503
+ // last re-break assumed give way to where the columns now end.
4504
+ const ends = [...guardedEnds.filter((n) => n <= placed), ...later];
4505
+ const hyphenatedEnds = (lines) => later.filter((n) => lines[n - 1]?.hyphenated).length;
4506
+ const keepBreaks = paragraphBreaks && paragraphBreaks.at.length >= placed
4507
+ ? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
4508
+ : undefined;
4509
+ const again = measureForColumns({ avoidHyphenAtLines: ends, ...(keepBreaks ? { keepBreaks } : {}) });
4510
+ const lines = again && (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0)
4511
+ ? [...raggedLooseLines(again.measured.lines, style.textAlign)]
4512
+ : undefined;
4513
+ const keepsPlaced = lines !== undefined && lines.length > placed
4514
+ && paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated);
4515
+ if (lines
4516
+ && keepsPlaced
4517
+ && (lines[placed + choice.splitAt - 1]?.hyphenated !== true
4518
+ || hyphenatedEnds(lines) < hyphenatedEnds(paragraphLines))) {
4519
+ paragraphLines = lines;
4520
+ paragraphBreaks = again.measured.breaks;
4521
+ guardedEnds = ends;
4522
+ remainingLines = lines.slice(placed);
4523
+ regateCitedFloats(blockIdx, lines);
4524
+ if (balancing && balancing.spaceAbove > 0) {
4525
+ balanceExtraInColumn.set(curCol, (balanceExtraInColumn.get(curCol) ?? 0) - balancing.spaceAbove);
4526
+ }
4527
+ continue;
4528
+ }
4529
+ }
3443
4530
  if (choice.splitAt > 0) {
3444
4531
  // Consume spacing (negative: a container margin pulling the block up)
3445
4532
  if (spacingBefore !== 0) {
@@ -3457,6 +4544,8 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3457
4544
  blk.headingLevel = headingLevel;
3458
4545
  if (numberPrefix)
3459
4546
  blk.numberPrefix = numberPrefix;
4547
+ if (headingNumber !== undefined)
4548
+ blk.headingNumber = headingNumber;
3460
4549
  }
3461
4550
  blk.lines = resetLinePositions(splitLines, style.lineHeightPx);
3462
4551
  blk.dirty = false;
@@ -3467,8 +4556,11 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3467
4556
  }
3468
4557
  blk.sourceMap = absoluteSourceMap;
3469
4558
  blk.plainPrefixLen = prefixLen;
4559
+ if (balancing)
4560
+ blk.balancing = balancing;
3470
4561
  const splitHeight = choice.splitAt * style.lineHeightPx;
3471
4562
  placeBlockInColumn(blk, splitHeight, curCol, cursor);
4563
+ citedLinesPlaced(blockIdx, blk);
3472
4564
  finalizeListItem(blk, partIndex === 0);
3473
4565
  doc.blocks.push(blk);
3474
4566
  remainingLines = remainingLines.slice(choice.splitAt);
@@ -3479,9 +4571,25 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3479
4571
  }
3480
4572
  // choice.splitAt === 0: fall through to push whole paragraph to next column
3481
4573
  }
4574
+ // An empty column a band cap cut, under a figure at its head: a block
4575
+ // that cannot start there — none of its lines stays, by the rules
4576
+ // that split it — goes on to the next column of the band, as it does
4577
+ // from such a column uncapped. Forced into it, it ran past the cut,
4578
+ // and the cut was taken where it ended, however low (EF-116: the
4579
+ // columns beside it then ended far above the box under them).
4580
+ const capShortUnderFloat = curCol.blocks.length === 0
4581
+ && uncappedBottoms.has(curCol)
4582
+ && reservedOf(curCol).top > 0
4583
+ && (() => {
4584
+ const cols = bandColumns(doc.pages[cursor.pageIndex], currentBand(doc.pages[cursor.pageIndex], cursor));
4585
+ const at = cols.indexOf(curCol);
4586
+ return at >= 0 && at < cols.length - 1;
4587
+ })();
3482
4588
  // Cannot split — advance to next column if current has content (or
3483
- // the column is a short band that cannot hold the block at all).
3484
- if (curCol.blocks.length > 0 || (shortColumn && effectiveRemainHeight <= contentArea.height)) {
4589
+ // the column is a short band that cannot hold the block at all). A
4590
+ // block that can split moves on from a short band however tall it is:
4591
+ // the next, taller column breaks it by the rules (EF-97).
4592
+ if (curCol.blocks.length > 0 || capShortUnderFloat || (shortColumn && (effectiveRemainHeight <= contentArea.height || canSplit))) {
3485
4593
  if (curCol.blocks.length === 0)
3486
4594
  shortColumnMoves++;
3487
4595
  // Heading keep-with-next (no-fit variant): when a block can't fit
@@ -3504,7 +4612,7 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3504
4612
  && headingRun > 0 && !runFillsColumn;
3505
4613
  const pullsRun = vdtType === 'heading' && (!runFillsColumn || shortColumn);
3506
4614
  if (resolved.headings.keepWithNext && (pullsRun || strands)) {
3507
- const rolledBack = rollbackTrailingBlocks(curCol, doc.blocks, isFreeHeading);
4615
+ const rolledBack = rollbackHeadings(curCol);
3508
4616
  if (rolledBack.length > 0) {
3509
4617
  blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
3510
4618
  pendingSpacing = 0;
@@ -3532,8 +4640,18 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3532
4640
  blk.letterSpacing = letterSpacingPx;
3533
4641
  blk.contentIndex = blockIdx;
3534
4642
  stampBlockExtras(blk, rawBlock);
3535
- if (partIndex === 0)
4643
+ // Stamp the prefix as the other placement branches do: a full-page
4644
+ // opener lands here, and paint and running heads read its
4645
+ // `{chapterNumber}` from `numberPrefix`, as the measure did (EF-59).
4646
+ if (partIndex === 0) {
3536
4647
  blk.headingLevel = headingLevel;
4648
+ if (numberPrefix)
4649
+ blk.numberPrefix = numberPrefix;
4650
+ if (headingNumber !== undefined)
4651
+ blk.headingNumber = headingNumber;
4652
+ }
4653
+ if (hiddenHeading)
4654
+ blk.hidden = true;
3537
4655
  if (partIndex === 0 && vdtType === 'heading' && rawBlock.attrs)
3538
4656
  blk.attrs = rawBlock.attrs;
3539
4657
  if (partIndex === 0 && vdtType === 'heading' && rawBlock.attrSources) {
@@ -3553,11 +4671,33 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3553
4671
  }
3554
4672
  blk.sourceMap = absoluteSourceMap;
3555
4673
  blk.plainPrefixLen = prefixLen;
3556
- placeBlockInColumn(blk, totalRemainHeight, curCol, cursor);
4674
+ if (balancing)
4675
+ blk.balancing = balancing;
4676
+ // A heading whose design (or `minHeight`) reaches past the column's
4677
+ // foot — a full-page cover, say — claims the rest of its column, and
4678
+ // of the page when it spans it: the next block opens the next column
4679
+ // or page instead of running on under (or beside) the design. Its
4680
+ // block runs down to the column's foot (EF-91), so the design is laid
4681
+ // out against the band it claims — elements anchored to the band's
4682
+ // middle or foot, or filling it, keep to that band, as they do when
4683
+ // the reservation fits — rather than against the height of its text.
4684
+ const claimsColumn = vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight;
4685
+ const placedHeight = claimsColumn
4686
+ ? Math.max(totalRemainHeight, Math.min(effectiveRemainHeight, curCol.availableHeight))
4687
+ : totalRemainHeight;
4688
+ placeBlockInColumn(blk, placedHeight, curCol, cursor);
4689
+ citedLinesPlaced(blockIdx, blk);
3557
4690
  finalizeListItem(blk, partIndex === 0);
3558
4691
  doc.blocks.push(blk);
4692
+ if (claimsColumn) {
4693
+ curCol.availableHeight = 0;
4694
+ if (headingLevels.forBlock(rawBlock)?.span === 'page')
4695
+ reserveOpenerBand(curCol, effectiveRemainHeight, blk, style.marginBottomPx);
4696
+ }
4697
+ if (headingDesign)
4698
+ markSideObstacles(blk, headingDesign);
3559
4699
  if (vdtType === 'listItem') {
3560
- pendingSpacing = nextIsListItem ? listBullet.itemSpacingPx : style.marginBottomPx;
4700
+ pendingSpacing = nextIsListItem ? itemGapPx(listBullet) : style.marginBottomPx;
3561
4701
  }
3562
4702
  else {
3563
4703
  pendingSpacing = style.marginBottomPx;
@@ -3569,6 +4709,52 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3569
4709
  // End of the document: level the closing band and place any floats still
3570
4710
  // pending (referenced on the last page) on pages appended after it.
3571
4711
  closeFlowSegment(contentBlocks.length);
4712
+ // A band that is still cut at the end of the pass (a trailing cap: the
4713
+ // closing band of a chapter or of the document) must hold what it took,
4714
+ // under the same rules as an uncut one. Two ways a cut too low breaks
4715
+ // them, and in both the cap is not delivered, so the driver grows it a
4716
+ // line (or drops it and keeps the uncut layout):
4717
+ // - a block that cannot split across the cut — a paragraph tail the
4718
+ // orphan and widow minimums keep whole, a keep-together box — is
4719
+ // force-placed into an empty capped column and runs past its foot; the
4720
+ // renderers clip the column to its box, so its last lines would never
4721
+ // be painted (EF-55, EF-72). What counts is what is painted: a list
4722
+ // tail's box bakes its bottom margin in and may reach past the cut
4723
+ // with every line inside it;
4724
+ // - a keep-with-next heading opening an empty capped column stays there
4725
+ // (a column-opening heading is never pushed on) while its text goes to
4726
+ // the next column of the band: the heading closes its column (EF-61).
4727
+ const capStrandsHeading = (cols) => {
4728
+ if (!resolved.headings.keepWithNext)
4729
+ return false;
4730
+ return cols.some((c, i) => {
4731
+ const last = c.blocks[c.blocks.length - 1];
4732
+ return c.bandCapped && last !== undefined && isFreeHeading(last) && !last.hidden
4733
+ && cols.slice(i + 1).some((next) => next.blocks.length > 0);
4734
+ });
4735
+ };
4736
+ for (const [capIndex, cols] of cappedBandColumns) {
4737
+ if (!spanPlacedInBand.has(capIndex))
4738
+ continue;
4739
+ if (capClipsLines(cols) || capStrandsHeading(cols))
4740
+ spanPlacedInBand.delete(capIndex);
4741
+ }
4742
+ // A styled section's pages draw the column rule of its own layout where
4743
+ // that differs from the document's (EF-112); the renderers take the
4744
+ // document's on every other page.
4745
+ const docRule = resolved.layout.columnRule;
4746
+ const docRuleWidthPx = dimensionToPx(docRule.lineWidth, dpi);
4747
+ for (const page of doc.pages) {
4748
+ const layout = pageLayoutOf(page);
4749
+ if (!layout || layout === resolved.layout || page.partInfo)
4750
+ continue;
4751
+ const rule = layout.columnRule;
4752
+ const lineWidthPx = dimensionToPx(rule.lineWidth, dpi);
4753
+ const same = rule.enabled === docRule.enabled
4754
+ && (!rule.enabled || (rule.color.hex === docRule.color.hex && lineWidthPx === docRuleWidthPx));
4755
+ if (!same)
4756
+ page.columnRule = { enabled: rule.enabled, color: rule.color.hex, lineWidthPx };
4757
+ }
3572
4758
  // Stamp page-number info onto every page (including blank parity pages).
3573
4759
  const labels = buildPageLabels(doc.pages.length, pageNumberSegments);
3574
4760
  for (let i = 0; i < doc.pages.length; i++) {
@@ -3585,7 +4771,22 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
3585
4771
  .map((s) => s.startPageIndex);
3586
4772
  if (restarts.length > 0)
3587
4773
  doc.pageNumberRestarts = restarts;
3588
- buildHeadersAndFooters(doc, resourceById);
4774
+ buildHeadersAndFooters(doc, resourceById, {
4775
+ // Where two palette entries share a base value, each kind of flow
4776
+ // colour follows its own settings under a part or section palette.
4777
+ // (The default resource types set no caption style of their own.)
4778
+ flowColorValues: (overrides) => flowColorValues(config, overrides, config?.resourceTypes),
4779
+ // `{firstMark.<style>}` / `{lastMark.<style>}`: a paragraph of a
4780
+ // `:::paragraphs` container marks the page with its leading bold run.
4781
+ paragraphMark: (idx) => {
4782
+ const styleId = paragraphContainers.byBlock[idx]?.styleId;
4783
+ const block = contentBlocks[idx];
4784
+ if (styleId === undefined || block?.type !== 'paragraph')
4785
+ return undefined;
4786
+ const text = leadingBoldText(block.spans);
4787
+ return text.length > 0 ? { styleId, text } : undefined;
4788
+ },
4789
+ });
3589
4790
  doc.converged = true;
3590
4791
  doc.iterationCount = 1;
3591
4792
  return { doc, forcedBreakPages, bandCapProposals, spanPlacedInBand, bandCapsApplied, looseOutcome, captionUnderProposals };
@@ -3636,6 +4837,24 @@ export async function buildDocumentAsync(content, config, cache, options) {
3636
4837
  }
3637
4838
  /** The build as a generator: one `yield` after every placement pass. */
3638
4839
  export function* buildDocumentGen(content, config, cache, options) {
4840
+ const doc = yield* buildDocumentRounds(content, config, cache, options);
4841
+ // References and markup the source names wrongly: read from the source
4842
+ // once, located on the pages of the finished layout. Kept apart from
4843
+ // `doc.warnings`, whose entries keep their postext 1.4 shape.
4844
+ const found = collectContentWarnings(content.markdown, config, content.resources ?? []);
4845
+ if (found.length > 0)
4846
+ doc.contentWarnings = locateContentWarnings(doc, found);
4847
+ // Config values the build replaced (an unknown number format, a font
4848
+ // stack, a side column no column width can take): walked once per build,
4849
+ // not per pass — they belong to no page.
4850
+ const configWarnings = collectConfigWarnings(config);
4851
+ if (configWarnings.length > 0)
4852
+ doc.configWarnings = configWarnings;
4853
+ return doc;
4854
+ }
4855
+ /** {@link buildDocumentGen} before the document-wide warnings: the
4856
+ * contents rounds around the balanced build. */
4857
+ function* buildDocumentRounds(content, config, cache, options) {
3639
4858
  // A document printing its own table of contents (`:::toc` with no
3640
4859
  // host-supplied outline) is laid out with the page labels of the previous
3641
4860
  // build until they no longer change: the contents' own length moves what
@@ -3741,7 +4960,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3741
4960
  range,
3742
4961
  bestScore: score,
3743
4962
  attempts: old?.attempts ?? 0,
3744
- done: score === 0,
4963
+ done: score === 0 && boxRoomIn(gaps, range) <= BOX_ROOM_EPS_PX,
3745
4964
  stable: score === 0,
3746
4965
  failedLoose: old?.failedLoose ?? new Set(),
3747
4966
  failedLines: old?.failedLines ?? new Set(),
@@ -3817,7 +5036,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3817
5036
  const warnings = [
3818
5037
  ...(best.doc.warnings ?? []).filter((w) => !taken(w.pageIndex)),
3819
5038
  ...(next.doc.warnings ?? []).filter((w) => taken(w.pageIndex)),
3820
- ].sort((a, b) => a.pageIndex - b.pageIndex);
5039
+ ].sort((a, b) => (a.pageIndex ?? -1) - (b.pageIndex ?? -1));
3821
5040
  const doc = { ...best.doc, pages, blocks, ...(warnings.length > 0 ? { warnings } : { warnings: undefined }) };
3822
5041
  const pageBest = pageOfContent(best.doc);
3823
5042
  const pageNext = pageOfContent(next.doc);
@@ -3917,6 +5136,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3917
5136
  optimalLineBreaking: best.doc.config.bodyText.optimalLineBreaking,
3918
5137
  failedLoose,
3919
5138
  failedLines,
5139
+ closingBox: balancing.closingBox,
3920
5140
  });
3921
5141
  // The levers newly proposed, by segment; those of a segment that is
3922
5142
  // done (plateaued, out of attempts) are withdrawn from the pass.
@@ -3965,6 +5185,7 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3965
5185
  const nextGaps = collectColumnGaps(next.doc, next.forcedBreakPages);
3966
5186
  const capPage = pageOfContent(best.doc);
3967
5187
  const accepted = [];
5188
+ const boxKeys = boxLeverKeys(gaps);
3968
5189
  for (const si of trying) {
3969
5190
  const s = segments[si];
3970
5191
  // An earlier segment's cascade shifted this one's pages: the pass
@@ -3992,6 +5213,13 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
3992
5213
  for (const k of looseFailed)
3993
5214
  s.failedLoose.add(k);
3994
5215
  const looseWon = keysLoose.filter((k) => typeof next.looseOutcome.get(k) === 'number');
5216
+ // A box closing its column that moves down onto the column's last
5217
+ // grid slot (or level with the column beside it) closes less than a
5218
+ // line, which the score does not count. On its own — no other new
5219
+ // lever in the segment — such a pass is kept when it holds the
5220
+ // score, moves nothing across a column and closes room under a box
5221
+ // (EF-70).
5222
+ const boxesOnly = keysLines.length > 0 && keysLoose.length === 0 && keysLines.every((k) => boxKeys.has(k));
3995
5223
  if (score < s.bestScore) {
3996
5224
  for (const k of keysLines)
3997
5225
  applied.lines.set(k, proposal.lines.get(k));
@@ -4004,14 +5232,31 @@ function* buildDocumentBalanced(content, config, cache, options, tocRound = 0) {
4004
5232
  }
4005
5233
  accepted.push(s.range);
4006
5234
  }
5235
+ else if (boxesOnly && score === s.bestScore
5236
+ && firstDivergentColumn(best.doc, next.doc, s.range) === null
5237
+ && boxRoomIn(nextGaps, s.range) < boxRoomIn(gaps, s.range) - BOX_ROOM_EPS_PX) {
5238
+ for (const k of keysLines)
5239
+ applied.lines.set(k, proposal.lines.get(k));
5240
+ accepted.push(s.range);
5241
+ }
4007
5242
  else if (!containCascade(next, s, keysLines, keysLoose, gaps)) {
4008
5243
  // Plateau or regression without a cascade to contain: retry when
4009
5244
  // a loose candidate was just blacklisted (the proposer falls
4010
5245
  // through to the next one), or when the new loose paragraphs
4011
5246
  // gained their lines yet the segment did not improve (the gain
4012
5247
  // landed elsewhere — drop them too). A pure spacing plateau means
4013
- // the segment is done: it keeps the best layout found so far.
4014
- if (looseFailed.length > 0 || looseWon.length > 0) {
5248
+ // the segment is done: it keeps the best layout found so far —
5249
+ // unless a closing box rode along with the levers that bought
5250
+ // nothing: those are dropped and the box is tried on its own.
5251
+ const boxes = keysLines.filter((k) => boxKeys.has(k));
5252
+ if (boxes.length > 0 && !boxesOnly) {
5253
+ for (const k of keysLines)
5254
+ if (!boxKeys.has(k))
5255
+ s.failedLines.add(k);
5256
+ for (const k of keysLoose)
5257
+ s.failedLoose.add(k);
5258
+ }
5259
+ else if (looseFailed.length > 0 || looseWon.length > 0) {
4015
5260
  for (const k of looseWon)
4016
5261
  s.failedLoose.add(k);
4017
5262
  }