postext 1.23.0 → 1.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/__tests__/bundle/bitmapInfo.test.d.ts +2 -0
  2. package/dist/__tests__/bundle/bitmapInfo.test.d.ts.map +1 -0
  3. package/dist/__tests__/bundle/bitmapInfo.test.js +78 -0
  4. package/dist/__tests__/bundle/bitmapInfo.test.js.map +1 -0
  5. package/dist/__tests__/bundle/designOverflowPin.test.d.ts +2 -0
  6. package/dist/__tests__/bundle/designOverflowPin.test.d.ts.map +1 -0
  7. package/dist/__tests__/bundle/designOverflowPin.test.js +87 -0
  8. package/dist/__tests__/bundle/designOverflowPin.test.js.map +1 -0
  9. package/dist/__tests__/bundle.test.js +1 -1
  10. package/dist/__tests__/bundle.test.js.map +1 -1
  11. package/dist/__tests__/codeBlocks.test.js +1 -1
  12. package/dist/__tests__/codeBlocks.test.js.map +1 -1
  13. package/dist/__tests__/design/designTextOverflow.test.d.ts +2 -0
  14. package/dist/__tests__/design/designTextOverflow.test.d.ts.map +1 -0
  15. package/dist/__tests__/design/designTextOverflow.test.js +102 -0
  16. package/dist/__tests__/design/designTextOverflow.test.js.map +1 -0
  17. package/dist/__tests__/exports.test.js +11 -0
  18. package/dist/__tests__/exports.test.js.map +1 -1
  19. package/dist/__tests__/frontmatterInvalid.test.d.ts +2 -0
  20. package/dist/__tests__/frontmatterInvalid.test.d.ts.map +1 -0
  21. package/dist/__tests__/frontmatterInvalid.test.js +101 -0
  22. package/dist/__tests__/frontmatterInvalid.test.js.map +1 -0
  23. package/dist/__tests__/hardBreaks.test.js +1 -1
  24. package/dist/__tests__/hardBreaks.test.js.map +1 -1
  25. package/dist/__tests__/headingFirstLineIndent.test.d.ts +2 -0
  26. package/dist/__tests__/headingFirstLineIndent.test.d.ts.map +1 -0
  27. package/dist/__tests__/headingFirstLineIndent.test.js +185 -0
  28. package/dist/__tests__/headingFirstLineIndent.test.js.map +1 -0
  29. package/dist/__tests__/headingInlineMarks.test.js +1 -1
  30. package/dist/__tests__/headingInlineMarks.test.js.map +1 -1
  31. package/dist/__tests__/headingLetterSpacing.test.js +1 -1
  32. package/dist/__tests__/headingLetterSpacing.test.js.map +1 -1
  33. package/dist/__tests__/mathSizeMigration.test.js +2 -2
  34. package/dist/__tests__/mathSizeMigration.test.js.map +1 -1
  35. package/dist/__tests__/pipeline/bitmapResolution.test.d.ts +2 -0
  36. package/dist/__tests__/pipeline/bitmapResolution.test.d.ts.map +1 -0
  37. package/dist/__tests__/pipeline/bitmapResolution.test.js +139 -0
  38. package/dist/__tests__/pipeline/bitmapResolution.test.js.map +1 -0
  39. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js +1 -1
  40. package/dist/__tests__/pipeline/calloutInlineResourceGap.test.js.map +1 -1
  41. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js +1 -1
  42. package/dist/__tests__/pipeline/calloutSplitMinLinesChild.test.js.map +1 -1
  43. package/dist/__tests__/pipeline/colonListRoom.test.js +1 -1
  44. package/dist/__tests__/pipeline/colonListRoom.test.js.map +1 -1
  45. package/dist/__tests__/pipeline/compoundBreaks.test.js +1 -1
  46. package/dist/__tests__/pipeline/compoundBreaks.test.js.map +1 -1
  47. package/dist/__tests__/pipeline/dashBreaks.test.js +1 -1
  48. package/dist/__tests__/pipeline/dashBreaks.test.js.map +1 -1
  49. package/dist/__tests__/pipeline/designTextTruncated.test.d.ts +2 -0
  50. package/dist/__tests__/pipeline/designTextTruncated.test.d.ts.map +1 -0
  51. package/dist/__tests__/pipeline/designTextTruncated.test.js +116 -0
  52. package/dist/__tests__/pipeline/designTextTruncated.test.js.map +1 -0
  53. package/dist/__tests__/pipeline/floatPlacement.test.js +3 -3
  54. package/dist/__tests__/pipeline/floatPlacement.test.js.map +1 -1
  55. package/dist/__tests__/pipeline/floatShrink.test.d.ts +2 -0
  56. package/dist/__tests__/pipeline/floatShrink.test.d.ts.map +1 -0
  57. package/dist/__tests__/pipeline/floatShrink.test.js +298 -0
  58. package/dist/__tests__/pipeline/floatShrink.test.js.map +1 -0
  59. package/dist/__tests__/pipeline/headingSplitOrphans.test.js +1 -1
  60. package/dist/__tests__/pipeline/headingSplitOrphans.test.js.map +1 -1
  61. package/dist/__tests__/pipeline/inlineFigureSpace.test.js +1 -1
  62. package/dist/__tests__/pipeline/inlineFigureSpace.test.js.map +1 -1
  63. package/dist/__tests__/pipeline/paragraphContainerTail.test.js +1 -1
  64. package/dist/__tests__/pipeline/paragraphContainerTail.test.js.map +1 -1
  65. package/dist/__tests__/pipeline/raggedOptimal.test.js +1 -1
  66. package/dist/__tests__/pipeline/raggedOptimal.test.js.map +1 -1
  67. package/dist/__tests__/pipeline/textWrap.test.d.ts +2 -0
  68. package/dist/__tests__/pipeline/textWrap.test.d.ts.map +1 -0
  69. package/dist/__tests__/pipeline/textWrap.test.js +453 -0
  70. package/dist/__tests__/pipeline/textWrap.test.js.map +1 -0
  71. package/dist/__tests__/preflight.test.js +30 -1
  72. package/dist/__tests__/preflight.test.js.map +1 -1
  73. package/dist/__tests__/presets/showcaseDesignOverflow.test.d.ts +2 -0
  74. package/dist/__tests__/presets/showcaseDesignOverflow.test.d.ts.map +1 -0
  75. package/dist/__tests__/presets/showcaseDesignOverflow.test.js +58 -0
  76. package/dist/__tests__/presets/showcaseDesignOverflow.test.js.map +1 -0
  77. package/dist/__tests__/table/booktabs.test.d.ts +2 -0
  78. package/dist/__tests__/table/booktabs.test.d.ts.map +1 -0
  79. package/dist/__tests__/table/booktabs.test.js +290 -0
  80. package/dist/__tests__/table/booktabs.test.js.map +1 -0
  81. package/dist/__tests__/textWrapMeasure.test.d.ts +2 -0
  82. package/dist/__tests__/textWrapMeasure.test.d.ts.map +1 -0
  83. package/dist/__tests__/textWrapMeasure.test.js +162 -0
  84. package/dist/__tests__/textWrapMeasure.test.js.map +1 -0
  85. package/dist/__tests__/verseLines.test.js +134 -4
  86. package/dist/__tests__/verseLines.test.js.map +1 -1
  87. package/dist/bitmapResolution.d.ts +42 -0
  88. package/dist/bitmapResolution.d.ts.map +1 -0
  89. package/dist/bitmapResolution.js +82 -0
  90. package/dist/bitmapResolution.js.map +1 -0
  91. package/dist/bundle/codec.d.ts +9 -2
  92. package/dist/bundle/codec.d.ts.map +1 -1
  93. package/dist/bundle/codec.js +24 -2
  94. package/dist/bundle/codec.js.map +1 -1
  95. package/dist/bundle/configVersion.d.ts +49 -8
  96. package/dist/bundle/configVersion.d.ts.map +1 -1
  97. package/dist/bundle/configVersion.js +161 -6
  98. package/dist/bundle/configVersion.js.map +1 -1
  99. package/dist/bundle/index.d.ts +3 -3
  100. package/dist/bundle/index.d.ts.map +1 -1
  101. package/dist/bundle/index.js +2 -2
  102. package/dist/bundle/index.js.map +1 -1
  103. package/dist/bundle/manifest.d.ts +12 -2
  104. package/dist/bundle/manifest.d.ts.map +1 -1
  105. package/dist/bundle/manifest.js +132 -6
  106. package/dist/bundle/manifest.js.map +1 -1
  107. package/dist/bundle/types.d.ts +28 -2
  108. package/dist/bundle/types.d.ts.map +1 -1
  109. package/dist/canvas-backend/blockRender.d.ts.map +1 -1
  110. package/dist/canvas-backend/blockRender.js +6 -4
  111. package/dist/canvas-backend/blockRender.js.map +1 -1
  112. package/dist/canvas-backend/renderResourceBlock.d.ts.map +1 -1
  113. package/dist/canvas-backend/renderResourceBlock.js +15 -1
  114. package/dist/canvas-backend/renderResourceBlock.js.map +1 -1
  115. package/dist/configWarnings.js +29 -2
  116. package/dist/configWarnings.js.map +1 -1
  117. package/dist/defaults/headerFooter.d.ts +21 -6
  118. package/dist/defaults/headerFooter.d.ts.map +1 -1
  119. package/dist/defaults/headerFooter.js +37 -14
  120. package/dist/defaults/headerFooter.js.map +1 -1
  121. package/dist/defaults/headings.d.ts +5 -4
  122. package/dist/defaults/headings.d.ts.map +1 -1
  123. package/dist/defaults/headings.js +15 -4
  124. package/dist/defaults/headings.js.map +1 -1
  125. package/dist/defaults/index.d.ts +1 -1
  126. package/dist/defaults/index.d.ts.map +1 -1
  127. package/dist/defaults/index.js +1 -1
  128. package/dist/defaults/index.js.map +1 -1
  129. package/dist/defaults/layout.d.ts +30 -1
  130. package/dist/defaults/layout.d.ts.map +1 -1
  131. package/dist/defaults/layout.js +105 -1
  132. package/dist/defaults/layout.js.map +1 -1
  133. package/dist/defaults/parts.js +4 -4
  134. package/dist/defaults/parts.js.map +1 -1
  135. package/dist/defaults/tableStyle.d.ts.map +1 -1
  136. package/dist/defaults/tableStyle.js +44 -0
  137. package/dist/defaults/tableStyle.js.map +1 -1
  138. package/dist/defaults/toc.js +2 -2
  139. package/dist/defaults/verse.d.ts +2 -1
  140. package/dist/defaults/verse.d.ts.map +1 -1
  141. package/dist/defaults/verse.js +6 -1
  142. package/dist/defaults/verse.js.map +1 -1
  143. package/dist/design/layout.d.ts +15 -1
  144. package/dist/design/layout.d.ts.map +1 -1
  145. package/dist/design/layout.js +28 -4
  146. package/dist/design/layout.js.map +1 -1
  147. package/dist/frontmatter.d.ts +20 -0
  148. package/dist/frontmatter.d.ts.map +1 -1
  149. package/dist/frontmatter.js +69 -3
  150. package/dist/frontmatter.js.map +1 -1
  151. package/dist/html-backend.d.ts.map +1 -1
  152. package/dist/html-backend.js +22 -4
  153. package/dist/html-backend.js.map +1 -1
  154. package/dist/index.d.ts +13 -8
  155. package/dist/index.d.ts.map +1 -1
  156. package/dist/index.js +6 -3
  157. package/dist/index.js.map +1 -1
  158. package/dist/measure/bidiLines.d.ts +4 -3
  159. package/dist/measure/bidiLines.d.ts.map +1 -1
  160. package/dist/measure/bidiLines.js +12 -7
  161. package/dist/measure/bidiLines.js.map +1 -1
  162. package/dist/measure/cjkCompose.d.ts.map +1 -1
  163. package/dist/measure/cjkCompose.js +7 -3
  164. package/dist/measure/cjkCompose.js.map +1 -1
  165. package/dist/measure/hardBreaks.d.ts.map +1 -1
  166. package/dist/measure/hardBreaks.js +13 -1
  167. package/dist/measure/hardBreaks.js.map +1 -1
  168. package/dist/measure/index.d.ts +3 -3
  169. package/dist/measure/index.d.ts.map +1 -1
  170. package/dist/measure/index.js +4 -4
  171. package/dist/measure/index.js.map +1 -1
  172. package/dist/measure/plain.d.ts.map +1 -1
  173. package/dist/measure/plain.js +11 -7
  174. package/dist/measure/plain.js.map +1 -1
  175. package/dist/measure/rich.d.ts.map +1 -1
  176. package/dist/measure/rich.js +9 -6
  177. package/dist/measure/rich.js.map +1 -1
  178. package/dist/measure/types.d.ts +49 -3
  179. package/dist/measure/types.d.ts.map +1 -1
  180. package/dist/measure/types.js +67 -3
  181. package/dist/measure/types.js.map +1 -1
  182. package/dist/pipeline/build.d.ts.map +1 -1
  183. package/dist/pipeline/build.js +488 -31
  184. package/dist/pipeline/build.js.map +1 -1
  185. package/dist/pipeline/buildBlockKind.d.ts +5 -0
  186. package/dist/pipeline/buildBlockKind.d.ts.map +1 -1
  187. package/dist/pipeline/buildBlockKind.js +14 -1
  188. package/dist/pipeline/buildBlockKind.js.map +1 -1
  189. package/dist/pipeline/buildMeasurement.js +1 -1
  190. package/dist/pipeline/buildMeasurement.js.map +1 -1
  191. package/dist/pipeline/columnBalancing.d.ts.map +1 -1
  192. package/dist/pipeline/columnBalancing.js +5 -0
  193. package/dist/pipeline/columnBalancing.js.map +1 -1
  194. package/dist/pipeline/contentWarnings.d.ts +16 -0
  195. package/dist/pipeline/contentWarnings.d.ts.map +1 -1
  196. package/dist/pipeline/contentWarnings.js +113 -17
  197. package/dist/pipeline/contentWarnings.js.map +1 -1
  198. package/dist/pipeline/floatPlacement.d.ts +34 -4
  199. package/dist/pipeline/floatPlacement.d.ts.map +1 -1
  200. package/dist/pipeline/floatPlacement.js +35 -5
  201. package/dist/pipeline/floatPlacement.js.map +1 -1
  202. package/dist/pipeline/floatSlots.d.ts +7 -0
  203. package/dist/pipeline/floatSlots.d.ts.map +1 -1
  204. package/dist/pipeline/floatSlots.js.map +1 -1
  205. package/dist/pipeline/headerFooter.d.ts +4 -1
  206. package/dist/pipeline/headerFooter.d.ts.map +1 -1
  207. package/dist/pipeline/headerFooter.js +19 -5
  208. package/dist/pipeline/headerFooter.js.map +1 -1
  209. package/dist/pipeline/measureContentBlock.d.ts +7 -0
  210. package/dist/pipeline/measureContentBlock.d.ts.map +1 -1
  211. package/dist/pipeline/measureContentBlock.js +15 -6
  212. package/dist/pipeline/measureContentBlock.js.map +1 -1
  213. package/dist/pipeline/placement.d.ts.map +1 -1
  214. package/dist/pipeline/placement.js +7 -0
  215. package/dist/pipeline/placement.js.map +1 -1
  216. package/dist/pipeline/resourceLayout.d.ts +15 -0
  217. package/dist/pipeline/resourceLayout.d.ts.map +1 -1
  218. package/dist/pipeline/resourceLayout.js +95 -15
  219. package/dist/pipeline/resourceLayout.js.map +1 -1
  220. package/dist/pipeline/styles.d.ts.map +1 -1
  221. package/dist/pipeline/styles.js +8 -5
  222. package/dist/pipeline/styles.js.map +1 -1
  223. package/dist/pipeline/textWrap.d.ts +94 -0
  224. package/dist/pipeline/textWrap.d.ts.map +1 -0
  225. package/dist/pipeline/textWrap.js +188 -0
  226. package/dist/pipeline/textWrap.js.map +1 -0
  227. package/dist/pipeline/verseLines.d.ts +22 -2
  228. package/dist/pipeline/verseLines.d.ts.map +1 -1
  229. package/dist/pipeline/verseLines.js +52 -4
  230. package/dist/pipeline/verseLines.js.map +1 -1
  231. package/dist/preflight.d.ts +46 -1
  232. package/dist/preflight.d.ts.map +1 -1
  233. package/dist/preflight.js +42 -7
  234. package/dist/preflight.js.map +1 -1
  235. package/dist/table/booktabs.d.ts +37 -0
  236. package/dist/table/booktabs.d.ts.map +1 -0
  237. package/dist/table/booktabs.js +82 -0
  238. package/dist/table/booktabs.js.map +1 -0
  239. package/dist/types.d.ts +227 -8
  240. package/dist/types.d.ts.map +1 -1
  241. package/dist/vdt.d.ts +131 -6
  242. package/dist/vdt.d.ts.map +1 -1
  243. package/dist/vdt.js +3 -2
  244. package/dist/vdt.js.map +1 -1
  245. package/dist/word/index.d.ts +1 -1
  246. package/dist/word/index.d.ts.map +1 -1
  247. package/dist/word/index.js +1 -1
  248. package/dist/word/index.js.map +1 -1
  249. package/dist/word/toMarkdown.d.ts +10 -0
  250. package/dist/word/toMarkdown.d.ts.map +1 -1
  251. package/dist/word/toMarkdown.js +16 -1
  252. package/dist/word/toMarkdown.js.map +1 -1
  253. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { withBitmapResolutions } from '../bitmapResolution';
1
2
  import { plainTitleBreaks } from '../parse/inlineFormatting';
2
3
  import { dimensionToPx } from '../units';
3
4
  import { createVDTDocument, createVDTBlock, createBoundingBox, createVDTColumn, pageIsMirrored, pageIsVertical, flowRectToPage, } from '../vdt';
@@ -42,13 +43,14 @@ import { planParts, derivePartMeasureContext } from './parts';
42
43
  import { layoutCallout, offsetBlock, offsetCalloutToAbsolute, pickCalloutStyle, planCallouts, resolveCalloutAttrs, } from './calloutLayout';
43
44
  import { layoutCaptionText, layoutResourceBlock, planTableSlice } from './resourceLayout';
44
45
  import { computeFloatPlan, floatedResourceIds, floatColumnCount, } from './floatPlacement';
45
- import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
46
+ import { enumerateCurrentPageSlots, measureFloatBand, floatGapAbove, floatGapBelow, measureSideStack, clearSideObstacles, columnHasFloatBand, fitsStrict, trueBottom, } from './floatSlots';
46
47
  import { computeHeadingContext, computeResourceNumberingState, } from './resourceNumbering';
47
48
  import { documentLocale, effectiveResourceTypes } from '../defaults/resourceTypes';
48
49
  import { resolveCalloutStylesConfig } from '../defaults/calloutStyles';
49
50
  import { codeBoxStyle, codeParseOptions, wrapCodeBlocks } from './codeBlocks';
50
51
  import { buildCodeLineNumbers, codeOverflowWarnings } from './codeLines';
51
52
  import { pickTableStyle } from '../defaults/tableStyle';
53
+ import { DEFAULT_FLOAT_MIN_SCALE } from '../defaults/layout';
52
54
  import { buildLineNumbers, lineNumberWarnings } from './lineNumbers';
53
55
  import { buildHeadersAndFooters, defaultOpenerTitle, headingDesignBoxes, measureDefaultOpenerHeight, measureHeadingDesign } from './headerFooter';
54
56
  import { flowColorValues } from './partPalette';
@@ -56,7 +58,7 @@ import { chapterNumberCounter, leadingBoldText } from './placeholders';
56
58
  import { proposeBalanceLines, collectColumnGaps, firstDivergentColumn, gapLinesIn, boxRoomIn, boxLeverKeys, pageSegments, MAX_BALANCING_PASSES, MAX_BALANCING_PASSES_PER_DOCUMENT, balanceKey, candidateKey, flexFigureKey, flexFloatKey } from './columnBalancing';
57
59
  import { applyBandCap, uncapBand, columnBottom, bandCapLines, bandTop, bareBandTop, resolveBandCapsGen, drainPasses, resolveTrailingCapsGen, bandCapLinesAroundZone, } from './bandCaps';
58
60
  import { raggedLooseLines } from './raggedLines';
59
- import { cjkLooseLineWarnings, collectContentWarnings, dropCapWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
61
+ import { cjkLooseLineWarnings, collectContentWarnings, designTruncationWarnings, dropCapWarnings, floatShrinkWarnings, joiningLetterSpacingWarnings, locateContentWarnings, wordOverflowWarnings } from './contentWarnings';
60
62
  import { dropCapSinkAfter } from './dropCap';
61
63
  import { mostlyJoiningScript } from '../measure/joining';
62
64
  import { annotateDocument } from '../cjkMarks';
@@ -67,6 +69,8 @@ import { overlineEmphasis } from '../emphasisOverline';
67
69
  import { withBookTitleBrackets } from './annotations';
68
70
  import { paperByBlock, stampPagePaper } from './paper';
69
71
  import { headingLineSpanBand } from './lineSpan';
72
+ import { columnCursorY, insetsBefore, minTextWidthPx, openColumnWrap, resolveCalloutWrap, resolveResourceWrap, runsBesideWrap, sameInsetsFrom, settleColumnWraps, transparentToWrap, wrapGapPx, wrapInsetSteps } from './textWrap';
73
+ import { lineInsetsAt } from '../measure/types';
70
74
  /** Tolerance for "does this block fit" checks against a column's free
71
75
  * height, absorbing floating-point drift between grid multiples. */
72
76
  const FIT_EPS = 0.01;
@@ -225,6 +229,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
225
229
  * shortened it, so what does not fit it may still fit a full column. */
226
230
  const shortSideColumn = (page, col) => col.bbox.height < page.contentArea.height - 0.5;
227
231
  const resolved = resolveAllConfig(config);
232
+ // Bitmaps sized by the document's `layout.bitmapResolution` carry it as
233
+ // their own `resolution` from here on (#631); under the default policy
234
+ // the content is untouched.
235
+ const stampedResources = content.resources && withBitmapResolutions(content.resources, resolved.layout.bitmapResolution);
236
+ if (stampedResources && stampedResources !== content.resources)
237
+ content = { ...content, resources: stampedResources };
228
238
  // Tracking rungs a loose paragraph may climb: none, then 5‰ steps up to
229
239
  // the cap (`headings.balancing.maxTracking`, thousandths of an em), the
230
240
  // cap itself always included. Only the smallest rung that gains the line
@@ -472,7 +482,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
472
482
  // it asks for is not applied there. Decided per block, by the writing
473
483
  // mode of the styled section the reference sits in (a horizontal
474
484
  // appendix of a vertical book turns its figures as asked).
475
- const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl');
485
+ const floatPlan = computeFloatPlan(contentBlocks, resources, resourceTypes, incorporated, (blockIdx) => sectionWritingMode(sectionPlan, resolved, blockIdx) === 'vertical-rl', resolved.layout.floatShrink, resolved.layout.wrap);
476
486
  const floatedIds = floatedResourceIds(floatPlan, incorporated, resources, resourceTypes);
477
487
  const floatsByFirstBlock = new Map();
478
488
  for (const f of floatPlan) {
@@ -835,7 +845,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
835
845
  /** Pages whose head or foot a floated box took (see the gallery rule in
836
846
  * `placeFloatInColumns`). */
837
847
  const calloutBandPages = new Set();
838
- const enqueueCalloutFloat = (startIdx, plan, style, L, placement, span) => {
848
+ const enqueueCalloutFloat = (startIdx, plan, style, L, placement, span,
849
+ /** Text wraps round the box (#627): a one-column float at its side. */
850
+ wrap) => {
839
851
  const key = `${CALLOUT_FLOAT_PREFIX}${startIdx}`;
840
852
  const columns = plan.attrs.columns !== undefined ? floatColumnCount(plan.attrs.columns) : style.columns;
841
853
  // A keep-with-next replay passes the fence again: enqueue once.
@@ -851,6 +863,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
851
863
  span: span === 'page' ? 'page' : 'column',
852
864
  ...(span === 'column' && columns > 1 ? { columns } : {}),
853
865
  callout: { startIdx },
866
+ ...(wrap && span === 'column' && columns === 1
867
+ ? { widthFraction: wrap.width, align: wrap.side, wrap: wrap.side, ...(wrap.gap ? { wrapGap: wrap.gap } : {}) }
868
+ : {}),
854
869
  ...(col ? { refPageIndex: page.index, refY: col.bbox.y + (col.bbox.height - col.availableHeight) } : {}),
855
870
  });
856
871
  }
@@ -918,7 +933,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
918
933
  };
919
934
  const rotationKey = (rotated) => rotated ? `:${rotated.direction}${rotated.length.toFixed(2)}${rotated.upright ? 'u' : ''}` : '';
920
935
  const asideKey = (a) => (a ? `:aside${a.dx.toFixed(1)}x${a.width.toFixed(1)}${a.alignBottom ? 'b' : 't'}${(a.offsetY ?? 0).toFixed(1)}` : '');
921
- const layoutFloat = (resourceId, width, slice, rotated, aside) => {
936
+ const capKey = (cap) => (cap ? `:cap${cap.maxBodyHeight.toFixed(2)}@${cap.minScale}` : '');
937
+ const layoutFloat = (resourceId, width, slice, rotated, aside, cap) => {
922
938
  const resource = resourceById.get(resourceId);
923
939
  if (!resource)
924
940
  return null;
@@ -939,6 +955,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
939
955
  ...(rotated ? (rotated.upright ? { upright: { maxLength: rotated.length } } : { rotate: rotated.direction, rotatedLength: rotated.length }) : {}),
940
956
  ...(aside ? { captionAside: aside } : {}),
941
957
  ...(flexPx > 0 ? { bodyHeightDelta: flexPx } : {}),
958
+ ...(cap && !rotated ? { maxBodyHeight: cap.maxBodyHeight, minBodyScale: cap.minScale } : {}),
942
959
  });
943
960
  };
944
961
  /** The rotation of a pending float on a band whose columns keep `avail`
@@ -989,8 +1006,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
989
1006
  /** Height (and caption baseline) of a float at a given width, memoised —
990
1007
  * fit checks run for every pending float on every loop iteration. */
991
1008
  const floatMeasureMemo = new Map();
992
- const measureFloat = (resourceId, width, slice, rotated, aside) => {
993
- const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}${asideKey(aside)}`;
1009
+ const measureFloat = (resourceId, width, slice, rotated, aside, cap) => {
1010
+ const key = `${resourceId}:${width.toFixed(2)}${sliceKey(slice)}${rotationKey(rotated)}${asideKey(aside)}${capKey(cap)}`;
994
1011
  const memo = floatMeasureMemo.get(key);
995
1012
  if (memo !== undefined)
996
1013
  return memo;
@@ -1024,7 +1041,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
1024
1041
  floatMeasureMemo.set(key, mc);
1025
1042
  return mc;
1026
1043
  }
1027
- const laid = layoutFloat(resourceId, width, slice, rotated, aside);
1044
+ const laid = layoutFloat(resourceId, width, slice, rotated, aside, cap);
1028
1045
  let m = null;
1029
1046
  if (laid && laid.block.rotation) {
1030
1047
  // A rotated block takes its band whole: no caption baseline to align,
@@ -1045,10 +1062,12 @@ function placeDocumentPass(content, config, cache, options, hints) {
1045
1062
  if (lastBaseline === undefined || ln.baseline > lastBaseline)
1046
1063
  lastBaseline = ln.baseline;
1047
1064
  }
1065
+ const picture = rb.kind === 'bitmap' || rb.kind === 'svg' || rb.kind === 'video';
1048
1066
  m = {
1049
1067
  height: laid.totalHeight,
1050
1068
  ...(lastBaseline !== undefined ? { lastCaptionBaseline: lastBaseline } : {}),
1051
1069
  ...(laid.asideHeight !== undefined ? { asideHeight: laid.asideHeight } : {}),
1070
+ ...(picture ? { body: { height: rb.bodyRect.height, scale: rb.shrinkScale ?? 1 } } : {}),
1052
1071
  };
1053
1072
  }
1054
1073
  floatMeasureMemo.set(key, m);
@@ -1064,7 +1083,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
1064
1083
  flushEnd = false, aside,
1065
1084
  /** The page is a verso of mirrored margins (a floated box's `'outer'`
1066
1085
  * corner icon hangs on the left there). */
1067
- mirrored = false) => {
1086
+ mirrored = false,
1087
+ /** A picture scaled to its slot (`placement.shrink`). */
1088
+ cap) => {
1068
1089
  const strip = comicStripOf(resourceId);
1069
1090
  if (strip) {
1070
1091
  const block = buildComicStripBlock(strip.idx, strip.source, x, width, contentArea.height - 2 * floatGapPx, cursor.pageIndex);
@@ -1077,7 +1098,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
1077
1098
  calloutFloatResults.set(result.frame, { result, startIdx, plan: cf.plan });
1078
1099
  return { block: result.frame, height: result.totalHeight };
1079
1100
  }
1080
- const laid = layoutFloat(resourceId, width, slice, rotated, aside);
1101
+ const laid = layoutFloat(resourceId, width, slice, rotated, aside, cap);
1081
1102
  if (!laid)
1082
1103
  return null;
1083
1104
  const { block: rb, totalHeight } = laid;
@@ -1116,6 +1137,52 @@ function placeDocumentPass(content, config, cache, options, hints) {
1116
1137
  /** Pages a `:::pagebreak{center}` opened (#424). */
1117
1138
  const centredPages = new Set();
1118
1139
  const reservedOf = (col) => floatReserved.get(col) ?? { top: 0, bottom: 0 };
1140
+ /** Text wrap (#627): floats set in a band at the head or foot of one
1141
+ * column that text may run beside, per column. The band stays a band
1142
+ * until running text comes to take it back (`openFloatWraps`). */
1143
+ /** The room a wrapped item takes from the lines beside it (#627): its
1144
+ * width and gap, in whole characters of the body under a CJK character
1145
+ * grid (`cjk.grid`), so the lines beside it keep to the grid. */
1146
+ const wrapExclusionWidth = (columnWidth, itemWidth, gap) => {
1147
+ const w = itemWidth + gap;
1148
+ const em = bodyStyle.fontSizePx;
1149
+ return Math.min(columnWidth, resolved.cjk.grid.enabled && em > 0 ? Math.ceil((w - 0.01) / em) * em : w);
1150
+ };
1151
+ /** The `textWrap` warnings of this pass (#627). */
1152
+ const textWrapNotes = [];
1153
+ const floatWraps = new Map();
1154
+ /** Give the bands of the wrapped floats of `col` back to the flow, as
1155
+ * exclusions the text runs beside: a float at the head of a column the
1156
+ * text has not entered yet, one at its foot the text has not reached. */
1157
+ const openFloatWraps = (col) => {
1158
+ const list = floatWraps.get(col);
1159
+ if (!list)
1160
+ return;
1161
+ floatWraps.delete(col);
1162
+ for (const w of list) {
1163
+ const r = { ...reservedOf(col) };
1164
+ if (w.position === 'top') {
1165
+ // A block already stands under it, or another band between: it
1166
+ // keeps its band.
1167
+ if (col.blocks.length > 0 || Math.abs(w.ex.y + w.need - col.bbox.y) > 0.5)
1168
+ continue;
1169
+ col.bbox.y -= w.need;
1170
+ col.bbox.height += w.need;
1171
+ col.availableHeight += w.need;
1172
+ r.top = Math.max(0, r.top - w.need);
1173
+ openColumnWrap(col, w.ex, 'top');
1174
+ }
1175
+ else {
1176
+ if (uncappedBottoms.has(col) || Math.abs(col.bbox.y + col.bbox.height - w.ex.y) > 0.5 || columnCursorY(col) > w.ex.y + 0.01)
1177
+ continue;
1178
+ col.bbox.height += w.need;
1179
+ col.availableHeight += w.need;
1180
+ r.bottom = Math.max(0, r.bottom - w.need);
1181
+ openColumnWrap(col, w.ex, 'bottom');
1182
+ }
1183
+ floatReserved.set(col, r);
1184
+ }
1185
+ };
1119
1186
  const floatHeadOf = (col) => reservedOf(col).top;
1120
1187
  /** `BandCap.headPx` for a cap proposed on `cols`: how far the float bands
1121
1188
  * every column holds at its head put the band's top below its bare one. */
@@ -1369,6 +1436,50 @@ function placeDocumentPass(content, config, cache, options, hints) {
1369
1436
  built.block.contentIndex = anchorIdx;
1370
1437
  (page.floats ??= []).push(built.block);
1371
1438
  };
1439
+ /**
1440
+ * A picture float scaled to `room` px of a slot instead of moving on
1441
+ * (`placement.shrink`, #626): the tallest body that keeps its band
1442
+ * within the room. A top band rounds up to the grid, so the float may
1443
+ * stand as tall as the grid multiple within the room, less its gap; a
1444
+ * bottom band (its caption's last baseline on the grid) and a side
1445
+ * stack give the room back px for px. The caption's height may change
1446
+ * with the picture's width (`captionMeasure: 'body'`) and a safe area
1447
+ * crops before it scales, so the cap is refined over a few rounds. The
1448
+ * scale never goes below the float's `minScale`; `fits` is false when
1449
+ * even that does not hold the room. Null for a float that does not
1450
+ * scale (no `shrink`, not an upright picture, the rest of a table) or
1451
+ * already fits.
1452
+ */
1453
+ const shrinkToRoom = (f, width, aside, full, room, topBand, bandOf) => {
1454
+ if (!f.shrink || !full.body || f.rotate || f.startRow)
1455
+ return null;
1456
+ const minScale = f.minScale ?? DEFAULT_FLOAT_MIN_SCALE;
1457
+ const excessOf = (m, band) => topBand
1458
+ ? m.height + floatGapBelow(m, floatGapPx) - Math.floor((room + 0.01) / baselineGrid) * baselineGrid
1459
+ : band.need - room;
1460
+ let measure = full;
1461
+ let band = bandOf(full);
1462
+ let cap;
1463
+ for (let round = 0; round < 4; round++) {
1464
+ const excess = excessOf(measure, band);
1465
+ if (excess <= 0.01)
1466
+ break;
1467
+ const next = { maxBodyHeight: Math.max(0, measure.body.height - excess), minScale };
1468
+ if (cap && next.maxBodyHeight >= cap.maxBodyHeight - 0.01)
1469
+ break;
1470
+ cap = next;
1471
+ const m = measureFloat(f.resourceId, width, undefined, undefined, aside, cap);
1472
+ if (!m?.body)
1473
+ return null;
1474
+ measure = m;
1475
+ band = bandOf(m);
1476
+ if (m.body.scale <= minScale + 1e-6)
1477
+ break;
1478
+ }
1479
+ if (!cap)
1480
+ return null;
1481
+ return { measure, ...band, cap, fits: excessOf(measure, band) <= 0.01 };
1482
+ };
1372
1483
  const placeFloatInColumns = (page, f, targetCols, position, pageSpan, mode, anchorToCap = false, side = false, refY) => {
1373
1484
  const first = targetCols[0];
1374
1485
  const probe = probeFloatBand(page, f, targetCols, position, pageSpan, anchorToCap, side, refY);
@@ -1377,6 +1488,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
1377
1488
  const { width, xLeft, rotated, aside, sideCol } = probe;
1378
1489
  let { slice, measure, need, y } = probe;
1379
1490
  let rest;
1491
+ /** A picture scaled to this slot (`placement.shrink`, #626). */
1492
+ let cap;
1493
+ const bandOf = (m) => measureFloatBand(position, m, targetCols, page.contentArea, baselineGrid, floatGapPx, (c) => (anchorToCap ? c.bbox.y + c.bbox.height : trueBottom(c, uncappedBottoms)));
1380
1494
  /** The float was cut to this slot (a table slice). */
1381
1495
  let cut = false;
1382
1496
  /** A rotated block wider than the band (its upright height). */
@@ -1386,24 +1500,36 @@ function placeDocumentPass(content, config, cache, options, hints) {
1386
1500
  // when the rest of the column takes it; otherwise it waits for the
1387
1501
  // side column of the next page — where it is set anyway, overflowing,
1388
1502
  // when even an empty column cannot hold it (a dominating figure).
1503
+ let cap;
1389
1504
  if (need > first.availableHeight + 0.01) {
1390
1505
  // A column that already holds something — a float, a box, or a
1391
1506
  // heading design standing in it — is no empty column to overflow.
1392
1507
  const holds = sideUsedBottom(first) > first.bbox.y + 0.5 || (sideObstacles.get(first)?.length ?? 0) > 0;
1393
- if (mode === 'strict' || holds)
1394
- return 'defer';
1395
1508
  // An empty column shortened by a band above it (a page-span box,
1396
1509
  // a top float) is no measure of the float: when a full column
1397
1510
  // would hold it, it waits for one instead of overflowing this one.
1398
- if (shortSideColumn(page, first) && measure.height <= page.contentArea.height + 0.01)
1399
- return 'defer';
1400
- // Overflowing a column a side caption has shortened: the next pass
1401
- // sets that caption under its figure and gives the column back.
1402
- const cutBy = asideCutBy.get(first);
1403
- if (cutBy !== undefined)
1404
- captionUnderProposals.add(cutBy);
1511
+ const waitsForFull = shortSideColumn(page, first) && measure.height <= page.contentArea.height + 0.01;
1512
+ const lastResort = mode === 'fresh' && !holds && !waitsForFull;
1513
+ // A picture scaled to the room the column keeps (#626): any slot
1514
+ // with `'slot'`, when it holds it at its smallest scale or more;
1515
+ // the empty column it would overflow with `'page'` too, at its
1516
+ // smallest scale if need be.
1517
+ if (f.shrink === 'slot' || (f.shrink && lastResort)) {
1518
+ const s = shrinkToRoom(f, width, undefined, measure, first.availableHeight, false, (m) => measureSideStack(m, first, refY, page.contentArea, baselineGrid, floatGapPx, sideObstacles.get(first)));
1519
+ if (s && (s.fits || lastResort))
1520
+ ({ measure, need, y, cap } = s);
1521
+ }
1522
+ if (need > first.availableHeight + 0.01) {
1523
+ if (!lastResort)
1524
+ return 'defer';
1525
+ // Overflowing a column a side caption has shortened: the next pass
1526
+ // sets that caption under its figure and gives the column back.
1527
+ const cutBy = asideCutBy.get(first);
1528
+ if (cutBy !== undefined)
1529
+ captionUnderProposals.add(cutBy);
1530
+ }
1405
1531
  }
1406
- const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated);
1532
+ const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, false, undefined, false, cap);
1407
1533
  if (!built)
1408
1534
  return 'skip';
1409
1535
  first.availableHeight = Math.max(0, first.availableHeight - need);
@@ -1431,9 +1557,26 @@ function placeDocumentPass(content, config, cache, options, hints) {
1431
1557
  const gallery = need <= minAvail + 0.01
1432
1558
  && f.refPageIndex !== undefined && f.refPageIndex < page.index
1433
1559
  && calloutBandPages.has(page) && !calloutFloatOf(f.resourceId);
1434
- if (!gallery)
1435
- return 'defer';
1436
- galleryFill = true;
1560
+ if (gallery) {
1561
+ galleryFill = true;
1562
+ }
1563
+ else {
1564
+ // A picture that scales to any slot (#626) takes the room left
1565
+ // beside the other band, the text room kept, when it holds it
1566
+ // at its smallest scale or more.
1567
+ const s = f.shrink === 'slot' ? shrinkToRoom(f, width, aside, measure, minAvail - minTextPx, position === 'top', bandOf) : null;
1568
+ if (!s?.fits)
1569
+ return 'defer';
1570
+ ({ measure, need, y, cap } = s);
1571
+ }
1572
+ }
1573
+ // Dominating the band: a picture that scales (`'page'` or `'slot'`,
1574
+ // #626) takes the band's room, at its smallest scale if need be —
1575
+ // then it runs past the band, as any dominating float does.
1576
+ if (need > minAvail + 0.01 && f.shrink && !rotated) {
1577
+ const s = shrinkToRoom(f, width, aside, measure, minAvail, position === 'top', bandOf);
1578
+ if (s)
1579
+ ({ measure, need, y, cap } = s);
1437
1580
  }
1438
1581
  if (need > minAvail + 0.01 || tooWide(measure)) {
1439
1582
  // Dominating the band: a table is cut to it (a rotated table, to
@@ -1459,6 +1602,19 @@ function placeDocumentPass(content, config, cache, options, hints) {
1459
1602
  // too wide.
1460
1603
  if (f.rotate || tooWide(measure))
1461
1604
  return 'defer';
1605
+ // A picture that scales to any slot (#626) takes this one when it
1606
+ // holds it at its smallest scale or more (the text room kept next
1607
+ // to another band, as `fitsStrict` keeps it); the loop below checks
1608
+ // the band as for any float.
1609
+ if (f.shrink === 'slot' && measure.body && !rotated) {
1610
+ const room = Math.min(...targetCols.map((c) => c.availableHeight - (columnHasFloatBand(page, c) ? minTextPx : 0)));
1611
+ if (need > room + 0.01) {
1612
+ const s = shrinkToRoom(f, width, aside, measure, room, position === 'top', bandOf);
1613
+ if (!s?.fits)
1614
+ return 'defer';
1615
+ ({ measure, need, y, cap } = s);
1616
+ }
1617
+ }
1462
1618
  for (const c of targetCols) {
1463
1619
  if (position === 'bottom' && uncappedBottoms.has(c) && !anchorToCap) {
1464
1620
  // Trailing cap: the band must lie entirely below the level cut.
@@ -1516,7 +1672,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
1516
1672
  // An upright figure of a vertical page stands at the head of its tier
1517
1673
  // (`placement.align`: `left` the top, `center`, `right` the foot).
1518
1674
  const flush = rotated?.upright ? (f.align === 'center' ? 0.5 : f.align === 'right' ? 1 : 0) : rotated ? rotatedFlushEnd(page) : false;
1519
- const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, flush, aside, mirroredOf(page));
1675
+ const built = buildFloatBlock(f.resourceId, xLeft, width, slice, rotated, flush, aside, mirroredOf(page), cap);
1520
1676
  if (!built)
1521
1677
  return 'skip';
1522
1678
  // The caption beside the figure takes its band of the side column: a
@@ -1571,6 +1727,40 @@ function placeDocumentPass(content, config, cache, options, hints) {
1571
1727
  if (galleryFill)
1572
1728
  for (const col of targetCols)
1573
1729
  col.availableHeight = 0;
1730
+ // Text wrap (#627): a one-column float at the head or foot of its
1731
+ // column, which running text may take back to run beside it — unless
1732
+ // the text left beside it would be too narrow, or the float too short
1733
+ // for lines beside it.
1734
+ if (f.wrap && !pageSpan && !side && targetCols.length === 1 && !rotated && !aside && !cut && !galleryFill
1735
+ && !(position === 'bottom' && uncappedBottoms.has(first))) {
1736
+ const gap = wrapGapPx(f.wrapGap, dpi, bodyStyle.fontSizePx, floatGapPx);
1737
+ const settings = resolved.layout.wrap;
1738
+ const reason = first.bbox.width - width - gap + 0.01 < minTextWidthPx(settings.minTextWidth, first.bbox.width, dpi, bodyStyle.fontSizePx)
1739
+ ? 'tooNarrow'
1740
+ : Math.round(need / bodyStyle.lineHeightPx) < settings.minLinesBeside ? 'fewLines' : undefined;
1741
+ if (reason) {
1742
+ textWrapNotes.push({ kind: 'textWrap', reason, resourceId: f.resourceId, pageIndex: page.index });
1743
+ }
1744
+ else {
1745
+ const exWidth = wrapExclusionWidth(first.bbox.width, width, gap);
1746
+ const exY = position === 'top' ? first.bbox.y - need : first.bbox.y + first.bbox.height;
1747
+ const list = floatWraps.get(first) ?? [];
1748
+ list.push({
1749
+ position,
1750
+ need,
1751
+ ex: {
1752
+ x: f.wrap === 'left' ? first.bbox.x : first.bbox.x + first.bbox.width - exWidth,
1753
+ y: exY,
1754
+ width: exWidth,
1755
+ height: need,
1756
+ side: f.wrap,
1757
+ gap: exWidth - width,
1758
+ ownerId: built.block.id,
1759
+ },
1760
+ });
1761
+ floatWraps.set(first, list);
1762
+ }
1763
+ }
1574
1764
  commitFloatBlock(page, first, built, xLeft, y, width, f.firstBlockIdx);
1575
1765
  floatsPlaced++;
1576
1766
  return rest ? { rest } : 'placed';
@@ -2313,7 +2503,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
2313
2503
  blk.lines = resetLinePositions(raggedLooseLines(m.measured.lines, noteStyle.textAlign), noteStyle.lineHeightPx).map((line) => ({
2314
2504
  ...line,
2315
2505
  bbox: createBoundingBox(line.bbox.x + x, line.bbox.y + y, line.bbox.width, line.bbox.height),
2316
- ...(line.measure ? { measure: { x: line.measure.x + x, width: line.measure.width } } : {}),
2506
+ ...(line.measure ? { measure: { ...line.measure, x: line.measure.x + x } } : {}),
2317
2507
  baseline: line.baseline + y,
2318
2508
  }));
2319
2509
  const h = blk.lines.length * noteStyle.lineHeightPx;
@@ -2775,7 +2965,8 @@ function placeDocumentPass(content, config, cache, options, hints) {
2775
2965
  return;
2776
2966
  const band = currentBand(page, cursor);
2777
2967
  const cols = bandColumns(page, band).filter((c) => c.bbox.height > 0.5);
2778
- if (cols.length < 2 || cols.some((c) => c.forcedBreak))
2968
+ // A band where text wraps round something (#627) is not levelled.
2969
+ if (cols.length < 2 || cols.some((c) => c.forcedBreak || (c.exclusions?.length ?? 0) > 0))
2779
2970
  return;
2780
2971
  if (activeCap && activeCap.spanIndex === boundaryIndex
2781
2972
  && activeCap.pageIndex === page.index && activeCap.band === band) {
@@ -2845,7 +3036,15 @@ function placeDocumentPass(content, config, cache, options, hints) {
2845
3036
  * page the boundary opens: they sit the drain out and stay pending (with
2846
3037
  * no reference stamp, which the citing line sets), so they land after
2847
3038
  * the line that cites them, in the new segment (EF-69). */
3039
+ /** Text wrap (#627): an inline figure that wraps, placed as a band, whose
3040
+ * band the next block takes back to run beside it when it can (see
3041
+ * `pipeline/textWrap.ts`). */
3042
+ let pendingWrap = null;
2848
3043
  const closeFlowSegment = (boundaryIndex) => {
3044
+ // What text wrapped round in the column the segment ends in keeps its
3045
+ // room (#627).
3046
+ pendingWrap = null;
3047
+ settleColumnWraps(currentColumn(doc, cursor));
2849
3048
  settleAwaitingSideBoxes(true);
2850
3049
  releaseStaleCitationGates();
2851
3050
  const ownFloats = [];
@@ -4102,6 +4301,83 @@ function placeDocumentPass(content, config, cache, options, hints) {
4102
4301
  * boxes) still fall back to this inline placement for v1 — the frame's
4103
4302
  * `callout.placement` records the request.
4104
4303
  */
4304
+ /**
4305
+ * A box set `here` that text wraps round (#627): laid out whole at its
4306
+ * share of the column, at its side, level with the next line's top, then
4307
+ * set as a band the next running text takes back to run beside it (see
4308
+ * `pendingWrap`). It never splits: too tall for the room left, it moves
4309
+ * on to the next column (a `textWrap` warning says so). Frame and
4310
+ * children go to the page's floats, where a floated box goes. Returns
4311
+ * false, the box set in the flow as any, when the text beside it would
4312
+ * be too narrow or the box too short for lines beside it.
4313
+ */
4314
+ const placeCalloutWrapped = (startIdx, plan, style, L, frameId, wrap) => {
4315
+ const settings = resolved.layout.wrap;
4316
+ const gap = wrapGapPx(wrap.gap, dpi, bodyStyle.fontSizePx, floatGapPx);
4317
+ const note = (reason) => {
4318
+ const start = contentBlocks[startIdx];
4319
+ textWrapNotes.push({ kind: 'textWrap', reason, box: style.id, sourceStart: start.sourceStart + bodyOffset, sourceEnd: start.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex });
4320
+ };
4321
+ let col = currentColumn(doc, cursor);
4322
+ if (col.bbox.width * (1 - wrap.width) - gap + 0.01 < minTextWidthPx(settings.minTextWidth, col.bbox.width, dpi, bodyStyle.fontSizePx)) {
4323
+ note('tooNarrow');
4324
+ return false;
4325
+ }
4326
+ for (let attempt = 0;; attempt++) {
4327
+ col = currentColumn(doc, cursor);
4328
+ const page = doc.pages[cursor.pageIndex];
4329
+ const width = col.bbox.width * wrap.width;
4330
+ const result = L.layoutRange(CUT_START, L.end, width, frameId, false, mirroredOf(page));
4331
+ if (attempt === 0 && Math.ceil((result.totalHeight + gap - 0.01) / bodyStyle.lineHeightPx) < settings.minLinesBeside) {
4332
+ note('fewLines');
4333
+ return false;
4334
+ }
4335
+ const spacing = col.blocks.length === 0 ? 0 : pendingSpacing;
4336
+ if (result.totalHeight > col.availableHeight - spacing + 0.01 && col.blocks.length > 0 && attempt < 4) {
4337
+ if (attempt === 0)
4338
+ note('moved');
4339
+ pendingSpacing = 0;
4340
+ advanceToNextColumn(doc, cursor, geomResolved, contentArea, pageWidthPx, pageHeightPx, onNewPage);
4341
+ continue;
4342
+ }
4343
+ const top = columnCursorY(col) + spacing;
4344
+ const x = wrap.side === 'left' ? col.bbox.x : col.bbox.x + col.bbox.width - width;
4345
+ const frame = result.frame;
4346
+ stampCalloutSource(frame, startIdx, plan);
4347
+ frame.pageIndex = page.index;
4348
+ frame.columnIndex = col.index;
4349
+ offsetCalloutToAbsolute(result, x, top);
4350
+ const floats = (page.floats ??= []);
4351
+ doc.blocks.push(frame);
4352
+ floats.push(frame);
4353
+ for (const child of result.children) {
4354
+ child.pageIndex = page.index;
4355
+ child.columnIndex = col.index;
4356
+ doc.blocks.push(child);
4357
+ floats.push(child);
4358
+ }
4359
+ const bottomRel = Math.ceil((top - col.bbox.y + result.totalHeight + gap - 0.01) / baselineGrid) * baselineGrid;
4360
+ col.availableHeight = Math.max(0, Math.min(col.availableHeight, col.bbox.height - bottomRel));
4361
+ pendingSpacing = 0;
4362
+ inlineGapOwed = null;
4363
+ const exWidth = wrapExclusionWidth(col.bbox.width, width, gap);
4364
+ pendingWrap = {
4365
+ col,
4366
+ blocks: col.blocks.length,
4367
+ cursorY: columnCursorY(col),
4368
+ ex: {
4369
+ x: wrap.side === 'left' ? col.bbox.x : col.bbox.x + col.bbox.width - exWidth,
4370
+ y: top,
4371
+ width: exWidth,
4372
+ height: col.bbox.y + bottomRel - top,
4373
+ side: wrap.side,
4374
+ gap: exWidth - width,
4375
+ ownerId: frame.id,
4376
+ },
4377
+ };
4378
+ return true;
4379
+ }
4380
+ };
4105
4381
  const placeCalloutInline = (startIdx, plan) => {
4106
4382
  const style = pickCalloutStyle(calloutStyles, plan.attrs.type);
4107
4383
  const firstFrameId = `block-${blockIdCounter++}`;
@@ -4130,7 +4406,17 @@ function placeDocumentPass(content, config, cache, options, hints) {
4130
4406
  const inFlow = tallerThanColumn(L.layoutRange(CUT_START, L.end, width, 'float-probe', false))
4131
4407
  && splitCalloutFragment(L, CUT_START, width, contentArea.height, 'float-probe', false, style.splitMinLines) !== null;
4132
4408
  if (!inFlow) {
4133
- enqueueCalloutFloat(startIdx, plan, style, L, placement, span);
4409
+ const wrap = pageIsVertical(page) ? undefined : resolveCalloutWrap(plan.attrs, resolved.layout.wrap);
4410
+ enqueueCalloutFloat(startIdx, plan, style, L, placement, span, wrap);
4411
+ calloutsOutOfFlow.add(startIdx);
4412
+ return undefined;
4413
+ }
4414
+ }
4415
+ // A box text wraps round (#627): at its side of the column, the text
4416
+ // after it beside it.
4417
+ if (placement === 'here' && span === 'column' && !pageIsVertical(doc.pages[cursor.pageIndex])) {
4418
+ const wrap = resolveCalloutWrap(plan.attrs, resolved.layout.wrap);
4419
+ if (wrap && placeCalloutWrapped(startIdx, plan, style, L, firstFrameId, wrap)) {
4134
4420
  calloutsOutOfFlow.add(startIdx);
4135
4421
  return undefined;
4136
4422
  }
@@ -4469,6 +4755,29 @@ function placeDocumentPass(content, config, cache, options, hints) {
4469
4755
  releaseStaleCitationGates(blockIdx);
4470
4756
  tryPlacePendingFloatsOnCurrentPage(spanBoxAt(blockIdx), blockIdx);
4471
4757
  enqueueFloatsFor(blockIdx);
4758
+ // Text wrap (#627): running text after a wrapped figure takes the band
4759
+ // the figure was set in back, and runs beside it; anything else that
4760
+ // takes room from the flow goes under what text wraps round first.
4761
+ // Markers and the `::resource` line of a floated resource leave it.
4762
+ if (!transparentToWrap(rawBlock) && !(rawBlock.type === 'resourceBlock' && rawBlock.resourceId !== undefined && floatedIds.has(rawBlock.resourceId))) {
4763
+ const wrapCol = currentColumn(doc, cursor);
4764
+ const beside = runsBesideWrap(rawBlock);
4765
+ if (pendingWrap) {
4766
+ const { col, blocks, cursorY, ex } = pendingWrap;
4767
+ pendingWrap = null;
4768
+ if (beside && col === wrapCol && col.blocks.length === blocks && Math.abs(columnCursorY(col) - cursorY) < 0.01) {
4769
+ // The cursor goes back to the figure's top; the text starts there.
4770
+ col.availableHeight += columnCursorY(col) - ex.y;
4771
+ pendingSpacing = 0;
4772
+ inlineGapOwed = null;
4773
+ openColumnWrap(col, ex, 'top');
4774
+ }
4775
+ }
4776
+ if (!beside && settleColumnWraps(wrapCol)) {
4777
+ pendingSpacing = 0;
4778
+ inlineGapOwed = null;
4779
+ }
4780
+ }
4472
4781
  // --- Directives ----------------------------------------------------
4473
4782
  // (A comic page, `:::page`, and a strip, `:::strip`, are placed below,
4474
4783
  // once a closed part or paper run has taken its page break.)
@@ -4962,12 +5271,55 @@ function placeDocumentPass(content, config, cache, options, hints) {
4962
5271
  segments: [],
4963
5272
  isLastLine: true,
4964
5273
  }];
5274
+ // Text wrap (#627): a picture the text after it may run beside, at
5275
+ // its side of the column — unless the text left beside it would be
5276
+ // too narrow, or the picture too short to set lines beside.
5277
+ const wrapSettings = blockMeasureCtx.resolved.layout.wrap;
5278
+ const wrapSpec = kind.resource && !pageIsVertical(blockPage)
5279
+ ? resolveResourceWrap(kind.resource, kind.resourceType, wrapSettings)
5280
+ : undefined;
5281
+ // Vertical text keeps its figures' bands whole (horizontal wrap only).
5282
+ if (kind.resource && pageIsVertical(blockPage) && resolveResourceWrap(kind.resource, kind.resourceType, wrapSettings)) {
5283
+ textWrapNotes.push({
5284
+ kind: 'textWrap', reason: 'verticalText', resourceId: kind.resource.id,
5285
+ sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
5286
+ });
5287
+ }
5288
+ let wrapGap = 0;
5289
+ let wraps = false;
5290
+ if (wrapSpec) {
5291
+ wrapGap = wrapGapPx(wrapSpec.gap, dpi, bodyStyle.fontSizePx, floatGapPx);
5292
+ const textWidth = col.bbox.width * (1 - wrapSpec.width) - wrapGap;
5293
+ const linesBeside = Math.ceil((groupHeight + wrapGap - 0.01) / bodyStyle.lineHeightPx);
5294
+ const reason = textWidth + 0.01 < minTextWidthPx(wrapSettings.minTextWidth, col.bbox.width, dpi, bodyStyle.fontSizePx)
5295
+ ? 'tooNarrow'
5296
+ : linesBeside < wrapSettings.minLinesBeside ? 'fewLines' : undefined;
5297
+ if (reason) {
5298
+ textWrapNotes.push({
5299
+ kind: 'textWrap', reason, resourceId: kind.resource.id,
5300
+ sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
5301
+ });
5302
+ }
5303
+ else {
5304
+ wraps = true;
5305
+ }
5306
+ }
4965
5307
  // An inline resource keeps the float gap (a line) above it, as a
4966
- // float would, unless the block before asked for more.
4967
- const spacingBefore = Math.max(pendingSpacing, floatGapPx);
5308
+ // float would, unless the block before asked for more. A wrapped one
5309
+ // stands level with the next line's top instead.
5310
+ const spacingBefore = wraps ? pendingSpacing : Math.max(pendingSpacing, floatGapPx);
5311
+ const columnBefore = col;
4968
5312
  enterBand(blockIdx, 0);
4969
5313
  placeAtomicBlock(blk, groupHeight, spacingBefore, cursor, doc, geomResolved, contentArea, pageWidthPx, pageHeightPx, onAtomicNewPage(groupHeight));
4970
5314
  enterBand(blockIdx, 0);
5315
+ // Too tall for the room left in its column, a wrapped picture moved
5316
+ // on with its anchor: say so.
5317
+ if (wraps && currentColumn(doc, cursor) !== columnBefore && columnBefore.blocks.length > 0) {
5318
+ textWrapNotes.push({
5319
+ kind: 'textWrap', reason: 'moved', resourceId: kind.resource.id,
5320
+ sourceStart: rawBlock.sourceStart + bodyOffset, sourceEnd: rawBlock.sourceEnd + bodyOffset, pageIndex: cursor.pageIndex,
5321
+ });
5322
+ }
4971
5323
  // `placeBlockInColumn` (inside placeAtomicBlock) shifts `blk.lines`; the
4972
5324
  // resource's own caption/table lines live on `resourceBlock` and must be
4973
5325
  // offset to absolute page coordinates here using the placed bbox origin.
@@ -4978,6 +5330,34 @@ function placeDocumentPass(content, config, cache, options, hints) {
4978
5330
  addBalanceExtra(currentColumn(doc, cursor), grown);
4979
5331
  }
4980
5332
  doc.blocks.push(blk);
5333
+ // A wrapped picture (#627): the flow goes on under it and its gap, on
5334
+ // a grid line, unless the next block takes the band back to run
5335
+ // beside it (see `pendingWrap`).
5336
+ if (wraps && wrapSpec) {
5337
+ const rCol = currentColumn(doc, cursor);
5338
+ const top = blk.bbox.y;
5339
+ const bottomRel = Math.ceil((top - rCol.bbox.y + groupHeight + wrapGap - 0.01) / baselineGrid) * baselineGrid;
5340
+ rCol.availableHeight = Math.max(0, Math.min(rCol.availableHeight, rCol.bbox.height - bottomRel));
5341
+ pendingSpacing = 0;
5342
+ inlineGapOwed = null;
5343
+ const exWidth = wrapExclusionWidth(rCol.bbox.width, rCol.bbox.width * wrapSpec.width, wrapGap);
5344
+ pendingWrap = {
5345
+ col: rCol,
5346
+ blocks: rCol.blocks.length,
5347
+ cursorY: columnCursorY(rCol),
5348
+ ex: {
5349
+ x: wrapSpec.side === 'left' ? rCol.bbox.x : rCol.bbox.x + rCol.bbox.width - exWidth,
5350
+ y: top,
5351
+ width: exWidth,
5352
+ height: rCol.bbox.y + bottomRel - top,
5353
+ side: wrapSpec.side,
5354
+ gap: exWidth - rCol.bbox.width * wrapSpec.width,
5355
+ ownerId: blk.id,
5356
+ },
5357
+ };
5358
+ flushPendingNumberingAtBoundary();
5359
+ continue;
5360
+ }
4981
5361
  // Snap the flow position after the resource to the baseline grid (the
4982
5362
  // group height is arbitrary), baking in at least marginBottom — same
4983
5363
  // convention as snapped headings — so the following text lands back on
@@ -5045,7 +5425,9 @@ function placeDocumentPass(content, config, cache, options, hints) {
5045
5425
  // `bulletXOffsetInColumn` is `indentPx` for unordered/task, and
5046
5426
  // `indentPx + (maxNumberWidth - thisNumberWidth)` for ordered — giving
5047
5427
  // the right-aligned separator.
5048
- blk.bulletOffsetX = blk.bbox.x + bulletXOffsetInColumn;
5428
+ // Beside a wrapped picture on the left (#627) the marker stands past
5429
+ // it, with its text.
5430
+ blk.bulletOffsetX = blk.bbox.x + bulletXOffsetInColumn + lineInsetsAt(wrapInsets, 0).start;
5049
5431
  if (listBullet.separatorText !== undefined) {
5050
5432
  blk.separatorText = listBullet.separatorText;
5051
5433
  blk.separatorFontString = listBullet.separatorFontString;
@@ -5143,6 +5525,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
5143
5525
  * default opener paints across the page. */
5144
5526
  let brokenForWidth = measureWidth;
5145
5527
  let laterWidths = [];
5528
+ /** Text wrap (#627): the lines of the block set short beside what text
5529
+ * wraps round, as its lines are now set (paragraph line numbers). */
5530
+ let wrapInsets = [];
5531
+ const wrapsBeside = canSplit && runsBesideWrap(rawBlock);
5146
5532
  const rebreaksForColumn = !tryLoose && rawBlock.toc === undefined && rawBlock.index === undefined
5147
5533
  && (canSplit || (vdtType === 'heading' && !opensDefaultOpener(rawBlock)));
5148
5534
  /** Measure the block again for the columns it is set in (see
@@ -5150,6 +5536,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5150
5536
  const measureForColumns = (extra) => measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
5151
5537
  styleOverride,
5152
5538
  ...(laterWidths.length > 0 ? { restColumnWidths: laterWidths } : {}),
5539
+ ...(wrapInsets.length > 0 ? { lineInsets: wrapInsets } : {}),
5153
5540
  ...extra,
5154
5541
  });
5155
5542
  // "Keep with next" for colon-introduced lists: a paragraph ending in `:`
@@ -5169,6 +5556,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
5169
5556
  while (remainingLines.length > 0) {
5170
5557
  enterBand(blockIdx, partIndex);
5171
5558
  const curCol = currentColumn(doc, cursor);
5559
+ // A wrapped float at the head or foot of this column (#627): running
5560
+ // text takes its band back to run beside it.
5561
+ if (wrapsBeside)
5562
+ openFloatWraps(curCol);
5172
5563
  // The rest of the block goes into a column of another width than the
5173
5564
  // one it was broken for (EF-157): break it again for this one. Up to
5174
5565
  // postext 1.4 it kept the lines of the column it started in, and a
@@ -5183,6 +5574,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5183
5574
  capInfo = again.dropCap;
5184
5575
  brokenForWidth = curCol.bbox.width;
5185
5576
  laterWidths = [];
5577
+ wrapInsets = [];
5186
5578
  letterSpacingPx = again.letterSpacingPx;
5187
5579
  remainingLines = [...raggedLooseLines(again.measured.lines, style.textAlign)];
5188
5580
  paragraphLines = remainingLines;
@@ -5201,8 +5593,10 @@ function placeDocumentPass(content, config, cache, options, hints) {
5201
5593
  const keepBreaks = paragraphBreaks && paragraphBreaks.at.length >= placed
5202
5594
  ? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
5203
5595
  : undefined;
5596
+ const keptInsets = insetsBefore(wrapInsets, placed);
5204
5597
  const again = measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
5205
5598
  styleOverride, restColumnWidths: steps, ...(keepBreaks ? { keepBreaks } : {}),
5599
+ ...(keptInsets.length > 0 ? { lineInsets: keptInsets } : {}),
5206
5600
  });
5207
5601
  const lines = again && (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0)
5208
5602
  ? [...raggedLooseLines(again.measured.lines, style.textAlign)]
@@ -5210,6 +5604,7 @@ function placeDocumentPass(content, config, cache, options, hints) {
5210
5604
  if (lines && lines.length > placed
5211
5605
  && paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated)) {
5212
5606
  laterWidths = steps;
5607
+ wrapInsets = keptInsets;
5213
5608
  paragraphLines = lines;
5214
5609
  paragraphBreaks = again.measured.breaks;
5215
5610
  remainingLines = lines.slice(placed);
@@ -5321,6 +5716,53 @@ function placeDocumentPass(content, config, cache, options, hints) {
5321
5716
  if (partIndex === 0 && capInfo && capInfo.rise > 0.5) {
5322
5717
  spacingBefore += Math.ceil((capInfo.rise - 0.5) / baselineGrid) * baselineGrid;
5323
5718
  }
5719
+ // Text wrap (#627): the lines this block would set beside what text
5720
+ // wraps round in this column are broken short of it; the lines it
5721
+ // already placed elsewhere keep their breaks. A block that cannot
5722
+ // keep them (a break trace of the other measuring path) goes under
5723
+ // what it would have run beside instead.
5724
+ if (wrapsBeside) {
5725
+ const placed = paragraphLines.length - remainingLines.length;
5726
+ const y0 = columnCursorY(curCol) + spacingBefore;
5727
+ const wanted = wrapInsetSteps(curCol, y0, style.lineHeightPx, placed);
5728
+ if (!sameInsetsFrom(wrapInsets, wanted, placed)) {
5729
+ const steps = [...insetsBefore(wrapInsets, placed), ...wanted];
5730
+ const keepBreaks = placed > 0 && paragraphBreaks && paragraphBreaks.at.length >= placed
5731
+ ? { path: paragraphBreaks.path, at: paragraphBreaks.at.slice(0, placed) }
5732
+ : undefined;
5733
+ const again = placed > 0 && !keepBreaks ? null : measureContentBlock(rawBlock, blockIdx, brokenForWidth, blockMeasureCtx, {
5734
+ styleOverride,
5735
+ ...(laterWidths.length > 0 ? { restColumnWidths: laterWidths } : {}),
5736
+ ...(steps.length > 0 ? { lineInsets: steps } : {}),
5737
+ ...(keepBreaks ? { keepBreaks } : {}),
5738
+ });
5739
+ const lines = again && (placed === 0 || (again.letterSpacingPx ?? 0) === (letterSpacingPx ?? 0))
5740
+ ? [...raggedLooseLines(again.measured.lines, style.textAlign)]
5741
+ : undefined;
5742
+ if (lines && lines.length > placed
5743
+ && paragraphLines.slice(0, placed).every((l, i) => lines[i].text === l.text && lines[i].hyphenated === l.hyphenated)) {
5744
+ wrapInsets = steps;
5745
+ paragraphLines = lines;
5746
+ paragraphBreaks = again.measured.breaks;
5747
+ remainingLines = lines.slice(placed);
5748
+ if (placed === 0) {
5749
+ capInfo = again.dropCap;
5750
+ letterSpacingPx = again.letterSpacingPx;
5751
+ guardedEnds = [];
5752
+ columnEndHyphenRetried = false;
5753
+ }
5754
+ regateCitedFloats(blockIdx, lines);
5755
+ }
5756
+ else if (wanted.length > 0) {
5757
+ if (balancing && balancing.spaceAbove > 0) {
5758
+ balanceExtraInColumn.set(curCol, (balanceExtraInColumn.get(curCol) ?? 0) - balancing.spaceAbove);
5759
+ }
5760
+ settleColumnWraps(curCol);
5761
+ pendingSpacing = 0;
5762
+ continue;
5763
+ }
5764
+ }
5765
+ }
5324
5766
  // A loose paragraph's extra line is balancing height too (it lands
5325
5767
  // whole in this column — loose candidates are never split parts).
5326
5768
  const looseTracking = looseOutcome.get(blockIdx);
@@ -6255,6 +6697,15 @@ function placeDocumentPass(content, config, cache, options, hints) {
6255
6697
  joiningSpacingWarnings.set(doc, joiningLetterSpacingWarnings(doc, joiningLetterSpacing));
6256
6698
  if (dropCapNotes.size > 0)
6257
6699
  dropCapWarningsByDoc.set(doc, dropCapWarnings(doc, dropCapNotes));
6700
+ // Pictures scaled to the room of their slot (#626), at the block citing them.
6701
+ const shrunk = floatShrinkWarnings(doc, (idx) => {
6702
+ const b = contentBlocks[idx];
6703
+ return b ? { start: b.sourceStart + bodyOffset, end: b.sourceEnd + bodyOffset } : undefined;
6704
+ });
6705
+ if (shrunk.length > 0)
6706
+ floatShrinkWarningsByDoc.set(doc, shrunk);
6707
+ if (textWrapNotes.length > 0)
6708
+ textWrapWarningsByDoc.set(doc, textWrapNotes);
6258
6709
  return { doc, forcedBreakPages, bandCapProposals, spanPlacedInBand, bandCapsApplied, looseOutcome, captionUnderProposals };
6259
6710
  }
6260
6711
  /** Passes a document printing its own contents gets at most, beyond the
@@ -6315,7 +6766,9 @@ export function* buildDocumentGen(content, config, cache, options) {
6315
6766
  // cap; words of a joining script that run past their line, and styles
6316
6767
  // whose letter-spacing such words do not take.
6317
6768
  // Line numbers set over a float of the side column (#621).
6318
- const loose = [...cjkLooseLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc)];
6769
+ const loose = [...cjkLooseLineWarnings(doc), ...wordOverflowWarnings(doc), ...(joiningSpacingWarnings.get(doc) ?? []), ...(dropCapWarningsByDoc.get(doc) ?? []), ...lineNumberWarnings(doc), ...codeOverflowWarnings(doc), ...(floatShrinkWarningsByDoc.get(doc) ?? []), ...(textWrapWarningsByDoc.get(doc) ?? [])];
6770
+ // Design texts cut to fit their width (#628).
6771
+ loose.push(...designTruncationWarnings(doc));
6319
6772
  // Comic panels whose cell cannot hold their picture's safe area (#556).
6320
6773
  for (const page of doc.pages) {
6321
6774
  if (page.comic)
@@ -6360,6 +6813,10 @@ const indexWarnings = new WeakMap();
6360
6813
  const joiningSpacingWarnings = new WeakMap();
6361
6814
  /** The `dropCap` warnings of the last pass, per document (#623). */
6362
6815
  const dropCapWarningsByDoc = new WeakMap();
6816
+ /** The `floatShrunk` warnings of a build (#626), found by its last pass. */
6817
+ const floatShrinkWarningsByDoc = new WeakMap();
6818
+ /** The `textWrap` warnings of a build (#627), found by its last pass. */
6819
+ const textWrapWarningsByDoc = new WeakMap();
6363
6820
  /** `doc` with the page of each of its index marks (`doc.indexMarks`). */
6364
6821
  function withIndexMarks(doc, content) {
6365
6822
  const { content: body, contentOffset } = extractFrontmatter(content.markdown);