postext 1.27.1 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (544) hide show
  1. package/dist/__tests__/bundle/balloonPlacementPin.test.d.ts +2 -0
  2. package/dist/__tests__/bundle/balloonPlacementPin.test.d.ts.map +1 -0
  3. package/dist/__tests__/bundle/balloonPlacementPin.test.js +71 -0
  4. package/dist/__tests__/bundle/balloonPlacementPin.test.js.map +1 -0
  5. package/dist/__tests__/bundle/cjkRulesPin.test.js +131 -7
  6. package/dist/__tests__/bundle/cjkRulesPin.test.js.map +1 -1
  7. package/dist/__tests__/bundle/designOverflowPin.test.js +3 -3
  8. package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -1
  9. package/dist/__tests__/bundle/flowColumnsPin.test.js +3 -1
  10. package/dist/__tests__/bundle/flowColumnsPin.test.js.map +1 -1
  11. package/dist/__tests__/bundle/gridBalancingPin.test.js +14 -5
  12. package/dist/__tests__/bundle/gridBalancingPin.test.js.map +1 -1
  13. package/dist/__tests__/bundle/gridBreakRulesPin.test.d.ts +2 -0
  14. package/dist/__tests__/bundle/gridBreakRulesPin.test.d.ts.map +1 -0
  15. package/dist/__tests__/bundle/gridBreakRulesPin.test.js +144 -0
  16. package/dist/__tests__/bundle/gridBreakRulesPin.test.js.map +1 -0
  17. package/dist/__tests__/bundle/indexGroupSplitPin.test.d.ts +2 -0
  18. package/dist/__tests__/bundle/indexGroupSplitPin.test.d.ts.map +1 -0
  19. package/dist/__tests__/bundle/indexGroupSplitPin.test.js +55 -0
  20. package/dist/__tests__/bundle/indexGroupSplitPin.test.js.map +1 -0
  21. package/dist/__tests__/bundle/inlineCaptionSidePin.test.d.ts +2 -0
  22. package/dist/__tests__/bundle/inlineCaptionSidePin.test.d.ts.map +1 -0
  23. package/dist/__tests__/bundle/inlineCaptionSidePin.test.js +60 -0
  24. package/dist/__tests__/bundle/inlineCaptionSidePin.test.js.map +1 -0
  25. package/dist/__tests__/bundle/inlineTableSplitPin.test.js +3 -1
  26. package/dist/__tests__/bundle/inlineTableSplitPin.test.js.map +1 -1
  27. package/dist/__tests__/bundle/looseOrderPin.test.d.ts +2 -0
  28. package/dist/__tests__/bundle/looseOrderPin.test.d.ts.map +1 -0
  29. package/dist/__tests__/bundle/looseOrderPin.test.js +76 -0
  30. package/dist/__tests__/bundle/looseOrderPin.test.js.map +1 -0
  31. package/dist/__tests__/bundle/openerHeadFloatsPin.test.js +2 -1
  32. package/dist/__tests__/bundle/openerHeadFloatsPin.test.js.map +1 -1
  33. package/dist/__tests__/bundle/refLabelBreaksPin.test.d.ts +2 -0
  34. package/dist/__tests__/bundle/refLabelBreaksPin.test.d.ts.map +1 -0
  35. package/dist/__tests__/bundle/refLabelBreaksPin.test.js +72 -0
  36. package/dist/__tests__/bundle/refLabelBreaksPin.test.js.map +1 -0
  37. package/dist/__tests__/bundle/scriptContextPin.test.d.ts +2 -0
  38. package/dist/__tests__/bundle/scriptContextPin.test.d.ts.map +1 -0
  39. package/dist/__tests__/bundle/scriptContextPin.test.js +66 -0
  40. package/dist/__tests__/bundle/scriptContextPin.test.js.map +1 -0
  41. package/dist/__tests__/bundle/trailingStepPin.test.d.ts +2 -0
  42. package/dist/__tests__/bundle/trailingStepPin.test.d.ts.map +1 -0
  43. package/dist/__tests__/bundle/trailingStepPin.test.js +59 -0
  44. package/dist/__tests__/bundle/trailingStepPin.test.js.map +1 -0
  45. package/dist/__tests__/bundle/wideFloatStretchPin.test.d.ts +2 -0
  46. package/dist/__tests__/bundle/wideFloatStretchPin.test.d.ts.map +1 -0
  47. package/dist/__tests__/bundle/wideFloatStretchPin.test.js +59 -0
  48. package/dist/__tests__/bundle/wideFloatStretchPin.test.js.map +1 -0
  49. package/dist/__tests__/bundle.test.js +8 -6
  50. package/dist/__tests__/bundle.test.js.map +1 -1
  51. package/dist/__tests__/cjk/annotations.test.js +223 -0
  52. package/dist/__tests__/cjk/annotations.test.js.map +1 -1
  53. package/dist/__tests__/cjk/annotationsRender.test.js +3 -3
  54. package/dist/__tests__/cjk/annotationsRender.test.js.map +1 -1
  55. package/dist/__tests__/cjk/chipLatinSpacing.test.js +13 -2
  56. package/dist/__tests__/cjk/chipLatinSpacing.test.js.map +1 -1
  57. package/dist/__tests__/cjk/designNumberSeams.test.d.ts +2 -0
  58. package/dist/__tests__/cjk/designNumberSeams.test.d.ts.map +1 -0
  59. package/dist/__tests__/cjk/designNumberSeams.test.js +221 -0
  60. package/dist/__tests__/cjk/designNumberSeams.test.js.map +1 -0
  61. package/dist/__tests__/cjk/grid.test.js +1 -1
  62. package/dist/__tests__/cjk/grid.test.js.map +1 -1
  63. package/dist/__tests__/cjk/gridBalancing.test.js +3 -1
  64. package/dist/__tests__/cjk/gridBalancing.test.js.map +1 -1
  65. package/dist/__tests__/cjk/japaneseSpacing.test.js +3 -1
  66. package/dist/__tests__/cjk/japaneseSpacing.test.js.map +1 -1
  67. package/dist/__tests__/cjk/keepAll.test.js +3 -1
  68. package/dist/__tests__/cjk/keepAll.test.js.map +1 -1
  69. package/dist/__tests__/cjk/keepAllLatinQuotation.test.d.ts +2 -0
  70. package/dist/__tests__/cjk/keepAllLatinQuotation.test.d.ts.map +1 -0
  71. package/dist/__tests__/cjk/keepAllLatinQuotation.test.js +166 -0
  72. package/dist/__tests__/cjk/keepAllLatinQuotation.test.js.map +1 -0
  73. package/dist/__tests__/cjk/latinSpacingEdge.test.d.ts +2 -0
  74. package/dist/__tests__/cjk/latinSpacingEdge.test.d.ts.map +1 -0
  75. package/dist/__tests__/cjk/latinSpacingEdge.test.js +179 -0
  76. package/dist/__tests__/cjk/latinSpacingEdge.test.js.map +1 -0
  77. package/dist/__tests__/cjk/runt.test.js +92 -1
  78. package/dist/__tests__/cjk/runt.test.js.map +1 -1
  79. package/dist/__tests__/codeBlocks.test.js +1 -1
  80. package/dist/__tests__/columnBalancing.test.js +216 -0
  81. package/dist/__tests__/columnBalancing.test.js.map +1 -1
  82. package/dist/__tests__/columnRuleBands.test.js +78 -0
  83. package/dist/__tests__/columnRuleBands.test.js.map +1 -1
  84. package/dist/__tests__/comics/readingDirection.test.js +52 -0
  85. package/dist/__tests__/comics/readingDirection.test.js.map +1 -1
  86. package/dist/__tests__/comics/script.test.js +5 -0
  87. package/dist/__tests__/comics/script.test.js.map +1 -1
  88. package/dist/__tests__/contrast.test.d.ts +2 -0
  89. package/dist/__tests__/contrast.test.d.ts.map +1 -0
  90. package/dist/__tests__/contrast.test.js +258 -0
  91. package/dist/__tests__/contrast.test.js.map +1 -0
  92. package/dist/__tests__/defaults/cjk.test.js +4 -4
  93. package/dist/__tests__/defaults/cjk.test.js.map +1 -1
  94. package/dist/__tests__/defaults/gridBreakRules.test.d.ts +2 -0
  95. package/dist/__tests__/defaults/gridBreakRules.test.d.ts.map +1 -0
  96. package/dist/__tests__/defaults/gridBreakRules.test.js +152 -0
  97. package/dist/__tests__/defaults/gridBreakRules.test.js.map +1 -0
  98. package/dist/__tests__/defaults/headingsBalancing.test.js +25 -0
  99. package/dist/__tests__/defaults/headingsBalancing.test.js.map +1 -1
  100. package/dist/__tests__/defaults/stripContextDefaults.test.js +41 -0
  101. package/dist/__tests__/defaults/stripContextDefaults.test.js.map +1 -1
  102. package/dist/__tests__/defaults/stripRoundTrip.test.js +8 -5
  103. package/dist/__tests__/defaults/stripRoundTrip.test.js.map +1 -1
  104. package/dist/__tests__/exports.test.js +30 -0
  105. package/dist/__tests__/exports.test.js.map +1 -1
  106. package/dist/__tests__/fontUsage.test.d.ts +2 -0
  107. package/dist/__tests__/fontUsage.test.d.ts.map +1 -0
  108. package/dist/__tests__/fontUsage.test.js +113 -0
  109. package/dist/__tests__/fontUsage.test.js.map +1 -0
  110. package/dist/__tests__/hardBreaks.test.js +1 -1
  111. package/dist/__tests__/headingInlineMarks.test.js +7 -5
  112. package/dist/__tests__/headingInlineMarks.test.js.map +1 -1
  113. package/dist/__tests__/hyphenationRules.test.d.ts +2 -0
  114. package/dist/__tests__/hyphenationRules.test.d.ts.map +1 -0
  115. package/dist/__tests__/hyphenationRules.test.js +295 -0
  116. package/dist/__tests__/hyphenationRules.test.js.map +1 -0
  117. package/dist/__tests__/indexGroupSplit.test.d.ts +2 -0
  118. package/dist/__tests__/indexGroupSplit.test.d.ts.map +1 -0
  119. package/dist/__tests__/indexGroupSplit.test.js +175 -0
  120. package/dist/__tests__/indexGroupSplit.test.js.map +1 -0
  121. package/dist/__tests__/mathSizeMigration.test.js +7 -5
  122. package/dist/__tests__/mathSizeMigration.test.js.map +1 -1
  123. package/dist/__tests__/multipleColumns.test.js +19 -0
  124. package/dist/__tests__/multipleColumns.test.js.map +1 -1
  125. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
  126. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +3 -3
  127. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js.map +1 -1
  128. package/dist/__tests__/pipeline/closingBandUnderOpener.test.d.ts +2 -0
  129. package/dist/__tests__/pipeline/closingBandUnderOpener.test.d.ts.map +1 -0
  130. package/dist/__tests__/pipeline/closingBandUnderOpener.test.js +90 -0
  131. package/dist/__tests__/pipeline/closingBandUnderOpener.test.js.map +1 -0
  132. package/dist/__tests__/pipeline/colonListRoom.test.js +5 -4
  133. package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
  134. package/dist/__tests__/pipeline/columnHeadSpace.test.d.ts +2 -0
  135. package/dist/__tests__/pipeline/columnHeadSpace.test.d.ts.map +1 -0
  136. package/dist/__tests__/pipeline/columnHeadSpace.test.js +175 -0
  137. package/dist/__tests__/pipeline/columnHeadSpace.test.js.map +1 -0
  138. package/dist/__tests__/pipeline/compoundBreaks.test.js +6 -6
  139. package/dist/__tests__/pipeline/compoundBreaks.test.js.map +1 -1
  140. package/dist/__tests__/pipeline/dashBreaks.test.js +5 -5
  141. package/dist/__tests__/pipeline/dashBreaks.test.js.map +1 -1
  142. package/dist/__tests__/pipeline/flowColumns.test.js +100 -0
  143. package/dist/__tests__/pipeline/flowColumns.test.js.map +1 -1
  144. package/dist/__tests__/pipeline/headingShortBand.test.js +2 -1
  145. package/dist/__tests__/pipeline/headingShortBand.test.js.map +1 -1
  146. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +6 -5
  147. package/dist/__tests__/pipeline/headingSplitOrphans.test.js.map +1 -1
  148. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js +188 -1
  149. package/dist/__tests__/pipeline/hyphenAcrossColumns.test.js.map +1 -1
  150. package/dist/__tests__/pipeline/inlineCaptionSide.test.d.ts +2 -0
  151. package/dist/__tests__/pipeline/inlineCaptionSide.test.d.ts.map +1 -0
  152. package/dist/__tests__/pipeline/inlineCaptionSide.test.js +270 -0
  153. package/dist/__tests__/pipeline/inlineCaptionSide.test.js.map +1 -0
  154. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +5 -5
  155. package/dist/__tests__/pipeline/inlineFigureSpace.test.js.map +1 -1
  156. package/dist/__tests__/pipeline/inlineRefRender.test.js +8 -6
  157. package/dist/__tests__/pipeline/inlineRefRender.test.js.map +1 -1
  158. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.d.ts +2 -0
  159. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.d.ts.map +1 -0
  160. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.js +73 -0
  161. package/dist/__tests__/pipeline/looseBeforeHeadingLines.test.js.map +1 -0
  162. package/dist/__tests__/pipeline/openerFloats.test.d.ts +2 -0
  163. package/dist/__tests__/pipeline/openerFloats.test.d.ts.map +1 -0
  164. package/dist/__tests__/pipeline/openerFloats.test.js +137 -0
  165. package/dist/__tests__/pipeline/openerFloats.test.js.map +1 -0
  166. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.d.ts +2 -0
  167. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.d.ts.map +1 -0
  168. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.js +162 -0
  169. package/dist/__tests__/pipeline/orderedNumberWidthItem.test.js.map +1 -0
  170. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +68 -8
  171. package/dist/__tests__/pipeline/paragraphContainerTail.test.js.map +1 -1
  172. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js +1 -1
  173. package/dist/__tests__/pipeline/paragraphStyleFaces.test.js.map +1 -1
  174. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.d.ts +2 -0
  175. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.d.ts.map +1 -0
  176. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.js +151 -0
  177. package/dist/__tests__/pipeline/raggedLineAndRuntFix.test.js.map +1 -0
  178. package/dist/__tests__/pipeline/raggedOptimal.test.js +6 -6
  179. package/dist/__tests__/pipeline/raggedOptimal.test.js.map +1 -1
  180. package/dist/__tests__/pipeline/refLabelBreaks.test.d.ts +2 -0
  181. package/dist/__tests__/pipeline/refLabelBreaks.test.d.ts.map +1 -0
  182. package/dist/__tests__/pipeline/refLabelBreaks.test.js +324 -0
  183. package/dist/__tests__/pipeline/refLabelBreaks.test.js.map +1 -0
  184. package/dist/__tests__/pipeline/resourceTemplateStyles.test.d.ts +2 -0
  185. package/dist/__tests__/pipeline/resourceTemplateStyles.test.d.ts.map +1 -0
  186. package/dist/__tests__/pipeline/resourceTemplateStyles.test.js +145 -0
  187. package/dist/__tests__/pipeline/resourceTemplateStyles.test.js.map +1 -0
  188. package/dist/__tests__/pipeline/shortListSplit.test.d.ts +2 -0
  189. package/dist/__tests__/pipeline/shortListSplit.test.d.ts.map +1 -0
  190. package/dist/__tests__/pipeline/shortListSplit.test.js +181 -0
  191. package/dist/__tests__/pipeline/shortListSplit.test.js.map +1 -0
  192. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.d.ts +2 -0
  193. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.d.ts.map +1 -0
  194. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.js +133 -0
  195. package/dist/__tests__/pipeline/snapTailAtColumnFoot.test.js.map +1 -0
  196. package/dist/__tests__/pipeline/stretchAfterDisplays.test.d.ts +2 -0
  197. package/dist/__tests__/pipeline/stretchAfterDisplays.test.d.ts.map +1 -0
  198. package/dist/__tests__/pipeline/stretchAfterDisplays.test.js +51 -0
  199. package/dist/__tests__/pipeline/stretchAfterDisplays.test.js.map +1 -0
  200. package/dist/__tests__/pipeline/trailingCapFloatHead.test.js +26 -0
  201. package/dist/__tests__/pipeline/trailingCapFloatHead.test.js.map +1 -1
  202. package/dist/__tests__/pipeline/trailingStep.test.d.ts +2 -0
  203. package/dist/__tests__/pipeline/trailingStep.test.d.ts.map +1 -0
  204. package/dist/__tests__/pipeline/trailingStep.test.js +187 -0
  205. package/dist/__tests__/pipeline/trailingStep.test.js.map +1 -0
  206. package/dist/__tests__/pipeline/wideFloatStretch.test.d.ts +2 -0
  207. package/dist/__tests__/pipeline/wideFloatStretch.test.d.ts.map +1 -0
  208. package/dist/__tests__/pipeline/wideFloatStretch.test.js +84 -0
  209. package/dist/__tests__/pipeline/wideFloatStretch.test.js.map +1 -0
  210. package/dist/__tests__/scriptContext.test.d.ts +2 -0
  211. package/dist/__tests__/scriptContext.test.d.ts.map +1 -0
  212. package/dist/__tests__/scriptContext.test.js +919 -0
  213. package/dist/__tests__/scriptContext.test.js.map +1 -0
  214. package/dist/__tests__/softHyphenEdges.test.d.ts +2 -0
  215. package/dist/__tests__/softHyphenEdges.test.d.ts.map +1 -0
  216. package/dist/__tests__/softHyphenEdges.test.js +217 -0
  217. package/dist/__tests__/softHyphenEdges.test.js.map +1 -0
  218. package/dist/__tests__/spaceLimitRatio.test.d.ts +2 -0
  219. package/dist/__tests__/spaceLimitRatio.test.d.ts.map +1 -0
  220. package/dist/__tests__/spaceLimitRatio.test.js +226 -0
  221. package/dist/__tests__/spaceLimitRatio.test.js.map +1 -0
  222. package/dist/__tests__/trackedLigatures.test.js +365 -24
  223. package/dist/__tests__/trackedLigatures.test.js.map +1 -1
  224. package/dist/__tests__/verseLines.test.js +9 -7
  225. package/dist/__tests__/verseLines.test.js.map +1 -1
  226. package/dist/bundle/configVersion.d.ts +201 -6
  227. package/dist/bundle/configVersion.d.ts.map +1 -1
  228. package/dist/bundle/configVersion.js +440 -6
  229. package/dist/bundle/configVersion.js.map +1 -1
  230. package/dist/bundle/index.d.ts +8 -0
  231. package/dist/bundle/index.d.ts.map +1 -1
  232. package/dist/bundle/index.js +8 -0
  233. package/dist/bundle/index.js.map +1 -1
  234. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  235. package/dist/canvas-backend/blockRender.js +3 -2
  236. package/dist/canvas-backend/blockRender.js.map +1 -1
  237. package/dist/canvas-backend/chip.js +1 -1
  238. package/dist/canvas-backend/chip.js.map +1 -1
  239. package/dist/canvas-backend/headerFooter.d.ts.map +1 -1
  240. package/dist/canvas-backend/headerFooter.js +17 -2
  241. package/dist/canvas-backend/headerFooter.js.map +1 -1
  242. package/dist/canvas-backend/runDirection.d.ts +10 -6
  243. package/dist/canvas-backend/runDirection.d.ts.map +1 -1
  244. package/dist/canvas-backend/runDirection.js +12 -7
  245. package/dist/canvas-backend/runDirection.js.map +1 -1
  246. package/dist/canvas-backend/scriptText.d.ts +11 -0
  247. package/dist/canvas-backend/scriptText.d.ts.map +1 -0
  248. package/dist/canvas-backend/scriptText.js +75 -0
  249. package/dist/canvas-backend/scriptText.js.map +1 -0
  250. package/dist/canvas-backend/segmentText.d.ts.map +1 -1
  251. package/dist/canvas-backend/segmentText.js +9 -1
  252. package/dist/canvas-backend/segmentText.js.map +1 -1
  253. package/dist/canvas-backend/trackedText.d.ts +35 -20
  254. package/dist/canvas-backend/trackedText.d.ts.map +1 -1
  255. package/dist/canvas-backend/trackedText.js +58 -22
  256. package/dist/canvas-backend/trackedText.js.map +1 -1
  257. package/dist/canvas-backend/verticalText.d.ts.map +1 -1
  258. package/dist/canvas-backend/verticalText.js +10 -2
  259. package/dist/canvas-backend/verticalText.js.map +1 -1
  260. package/dist/canvas-backend/wordRuns.d.ts +14 -5
  261. package/dist/canvas-backend/wordRuns.d.ts.map +1 -1
  262. package/dist/canvas-backend/wordRuns.js +20 -7
  263. package/dist/canvas-backend/wordRuns.js.map +1 -1
  264. package/dist/citations/names.d.ts +8 -0
  265. package/dist/citations/names.d.ts.map +1 -0
  266. package/dist/citations/names.js +25 -0
  267. package/dist/citations/names.js.map +1 -0
  268. package/dist/columnClip.d.ts +8 -1
  269. package/dist/columnClip.d.ts.map +1 -1
  270. package/dist/columnClip.js +32 -3
  271. package/dist/columnClip.js.map +1 -1
  272. package/dist/columnRule.d.ts +6 -3
  273. package/dist/columnRule.d.ts.map +1 -1
  274. package/dist/columnRule.js +6 -3
  275. package/dist/columnRule.js.map +1 -1
  276. package/dist/comics/layoutPage.d.ts.map +1 -1
  277. package/dist/comics/layoutPage.js +51 -22
  278. package/dist/comics/layoutPage.js.map +1 -1
  279. package/dist/comics/lettering/__tests__/bystanderFaces.test.d.ts +2 -0
  280. package/dist/comics/lettering/__tests__/bystanderFaces.test.d.ts.map +1 -0
  281. package/dist/comics/lettering/__tests__/bystanderFaces.test.js +204 -0
  282. package/dist/comics/lettering/__tests__/bystanderFaces.test.js.map +1 -0
  283. package/dist/comics/lettering/__tests__/shapeTextDash.test.d.ts +2 -0
  284. package/dist/comics/lettering/__tests__/shapeTextDash.test.d.ts.map +1 -0
  285. package/dist/comics/lettering/__tests__/shapeTextDash.test.js +173 -0
  286. package/dist/comics/lettering/__tests__/shapeTextDash.test.js.map +1 -0
  287. package/dist/comics/lettering/__tests__/strictPlacement.test.d.ts +2 -0
  288. package/dist/comics/lettering/__tests__/strictPlacement.test.d.ts.map +1 -0
  289. package/dist/comics/lettering/__tests__/strictPlacement.test.js +130 -0
  290. package/dist/comics/lettering/__tests__/strictPlacement.test.js.map +1 -0
  291. package/dist/comics/lettering/letter.js +21 -8
  292. package/dist/comics/lettering/letter.js.map +1 -1
  293. package/dist/comics/lettering/placement.d.ts.map +1 -1
  294. package/dist/comics/lettering/placement.js +12 -3
  295. package/dist/comics/lettering/placement.js.map +1 -1
  296. package/dist/comics/lettering/shape-text.d.ts +3 -0
  297. package/dist/comics/lettering/shape-text.d.ts.map +1 -1
  298. package/dist/comics/lettering/shape-text.js +71 -12
  299. package/dist/comics/lettering/shape-text.js.map +1 -1
  300. package/dist/comics/lettering/text.d.ts +5 -0
  301. package/dist/comics/lettering/text.d.ts.map +1 -1
  302. package/dist/comics/lettering/text.js +6 -1
  303. package/dist/comics/lettering/text.js.map +1 -1
  304. package/dist/comics/lettering/types.d.ts +12 -2
  305. package/dist/comics/lettering/types.d.ts.map +1 -1
  306. package/dist/comics/script.js +1 -1
  307. package/dist/comics/script.js.map +1 -1
  308. package/dist/comics/types.d.ts +7 -3
  309. package/dist/comics/types.d.ts.map +1 -1
  310. package/dist/configWarnings.d.ts.map +1 -1
  311. package/dist/configWarnings.js +33 -6
  312. package/dist/configWarnings.js.map +1 -1
  313. package/dist/contrast.d.ts +188 -0
  314. package/dist/contrast.d.ts.map +1 -0
  315. package/dist/contrast.js +583 -0
  316. package/dist/contrast.js.map +1 -0
  317. package/dist/defaults/bodyText.d.ts +31 -3
  318. package/dist/defaults/bodyText.d.ts.map +1 -1
  319. package/dist/defaults/bodyText.js +76 -8
  320. package/dist/defaults/bodyText.js.map +1 -1
  321. package/dist/defaults/citations.d.ts.map +1 -1
  322. package/dist/defaults/citations.js +4 -0
  323. package/dist/defaults/citations.js.map +1 -1
  324. package/dist/defaults/cjk.d.ts +11 -1
  325. package/dist/defaults/cjk.d.ts.map +1 -1
  326. package/dist/defaults/cjk.js +41 -3
  327. package/dist/defaults/cjk.js.map +1 -1
  328. package/dist/defaults/comics.d.ts.map +1 -1
  329. package/dist/defaults/comics.js +2 -0
  330. package/dist/defaults/comics.js.map +1 -1
  331. package/dist/defaults/contrast.d.ts +11 -0
  332. package/dist/defaults/contrast.d.ts.map +1 -0
  333. package/dist/defaults/contrast.js +65 -0
  334. package/dist/defaults/contrast.js.map +1 -0
  335. package/dist/defaults/crossRefs.d.ts +1 -1
  336. package/dist/defaults/crossRefs.d.ts.map +1 -1
  337. package/dist/defaults/crossRefs.js +6 -1
  338. package/dist/defaults/crossRefs.js.map +1 -1
  339. package/dist/defaults/footnotes.d.ts.map +1 -1
  340. package/dist/defaults/footnotes.js +5 -0
  341. package/dist/defaults/footnotes.js.map +1 -1
  342. package/dist/defaults/headings.d.ts +5 -1
  343. package/dist/defaults/headings.d.ts.map +1 -1
  344. package/dist/defaults/headings.js +41 -1
  345. package/dist/defaults/headings.js.map +1 -1
  346. package/dist/defaults/index.d.ts +3 -2
  347. package/dist/defaults/index.d.ts.map +1 -1
  348. package/dist/defaults/index.js +15 -4
  349. package/dist/defaults/index.js.map +1 -1
  350. package/dist/defaults/indexConfig.d.ts +1 -0
  351. package/dist/defaults/indexConfig.d.ts.map +1 -1
  352. package/dist/defaults/indexConfig.js +7 -1
  353. package/dist/defaults/indexConfig.js.map +1 -1
  354. package/dist/defaults/layout.d.ts.map +1 -1
  355. package/dist/defaults/layout.js +13 -1
  356. package/dist/defaults/layout.js.map +1 -1
  357. package/dist/defaults/orderedLists.js +1 -1
  358. package/dist/defaults/orderedLists.js.map +1 -1
  359. package/dist/defaults/paragraphStyles.d.ts.map +1 -1
  360. package/dist/defaults/paragraphStyles.js +5 -0
  361. package/dist/defaults/paragraphStyles.js.map +1 -1
  362. package/dist/defaults/shared.d.ts +4 -1
  363. package/dist/defaults/shared.d.ts.map +1 -1
  364. package/dist/defaults/shared.js +5 -0
  365. package/dist/defaults/shared.js.map +1 -1
  366. package/dist/design/cjkText.d.ts +7 -2
  367. package/dist/design/cjkText.d.ts.map +1 -1
  368. package/dist/design/cjkText.js +8 -2
  369. package/dist/design/cjkText.js.map +1 -1
  370. package/dist/design/layout.d.ts.map +1 -1
  371. package/dist/design/layout.js +85 -12
  372. package/dist/design/layout.js.map +1 -1
  373. package/dist/design/richText.d.ts.map +1 -1
  374. package/dist/design/richText.js +7 -3
  375. package/dist/design/richText.js.map +1 -1
  376. package/dist/fonts/usage.d.ts.map +1 -1
  377. package/dist/fonts/usage.js +89 -0
  378. package/dist/fonts/usage.js.map +1 -1
  379. package/dist/html-backend.d.ts.map +1 -1
  380. package/dist/html-backend.js +97 -27
  381. package/dist/html-backend.js.map +1 -1
  382. package/dist/hyphenate.d.ts +58 -1
  383. package/dist/hyphenate.d.ts.map +1 -1
  384. package/dist/hyphenate.js +228 -19
  385. package/dist/hyphenate.js.map +1 -1
  386. package/dist/hyphenationExceptions.d.ts +10 -0
  387. package/dist/hyphenationExceptions.d.ts.map +1 -0
  388. package/dist/hyphenationExceptions.js +29 -0
  389. package/dist/hyphenationExceptions.js.map +1 -0
  390. package/dist/index.d.ts +12 -4
  391. package/dist/index.d.ts.map +1 -1
  392. package/dist/index.js +10 -3
  393. package/dist/index.js.map +1 -1
  394. package/dist/knuthPlass/pretextAdapter.d.ts.map +1 -1
  395. package/dist/knuthPlass/pretextAdapter.js +4 -17
  396. package/dist/knuthPlass/pretextAdapter.js.map +1 -1
  397. package/dist/knuthPlass/richAdapter.d.ts +9 -0
  398. package/dist/knuthPlass/richAdapter.d.ts.map +1 -1
  399. package/dist/knuthPlass/richAdapter.js +15 -18
  400. package/dist/knuthPlass/richAdapter.js.map +1 -1
  401. package/dist/looseLines.d.ts +15 -4
  402. package/dist/looseLines.d.ts.map +1 -1
  403. package/dist/looseLines.js +17 -4
  404. package/dist/looseLines.js.map +1 -1
  405. package/dist/measure/cache.d.ts.map +1 -1
  406. package/dist/measure/cache.js +14 -4
  407. package/dist/measure/cache.js.map +1 -1
  408. package/dist/measure/canvas.d.ts +18 -0
  409. package/dist/measure/canvas.d.ts.map +1 -1
  410. package/dist/measure/canvas.js +104 -0
  411. package/dist/measure/canvas.js.map +1 -1
  412. package/dist/measure/cjkAnnotate.d.ts +11 -6
  413. package/dist/measure/cjkAnnotate.d.ts.map +1 -1
  414. package/dist/measure/cjkAnnotate.js +27 -11
  415. package/dist/measure/cjkAnnotate.js.map +1 -1
  416. package/dist/measure/cjkCompose.d.ts +6 -2
  417. package/dist/measure/cjkCompose.d.ts.map +1 -1
  418. package/dist/measure/cjkCompose.js +146 -30
  419. package/dist/measure/cjkCompose.js.map +1 -1
  420. package/dist/measure/cjkPunctuation.d.ts +17 -0
  421. package/dist/measure/cjkPunctuation.d.ts.map +1 -1
  422. package/dist/measure/cjkPunctuation.js +15 -1
  423. package/dist/measure/cjkPunctuation.js.map +1 -1
  424. package/dist/measure/font.d.ts +2 -1
  425. package/dist/measure/font.d.ts.map +1 -1
  426. package/dist/measure/font.js +4 -2
  427. package/dist/measure/font.js.map +1 -1
  428. package/dist/measure/hardBreaks.d.ts.map +1 -1
  429. package/dist/measure/hardBreaks.js +6 -2
  430. package/dist/measure/hardBreaks.js.map +1 -1
  431. package/dist/measure/kashida.d.ts.map +1 -1
  432. package/dist/measure/kashida.js +8 -11
  433. package/dist/measure/kashida.js.map +1 -1
  434. package/dist/measure/plain.d.ts +3 -0
  435. package/dist/measure/plain.d.ts.map +1 -1
  436. package/dist/measure/plain.js +70 -5
  437. package/dist/measure/plain.js.map +1 -1
  438. package/dist/measure/rich.d.ts +32 -0
  439. package/dist/measure/rich.d.ts.map +1 -1
  440. package/dist/measure/rich.js +227 -35
  441. package/dist/measure/rich.js.map +1 -1
  442. package/dist/measure/script.d.ts +180 -0
  443. package/dist/measure/script.d.ts.map +1 -0
  444. package/dist/measure/script.js +329 -0
  445. package/dist/measure/script.js.map +1 -0
  446. package/dist/measure/spaceRatios.d.ts +28 -0
  447. package/dist/measure/spaceRatios.d.ts.map +1 -0
  448. package/dist/measure/spaceRatios.js +48 -0
  449. package/dist/measure/spaceRatios.js.map +1 -0
  450. package/dist/measure/spaces.d.ts +10 -0
  451. package/dist/measure/spaces.d.ts.map +1 -1
  452. package/dist/measure/spaces.js +36 -0
  453. package/dist/measure/spaces.js.map +1 -1
  454. package/dist/measure/types.d.ts +9 -0
  455. package/dist/measure/types.d.ts.map +1 -1
  456. package/dist/measure/types.js.map +1 -1
  457. package/dist/numbering.d.ts +5 -0
  458. package/dist/numbering.d.ts.map +1 -1
  459. package/dist/numbering.js +5 -1
  460. package/dist/numbering.js.map +1 -1
  461. package/dist/parse/types.d.ts +13 -0
  462. package/dist/parse/types.d.ts.map +1 -1
  463. package/dist/pipeline/bandCaps.d.ts +35 -1
  464. package/dist/pipeline/bandCaps.d.ts.map +1 -1
  465. package/dist/pipeline/bandCaps.js +95 -3
  466. package/dist/pipeline/bandCaps.js.map +1 -1
  467. package/dist/pipeline/build.d.ts.map +1 -1
  468. package/dist/pipeline/build.js +754 -49
  469. package/dist/pipeline/build.js.map +1 -1
  470. package/dist/pipeline/buildHelpers.d.ts.map +1 -1
  471. package/dist/pipeline/buildHelpers.js +5 -1
  472. package/dist/pipeline/buildHelpers.js.map +1 -1
  473. package/dist/pipeline/buildMeasurement.d.ts +9 -0
  474. package/dist/pipeline/buildMeasurement.d.ts.map +1 -1
  475. package/dist/pipeline/buildMeasurement.js +3 -2
  476. package/dist/pipeline/buildMeasurement.js.map +1 -1
  477. package/dist/pipeline/calloutLayout.d.ts +10 -1
  478. package/dist/pipeline/calloutLayout.d.ts.map +1 -1
  479. package/dist/pipeline/calloutLayout.js +45 -9
  480. package/dist/pipeline/calloutLayout.js.map +1 -1
  481. package/dist/pipeline/columnBalancing.d.ts +44 -7
  482. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  483. package/dist/pipeline/columnBalancing.js +167 -40
  484. package/dist/pipeline/columnBalancing.js.map +1 -1
  485. package/dist/pipeline/config.d.ts +2 -2
  486. package/dist/pipeline/config.d.ts.map +1 -1
  487. package/dist/pipeline/config.js +18 -3
  488. package/dist/pipeline/config.js.map +1 -1
  489. package/dist/pipeline/contentWarnings.d.ts +6 -0
  490. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  491. package/dist/pipeline/contentWarnings.js +32 -0
  492. package/dist/pipeline/contentWarnings.js.map +1 -1
  493. package/dist/pipeline/crossRefs.d.ts +12 -1
  494. package/dist/pipeline/crossRefs.d.ts.map +1 -1
  495. package/dist/pipeline/crossRefs.js +22 -14
  496. package/dist/pipeline/crossRefs.js.map +1 -1
  497. package/dist/pipeline/flowColumns.d.ts +2 -1
  498. package/dist/pipeline/flowColumns.d.ts.map +1 -1
  499. package/dist/pipeline/flowColumns.js +12 -2
  500. package/dist/pipeline/flowColumns.js.map +1 -1
  501. package/dist/pipeline/footnotes.d.ts.map +1 -1
  502. package/dist/pipeline/footnotes.js +3 -0
  503. package/dist/pipeline/footnotes.js.map +1 -1
  504. package/dist/pipeline/indexDirective.d.ts.map +1 -1
  505. package/dist/pipeline/indexDirective.js +15 -0
  506. package/dist/pipeline/indexDirective.js.map +1 -1
  507. package/dist/pipeline/lists.d.ts +1 -1
  508. package/dist/pipeline/lists.d.ts.map +1 -1
  509. package/dist/pipeline/lists.js +9 -1
  510. package/dist/pipeline/lists.js.map +1 -1
  511. package/dist/pipeline/measureContentBlock.d.ts +18 -0
  512. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  513. package/dist/pipeline/measureContentBlock.js +13 -3
  514. package/dist/pipeline/measureContentBlock.js.map +1 -1
  515. package/dist/pipeline/paragraphContainers.d.ts +4 -1
  516. package/dist/pipeline/paragraphContainers.d.ts.map +1 -1
  517. package/dist/pipeline/paragraphContainers.js +2 -0
  518. package/dist/pipeline/paragraphContainers.js.map +1 -1
  519. package/dist/pipeline/placement.d.ts +15 -0
  520. package/dist/pipeline/placement.d.ts.map +1 -1
  521. package/dist/pipeline/placement.js +36 -4
  522. package/dist/pipeline/placement.js.map +1 -1
  523. package/dist/pipeline/raggedLines.d.ts +8 -6
  524. package/dist/pipeline/raggedLines.d.ts.map +1 -1
  525. package/dist/pipeline/raggedLines.js +13 -10
  526. package/dist/pipeline/raggedLines.js.map +1 -1
  527. package/dist/pipeline/resourceLayout.d.ts +4 -0
  528. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  529. package/dist/pipeline/resourceLayout.js +22 -7
  530. package/dist/pipeline/resourceLayout.js.map +1 -1
  531. package/dist/pipeline/resourceNumbering.d.ts +6 -3
  532. package/dist/pipeline/resourceNumbering.d.ts.map +1 -1
  533. package/dist/pipeline/resourceNumbering.js +36 -19
  534. package/dist/pipeline/resourceNumbering.js.map +1 -1
  535. package/dist/pipeline/toc.d.ts +0 -3
  536. package/dist/pipeline/toc.d.ts.map +1 -1
  537. package/dist/pipeline/toc.js +19 -2
  538. package/dist/pipeline/toc.js.map +1 -1
  539. package/dist/types.d.ts +411 -15
  540. package/dist/types.d.ts.map +1 -1
  541. package/dist/vdt.d.ts +90 -7
  542. package/dist/vdt.d.ts.map +1 -1
  543. package/dist/vdt.js.map +1 -1
  544. package/package.json +1 -1
@@ -8,6 +8,8 @@ import { buildPageLabels, computeHeadingNumbering, parseNumberFormat, } from '..
8
8
  import { extractFrontmatter, normalizeMetadata } from '../frontmatter';
9
9
  import { collectConfigWarnings } from '../configWarnings';
10
10
  import { initHyphenator } from '../measure';
11
+ import { setHyphenationRules } from '../hyphenate';
12
+ import { citedNames } from '../citations/names';
11
13
  import { getCjkLineBreak, setCjkLineBreak } from '../measure/cjkClasses';
12
14
  import { cjkCompositionOf, getCjkComposition, setCjkComposition } from '../measure/cjkPunctuation';
13
15
  import { getMeasureRegion, getMeasureUprightDigits, getMeasureWritingMode, measuringVertically, setMeasureUprightDigits, setMeasureWritingMode } from '../measure/vertical';
@@ -21,6 +23,7 @@ import { defaultFontSet, documentFontFaces, fontFallbacks } from '../fonts/faces
21
23
  import { syncFontSet } from '../fonts/fontSet';
22
24
  import { asciiDigits } from '../arabicNumerals';
23
25
  import { getMeasureArabicNumbers, getMeasureDirection, setMeasureArabicNumbers, setMeasureDirection, shiftLineX } from '../measure/bidiLines';
26
+ import { getMeasureScriptContext, hanMarksOfLocale, scriptContextOfLocale, setMeasureScriptContext } from '../measure/script';
24
27
  import { createHeadingLevelResolver, deriveSectionGeometryConfig, sectionWritingMode, deriveSectionMeasureContext, headingIsHidden, headingIsNumbered, headingMarksFor, headingStyleOf, numberTitlesFor, planHeadingSections, } from './headingStyles';
25
28
  import { locateAnchors } from './anchors';
26
29
  import { applyCitations, citationLocale, processCitations, withBibliographyStyle } from './citations';
@@ -34,7 +37,7 @@ import { expandTocDirectives } from './toc';
34
37
  import { expandIndexDirectives, locateIndexMarks } from './indexDirective';
35
38
  import { resolveBodyStyle, resolveBlockquoteStyle, resolveParagraphStyle } from './styles';
36
39
  import { computeLevelIndentsPx, computeOrderedLevelIndentsPx, computeOrderedListRunMetrics, directListMarker, listBulletPosition, listItemGapPx, listItemSpacingPx, mirrorListMarker, } from './lists';
37
- import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, closeBandAt, pageLayoutOf, FIT_EPS, } from './placement';
40
+ import { resetLinePositions, createPageWithColumns, currentColumn, advanceToNextColumn, columnClosesPage, flowColumnsAfter, advanceToNextPageBoundary, enforcePageParity, placeBlockInColumn, placeAtomicBlock, createPartPage, pageHasContent, sideColumnOf, sideColumns, sideUsedBottom, pageIsOccupied, bandColumns, currentBand, isBandLevel, bandUsedBottom, closeBandAndInsertSpan, closeBandAt, pageLayoutOf, FIT_EPS, } from './placement';
38
41
  import { chooseParagraphSplit } from './orphanWidow';
39
42
  import { layoutComicPage, comicPageLayoutWarnings } from '../comics/layoutPage';
40
43
  import { comicStripExtent, comicStripOffset, comicStripPlacement, comicStripWidth, layoutComicStrip } from '../comics/strip';
@@ -47,7 +50,7 @@ import { planParts, derivePartMeasureContext } from './parts';
47
50
  import { layoutCallout, offsetBlock, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
48
51
  import { fillGalley, galleyBreaks } from './columnsGroup';
49
52
  import { chooseTableSlice, layoutCaptionText, layoutResourceBlock, MIN_TAIL_ROWS } from './resourceLayout';
50
- import { computeFloatPlan, floatedResourceIds, floatColumnCount, } from './floatPlacement';
53
+ import { computeFloatPlan, floatedResourceIds, floatColumnCount, resolveResourcePlacement, } from './floatPlacement';
51
54
  import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, floatGapBelow, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
52
55
  import { computeHeadingContext, computeResourceNumberingState, } from './resourceNumbering';
53
56
  import { documentLocale, effectiveResourceTypes } from '../defaults/resourceTypes';
@@ -61,9 +64,9 @@ import { buildHeadersAndFooters, defaultOpenerTitle, headingDesignBoxes, measure
61
64
  import { flowColorValues } from './partPalette';
62
65
  import { chapterNumberCounter, leadingBoldText } from './placeholders';
63
66
  import { proposeBalanceLines, GRID_LINE_LEVERS, collectColumnGaps, firstDivergentColumn, gapLinesIn, boxRoomIn, boxLeverKeys, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey, candidateKey, flexFigureKey, flexFloatKey } from './columnBalancing';
64
- import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
67
+ import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, bandLinePitch, capRetryLimit, } from './bandCaps';
65
68
  import { raggedLooseLines } from './raggedLines';
66
- import { cjkLooseLineWarnings, collectContentWarnings, designTruncationWarnings, dropCapWarnings, floatShrinkWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
69
+ import { cjkLooseLineWarnings, raggedLineWarnings, collectContentWarnings, designTruncationWarnings, dropCapWarnings, floatShrinkWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
67
70
  import { dropCapSinkAfter } from './dropCap';
68
71
  import { mostlyJoiningScript } from '../measure/joining';
69
72
  import { composesAsCjk } from '../measure/cjkCompose';
@@ -236,10 +239,12 @@ export function buildDocumentPass(content, config, cache, options, hints = {}) {
236
239
  const uprightDigits = getMeasureUprightDigits();
237
240
  const direction = getMeasureDirection();
238
241
  const arabicNumbers = getMeasureArabicNumbers();
242
+ const scriptContext = getMeasureScriptContext();
239
243
  try {
240
244
  return placeDocumentPass(content, config, cache, options, hints);
241
245
  }
242
246
  finally {
247
+ setMeasureScriptContext(scriptContext);
243
248
  setMeasureArabicNumbers(arabicNumbers);
244
249
  setCjkLineBreak(lineBreak);
245
250
  setCjkComposition(composition);
@@ -287,7 +292,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
287
292
  // design text — and so do the words wider than their measure, divided at a
288
293
  // syllable even in ragged text. Left unset, a build would hyphenate with
289
294
  // whichever dictionary the previous one (or the default, en-us) chose.
290
- initHyphenator(resolved.bodyText.hyphenation.locale);
295
+ // With the words the document keeps whole (#699); the names of the
296
+ // authors it cites join them below, once its references are read.
297
+ const hyphenationRules = { exceptions: resolved.bodyText.hyphenation.exceptions, capitalized: resolved.bodyText.hyphenation.capitalized };
298
+ initHyphenator(resolved.bodyText.hyphenation.locale, hyphenationRules);
291
299
  // CJK text breaks at the document's level wherever it is measured (body,
292
300
  // captions, cells, notes, boxes), as it hyphenates in its language.
293
301
  setCjkLineBreak(resolved.cjk.lineBreak);
@@ -306,6 +314,15 @@ function placeDocumentPass(content, config, cache, options, hints) {
306
314
  // And, in an Arabic-script book, read a number that opens a paragraph as
307
315
  // the numbers after its letters (#665).
308
316
  setMeasureArabicNumbers(arabicNumbersOf(resolved));
317
+ // A word with no letter is shaped in the script of the text around it,
318
+ // and with no letter near in the script of the document's language (#725).
319
+ // A Chinese or Japanese document sets its doubled ellipsis and dash as
320
+ // Han text wherever they are cited.
321
+ setMeasureScriptContext({
322
+ enabled: resolved.bodyText.scriptContext !== false,
323
+ script: scriptContextOfLocale(resolvedLocale(resolved)),
324
+ ...(hanMarksOfLocale(resolvedLocale(resolved)) ? { hanMarks: true } : {}),
325
+ });
309
326
  // Compute baseline grid
310
327
  const baselineGrid = computeBaselineGrid(resolved);
311
328
  // Create document
@@ -337,6 +354,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
337
354
  // styled section with its own margins / layout — the section's.
338
355
  let contentArea = pageMetrics.contentArea;
339
356
  let geomResolved = resolved;
357
+ /** How many columns of a fresh page take body text, in the geometry
358
+ * pages are opened with now (`hyphenateAcrossColumns: 'withinPage'`,
359
+ * #701: every that many column ends, a page ends). */
360
+ let freshFlowColumnsOf;
361
+ let freshFlowColumns = 1;
362
+ const freshPageFlowColumns = () => {
363
+ if (freshFlowColumnsOf !== geomResolved) {
364
+ freshFlowColumnsOf = geomResolved;
365
+ const fresh = createPageWithColumns(doc.pages.length, geomResolved, contentArea, pageWidthPx, pageHeightPx, pageIndexOffset);
366
+ freshFlowColumns = Math.max(1, fresh.columns.filter((c) => c.kind !== 'side').length);
367
+ }
368
+ return freshFlowColumns;
369
+ };
340
370
  // Page/bleed frames for design elements anchored to `'page'` / `'bleed'`,
341
371
  // in the frame of the page's flow: the sheet's, or on a vertical page
342
372
  // (`page.flow`) the flow frame, where "the top of the page" is the
@@ -382,6 +412,11 @@ function placeDocumentPass(content, config, cache, options, hints) {
382
412
  const parsedBody = numberTitlesFor(headingMarksFor(tashkilFor(literalBreaksFor(parseMarkdownMemo(markdownBody, codeParseOptions(resolved)), resolved), resolved.bodyText.tashkil), resolved), resolved);
383
413
  const citationContext = content.citations
384
414
  ?? (needsCitationContext(parsedBody, frontmatterMeta) ? bookCitationContexts([{ metadata: frontmatterMeta, blocks: parsedBody }], content.resources)[0] : undefined);
415
+ // `citations.hyphenateNames: false`: the cited authors' names are never
416
+ // divided, in a citation, in the text or in the bibliography (#699).
417
+ if (citationContext && !resolved.citations.hyphenateNames) {
418
+ setHyphenationRules({ ...hyphenationRules, names: citedNames(citationContext.items) });
419
+ }
385
420
  const citationsApplied = citationContext
386
421
  ? applyCitations(parsedBody, citationContext, processCitations(citationContext, resolved, citationLocale(resolved, resolvedLocale(resolved))), resolved, resolvedLocale(resolved))
387
422
  : undefined;
@@ -543,6 +578,30 @@ function placeDocumentPass(content, config, cache, options, hints) {
543
578
  /** The flow segment has ended and its floats and side boxes are taking
544
579
  * pages after the text (see `drainPendingFloats`, #639). */
545
580
  let drainingAfterText = false;
581
+ /** Opener pages (`layout.openerFloats`, #749): the page a part opens,
582
+ * the page a heading opens with its own break, and the first page of a
583
+ * document that starts with a heading. Under `'avoid'` they offer a
584
+ * float no slot (see `placeFloatInColumns`), so it takes the first one
585
+ * after them; under `'warn'` each float set on one is reported. */
586
+ const openerPages = new Set();
587
+ const openerFloatRule = resolved.layout.openerFloats;
588
+ /** The `openerFloat` warnings of this pass, and the floats they name. */
589
+ const openerFloatNotes = [];
590
+ const openerFloatsNoted = new Set();
591
+ const noteOpenerFloat = (page, f) => {
592
+ if (openerFloatRule !== 'warn' || !openerPages.has(page) || openerFloatsNoted.has(f.resourceId))
593
+ return;
594
+ openerFloatsNoted.add(f.resourceId);
595
+ const box = f.callout ? calloutFloats.get(f.callout.startIdx) : undefined;
596
+ const first = contentBlocks[box ? f.callout.startIdx : f.firstBlockIdx];
597
+ const last = contentBlocks[box ? box.plan.endIdx : f.firstBlockIdx];
598
+ openerFloatNotes.push({
599
+ kind: 'openerFloat',
600
+ ...(box ? {} : { resourceId: f.resourceId }),
601
+ ...(first && last ? { sourceStart: first.sourceStart + bodyOffset, sourceEnd: last.sourceEnd + bodyOffset } : {}),
602
+ pageIndex: page.index,
603
+ });
604
+ };
546
605
  /** `span: 'side'` boxes of a style with `sideAtColumnEnd: 'after'`
547
606
  * (EF-161), and the side boxes fenced behind them, waiting for the text
548
607
  * after their fence to land (see `settleAwaitingSideBoxes`). Each keeps
@@ -1496,6 +1555,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
1496
1555
  return { measure, ...band, cap, fits: excessOf(measure, band) <= 0.01 };
1497
1556
  };
1498
1557
  const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false, side = false, refY) => {
1558
+ // An opener page takes no float (`layout.openerFloats: 'avoid'`, #749):
1559
+ // it waits for the page the flow opens next.
1560
+ if (openerFloatRule === 'avoid' && openerPages.has(page))
1561
+ return 'defer';
1499
1562
  const first = targetCols[0];
1500
1563
  const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap, side, refY);
1501
1564
  if (!probe)
@@ -1568,6 +1631,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
1568
1631
  return 'skip';
1569
1632
  first.availableHeight = Math.max(0, first.availableHeight - need);
1570
1633
  commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
1634
+ noteOpenerFloat(page, f);
1571
1635
  if (drainingAfterText && !calloutFloatOf(f.resourceId)) {
1572
1636
  const citing = contentBlocks[f.firstBlockIdx];
1573
1637
  setAfterText.set(built.block, {
@@ -1843,6 +1907,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
1843
1907
  }
1844
1908
  }
1845
1909
  commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
1910
+ noteOpenerFloat(page, f);
1846
1911
  floatsPlaced++;
1847
1912
  return rest ? { rest } : 'placed';
1848
1913
  };
@@ -2067,8 +2132,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
2067
2132
  const cursorCol = page.columns[cursor.columnIndex];
2068
2133
  if (!cursorCol)
2069
2134
  return false;
2135
+ // (A short list set whole stays under what introduces it: only the
2136
+ // cursor's column counts for it, #749.)
2070
2137
  const candidates = bandColumns(page, currentBand(page, cursor))
2071
- .filter((c) => c.kind !== 'span' && c.index >= cursorCol.index && (c === cursorCol || c.blocks.length === 0));
2138
+ .filter((c) => c.kind !== 'span' && c.index >= cursorCol.index && (c === cursorCol || (!box.stays && c.blocks.length === 0)));
2072
2139
  const fitsIn = (c, available) => available >= box.heightAt(c.bbox.width) - 0.01;
2073
2140
  if (!candidates.some((c) => fitsIn(c, c.availableHeight)))
2074
2141
  return false;
@@ -2083,12 +2150,13 @@ function placeDocumentPass(content, config, cache, options, hints) {
2083
2150
  * empty columns; the band bottom for page-span floats). Runs before each
2084
2151
  * block is placed, so a float lands in the first gap after its reference.
2085
2152
  * `nextBlockIdx` is that block: a keep-together box it opens holds the
2086
- * slots that would starve it (see `slotStarvesBox`). */
2153
+ * slots that would starve it (see `slotStarvesBox`), and so does a
2154
+ * short list set whole (`shortListBoxAt`). */
2087
2155
  const tryPlacePendingFloatsOnCurrentPage = (preferTop = false, nextBlockIdx, anyPosition = false) => {
2088
2156
  if (pendingFloats.length === 0)
2089
2157
  return;
2090
2158
  const page = doc.pages[cursor.pageIndex];
2091
- const box = nextBlockIdx !== undefined ? keepTogetherBoxAt(nextBlockIdx) : null;
2159
+ const box = nextBlockIdx !== undefined ? (keepTogetherBoxAt(nextBlockIdx) ?? shortListBoxAt(nextBlockIdx)) : null;
2092
2160
  for (let i = 0; i < pendingFloats.length;) {
2093
2161
  if (heldBack(i)) {
2094
2162
  i++;
@@ -2848,6 +2916,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
2848
2916
  // setting it one line shorter, #662): painted with it too.
2849
2917
  if (m.letterSpacingPx !== undefined)
2850
2918
  blk.letterSpacing = m.letterSpacingPx;
2919
+ if (m.runtTracking !== undefined)
2920
+ blk.runtFix = { tracking: m.runtTracking };
2851
2921
  blk.lines = resetLinePositions(raggedLooseLines(m.measured.lines, noteStyle.textAlign), noteStyle.lineHeightPx).map((line) => ({
2852
2922
  ...line,
2853
2923
  bbox: createBoundingBox(line.bbox.x + x, line.bbox.y + y, line.bbox.width, line.bbox.height),
@@ -2979,6 +3049,70 @@ function placeDocumentPass(content, config, cache, options, hints) {
2979
3049
  let inlineGapOwed = null;
2980
3050
  /** The pending spacing a heading, a display formula or a box at `idx`
2981
3051
  * collapses its top margin with (see `inlineGapOwed`). */
3052
+ /**
3053
+ * What a heading that opens a column keeps of its space above (#706),
3054
+ * 0 when it keeps none. `s0` is the space it takes above it after a
3055
+ * paragraph of body text, `S` its height plus its bottom margin as the
3056
+ * snap reads them, `u` the column's used height:
3057
+ *
3058
+ * kept = s0 − (snap(u + s0 + S) − snap(u + S))
3059
+ *
3060
+ * the whole grid lines the column head saves are dropped and the rest
3061
+ * stays. `kept` is less than a grid line and `u + kept + S` never passes
3062
+ * `snap(u + S)`, so the box's foot stays on its grid line. A run of
3063
+ * headings (the first does not snap because a heading follows it) reads
3064
+ * as one block: `S` runs from the first heading's top to the last one's
3065
+ * foot plus its bottom margin, the margins between them collapsed as the
3066
+ * placement loop collapses them, and `kept` goes above the first. A run
3067
+ * with a designed, page-span, hidden or `lineSpan` heading in it, or one
3068
+ * whose last heading does not snap, keeps today's placement; so does a
3069
+ * heading whose snapped box does not fit the column.
3070
+ */
3071
+ const columnHeadKept = (blockIdx, rawBlock, usedHeight, height, style, snaps, col, ctx) => {
3072
+ let S;
3073
+ if (snaps) {
3074
+ S = height + style.marginBottomPx;
3075
+ }
3076
+ else {
3077
+ const own = headingLevels.forBlock(rawBlock);
3078
+ const ownSnaps = own?.snapToGrid ?? resolved.headings.snapToGrid;
3079
+ if (!own || !ownSnaps || own.advancedDesign.enabled || nextNonMarkerBlock(contentBlocks, blockIdx)?.type !== 'heading')
3080
+ return 0;
3081
+ let run = height;
3082
+ let pending = style.marginBottomPx;
3083
+ let closed;
3084
+ for (let j = blockIdx + 1; j < contentBlocks.length; j++) {
3085
+ const b = contentBlocks[j];
3086
+ if (isMarkerBlock(b))
3087
+ continue;
3088
+ const lvl = b.type === 'heading' ? headingLevels.forBlock(b) : undefined;
3089
+ if (!lvl || lvl.span === 'page' || lvl.advancedDesign.enabled || headingIsHidden(b, lvl))
3090
+ return 0;
3091
+ const m = measureContentBlock(b, j, col.bbox.width, ctx);
3092
+ if (!m || m.kind.vdtType !== 'heading' || m.kind.style.lineSpan !== undefined)
3093
+ return 0;
3094
+ const st = m.kind.style;
3095
+ run += Math.max(pending, st.marginTopPx) + m.measured.lines.length * st.lineHeightPx;
3096
+ if (nextNonMarkerBlock(contentBlocks, j)?.type === 'heading') {
3097
+ pending = st.marginBottomPx;
3098
+ continue;
3099
+ }
3100
+ if (lvl.snapToGrid ?? resolved.headings.snapToGrid)
3101
+ closed = run + st.marginBottomPx;
3102
+ break;
3103
+ }
3104
+ if (closed === undefined)
3105
+ return 0;
3106
+ S = closed;
3107
+ }
3108
+ const snap = (x) => Math.ceil((x - 0.01) / baselineGrid) * baselineGrid;
3109
+ const s0 = Math.max(ctx.bodyStyle.marginBottomPx, style.marginTopPx);
3110
+ const foot = snap(usedHeight + S);
3111
+ if (foot > col.bbox.height + FIT_EPS)
3112
+ return 0;
3113
+ const kept = s0 - (snap(usedHeight + s0 + S) - foot);
3114
+ return kept > 0.01 ? kept : 0;
3115
+ };
2982
3116
  const pendingBeforeSelfSnapping = (idx) => inlineGapOwed && inlineGapOwed.afterIdx === idx - 1 && pendingSpacing === inlineGapOwed.pending
2983
3117
  ? inlineGapOwed.exact
2984
3118
  : pendingSpacing;
@@ -3324,7 +3458,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
3324
3458
  // The cut is measured from under the float bands the proposing pass
3325
3459
  // had at the band's head, which may not have landed yet.
3326
3460
  const top = Math.max(bandTop(cols), bareBandTop(cols, floatHeadOf) + (cap.headPx ?? 0));
3327
- applyBandCap(cols, cap.lines * baselineGrid, uncappedBottoms, cap.zone, cap.kind === 'trailing', top);
3461
+ // (A trailing cap counts the lines of its band's own leading, #711.)
3462
+ applyBandCap(cols, cap.lines * (cap.pitchPx ?? baselineGrid), uncappedBottoms, cap.zone, cap.kind === 'trailing', top);
3328
3463
  cappedBandColumns.set(spanIndex, cols);
3329
3464
  activeCap = { spanIndex, pageIndex: page.index, band };
3330
3465
  bandCapsApplied.add(spanIndex);
@@ -3349,7 +3484,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
3349
3484
  * the displaced text (see {@link bandCapLinesAroundZone}) — the box then
3350
3485
  * fits on the page, under its columns, rather than opening a page alone.
3351
3486
  */
3352
- const proposeTrailingCap = (boundaryIndex, zone) => {
3487
+ const proposeTrailingCap = (boundaryIndex, zone, closeLevel = false) => {
3353
3488
  const balancingCfg = resolved.headings.balancing;
3354
3489
  if (!balancingCfg.enabled || !balancingCfg.trailing)
3355
3490
  return;
@@ -3385,9 +3520,25 @@ function placeDocumentPass(content, config, cache, options, hints) {
3385
3520
  if (!bandStart || !registeredBand || registeredBand.pageIndex !== page.index || registeredBand.band !== band)
3386
3521
  return;
3387
3522
  const bottoms = cols.map((c) => c.bbox.y + (c.bbox.height - c.availableHeight));
3388
- const aroundZone = zone ? bandCapLinesAroundZone(cols, baselineGrid, zone, (c) => columnBottom(c, uncappedBottoms)) : null;
3389
- if (aroundZone === null && Math.max(...bottoms) - Math.min(...bottoms) <= baselineGrid + 0.5)
3523
+ // The cut counts lines of the band's own text (#711,
3524
+ // `trailingStep: 'lines'`): an index or a list of references set on a
3525
+ // leading of its own is cut, and retried, by whole lines of that
3526
+ // leading, where a cut on the body grid lets a line more into one
3527
+ // column and each retry lowers it by more than a line of the band. A
3528
+ // band of body text has the grid for its pitch.
3529
+ const pitch = balancingCfg.trailingStep === 'grid' ? baselineGrid : bandLinePitch(cols, baselineGrid);
3530
+ const aroundZone = zone ? bandCapLinesAroundZone(cols, pitch, zone, (c) => columnBottom(c, uncappedBottoms)) : null;
3531
+ if (aroundZone === null && Math.max(...bottoms) - Math.min(...bottoms) <= baselineGrid + 0.5) {
3532
+ // A band that is level already needs no cap and no second pass, but
3533
+ // it ends here all the same: where the boundary closes the flow of
3534
+ // its page, its columns are cut on the grid line under its text, as
3535
+ // a delivered cap would leave them, so the column rule stops with
3536
+ // the text and not at the page foot (#713). Nothing is to be filled
3537
+ // up to the cut, so the columns are not flagged `trailingCap`.
3538
+ if (closeLevel)
3539
+ closeLevelBand(cols, bottoms);
3390
3540
  return;
3541
+ }
3391
3542
  // Float bands reserved at the columns' feet are the band's content too:
3392
3543
  // the level cut must leave them room under the text (a figure placed
3393
3544
  // before a span box then anchors to the cut, see `anchorToCap`). A band
@@ -3405,24 +3556,43 @@ function placeDocumentPass(content, config, cache, options, hints) {
3405
3556
  const top = bandTop(cols);
3406
3557
  const floatHeads = cols
3407
3558
  .filter((c) => c.blocks.length === 0 && reservedOf(c).top > 0)
3408
- .map((c) => Math.ceil((c.bbox.y - top - 0.01) / baselineGrid));
3559
+ .map((c) => Math.ceil((c.bbox.y - top - 0.01) / pitch));
3409
3560
  // So does one set under a page-span opener at the head of the opener's
3410
3561
  // own column (#639).
3411
3562
  for (const c of cols) {
3412
3563
  if (c.headFloatFoot !== undefined)
3413
- floatHeads.push(Math.ceil((c.headFloatFoot - top - 0.01) / baselineGrid));
3564
+ floatHeads.push(Math.ceil((c.headFloatFoot - top - 0.01) / pitch));
3414
3565
  }
3415
3566
  bandCapProposals.set(boundaryIndex, {
3416
3567
  kind: 'trailing',
3417
3568
  startContentIndex: bandStart.contentIndex,
3418
3569
  startPart: bandStart.part,
3419
3570
  ...(bandStart.visit > 0 ? { startVisit: bandStart.visit } : {}),
3420
- lines: aroundZone ?? Math.max(bandCapLines(cols, baselineGrid, footBands), ...floatHeads),
3571
+ lines: aroundZone ?? Math.max(bandCapLines(cols, pitch, footBands), ...floatHeads),
3572
+ ...(pitch !== baselineGrid ? { pitchPx: pitch, retryLimit: capRetryLimit(pitch, baselineGrid) } : {}),
3421
3573
  retries: 0,
3422
3574
  ...(aroundZone !== null ? { zone } : {}),
3423
3575
  ...capHeadPx(cols),
3424
3576
  });
3425
3577
  };
3578
+ /** Cut the columns of a closing band that ends level on the grid line
3579
+ * under its lowest text (#713). What a column paints below its used
3580
+ * height counts too (a tail whose negative margin snapped the flow back
3581
+ * above its last line): the renderers clip a column to its box. The true
3582
+ * bottoms go to `uncappedBottoms`, as under a delivered cap, so notes and
3583
+ * floats keep reading the page's foot. */
3584
+ const closeLevelBand = (cols, bottoms) => {
3585
+ const top = bandTop(cols);
3586
+ let lowest = Math.max(...bottoms);
3587
+ for (const c of cols) {
3588
+ for (const b of c.blocks) {
3589
+ if (!b.hidden)
3590
+ lowest = Math.max(lowest, paintedBottom(b), b.bbox.y + b.bbox.height);
3591
+ }
3592
+ }
3593
+ const lines = Math.ceil((lowest - top - 0.01) / baselineGrid);
3594
+ applyBandCap(cols, lines * baselineGrid, uncappedBottoms, undefined, false, top);
3595
+ };
3426
3596
  /** Close the flow at a chapter-level boundary (block `boundaryIndex`):
3427
3597
  * floats take the page's free slots, the closing band is levelled, the
3428
3598
  * page is marked as an explicit break, and every float still pending is
@@ -3458,7 +3628,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
3458
3628
  pendingFloats.splice(i, 1);
3459
3629
  }
3460
3630
  tryPlacePendingFloatsOnCurrentPage();
3461
- proposeTrailingCap(boundaryIndex);
3631
+ proposeTrailingCap(boundaryIndex, undefined, true);
3462
3632
  markForcedBreak();
3463
3633
  hugClosingText();
3464
3634
  drainPendingFloats();
@@ -3613,6 +3783,60 @@ function placeDocumentPass(content, config, cache, options, hints) {
3613
3783
  n++;
3614
3784
  return n;
3615
3785
  };
3786
+ /**
3787
+ * An index group divided between two columns keeps at least
3788
+ * `index.groups.splitMinEntries` entries on each side (#712). `entry` is
3789
+ * the entry block about to leave `col` (it does not fit, or would be
3790
+ * divided between its lines); `k` entries of its group already close the
3791
+ * column and `r` are still to place, this one included. Too few behind
3792
+ * (`k < min`): the `k` go on with it. Too few ahead (`r < min`): enough of
3793
+ * them go on for the next column to open with `min`, and all of them when
3794
+ * that would leave too few behind. The blocks taken off the column are
3795
+ * returned in column order (the caller rewinds to the first), with the
3796
+ * run of headings they would strand; empty when the rule asks for
3797
+ * nothing or cannot be met: the group's entries are all the column holds
3798
+ * under its headings (they would open the next column as they open this
3799
+ * one), or the entry that would lead the move is the rest of one begun
3800
+ * in the column before.
3801
+ */
3802
+ const rollbackIndexGroup = (col, entry, min) => {
3803
+ const info = entry.index;
3804
+ if (!info || min <= 1 || info.groupIndex === undefined || info.groupPlace === undefined || info.groupSize === undefined)
3805
+ return [];
3806
+ const sameGroup = (b) => {
3807
+ const raw = b.contentIndex !== undefined ? contentBlocks[b.contentIndex] : undefined;
3808
+ return raw?.index !== undefined && raw.index.groupIndex === info.groupIndex && raw.sourceStart === entry.sourceStart;
3809
+ };
3810
+ let k = 0;
3811
+ for (let j = col.blocks.length - 1; j >= 0 && sameGroup(col.blocks[j]); j--)
3812
+ k++;
3813
+ if (k === 0)
3814
+ return [];
3815
+ const r = info.groupSize - info.groupPlace;
3816
+ let take = 0;
3817
+ if (k < min)
3818
+ take = k;
3819
+ else if (r < min)
3820
+ take = k - (min - r) < min ? k : min - r;
3821
+ if (take === 0)
3822
+ return [];
3823
+ // Nothing but the group under the column's headings: it stays.
3824
+ const above = col.blocks.slice(0, col.blocks.length - k);
3825
+ if (take === k && above.every((b) => isFreeHeading(b) || b.hidden))
3826
+ return [];
3827
+ // The rest of an entry begun in the column before cannot move back.
3828
+ if (col.blocks[col.blocks.length - take].id.includes('-cont-'))
3829
+ return [];
3830
+ let left = take;
3831
+ const popped = rollbackTrailingBlocks(col, doc.blocks, () => left-- > 0);
3832
+ // The headings the group stood under go with it.
3833
+ const heads = take === k ? rollbackHeadings(col) : [];
3834
+ // The space above the blocks taken off is the column's again.
3835
+ const last = col.blocks[col.blocks.length - 1];
3836
+ if (last)
3837
+ col.availableHeight = col.bbox.y + col.bbox.height - (last.bbox.y + last.bbox.height);
3838
+ return [...heads, ...popped];
3839
+ };
3616
3840
  /** Stamp a callout frame's content index and source range: the whole
3617
3841
  * fence (opening to closing marker) for an unsplit box; for a fragment
3618
3842
  * of a split box, from the fence start (first fragment) or the first
@@ -4090,6 +4314,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
4090
4314
  * to the head or foot of a page, the bands it was floated to.
4091
4315
  */
4092
4316
  const placeSpanFloatsAtCut = (page, capActiveHere) => {
4317
+ // Not on an opener page that takes no float (#749).
4318
+ if (openerFloatRule === 'avoid' && openerPages.has(page))
4319
+ return false;
4093
4320
  let placedAny = false;
4094
4321
  for (let i = 0; i < pendingFloats.length;) {
4095
4322
  const f = pendingFloats[i];
@@ -4135,6 +4362,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
4135
4362
  offsetResourceBlockToAbsolute(built.block.resourceBlock, 0, built.block.bbox.y);
4136
4363
  built.block.contentIndex = f.firstBlockIdx;
4137
4364
  doc.blocks.push(built.block);
4365
+ noteOpenerFloat(page, f);
4138
4366
  floatsPlaced++;
4139
4367
  placedAny = true;
4140
4368
  pendingFloats.splice(i, 1);
@@ -4393,8 +4621,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
4393
4621
  if (levelBefore && !capActive && !proposedSpan) {
4394
4622
  // The box leaves an uncapped band: level it behind the box (a
4395
4623
  // trailing cap, resolved after balancing) and keep balancing from
4396
- // stretching its last column to the page bottom meanwhile.
4397
- proposeTrailingCap(startIdx);
4624
+ // stretching its last column to the page bottom meanwhile. A band
4625
+ // that is level already is closed at its text (#713).
4626
+ proposeTrailingCap(startIdx, undefined, true);
4398
4627
  markForcedBreak();
4399
4628
  }
4400
4629
  else if (capActive && cap?.kind === 'trailing') {
@@ -4921,6 +5150,65 @@ function placeDocumentPass(content, config, cache, options, hints) {
4921
5150
  return true;
4922
5151
  }
4923
5152
  };
5153
+ /** Whether a resource set in the text asks for its caption in the side
5154
+ * column (#729): `placement.captionSide` on a `span: 'column'` embed,
5155
+ * `layout.inlineCaptionSide` on, a horizontal page, and no text wrapped
5156
+ * round it. The side column and its room are looked at where the block
5157
+ * is placed ({@link measureInlineAside}). */
5158
+ const inlineAsideWanted = (resource, type, page) => {
5159
+ if (!resource || !resolved.layout.inlineCaptionSide || pageIsVertical(page))
5160
+ return false;
5161
+ const placement = resolveResourcePlacement(resource, type);
5162
+ if (!placement.captionSide || placement.span !== 'column')
5163
+ return false;
5164
+ return resolveResourceWrap(resource, type, resolved.layout.wrap) === undefined;
5165
+ };
5166
+ /** Whether the side column `sc` is free over the band a caption
5167
+ * `asideHeight` tall takes beside an embed placed next in `c`: the side
5168
+ * stack ends above the embed's top, the band ends inside the side
5169
+ * column, and no obstacle (a heading design, another embed's caption)
5170
+ * stands in it or within the float gap of it. */
5171
+ const inlineAsideFree = (c, sc, spacing, asideHeight) => {
5172
+ const top = c.bbox.y + (c.bbox.height - c.availableHeight) + (c.blocks.length === 0 ? 0 : spacing);
5173
+ const bottom = top + asideHeight;
5174
+ if (sideUsedBottom(sc) > top + 0.5)
5175
+ return false;
5176
+ if (bottom > sc.bbox.y + sc.bbox.height + 0.5)
5177
+ return false;
5178
+ return !(sideObstacles.get(sc) ?? []).some((o) => o.bottom + floatGapPx > top + 0.5 && o.top - floatGapPx < bottom - 0.5);
5179
+ };
5180
+ /** The embed measured for column `c` with its caption and note in the
5181
+ * band's float-only side column, level with the body's top (#729), or
5182
+ * `null` when the band has no such column or it is not free there: the
5183
+ * caption then stays under the body. */
5184
+ const measureInlineAside = (raw, blockIdx, c, page, ctx, base, spacing) => {
5185
+ if (c.kind === 'span' || c.kind === 'side')
5186
+ return null;
5187
+ const sideCol = sideColumnOf(page, c.band ?? 0);
5188
+ if (!sideCol || sideCol.bbox.width <= 0.5)
5189
+ return null;
5190
+ const opts = { captionAside: { dx: sideCol.bbox.x - c.bbox.x, width: sideCol.bbox.width } };
5191
+ const block = measureContentBlock(raw, blockIdx, c.bbox.width, ctx, { ...base, ...opts });
5192
+ if (!block?.resourceBlock || block.asideHeight === undefined || block.resourceBlock.rotation)
5193
+ return null;
5194
+ if (!inlineAsideFree(c, sideCol, spacing, block.asideHeight))
5195
+ return null;
5196
+ return { block, opts, sideCol, col: c };
5197
+ };
5198
+ /** The band an embed's side caption takes in the side column, recorded
5199
+ * as an obstacle of its stack: side floats and side boxes set later go
5200
+ * above it when they fit there, else under it. */
5201
+ const markInlineAside = (blk, sideCol, asideHeight) => {
5202
+ if (!(asideHeight > 0))
5203
+ return;
5204
+ const mark = { owner: blk, top: blk.bbox.y, bottom: blk.bbox.y + asideHeight };
5205
+ const list = sideObstacles.get(sideCol);
5206
+ if (list)
5207
+ list.push(mark);
5208
+ else
5209
+ sideObstacles.set(sideCol, [mark]);
5210
+ sideObstacleColumn.set(blk, sideCol);
5211
+ };
4924
5212
  /**
4925
5213
  * An inline table (`::resource` placed `here`) that does not fit the room
4926
5214
  * left in its column (#634), by its style's `overflow` when
@@ -4937,10 +5225,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
4937
5225
  * table is placed as one block by the caller (it fits, or the rule does
4938
5226
  * not apply), `'dropped'`, or `'placed'`: every slice is placed and the
4939
5227
  * cursor sits in the column of the last one, which the caller snaps
4940
- * under. `blk` is the first slice's block, filled in here.
5228
+ * under. `blk` is the first slice's block, filled in here. With
5229
+ * `asideWanted` the first slice sets its caption in the side column when
5230
+ * that is free beside it (#729); the continuations are set as ever.
4941
5231
  */
4942
5232
  const MIN_HEAD_ROWS = 2;
4943
- const placeInlineTableSlices = (raw, blockIdx, blk, ctx, styleOverride, resource, wholeHeight, spacingBefore) => {
5233
+ const placeInlineTableSlices = (raw, blockIdx, blk, ctx, styleOverride, resource, wholeHeight, spacingBefore, asideWanted = false) => {
4944
5234
  const style = pickTableStyle(resolved, resource.table?.styleId);
4945
5235
  if (!style.splitInline || pageIsVertical(doc.pages[cursor.pageIndex]))
4946
5236
  return 'whole';
@@ -4988,6 +5278,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
4988
5278
  let part = 0;
4989
5279
  let moves = 0;
4990
5280
  let block = blk;
5281
+ /** Columns where the first slice's caption found no room at the side. */
5282
+ const asideRefused = new Set();
4991
5283
  for (;;) {
4992
5284
  enterBand(blockIdx, part);
4993
5285
  let col = currentColumn(doc, cursor);
@@ -4999,8 +5291,15 @@ function placeDocumentPass(content, config, cache, options, hints) {
4999
5291
  const metrics = metricsAt(width) ?? firstMetrics;
5000
5292
  const spacing = part === 0 && col.blocks.length > 0 ? spacingBefore : 0;
5001
5293
  const avail = col.availableHeight - spacing;
5294
+ // The first slice with its caption in the side column (#729).
5295
+ const asideHere = asideWanted && startRow === 0 && !asideRefused.has(col)
5296
+ ? measureInlineAside(raw, blockIdx, col, doc.pages[cursor.pageIndex], ctx, { styleOverride }, spacing)
5297
+ : null;
5298
+ const sliceAt = (s) => (asideHere
5299
+ ? measureContentBlock(raw, blockIdx, width, ctx, { styleOverride, ...(s ? { tableSlice: s } : {}), ...asideHere.opts })
5300
+ : measureAt(width, s));
5002
5301
  const restSlice = startRow > 0 ? { startRow, endRow: rowCount, continues: false } : undefined;
5003
- const rest = measureAt(width, restSlice);
5302
+ const rest = sliceAt(restSlice);
5004
5303
  if (!rest?.resourceBlock)
5005
5304
  return part === 0 ? 'whole' : 'placed';
5006
5305
  let placed = rest;
@@ -5014,7 +5313,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5014
5313
  continue;
5015
5314
  }
5016
5315
  const firstBody = startRow > 0 ? Math.max(startRow, headerRows) : headerRows;
5017
- const probe = measureAt(width, { startRow, endRow: firstBody + 1, continues: true });
5316
+ const probe = sliceAt({ startRow, endRow: firstBody + 1, continues: true });
5018
5317
  const overhead = probe?.resourceBlock ? probe.measured.totalHeight - probe.resourceBlock.bodyRect.height : 0;
5019
5318
  // An empty column of full height (or a run of short ones behind)
5020
5319
  // takes the smallest slice even when it overflows: a fresh page
@@ -5024,7 +5323,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5024
5323
  floor: floorOf(startRow),
5025
5324
  force,
5026
5325
  split: overflow === 'split',
5027
- fits: (s) => (measureAt(width, s)?.measured.totalHeight ?? Infinity) <= avail + FIT_EPS,
5326
+ fits: (s) => (sliceAt(s)?.measured.totalHeight ?? Infinity) <= avail + FIT_EPS,
5028
5327
  });
5029
5328
  if (!slice) {
5030
5329
  // Not even the head rows fit: the (rest of the) table starts in
@@ -5034,11 +5333,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
5034
5333
  advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(minHeightAt(width, startRow)));
5035
5334
  continue;
5036
5335
  }
5037
- const m = measureAt(width, slice);
5336
+ const m = sliceAt(slice);
5038
5337
  if (!m?.resourceBlock)
5039
5338
  return part === 0 ? 'whole' : 'placed';
5040
5339
  placed = m;
5041
5340
  }
5341
+ // The slice's caption (a line longer with its continuation suffix,
5342
+ // perhaps) must still find the side column free.
5343
+ if (asideHere && (placed.asideHeight === undefined || !inlineAsideFree(col, asideHere.sideCol, spacing, placed.asideHeight))) {
5344
+ asideRefused.add(col);
5345
+ continue;
5346
+ }
5042
5347
  // The slice's block: the first one is the caller's.
5043
5348
  if (part > 0) {
5044
5349
  block = createVDTBlock(`block-${blockIdCounter++}`, 'resource', blk.fontString, blk.color, blk.textAlign);
@@ -5065,6 +5370,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
5065
5370
  col.availableHeight -= spacing;
5066
5371
  placeBlockInColumn(block, height, col, cursor);
5067
5372
  offsetResourceBlockToAbsolute(rb, block.bbox.x, block.bbox.y);
5373
+ if (asideHere && placed.asideHeight !== undefined)
5374
+ markInlineAside(block, asideHere.sideCol, placed.asideHeight);
5068
5375
  doc.blocks.push(block);
5069
5376
  enterBand(blockIdx, part);
5070
5377
  if (!slice?.continues)
@@ -5316,6 +5623,188 @@ function placeDocumentPass(content, config, cache, options, hints) {
5316
5623
  slackWeight: body.slackWeight,
5317
5624
  }).splitAt > 0;
5318
5625
  };
5626
+ // --- Short lists kept whole (`bodyText.shortListSplit`, #749) -------------
5627
+ const shortListRule = resolved.bodyText.shortListSplit;
5628
+ /** The short lists of the running text by the content index of their
5629
+ * first item (`null`: a block that opens none). */
5630
+ const shortLists = new Map();
5631
+ /**
5632
+ * The short list content block `idx` opens: a run of list items (seen
5633
+ * through container markers, its nested lists included) of up to
5634
+ * `bodyText.shortListMaxItems` items, `idx` being its first. Not the
5635
+ * entries of the contents or of an index, which are set as list items.
5636
+ */
5637
+ const shortListFrom = (idx) => {
5638
+ const known = shortLists.get(idx);
5639
+ if (known !== undefined)
5640
+ return known ?? undefined;
5641
+ const isItem = (b) => b?.type === 'listItem' && b.toc === undefined && b.index === undefined;
5642
+ let found = null;
5643
+ if (isItem(contentBlocks[idx]) && prevNonMarkerBlock(contentBlocks, idx)?.type !== 'listItem') {
5644
+ const items = [];
5645
+ let i = idx;
5646
+ for (; i < contentBlocks.length; i++) {
5647
+ const b = contentBlocks[i];
5648
+ if (isMarkerBlock(b))
5649
+ continue;
5650
+ if (!isItem(b))
5651
+ break;
5652
+ items.push(i);
5653
+ if (items.length > resolved.bodyText.shortListMaxItems)
5654
+ break;
5655
+ }
5656
+ if (items.length <= resolved.bodyText.shortListMaxItems)
5657
+ found = { items };
5658
+ }
5659
+ shortLists.set(idx, found);
5660
+ return found ?? undefined;
5661
+ };
5662
+ /** The room a short list takes in a column `width` wide, from the top of
5663
+ * its first line to the foot of its last (the space above it aside),
5664
+ * and its lines, for the notes they cite. Undefined when an item
5665
+ * cannot be measured. */
5666
+ const shortListNeed = (list, width, ctx) => {
5667
+ let height = 0;
5668
+ const lines = [];
5669
+ let prev;
5670
+ for (const idx of list.items) {
5671
+ const raw = contentBlocks[idx];
5672
+ const item = measureContentBlock(raw, idx, width, ctx);
5673
+ if (!item)
5674
+ return undefined;
5675
+ if (prev) {
5676
+ height += listItemGapPx({ spacingPx: listItemSpacingPx(prev, ctx.resolved), depth: prev.depth ?? 1 }, { spacingPx: listItemSpacingPx(raw, ctx.resolved), depth: raw.depth ?? 1 });
5677
+ }
5678
+ height += item.measured.lines.length * item.kind.style.lineHeightPx;
5679
+ lines.push(...item.measured.lines);
5680
+ prev = raw;
5681
+ }
5682
+ return { height, lines };
5683
+ };
5684
+ /**
5685
+ * The room the floats text block `blockIdx` cites would take from the
5686
+ * foot of `col` once their citing lines are placed: for each one still
5687
+ * held back for it, the band of its first slot on the page, when that is
5688
+ * a foot band over `col` that the room the block leaves under it
5689
+ * (`roomAfter`) still holds. Read before the block is placed, so an
5690
+ * estimate: the colon line of a short list set whole counts it, since
5691
+ * the figure that line cites takes the foot of the column right after
5692
+ * it, where the list would have gone.
5693
+ */
5694
+ const citedFootTake = (blockIdx, col, roomAfter) => {
5695
+ const page = doc.pages[cursor.pageIndex];
5696
+ if (openerFloatRule === 'avoid' && openerPages.has(page))
5697
+ return 0;
5698
+ let take = 0;
5699
+ for (const [id, gate] of citationGates) {
5700
+ if (gate.blockIdx !== blockIdx)
5701
+ continue;
5702
+ const f = pendingFloats.find((p) => p.resourceId === id);
5703
+ const slot = f ? enumerateCurrentPageSlots(page, cursor.columnIndex, f, capKindOf, openerHeadOf)[0] : undefined;
5704
+ if (!f || !slot || slot.side || slot.position !== 'bottom' || !slot.cols.includes(col))
5705
+ continue;
5706
+ const probe = probeFloatBand(page, f, slot.cols, slot.position, slot.pageSpan, false);
5707
+ if (!probe)
5708
+ continue;
5709
+ const band = col.availableHeight - availableAfterBand(col, 'bottom', probe, false);
5710
+ if (band <= roomAfter + FIT_EPS)
5711
+ take = Math.max(take, band);
5712
+ }
5713
+ return take;
5714
+ };
5715
+ /** The short list item `idx` belongs to, and its place in it. */
5716
+ const shortListItemAt = (idx) => {
5717
+ if (contentBlocks[idx]?.type !== 'listItem')
5718
+ return undefined;
5719
+ let start = idx;
5720
+ for (let i = idx - 1, seen = 0; i >= 0 && seen <= resolved.bodyText.shortListMaxItems; i--) {
5721
+ const b = contentBlocks[i];
5722
+ if (isMarkerBlock(b))
5723
+ continue;
5724
+ if (b.type !== 'listItem')
5725
+ break;
5726
+ start = i;
5727
+ seen++;
5728
+ }
5729
+ const list = shortListFrom(start);
5730
+ const item = list ? list.items.indexOf(idx) : -1;
5731
+ return list && item >= 0 ? { list, item } : undefined;
5732
+ };
5733
+ /**
5734
+ * Under `'avoid'`, the short list content block `idx` opens or goes on
5735
+ * with, as the floats offered a slot right before it see a keep-together
5736
+ * box (`slotStarvesBox`): its height at a column width, from that item
5737
+ * on. A float yields to it when the cursor's column holds the list whole
5738
+ * and would not with the float: the figure a list's own lead-in cites
5739
+ * would otherwise take the foot of the column once its citing line is
5740
+ * placed, and cut the list under it.
5741
+ */
5742
+ const shortListBoxAt = (idx) => {
5743
+ if (shortListRule !== 'avoid')
5744
+ return null;
5745
+ const at = shortListItemAt(idx);
5746
+ if (!at)
5747
+ return null;
5748
+ const ctx = partPlan.byBlock[idx] ? partMeasureCtx : sectionMeasureCtx(sectionPlan.byBlock[idx]);
5749
+ const rest = { items: at.list.items.slice(at.item) };
5750
+ const heights = new Map();
5751
+ return {
5752
+ stays: true,
5753
+ heightAt: (width) => {
5754
+ const key = Math.round(width * 100);
5755
+ let h = heights.get(key);
5756
+ if (h === undefined) {
5757
+ const need = shortListNeed(rest, width, ctx);
5758
+ h = need ? need.height + pendingSpacing : 0;
5759
+ heights.set(key, h);
5760
+ }
5761
+ return h;
5762
+ },
5763
+ };
5764
+ };
5765
+ /** A `shortListSplit` warning for each short list whose items the pass
5766
+ * set in more than one column: all of them under `'warn'`, under
5767
+ * `'avoid'` the ones no column could hold whole. */
5768
+ const shortListWarnings = () => {
5769
+ if (shortListRule === 'allow')
5770
+ return [];
5771
+ // Where each list item of the running text landed (a box's children
5772
+ // carry `containerId`).
5773
+ const placed = new Map();
5774
+ for (const page of doc.pages) {
5775
+ for (const c of page.columns) {
5776
+ for (const b of c.blocks) {
5777
+ if (b.type !== 'listItem' || b.containerId !== undefined || b.contentIndex === undefined)
5778
+ continue;
5779
+ const at = placed.get(b.contentIndex) ?? { pageIndex: page.index, columns: new Set() };
5780
+ at.columns.add(`${page.index}:${c.index}`);
5781
+ placed.set(b.contentIndex, at);
5782
+ }
5783
+ }
5784
+ }
5785
+ const out = [];
5786
+ for (let idx = 0; idx < contentBlocks.length; idx++) {
5787
+ const list = placed.has(idx) ? shortListFrom(idx) : undefined;
5788
+ if (!list)
5789
+ continue;
5790
+ const columns = new Set();
5791
+ for (const item of list.items)
5792
+ for (const key of placed.get(item)?.columns ?? [])
5793
+ columns.add(key);
5794
+ if (columns.size < 2)
5795
+ continue;
5796
+ const first = contentBlocks[list.items[0]];
5797
+ const last = contentBlocks[list.items[list.items.length - 1]];
5798
+ out.push({
5799
+ kind: 'shortListSplit',
5800
+ items: list.items.length,
5801
+ sourceStart: first.sourceStart + bodyOffset,
5802
+ sourceEnd: last.sourceEnd + bodyOffset,
5803
+ pageIndex: placed.get(idx).pageIndex,
5804
+ });
5805
+ }
5806
+ return out;
5807
+ };
5319
5808
  /** The page break into a page of the parity `even` asks (a spread opens
5320
5809
  * on a verso, an even page): the page the cursor stands on, an empty
5321
5810
  * one, is marked a parity blank and passed while its number is odd. Even
@@ -5433,6 +5922,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
5433
5922
  inlineGapOwed = pendingSpacing > 0 ? { afterIdx: blockIdx, pending: pendingSpacing, exact: owed } : null;
5434
5923
  return undefined;
5435
5924
  };
5925
+ /** The first block of the document that sets something: a heading there
5926
+ * opens the document's first page (`openerPages`, #749). */
5927
+ const firstFlowBlockIdx = contentBlocks.findIndex((b) => !isMarkerBlock(b) && b.type !== 'directive');
5436
5928
  for (let blockIdx = 0; blockIdx < contentBlocks.length; blockIdx++) {
5437
5929
  if (options?.shouldCancel?.())
5438
5930
  throw new BuildCancelledError();
@@ -5606,6 +6098,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5606
6098
  titleSourceStart,
5607
6099
  titleSourceEnd,
5608
6100
  }, pageIndexOffset);
6101
+ openerPages.add(doc.pages[cursor.pageIndex]);
5609
6102
  flushPendingNumberingAtBoundary();
5610
6103
  continue;
5611
6104
  }
@@ -5774,6 +6267,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
5774
6267
  if (rawBlock.type === 'heading' && rawBlock.level) {
5775
6268
  const level = headingLevels.forBlock(rawBlock);
5776
6269
  const bb = level?.breakBefore;
6270
+ /** The heading opens its page (#749): with its own break, or as the
6271
+ * first block of the document. */
6272
+ let opensPage = blockIdx === firstFlowBlockIdx;
5777
6273
  if (bb && bb.enabled) {
5778
6274
  pendingSpacing = 0;
5779
6275
  closeFlowSegment(blockIdx);
@@ -5782,6 +6278,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5782
6278
  enforcePageParity(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, bb.parity);
5783
6279
  }
5784
6280
  flushPendingNumberingAtBoundary();
6281
+ opensPage = true;
5785
6282
  }
5786
6283
  // `span: 'page'` headings open a chapter band across the full content
5787
6284
  // width. Always start on a fresh page boundary so the band sits at the
@@ -5795,7 +6292,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
5795
6292
  advanceToNextPageBoundary(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx);
5796
6293
  cursor.columnIndex = 0;
5797
6294
  flushPendingNumberingAtBoundary();
6295
+ opensPage = true;
5798
6296
  }
6297
+ if (opensPage)
6298
+ openerPages.add(doc.pages[cursor.pageIndex]);
5799
6299
  }
5800
6300
  const id = `block-${blockIdCounter++}`;
5801
6301
  // Enclosing `:::paragraphs` container (if any). Its last paragraph — the
@@ -5828,8 +6328,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
5828
6328
  * block keeps its own space in its snap, and the container's
5829
6329
  * `marginBottom` follows as pending spacing from the closing marker. */
5830
6330
  const containerTail = isContainerTail && rawBlock.type === 'paragraph' ? paragraphContainer : undefined;
6331
+ // (A paragraph style may set the rule for its own containers, #715.)
5831
6332
  const collapseTail = containerTail !== undefined
5832
- && resolved.bodyText.paragraphContainerSpacing === 'collapse'
6333
+ && (containerTail.spacing ?? resolved.bodyText.paragraphContainerSpacing) === 'collapse'
5833
6334
  && containerTail.tailStyle.marginBottomPx >= 0;
5834
6335
  const tailStyle = collapseTail && blockMeasureCtx.bodyStyle.marginBottomPx > containerTail.tailStyle.marginBottomPx
5835
6336
  ? { ...containerTail.tailStyle, marginBottomPx: blockMeasureCtx.bodyStyle.marginBottomPx }
@@ -5872,6 +6373,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
5872
6373
  let measuredBlock = tryLoose
5873
6374
  ? measureLooseParagraph(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, styleOverride, looseLines, trackingLadder, looseOutcome, looseOffGrid)
5874
6375
  : measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, { styleOverride, ...upright, ...figureDelta });
6376
+ // A resource set in the text with `captionSide` (#729): its caption and
6377
+ // note go to the band's side column when that is free beside it, and
6378
+ // the block is as tall as the body alone. Decided again in every
6379
+ // column the block is tried in (see the resource branch below).
6380
+ const asideWanted = measuredBlock?.kind.vdtType === 'resource' && rawBlock.type === 'resourceBlock'
6381
+ && inlineAsideWanted(measuredBlock.kind.resource, measuredBlock.kind.resourceType, blockPage);
6382
+ let inlineAside = asideWanted
6383
+ ? measureInlineAside(rawBlock, blockIdx, col, blockPage, blockMeasureCtx, { styleOverride, ...figureDelta }, Math.max(pendingSpacing, floatGapPx))
6384
+ : null;
6385
+ if (inlineAside)
6386
+ measuredBlock = inlineAside.block;
6387
+ const asideOpts = inlineAside?.opts;
5875
6388
  // An inline picture with a safe area a little too tall for the room
5876
6389
  // left in its column is cropped within its safe area to stay there
5877
6390
  // rather than move on and leave the column short (#442).
@@ -5881,7 +6394,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5881
6394
  const over = measuredBlock.measured.totalHeight - room;
5882
6395
  if (over > 0.01 && room > 0 && over <= flex.shrink + 0.01) {
5883
6396
  const cropped = measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, {
5884
- styleOverride, ...upright, figureHeightDelta: flex.delta - over,
6397
+ styleOverride, ...upright, ...asideOpts, figureHeightDelta: flex.delta - over,
5885
6398
  });
5886
6399
  if (cropped && cropped.measured.totalHeight <= room + 0.01)
5887
6400
  measuredBlock = cropped;
@@ -5905,7 +6418,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5905
6418
  width = current.bodyRect.width * Math.min(scale, 0.99);
5906
6419
  if (width < rb.bodyRect.width * MIN_INLINE_FIGURE_SCALE)
5907
6420
  break;
5908
- const smaller = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride, figureMaxBodyWidth: width });
6421
+ const smaller = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride, ...asideOpts, figureMaxBodyWidth: width });
5909
6422
  if (!smaller)
5910
6423
  break;
5911
6424
  measuredBlock = smaller;
@@ -5915,10 +6428,13 @@ function placeDocumentPass(content, config, cache, options, hints) {
5915
6428
  // Still too tall at the smallest acceptable size: back to full size
5916
6429
  // and on to the next column, as without the option.
5917
6430
  if (measuredBlock.measured.totalHeight > room + FIT_EPS) {
5918
- measuredBlock = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride });
6431
+ measuredBlock = measureContentBlock(rawBlock, blockIdx, col.bbox.width, blockMeasureCtx, { styleOverride, ...asideOpts });
5919
6432
  }
5920
6433
  }
5921
6434
  }
6435
+ // The fits above keep the caption at the side: they work on the body.
6436
+ if (inlineAside)
6437
+ inlineAside = measuredBlock?.asideHeight !== undefined ? { ...inlineAside, block: measuredBlock } : null;
5922
6438
  if (tryLoose && budget !== undefined && knownOutcome === undefined && typeof looseOutcome.get(blockIdx) === 'number') {
5923
6439
  looseGained.set(budget.group, (looseGained.get(budget.group) ?? 0) + 1);
5924
6440
  }
@@ -5931,7 +6447,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5931
6447
  let capInfo = measuredBlock.dropCap;
5932
6448
  /** Tracking the block's lines are set with; a paragraph moved whole into
5933
6449
  * a column of another width is measured again, and may change it. */
5934
- let { letterSpacingPx } = measuredBlock;
6450
+ let { letterSpacingPx, runtTracking } = measuredBlock;
5935
6451
  const { vdtType, headingLevel, numberPrefix, numberSeparator, headingNumber, listBullet, listDepth, listKind, bulletXOffsetInColumn, strikethroughText } = kind;
5936
6452
  // A structural heading (`hidden`) is placed like any heading — its
5937
6453
  // break, keep-with-next and grid snap apply — but with no line height
@@ -5950,8 +6466,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
5950
6466
  // --- Resource blocks (image / svg / table + caption) -----------------
5951
6467
  // Placed atomically (kept-together) — no mid-content split for v1.
5952
6468
  if (vdtType === 'resource') {
5953
- const resourceBlock = measuredBlock.resourceBlock;
5954
- const groupHeight = measured.totalHeight;
6469
+ let resourceBlock = measuredBlock.resourceBlock;
6470
+ let groupHeight = measured.totalHeight;
5955
6471
  const blk = createVDTBlock(id, 'resource', style.fontString, style.color, style.textAlign);
5956
6472
  blk.contentIndex = blockIdx;
5957
6473
  stampBlockExtras(blk, rawBlock);
@@ -6010,7 +6526,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
6010
6526
  // An inline table that does not fit the room left in its column is
6011
6527
  // cut between rows (#634), by the rules of a floated one.
6012
6528
  const sliced = !wraps && !resourceBlock.rotation && resourceBlock.table && kind.resource
6013
- ? placeInlineTableSlices(rawBlock, blockIdx, blk, blockMeasureCtx, styleOverride, kind.resource, groupHeight, spacingBefore)
6529
+ ? placeInlineTableSlices(rawBlock, blockIdx, blk, blockMeasureCtx, styleOverride, kind.resource, groupHeight, spacingBefore, asideWanted)
6014
6530
  : 'whole';
6015
6531
  if (sliced === 'dropped') {
6016
6532
  flushPendingNumberingAtBoundary();
@@ -6019,6 +6535,44 @@ function placeDocumentPass(content, config, cache, options, hints) {
6019
6535
  const columnBefore = col;
6020
6536
  if (sliced === 'whole') {
6021
6537
  enterBand(blockIdx, 0);
6538
+ // A side caption is decided where the block is placed (#729): an
6539
+ // embed that does not fit its column moves on by the rule of
6540
+ // `placeAtomicBlock`, and in each column it reaches its caption
6541
+ // goes to the side when the side column is free there, else under
6542
+ // the body.
6543
+ if (asideWanted) {
6544
+ let decidedFor = col;
6545
+ let plain = inlineAside ? undefined : measuredBlock;
6546
+ for (let guard = 0;; guard++) {
6547
+ const c = currentColumn(doc, cursor);
6548
+ if (c !== decidedFor) {
6549
+ decidedFor = c;
6550
+ inlineAside = measureInlineAside(rawBlock, blockIdx, c, doc.pages[cursor.pageIndex], blockMeasureCtx, { styleOverride, ...figureDelta }, spacingBefore);
6551
+ if (!inlineAside && plain === undefined) {
6552
+ plain = measureContentBlock(rawBlock, blockIdx, measureWidth, blockMeasureCtx, { styleOverride, ...figureDelta });
6553
+ }
6554
+ const next = inlineAside?.block ?? plain;
6555
+ if (next?.resourceBlock) {
6556
+ resourceBlock = next.resourceBlock;
6557
+ groupHeight = next.measured.totalHeight;
6558
+ }
6559
+ else {
6560
+ inlineAside = null;
6561
+ }
6562
+ }
6563
+ const available = c.availableHeight - (c.blocks.length === 0 ? 0 : spacingBefore);
6564
+ if (groupHeight <= available + FIT_EPS)
6565
+ break;
6566
+ if (c.blocks.length === 0 && c.availableHeight >= 0.5 && groupHeight > contentArea.height + FIT_EPS)
6567
+ break;
6568
+ if (guard >= 8)
6569
+ break;
6570
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(groupHeight));
6571
+ }
6572
+ blk.resourceBlock = resourceBlock;
6573
+ blk.lines[0].bbox = { x: 0, y: 0, width: resourceBlock.bodyRect.width, height: groupHeight };
6574
+ }
6575
+ const decidedColumn = currentColumn(doc, cursor);
6022
6576
  placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(groupHeight));
6023
6577
  enterBand(blockIdx, 0);
6024
6578
  // Too tall for the room left in its column, a wrapped picture moved
@@ -6033,6 +6587,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
6033
6587
  // resource's own caption/table lines live on `resourceBlock` and must be
6034
6588
  // offset to absolute page coordinates here using the placed bbox origin.
6035
6589
  offsetResourceBlockToAbsolute(resourceBlock, blk.bbox.x, blk.bbox.y);
6590
+ if (inlineAside && inlineAside.block.asideHeight !== undefined && currentColumn(doc, cursor) === decidedColumn) {
6591
+ markInlineAside(blk, inlineAside.sideCol, inlineAside.block.asideHeight);
6592
+ }
6036
6593
  const grown = figureGrowPx > 0 ? resourceBlock.bodyFlex?.delta ?? 0 : 0;
6037
6594
  if (grown > 0.01) {
6038
6595
  blk.balancing = { levers: ['flexFigure'], spaceAbove: 0, bodyGrowth: grown };
@@ -6219,6 +6776,23 @@ function placeDocumentPass(content, config, cache, options, hints) {
6219
6776
  /** Whether the paragraph was already re-broken to keep a hyphen off
6220
6777
  * the last line of a column (`bodyText.hyphenateAcrossColumns`). */
6221
6778
  let columnEndHyphenRetried = false;
6779
+ /** The lines (1-based, counted from the first line still to place) the
6780
+ * guard keeps a hyphen off: the line the split falls after and one
6781
+ * every full column after it, or, under `'withinPage'` (#701), those
6782
+ * of them that close a page — the j-th column end from here does when
6783
+ * the `rest` columns left on this page that still take a line of this
6784
+ * text are used up and a whole number of fresh pages, of `perPage`
6785
+ * flow columns each, has gone by. */
6786
+ const guardedColumnEnds = (mode, splitAt, lineCount, fullColumn, page, columnIndex) => {
6787
+ const ends = [];
6788
+ const rest = mode === false ? 0 : flowColumnsAfter(page, columnIndex, style.lineHeightPx);
6789
+ const perPage = mode === false ? 1 : freshPageFlowColumns();
6790
+ for (let n = splitAt, j = 0; n < lineCount; n += fullColumn, j++) {
6791
+ if (j >= rest && (j - rest) % perPage === 0)
6792
+ ends.push(n);
6793
+ }
6794
+ return ends;
6795
+ };
6222
6796
  /** `hyphenateAcrossColumns: false`: the paragraph's lines as set now
6223
6797
  * (a re-break replaces them) and where the breaker broke them, the
6224
6798
  * lines (1-based) its last re-break kept a hyphen off, and the part
@@ -6288,6 +6862,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
6288
6862
  laterWidths = [];
6289
6863
  wrapInsets = [];
6290
6864
  letterSpacingPx = again.letterSpacingPx;
6865
+ runtTracking = again.runtTracking;
6291
6866
  remainingLines = [...raggedLooseLines(again.measured.lines, style.textAlign)];
6292
6867
  paragraphLines = remainingLines;
6293
6868
  paragraphBreaks = again.measured.breaks;
@@ -6462,6 +7037,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
6462
7037
  if (placed === 0) {
6463
7038
  capInfo = again.dropCap;
6464
7039
  letterSpacingPx = again.letterSpacingPx;
7040
+ runtTracking = again.runtTracking;
6465
7041
  guardedEnds = [];
6466
7042
  columnEndHyphenRetried = false;
6467
7043
  }
@@ -6493,7 +7069,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
6493
7069
  // block fits when its lines and all their notes do, and a split
6494
7070
  // keeps the lines whose notes fit under them.
6495
7071
  const noteCosts = vdtType === 'mathDisplay' ? undefined : notesPrefixCost(curCol, remainingLines);
6496
- const effectiveAvailable = curCol.availableHeight - spacingBefore - (noteCosts ? noteCosts[noteCosts.length - 1] : 0);
7072
+ let effectiveAvailable = curCol.availableHeight - spacingBefore - (noteCosts ? noteCosts[noteCosts.length - 1] : 0);
6497
7073
  // A hair of tolerance: room of exactly N lines (grid arithmetic in
6498
7074
  // floats — a column cut by a band cap, a balancing extra line) must
6499
7075
  // hold N lines, not N - 1.
@@ -6567,6 +7143,28 @@ function placeDocumentPass(content, config, cache, options, hints) {
6567
7143
  headingDesign = { lvl, info };
6568
7144
  }
6569
7145
  }
7146
+ // With `headings.columnHeadSpace: 'remainder'` (#706) a heading that
7147
+ // opens a column drops its space above in whole grid lines only: what is
7148
+ // left of a line stays above it, so its text starts as far under it
7149
+ // as under the same heading set after a paragraph. The foot of its
7150
+ // box stays on the grid line the snap puts it on, and no line of body
7151
+ // text moves. A heading the grid does not snap, a hidden one, a
7152
+ // `lineSpan` band, a page-span opener and a heading painted from its
7153
+ // own design (its band is the design's, drawn from the column's head)
7154
+ // keep the head of the column; so does the first block of a column
7155
+ // under an opener band, which is set level with the first block
7156
+ // under the opener (`openerHead`).
7157
+ if (vdtType === 'heading' && partIndex === 0 && !hiddenHeading && lineSpan === undefined
7158
+ && resolved.headings.columnHeadSpace === 'remainder'
7159
+ && (isFirstInColumn || underOpenerHeadFloat(curCol)) && openerHead === undefined
7160
+ && !headingSetsOwnBand(headingLevels.forBlock(rawBlock))) {
7161
+ const usedHeight = curCol.bbox.height - curCol.availableHeight;
7162
+ const kept = columnHeadKept(blockIdx, rawBlock, usedHeight, effectiveRemainHeight, style, shouldSnapToGrid, curCol, blockMeasureCtx);
7163
+ if (kept > 0) {
7164
+ spacingBefore += kept;
7165
+ effectiveAvailable -= kept;
7166
+ }
7167
+ }
6570
7168
  // Keep-with-list: if this colon-paragraph would fit but would leave no
6571
7169
  // room for the first list item, split off the colon line (or push the
6572
7170
  // whole paragraph when it is a single line or the split would leave a
@@ -6595,6 +7193,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
6595
7193
  effectiveGap = Math.ceil((paragraphBottom + effectiveGap - 0.01) / baselineGrid) * baselineGrid - paragraphBottom;
6596
7194
  }
6597
7195
  const minSpaceForList = effectiveGap + bodyStyle.lineHeightPx;
7196
+ // A short list set whole (`shortListSplit: 'avoid'`, #749) needs
7197
+ // all its room under the colon line, when a column can hold the
7198
+ // line and the list at all — less what a figure this paragraph
7199
+ // cites will take from the column's foot (`citedFootTake`).
7200
+ let listIdx = blockIdx + 1;
7201
+ while (listIdx < contentBlocks.length && isMarkerBlock(contentBlocks[listIdx]))
7202
+ listIdx++;
7203
+ const wholeList = shortListRule === 'avoid' ? shortListFrom(listIdx) : undefined;
7204
+ const wholeNeed = wholeList ? shortListNeed(wholeList, curCol.bbox.width, blockMeasureCtx) : undefined;
7205
+ const keepsListWhole = wholeNeed !== undefined
7206
+ && style.lineHeightPx + effectiveGap + wholeNeed.height <= contentArea.height + FIT_EPS;
6598
7207
  // A line of room is not always enough (EF-110): with
6599
7208
  // `colonListRoom: 'item'` the first item has to be able to start
6600
7209
  // there, by the rules that split it — a two-line item the orphan
@@ -6602,7 +7211,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
6602
7211
  // check, one line.
6603
7212
  if (availableAfter < minSpaceForList - FIT_EPS
6604
7213
  || (resolved.bodyText.colonListRoom === 'item'
6605
- && !firstListItemStarts(blockIdx, availableAfter - effectiveGap, curCol.bbox.width, blockMeasureCtx))) {
7214
+ && !firstListItemStarts(blockIdx, availableAfter - effectiveGap, curCol.bbox.width, blockMeasureCtx))
7215
+ || (keepsListWhole && availableAfter - effectiveGap - citedFootTake(blockIdx, curCol, availableAfter) < wholeNeed.height - FIT_EPS)) {
6606
7216
  const effectiveWidowMin = resolved.bodyText.avoidWidows
6607
7217
  ? Math.max(1, resolved.bodyText.widowMinLines)
6608
7218
  : 1;
@@ -6615,6 +7225,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
6615
7225
  applyStyleAttrs(blk, style);
6616
7226
  if (letterSpacingPx !== undefined)
6617
7227
  blk.letterSpacing = letterSpacingPx;
7228
+ if (runtTracking !== undefined)
7229
+ blk.runtFix = { tracking: runtTracking };
6618
7230
  blk.contentIndex = blockIdx;
6619
7231
  stampBlockExtras(blk, rawBlock);
6620
7232
  blk.headingLevel = headingLevel;
@@ -6719,6 +7331,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
6719
7331
  applyStyleAttrs(blk, style);
6720
7332
  if (letterSpacingPx !== undefined)
6721
7333
  blk.letterSpacing = letterSpacingPx;
7334
+ if (runtTracking !== undefined)
7335
+ blk.runtFix = { tracking: runtTracking };
6722
7336
  blk.contentIndex = blockIdx;
6723
7337
  stampBlockExtras(blk, rawBlock);
6724
7338
  blk.lines = resetLinePositions(splitLines, style.lineHeightPx);
@@ -6759,6 +7373,40 @@ function placeDocumentPass(content, config, cache, options, hints) {
6759
7373
  }
6760
7374
  }
6761
7375
  }
7376
+ // A short list is set whole (`bodyText.shortListSplit: 'avoid'`,
7377
+ // #749): when its items do not all fit under what the column holds
7378
+ // and an empty column would hold them, the list opens the next
7379
+ // column, with the headings right above it. Not a list a colon line
7380
+ // introduces: that line already took the list's room into account,
7381
+ // and the two stay together. Nor one that opens its column, or
7382
+ // stands under nothing but headings: it cannot do better anywhere.
7383
+ if (vdtType === 'listItem' && partIndex === 0 && shortListRule === 'avoid' && curCol.blocks.length > 0) {
7384
+ const list = shortListFrom(blockIdx);
7385
+ const headingRun = trailingHeadingRun(curCol);
7386
+ const lead = list ? prevNonMarkerBlock(contentBlocks, blockIdx) : undefined;
7387
+ const colonLed = lead?.type === 'paragraph' && resolved.bodyText.keepColonWithList && /[::︰﹕]\s*$/.test(lead.text);
7388
+ const need = list && !colonLed && headingRun < curCol.blocks.length
7389
+ ? shortListNeed(list, curCol.bbox.width, blockMeasureCtx)
7390
+ : undefined;
7391
+ if (need && need.height <= contentArea.height + FIT_EPS) {
7392
+ const listNotes = notesPrefixCost(curCol, need.lines);
7393
+ const room = curCol.availableHeight - spacingBefore - (listNotes ? listNotes[listNotes.length - 1] : 0);
7394
+ if (need.height > room + FIT_EPS) {
7395
+ if (resolved.headings.keepWithNext && headingRun > 0) {
7396
+ const rolledBack = rollbackHeadings(curCol);
7397
+ if (rolledBack.length > 0) {
7398
+ blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
7399
+ pendingSpacing = 0;
7400
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
7401
+ break;
7402
+ }
7403
+ }
7404
+ pendingSpacing = 0;
7405
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
7406
+ continue;
7407
+ }
7408
+ }
7409
+ }
6762
7410
  // A block opening a column normally stays (no column can take it any
6763
7411
  // better), except in a column at least a line shorter than the content
6764
7412
  // area — the band under a page-span box, or a column cut by a float:
@@ -6833,6 +7481,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
6833
7481
  applyStyleAttrs(blk, style);
6834
7482
  if (letterSpacingPx !== undefined)
6835
7483
  blk.letterSpacing = letterSpacingPx;
7484
+ if (runtTracking !== undefined)
7485
+ blk.runtFix = { tracking: runtTracking };
6836
7486
  blk.contentIndex = blockIdx;
6837
7487
  stampBlockExtras(blk, rawBlock);
6838
7488
  if (partIndex === 0) {
@@ -6911,6 +7561,21 @@ function placeDocumentPass(content, config, cache, options, hints) {
6911
7561
  if (vdtType === 'heading' && effectiveRemainHeight > totalRemainHeight && h > curCol.availableHeight + FIT_EPS) {
6912
7562
  h = Math.max(effectiveRemainHeight, curCol.availableHeight);
6913
7563
  }
7564
+ // A block whose lines fit, and whose snap would reach the grid line
7565
+ // under the column's foot (a foot the notes reserved there took off
7566
+ // the grid, or a bottom margin on the column's last line), ends at
7567
+ // the foot (#716): its box stays inside its column and the room left
7568
+ // reads 0, not a negative height. The space it still owed is dropped,
7569
+ // as pending spacing is at any column's end. A column cut by a band
7570
+ // cap keeps the overrun: there it is how the block's bottom margin
7571
+ // reaches the page-span box under the cut (`bandUsedBottomWithSide`).
7572
+ if (shouldSnapToGrid && partIndex === 0
7573
+ && !uncappedBottoms.has(curCol)
7574
+ && h > curCol.availableHeight + FIT_EPS
7575
+ && effectiveRemainHeight <= curCol.availableHeight + FIT_EPS) {
7576
+ h = Math.max(effectiveRemainHeight, curCol.availableHeight);
7577
+ tailOwed = 0;
7578
+ }
6914
7579
  placeBlockInColumn(blk, h, curCol, cursor);
6915
7580
  citedLinesPlaced(blockIdx, blk);
6916
7581
  finalizeListItem(blk, partIndex === 0);
@@ -6942,6 +7607,18 @@ function placeDocumentPass(content, config, cache, options, hints) {
6942
7607
  }
6943
7608
  break;
6944
7609
  }
7610
+ // An index entry that leaves the column takes the entries of its
7611
+ // group the keep rule does not let stay with it (#712): the roll-back
7612
+ // comes before any cut between its own lines.
7613
+ if (rawBlock.index && partIndex === 0 && curCol.blocks.length > 0) {
7614
+ const rolledBack = rollbackIndexGroup(curCol, rawBlock, blockMeasureCtx.resolved.index.groups.splitMinEntries);
7615
+ if (rolledBack.length > 0) {
7616
+ blockIdx = (rolledBack[0].contentIndex ?? blockIdx - rolledBack.length) - 1;
7617
+ pendingSpacing = 0;
7618
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
7619
+ break;
7620
+ }
7621
+ }
6945
7622
  // Block doesn't fit — try to split (orphan/widow-aware)
6946
7623
  const inList = vdtType === 'listItem';
6947
7624
  const effectiveAvoidOrphans = resolved.bodyText.avoidOrphans
@@ -6999,19 +7676,23 @@ function placeDocumentPass(content, config, cache, options, hints) {
6999
7676
  // placement then starts over with its lines. Only a paragraph set
7000
7677
  // from its first line here — its earlier lines are not placed yet —
7001
7678
  // and never one the balancing is running long.
7679
+ //
7680
+ // `'withinPage'` (#701) guards only the ends that close a page: this
7681
+ // column's when no later column of its page takes text, and then
7682
+ // one every page of full columns. With none in the paragraph,
7683
+ // nothing is measured again.
7684
+ const hyphenGuard = resolved.bodyText.hyphenateAcrossColumns;
7002
7685
  if (choice.splitAt > 0
7003
- && !resolved.bodyText.hyphenateAcrossColumns
7686
+ && hyphenGuard !== true
7004
7687
  && !columnEndHyphenRetried
7005
7688
  && partIndex === 0
7006
7689
  && looseLines === undefined) {
7007
7690
  const fullColumn = Math.max(1, Math.floor((contentArea.height + 0.01) / style.lineHeightPx));
7008
- const ends = [];
7009
- for (let n = choice.splitAt; n < remainingLines.length; n += fullColumn)
7010
- ends.push(n);
7691
+ const ends = guardedColumnEnds(hyphenGuard, choice.splitAt, remainingLines.length, fullColumn, doc.pages[cursor.pageIndex], cursor.columnIndex);
7011
7692
  /** How a setting ends its guarded lines: whether this column's
7012
7693
  * last line is hyphenated, and how many guarded lines are. */
7013
7694
  const columnEndHyphens = (lines) => ({
7014
- first: lines[choice.splitAt - 1]?.hyphenated === true,
7695
+ first: ends[0] === choice.splitAt && lines[choice.splitAt - 1]?.hyphenated === true,
7015
7696
  count: ends.filter((n) => lines[n - 1]?.hyphenated).length,
7016
7697
  });
7017
7698
  const natural = columnEndHyphens(remainingLines);
@@ -7050,18 +7731,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
7050
7731
  // this column's end (or fewer at the ends still to come). The
7051
7732
  // placed lines are checked again all the same: a break trace from
7052
7733
  // the other measuring path cannot keep them.
7734
+ // Under `'withinPage'` only from a column that closes its page.
7053
7735
  if (choice.splitAt > 0
7054
- && !resolved.bodyText.hyphenateAcrossColumns
7736
+ && hyphenGuard !== true
7055
7737
  && partIndex > 0
7056
7738
  && columnEndHyphenPart !== partIndex
7057
7739
  && looseLines === undefined
7058
- && remainingLines[choice.splitAt - 1]?.hyphenated === true) {
7740
+ && remainingLines[choice.splitAt - 1]?.hyphenated === true
7741
+ && (hyphenGuard === false || columnClosesPage(doc.pages[cursor.pageIndex], cursor.columnIndex, style.lineHeightPx))) {
7059
7742
  columnEndHyphenPart = partIndex;
7060
7743
  const placed = paragraphLines.length - remainingLines.length;
7061
7744
  const fullColumn = Math.max(1, Math.floor((contentArea.height + 0.01) / style.lineHeightPx));
7062
- const later = [];
7063
- for (let n = choice.splitAt; n < remainingLines.length; n += fullColumn)
7064
- later.push(placed + n);
7745
+ const later = guardedColumnEnds(hyphenGuard, choice.splitAt, remainingLines.length, fullColumn, doc.pages[cursor.pageIndex], cursor.columnIndex)
7746
+ .map((n) => placed + n);
7065
7747
  // The ends already placed keep their guard; the later ones the
7066
7748
  // last re-break assumed give way to where the columns now end.
7067
7749
  const ends = [...guardedEnds.filter((n) => n <= placed), ...later];
@@ -7123,6 +7805,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
7123
7805
  applyStyleAttrs(blk, style);
7124
7806
  if (letterSpacingPx !== undefined)
7125
7807
  blk.letterSpacing = letterSpacingPx;
7808
+ if (runtTracking !== undefined)
7809
+ blk.runtFix = { tracking: runtTracking };
7126
7810
  blk.contentIndex = blockIdx;
7127
7811
  stampBlockExtras(blk, rawBlock);
7128
7812
  if (partIndex === 0) {
@@ -7226,6 +7910,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
7226
7910
  applyStyleAttrs(blk, style);
7227
7911
  if (letterSpacingPx !== undefined)
7228
7912
  blk.letterSpacing = letterSpacingPx;
7913
+ if (runtTracking !== undefined)
7914
+ blk.runtFix = { tracking: runtTracking };
7229
7915
  blk.contentIndex = blockIdx;
7230
7916
  stampBlockExtras(blk, rawBlock);
7231
7917
  // Stamp the prefix as the other placement branches do: a full-page
@@ -7444,6 +8130,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
7444
8130
  floatShrinkWarningsByDoc.set(doc, shrunk);
7445
8131
  if (textWrapNotes.length > 0 || columnsNotes.length > 0)
7446
8132
  textWrapWarningsByDoc.set(doc, [...textWrapNotes, ...columnsNotes]);
8133
+ // The composition rules (#749): short lists cut, floats on opener pages.
8134
+ const ruleNotes = [...shortListWarnings(), ...openerFloatNotes];
8135
+ if (ruleNotes.length > 0)
8136
+ compositionWarningsByDoc.set(doc, ruleNotes);
7447
8137
  const { proposals: citingPageProposals, refused: citingPageRefused } = settleCitingPageFloats();
7448
8138
  return { doc, forcedBreakPages, bandCapProposals, spanPlacedInBand, bandCapsApplied, looseOutcome, looseOffGrid, captionUnderProposals, citingPageProposals, citingPageRefused };
7449
8139
  }
@@ -7514,7 +8204,7 @@ export function* buildDocumentGen(content, config, cache, options) {
7514
8204
  // cap; words of a joining script that run past their line, and styles
7515
8205
  // whose letter-spacing such words do not take.
7516
8206
  // Line numbers set over a float of the side column (#621).
7517
- const loose = [...cjkLooseLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc), ...(floatShrinkWarningsByDoc.get(doc) ?? []), ...(textWrapWarningsByDoc.get(doc) ?? [])];
8207
+ const loose = [...cjkLooseLineWarnings(doc), ...raggedLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc), ...(floatShrinkWarningsByDoc.get(doc) ?? []), ...(textWrapWarningsByDoc.get(doc) ?? []), ...(compositionWarningsByDoc.get(doc) ?? [])];
7518
8208
  // Side boxes and side floats set on pages after the text, and what the
7519
8209
  // layout could place nowhere (#639).
7520
8210
  loose.push(...afterTextWarnings(doc), ...(unplacedByDoc.get(doc) ?? []));
@@ -7573,6 +8263,9 @@ const floatShrinkWarningsByDoc = new WeakMap();
7573
8263
  /** The `textWrap` warnings of a build (#627) and its `columnsTooNarrow`
7574
8264
  * ones (#634), found by its last pass. */
7575
8265
  const textWrapWarningsByDoc = new WeakMap();
8266
+ /** The `shortListSplit` and `openerFloat` warnings of a build (#749),
8267
+ * found by its last pass. */
8268
+ const compositionWarningsByDoc = new WeakMap();
7576
8269
  /** What a pass set in the side column of a page opened after the text of
7577
8270
  * its chapter (#639): a side box's frame, a side float's block, with the
7578
8271
  * source range an `afterText` warning points at. Read from the finished
@@ -8017,16 +8710,25 @@ citing = { hints: new Map(), refused: new Set() }) {
8017
8710
  return false;
8018
8711
  if (newLines.length === 0 && newLoose.length === 0)
8019
8712
  return false;
8713
+ // Where the headings hold lines back for the loose paragraphs (#707),
8714
+ // a pass that tried both in the columns at fault blames the paragraphs
8715
+ // first: run a line long, they are what moves text across a column
8716
+ // break, and a heading blacklisted with them could never take the lines
8717
+ // it took alone up to 1.27. The spacing levers are tried again without
8718
+ // them, and blacklisted in their turn if the cascade is theirs.
8719
+ const looseFirst = balancing.maxLinesBeforeLoose < balancing.maxLinesPerHeading;
8020
8720
  const blacklist = (cands) => {
8021
8721
  let hit = false;
8022
- for (const k of newLines)
8722
+ for (const k of newLoose)
8023
8723
  if (!cands || cands.has(k)) {
8024
- s.failedLines.add(k);
8724
+ s.failedLoose.add(k);
8025
8725
  hit = true;
8026
8726
  }
8027
- for (const k of newLoose)
8727
+ if (hit && looseFirst)
8728
+ return true;
8729
+ for (const k of newLines)
8028
8730
  if (!cands || cands.has(k)) {
8029
- s.failedLoose.add(k);
8731
+ s.failedLines.add(k);
8030
8732
  hit = true;
8031
8733
  }
8032
8734
  return hit;
@@ -8056,10 +8758,13 @@ citing = { hints: new Map(), refused: new Set() }) {
8056
8758
  }
8057
8759
  const proposal = proposeBalanceLines(best.doc, best.forcedBreakPages, applied, {
8058
8760
  maxLinesPerHeading: balancing.maxLinesPerHeading,
8761
+ maxLinesBeforeLoose: balancing.maxLinesBeforeLoose,
8059
8762
  stretchAfterLists: balancing.stretchAfterLists,
8060
8763
  maxLinesAfterList: balancing.maxLinesAfterList,
8764
+ stretchAfterDisplays: balancing.stretchAfterDisplays,
8061
8765
  stretchAfterFloats: balancing.stretchAfterFloats,
8062
8766
  maxLinesAfterFloat: balancing.maxLinesAfterFloat,
8767
+ stretchAfterWideFloats: balancing.stretchAfterWideFloats,
8063
8768
  looseParagraphs: balancing.looseParagraphs,
8064
8769
  maxLooseParagraphs: balancing.maxLooseParagraphs,
8065
8770
  optimalLineBreaking: best.doc.config.bodyText.optimalLineBreaking,