@react-x11/components 0.8.0 → 0.10.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 (721) hide show
  1. package/README.md +105 -38
  2. package/dist/charts/node.d.ts +12 -2
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +50 -16
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/charts/render.d.ts +12 -3
  7. package/dist/charts/render.d.ts.map +1 -1
  8. package/dist/charts/render.js +94 -25
  9. package/dist/charts/render.js.map +1 -1
  10. package/dist/code/index.d.ts.map +1 -1
  11. package/dist/code/index.js +3 -1
  12. package/dist/code/index.js.map +1 -1
  13. package/dist/code-editor/completion.d.ts.map +1 -1
  14. package/dist/code-editor/completion.js +3 -2
  15. package/dist/code-editor/completion.js.map +1 -1
  16. package/dist/code-editor/index.d.ts.map +1 -1
  17. package/dist/code-editor/index.js +19 -0
  18. package/dist/code-editor/index.js.map +1 -1
  19. package/dist/code-editor/node.d.ts +177 -7
  20. package/dist/code-editor/node.d.ts.map +1 -1
  21. package/dist/code-editor/node.js +1329 -164
  22. package/dist/code-editor/node.js.map +1 -1
  23. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  24. package/dist/code-language/languages/glsl.js +10 -2
  25. package/dist/code-language/languages/glsl.js.map +1 -1
  26. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  27. package/dist/code-language/languages/javascript.js +18 -3
  28. package/dist/code-language/languages/javascript.js.map +1 -1
  29. package/dist/code-language/languages/json.d.ts.map +1 -1
  30. package/dist/code-language/languages/json.js +10 -2
  31. package/dist/code-language/languages/json.js.map +1 -1
  32. package/dist/code-language/languages/shell.d.ts.map +1 -1
  33. package/dist/code-language/languages/shell.js +10 -2
  34. package/dist/code-language/languages/shell.js.map +1 -1
  35. package/dist/code-language/languages/sql.d.ts.map +1 -1
  36. package/dist/code-language/languages/sql.js +20 -2
  37. package/dist/code-language/languages/sql.js.map +1 -1
  38. package/dist/code-language/stream.d.ts +22 -0
  39. package/dist/code-language/stream.d.ts.map +1 -1
  40. package/dist/code-language/stream.js +259 -9
  41. package/dist/code-language/stream.js.map +1 -1
  42. package/dist/code-language/types.d.ts +5 -4
  43. package/dist/code-language/types.d.ts.map +1 -1
  44. package/dist/codeblock/index.d.ts +13 -2
  45. package/dist/codeblock/index.d.ts.map +1 -1
  46. package/dist/codeblock/index.js +20 -3
  47. package/dist/codeblock/index.js.map +1 -1
  48. package/dist/embed/client.d.ts +48 -3
  49. package/dist/embed/client.d.ts.map +1 -1
  50. package/dist/embed/client.js +79 -5
  51. package/dist/embed/client.js.map +1 -1
  52. package/dist/embed/index.d.ts +1 -1
  53. package/dist/embed/index.d.ts.map +1 -1
  54. package/dist/embed/index.js +1 -1
  55. package/dist/embed/index.js.map +1 -1
  56. package/dist/flow/draw.d.ts +50 -0
  57. package/dist/flow/draw.d.ts.map +1 -1
  58. package/dist/flow/draw.js +132 -33
  59. package/dist/flow/draw.js.map +1 -1
  60. package/dist/flow/gl/color.d.ts +13 -0
  61. package/dist/flow/gl/color.d.ts.map +1 -0
  62. package/dist/flow/gl/color.js +82 -0
  63. package/dist/flow/gl/color.js.map +1 -0
  64. package/dist/flow/gl/index.d.ts +60 -0
  65. package/dist/flow/gl/index.d.ts.map +1 -0
  66. package/dist/flow/gl/index.js +283 -0
  67. package/dist/flow/gl/index.js.map +1 -0
  68. package/dist/flow/gl/pack.d.ts +133 -0
  69. package/dist/flow/gl/pack.d.ts.map +1 -0
  70. package/dist/flow/gl/pack.js +408 -0
  71. package/dist/flow/gl/pack.js.map +1 -0
  72. package/dist/flow/gl/renderer.d.ts +95 -0
  73. package/dist/flow/gl/renderer.d.ts.map +1 -0
  74. package/dist/flow/gl/renderer.js +459 -0
  75. package/dist/flow/gl/renderer.js.map +1 -0
  76. package/dist/flow/gl/shaders.d.ts +79 -0
  77. package/dist/flow/gl/shaders.d.ts.map +1 -0
  78. package/dist/flow/gl/shaders.js +346 -0
  79. package/dist/flow/gl/shaders.js.map +1 -0
  80. package/dist/flow/gl/text.d.ts +318 -0
  81. package/dist/flow/gl/text.d.ts.map +1 -0
  82. package/dist/flow/gl/text.js +1103 -0
  83. package/dist/flow/gl/text.js.map +1 -0
  84. package/dist/flow/index.d.ts +1 -1
  85. package/dist/flow/index.d.ts.map +1 -1
  86. package/dist/flow/index.js +619 -64
  87. package/dist/flow/index.js.map +1 -1
  88. package/dist/flow/node.d.ts +621 -72
  89. package/dist/flow/node.d.ts.map +1 -1
  90. package/dist/flow/node.js +2253 -1018
  91. package/dist/flow/node.js.map +1 -1
  92. package/dist/flow/paint.d.ts +35 -0
  93. package/dist/flow/paint.d.ts.map +1 -0
  94. package/dist/flow/paint.js +393 -0
  95. package/dist/flow/paint.js.map +1 -0
  96. package/dist/flow/paths.d.ts +14 -0
  97. package/dist/flow/paths.d.ts.map +1 -1
  98. package/dist/flow/paths.js +45 -19
  99. package/dist/flow/paths.js.map +1 -1
  100. package/dist/flow/scene.d.ts +584 -0
  101. package/dist/flow/scene.d.ts.map +1 -0
  102. package/dist/flow/scene.js +1453 -0
  103. package/dist/flow/scene.js.map +1 -0
  104. package/dist/flow/types.d.ts +111 -1
  105. package/dist/flow/types.d.ts.map +1 -1
  106. package/dist/html/controls.d.ts +28 -0
  107. package/dist/html/controls.d.ts.map +1 -1
  108. package/dist/html/controls.js +52 -2
  109. package/dist/html/controls.js.map +1 -1
  110. package/dist/html/css/calc.d.ts +14 -0
  111. package/dist/html/css/calc.d.ts.map +1 -0
  112. package/dist/html/css/calc.js +344 -0
  113. package/dist/html/css/calc.js.map +1 -0
  114. package/dist/html/css/cascade.d.ts +240 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1327 -66
  117. package/dist/html/css/cascade.js.map +1 -1
  118. package/dist/html/css/color.d.ts +23 -0
  119. package/dist/html/css/color.d.ts.map +1 -0
  120. package/dist/html/css/color.js +740 -0
  121. package/dist/html/css/color.js.map +1 -0
  122. package/dist/html/css/content.d.ts +92 -0
  123. package/dist/html/css/content.d.ts.map +1 -0
  124. package/dist/html/css/content.js +388 -0
  125. package/dist/html/css/content.js.map +1 -0
  126. package/dist/html/css/counter-styles.d.ts +76 -0
  127. package/dist/html/css/counter-styles.d.ts.map +1 -0
  128. package/dist/html/css/counter-styles.js +893 -0
  129. package/dist/html/css/counter-styles.js.map +1 -0
  130. package/dist/html/css/decode.d.ts +12 -0
  131. package/dist/html/css/decode.d.ts.map +1 -0
  132. package/dist/html/css/decode.js +116 -0
  133. package/dist/html/css/decode.js.map +1 -0
  134. package/dist/html/css/parse.d.ts +110 -10
  135. package/dist/html/css/parse.d.ts.map +1 -1
  136. package/dist/html/css/parse.js +1473 -180
  137. package/dist/html/css/parse.js.map +1 -1
  138. package/dist/html/css/style.d.ts +498 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3698 -206
  141. package/dist/html/css/style.js.map +1 -1
  142. package/dist/html/css/ua.d.ts.map +1 -1
  143. package/dist/html/css/ua.js +57 -11
  144. package/dist/html/css/ua.js.map +1 -1
  145. package/dist/html/css/values.d.ts +37 -3
  146. package/dist/html/css/values.d.ts.map +1 -1
  147. package/dist/html/css/values.js +122 -48
  148. package/dist/html/css/values.js.map +1 -1
  149. package/dist/html/css/vars.d.ts +52 -0
  150. package/dist/html/css/vars.d.ts.map +1 -0
  151. package/dist/html/css/vars.js +358 -0
  152. package/dist/html/css/vars.js.map +1 -0
  153. package/dist/html/dom.d.ts +30 -4
  154. package/dist/html/dom.d.ts.map +1 -1
  155. package/dist/html/dom.js +278 -17
  156. package/dist/html/dom.js.map +1 -1
  157. package/dist/html/fonts.d.ts +106 -0
  158. package/dist/html/fonts.d.ts.map +1 -0
  159. package/dist/html/fonts.js +565 -0
  160. package/dist/html/fonts.js.map +1 -0
  161. package/dist/html/gif.d.ts +15 -0
  162. package/dist/html/gif.d.ts.map +1 -0
  163. package/dist/html/gif.js +229 -0
  164. package/dist/html/gif.js.map +1 -0
  165. package/dist/html/index.d.ts +50 -11
  166. package/dist/html/index.d.ts.map +1 -1
  167. package/dist/html/index.js +115 -9
  168. package/dist/html/index.js.map +1 -1
  169. package/dist/html/layout/block.d.ts +226 -3
  170. package/dist/html/layout/block.d.ts.map +1 -1
  171. package/dist/html/layout/block.js +2937 -197
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +340 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1714 -182
  176. package/dist/html/layout/boxes.js.map +1 -1
  177. package/dist/html/layout/cache.d.ts +58 -0
  178. package/dist/html/layout/cache.d.ts.map +1 -0
  179. package/dist/html/layout/cache.js +224 -0
  180. package/dist/html/layout/cache.js.map +1 -0
  181. package/dist/html/layout/collapse.d.ts +45 -0
  182. package/dist/html/layout/collapse.d.ts.map +1 -0
  183. package/dist/html/layout/collapse.js +246 -0
  184. package/dist/html/layout/collapse.js.map +1 -0
  185. package/dist/html/layout/css-grid.d.ts +7 -0
  186. package/dist/html/layout/css-grid.d.ts.map +1 -0
  187. package/dist/html/layout/css-grid.js +900 -0
  188. package/dist/html/layout/css-grid.js.map +1 -0
  189. package/dist/html/layout/flex.d.ts.map +1 -1
  190. package/dist/html/layout/flex.js +1025 -89
  191. package/dist/html/layout/flex.js.map +1 -1
  192. package/dist/html/layout/floats.d.ts +45 -6
  193. package/dist/html/layout/floats.d.ts.map +1 -1
  194. package/dist/html/layout/floats.js +113 -14
  195. package/dist/html/layout/floats.js.map +1 -1
  196. package/dist/html/layout/grid-lines.d.ts +39 -0
  197. package/dist/html/layout/grid-lines.d.ts.map +1 -0
  198. package/dist/html/layout/grid-lines.js +119 -0
  199. package/dist/html/layout/grid-lines.js.map +1 -0
  200. package/dist/html/layout/grid.d.ts +53 -0
  201. package/dist/html/layout/grid.d.ts.map +1 -0
  202. package/dist/html/layout/grid.js +153 -0
  203. package/dist/html/layout/grid.js.map +1 -0
  204. package/dist/html/layout/inline.d.ts +151 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3375 -174
  207. package/dist/html/layout/inline.js.map +1 -1
  208. package/dist/html/layout/shaping.d.ts +6 -0
  209. package/dist/html/layout/shaping.d.ts.map +1 -0
  210. package/dist/html/layout/shaping.js +100 -0
  211. package/dist/html/layout/shaping.js.map +1 -0
  212. package/dist/html/layout/table.d.ts +3 -0
  213. package/dist/html/layout/table.d.ts.map +1 -1
  214. package/dist/html/layout/table.js +681 -175
  215. package/dist/html/layout/table.js.map +1 -1
  216. package/dist/html/layout/tracks.d.ts +44 -0
  217. package/dist/html/layout/tracks.d.ts.map +1 -0
  218. package/dist/html/layout/tracks.js +349 -0
  219. package/dist/html/layout/tracks.js.map +1 -0
  220. package/dist/html/node.d.ts +172 -5
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1336 -103
  223. package/dist/html/node.js.map +1 -1
  224. package/dist/html/paint.d.ts +92 -3
  225. package/dist/html/paint.d.ts.map +1 -1
  226. package/dist/html/paint.js +3503 -141
  227. package/dist/html/paint.js.map +1 -1
  228. package/dist/html/resources.d.ts +60 -14
  229. package/dist/html/resources.d.ts.map +1 -1
  230. package/dist/html/resources.js +122 -36
  231. package/dist/html/resources.js.map +1 -1
  232. package/dist/html/surfaces.d.ts +23 -0
  233. package/dist/html/surfaces.d.ts.map +1 -0
  234. package/dist/html/surfaces.js +68 -0
  235. package/dist/html/surfaces.js.map +1 -0
  236. package/dist/html/svg.d.ts +80 -0
  237. package/dist/html/svg.d.ts.map +1 -0
  238. package/dist/html/svg.js +435 -0
  239. package/dist/html/svg.js.map +1 -0
  240. package/dist/html/url.d.ts +23 -0
  241. package/dist/html/url.d.ts.map +1 -0
  242. package/dist/html/url.js +80 -0
  243. package/dist/html/url.js.map +1 -0
  244. package/dist/index.d.ts +2 -2
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js +1 -1
  247. package/dist/index.js.map +1 -1
  248. package/dist/internal/collate.d.ts +3 -0
  249. package/dist/internal/collate.d.ts.map +1 -0
  250. package/dist/internal/collate.js +11 -0
  251. package/dist/internal/collate.js.map +1 -0
  252. package/dist/{markdown → internal/markdown}/ast.d.ts +23 -0
  253. package/dist/internal/markdown/ast.d.ts.map +1 -0
  254. package/dist/internal/markdown/ast.js +22 -0
  255. package/dist/internal/markdown/ast.js.map +1 -0
  256. package/dist/internal/markdown/parse.d.ts.map +1 -0
  257. package/dist/{markdown → internal/markdown}/parse.js +221 -30
  258. package/dist/internal/markdown/parse.js.map +1 -0
  259. package/dist/internal/markdown/stringify.d.ts +4 -0
  260. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  261. package/dist/internal/markdown/stringify.js +684 -0
  262. package/dist/internal/markdown/stringify.js.map +1 -0
  263. package/dist/internal/markdown/tags.d.ts.map +1 -0
  264. package/dist/internal/markdown/tags.js.map +1 -0
  265. package/dist/internal/prewarm.d.ts +18 -0
  266. package/dist/internal/prewarm.d.ts.map +1 -0
  267. package/dist/internal/prewarm.js +42 -0
  268. package/dist/internal/prewarm.js.map +1 -0
  269. package/dist/internal/scroll.d.ts +3 -2
  270. package/dist/internal/scroll.d.ts.map +1 -1
  271. package/dist/internal/scroll.js +1 -0
  272. package/dist/internal/scroll.js.map +1 -1
  273. package/dist/internal/sdf.d.ts +20 -0
  274. package/dist/internal/sdf.d.ts.map +1 -0
  275. package/dist/internal/sdf.js +216 -0
  276. package/dist/internal/sdf.js.map +1 -0
  277. package/dist/internal/splice.d.ts +12 -0
  278. package/dist/internal/splice.d.ts.map +1 -0
  279. package/dist/internal/splice.js +23 -0
  280. package/dist/internal/splice.js.map +1 -0
  281. package/dist/internal/window.d.ts +12 -0
  282. package/dist/internal/window.d.ts.map +1 -1
  283. package/dist/internal/window.js +23 -6
  284. package/dist/internal/window.js.map +1 -1
  285. package/dist/maps/anchors.d.ts +103 -0
  286. package/dist/maps/anchors.d.ts.map +1 -0
  287. package/dist/maps/anchors.js +790 -0
  288. package/dist/maps/anchors.js.map +1 -0
  289. package/dist/maps/controller.d.ts +340 -0
  290. package/dist/maps/controller.d.ts.map +1 -0
  291. package/dist/maps/controller.js +822 -0
  292. package/dist/maps/controller.js.map +1 -0
  293. package/dist/maps/gl/buckets.d.ts +99 -0
  294. package/dist/maps/gl/buckets.d.ts.map +1 -0
  295. package/dist/maps/gl/buckets.js +365 -0
  296. package/dist/maps/gl/buckets.js.map +1 -0
  297. package/dist/maps/gl/build-worker.d.ts +15 -0
  298. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  299. package/dist/maps/gl/build-worker.js +44 -0
  300. package/dist/maps/gl/build-worker.js.map +1 -0
  301. package/dist/maps/gl/color.d.ts +8 -0
  302. package/dist/maps/gl/color.d.ts.map +1 -0
  303. package/dist/maps/gl/color.js +57 -0
  304. package/dist/maps/gl/color.js.map +1 -0
  305. package/dist/maps/gl/cover.d.ts +70 -0
  306. package/dist/maps/gl/cover.d.ts.map +1 -0
  307. package/dist/maps/gl/cover.js +137 -0
  308. package/dist/maps/gl/cover.js.map +1 -0
  309. package/dist/maps/gl/index.d.ts +3 -0
  310. package/dist/maps/gl/index.d.ts.map +1 -0
  311. package/dist/maps/gl/index.js +20 -0
  312. package/dist/maps/gl/index.js.map +1 -0
  313. package/dist/maps/gl/markers.d.ts +28 -0
  314. package/dist/maps/gl/markers.d.ts.map +1 -0
  315. package/dist/maps/gl/markers.js +60 -0
  316. package/dist/maps/gl/markers.js.map +1 -0
  317. package/dist/maps/gl/overlays.d.ts +68 -0
  318. package/dist/maps/gl/overlays.d.ts.map +1 -0
  319. package/dist/maps/gl/overlays.js +300 -0
  320. package/dist/maps/gl/overlays.js.map +1 -0
  321. package/dist/maps/gl/pane.d.ts +12 -0
  322. package/dist/maps/gl/pane.d.ts.map +1 -0
  323. package/dist/maps/gl/pane.js +37 -0
  324. package/dist/maps/gl/pane.js.map +1 -0
  325. package/dist/maps/gl/placement.d.ts +124 -0
  326. package/dist/maps/gl/placement.d.ts.map +1 -0
  327. package/dist/maps/gl/placement.js +642 -0
  328. package/dist/maps/gl/placement.js.map +1 -0
  329. package/dist/maps/gl/renderer.d.ts +384 -0
  330. package/dist/maps/gl/renderer.d.ts.map +1 -0
  331. package/dist/maps/gl/renderer.js +1184 -0
  332. package/dist/maps/gl/renderer.js.map +1 -0
  333. package/dist/maps/gl/shaders.d.ts +121 -0
  334. package/dist/maps/gl/shaders.d.ts.map +1 -0
  335. package/dist/maps/gl/shaders.js +453 -0
  336. package/dist/maps/gl/shaders.js.map +1 -0
  337. package/dist/maps/gl/store.d.ts +125 -0
  338. package/dist/maps/gl/store.d.ts.map +1 -0
  339. package/dist/maps/gl/store.js +512 -0
  340. package/dist/maps/gl/store.js.map +1 -0
  341. package/dist/maps/gl/text.d.ts +283 -0
  342. package/dist/maps/gl/text.d.ts.map +1 -0
  343. package/dist/maps/gl/text.js +698 -0
  344. package/dist/maps/gl/text.js.map +1 -0
  345. package/dist/maps/gl/view.d.ts +345 -0
  346. package/dist/maps/gl/view.d.ts.map +1 -0
  347. package/dist/maps/gl/view.js +1209 -0
  348. package/dist/maps/gl/view.js.map +1 -0
  349. package/dist/maps/icons.d.ts +32 -0
  350. package/dist/maps/icons.d.ts.map +1 -0
  351. package/dist/maps/icons.js +151 -0
  352. package/dist/maps/icons.js.map +1 -0
  353. package/dist/maps/index.d.ts +3 -1
  354. package/dist/maps/index.d.ts.map +1 -1
  355. package/dist/maps/index.js +218 -52
  356. package/dist/maps/index.js.map +1 -1
  357. package/dist/maps/labels.d.ts +91 -18
  358. package/dist/maps/labels.d.ts.map +1 -1
  359. package/dist/maps/labels.js +376 -140
  360. package/dist/maps/labels.js.map +1 -1
  361. package/dist/maps/node.d.ts +179 -56
  362. package/dist/maps/node.d.ts.map +1 -1
  363. package/dist/maps/node.js +663 -537
  364. package/dist/maps/node.js.map +1 -1
  365. package/dist/maps/overlay.d.ts +72 -9
  366. package/dist/maps/overlay.d.ts.map +1 -1
  367. package/dist/maps/overlay.js +108 -28
  368. package/dist/maps/overlay.js.map +1 -1
  369. package/dist/maps/paint.d.ts +3 -0
  370. package/dist/maps/paint.d.ts.map +1 -1
  371. package/dist/maps/paint.js.map +1 -1
  372. package/dist/maps/proj.d.ts +62 -0
  373. package/dist/maps/proj.d.ts.map +1 -1
  374. package/dist/maps/proj.js +101 -0
  375. package/dist/maps/proj.js.map +1 -1
  376. package/dist/maps/renderer.d.ts +69 -0
  377. package/dist/maps/renderer.d.ts.map +1 -0
  378. package/dist/maps/renderer.js +66 -0
  379. package/dist/maps/renderer.js.map +1 -0
  380. package/dist/maps/sources.d.ts +30 -6
  381. package/dist/maps/sources.d.ts.map +1 -1
  382. package/dist/maps/sources.js +22 -3
  383. package/dist/maps/sources.js.map +1 -1
  384. package/dist/maps/style.d.ts +35 -0
  385. package/dist/maps/style.d.ts.map +1 -1
  386. package/dist/maps/style.js.map +1 -1
  387. package/dist/maps/styles.d.ts +12 -0
  388. package/dist/maps/styles.d.ts.map +1 -1
  389. package/dist/maps/styles.js +120 -6
  390. package/dist/maps/styles.js.map +1 -1
  391. package/dist/maps/theme.d.ts +19 -0
  392. package/dist/maps/theme.d.ts.map +1 -0
  393. package/dist/maps/theme.js +44 -0
  394. package/dist/maps/theme.js.map +1 -0
  395. package/dist/maps/tiles.d.ts +147 -27
  396. package/dist/maps/tiles.d.ts.map +1 -1
  397. package/dist/maps/tiles.js +221 -54
  398. package/dist/maps/tiles.js.map +1 -1
  399. package/dist/maps/types.d.ts +213 -21
  400. package/dist/maps/types.d.ts.map +1 -1
  401. package/dist/markdown/expressions.d.ts +1 -1
  402. package/dist/markdown/expressions.d.ts.map +1 -1
  403. package/dist/markdown/expressions.js.map +1 -1
  404. package/dist/markdown/index.d.ts +7 -6
  405. package/dist/markdown/index.d.ts.map +1 -1
  406. package/dist/markdown/index.js +121 -50
  407. package/dist/markdown/index.js.map +1 -1
  408. package/dist/markdown/spans.d.ts +1 -1
  409. package/dist/markdown/spans.d.ts.map +1 -1
  410. package/dist/markdown/spans.js +6 -0
  411. package/dist/markdown/spans.js.map +1 -1
  412. package/dist/media-player/index.d.ts +11 -3
  413. package/dist/media-player/index.d.ts.map +1 -1
  414. package/dist/media-player/index.js +13 -2
  415. package/dist/media-player/index.js.map +1 -1
  416. package/dist/qml/containers.d.ts.map +1 -1
  417. package/dist/qml/containers.js +6 -1
  418. package/dist/qml/containers.js.map +1 -1
  419. package/dist/qml/interaction.d.ts.map +1 -1
  420. package/dist/qml/interaction.js +38 -5
  421. package/dist/qml/interaction.js.map +1 -1
  422. package/dist/qml/layouts.d.ts.map +1 -1
  423. package/dist/qml/layouts.js +21 -6
  424. package/dist/qml/layouts.js.map +1 -1
  425. package/dist/qml/qtquick.d.ts.map +1 -1
  426. package/dist/qml/qtquick.js +9 -2
  427. package/dist/qml/qtquick.js.map +1 -1
  428. package/dist/qml/react.d.ts.map +1 -1
  429. package/dist/qml/react.js +9 -7
  430. package/dist/qml/react.js.map +1 -1
  431. package/dist/reorder/clock.d.ts +7 -0
  432. package/dist/reorder/clock.d.ts.map +1 -0
  433. package/dist/reorder/clock.js +26 -0
  434. package/dist/reorder/clock.js.map +1 -0
  435. package/dist/reorder/index.d.ts +25 -3
  436. package/dist/reorder/index.d.ts.map +1 -1
  437. package/dist/reorder/index.js +64 -19
  438. package/dist/reorder/index.js.map +1 -1
  439. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  440. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  441. package/dist/rich-text-editor/clipboard.js +307 -0
  442. package/dist/rich-text-editor/clipboard.js.map +1 -0
  443. package/dist/rich-text-editor/collab.d.ts +32 -0
  444. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  445. package/dist/rich-text-editor/collab.js +55 -0
  446. package/dist/rich-text-editor/collab.js.map +1 -0
  447. package/dist/rich-text-editor/commands.d.ts +50 -0
  448. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  449. package/dist/rich-text-editor/commands.js +376 -0
  450. package/dist/rich-text-editor/commands.js.map +1 -0
  451. package/dist/rich-text-editor/drag.d.ts +68 -0
  452. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  453. package/dist/rich-text-editor/drag.js +148 -0
  454. package/dist/rich-text-editor/drag.js.map +1 -0
  455. package/dist/rich-text-editor/html.d.ts +145 -0
  456. package/dist/rich-text-editor/html.d.ts.map +1 -0
  457. package/dist/rich-text-editor/html.js +516 -0
  458. package/dist/rich-text-editor/html.js.map +1 -0
  459. package/dist/rich-text-editor/index.d.ts +252 -0
  460. package/dist/rich-text-editor/index.d.ts.map +1 -0
  461. package/dist/rich-text-editor/index.js +856 -0
  462. package/dist/rich-text-editor/index.js.map +1 -0
  463. package/dist/rich-text-editor/inline.d.ts +94 -0
  464. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  465. package/dist/rich-text-editor/inline.js +252 -0
  466. package/dist/rich-text-editor/inline.js.map +1 -0
  467. package/dist/rich-text-editor/keymap.d.ts +71 -0
  468. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  469. package/dist/rich-text-editor/keymap.js +394 -0
  470. package/dist/rich-text-editor/keymap.js.map +1 -0
  471. package/dist/rich-text-editor/keys.d.ts +40 -0
  472. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  473. package/dist/rich-text-editor/keys.js +202 -0
  474. package/dist/rich-text-editor/keys.js.map +1 -0
  475. package/dist/rich-text-editor/look.d.ts +71 -0
  476. package/dist/rich-text-editor/look.d.ts.map +1 -0
  477. package/dist/rich-text-editor/look.js +342 -0
  478. package/dist/rich-text-editor/look.js.map +1 -0
  479. package/dist/rich-text-editor/markdown.d.ts +18 -0
  480. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  481. package/dist/rich-text-editor/markdown.js +496 -0
  482. package/dist/rich-text-editor/markdown.js.map +1 -0
  483. package/dist/rich-text-editor/marks.d.ts +9 -0
  484. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  485. package/dist/rich-text-editor/marks.js +83 -0
  486. package/dist/rich-text-editor/marks.js.map +1 -0
  487. package/dist/rich-text-editor/nodes.d.ts +135 -0
  488. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  489. package/dist/rich-text-editor/nodes.js +397 -0
  490. package/dist/rich-text-editor/nodes.js.map +1 -0
  491. package/dist/rich-text-editor/render.d.ts +68 -0
  492. package/dist/rich-text-editor/render.d.ts.map +1 -0
  493. package/dist/rich-text-editor/render.js +518 -0
  494. package/dist/rich-text-editor/render.js.map +1 -0
  495. package/dist/rich-text-editor/schema.d.ts +16 -0
  496. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  497. package/dist/rich-text-editor/schema.js +386 -0
  498. package/dist/rich-text-editor/schema.js.map +1 -0
  499. package/dist/rich-text-editor/store.d.ts +11 -0
  500. package/dist/rich-text-editor/store.d.ts.map +1 -0
  501. package/dist/rich-text-editor/store.js +43 -0
  502. package/dist/rich-text-editor/store.js.map +1 -0
  503. package/dist/rich-text-editor/suggest.d.ts +127 -0
  504. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  505. package/dist/rich-text-editor/suggest.js +0 -0
  506. package/dist/rich-text-editor/suggest.js.map +1 -0
  507. package/dist/rich-text-editor/tables.d.ts +41 -0
  508. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  509. package/dist/rich-text-editor/tables.js +204 -0
  510. package/dist/rich-text-editor/tables.js.map +1 -0
  511. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  512. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  513. package/dist/rich-text-editor/toolbar.js +548 -0
  514. package/dist/rich-text-editor/toolbar.js.map +1 -0
  515. package/dist/rich-text-editor/view.d.ts +368 -0
  516. package/dist/rich-text-editor/view.d.ts.map +1 -0
  517. package/dist/rich-text-editor/view.js +1968 -0
  518. package/dist/rich-text-editor/view.js.map +1 -0
  519. package/dist/rich-text-editor/virtual.d.ts +54 -0
  520. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  521. package/dist/rich-text-editor/virtual.js +279 -0
  522. package/dist/rich-text-editor/virtual.js.map +1 -0
  523. package/dist/richtext/node.d.ts +71 -4
  524. package/dist/richtext/node.d.ts.map +1 -1
  525. package/dist/richtext/node.js +233 -28
  526. package/dist/richtext/node.js.map +1 -1
  527. package/dist/richtext/runs.d.ts +13 -8
  528. package/dist/richtext/runs.d.ts.map +1 -1
  529. package/dist/richtext/runs.js +18 -8
  530. package/dist/richtext/runs.js.map +1 -1
  531. package/dist/table/index.d.ts.map +1 -1
  532. package/dist/table/index.js +23 -0
  533. package/dist/table/index.js.map +1 -1
  534. package/dist/table/rows.d.ts +3 -2
  535. package/dist/table/rows.d.ts.map +1 -1
  536. package/dist/table/rows.js +20 -6
  537. package/dist/table/rows.js.map +1 -1
  538. package/dist/terminal/backends.d.ts +4 -1
  539. package/dist/terminal/backends.d.ts.map +1 -1
  540. package/dist/terminal/backends.js.map +1 -1
  541. package/dist/terminal/index.d.ts +18 -7
  542. package/dist/terminal/index.d.ts.map +1 -1
  543. package/dist/terminal/index.js +32 -11
  544. package/dist/terminal/index.js.map +1 -1
  545. package/dist/terminal/vt/pty.d.ts +13 -2
  546. package/dist/terminal/vt/pty.d.ts.map +1 -1
  547. package/dist/terminal/vt/pty.js +18 -3
  548. package/dist/terminal/vt/pty.js.map +1 -1
  549. package/dist/terminal-output/index.d.ts.map +1 -1
  550. package/dist/terminal-output/index.js +6 -1
  551. package/dist/terminal-output/index.js.map +1 -1
  552. package/dist/three/canvas.d.ts.map +1 -1
  553. package/dist/three/canvas.js +85 -7
  554. package/dist/three/canvas.js.map +1 -1
  555. package/dist/three/events.d.ts +22 -4
  556. package/dist/three/events.d.ts.map +1 -1
  557. package/dist/three/events.js +34 -4
  558. package/dist/three/events.js.map +1 -1
  559. package/dist/tree/index.d.ts.map +1 -1
  560. package/dist/tree/index.js +17 -0
  561. package/dist/tree/index.js.map +1 -1
  562. package/package.json +59 -31
  563. package/src/charts/node.ts +68 -28
  564. package/src/charts/render.ts +120 -30
  565. package/src/code/index.ts +3 -1
  566. package/src/code-editor/completion.ts +3 -2
  567. package/src/code-editor/index.ts +19 -0
  568. package/src/code-editor/node.ts +1574 -184
  569. package/src/code-language/languages/glsl.ts +12 -2
  570. package/src/code-language/languages/javascript.ts +21 -4
  571. package/src/code-language/languages/json.ts +12 -2
  572. package/src/code-language/languages/shell.ts +12 -2
  573. package/src/code-language/languages/sql.ts +22 -2
  574. package/src/code-language/stream.ts +257 -17
  575. package/src/code-language/types.ts +5 -4
  576. package/src/codeblock/index.ts +20 -3
  577. package/src/embed/client.ts +102 -8
  578. package/src/embed/index.ts +2 -0
  579. package/src/flow/draw.ts +167 -41
  580. package/src/flow/gl/color.ts +85 -0
  581. package/src/flow/gl/index.ts +342 -0
  582. package/src/flow/gl/pack.ts +585 -0
  583. package/src/flow/gl/renderer.ts +679 -0
  584. package/src/flow/gl/shaders.ts +371 -0
  585. package/src/flow/gl/text.ts +1295 -0
  586. package/src/flow/index.ts +794 -67
  587. package/src/flow/node.ts +2496 -1256
  588. package/src/flow/paint.ts +511 -0
  589. package/src/flow/paths.ts +80 -15
  590. package/src/flow/scene.ts +2199 -0
  591. package/src/flow/types.ts +113 -1
  592. package/src/html/controls.ts +73 -2
  593. package/src/html/css/calc.ts +385 -0
  594. package/src/html/css/cascade.ts +1403 -62
  595. package/src/html/css/color.ts +755 -0
  596. package/src/html/css/content.ts +417 -0
  597. package/src/html/css/counter-styles.ts +973 -0
  598. package/src/html/css/decode.ts +130 -0
  599. package/src/html/css/parse.ts +1514 -160
  600. package/src/html/css/style.ts +4102 -222
  601. package/src/html/css/ua.ts +58 -12
  602. package/src/html/css/values.ts +150 -50
  603. package/src/html/css/vars.ts +348 -0
  604. package/src/html/dom.ts +284 -17
  605. package/src/html/fonts.ts +641 -0
  606. package/src/html/gif.ts +233 -0
  607. package/src/html/index.ts +180 -20
  608. package/src/html/layout/block.ts +3456 -230
  609. package/src/html/layout/boxes.ts +2100 -190
  610. package/src/html/layout/cache.ts +289 -0
  611. package/src/html/layout/collapse.ts +317 -0
  612. package/src/html/layout/css-grid.ts +1063 -0
  613. package/src/html/layout/flex.ts +1115 -106
  614. package/src/html/layout/floats.ts +128 -14
  615. package/src/html/layout/grid-lines.ts +141 -0
  616. package/src/html/layout/grid.ts +181 -0
  617. package/src/html/layout/inline.ts +3943 -192
  618. package/src/html/layout/shaping.ts +140 -0
  619. package/src/html/layout/table.ts +696 -186
  620. package/src/html/layout/tracks.ts +437 -0
  621. package/src/html/node.ts +1486 -121
  622. package/src/html/paint.ts +4293 -169
  623. package/src/html/resources.ts +161 -50
  624. package/src/html/surfaces.ts +81 -0
  625. package/src/html/svg.ts +493 -0
  626. package/src/html/url.ts +86 -0
  627. package/src/index.ts +4 -0
  628. package/src/internal/collate.ts +12 -0
  629. package/src/{markdown → internal/markdown}/ast.ts +35 -7
  630. package/src/{markdown → internal/markdown}/parse.ts +288 -35
  631. package/src/internal/markdown/stringify.ts +772 -0
  632. package/src/internal/prewarm.ts +45 -0
  633. package/src/internal/scroll.ts +5 -3
  634. package/src/internal/sdf.ts +227 -0
  635. package/src/internal/splice.ts +25 -0
  636. package/src/internal/window.ts +36 -5
  637. package/src/maps/anchors.ts +913 -0
  638. package/src/maps/controller.ts +1084 -0
  639. package/src/maps/gl/buckets.ts +461 -0
  640. package/src/maps/gl/build-worker.ts +66 -0
  641. package/src/maps/gl/color.ts +62 -0
  642. package/src/maps/gl/cover.ts +205 -0
  643. package/src/maps/gl/index.ts +23 -0
  644. package/src/maps/gl/markers.ts +83 -0
  645. package/src/maps/gl/overlays.ts +379 -0
  646. package/src/maps/gl/pane.ts +42 -0
  647. package/src/maps/gl/placement.ts +968 -0
  648. package/src/maps/gl/renderer.ts +1660 -0
  649. package/src/maps/gl/shaders.ts +489 -0
  650. package/src/maps/gl/store.ts +615 -0
  651. package/src/maps/gl/text.ts +868 -0
  652. package/src/maps/gl/view.ts +1489 -0
  653. package/src/maps/icons.ts +183 -0
  654. package/src/maps/index.ts +274 -81
  655. package/src/maps/labels.ts +457 -144
  656. package/src/maps/node.ts +832 -665
  657. package/src/maps/overlay.ts +148 -43
  658. package/src/maps/paint.ts +3 -0
  659. package/src/maps/proj.ts +118 -0
  660. package/src/maps/renderer.ts +155 -0
  661. package/src/maps/sources.ts +43 -6
  662. package/src/maps/style.ts +32 -0
  663. package/src/maps/styles.ts +215 -4
  664. package/src/maps/theme.ts +51 -0
  665. package/src/maps/tiles.ts +253 -62
  666. package/src/maps/types.ts +213 -21
  667. package/src/markdown/expressions.ts +5 -1
  668. package/src/markdown/index.ts +163 -78
  669. package/src/markdown/spans.ts +6 -1
  670. package/src/media-player/index.ts +18 -4
  671. package/src/qml/containers.tsx +6 -1
  672. package/src/qml/interaction.tsx +42 -5
  673. package/src/qml/layouts.tsx +21 -6
  674. package/src/qml/qtquick.tsx +9 -2
  675. package/src/qml/react.tsx +7 -5
  676. package/src/reorder/clock.ts +27 -0
  677. package/src/reorder/index.ts +100 -27
  678. package/src/rich-text-editor/clipboard.ts +439 -0
  679. package/src/rich-text-editor/collab.ts +91 -0
  680. package/src/rich-text-editor/commands.ts +448 -0
  681. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  682. package/src/rich-text-editor/drag.ts +211 -0
  683. package/src/rich-text-editor/html.ts +619 -0
  684. package/src/rich-text-editor/index.ts +1364 -0
  685. package/src/rich-text-editor/inline.ts +345 -0
  686. package/src/rich-text-editor/keymap.ts +510 -0
  687. package/src/rich-text-editor/keys.ts +228 -0
  688. package/src/rich-text-editor/look.ts +427 -0
  689. package/src/rich-text-editor/markdown.ts +653 -0
  690. package/src/rich-text-editor/marks.ts +101 -0
  691. package/src/rich-text-editor/nodes.ts +497 -0
  692. package/src/rich-text-editor/render.ts +837 -0
  693. package/src/rich-text-editor/schema.ts +431 -0
  694. package/src/rich-text-editor/store.ts +39 -0
  695. package/src/rich-text-editor/suggest.ts +597 -0
  696. package/src/rich-text-editor/tables.ts +272 -0
  697. package/src/rich-text-editor/toolbar.ts +724 -0
  698. package/src/rich-text-editor/view.ts +2287 -0
  699. package/src/rich-text-editor/virtual.ts +372 -0
  700. package/src/richtext/node.ts +294 -44
  701. package/src/richtext/runs.ts +23 -12
  702. package/src/table/index.ts +29 -0
  703. package/src/table/rows.ts +18 -8
  704. package/src/terminal/backends.ts +4 -1
  705. package/src/terminal/index.ts +47 -15
  706. package/src/terminal/vt/pty.ts +16 -2
  707. package/src/terminal-output/index.ts +6 -1
  708. package/src/three/canvas.ts +102 -8
  709. package/src/three/events.ts +49 -9
  710. package/src/tree/index.ts +19 -0
  711. package/dist/markdown/ast.d.ts.map +0 -1
  712. package/dist/markdown/ast.js +0 -17
  713. package/dist/markdown/ast.js.map +0 -1
  714. package/dist/markdown/parse.d.ts.map +0 -1
  715. package/dist/markdown/parse.js.map +0 -1
  716. package/dist/markdown/tags.d.ts.map +0 -1
  717. package/dist/markdown/tags.js.map +0 -1
  718. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  719. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  720. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  721. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -10,21 +10,84 @@
10
10
  // Float coordinates are absolute for the same reason, which is what lets the
11
11
  // inline pass ask "how wide is the line at this y" without knowing whose
12
12
  // formatting context it is inside.
13
- import { AUTO, isPct, resolve, resolveOrNull } from '../css/values.js';
13
+ import {
14
+ AUTO,
15
+ gapOf,
16
+ isPct,
17
+ isTransparent,
18
+ resolve,
19
+ resolveOrNull,
20
+ } from '../css/values.js';
14
21
  import type { Len } from '../css/values.js';
15
- import { Box } from './boxes.js';
16
- import type { BoxTree, LineBox } from './boxes.js';
22
+ import type {
23
+ BorderStyle,
24
+ ComputedStyle,
25
+ ContentSize,
26
+ GridLine,
27
+ } from '../css/style.js';
28
+ import {
29
+ CONTAIN_INLINE_SIZE,
30
+ CONTAIN_LAYOUT,
31
+ CONTAIN_PAINT,
32
+ CONTAIN_SIZE,
33
+ scrolls,
34
+ } from '../css/style.js';
35
+ import { lineOf, spanToName } from './grid-lines.js';
36
+ import type { GridLines } from './grid-lines.js';
37
+ import {
38
+ BOX_RAISES,
39
+ Box,
40
+ CLAMPED,
41
+ CUT_BLOCKS,
42
+ GRID_TRACKS,
43
+ FIRST_LINE,
44
+ INLINE_OFFSETS,
45
+ LINE_BOX_RAISES,
46
+ SHIFTED_LINES,
47
+ TEXT_SHIFTS,
48
+ isBlank,
49
+ } from './boxes.js';
50
+ import type {
51
+ BoxTree,
52
+ FirstLineStyler,
53
+ Intrinsic,
54
+ LineBox,
55
+ MarginStrut,
56
+ Marker,
57
+ } from './boxes.js';
17
58
  import { FloatContext } from './floats.js';
18
- import { layoutInline } from './inline.js';
19
- import type { FontsLike } from './inline.js';
59
+ import {
60
+ faceExtentOf,
61
+ layoutInline,
62
+ lineHeightMultiplier,
63
+ spaceBreaksOnly,
64
+ strutOf,
65
+ widestWord,
66
+ } from './inline.js';
67
+ import type { FontsLike, InlineOptions, InlineResult } from './inline.js';
68
+ import type { TextRun } from '../../richtext/index.js';
20
69
  import { layoutFlex } from './flex.js';
21
- import { layoutTable } from './table.js';
22
- import { computePaintBounds } from '../paint.js';
70
+ import { finishCaptions, layoutTable } from './table.js';
71
+ import { collapseEdges } from './collapse.js';
72
+ import { computePaintBounds, hoistNegative, stackLayers } from '../paint.js';
23
73
 
24
74
  export interface LayoutContext {
25
75
  fonts: FontsLike | null;
76
+ /** Whether any block has a `::first-line` (`BoxTree.firstLine`). */
77
+ firstLine?: boolean;
78
+ /** Each box's style on a first line (`BoxTree.firstLineStyler`). */
79
+ firstLineStyler?: FirstLineStyler | null;
80
+ /** Whether a float or an out-of-flow box is in an inline box
81
+ * (`BoxTree.nestedOutOfLine`). */
82
+ nestedOutOfLine?: boolean;
83
+ /** Whether any box paints its background through its text
84
+ * (`BoxTree.clipText`). */
85
+ clipText?: boolean;
26
86
  viewportWidth: number;
27
87
  viewportHeight: number;
88
+ /** Set once a box is placed against the initial containing block, whose
89
+ * height is the viewport's (`LayoutResult.readsViewportHeight`). */
90
+ readViewportHeight?: boolean;
28
91
  /** Out-of-flow boxes, collected in flow order and laid out afterwards —
29
92
  * an absolutely positioned box may be positioned against an ancestor
30
93
  * whose size is not known until its in-flow content has been laid out. */
@@ -41,11 +104,48 @@ export interface LayoutContext {
41
104
  * exception would be.
42
105
  */
43
106
  layoutSubtree(box: Box, width: number): void;
107
+ /** How many flex boxes' Yoga passes are running, one inside the last's
108
+ * measure (`layoutFlex`). */
109
+ flexDepth?: number;
110
+ /** The clamp of the line-clamp container whose formatting context is
111
+ * being laid out, if any (`layoutInternals`). */
112
+ clamp?: Clamp | null;
113
+ }
114
+
115
+ /**
116
+ * A line-clamp container's clamp, carried through its formatting context as
117
+ * the lines in it are laid out (CSS Overflow 4, 5.2 and 5.3.1). Only the
118
+ * lines of that formatting context count: a box that makes one of its own
119
+ * is laid out with none, and counts as a block.
120
+ */
121
+ interface Clamp {
122
+ /** The line-clamp container. */
123
+ box: Box;
124
+ /** How many more lines show before the clamp point. */
125
+ left: number;
126
+ /** Whether the clamp point is passed: every box after it in the flow is
127
+ * invisible, and takes no room (`CLAMPED`). */
128
+ done: boolean;
129
+ /**
130
+ * For `line-clamp: auto`, whose clamp point is after as many lines as
131
+ * the box's height holds: the first pass lays the content out whole and
132
+ * counts the lines that end above `floor` — the box's content bottom, in
133
+ * document coordinates — with what closes below each. `over` once one
134
+ * does not; null where the lines are cut rather than counted.
135
+ */
136
+ fit: { floor: number; lines: number; over: boolean } | null;
44
137
  }
45
138
 
46
139
  export interface LayoutResult {
47
140
  width: number;
48
141
  height: number;
142
+ /**
143
+ * Whether the layout read the viewport's height: a percentage height on
144
+ * the root element, or a box positioned against the initial containing
145
+ * block. A document that did lays out differently in a viewport of
146
+ * another height, and one that did not — most — lays out the same.
147
+ */
148
+ readsViewportHeight: boolean;
49
149
  }
50
150
 
51
151
  /** Lay the whole document out at a width. */
@@ -61,6 +161,10 @@ export function layoutDocument(
61
161
  viewportHeight,
62
162
  positioned: [],
63
163
  layoutSubtree: (box, width) => layoutSubtree(box, ctx, width),
164
+ firstLine: tree.firstLine,
165
+ firstLineStyler: tree.firstLineStyler,
166
+ nestedOutOfLine: tree.nestedOutOfLine,
167
+ clipText: tree.clipText,
64
168
  };
65
169
  const root = tree.root;
66
170
  root.x = 0;
@@ -68,11 +172,15 @@ export function layoutDocument(
68
172
  root.width = viewportWidth;
69
173
  // The root box stands in for `<body>` when the document has none (see
70
174
  // `Cascade.rootStyle`), so it may carry a margin — and a margin on the
71
- // initial containing block has nothing to collapse against and nothing to
72
- // sit inside. Folding it into the padding is what a browser's body margin
73
- // amounts to, and it keeps `contentX` the one thing layout reads.
175
+ // initial containing block has nothing to sit inside. Folded into the
176
+ // padding at the sides and the bottom, which keeps `contentX` the one
177
+ // thing layout reads. Not at the top: a body's top margin collapses with
178
+ // its first block's, `<html>` being the formatting context's root and
179
+ // `<body>` not, so it is handed to the flow as the margin already pending
180
+ // there — or `<p>hi</p>` would stand 8px lower than the same paragraph in
181
+ // `<body>`.
74
182
  resolveEdges(root, viewportWidth);
75
- root.padTop += root.marginTop;
183
+ const leading = root.marginTop;
76
184
  root.padRight += root.marginRight;
77
185
  root.padBottom += root.marginBottom;
78
186
  root.padLeft += root.marginLeft;
@@ -83,7 +191,31 @@ export function layoutDocument(
83
191
 
84
192
  const contentWidth = Math.max(0, viewportWidth - root.horizontalExtra);
85
193
  const floats = new FloatContext(root.contentX, root.contentX + contentWidth);
86
- const flow = layoutChildren(root, ctx, floats, root.contentY, contentWidth);
194
+ // the initial containing block is the viewport's size, which a percentage
195
+ // height on the root element resolves against (CSS 2.1 10.1, 10.5); a
196
+ // fragment has no root element, and the box standing in for its body has
197
+ // the body's `auto` height to give
198
+ let readsViewportHeight = false;
199
+ for (const child of root.children) {
200
+ if (child.el?.name !== 'html') continue;
201
+ child.percentHeightBase = viewportHeight;
202
+ const { height, minHeight, maxHeight } = child.style;
203
+ if (
204
+ isPct(height) ||
205
+ isPct(minHeight) ||
206
+ (maxHeight !== 'none' && isPct(maxHeight))
207
+ ) {
208
+ readsViewportHeight = true;
209
+ }
210
+ }
211
+ const flow = layoutChildren(
212
+ root,
213
+ ctx,
214
+ floats,
215
+ root.contentY,
216
+ contentWidth,
217
+ leading,
218
+ );
87
219
  const floatBottom =
88
220
  floats.bottom === -Infinity ? 0 : floats.bottom - root.contentY;
89
221
  root.height = Math.max(flow.height, floatBottom) + root.verticalExtra;
@@ -99,32 +231,442 @@ export function layoutDocument(
99
231
  // against where it *was* — which is the whole of what makes it relative —
100
232
  // and the ink bounds are computed after that, so culling sees where boxes
101
233
  // ended up rather than where they were laid out.
102
- applyRelativeOffsets(root);
103
- computePaintBounds(root);
234
+ if (tree.relative) applyRelativeOffsets(root);
235
+ const reach = computePaintBounds(root, tree.movedInline);
236
+ if (tree.negative) hoistNegative(root);
237
+ stackLayers(root);
104
238
 
105
- let bottom = root.height;
239
+ // The document is as tall as what overflows the root, not the root: an
240
+ // `html, body { height: 100% }` a window tall holds a message longer than
241
+ // the window, and the element sizes to all of it.
242
+ let bottom = Math.max(root.height, reach);
106
243
  for (const { box } of ctx.positioned) {
107
244
  if (box.style.position !== 'fixed')
108
245
  bottom = Math.max(bottom, box.y + box.height);
109
246
  }
110
- return { width: viewportWidth, height: bottom };
247
+ return {
248
+ width: viewportWidth,
249
+ height: bottom,
250
+ readsViewportHeight: readsViewportHeight || ctx.readViewportHeight === true,
251
+ };
252
+ }
253
+
254
+ /** No margin at all, shared: most joins leave a strut as it was. */
255
+ const NO_MARGIN: MarginStrut = Object.freeze({ pos: 0, neg: 0 });
256
+
257
+ /** One margin, as a strut to collapse others with. */
258
+ function marginStrut(margin: number): MarginStrut {
259
+ if (margin > 0) return { pos: margin, neg: 0 };
260
+ if (margin < 0) return { pos: 0, neg: margin };
261
+ return NO_MARGIN;
262
+ }
263
+
264
+ /**
265
+ * A strut with one more margin collapsed into it: the largest positive
266
+ * and the most negative stay apart, and add up only where the margin is
267
+ * read (`marginOf`) — CSS 8.3.1's rule, whole. A `-8px` pull still works
268
+ * against a `40px` push and comes out at `32`, and a third margin after
269
+ * them collapses with the two, not with their sum.
270
+ */
271
+ function join(strut: MarginStrut, margin: number): MarginStrut {
272
+ if (margin > strut.pos) return { pos: margin, neg: strut.neg };
273
+ if (margin < strut.neg) return { pos: strut.pos, neg: margin };
274
+ return strut;
275
+ }
276
+
277
+ /** Two sets of adjoining margins collapsed into one. */
278
+ function joinStruts(a: MarginStrut, b: MarginStrut): MarginStrut {
279
+ if (b.pos <= a.pos && b.neg >= a.neg) return a;
280
+ if (a.pos <= b.pos && a.neg >= b.neg) return b;
281
+ return { pos: Math.max(a.pos, b.pos), neg: Math.min(a.neg, b.neg) };
282
+ }
283
+
284
+ /** The margin a strut comes to. */
285
+ function marginOf(strut: MarginStrut): number {
286
+ return strut.pos + strut.neg;
287
+ }
288
+
289
+ /** A box's bottom margin as the next sibling's collapses with it, with
290
+ * what came out through its bottom edge where anything did. */
291
+ function bottomOf(box: Box): MarginStrut {
292
+ return box.bottomStrut ?? marginStrut(box.marginBottom);
293
+ }
294
+
295
+ /**
296
+ * A block's top margin as its placement uses it: its own, collapsed with
297
+ * every margin that adjoins it (CSS 2.1 8.3.1). That is its first in-flow
298
+ * child's while nothing parts them — no top border, no top padding, no
299
+ * formatting context of its own, no line of text first — and that child's
300
+ * first child's, down the chain; and past a child that is empty, its bottom
301
+ * margin and the next child's top, because an empty block's two margins
302
+ * adjoin each other. The children the chain went through are marked, so
303
+ * their own layout does not apply the margin a second time. Without it
304
+ * `<div><p>` stood a paragraph's margin lower than `<p>`, and a `<div>`
305
+ * holding only an absolute image stood the rest of a document a body's
306
+ * margin lower than a browser does.
307
+ */
308
+ function collapsedTopMargin(
309
+ box: Box,
310
+ containingWidth: number,
311
+ floats: FloatContext | null = null,
312
+ y = 0,
313
+ pending: MarginStrut = NO_MARGIN,
314
+ ): MarginStrut {
315
+ floatsPassed = false;
316
+ floatsLeft = 0;
317
+ floatsRight = 0;
318
+ passedLeft = false;
319
+ passedRight = false;
320
+ walkFloats = floats;
321
+ walkY = y;
322
+ walkPending = pending;
323
+ return absorbChildren(box, containingWidth, marginStrut(box.marginTop));
324
+ }
325
+
326
+ /** Whether the last `absorbChildren` went through every child in flow of
327
+ * its box, each empty and absorbed whole — which is what lets the caller
328
+ * take that box's bottom margin too. Read straight after the call. */
329
+ let throughAll = false;
330
+ /** Whether the walk has passed a float, and how wide the floats it passed
331
+ * on each side are, their margin boxes as their styles state them — NaN
332
+ * once one does not. Such a float is placed where the margin ends, so it
333
+ * moves with the margin, and a new formatting context the margin reaches
334
+ * next has to be able to sit beside it. */
335
+ let floatsPassed = false;
336
+ let floatsLeft = 0;
337
+ let floatsRight = 0;
338
+ /** Which sides the floats the walk passed are on. */
339
+ let passedLeft = false;
340
+ let passedRight = false;
341
+ /** Where the margin the walk collapses starts, and the floats placed before
342
+ * it: what says whether a cleared child in it has clearance. */
343
+ let walkFloats: FloatContext | null = null;
344
+ let walkY = 0;
345
+ let walkPending: MarginStrut = NO_MARGIN;
346
+
347
+ /**
348
+ * Collapse into `margin` the margins of `at`'s children that adjoin its top
349
+ * edge, marking each child it takes, and return it. The walk stops at the
350
+ * first child something parts from the margin — a line, a clearance, a
351
+ * border, padding or height, a formatting context of its own — and counts
352
+ * a child empty only where its layout is sure to agree, since this runs
353
+ * before anything is laid out and the parent is placed by what it answers:
354
+ * an inline box with an edge, or a percentage, stops it where it might have
355
+ * gone on.
356
+ */
357
+ function absorbChildren(
358
+ at: Box,
359
+ width: number,
360
+ margin: MarginStrut,
361
+ ): MarginStrut {
362
+ let whole = true;
363
+ /** `at`'s content width, worked out when a child first needs it: most
364
+ * blocks start with text, and never do. */
365
+ let inner = -1;
366
+ for (const child of at.children) {
367
+ if (child.outOfFlow) continue;
368
+ if (child.kind === 'text') {
369
+ if (makesNoLine(child)) continue;
370
+ // a line box stops a margin
371
+ whole = false;
372
+ break;
373
+ }
374
+ const floated = child.isFloat;
375
+ const inLine =
376
+ !floated &&
377
+ (child.kind === 'inline' ||
378
+ child.kind === 'break' ||
379
+ isInlineLevel(child));
380
+ if (inLine) {
381
+ const made = phantom(child);
382
+ if (made === 0) {
383
+ whole = false;
384
+ break;
385
+ }
386
+ if (made === 1) continue;
387
+ }
388
+ if (inner < 0) {
389
+ if (!topOpen(at)) {
390
+ whole = false;
391
+ break;
392
+ }
393
+ inner = Math.max(0, blockWidth(at, width) - at.horizontalExtra);
394
+ }
395
+ if (floated) {
396
+ passFloat(child, inner);
397
+ continue;
398
+ }
399
+ if (inLine) {
400
+ passFloatsIn(child, inner);
401
+ continue;
402
+ }
403
+ resolveEdges(child, inner);
404
+ /** The margin that collapses up through the child's top edge, where
405
+ * it had to be worked out before the child was taken. */
406
+ let through: MarginStrut | null = null;
407
+ if (child.style.clear !== 'none') {
408
+ // Where it would be with `clear: none` is where its top margin and
409
+ // every one that comes up through its top edge put it: a large
410
+ // margin inside it takes it past the floats as surely as its own
411
+ // (CSS 2.1 9.5.2). The walk into it is undone where it has
412
+ // clearance, which it passes no float for.
413
+ const passed = passedFloats(child.style.clear);
414
+ const walked = passed ? null : saveWalk();
415
+ if (!passed) {
416
+ through = absorbChildren(child, inner, join(margin, child.marginTop));
417
+ }
418
+ if (passed || hasClearance(child, through!)) {
419
+ if (walked) restoreWalk(walked);
420
+ child.topAbsorbed = 0;
421
+ whole = false;
422
+ break;
423
+ }
424
+ }
425
+ if (
426
+ floatsPassed &&
427
+ (child.kind === 'replaced' || establishesBFC(child)) &&
428
+ !fitsBeside(child, inner)
429
+ ) {
430
+ // it would be pushed below the floats the margin carries, so it
431
+ // separates from them as clearance does: the margin stops above it,
432
+ // and it sits under the floats rather than a margin under them
433
+ child.topAbsorbed = 3;
434
+ whole = false;
435
+ break;
436
+ }
437
+ margin =
438
+ through ?? absorbChildren(child, inner, join(margin, child.marginTop));
439
+ if (!throughAll || !bottomOpen(child)) {
440
+ child.topAbsorbed = 1;
441
+ whole = false;
442
+ break;
443
+ }
444
+ child.topAbsorbed = 2;
445
+ margin = join(margin, child.marginBottom);
446
+ }
447
+ throughAll = whole && (inner >= 0 || topOpen(at));
448
+ return margin;
449
+ }
450
+
451
+ /** Whether a box's top margin adjoins its first child's: a block, with no
452
+ * border or padding at the top and no formatting context of its own. */
453
+ function topOpen(box: Box): boolean {
454
+ return (
455
+ box.kind === 'block' &&
456
+ box.borderTop === 0 &&
457
+ box.padTop === 0 &&
458
+ !establishesBFC(box)
459
+ );
460
+ }
461
+
462
+ /** Whether nothing at an empty block's bottom holds its margins apart: no
463
+ * border, padding or height there — nor a height its ratio gives it from
464
+ * its width — and no marker to stand a line tall. A percentage says no,
465
+ * whatever it resolves to. */
466
+ function bottomOpen(box: Box): boolean {
467
+ const { height, minHeight, aspectRatio } = box.style;
468
+ return (
469
+ box.borderBottom === 0 &&
470
+ box.padBottom === 0 &&
471
+ !box.marker &&
472
+ (height === 0 ||
473
+ (height === AUTO &&
474
+ !aspectRatio &&
475
+ box.style.heightKeyword !== 'stretch')) &&
476
+ (minHeight === AUTO || minHeight === 0) &&
477
+ box.style.minHeightKeyword !== 'stretch'
478
+ );
479
+ }
480
+
481
+ /** Whether a text makes no line of its own: white space that collapses
482
+ * away. A no-break space is not white space to CSS, though `\s` says it
483
+ * is, and white space that is kept makes a line. */
484
+ function makesNoLine(text: Box): boolean {
485
+ const ws = text.style.whiteSpace;
486
+ return (ws === 'normal' || ws === 'nowrap') && COLLAPSIBLE.test(text.text);
487
+ }
488
+ const COLLAPSIBLE = /^[ \t\n\r\f]*$/;
489
+
490
+ /** Whether an inline box makes no line either — nothing in it that does,
491
+ * and no margin, border or padding on any side: the phantom line of CSS
492
+ * 2.1 9.4.2, which a margin collapses through. `0` where it makes one,
493
+ * `1` where it does not, and `2` where it does not and holds a float, which
494
+ * the walk has to pass. The children go first: most inline boxes hold text,
495
+ * and that answers without a look at their edges. */
496
+ function phantom(box: Box): 0 | 1 | 2 {
497
+ if (box.kind !== 'inline' || box.style.display !== 'inline') return 0;
498
+ let made: 1 | 2 = 1;
499
+ for (const child of box.children) {
500
+ if (child.outOfFlow) continue;
501
+ if (child.isFloat) {
502
+ made = 2;
503
+ continue;
504
+ }
505
+ if (child.kind === 'text') {
506
+ if (makesNoLine(child)) continue;
507
+ return 0;
508
+ }
509
+ const inner = phantom(child);
510
+ if (inner === 0) return 0;
511
+ if (inner === 2) made = 2;
512
+ }
513
+ const style = box.style;
514
+ return noLength(style.marginLeft) &&
515
+ noLength(style.marginRight) &&
516
+ noLength(style.marginTop) &&
517
+ noLength(style.marginBottom) &&
518
+ noLength(style.paddingLeft) &&
519
+ noLength(style.paddingRight) &&
520
+ noLength(style.paddingTop) &&
521
+ noLength(style.paddingBottom) &&
522
+ !hasBorder(style.borderLeftStyle, style.borderLeftWidth) &&
523
+ !hasBorder(style.borderRightStyle, style.borderRightWidth) &&
524
+ !hasBorder(style.borderTopStyle, style.borderTopWidth) &&
525
+ !hasBorder(style.borderBottomStyle, style.borderBottomWidth)
526
+ ? made
527
+ : 0;
528
+ }
529
+
530
+ /** Pass the floats in a phantom inline box, at any depth. */
531
+ function passFloatsIn(box: Box, width: number): void {
532
+ for (const child of box.children) {
533
+ if (child.isFloat) passFloat(child, width);
534
+ else if (child.kind === 'inline') passFloatsIn(child, width);
535
+ }
536
+ }
537
+
538
+ function noLength(len: Len): boolean {
539
+ return len === 0 || len === AUTO;
540
+ }
541
+
542
+ function hasBorder(style: BorderStyle, width: number): boolean {
543
+ return style !== 'none' && style !== 'hidden' && width !== 0;
544
+ }
545
+
546
+ /** Count a float the walk passes toward the room a new formatting context
547
+ * after it would have beside it. */
548
+ function passFloat(float: Box, width: number): void {
549
+ resolveEdges(float, width);
550
+ floatsPassed = true;
551
+ if (float.style.float === 'right') {
552
+ floatsRight += statedOuterWidth(float, width);
553
+ passedRight = true;
554
+ } else {
555
+ floatsLeft += statedOuterWidth(float, width);
556
+ passedLeft = true;
557
+ }
558
+ }
559
+
560
+ /** Whether the walk passed a float that a `clear` clears, which the margin
561
+ * carries down with it: a child that clears one has clearance whatever
562
+ * its margin is. */
563
+ function passedFloats(clear: Box['style']['clear']): boolean {
564
+ return (
565
+ ((clear === 'left' || clear === 'both') && passedLeft) ||
566
+ ((clear === 'right' || clear === 'both') && passedRight)
567
+ );
568
+ }
569
+
570
+ /**
571
+ * Whether a cleared child in the margin the walk collapses has clearance
572
+ * from a float placed before the walk, which parts it from that margin
573
+ * (CSS 2.1 9.5.2): where `through`, the margin that collapses up through
574
+ * its top edge, does not take it below the float already. A child with none
575
+ * is like any other, and the margin goes on up through it: stopped there, a
576
+ * large margin under a float was spent inside its parent, whose background
577
+ * showed above it.
578
+ */
579
+ function hasClearance(child: Box, through: MarginStrut): boolean {
580
+ if (!walkFloats) return true;
581
+ const floor = walkFloats.clearance(child.style.clear);
582
+ if (floor === -Infinity) return false;
583
+ return walkY + marginOf(joinStruts(walkPending, through)) < floor;
584
+ }
585
+
586
+ /** What the walk has passed, to be put back where a look ahead into a
587
+ * child is undone. */
588
+ function saveWalk(): [boolean, number, number, boolean, boolean] {
589
+ return [floatsPassed, floatsLeft, floatsRight, passedLeft, passedRight];
590
+ }
591
+
592
+ function restoreWalk([passed, left, right, onLeft, onRight]: ReturnType<
593
+ typeof saveWalk
594
+ >): void {
595
+ floatsPassed = passed;
596
+ floatsLeft = left;
597
+ floatsRight = right;
598
+ passedLeft = onLeft;
599
+ passedRight = onRight;
600
+ }
601
+
602
+ /** Whether a new formatting context fits beside the floats the walk passed
603
+ * (CSS 2.1 9.5), as `layoutBesideFloats` places it: where its width and
604
+ * theirs are stated, whether its border box, set in from its margins or
605
+ * the floats, ends before them or the room does; where the floats' are
606
+ * not, yes, and a box of `width: auto` there shrinks to the room it has. */
607
+ function fitsBeside(box: Box, width: number): boolean {
608
+ const stated =
609
+ statedOuterWidth(box, width) - box.marginLeft - box.marginRight;
610
+ const lo = floatsLeft ? floatsLeft : -Infinity;
611
+ const hi = floatsRight ? width - floatsRight : Infinity;
612
+ const [start, end] = besideFloats(box, 0, width, lo, hi);
613
+ const across = Number.isNaN(stated)
614
+ ? Math.max(box.horizontalExtra, end - start)
615
+ : stated;
616
+ return box.parent?.style.direction === 'rtl'
617
+ ? !(end - across < Math.max(Math.min(0, box.marginLeft), lo) - 0.01)
618
+ : !(
619
+ start + across >
620
+ Math.min(width - Math.min(0, box.marginRight), hi) + 0.01
621
+ );
622
+ }
623
+
624
+ /** A box's margin-box width as its style states it, before it is laid out:
625
+ * NaN for `width: auto`, which only layout can answer. */
626
+ function statedOuterWidth(box: Box, containingWidth: number): number {
627
+ const { width, boxSizing } = box.style;
628
+ if (width === AUTO) return NaN;
629
+ const set = resolve(width, containingWidth, NaN);
630
+ const border =
631
+ boxSizing === 'border-box'
632
+ ? Math.max(set, box.horizontalExtra)
633
+ : set + box.horizontalExtra;
634
+ return border + box.marginLeft + box.marginRight;
111
635
  }
112
636
 
113
637
  /**
114
- * The collapsed value of two adjoining margins: the largest positive plus
115
- * the most negative — CSS 8.3.1's rule, whole. Both-positive takes the max,
116
- * both-negative the min, and a mixed pair genuinely adds, which is what
117
- * makes a `-8px` pull work against a `40px` push and come out at `32`.
638
+ * Whether a box's top and bottom margins collapse through it: a block with
639
+ * no height, no border or padding, no line in it and no formatting context
640
+ * of its own, whose blocks are all the same (CSS 2.1 8.3.1). An empty
641
+ * `<div>` between two paragraphs is the case: its margins and theirs are
642
+ * one margin, where they were stacked.
118
643
  */
119
- function collapseMargins(a: number, b: number): number {
120
- return Math.max(0, Math.max(a, b)) + Math.min(0, Math.min(a, b));
644
+ function collapsesThrough(box: Box): boolean {
645
+ if (box.kind !== 'block' || box.height !== 0) return false;
646
+ if (box.lines?.length || establishesBFC(box)) return false;
647
+ const min = resolveOrNull(box.style.minHeight, box.percentHeightBase);
648
+ if (min !== null && min > 0) return false;
649
+ for (const child of box.children) {
650
+ if (child.outOfFlow || child.isFloat) continue;
651
+ // what is in line made a line or did not, and the lines said which
652
+ if (
653
+ child.kind === 'text' ||
654
+ child.kind === 'inline' ||
655
+ child.kind === 'break' ||
656
+ isInlineLevel(child)
657
+ ) {
658
+ continue;
659
+ }
660
+ if (!collapsesThrough(child)) return false;
661
+ }
662
+ return true;
121
663
  }
122
664
 
123
665
  /** What a box's children came to: their height, and the margin still hanging
124
666
  * past the last of them when the box's own bottom edge does not stop it. */
125
667
  interface FlowResult {
126
668
  height: number;
127
- hanging: number;
669
+ hanging: MarginStrut;
128
670
  }
129
671
 
130
672
  /**
@@ -137,28 +679,78 @@ function layoutChildren(
137
679
  floats: FloatContext,
138
680
  contentTop: number,
139
681
  contentWidth: number,
682
+ leading = 0,
140
683
  ): FlowResult {
141
684
  const contentLeft = box.contentX;
142
685
  if (establishesInlineContext(box)) {
686
+ // a line box stops a margin: the leading one is spent above the lines
143
687
  const height = layoutInlineContent(
144
688
  box,
145
689
  ctx,
146
690
  floats,
147
- contentTop,
691
+ contentTop + leading,
148
692
  contentWidth,
149
693
  contentLeft,
150
694
  );
151
- return { height, hanging: 0 };
695
+ return { height: height + leading, hanging: NO_MARGIN };
152
696
  }
153
697
 
154
698
  let y = contentTop;
155
- /** The margin left hanging by the previous sibling, for collapsing. */
156
- let pendingMargin = 0;
699
+ /** The margin left hanging by the previous sibling — or, before the first
700
+ * child, the one `leading` hands down — for collapsing. */
701
+ let pendingMargin = marginStrut(leading);
702
+ /** How many floats were placed before this box's content began, while
703
+ * nothing has yet fixed where the content is: a margin that collapses
704
+ * up through its top edge would take the ones placed since down with
705
+ * it (`clearanceSince`). Infinity once a child in flow has fixed it,
706
+ * and from the start where a border, padding or a formatting context
707
+ * of its own does. */
708
+ let openFloats = topOpen(box) ? floats.count : Infinity;
157
709
  let first = true;
710
+ /** Whether every child in flow so far was absorbed whole into this box's
711
+ * top margin: the walk that marked the next child reached it only
712
+ * through them, so past any other its mark is an earlier pass's. Asked
713
+ * of the box only once a child carries a mark, which few do. */
714
+ let open: boolean | null = null;
715
+ /** Whether the margin still hanging collapsed with a top margin that has
716
+ * clearance, an empty block cleared past a float's, and the margin that
717
+ * makes: the block's own two and what the empty blocks after it add to
718
+ * them. It stays in this box rather than escape its bottom (CSS 2.1
719
+ * 8.3.1, 10.6.3). */
720
+ let cleared = false;
721
+ let afterClear = NO_MARGIN;
722
+ /** The cleared block's own top margin: its top border edge is that far
723
+ * inside the margin its margins and the ones after it collapse to. */
724
+ let clearTop = 0;
725
+ // this box's first formatted line is its first child's in flow (CSS 2.1
726
+ // 5.12.1), which its `::first-line` is handed to
727
+ let firstLine = ctx.firstLine ? firstLineOf(box) : null;
728
+ const clamp = ctx.clamp ?? null;
158
729
 
159
730
  for (const child of box.children) {
160
- if (child.kind === 'text' && !child.text.trim()) continue;
731
+ if (child.kind === 'text' && isBlank(child.text)) continue;
732
+ // Past a line-clamp container's clamp point, a box in flow or floating
733
+ // is invisible and takes no room (CSS Overflow 4, 5.3.1); a positioned
734
+ // box keeps its place, invisible only where its containing block is.
735
+ if (clamp !== null && !child.outOfFlow) {
736
+ if (clamp.done) {
737
+ hideClamped(child);
738
+ continue;
739
+ }
740
+ CLAMPED.delete(child);
741
+ }
742
+ if (firstLine && !child.outOfFlow && !child.isFloat) {
743
+ if (!FIRST_LINE.has(child)) HANDED.set(child, firstLine);
744
+ firstLine = null;
745
+ }
161
746
  if (child.outOfFlow) {
747
+ placeStatic(
748
+ child,
749
+ box,
750
+ contentLeft,
751
+ contentWidth,
752
+ y + marginOf(pendingMargin),
753
+ );
162
754
  ctx.positioned.push({
163
755
  box: child,
164
756
  containing: containingBlockFor(child) ?? box,
@@ -166,21 +758,109 @@ function layoutChildren(
166
758
  continue;
167
759
  }
168
760
  if (child.isFloat) {
169
- layoutFloat(child, ctx, floats, y + pendingMargin, contentWidth);
761
+ layoutFloat(
762
+ child,
763
+ ctx,
764
+ floats,
765
+ y + marginOf(pendingMargin),
766
+ contentLeft,
767
+ contentWidth,
768
+ );
170
769
  continue;
171
770
  }
172
771
 
173
772
  resolveEdges(child, contentWidth);
174
- const top = child.marginTop;
175
- const collapsed = first ? top : collapseMargins(pendingMargin, top);
176
- let childY = y + collapsed;
177
- const clearance = floats.clearance(child.style.clear);
178
- if (clearance > -Infinity && clearance > childY) childY = clearance;
773
+ // A child whose margin this box already spent — collapsed through its
774
+ // top edge — sits at the content top; any other brings its own margin,
775
+ // collapsed with its first descendants' where nothing parts them.
776
+ let absorbed = child.topAbsorbed;
777
+ if (absorbed !== 0 && !(open ??= topOpen(box))) absorbed = 0;
778
+ const top = absorbed
779
+ ? NO_MARGIN
780
+ : collapsedTopMargin(child, contentWidth, floats, y, pendingMargin);
781
+ const collapsed = joinStruts(pendingMargin, top);
782
+ // After an empty block cleared past a float, what collapses with its
783
+ // margins is placed from its top border edge, less its own top
784
+ // margin, which is above that edge: the depth the margin they make
785
+ // reaches below it (CSS 2.1 8.3.1), as the box's bottom is below it
786
+ // where the block is the last in it
787
+ const at: number = cleared
788
+ ? y + Math.max(0, marginOf(joinStruts(afterClear, top)) - clearTop)
789
+ : y + marginOf(collapsed);
790
+ let childY = at;
791
+ const clear = child.style.clear;
792
+ const clearance = floats.clearance(clear);
793
+ if (clearance > -Infinity) {
794
+ // A float placed before anything fixed where this box's content is
795
+ // goes down with the margin where `clear` is none, so the child is
796
+ // never below it there: it has clearance, and its border edge goes
797
+ // under the floats whatever its margin is, which may take it back
798
+ // up (CSS 2.1 9.5.2, as the browsers read it). A margin under a
799
+ // float in an empty block put a block that cleared it the margin
800
+ // below the float, and its parent's background showed between.
801
+ // And one the walk that placed this box reached through every child
802
+ // before it, and stopped at for its clearance, has it too: where
803
+ // it would have been with `clear: none`, its margin collapsed up
804
+ // through this box's top, is above the floats, so its border edge
805
+ // goes under them, and its own margin, held apart there, may not
806
+ // take it lower. The clearance is negative then.
807
+ if (
808
+ (openFloats !== Infinity &&
809
+ floats.clearanceSince(clear, openFloats) > -Infinity) ||
810
+ (child.topAbsorbed === 0 && (open ?? topOpen(box)))
811
+ ) {
812
+ childY = clearance;
813
+ } else if (clearance > childY) childY = clearance;
814
+ }
179
815
 
180
- layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
181
- y = child.y + child.height;
182
- pendingMargin = child.marginBottom;
816
+ const counted = clamp?.fit?.lines ?? 0;
817
+ if (
818
+ !floats.isEmpty &&
819
+ (child.kind === 'replaced' || establishesBFC(child))
820
+ ) {
821
+ layoutBesideFloats(child, ctx, floats, contentLeft, childY, contentWidth);
822
+ } else {
823
+ layoutBlockLevel(child, ctx, floats, contentLeft, childY, contentWidth);
824
+ }
825
+ // a block with none of the flow's lines in it — an image, a box of a
826
+ // formatting context of its own — puts the clamp point of an `auto`
827
+ // clamp before it where it ends below the room
828
+ const fit = clamp?.fit;
829
+ if (fit && !fit.over && fit.lines === counted) {
830
+ const bottom =
831
+ child.y +
832
+ child.height -
833
+ child.padBottom -
834
+ child.borderBottom +
835
+ closingBelow(child, clamp!.box);
836
+ if (bottom > fit.floor) fit.over = true;
837
+ }
183
838
  first = false;
839
+ const moved = childY !== at;
840
+ if (!moved && collapsesThrough(child)) {
841
+ // nothing in it parts its margins: they and the ones either side of
842
+ // it are one (CSS 2.1 8.3.1), still hanging for what comes next —
843
+ // or, where the walk took them all into this box's top margin, spent
844
+ if (absorbed === 2) {
845
+ pendingMargin = collapsed;
846
+ continue;
847
+ }
848
+ open = false;
849
+ pendingMargin = joinStruts(collapsed, bottomOf(child));
850
+ if (cleared) {
851
+ afterClear = joinStruts(joinStruts(afterClear, top), bottomOf(child));
852
+ }
853
+ continue;
854
+ }
855
+ open = false;
856
+ y = child.y + child.height;
857
+ pendingMargin = bottomOf(child);
858
+ openFloats = Infinity;
859
+ // cleared, and empty: its margins collapse together, and what follows
860
+ // collapses with them, a top margin that has clearance (CSS 2.1 8.3.1)
861
+ cleared = moved && collapsesThrough(child);
862
+ afterClear = cleared ? joinStruts(top, bottomOf(child)) : NO_MARGIN;
863
+ clearTop = cleared ? marginOf(top) : 0;
184
864
  }
185
865
 
186
866
  // The last child's bottom margin collapses through the parent's bottom
@@ -192,14 +872,159 @@ function layoutChildren(
192
872
  // `<div><p>…</p></div><p>…</p>` set the two paragraphs solid.
193
873
  if (
194
874
  !first &&
875
+ !cleared &&
195
876
  !box.borderBottom &&
196
877
  !box.padBottom &&
197
878
  box.style.height === AUTO &&
198
879
  !establishesBFC(box)
199
880
  ) {
200
- return { height: y - contentTop, hanging: pendingMargin };
881
+ const height = y - contentTop;
882
+ // A minimum height that makes the box taller parts the margin from its
883
+ // bottom, and spends it: the margin neither escapes nor makes the box
884
+ // taller still. 8.3.1 has it collapse only through a box of no
885
+ // `min-height`; browsers part it only where the minimum is what sets
886
+ // the height, and the suite has it so. A maximum leaves it to collapse
887
+ // through, as 8.3.1 has it, where the suite has a test either way
888
+ const parted =
889
+ box.style.minHeight !== AUTO &&
890
+ clampHeight(box, height + box.verticalExtra) > height + box.verticalExtra;
891
+ return { height, hanging: parted ? NO_MARGIN : pendingMargin };
892
+ }
893
+ // The cleared block's top border edge is where it would be with a border
894
+ // at its bottom, its top margin's depth inside the margin they all make:
895
+ // the box ends where that margin does, below the edge by what exceeds
896
+ // the top margin (8.3.1, 10.6.3)
897
+ if (cleared) {
898
+ return {
899
+ height: y + Math.max(0, marginOf(afterClear) - clearTop) - contentTop,
900
+ hanging: NO_MARGIN,
901
+ };
902
+ }
903
+ return {
904
+ height: y + marginOf(pendingMargin) - contentTop,
905
+ hanging: NO_MARGIN,
906
+ };
907
+ }
908
+
909
+ /**
910
+ * A block that makes its own formatting context — a table, a box that
911
+ * clips its overflow — or a block-level image does not flow round the
912
+ * floats beside it: it is a rectangle beside them (CSS 2.1 9.5), in the
913
+ * room they leave at its top, or lower down where it does not fit there.
914
+ * An image floated left with an `overflow: hidden` block of text beside it
915
+ * is the layout this is for; the block ran under the image.
916
+ */
917
+ function layoutBesideFloats(
918
+ box: Box,
919
+ ctx: LayoutContext,
920
+ floats: FloatContext,
921
+ contentLeft: number,
922
+ y: number,
923
+ contentWidth: number,
924
+ ): void {
925
+ const right = contentLeft + contentWidth;
926
+ const style = box.style;
927
+ // auto margins, and HTML's alignment, place the box in the room the
928
+ // floats leave, as browsers do
929
+ const inRoom =
930
+ style.marginLeft === AUTO ||
931
+ style.marginRight === AUTO ||
932
+ !!box.parent?.style.alignBlocks;
933
+ const rtl = box.parent?.style.direction === 'rtl';
934
+ // Otherwise its margins are its containing block's, and may overlap a
935
+ // float on their own side; its border box may not. So a column beside a
936
+ // 200px sidebar with a margin of 220px starts 220px in, not 420px. A
937
+ // float is looked for as far out as a negative margin takes the box
938
+ resolveEdges(box, contentWidth);
939
+ const reachLeft = inRoom
940
+ ? contentLeft
941
+ : Math.min(contentLeft, contentLeft + box.marginLeft);
942
+ const reachRight = inRoom ? right : Math.max(right, right - box.marginRight);
943
+ let at = y;
944
+ for (let tries = 0; tries < 64; tries += 1) {
945
+ let band = floats.bandAt(at, 1, reachLeft, reachRight, !inRoom);
946
+ let fits = false;
947
+ // The room is what the floats leave over the box's whole height, not at
948
+ // its top: a float that starts lower down, beside the box, narrows it
949
+ // too (CSS 2.1 9.5). Known only once the box is laid out, so a box
950
+ // that runs into one is laid out again in what is left.
951
+ for (let settle = 0; settle < 8; settle += 1) {
952
+ const room = band.right - band.left;
953
+ const lo = band.left > reachLeft ? band.left : -Infinity;
954
+ const hi = band.right < reachRight ? band.right : Infinity;
955
+ if (inRoom) layoutBlockLevel(box, ctx, floats, band.left, at, room);
956
+ else {
957
+ const [start, end] = besideFloats(box, contentLeft, right, lo, hi);
958
+ layoutBlockLevel(
959
+ box,
960
+ ctx,
961
+ floats,
962
+ start - box.marginLeft,
963
+ at,
964
+ end - start + box.marginLeft + box.marginRight,
965
+ contentWidth,
966
+ );
967
+ }
968
+ // its border box, from its top: its margins may overlap a float, and
969
+ // a negative top margin takes the box up past one it then meets
970
+ const over = floats.bandAt(
971
+ at,
972
+ box.height,
973
+ reachLeft,
974
+ reachRight,
975
+ !inRoom,
976
+ );
977
+ if (over.left > band.left + 0.5 || over.right < band.right - 0.5) {
978
+ band = over;
979
+ continue;
980
+ }
981
+ // What is too wide overflows at the end of the line, as far as a
982
+ // negative margin there takes it, but no further into a float or
983
+ // past the room; at the start it would run into the floats, and
984
+ // waits below them (the suite's floats-wrap-bfc-with-margin tests)
985
+ if (inRoom) {
986
+ fits = box.marginLeft + box.width + box.marginRight <= room + 0.5;
987
+ } else if (rtl) {
988
+ const limit = Math.max(contentLeft + Math.min(0, box.marginLeft), lo);
989
+ fits = box.x >= limit - 0.5;
990
+ } else {
991
+ const limit = Math.min(right - Math.min(0, box.marginRight), hi);
992
+ fits = box.x + box.width <= limit + 0.5;
993
+ }
994
+ break;
995
+ }
996
+ if (fits) return;
997
+ // under the float it meets, which may start below its top
998
+ const below = floats.nextEdgeBelow(at, Math.max(1, box.height));
999
+ if (below === null || below <= at) break;
1000
+ at = below;
201
1001
  }
202
- return { height: y + pendingMargin - contentTop, hanging: 0 };
1002
+ layoutBlockLevel(box, ctx, floats, contentLeft, at, contentWidth);
1003
+ }
1004
+
1005
+ /**
1006
+ * The left and right edges a block with a formatting context of its own
1007
+ * has for its border box beside floats that reach `lo` from the left and
1008
+ * `hi` from the right (infinite where none does): each margin set in from
1009
+ * its containing block's edge, or the float's where that is further in.
1010
+ */
1011
+ function besideFloats(
1012
+ box: Box,
1013
+ left: number,
1014
+ right: number,
1015
+ lo: number,
1016
+ hi: number,
1017
+ ): [number, number] {
1018
+ // Where a float narrows the containing block's room on either side, a
1019
+ // negative margin takes the box no further out than the containing
1020
+ // block's edge, as every engine has it; where none does, the margins
1021
+ // are the containing block's in full. The suite's
1022
+ // floats-wrap-bfc-with-margin-006 to -009 propose otherwise, and pass in
1023
+ // no browser.
1024
+ const beside = lo > left || hi < right;
1025
+ const ml = beside ? Math.max(0, box.marginLeft) : box.marginLeft;
1026
+ const mr = beside ? Math.max(0, box.marginRight) : box.marginRight;
1027
+ return [Math.max(left + ml, lo), Math.min(right - mr, hi)];
203
1028
  }
204
1029
 
205
1030
  function layoutInlineContent(
@@ -210,28 +1035,102 @@ function layoutInlineContent(
210
1035
  contentWidth: number,
211
1036
  contentLeft: number,
212
1037
  ): number {
213
- // Floats inside an inline context are placed before the lines are built,
214
- // so the lines already know to avoid them.
215
- for (const child of box.children) {
216
- if (child.outOfFlow) {
217
- ctx.positioned.push({
218
- box: child,
219
- containing: containingBlockFor(child) ?? box,
220
- });
221
- } else if (child.isFloat) {
222
- layoutFloat(child, ctx, floats, contentTop, contentWidth);
223
- }
1038
+ // a line-clamp container whose clamp point is at its start shows none of
1039
+ // its own lines
1040
+ const clamp = ctx.clamp ?? null;
1041
+ if (clamp !== null && (clamp.done || clamp.left <= 0)) {
1042
+ box.lines = [];
1043
+ box.floatRow = 0;
1044
+ return 0;
224
1045
  }
1046
+ // An absolute box's static position is the block's; a float is placed
1047
+ // on the line it is on, as the lines reach it (`floatBoxes`).
1048
+ const floated = placeOutOfLine(
1049
+ box,
1050
+ box,
1051
+ ctx,
1052
+ contentTop,
1053
+ contentWidth,
1054
+ contentLeft,
1055
+ );
225
1056
  // Atomics have to be sized before the line breaker can place them.
226
1057
  sizeAtomics(box, ctx, contentWidth);
227
1058
 
228
- const result = layoutInline(box, {
1059
+ const options = {
229
1060
  fonts: ctx.fonts,
230
1061
  width: contentWidth,
231
1062
  startY: contentTop,
232
1063
  floats,
233
1064
  originX: contentLeft,
234
- });
1065
+ clipText: ctx.clipText,
1066
+ floatBoxes: floated
1067
+ ? {
1068
+ size: (child: Box) => sizeFloat(child, ctx, contentWidth),
1069
+ place: (child: Box, y: number) =>
1070
+ placeFloat(child, floats, y, contentLeft, contentWidth),
1071
+ }
1072
+ : undefined,
1073
+ // the lines the clamp leaves, where it cuts rather than counts
1074
+ clamp:
1075
+ clamp !== null && clamp.fit === null && clamp.left !== Infinity
1076
+ ? {
1077
+ lines: clamp.left,
1078
+ ellipsis: box.style.blockEllipsis,
1079
+ follows: followsInFlow(box, clamp.box),
1080
+ }
1081
+ : undefined,
1082
+ };
1083
+ const firstLine = ctx.firstLine ? firstLineOf(box) : null;
1084
+ // A `::first-line` that sets the line's fonts moves where it breaks, so
1085
+ // the line is found in them (`InlineOptions.firstLineStyle`); its text
1086
+ // takes the pseudo-element's colour there too
1087
+ const restyled =
1088
+ firstLine && ctx.firstLineStyler && setsFonts(firstLine, box.style)
1089
+ ? { style: firstLine, styler: ctx.firstLineStyler }
1090
+ : undefined;
1091
+ let result = layoutInline(
1092
+ box,
1093
+ restyled ? { ...options, firstLineStyle: restyled } : options,
1094
+ );
1095
+ if (
1096
+ firstLine &&
1097
+ !restyled &&
1098
+ firstLine.color !== box.style.color &&
1099
+ result.lines.length
1100
+ ) {
1101
+ // the first line's text in its `::first-line` colour, laid out again
1102
+ // with its runs cut where the line ends (CSS 2.1 5.12.1), beside the
1103
+ // floats the first pass placed — and measured with them
1104
+ const placed = result;
1105
+ result = layoutInline(box, {
1106
+ ...options,
1107
+ floatBoxes: undefined,
1108
+ firstLine: {
1109
+ color: firstLine.color,
1110
+ from: box.style.color,
1111
+ end: result.lines[0].textEnd,
1112
+ },
1113
+ });
1114
+ if (placed.lines.length === result.lines.length) {
1115
+ placed.lines.forEach((line, i) => {
1116
+ if (line.floats) result.lines[i].floats = line.floats;
1117
+ });
1118
+ }
1119
+ result.floatRow = placed.floatRow;
1120
+ }
1121
+ box.floatRow = result.floatRow ?? 0;
1122
+ if (clamp !== null) countLines(clamp, box, result, contentTop, options.clamp);
1123
+ if (
1124
+ firstLine &&
1125
+ ctx.fonts &&
1126
+ result.lines.length &&
1127
+ !isTransparent(firstLine.backgroundColor)
1128
+ ) {
1129
+ result.lines[0].background = {
1130
+ style: firstLine,
1131
+ ...faceExtentOf(ctx.fonts, firstLine),
1132
+ };
1133
+ }
235
1134
  // The inline pass works in content-box coordinates; move the result into
236
1135
  // document space now, so nothing below this line has to know the
237
1136
  // difference.
@@ -247,11 +1146,149 @@ function layoutInlineContent(
247
1146
  text.drawX += contentLeft;
248
1147
  text.drawY += contentTop;
249
1148
  }
1149
+ for (const edge of line.edges ?? []) edge.x += contentLeft;
250
1150
  }
251
1151
  box.lines = result.lines;
252
1152
  return result.height;
253
1153
  }
254
1154
 
1155
+ /**
1156
+ * A block's lines, counted against the clamp: the ones left, or for an
1157
+ * `auto` clamp's first pass, the ones that end above its floor. The clamp
1158
+ * point is passed where they were cut, or where they were all the lines
1159
+ * left and more of the container follows them.
1160
+ */
1161
+ function countLines(
1162
+ clamp: Clamp,
1163
+ box: Box,
1164
+ result: InlineResult,
1165
+ contentTop: number,
1166
+ cut: InlineOptions['clamp'],
1167
+ ): void {
1168
+ const fit = clamp.fit;
1169
+ if (fit !== null) {
1170
+ if (fit.over) return;
1171
+ const below = closingBelow(box, clamp.box);
1172
+ for (const line of result.lines) {
1173
+ if (contentTop + line.y + line.height + below > fit.floor) {
1174
+ fit.over = true;
1175
+ return;
1176
+ }
1177
+ fit.lines += 1;
1178
+ }
1179
+ return;
1180
+ }
1181
+ clamp.left -= result.lines.length;
1182
+ if (result.cut || (clamp.left <= 0 && cut?.follows)) clamp.done = true;
1183
+ }
1184
+
1185
+ /** Mark a box past a clamp point, and take from everything in it the lines
1186
+ * a layout before gave it: a hidden box is not laid out, and nothing —
1187
+ * a selection, a caret — may find text in it where it stood. */
1188
+ function hideClamped(box: Box): void {
1189
+ CLAMPED.add(box);
1190
+ const stack = [box];
1191
+ while (stack.length) {
1192
+ const at = stack.pop()!;
1193
+ at.lines = null;
1194
+ for (const child of at.children) stack.push(child);
1195
+ }
1196
+ }
1197
+
1198
+ /**
1199
+ * How far below a box's content a line-clamp container's content ends,
1200
+ * where the clamp point is just after it: the bottom padding, border and
1201
+ * margin of the box and of each block around it in the container, the
1202
+ * margins collapsed where nothing parts them.
1203
+ */
1204
+ function closingBelow(box: Box, container: Box): number {
1205
+ let below = 0;
1206
+ let margin = 0;
1207
+ for (let at: Box | null = box; at && at !== container; at = at.parent) {
1208
+ const edge = at.padBottom + at.borderBottom;
1209
+ if (edge) {
1210
+ below += margin + edge;
1211
+ margin = 0;
1212
+ }
1213
+ margin = Math.max(margin, at.marginBottom);
1214
+ }
1215
+ return below + margin;
1216
+ }
1217
+
1218
+ /**
1219
+ * Whether anything in flow follows a block in a line-clamp container — a
1220
+ * block after it, or after a block around it. Then a clamp point is just
1221
+ * after its last line, which ends in an ellipsis (CSS Overflow 4, 4.2);
1222
+ * where nothing does, the lines end where the content does.
1223
+ */
1224
+ function followsInFlow(box: Box, container: Box): boolean {
1225
+ for (let at = box; at !== container && at.parent; at = at.parent) {
1226
+ const siblings = at.parent.children;
1227
+ for (let i = siblings.indexOf(at) + 1; i < siblings.length; i += 1) {
1228
+ const next = siblings[i];
1229
+ if (next.outOfFlow || next.isFloat || makesNothing(next)) continue;
1230
+ return true;
1231
+ }
1232
+ }
1233
+ return false;
1234
+ }
1235
+
1236
+ /** Whether a box in flow makes neither a line nor a height of its own: a
1237
+ * block of phantom lines — the rest of an inline box a block broke — with
1238
+ * no edge or height to part it, which puts no clamp point after the line
1239
+ * before it. */
1240
+ function makesNothing(box: Box): boolean {
1241
+ if (box.kind === 'text') return makesNoLine(box);
1242
+ if (box.kind === 'inline') return phantom(box) !== 0;
1243
+ if (box.kind !== 'block' || establishesBFC(box)) return false;
1244
+ const style = box.style;
1245
+ if (
1246
+ style.height !== AUTO ||
1247
+ style.minHeight !== AUTO ||
1248
+ !noLength(style.paddingTop) ||
1249
+ !noLength(style.paddingBottom) ||
1250
+ hasBorder(style.borderTopStyle, style.borderTopWidth) ||
1251
+ hasBorder(style.borderBottomStyle, style.borderBottomWidth)
1252
+ ) {
1253
+ return false;
1254
+ }
1255
+ for (const child of box.children) {
1256
+ if (child.outOfFlow || child.isFloat) continue;
1257
+ if (!makesNothing(child)) return false;
1258
+ }
1259
+ return true;
1260
+ }
1261
+
1262
+ /** Whether a `::first-line` style sets its line's text otherwise than its
1263
+ * block's: in another face, size or line height, or spaced apart. */
1264
+ function setsFonts(line: ComputedStyle, block: ComputedStyle): boolean {
1265
+ return (
1266
+ line.fontFamily !== block.fontFamily ||
1267
+ line.fontSize !== block.fontSize ||
1268
+ line.fontWeight !== block.fontWeight ||
1269
+ line.fontStyle !== block.fontStyle ||
1270
+ line.lineHeight !== block.lineHeight ||
1271
+ line.lineHeightIsLength !== block.lineHeightIsLength ||
1272
+ line.letterSpacing !== block.letterSpacing ||
1273
+ line.fontVariantCaps !== block.fontVariantCaps ||
1274
+ line.fontVariantNumeric !== block.fontVariantNumeric ||
1275
+ line.fontVariantLigatures !== block.fontVariantLigatures ||
1276
+ line.fontVariantPosition !== block.fontVariantPosition ||
1277
+ line.fontKerning !== block.fontKerning ||
1278
+ line.fontFeatureSettings !== block.fontFeatureSettings
1279
+ );
1280
+ }
1281
+
1282
+ /** `::first-line` styles handed down to the first child in flow of a box
1283
+ * that has one, whose first line is the box's (`layoutChildren`). */
1284
+ const HANDED = new WeakMap<Box, ComputedStyle>();
1285
+
1286
+ /** The `::first-line` style a box's first line takes: its own, or one an
1287
+ * ancestor handed it. */
1288
+ function firstLineOf(box: Box): ComputedStyle | null {
1289
+ return FIRST_LINE.get(box) ?? HANDED.get(box) ?? null;
1290
+ }
1291
+
255
1292
  /** Size every atomic in an inline context. */
256
1293
  function sizeAtomics(box: Box, ctx: LayoutContext, contentWidth: number): void {
257
1294
  for (const child of box.children) {
@@ -278,9 +1315,123 @@ function layoutAtomic(
278
1315
  return;
279
1316
  }
280
1317
  const width = shrinkToFitWidth(box, ctx, availableWidth);
1318
+ layoutOwn(box, ctx, width);
1319
+ }
1320
+
1321
+ /**
1322
+ * Lay out a box that sizes itself — a float, an inline-block, an absolute
1323
+ * box — at its own width, where nothing outside it reaches in: or, where
1324
+ * this pass has already laid it out at that width with the same edges,
1325
+ * leave it as it was and move it back to where a layout puts it.
1326
+ *
1327
+ * Measuring a box's content lays its content out, and so a float in a
1328
+ * float was laid out twice a level — as its parent measured, and as its
1329
+ * parent was laid out at the width it measured — and a hundred of them,
1330
+ * the markup of a broken page, never finished. What a caller does to the
1331
+ * box after its layout is move it, and stretch its height, which is put
1332
+ * back; its own, and its content's, are the layout's.
1333
+ */
1334
+ function layoutOwn(box: Box, ctx: LayoutContext, width: number): void {
1335
+ const laid = LAID_OWN.get(box);
1336
+ if (
1337
+ laid !== undefined &&
1338
+ laid.ctx === ctx &&
1339
+ // and nothing, a probe of its content, has laid it out since
1340
+ laid.serial === box.layoutSerial &&
1341
+ laid.width === width &&
1342
+ // NaN where no height is a base, which `===` would never find again
1343
+ Object.is(laid.base, box.percentHeightBase) &&
1344
+ laid.padTop === box.padTop &&
1345
+ laid.padRight === box.padRight &&
1346
+ laid.padBottom === box.padBottom &&
1347
+ laid.padLeft === box.padLeft &&
1348
+ // and was put somewhere it can be moved back from: a right float in a
1349
+ // content measured at no width limit stands at an infinite x, and
1350
+ // moved back by it, came out at NaN
1351
+ Number.isFinite(box.x) &&
1352
+ Number.isFinite(box.y)
1353
+ ) {
1354
+ box.height = laid.height;
1355
+ moveTo(box, 0, 0);
1356
+ return;
1357
+ }
281
1358
  layoutInternals(box, ctx, width, 0, 0);
1359
+ LAID_OWN.set(box, {
1360
+ ctx,
1361
+ serial: box.layoutSerial,
1362
+ width,
1363
+ base: box.percentHeightBase,
1364
+ padTop: box.padTop,
1365
+ padRight: box.padRight,
1366
+ padBottom: box.padBottom,
1367
+ padLeft: box.padLeft,
1368
+ height: box.height,
1369
+ });
1370
+ }
1371
+
1372
+ /** Every box's layout counted, for `Box.layoutSerial`. */
1373
+ let layoutSerial = 0;
1374
+
1375
+ /**
1376
+ * Whether a box's layout is one this pass made at `width`, with the same
1377
+ * edges and the same height to take percentages of, and nothing has laid
1378
+ * it out since (`keep`): what a table in a table in a table is asked for
1379
+ * again at every level, and at the same width each time. Its height is put
1380
+ * back, where a caller may have stretched it; where it stands is the
1381
+ * caller's to move it to.
1382
+ */
1383
+ function kept(box: Box, ctx: LayoutContext, width: number): boolean {
1384
+ const laid = LAID_OWN.get(box);
1385
+ if (
1386
+ laid === undefined ||
1387
+ laid.ctx !== ctx ||
1388
+ laid.serial !== box.layoutSerial ||
1389
+ laid.width !== width ||
1390
+ !Object.is(laid.base, box.percentHeightBase) ||
1391
+ laid.padTop !== box.padTop ||
1392
+ laid.padRight !== box.padRight ||
1393
+ laid.padBottom !== box.padBottom ||
1394
+ laid.padLeft !== box.padLeft ||
1395
+ !Number.isFinite(box.x) ||
1396
+ !Number.isFinite(box.y)
1397
+ ) {
1398
+ return false;
1399
+ }
1400
+ box.height = laid.height;
1401
+ return true;
1402
+ }
1403
+
1404
+ /** Note the layout a box was just given, at `width`, for `kept`. */
1405
+ function keep(box: Box, ctx: LayoutContext, width: number): void {
1406
+ LAID_OWN.set(box, {
1407
+ ctx,
1408
+ serial: box.layoutSerial,
1409
+ width,
1410
+ base: box.percentHeightBase,
1411
+ padTop: box.padTop,
1412
+ padRight: box.padRight,
1413
+ padBottom: box.padBottom,
1414
+ padLeft: box.padLeft,
1415
+ height: box.height,
1416
+ });
282
1417
  }
283
1418
 
1419
+ /** What `layoutOwn` laid each box out at, and in which pass. */
1420
+ const LAID_OWN = new WeakMap<
1421
+ Box,
1422
+ {
1423
+ ctx: LayoutContext;
1424
+ serial: number;
1425
+ width: number;
1426
+ base: number;
1427
+ padTop: number;
1428
+ padRight: number;
1429
+ padBottom: number;
1430
+ padLeft: number;
1431
+ height: number;
1432
+ }
1433
+ >();
1434
+
284
1435
  function layoutBlockLevel(
285
1436
  box: Box,
286
1437
  ctx: LayoutContext,
@@ -288,39 +1439,114 @@ function layoutBlockLevel(
288
1439
  contentLeft: number,
289
1440
  y: number,
290
1441
  containingWidth: number,
1442
+ /** What percentages are of: the containing block's width, where the
1443
+ * room is narrower than it beside floats */
1444
+ percentBase = containingWidth,
291
1445
  ): void {
292
- resolveEdges(box, containingWidth);
1446
+ resolveEdges(box, percentBase);
293
1447
 
294
1448
  if (box.kind === 'replaced') {
295
- sizeReplaced(box, containingWidth);
1449
+ sizeReplaced(box, percentBase, containingWidth);
296
1450
  placeBlock(box, contentLeft, y, containingWidth);
297
1451
  return;
298
1452
  }
299
1453
 
300
- const width = blockWidth(box, containingWidth);
1454
+ const width = blockWidth(box, containingWidth, percentBase, ctx);
1455
+ if (box.kind === 'table' && kept(box, ctx, width)) {
1456
+ // laid out at this width earlier in the pass, and not since: put where
1457
+ // a layout would put it, and moved there whole
1458
+ const laid = box.width;
1459
+ const x = box.x;
1460
+ const top = box.y;
1461
+ box.width = width;
1462
+ placeBlock(box, contentLeft, y, containingWidth);
1463
+ if (laid !== width && Number.isFinite(containingWidth)) {
1464
+ box.width = laid;
1465
+ placeBlock(box, contentLeft, y, containingWidth);
1466
+ }
1467
+ box.width = laid;
1468
+ const to = { x: box.x, y: box.y };
1469
+ box.x = x;
1470
+ box.y = top;
1471
+ moveTo(box, to.x, to.y);
1472
+ return;
1473
+ }
301
1474
  box.width = width;
302
1475
  placeBlock(box, contentLeft, y, containingWidth);
303
1476
  layoutInternals(box, ctx, width, box.x, box.y, outerFloats);
1477
+ if (
1478
+ box.kind === 'table' &&
1479
+ box.width !== width &&
1480
+ Number.isFinite(containingWidth)
1481
+ ) {
1482
+ // a table shrinks to its columns once it is laid out, where it was
1483
+ // placed as wide as its room: placed again, its auto margins centre it
1484
+ // — in a room that has a width, and not in the unbounded one a
1485
+ // shrink-to-fit probe lays it out in
1486
+ const x = box.x;
1487
+ placeBlock(box, contentLeft, y, containingWidth);
1488
+ const dx = box.x - x;
1489
+ box.x = x;
1490
+ translate(box, dx, 0);
1491
+ }
1492
+ if (box.kind === 'table') keep(box, ctx, width);
304
1493
  }
305
1494
 
306
1495
  /**
307
- * Lay a box out at a width and report the widest thing it drew.
308
- *
309
- * This is what a table column asks twice — once unbounded for max-content,
310
- * once at a hair's width for min-content — and it exists rather than the
311
- * caller reading `box.width` because a `width: auto` box laid out unbounded
312
- * *is* unbounded: it fills its containing block, and its containing block was
313
- * `Infinity`. The answer has to come from what the content came to, which is
314
- * exactly what `intrinsicWidth` walks.
1496
+ * Lay a block-level box out as a block in a containing block of a width,
1497
+ * at its own `width` or at the room its margins leave, with the offset its
1498
+ * margins give it; the caller moves it into place. What a table's caption
1499
+ * is, in the box around the table.
315
1500
  */
316
- export function measureIntrinsicWidth(
1501
+ export function layoutBlockIn(
317
1502
  box: Box,
318
1503
  ctx: LayoutContext,
319
- available: number,
320
- ): number {
321
- ctx.layoutSubtree(box, available);
1504
+ containingWidth: number,
1505
+ ): void {
1506
+ resolveEdges(box, containingWidth);
1507
+ const width = blockWidth(box, containingWidth, containingWidth, ctx);
1508
+ box.width = width;
1509
+ placeBlock(box, 0, 0, containingWidth);
1510
+ layoutInternals(box, ctx, width, box.x, 0);
1511
+ }
1512
+
1513
+ /**
1514
+ * The width a min-content probe lays a box out at: none at all. Every line
1515
+ * breaks at every opportunity there, and the widest is the longest word —
1516
+ * which is what both text engines answer at zero; react-x11's CoreText
1517
+ * engine answers it at zero only, and at a pixel breaks inside words, so a
1518
+ * probe a pixel wide gave every table cell on macOS a min-content a
1519
+ * character or two wide.
1520
+ */
1521
+ export const MIN_CONTENT_PROBE = 0;
1522
+
1523
+ /**
1524
+ * Lay a box out at a width and report the widest thing it drew.
1525
+ *
1526
+ * This is what a table column asks twice — once unbounded for max-content,
1527
+ * once at a hair's width for min-content — and it exists rather than the
1528
+ * caller reading `box.width` because a `width: auto` box laid out unbounded
1529
+ * *is* unbounded: it fills its containing block, and its containing block was
1530
+ * `Infinity`. The answer has to come from what the content came to, which is
1531
+ * exactly what `intrinsicWidth` walks.
1532
+ *
1533
+ * A box with a width of its own answers with it, as its contribution to
1534
+ * whatever is sizing it — unless `content` asks for what the content
1535
+ * alone came to, which is how a table cell's is taken: its `width` is
1536
+ * weighed apart, and answered with the width the probe laid it out at,
1537
+ * a cell set to 10% measured no wider than its padding.
1538
+ */
1539
+ export function measureIntrinsicWidth(
1540
+ box: Box,
1541
+ ctx: LayoutContext,
1542
+ available: number,
1543
+ content = false,
1544
+ ): number {
1545
+ ctx.layoutSubtree(box, available);
322
1546
  const specified = box.style.width;
323
- if (specified !== AUTO && Number.isFinite(box.width)) return box.width;
1547
+ if (!content && specified !== AUTO && Number.isFinite(box.width)) {
1548
+ return box.width;
1549
+ }
324
1550
  return intrinsicWidth(box) + box.horizontalExtra;
325
1551
  }
326
1552
 
@@ -349,6 +1575,9 @@ function layoutSubtree(box: Box, ctx: LayoutContext, width: number): void {
349
1575
  * float context, passed only when this box does *not* establish one of its
350
1576
  * own — which is the difference between text flowing beside a float that
351
1577
  * started in an earlier sibling and text that starts below it.
1578
+ *
1579
+ * A line-clamp container's clamp is carried through its formatting context
1580
+ * (`Clamp`), and a box that makes one of its own is laid out outside it.
352
1581
  */
353
1582
  function layoutInternals(
354
1583
  box: Box,
@@ -358,10 +1587,85 @@ function layoutInternals(
358
1587
  y: number,
359
1588
  outerFloats?: FloatContext,
360
1589
  ): void {
1590
+ const outer = ctx.clamp ?? null;
1591
+ const lines = box.style.lineClamp;
1592
+ if (lines === null && (outer === null || !establishesBFC(box))) {
1593
+ layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
1594
+ return;
1595
+ }
1596
+ if (lines === null) {
1597
+ ctx.clamp = null;
1598
+ layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
1599
+ } else {
1600
+ layoutClamped(box, ctx, lines, borderBoxWidth, x, y, outerFloats);
1601
+ }
1602
+ ctx.clamp = outer;
1603
+ }
1604
+
1605
+ /**
1606
+ * A line-clamp container, laid out with its clamp. `line-clamp: auto`'s
1607
+ * clamp point is after the lines its height holds, where it has a height
1608
+ * of its own (CSS Overflow 4, 5.3.1): the content is laid out whole to
1609
+ * count them, and again cut after them, where they are not all of it.
1610
+ */
1611
+ function layoutClamped(
1612
+ box: Box,
1613
+ ctx: LayoutContext,
1614
+ lines: number,
1615
+ borderBoxWidth: number,
1616
+ x: number,
1617
+ y: number,
1618
+ outerFloats?: FloatContext,
1619
+ ): void {
1620
+ const floor = lines === Infinity ? autoFloor(box, y) : Infinity;
1621
+ if (floor === Infinity) {
1622
+ // counted against the number, or where `auto` has no height to fill,
1623
+ // against none: the boxes a clamp hid before show again
1624
+ ctx.clamp = { box, left: lines, done: false, fit: null };
1625
+ layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
1626
+ return;
1627
+ }
1628
+ const fit = { floor, lines: 0, over: false };
1629
+ ctx.clamp = { box, left: Infinity, done: false, fit };
1630
+ const positioned = ctx.positioned.length;
1631
+ layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
1632
+ if (!fit.over) return;
1633
+ ctx.positioned.length = positioned;
1634
+ ctx.clamp = { box, left: fit.lines, done: fit.lines === 0, fit: null };
1635
+ layoutBox(box, ctx, borderBoxWidth, x, y, outerFloats);
1636
+ }
1637
+
1638
+ /** Where an `auto` clamp's lines have to end by, in document coordinates:
1639
+ * the content bottom of a box as tall as its `height`, or `max-height`,
1640
+ * lets it be — Infinity where neither is set. */
1641
+ function autoFloor(box: Box, y: number): number {
1642
+ const set = resolveOrNull(box.style.height, box.percentHeightBase);
1643
+ const outer =
1644
+ set === null
1645
+ ? Infinity
1646
+ : box.style.boxSizing === 'border-box'
1647
+ ? Math.max(set, box.verticalExtra)
1648
+ : set + box.verticalExtra;
1649
+ const height = clampHeight(box, outer);
1650
+ return Number.isFinite(height)
1651
+ ? y + height - box.padBottom - box.borderBottom
1652
+ : Infinity;
1653
+ }
1654
+
1655
+ function layoutBox(
1656
+ box: Box,
1657
+ ctx: LayoutContext,
1658
+ borderBoxWidth: number,
1659
+ x: number,
1660
+ y: number,
1661
+ outerFloats?: FloatContext,
1662
+ ): void {
1663
+ box.layoutSerial = ++layoutSerial;
361
1664
  box.x = x;
362
1665
  box.y = y;
363
1666
  box.width = borderBoxWidth;
364
1667
  const contentWidth = box.contentWidth;
1668
+ givePercentBase(box, percentBaseInside(box));
365
1669
 
366
1670
  if (box.kind === 'flex') {
367
1671
  const height = layoutFlex(box, ctx, contentWidth);
@@ -371,6 +1675,7 @@ function layoutInternals(
371
1675
  if (box.kind === 'table') {
372
1676
  const height = layoutTable(box, ctx, contentWidth);
373
1677
  finishHeight(box, height);
1678
+ finishCaptions(box);
374
1679
  return;
375
1680
  }
376
1681
 
@@ -378,24 +1683,56 @@ function layoutInternals(
378
1683
  const floats = ownFloats
379
1684
  ? new FloatContext(box.contentX, box.contentX + contentWidth)
380
1685
  : outerFloats;
1686
+ // the line a pass before gave a marker, which this one may not
1687
+ if (box.marker) box.lines = null;
381
1688
  const flow = layoutChildren(box, ctx, floats, box.contentY, contentWidth);
1689
+ // A list item with a marker and no line holds one, the marker's, a line
1690
+ // of its own face tall: an empty `<li>` is a line tall in a browser, and
1691
+ // an inline-block around one sits on its marker's baseline.
1692
+ let height = flow.height;
1693
+ if (box.marker && ctx.fonts && !firstLineIn(box)) {
1694
+ const strut = strutOf(ctx.fonts, box.style);
1695
+ const line = strut.ascent + strut.descent;
1696
+ box.lines = [
1697
+ {
1698
+ x: box.contentX,
1699
+ y: box.contentY,
1700
+ width: 0,
1701
+ height: line,
1702
+ baseline: strut.ascent,
1703
+ texts: [],
1704
+ textStart: box.subtreeTextStart,
1705
+ textEnd: box.subtreeTextStart,
1706
+ atomics: [],
1707
+ },
1708
+ ];
1709
+ height = Math.max(height, line);
1710
+ }
382
1711
  // A box that establishes a formatting context contains its own floats, so
383
1712
  // it has to be at least as tall as they are. One that does not, does not —
384
1713
  // that is the classic "collapsed parent" every author has met.
385
- const withFloats = ownFloats
386
- ? Math.max(
387
- flow.height,
388
- floats.bottom === -Infinity ? 0 : floats.bottom - box.contentY,
389
- )
390
- : flow.height;
1714
+ // Past a clamp point, a line-clamp container clips its floats rather
1715
+ // than holding them (CSS Overflow 4, 5.3.1).
1716
+ const clamped = ctx.clamp?.box === box && ctx.clamp.done;
1717
+ const withFloats =
1718
+ ownFloats && !clamped
1719
+ ? Math.max(
1720
+ height,
1721
+ floats.bottom === -Infinity ? 0 : floats.bottom - box.contentY,
1722
+ )
1723
+ : height;
391
1724
  finishHeight(box, withFloats);
392
1725
  // The margin that escaped through this box's bottom edge becomes part of
393
1726
  // its own: the parent's flow loop reads `child.marginBottom` for the next
394
1727
  // sibling's collapse, which is exactly where an escaped margin goes.
395
- if (flow.hanging) {
396
- box.marginBottom = collapseMargins(box.marginBottom, flow.hanging);
397
- }
398
- if (box.markerText) layoutMarker(box, ctx);
1728
+ // Kept as a strut too, for the next sibling's margins to collapse with
1729
+ // all of the ones it came to rather than with their sum.
1730
+ if (flow.hanging.pos || flow.hanging.neg) {
1731
+ const bottom = join(flow.hanging, box.marginBottom);
1732
+ box.bottomStrut = bottom;
1733
+ box.marginBottom = marginOf(bottom);
1734
+ } else box.bottomStrut = null;
1735
+ if (box.marker) layoutMarker(box, box.marker, ctx);
399
1736
  }
400
1737
 
401
1738
  /** The first line box anywhere under a box, in layout order. */
@@ -421,41 +1758,468 @@ function firstLineIn(box: Box): LineBox | null {
421
1758
  * content: it must not join the selection, or copying a list would paste a
422
1759
  * bullet before every line.
423
1760
  */
424
- function layoutMarker(box: Box, ctx: LayoutContext): void {
1761
+ const MARKER_GAPS = new WeakMap<FontsLike, Map<string, number>>();
1762
+
1763
+ /**
1764
+ * The gap after a marker, a space's width in its face: kept per face and
1765
+ * fonts, since a list is a column of markers set alike, and laying the
1766
+ * space out for each of them was most of what a long document's markers
1767
+ * cost.
1768
+ */
1769
+ function markerGap(fonts: FontsLike, face: Omit<TextRun, 'text'>): number {
1770
+ let gaps = MARKER_GAPS.get(fonts);
1771
+ if (!gaps) MARKER_GAPS.set(fonts, (gaps = new Map()));
1772
+ const key = `${face.family}\u0001${face.size}\u0001${String(face.weight ?? '')}\u0001${String(face.style ?? '')}`;
1773
+ let gap = gaps.get(key);
1774
+ if (gap === undefined) {
1775
+ gap = fonts.layout([{ text: '\u00a0', ...face }], face, {}).width;
1776
+ gaps.set(key, gap);
1777
+ }
1778
+ return gap;
1779
+ }
1780
+
1781
+ function layoutMarker(box: Box, marker: Marker, ctx: LayoutContext): void {
425
1782
  const fonts = ctx.fonts;
426
1783
  if (!fonts) return;
427
1784
  const style = box.style;
428
- const layout = fonts.layout(
429
- [
430
- {
431
- text: box.markerText,
432
- family: style.fontFamily,
433
- size: style.fontSize,
434
- color: style.color,
435
- },
436
- ],
437
- { family: style.fontFamily, size: style.fontSize, color: style.color },
438
- {},
439
- );
440
- box.markerLayout = layout;
441
- const gap = Math.round(style.fontSize * 0.4);
1785
+ if (marker.image) {
1786
+ // an image: its bottom on the first line's baseline, as an inline
1787
+ // image's is, and the gap a bullet has before the content
1788
+ const first = firstLineIn(box);
1789
+ marker.y = first
1790
+ ? first.y + first.baseline - marker.image.height
1791
+ : box.contentY;
1792
+ const gap = Math.round(style.fontSize * 0.4);
1793
+ // at the start of the line, which is its right in a right-to-left item
1794
+ marker.x =
1795
+ style.direction === 'rtl'
1796
+ ? box.contentX + box.contentWidth + gap
1797
+ : box.contentX - gap - marker.image.width;
1798
+ return;
1799
+ }
1800
+ // in its own style where a `::marker` rule gives it one — the colour and
1801
+ // the face a bullet or a number is set in — and the item's where not
1802
+ const set = marker.style ?? style;
1803
+ const face = {
1804
+ family: set.fontFamily,
1805
+ size: set.fontSize,
1806
+ color: set.color,
1807
+ ...(marker.style
1808
+ ? {
1809
+ weight: set.fontWeight,
1810
+ style:
1811
+ set.fontStyle === 'normal'
1812
+ ? ('normal' as const)
1813
+ : ('italic' as const),
1814
+ }
1815
+ : null),
1816
+ };
1817
+ // set in a line of the item's own height, so that where the item has no
1818
+ // line of its own the marker stands where its first would have been
1819
+ // in the item's direction, which puts a number's full stop on its left
1820
+ // in a right-to-left list, as the bidi algorithm has it
1821
+ const layout = fonts.layout([{ text: marker.text, ...face }], face, {
1822
+ lineHeight: lineHeightMultiplier(fonts, style),
1823
+ direction: style.direction,
1824
+ });
1825
+ marker.layout = layout;
1826
+ // a space between it and the content — the one its style's suffix ends
1827
+ // in, or its own text's — in its own face: a line measures without it
1828
+ const gap =
1829
+ marker.flush && !/\s$/.test(marker.text) ? 0 : markerGap(fonts, face);
442
1830
  // The marker sits on the first line of the item's *content*, which is not
443
1831
  // always the item's own: an `<li>` holding a paragraph, or one holding text
444
1832
  // and a nested list, has its inline content in an anonymous block. Looking
445
1833
  // only at `box.lines` puts the marker of every such item at the content
446
- // top, which reads as a missing bullet rather than a misplaced one.
1834
+ // top, which reads as a missing bullet rather than a misplaced one. An
1835
+ // empty item is still a list item, and its marker a line at its top.
447
1836
  const first = firstLineIn(box);
448
- const baselineY = first ? first.y + first.baseline : box.contentY;
449
1837
  const own = layout.lines[0];
450
- box.markerY = baselineY - (own ? own.baseline : style.fontSize);
451
- box.markerX =
1838
+ marker.y = first
1839
+ ? first.y + first.baseline - (own ? own.baseline : style.fontSize)
1840
+ : box.contentY;
1841
+ marker.x =
452
1842
  style.listStylePosition === 'inside'
453
1843
  ? box.contentX
454
- : box.contentX - gap - layout.width;
1844
+ : style.direction === 'rtl'
1845
+ ? box.contentX + box.contentWidth + gap
1846
+ : box.contentX - gap - layout.width;
1847
+ }
1848
+
1849
+ /**
1850
+ * The height a percentage `height` inside a box resolves against: its own
1851
+ * content height, where that is set rather than grown from its content —
1852
+ * a length, or a percentage that itself resolved (CSS 2.1 10.5). NaN, and
1853
+ * so `auto`, where it is not. An anonymous box is no containing block for
1854
+ * this (9.2.1.1) and hands on its parent's.
1855
+ *
1856
+ * The root element's is the viewport's height, so `html, body { height:
1857
+ * 100% }` — which mail sets as often as not — is a window tall, as in a
1858
+ * browser; the document is as tall as what overflows it, so a message
1859
+ * longer than the window is not cut off (`layoutDocument`).
1860
+ */
1861
+ export function percentBaseInside(box: Box): number {
1862
+ const flexed = FLEXED_HEIGHT.get(box);
1863
+ if (flexed !== undefined) return flexed;
1864
+ if (!box.el && !box.pseudo) return box.percentHeightBase;
1865
+ const resolved = specifiedHeight(box);
1866
+ if (resolved === null) {
1867
+ // a height `aspect-ratio` gives from a width is as definite as the width
1868
+ const ratio = ratioHeight(box);
1869
+ if (ratio === null) return NaN;
1870
+ return Math.max(
1871
+ 0,
1872
+ clampHeight(box, ratio + box.verticalExtra) - box.verticalExtra,
1873
+ );
1874
+ }
1875
+ const set = Math.max(0, resolved);
1876
+ const borderBox =
1877
+ box.style.boxSizing === 'border-box'
1878
+ ? Math.max(set, box.verticalExtra)
1879
+ : set + box.verticalExtra;
1880
+ return Math.max(0, clampHeight(box, borderBox) - box.verticalExtra);
1881
+ }
1882
+
1883
+ /** Whether anything in a box takes a percentage of a height: what a height
1884
+ * a flex or grid layout makes definite changes. Kept for the tree's life. */
1885
+ export function percentHeightsIn(box: Box): boolean {
1886
+ let found = PERCENT_HEIGHTS.get(box);
1887
+ if (found === undefined) {
1888
+ found = false;
1889
+ // a column's basis is a height too
1890
+ const column =
1891
+ box.kind === 'flex' &&
1892
+ !box.style.grid &&
1893
+ box.style.flexDirection.startsWith('column');
1894
+ for (const child of box.children) {
1895
+ const style = child.style;
1896
+ if (
1897
+ isPct(style.height) ||
1898
+ isPct(style.minHeight) ||
1899
+ (style.maxHeight !== 'none' && isPct(style.maxHeight)) ||
1900
+ (column && style.flexBasis !== 'content' && isPct(style.flexBasis)) ||
1901
+ percentHeightsIn(child)
1902
+ ) {
1903
+ found = true;
1904
+ break;
1905
+ }
1906
+ }
1907
+ PERCENT_HEIGHTS.set(box, found);
1908
+ }
1909
+ return found;
1910
+ }
1911
+
1912
+ const PERCENT_HEIGHTS = new WeakMap<Box, boolean>();
1913
+
1914
+ /**
1915
+ * A box's own height, where it has one, as `box-sizing` measures it: a
1916
+ * length, a percentage of a height that resolves, or `stretch`'s, and else
1917
+ * none, which is `auto`.
1918
+ */
1919
+ function specifiedHeight(box: Box): number | null {
1920
+ const style = box.style;
1921
+ if (style.heightKeyword === 'stretch') {
1922
+ const outer = stretchHeight(box);
1923
+ if (outer === null) return null;
1924
+ return style.boxSizing === 'border-box'
1925
+ ? outer
1926
+ : Math.max(0, outer - box.verticalExtra);
1927
+ }
1928
+ return resolveOrNull(style.height, box.percentHeightBase);
1929
+ }
1930
+
1931
+ /** Where an absolutely positioned box's `stretch` height starts in its
1932
+ * containing block: its `top`, or its static position where it has
1933
+ * neither offset (`layoutPositioned`). */
1934
+ const STRETCH_TOP = new WeakMap<Box, number>();
1935
+
1936
+ /**
1937
+ * A replaced box's `stretch` sizes, which `sizeReplaced` knows nothing of:
1938
+ * the width it was given for one across, the height its offsets leave for
1939
+ * one down, and through its ratio, the other where that is `auto`.
1940
+ */
1941
+ function stretchReplaced(
1942
+ box: Box,
1943
+ across: number | null,
1944
+ containingWidth: number,
1945
+ ): void {
1946
+ const style = box.style;
1947
+ const down = style.heightKeyword === 'stretch' ? stretchHeight(box) : null;
1948
+ if (across === null && down === null) return;
1949
+ if (across !== null) box.width = across;
1950
+ if (down !== null) box.height = clampHeight(box, down);
1951
+ if (across !== null && down === null && style.height === AUTO) {
1952
+ const tall = heightThroughRatio(box, across);
1953
+ if (tall !== null) box.height = clampHeight(box, tall);
1954
+ } else if (down !== null && across === null && style.width === AUTO) {
1955
+ const wide = widthThroughRatio(box, box.height);
1956
+ if (wide !== null) box.width = clampWidth(box, wide, containingWidth);
1957
+ }
1958
+ }
1959
+
1960
+ /**
1961
+ * The border-box height `stretch` makes of a box (CSS Sizing 3, 4.2): what
1962
+ * its margins leave of the height its percentages are of, where that is
1963
+ * definite, and else none. A margin of an in-flow block that its parent's
1964
+ * border or padding does not part from the parent's edge, in a parent that
1965
+ * is no formatting context of its own, counts as none, as the margin would
1966
+ * collapse with the parent's.
1967
+ */
1968
+ function stretchHeight(box: Box): number | null {
1969
+ const base = box.percentHeightBase;
1970
+ if (!Number.isFinite(base)) return null;
1971
+ const style = box.style;
1972
+ if (style.position === 'absolute' || style.position === 'fixed') {
1973
+ // what its offsets leave of its containing block, an `auto` one none
1974
+ // — or its static position, where both are (`layoutPositioned`)
1975
+ const top = STRETCH_TOP.get(box) ?? resolveOrNull(style.top, base) ?? 0;
1976
+ const bottom = resolveOrNull(style.bottom, base) ?? 0;
1977
+ return Math.max(
1978
+ box.verticalExtra,
1979
+ base - top - bottom - box.marginTop - box.marginBottom,
1980
+ );
1981
+ }
1982
+ const parent = box.parent;
1983
+ const inFlow =
1984
+ parent !== null &&
1985
+ !box.outOfFlow &&
1986
+ !box.isFloat &&
1987
+ box.kind !== 'replaced' &&
1988
+ !isInlineLevel(box) &&
1989
+ !establishesBFC(parent);
1990
+ const top = inFlow && !parent.borderTop && !parent.padTop ? 0 : box.marginTop;
1991
+ const bottom =
1992
+ inFlow && !parent.borderBottom && !parent.padBottom ? 0 : box.marginBottom;
1993
+ return Math.max(box.verticalExtra, base - top - bottom);
1994
+ }
1995
+
1996
+ /**
1997
+ * The content height a flex layout made definite for an item — a stretched
1998
+ * item's, a flexed one's in a container of a definite height (CSS Flexbox
1999
+ * 9.8) — which what is in it takes its percentages of. Set by `flex.ts`
2000
+ * for the item's own layout alone.
2001
+ */
2002
+ export const FLEXED_HEIGHT = new WeakMap<Box, number>();
2003
+
2004
+ /**
2005
+ * Hand a box's children the height their percentages resolve against —
2006
+ * through inline boxes, which are no containing block, to the inline-block
2007
+ * or image in them. Nearly every box has none to give, and its children
2008
+ * hold NaN until it has, so it visits them only when there is a height, or
2009
+ * was one on the last pass: visiting every child of every block on every
2010
+ * pass cost more than laying out a table.
2011
+ */
2012
+ function givePercentBase(box: Box, base: number): void {
2013
+ const given = !Number.isNaN(base);
2014
+ if (!given && !box.gavePercentBase) return;
2015
+ box.gavePercentBase = given;
2016
+ handPercentBase(box, base);
2017
+ }
2018
+
2019
+ function handPercentBase(box: Box, base: number): void {
2020
+ for (const child of box.children) {
2021
+ child.percentHeightBase = base;
2022
+ if (child.kind === 'inline') handPercentBase(child, base);
2023
+ }
2024
+ }
2025
+
2026
+ /**
2027
+ * The content height `aspect-ratio` gives a box whose height is `auto`, from
2028
+ * its width (CSS Sizing 4, 5.1): the ratio is of the box its `box-sizing`
2029
+ * names, which is the border box in everything Tailwind writes. Null where
2030
+ * it has none, or a height of its own; a replaced box is sized apart.
2031
+ */
2032
+ export function ratioHeight(box: Box): number | null {
2033
+ const ratio = boxRatio(box);
2034
+ if (!ratio) return null;
2035
+ if (resolveOrNull(box.style.height, box.percentHeightBase) !== null) {
2036
+ return null;
2037
+ }
2038
+ return ratio.border
2039
+ ? Math.max(0, box.width / ratio.ratio - box.verticalExtra)
2040
+ : Math.max(0, box.contentWidth / ratio.ratio);
2041
+ }
2042
+
2043
+ /**
2044
+ * A box's `aspect-ratio` where it is not replaced, and whether it is of its
2045
+ * border box — a ratio written alone under `box-sizing: border-box` — or
2046
+ * of its content box, as one written `auto` with it is whatever the
2047
+ * `box-sizing` (CSS Sizing 4, 5.1).
2048
+ */
2049
+ export function boxRatio(box: Box): { ratio: number; border: boolean } | null {
2050
+ const aspect = box.style.aspectRatio;
2051
+ if (!aspect || box.kind === 'replaced' || !(aspect.ratio > 0)) return null;
2052
+ return {
2053
+ ratio: aspect.ratio,
2054
+ border: box.style.boxSizing === 'border-box' && !aspect.auto,
2055
+ };
2056
+ }
2057
+
2058
+ /** A border-box height through a box's ratio into its border-box width,
2059
+ * or null where the box has no ratio of its own. */
2060
+ export function widthFromHeight(box: Box, height: number): number | null {
2061
+ const ratio = boxRatio(box);
2062
+ return ratio ? acrossRatio(box, ratio, height) : null;
2063
+ }
2064
+
2065
+ /** A replaced element's ratio: its `aspect-ratio`, unless that says `auto`
2066
+ * and it has one of its own, as `sizeReplaced` takes it. */
2067
+ export function replacedRatio(box: Box): number {
2068
+ const own = box.intrinsic;
2069
+ const aspect = box.style.aspectRatio;
2070
+ const natural = own ? own.ratio : 0;
2071
+ return aspect && !(aspect.auto && natural > 0) ? aspect.ratio : natural;
2072
+ }
2073
+
2074
+ /** A border-box height through any box's ratio into its border-box width
2075
+ * — a replaced element's is of its content box — or null where it has
2076
+ * none. */
2077
+ export function widthThroughRatio(box: Box, height: number): number | null {
2078
+ if (box.kind !== 'replaced') return widthFromHeight(box, height);
2079
+ const ratio = replacedRatio(box);
2080
+ return ratio > 0
2081
+ ? Math.max(0, height - box.verticalExtra) * ratio + box.horizontalExtra
2082
+ : null;
2083
+ }
2084
+
2085
+ /** A border-box width through any box's ratio into its border-box height,
2086
+ * or null where it has none. */
2087
+ export function heightThroughRatio(box: Box, width: number): number | null {
2088
+ if (box.kind !== 'replaced') return heightFromWidth(box, width);
2089
+ const ratio = replacedRatio(box);
2090
+ return ratio > 0
2091
+ ? Math.max(0, width - box.horizontalExtra) / ratio + box.verticalExtra
2092
+ : null;
2093
+ }
2094
+
2095
+ /** A border-box width through a box's ratio into its border-box height,
2096
+ * or null where the box has no ratio of its own. */
2097
+ export function heightFromWidth(box: Box, width: number): number | null {
2098
+ const ratio = boxRatio(box);
2099
+ if (!ratio) return null;
2100
+ return ratio.border
2101
+ ? width / ratio.ratio
2102
+ : Math.max(0, width - box.horizontalExtra) / ratio.ratio +
2103
+ box.verticalExtra;
2104
+ }
2105
+
2106
+ /** A border-box height through a box's ratio into its border-box width. */
2107
+ function acrossRatio(
2108
+ box: Box,
2109
+ ratio: { ratio: number; border: boolean },
2110
+ height: number,
2111
+ ): number {
2112
+ return ratio.border
2113
+ ? height * ratio.ratio
2114
+ : Math.max(0, height - box.verticalExtra) * ratio.ratio +
2115
+ box.horizontalExtra;
2116
+ }
2117
+
2118
+ /**
2119
+ * The border-box width `aspect-ratio` gives a box whose width is `auto`
2120
+ * from a height of its own, within its least and greatest heights (CSS
2121
+ * Sizing 4, 5.1): a block is that wide, not as wide as its room — and so
2122
+ * is one whose width is its content's, `min-content` or `fit-content`, as
2123
+ * its content is that height through its ratio (5.3). Null where it has
2124
+ * no ratio, no height or a length for a width.
2125
+ */
2126
+ export function ratioWidth(box: Box): number | null {
2127
+ const ratio = boxRatio(box);
2128
+ const style = box.style;
2129
+ if (!ratio || style.width !== AUTO) return null;
2130
+ const set = resolveOrNull(style.height, box.percentHeightBase);
2131
+ if (set === null) return null;
2132
+ const outer = clampHeight(
2133
+ box,
2134
+ style.boxSizing === 'border-box'
2135
+ ? Math.max(set, box.verticalExtra)
2136
+ : set + box.verticalExtra,
2137
+ );
2138
+ return acrossRatio(box, ratio, outer);
455
2139
  }
456
2140
 
2141
+ /**
2142
+ * A width its ratio gave a box, no narrower than its content at its
2143
+ * narrowest where its least width is `auto` and it shows what overflows
2144
+ * it — the automatic minimum of a box with a ratio (5.2) — though no
2145
+ * wider for that than its greatest width.
2146
+ */
2147
+ function ratioMinimum(
2148
+ box: Box,
2149
+ width: number,
2150
+ containingWidth: number,
2151
+ ctx?: LayoutContext,
2152
+ ): number {
2153
+ const style = box.style;
2154
+ if (
2155
+ !ctx ||
2156
+ style.minWidth !== AUTO ||
2157
+ style.minWidthKeyword ||
2158
+ scrolls(style)
2159
+ ) {
2160
+ return width;
2161
+ }
2162
+ if (box.intrinsicMinContent < 0) {
2163
+ const saved = box.lines;
2164
+ box.intrinsicMinContent = measureIntrinsicWidth(
2165
+ box,
2166
+ ctx,
2167
+ MIN_CONTENT_PROBE,
2168
+ );
2169
+ box.lines = saved;
2170
+ resolveEdges(box, containingWidth);
2171
+ }
2172
+ const max =
2173
+ style.maxWidth === 'none'
2174
+ ? Infinity
2175
+ : (resolveOrNull(style.maxWidth, containingWidth) ?? Infinity) +
2176
+ (style.boxSizing === 'border-box' ? 0 : box.horizontalExtra);
2177
+ return Math.max(width, Math.min(box.intrinsicMinContent, max));
2178
+ }
2179
+
2180
+ /** A table cell's height as its content came to, border box, apart from a
2181
+ * height it sets: what `vertical-align` moves in the cell (`table.ts`). */
2182
+ export const CELL_CONTENT = new WeakMap<Box, number>();
2183
+
457
2184
  function finishHeight(box: Box, contentHeight: number): void {
458
- const specified = resolveOrNull(box.style.height, NaN);
2185
+ // as though it held nothing, under size containment
2186
+ if (contained(box, CONTAIN_SIZE)) {
2187
+ contentHeight = box.style.containIntrinsicHeight ?? 0;
2188
+ }
2189
+ if (box.kind === 'table-cell') {
2190
+ CELL_CONTENT.set(box, contentHeight + box.verticalExtra);
2191
+ }
2192
+ const set = specifiedHeight(box);
2193
+ // at least zero: a `calc()` may come to less
2194
+ const specified = set === null ? null : Math.max(0, set);
2195
+ if (specified !== null && box.kind === 'table') {
2196
+ // A table's height is a least one, which `layoutTable` has shared out
2197
+ // to its rows: one set shorter than they are leaves them their height
2198
+ // (CSS 2.1 17.5.3). Taken as the table's, it ended its background
2199
+ // over its rows, and a float after it went up beside them.
2200
+ const outer =
2201
+ box.style.boxSizing === 'border-box'
2202
+ ? specified
2203
+ : specified + box.verticalExtra;
2204
+ box.height = Math.max(
2205
+ clampHeight(box, outer),
2206
+ contentHeight + box.verticalExtra,
2207
+ );
2208
+ return;
2209
+ }
2210
+ if (specified === null && box.style.aspectRatio) {
2211
+ const ratio = ratioHeight(box);
2212
+ if (ratio !== null) {
2213
+ // the ratio's height, grown to what the box holds unless it clips it
2214
+ // or has a least height of its own (5.2: the automatic minimum of a
2215
+ // box with a ratio is its content, and `min-height: 0` is none)
2216
+ const automatic =
2217
+ box.style.minHeight === AUTO &&
2218
+ box.style.minHeightKeyword === null &&
2219
+ !scrolls(box.style);
2220
+ contentHeight = automatic ? Math.max(ratio, contentHeight) : ratio;
2221
+ }
2222
+ }
459
2223
  const height = specified ?? contentHeight;
460
2224
  const borderBox =
461
2225
  box.style.boxSizing === 'border-box' && specified !== null
@@ -464,13 +2228,26 @@ function finishHeight(box: Box, contentHeight: number): void {
464
2228
  box.height = clampHeight(box, borderBox);
465
2229
  }
466
2230
 
467
- function clampHeight(box: Box, height: number): number {
2231
+ export function clampHeight(box: Box, height: number): number {
468
2232
  let out = height;
469
- const min = resolveOrNull(box.style.minHeight, NaN);
2233
+ const base = box.percentHeightBase;
2234
+ // a percentage of a height nothing sets is zero for a minimum (CSS 2.1
2235
+ // 10.7), which leaves a `calc()` its pixels; so is `stretch`, which is a
2236
+ // border-box height where it is one
2237
+ if (box.style.maxHeightKeyword === 'stretch') {
2238
+ out = Math.min(out, stretchHeight(box) ?? Infinity);
2239
+ }
2240
+ if (box.style.minHeightKeyword === 'stretch') {
2241
+ out = Math.max(out, stretchHeight(box) ?? 0);
2242
+ }
2243
+ const min =
2244
+ box.style.minHeight === AUTO
2245
+ ? 0
2246
+ : resolve(box.style.minHeight, Number.isFinite(base) ? base : 0);
470
2247
  const max =
471
2248
  box.style.maxHeight === 'none'
472
2249
  ? null
473
- : resolveOrNull(box.style.maxHeight, NaN);
2250
+ : resolveOrNull(box.style.maxHeight, base);
474
2251
  if (max !== null)
475
2252
  out = Math.min(
476
2253
  out,
@@ -495,31 +2272,147 @@ function placeBlock(
495
2272
  const leftAuto = style.marginLeft === AUTO;
496
2273
  const rightAuto = style.marginRight === AUTO;
497
2274
  const slack = containingWidth - box.width - box.marginLeft - box.marginRight;
2275
+ // what the width and the margins do not add up to goes to the margin at
2276
+ // the end: the right one, or in a right-to-left containing block the
2277
+ // left one, and a box too wide overflows there (CSS 2.1 10.3.3)
2278
+ const rtl = box.parent?.style.direction === 'rtl';
2279
+ // an alignment around it places a box its margins do not (HTML's
2280
+ // `<center>` and `align`)
2281
+ const aligned = leftAuto || rightAuto ? null : box.parent?.style.alignBlocks;
498
2282
  let left = contentLeft + box.marginLeft;
2283
+ // In the unbounded room a shrink-to-fit probe lays a box out in, there is
2284
+ // no slack to share: half of an infinite one put an auto-margined box at
2285
+ // x = Infinity, and the pass after moved it from there by a finite amount
2286
+ // — NaN, which went up every ink bound above it and left a whole article
2287
+ // unpainted.
2288
+ if (!Number.isFinite(slack)) {
2289
+ box.x = left;
2290
+ box.y = y;
2291
+ return;
2292
+ }
499
2293
  if (slack > 0) {
500
2294
  if (leftAuto && rightAuto) left = contentLeft + slack / 2 + box.marginLeft;
501
- else if (leftAuto) left = contentLeft + slack + box.marginLeft;
2295
+ else if (aligned) {
2296
+ if (aligned === 'center') left += slack / 2;
2297
+ else if (aligned === 'right') left += slack;
2298
+ } else if (leftAuto || (rtl && !rightAuto)) {
2299
+ left = contentLeft + slack + box.marginLeft;
2300
+ }
2301
+ } else if (rtl) {
2302
+ left = contentLeft + slack + box.marginLeft;
502
2303
  }
503
2304
  box.x = left;
504
2305
  box.y = y;
505
2306
  }
506
2307
 
507
- /** The border-box width of an in-flow block-level box. */
508
- function blockWidth(box: Box, containingWidth: number): number {
2308
+ /** The border-box width of an in-flow block-level box: the room its
2309
+ * margins leave, a length, or an intrinsic size where `ctx` is there to
2310
+ * measure it with. */
2311
+ function blockWidth(
2312
+ box: Box,
2313
+ containingWidth: number,
2314
+ percentBase = containingWidth,
2315
+ ctx?: LayoutContext,
2316
+ ): number {
509
2317
  const style = box.style;
510
2318
  const available = containingWidth - box.marginLeft - box.marginRight;
511
2319
  if (style.width === AUTO) {
512
- return clampWidth(box, Math.max(0, available), containingWidth);
2320
+ // a width its height gives it through its ratio
2321
+ const fromRatio = ratioWidth(box);
2322
+ if (fromRatio !== null) {
2323
+ const clamped = clampWidth(box, fromRatio, percentBase, ctx);
2324
+ return ratioMinimum(box, clamped, percentBase, ctx);
2325
+ }
2326
+ const room = Math.max(0, available);
2327
+ const width =
2328
+ style.widthKeyword && ctx
2329
+ ? contentSizedWidth(box, ctx, style.widthKeyword, room, percentBase)
2330
+ : room;
2331
+ return clampWidth(
2332
+ box,
2333
+ transferredWidth(box, width),
2334
+ percentBase,
2335
+ ctx,
2336
+ containingWidth,
2337
+ );
513
2338
  }
514
- const specified = resolve(style.width, containingWidth, 0);
2339
+ // at least zero: a `calc()` may come to less
2340
+ const specified = Math.max(0, resolve(style.width, percentBase, 0));
515
2341
  const borderBox =
516
2342
  style.boxSizing === 'border-box'
517
2343
  ? Math.max(specified, box.horizontalExtra)
518
2344
  : specified + box.horizontalExtra;
519
- return clampWidth(box, borderBox, containingWidth);
2345
+ return clampWidth(box, borderBox, percentBase, ctx, containingWidth);
520
2346
  }
521
2347
 
522
- function clampWidth(box: Box, width: number, containingWidth: number): number {
2348
+ /**
2349
+ * A width a box with a ratio and no height of its own works out for itself
2350
+ * within the least and greatest widths its least and greatest heights make
2351
+ * through its ratio (CSS Sizing 4, 5.2) — those of its own winning where
2352
+ * the two disagree, as `clampWidth` after this makes them. A block the
2353
+ * room would stretch is no wider than its greatest height lets it be. A
2354
+ * width a flex box or a grid gives an item is not, which is theirs.
2355
+ */
2356
+ export function transferredWidth(box: Box, width: number): number {
2357
+ const style = box.style;
2358
+ const ratio = boxRatio(box);
2359
+ if (!ratio || resolveOrNull(style.height, box.percentHeightBase) !== null) {
2360
+ return width;
2361
+ }
2362
+ const heightOf = (len: Len) => {
2363
+ const px = resolveOrNull(len, box.percentHeightBase);
2364
+ if (px === null || !Number.isFinite(px)) return null;
2365
+ return style.boxSizing === 'border-box'
2366
+ ? Math.max(px, box.verticalExtra)
2367
+ : px + box.verticalExtra;
2368
+ };
2369
+ const maxH = style.maxHeight === 'none' ? null : heightOf(style.maxHeight);
2370
+ const minH = style.minHeight === AUTO ? null : heightOf(style.minHeight);
2371
+ let out = width;
2372
+ if (maxH !== null) out = Math.min(out, acrossRatio(box, ratio, maxH));
2373
+ if (minH !== null) out = Math.max(out, acrossRatio(box, ratio, minH));
2374
+ return out;
2375
+ }
2376
+
2377
+ /**
2378
+ * A border-box height within what a box's ratio makes of its least and
2379
+ * greatest widths: how a flex item's content size suggestion down a
2380
+ * column is held (CSS Flexbox 4.5). Unchanged where it has no ratio.
2381
+ */
2382
+ export function transferredHeight(
2383
+ box: Box,
2384
+ height: number,
2385
+ containingWidth: number,
2386
+ ): number {
2387
+ const ratio = boxRatio(box);
2388
+ if (!ratio) return height;
2389
+ const style = box.style;
2390
+ const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
2391
+ const down = (width: number) => heightFromWidth(box, width + extra)!;
2392
+ let out = height;
2393
+ if (style.maxWidth !== 'none') {
2394
+ const most = resolveOrNull(style.maxWidth, containingWidth);
2395
+ if (most !== null) out = Math.min(out, down(most));
2396
+ }
2397
+ const least =
2398
+ style.minWidth === AUTO
2399
+ ? null
2400
+ : resolveOrNull(style.minWidth, containingWidth);
2401
+ if (least !== null && least > 0) out = Math.max(out, down(least));
2402
+ return out;
2403
+ }
2404
+
2405
+ /** A width within `min-width` and `max-width` — the minimum winning — the
2406
+ * intrinsic ones among them where `ctx` is there to measure them. */
2407
+ export function clampWidth(
2408
+ box: Box,
2409
+ width: number,
2410
+ containingWidth: number,
2411
+ ctx?: LayoutContext,
2412
+ /** The room a `stretch` limit fills, where it is less than the
2413
+ * containing block beside floats. */
2414
+ room = containingWidth,
2415
+ ): number {
523
2416
  const style = box.style;
524
2417
  const extra = style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
525
2418
  let out = width;
@@ -528,11 +2421,108 @@ function clampWidth(box: Box, width: number, containingWidth: number): number {
528
2421
  ? null
529
2422
  : resolveOrNull(style.maxWidth, containingWidth);
530
2423
  if (max !== null) out = Math.min(out, max + extra);
531
- const min = resolveOrNull(style.minWidth, containingWidth);
2424
+ if (style.maxWidthKeyword && ctx) {
2425
+ out = Math.min(
2426
+ out,
2427
+ keywordWidth(box, ctx, style.maxWidthKeyword, containingWidth, room),
2428
+ );
2429
+ }
2430
+ // `auto` is 0 but for a flex item, which `flex.ts` answers
2431
+ const min =
2432
+ style.minWidth === AUTO
2433
+ ? 0
2434
+ : resolveOrNull(style.minWidth, containingWidth);
532
2435
  if (min !== null) out = Math.max(out, min + extra);
2436
+ if (style.minWidthKeyword && ctx) {
2437
+ out = Math.max(
2438
+ out,
2439
+ keywordWidth(box, ctx, style.minWidthKeyword, containingWidth, room),
2440
+ );
2441
+ }
533
2442
  return Math.max(0, out);
534
2443
  }
535
2444
 
2445
+ /** An intrinsic `min-width` or `max-width` in the room a box's margins
2446
+ * leave it. Apart from `clampWidth`, which every block is sized through,
2447
+ * so that it makes nothing for the ones that have none. */
2448
+ function keywordWidth(
2449
+ box: Box,
2450
+ ctx: LayoutContext,
2451
+ keyword: ContentSize,
2452
+ containingWidth: number,
2453
+ stretchRoom = containingWidth,
2454
+ ): number {
2455
+ const of = keyword === 'stretch' ? stretchRoom : containingWidth;
2456
+ const room = Math.max(0, of - box.marginLeft - box.marginRight);
2457
+ return contentSizedWidth(box, ctx, keyword, room, containingWidth);
2458
+ }
2459
+
2460
+ /**
2461
+ * A box's border-box width from an intrinsic size (CSS Sizing 3, 3.1): its
2462
+ * max-content width, its min-content width, or `fit-content` — the
2463
+ * max-content width where `available` holds it, and `available` where it
2464
+ * does not, but never less than the min-content width. Each is measured
2465
+ * once a box, as a table cell's are, and by laying the box out on its own,
2466
+ * which resolves its edges against nothing: they are resolved again
2467
+ * against `percentBase`, as its caller had them.
2468
+ */
2469
+ export function contentSizedWidth(
2470
+ box: Box,
2471
+ ctx: LayoutContext,
2472
+ size: ContentSize,
2473
+ available: number,
2474
+ percentBase: number,
2475
+ ): number {
2476
+ // `stretch` is the room, whatever the content (CSS Sizing 4)
2477
+ if (size === 'stretch') return Math.max(0, available);
2478
+ // a height of its own through a ratio is its content's width at any size
2479
+ const fromRatio = ratioWidth(box);
2480
+ if (fromRatio !== null) return fromRatio;
2481
+ // the content's sizes, whatever width the box has of its own: a
2482
+ // `min-width: max-content` beside a `width` is its content's widest,
2483
+ // and a probe of the box at no width answered the probe's width
2484
+ let probed = false;
2485
+ if (box.intrinsicMaxContent < 0) {
2486
+ box.intrinsicMaxContent = measureIntrinsicWidth(box, ctx, Infinity, true);
2487
+ probed = true;
2488
+ }
2489
+ if (size !== 'max-content' && box.intrinsicMinContent < 0) {
2490
+ box.intrinsicMinContent = measureIntrinsicWidth(
2491
+ box,
2492
+ ctx,
2493
+ MIN_CONTENT_PROBE,
2494
+ true,
2495
+ );
2496
+ probed = true;
2497
+ }
2498
+ if (probed) resolveEdges(box, percentBase);
2499
+ const max = box.intrinsicMaxContent;
2500
+ if (size === 'max-content') return max;
2501
+ const min = box.intrinsicMinContent;
2502
+ if (size === 'min-content') return min;
2503
+ return Math.min(
2504
+ max,
2505
+ Math.max(min, fitRoom(box, size, available, percentBase)),
2506
+ );
2507
+ }
2508
+
2509
+ /** The room `fit-content` fits a box's content in: what its margins leave,
2510
+ * or `fit-content()`'s argument as a width of its own, of `percentBase`
2511
+ * where it is a percentage — an indefinite one leaving the content its
2512
+ * widest. */
2513
+ function fitRoom(
2514
+ box: Box,
2515
+ size: ContentSize,
2516
+ available: number,
2517
+ percentBase: number,
2518
+ ): number {
2519
+ if (typeof size !== 'object') return available;
2520
+ const fit = Math.max(0, resolve(size.fit, percentBase, 0));
2521
+ return box.style.boxSizing === 'border-box'
2522
+ ? Math.max(fit, box.horizontalExtra)
2523
+ : fit + box.horizontalExtra;
2524
+ }
2525
+
536
2526
  /**
537
2527
  * Shrink-to-fit: the width a float, an inline-block or a positioned box with
538
2528
  * `width: auto` takes.
@@ -545,30 +2535,231 @@ function clampWidth(box: Box, width: number, containingWidth: number): number {
545
2535
  * overflows either way. The measured pass is thrown away; the caller lays the
546
2536
  * box out again at the width this returns.
547
2537
  */
548
- function shrinkToFitWidth(
549
- box: Box,
550
- ctx: LayoutContext,
551
- available: number,
552
- ): number {
553
- const style = box.style;
554
- if (style.width !== AUTO) {
555
- const specified = resolve(style.width, available, 0);
556
- const borderBox =
557
- style.boxSizing === 'border-box'
558
- ? Math.max(specified, box.horizontalExtra)
559
- : specified + box.horizontalExtra;
560
- return clampWidth(box, borderBox, available);
2538
+ function shrinkToFitWidth(
2539
+ box: Box,
2540
+ ctx: LayoutContext,
2541
+ available: number,
2542
+ /** How far into the containing block the box's room starts: an
2543
+ * absolute box's offset, or its static position. */
2544
+ offset = 0,
2545
+ ): number {
2546
+ const style = box.style;
2547
+ if (style.width !== AUTO) {
2548
+ const specified = Math.max(0, resolve(style.width, available, 0));
2549
+ const borderBox =
2550
+ style.boxSizing === 'border-box'
2551
+ ? Math.max(specified, box.horizontalExtra)
2552
+ : specified + box.horizontalExtra;
2553
+ return clampWidth(box, borderBox, available, ctx);
2554
+ }
2555
+ // `stretch` fills the room its margins leave, as a block's `auto` does
2556
+ if (style.widthKeyword === 'stretch') {
2557
+ const room = available - offset - box.marginLeft - box.marginRight;
2558
+ return clampWidth(box, Math.max(0, room), available, ctx);
2559
+ }
2560
+ // a width its height gives it through its ratio, as a block's
2561
+ const fromRatio = ratioWidth(box);
2562
+ if (fromRatio !== null) {
2563
+ const clamped = clampWidth(box, fromRatio, available, ctx);
2564
+ return ratioMinimum(box, clamped, available, ctx);
2565
+ }
2566
+ // `fit-content` is what shrink-to-fit is; the other two are not bounded
2567
+ // by the room, or not by the longest line, and `fit-content()` by a room
2568
+ // of its own
2569
+ const keyword = style.widthKeyword;
2570
+ if (keyword !== null && keyword !== 'fit-content') {
2571
+ const width = contentSizedWidth(box, ctx, keyword, available, available);
2572
+ return clampWidth(box, transferredWidth(box, width), available, ctx);
2573
+ }
2574
+ const probe = new FloatContext(0, Infinity);
2575
+ const saved = box.lines;
2576
+ // Its content's width with no limit on it is the same at every width, so
2577
+ // it is probed once in the box's life, as a cell's is. Probed every time
2578
+ // it was asked, a float in a float was probed as often as there were
2579
+ // floats around it, and each probe laid out all of the floats inside.
2580
+ let content = PREFERRED.get(box);
2581
+ let probed = false;
2582
+ if (content === undefined) {
2583
+ layoutInternals(box, ctx, Infinity, 0, 0, probe);
2584
+ content = intrinsicWidth(box);
2585
+ PREFERRED.set(box, content);
2586
+ probed = true;
2587
+ }
2588
+ const preferred = content + box.horizontalExtra;
2589
+ // the room is the containing block's, less the box's margins and where
2590
+ // it starts (CSS 2.1 10.3.5, 10.3.7, 10.3.9): a float with side margins
2591
+ // was as wide as its containing block, and stood out of it by them
2592
+ const room = Math.max(
2593
+ 0,
2594
+ available - offset - box.marginLeft - box.marginRight,
2595
+ );
2596
+ let width = Math.max(preferred, 0);
2597
+ if (
2598
+ width > room &&
2599
+ box.intrinsicMinContent < 0 &&
2600
+ ctx.fonts !== null &&
2601
+ !probed
2602
+ ) {
2603
+ // the words the floor is read from are the probe's
2604
+ layoutInternals(box, ctx, Infinity, 0, 0, probe);
2605
+ }
2606
+ // and where its content does not fit the room, its longest word is the
2607
+ // least it comes to: `min(max(min-content, room), max-content)`. That
2608
+ // matters only for a word wider than the room, which the widest word
2609
+ // between spaces bounds, from the probe just laid out; only past the
2610
+ // room is the min-content width measured — a second probe, at a width
2611
+ // every line breaks at — and it is kept on the box, as a cell's is
2612
+ const fonts = ctx.fonts;
2613
+ const floored =
2614
+ width > room &&
2615
+ box.intrinsicMinContent < 0 &&
2616
+ fonts !== null &&
2617
+ wordBound(box, fonts, false) + box.horizontalExtra > room &&
2618
+ wordBound(box, fonts, true) + box.horizontalExtra > room;
2619
+ box.lines = saved;
2620
+ if (floored) {
2621
+ box.intrinsicMinContent = measureIntrinsicWidth(
2622
+ box,
2623
+ ctx,
2624
+ MIN_CONTENT_PROBE,
2625
+ );
2626
+ box.lines = saved;
2627
+ // the probe resolved the box's edges against the width it was given
2628
+ resolveEdges(box, available);
2629
+ }
2630
+ if (width > room)
2631
+ width = Math.min(width, Math.max(box.intrinsicMinContent, room));
2632
+ return clampWidth(box, transferredWidth(box, width), available, ctx);
2633
+ }
2634
+
2635
+ /** A shrink-to-fit box's content width at no width limit, the once
2636
+ * (`shrinkToFitWidth`); a box's lifetime is the cache's, as for
2637
+ * `Box.intrinsicMaxContent`. */
2638
+ const PREFERRED = new WeakMap<Box, number>();
2639
+
2640
+ /**
2641
+ * A box's min-content width as its words give it, laid out at no width
2642
+ * limit, and null where they may not: blocks and plain text that breaks
2643
+ * only at its spaces (`spaceBreaksOnly`) are as narrow as their widest
2644
+ * word (`widestWord`) with the edges of the blocks between, which is what
2645
+ * the probe finds (`MIN_CONTENT_PROBE`) — for a fifteenth of the cost on
2646
+ * ntk, which searches every word of that probe for a place to cut it, as
2647
+ * none fits in no width at all. Anything else
2648
+ * is left to the probe: another kind of break, an inline box's edges, a
2649
+ * box on a line, a float, a table or a flex box, text that does not wrap
2650
+ * or is indented, a block with widths of its own.
2651
+ */
2652
+ export function exactMinContent(box: Box, fonts: FontsLike): number | null {
2653
+ if (hasWidths(box.style) || contained(box, CONTAIN_WIDTH)) return null;
2654
+ const inner = exactWords(box, fonts);
2655
+ return inner === null ? null : inner + box.horizontalExtra;
2656
+ }
2657
+
2658
+ /** Whether a style sets its box's width, or its least or greatest width. */
2659
+ function hasWidths(style: ComputedStyle): boolean {
2660
+ return (
2661
+ style.width !== AUTO ||
2662
+ style.widthKeyword !== null ||
2663
+ (style.minWidth !== 0 && style.minWidth !== AUTO) ||
2664
+ style.minWidthKeyword !== null ||
2665
+ style.maxWidth !== 'none' ||
2666
+ style.maxWidthKeyword !== null
2667
+ );
2668
+ }
2669
+
2670
+ function exactWords(box: Box, fonts: FontsLike): number | null {
2671
+ const style = box.style;
2672
+ if (style.whiteSpace !== 'normal' || style.textIndent !== 0) return null;
2673
+ let widest = 0;
2674
+ if (box.lines) {
2675
+ let last: object | null = null;
2676
+ for (const line of box.lines) {
2677
+ if (line.atomics.length || line.edges?.length) return null;
2678
+ for (const text of line.texts) {
2679
+ if (text.layout === last) continue;
2680
+ last = text.layout;
2681
+ if (!spaceBreaksOnly(text.layout)) return null;
2682
+ widest = Math.max(widest, widestWord(fonts, text.layout));
2683
+ }
2684
+ }
2685
+ }
2686
+ for (const child of box.children) {
2687
+ if (child.kind === 'text' || child.kind === 'break') continue;
2688
+ if (child.kind === 'inline') {
2689
+ if (!plainInline(child)) return null;
2690
+ continue;
2691
+ }
2692
+ if (child.kind !== 'block' || child.isFloat) return null;
2693
+ if (child.outOfFlow) continue;
2694
+ if (hasWidths(child.style)) return null;
2695
+ const inner = exactWords(child, fonts);
2696
+ if (inner === null) return null;
2697
+ widest = Math.max(
2698
+ widest,
2699
+ inner + child.horizontalExtra + child.marginLeft + child.marginRight,
2700
+ );
2701
+ }
2702
+ return widest;
2703
+ }
2704
+
2705
+ /** Whether an inline box and what is in it are text that wraps. */
2706
+ function plainInline(box: Box): boolean {
2707
+ if (box.style.whiteSpace !== 'normal') return false;
2708
+ for (const child of box.children) {
2709
+ if (child.kind === 'text' || child.kind === 'break') continue;
2710
+ if (child.kind !== 'inline' || !plainInline(child)) return false;
2711
+ }
2712
+ return true;
2713
+ }
2714
+
2715
+ /**
2716
+ * An upper bound of a laid-out block's min-content width inside its edges:
2717
+ * the widest word of its lines (`widestWord`, measured or estimated from
2718
+ * its characters), the widest box on them, and the same of each block in
2719
+ * it with that block's edges. A table, a flex
2720
+ * box and anything else not laid out as blocks and lines is as wide as the
2721
+ * probe made it, which is more than it has to be; a box that is itself one
2722
+ * has no bound.
2723
+ */
2724
+ function wordBound(box: Box, fonts: FontsLike, measured: boolean): number {
2725
+ if (box.kind !== 'block') return Infinity;
2726
+ let widest = 0;
2727
+ // a line that does not wrap is one word: a `nowrap` tooltip in a narrow
2728
+ // containing block was cut to its width, the rest of the line out of it
2729
+ const unbroken =
2730
+ box.style.whiteSpace === 'nowrap' || box.style.whiteSpace === 'pre';
2731
+ if (box.lines) {
2732
+ let last: object | null = null;
2733
+ for (const line of box.lines) {
2734
+ if (unbroken) widest = Math.max(widest, line.width);
2735
+ for (const text of line.texts) {
2736
+ if (text.layout === last) continue;
2737
+ last = text.layout;
2738
+ widest = Math.max(widest, widestWord(fonts, text.layout, measured));
2739
+ }
2740
+ for (const placed of line.atomics) {
2741
+ const b = placed.box;
2742
+ widest = Math.max(widest, b.width + b.marginLeft + b.marginRight);
2743
+ }
2744
+ }
561
2745
  }
562
- const probe = new FloatContext(0, Infinity);
563
- const saved = box.lines;
564
- layoutInternals(box, ctx, Infinity, 0, 0, probe);
565
- const preferred = intrinsicWidth(box) + box.horizontalExtra;
566
- box.lines = saved;
567
- return clampWidth(
568
- box,
569
- Math.min(Math.max(preferred, 0), available),
570
- available,
571
- );
2746
+ for (const child of box.children) {
2747
+ if (child.outOfFlow) continue;
2748
+ if (
2749
+ child.kind !== 'block' &&
2750
+ child.kind !== 'flex' &&
2751
+ child.kind !== 'table' &&
2752
+ child.kind !== 'replaced'
2753
+ )
2754
+ continue;
2755
+ const edges = child.marginLeft + child.marginRight;
2756
+ const inner =
2757
+ child.kind === 'block' && child.style.width === AUTO
2758
+ ? wordBound(child, fonts, measured) + child.horizontalExtra
2759
+ : child.width;
2760
+ widest = Math.max(widest, inner + edges);
2761
+ }
2762
+ return widest;
572
2763
  }
573
2764
 
574
2765
  /**
@@ -580,88 +2771,364 @@ function shrinkToFitWidth(
580
2771
  * Skipping the non-finite ones is what stops the probe from answering
581
2772
  * `Infinity` for every box that contains a paragraph.
582
2773
  */
583
- function intrinsicWidth(box: Box): number {
2774
+ export function intrinsicWidth(box: Box): number {
2775
+ // under size containment, as though it held nothing: the size
2776
+ // `contain-intrinsic-size` gives it, or none (CSS Containment 2, 3.2)
2777
+ if (contained(box, CONTAIN_WIDTH)) {
2778
+ return box.style.containIntrinsicWidth ?? 0;
2779
+ }
2780
+ // a grid laid out at no width limit has its columns at their widest, and
2781
+ // is as wide as they are — an item that runs past its column makes it no
2782
+ // wider — or, where it has none, as where its items end
2783
+ if (box.style.grid) {
2784
+ const cols = GRID_TRACKS.get(box)?.cols;
2785
+ if (cols?.length) return cols[cols.length - 1][1] - cols[0][0];
2786
+ let right = 0;
2787
+ for (const child of box.children) {
2788
+ if (child.outOfFlow || child.kind === 'text') continue;
2789
+ right = Math.max(
2790
+ right,
2791
+ child.x - box.contentX + child.width + child.marginRight,
2792
+ );
2793
+ }
2794
+ return right;
2795
+ }
584
2796
  let widest = 0;
585
- if (box.lines) {
586
- for (const line of box.lines) widest = Math.max(widest, line.width);
2797
+ // A row of flex items is as wide as all of them side by side, and gaps
2798
+ // between them; any other box is as wide as the widest thing in it
2799
+ const display = box.style.display;
2800
+ const row =
2801
+ (display === 'flex' || display === 'inline-flex') &&
2802
+ box.style.flexDirection.startsWith('row');
2803
+ let total = 0;
2804
+ let items = 0;
2805
+ const lines = box.lines;
2806
+ if (lines) {
2807
+ // a float met in a line stands beside what the line holds, at its
2808
+ // width, as does a row of them beside no line
2809
+ for (const line of lines) {
2810
+ widest = Math.max(widest, line.width + (line.floats ?? 0));
2811
+ }
2812
+ widest = Math.max(widest, box.floatRow);
587
2813
  }
2814
+ // Floats among blocks stand side by side too, as many as come together,
2815
+ // where each block in flow is a line of its own; one with a formatting
2816
+ // context of its own stands beside them (as Blink measures them). Taken
2817
+ // for the widest of them, a floated menu's items were one under another.
2818
+ // That is at the box's widest, laid out at no width limit; at its
2819
+ // narrowest each is as narrow as it can be, and one under another.
2820
+ const sideBySide = !Number.isFinite(box.width);
2821
+ let left = 0;
2822
+ let right = 0;
588
2823
  for (const child of box.children) {
589
2824
  if (child.kind === 'text' || child.kind === 'break') continue;
590
2825
  if (child.outOfFlow) continue;
2826
+ // what sits in the lines is measured with them, indent and all: an
2827
+ // inline-block after a negative `text-indent` ends where its line does
2828
+ if (lines && (isInlineLevel(child) || child.isFloat)) continue;
591
2829
  const margins = child.marginLeft + child.marginRight;
592
- const inner = intrinsicWidth(child) + child.horizontalExtra + margins;
593
2830
  const own = Number.isFinite(child.width) ? child.width + margins : 0;
594
- widest = Math.max(widest, inner, own);
2831
+ const style = child.style;
2832
+ // a minimum's percentage is of zero here (CSS Sizing 3 5.2.1), which
2833
+ // leaves a `calc()` its pixels
2834
+ const min =
2835
+ style.minWidth === 0 || style.minWidth === AUTO
2836
+ ? 0
2837
+ : resolve(style.minWidth, 0) + contentExtra(child);
2838
+ let contribution: number;
2839
+ // A width of its own is what a child contributes, whatever its content
2840
+ // does past it (CSS Sizing 3 5.1); a percentage one is cyclic here and
2841
+ // counts as `auto`, so its content decides.
2842
+ if (typeof style.width === 'number') {
2843
+ contribution = Math.max(own, min + margins);
2844
+ } else {
2845
+ let inner = intrinsicWidth(child) + child.horizontalExtra;
2846
+ if (typeof style.maxWidth === 'number') {
2847
+ inner = Math.min(inner, style.maxWidth + contentExtra(child));
2848
+ }
2849
+ if (min > inner) inner = min;
2850
+ contribution = Math.max(inner + margins, own);
2851
+ }
2852
+ if (row) {
2853
+ total += contribution;
2854
+ items += 1;
2855
+ } else if (!sideBySide) widest = Math.max(widest, contribution);
2856
+ else if (child.isFloat) {
2857
+ const clear = style.clear;
2858
+ if (clear !== 'none') {
2859
+ widest = Math.max(widest, left + right);
2860
+ if (clear !== 'right') left = 0;
2861
+ if (clear !== 'left') right = 0;
2862
+ }
2863
+ if (style.float === 'right') right += contribution;
2864
+ else left += contribution;
2865
+ } else {
2866
+ const beside = establishesBFC(child) ? left + right : 0;
2867
+ widest = Math.max(widest, beside + contribution, left + right);
2868
+ left = 0;
2869
+ right = 0;
2870
+ }
2871
+ }
2872
+ widest = Math.max(widest, left + right);
2873
+ if (row && items) {
2874
+ const gap = gapOf(box.style.columnGap, NaN);
2875
+ widest = Math.max(widest, total + gap * (items - 1));
595
2876
  }
596
2877
  return widest;
597
2878
  }
598
2879
 
599
- /** A replaced box's size: the style wins, then the attributes, then the
600
- * intrinsic size, and an aspect ratio is kept when only one axis is given. */
601
- function sizeReplaced(box: Box, containingWidth: number): void {
2880
+ /** What a box's own width leaves out of its border box: its padding and
2881
+ * border, unless `box-sizing` puts them in. */
2882
+ function contentExtra(box: Box): number {
2883
+ return box.style.boxSizing === 'border-box' ? 0 : box.horizontalExtra;
2884
+ }
2885
+
2886
+ /**
2887
+ * A replaced box's size, from its style and what it has of an intrinsic
2888
+ * width, height and ratio (CSS 2.1 10.3.2 and 10.6.2), within its minimum
2889
+ * and maximum (10.4, 10.7).
2890
+ *
2891
+ * An image has all three, a frame and a control their default size and no
2892
+ * ratio — so the axis the style does not set keeps its own, and a text field
2893
+ * `width: 100%` is not stretched to twice its height. An SVG may have any of
2894
+ * them: a height and nothing else is 300 pixels wide, a ratio and nothing
2895
+ * else fills its containing block, and nothing at all is 300 by 150 — the
2896
+ * default object size, which the builder leaves in `Intrinsic` for an axis
2897
+ * its `missing` names.
2898
+ *
2899
+ * A limit applied to one axis carries to the other through the ratio: an
2900
+ * image `max-width: 100%` narrower than itself keeps its proportions, where
2901
+ * clamping each axis alone squashed it — the `<img width="600">` of every
2902
+ * mail template, in a narrow column.
2903
+ */
2904
+ function sizeReplaced(
2905
+ box: Box,
2906
+ containingWidth: number,
2907
+ /** The room `stretch` fills: the containing block's width, or less of
2908
+ * it beside floats (CSS Sizing 3, 4.2). */
2909
+ stretchRoom = containingWidth,
2910
+ ): void {
602
2911
  const style = box.style;
603
- const intrinsicW = box.intrinsicWidth || 0;
604
- const intrinsicH = box.intrinsicHeight || 0;
605
- const ratio = intrinsicW > 0 && intrinsicH > 0 ? intrinsicH / intrinsicW : 0;
606
-
607
- let width = resolveOrNull(style.width, containingWidth);
608
- let height = resolveOrNull(style.height, NaN);
609
- if (width === null && height === null) {
610
- width = intrinsicW;
611
- height = intrinsicH;
612
- } else if (width === null) {
613
- width = ratio ? (height as number) / ratio : intrinsicW;
614
- } else if (height === null) {
615
- height = ratio ? width * ratio : intrinsicH;
616
- }
617
-
618
- const contentW = Math.max(0, width ?? 0);
619
- const contentH = Math.max(0, height ?? 0);
620
- const borderBoxW =
621
- style.boxSizing === 'border-box'
622
- ? Math.max(contentW, box.horizontalExtra)
623
- : contentW + box.horizontalExtra;
624
- const borderBoxH =
625
- style.boxSizing === 'border-box'
626
- ? Math.max(contentH, box.verticalExtra)
627
- : contentH + box.verticalExtra;
628
- box.width = clampWidth(box, borderBoxW, containingWidth);
629
- box.height = clampHeight(box, borderBoxH);
630
- // `hr` has no intrinsic size and its whole appearance is its border, so a
631
- // zero content height is the right answer rather than a missing one.
632
- if (box.replaced === 'hr' && box.style.height === AUTO) {
633
- box.height = box.verticalExtra;
2912
+ const base = box.percentHeightBase;
2913
+ // everything below is the content box's; a `border-box` length is not
2914
+ const hx = style.boxSizing === 'border-box' ? box.horizontalExtra : 0;
2915
+ const vx = style.boxSizing === 'border-box' ? box.verticalExtra : 0;
2916
+ const across = (len: Len): number | null => {
2917
+ const v = resolveOrNull(len, containingWidth);
2918
+ return v === null ? null : Math.max(0, v - hx);
2919
+ };
2920
+ const down = (len: Len): number | null => {
2921
+ const v = resolveOrNull(len, base);
2922
+ return v === null ? null : Math.max(0, v - vx);
2923
+ };
2924
+ // `stretch` is what the margins leave of the containing block, as a
2925
+ // content box, and of its height where that is definite
2926
+ const stretchedHeight =
2927
+ style.heightKeyword === 'stretch' ? stretchHeight(box) : null;
2928
+ const stretchedWidth = Number.isFinite(stretchRoom)
2929
+ ? Math.max(
2930
+ 0,
2931
+ stretchRoom - box.marginLeft - box.marginRight - box.horizontalExtra,
2932
+ )
2933
+ : null;
2934
+ const width =
2935
+ style.widthKeyword === 'stretch' && stretchedWidth !== null
2936
+ ? stretchedWidth
2937
+ : across(style.width);
2938
+ const height =
2939
+ stretchedHeight !== null
2940
+ ? Math.max(0, stretchedHeight - box.verticalExtra)
2941
+ : down(style.height);
2942
+ const minW =
2943
+ (style.minWidthKeyword === 'stretch' ? stretchedWidth : null) ??
2944
+ across(style.minWidth) ??
2945
+ 0;
2946
+ const minH = down(style.minHeight) ?? 0;
2947
+ // a maximum below the minimum is the minimum (10.4)
2948
+ const maxW = Math.max(
2949
+ minW,
2950
+ style.maxWidthKeyword === 'stretch' && stretchedWidth !== null
2951
+ ? stretchedWidth
2952
+ : style.maxWidth === 'none'
2953
+ ? Infinity
2954
+ : (across(style.maxWidth) ?? Infinity),
2955
+ );
2956
+ const maxH = Math.max(
2957
+ minH,
2958
+ style.maxHeight === 'none' ? Infinity : (down(style.maxHeight) ?? Infinity),
2959
+ );
2960
+ // under size containment, as though it had no size or ratio of its own
2961
+ // but the one `contain-intrinsic-size` gives it (CSS Containment 2, 3.2)
2962
+ const own = contained(box, CONTAIN_SIZE)
2963
+ ? {
2964
+ width: style.containIntrinsicWidth ?? 0,
2965
+ height: style.containIntrinsicHeight ?? 0,
2966
+ ratio: 0,
2967
+ missing: 0,
2968
+ }
2969
+ : (box.intrinsic ?? NO_INTRINSIC);
2970
+ const iw = own.missing & 1 ? null : own.width;
2971
+ const ih = own.missing & 2 ? null : own.height;
2972
+ // `aspect-ratio` over its own, unless written `auto` and it has one
2973
+ const aspect = style.aspectRatio;
2974
+ const ratio =
2975
+ aspect && !(aspect.auto && own.ratio > 0) ? aspect.ratio : own.ratio;
2976
+ // the width a block would have here: what a ratio with no size fills, and
2977
+ // an `hr`, whose whole appearance is its border across the line
2978
+ const room = (): number => {
2979
+ const w =
2980
+ containingWidth - box.marginLeft - box.marginRight - box.horizontalExtra;
2981
+ return Number.isFinite(w) ? Math.max(0, w) : 0;
2982
+ };
2983
+
2984
+ let w: number;
2985
+ let h: number;
2986
+ if (width === null && height === null && ratio > 0) {
2987
+ // both auto, with a ratio: the size it asks for, then the table in 10.4,
2988
+ // which moves both axes together
2989
+ if (iw !== null) {
2990
+ w = iw;
2991
+ h = ih ?? iw / ratio;
2992
+ } else if (ih !== null) {
2993
+ h = ih;
2994
+ w = ih * ratio;
2995
+ } else {
2996
+ // a ratio and no size: CSS 2.1 leaves it undefined and suggests the
2997
+ // width a block would have, which is what browsers do — or, where the
2998
+ // containing block waits on this box, the default object size
2999
+ w = Number.isFinite(containingWidth) ? room() : own.width;
3000
+ h = w / ratio;
3001
+ }
3002
+ [w, h] = constrained(w, h, minW, maxW, minH, maxH);
3003
+ } else {
3004
+ // one axis set, or no ratio: the width, clamped, and then the height
3005
+ // from the width that was used (10.3.2's "the rules above are applied
3006
+ // again" for a limit)
3007
+ const usedHeight = height === null ? null : clamp(height, minH, maxH);
3008
+ if (width !== null) w = width;
3009
+ else if (usedHeight !== null && ratio > 0) w = usedHeight * ratio;
3010
+ else if (box.replaced === 'hr') w = room();
3011
+ else w = own.width;
3012
+ w = clamp(w, minW, maxW);
3013
+ if (usedHeight !== null) h = usedHeight;
3014
+ else if (ratio > 0) h = clamp(w / ratio, minH, maxH);
3015
+ else h = clamp(own.height, minH, maxH);
634
3016
  }
3017
+ box.width = w + box.horizontalExtra;
3018
+ box.height = h + box.verticalExtra;
635
3019
  }
636
3020
 
637
- /** Place and size a float, and register it with the formatting context. */
3021
+ /** What a replaced box with nothing to say about its size has: `hr`. */
3022
+ const NO_INTRINSIC: Intrinsic = { width: 0, height: 0, missing: 0, ratio: 0 };
3023
+
3024
+ function clamp(value: number, min: number, max: number): number {
3025
+ return Math.max(min, Math.min(value, max));
3026
+ }
3027
+
3028
+ /**
3029
+ * CSS 2.1 10.4's table, for a replaced box sized by its ratio alone: a
3030
+ * limit on either axis is met by scaling both, and where the two axes' limits
3031
+ * disagree, the one that asks for more change wins.
3032
+ */
3033
+ function constrained(
3034
+ w: number,
3035
+ h: number,
3036
+ minW: number,
3037
+ maxW: number,
3038
+ minH: number,
3039
+ maxH: number,
3040
+ ): [number, number] {
3041
+ if (!(w > 0 && h > 0)) return [clamp(w, minW, maxW), clamp(h, minH, maxH)];
3042
+ if (w > maxW && h > maxH) {
3043
+ return maxW / w <= maxH / h
3044
+ ? [maxW, Math.max(minH, (maxW * h) / w)]
3045
+ : [Math.max(minW, (maxH * w) / h), maxH];
3046
+ }
3047
+ if (w < minW && h < minH) {
3048
+ return minW / w <= minH / h
3049
+ ? [Math.min(maxW, (minH * w) / h), minH]
3050
+ : [minW, Math.min(maxH, (minW * h) / w)];
3051
+ }
3052
+ if (w < minW && h > maxH) return [minW, maxH];
3053
+ if (w > maxW && h < minH) return [maxW, minH];
3054
+ if (w > maxW) return [maxW, Math.max((maxW * h) / w, minH)];
3055
+ if (w < minW) return [minW, Math.min((minW * h) / w, maxH)];
3056
+ if (h > maxH) return [Math.max((maxH * w) / h, minW), maxH];
3057
+ if (h < minH) return [Math.min((minH * w) / h, maxW), minH];
3058
+ return [w, h];
3059
+ }
3060
+
3061
+ /**
3062
+ * Place and size a float, and register it with the formatting context.
3063
+ *
3064
+ * The formatting context holds every float in it, and its band is as wide
3065
+ * as its root; a float is placed within its own containing block's content
3066
+ * box, which may be narrower (CSS 2.1 9.5.1, rules 1 and 7). Floats placed
3067
+ * earlier still push it over wherever they reach into that box. Placed in
3068
+ * the formatting context's band, a float in a nested block sat at the
3069
+ * root's edge — outside its parent's padding and the body's margin.
3070
+ */
638
3071
  function layoutFloat(
639
3072
  box: Box,
640
3073
  ctx: LayoutContext,
641
3074
  floats: FloatContext,
642
3075
  y: number,
3076
+ containingLeft: number,
643
3077
  containingWidth: number,
644
3078
  ): void {
3079
+ sizeFloat(box, ctx, containingWidth);
3080
+ placeFloat(box, floats, y, containingLeft, containingWidth);
3081
+ }
3082
+
3083
+ /** Lay a float out at its own width, and answer its outer width. */
3084
+ function sizeFloat(
3085
+ box: Box,
3086
+ ctx: LayoutContext,
3087
+ containingWidth: number,
3088
+ ): number {
645
3089
  resolveEdges(box, containingWidth);
646
3090
  if (box.kind === 'replaced') sizeReplaced(box, containingWidth);
647
- else {
648
- const width = shrinkToFitWidth(box, ctx, containingWidth);
649
- layoutInternals(box, ctx, width, 0, 0);
650
- }
3091
+ else layoutOwn(box, ctx, shrinkToFitWidth(box, ctx, containingWidth));
3092
+ return box.width + box.marginLeft + box.marginRight;
3093
+ }
3094
+
3095
+ /** Put a float that is laid out as high as it goes from `y`, and to its
3096
+ * side, and note it in the float context. */
3097
+ function placeFloat(
3098
+ box: Box,
3099
+ floats: FloatContext,
3100
+ y: number,
3101
+ containingLeft: number,
3102
+ containingWidth: number,
3103
+ ): void {
651
3104
  const outerWidth = box.width + box.marginLeft + box.marginRight;
652
3105
  const clearance = floats.clearance(box.style.clear);
653
3106
  const from = Math.max(y, clearance === -Infinity ? y : clearance);
3107
+ const left = containingLeft;
3108
+ const right = containingLeft + containingWidth;
654
3109
  const top = floats.placeAt(
655
3110
  from,
656
3111
  outerWidth,
657
3112
  box.style.float === 'right' ? 'right' : 'left',
3113
+ left,
3114
+ right,
658
3115
  );
659
- const band = floats.bandAt(top, 1);
3116
+ const band = floats.bandAt(top, 1, left, right);
660
3117
  const x =
661
3118
  box.style.float === 'right'
662
3119
  ? band.right - outerWidth + box.marginLeft
663
3120
  : band.left + box.marginLeft;
664
- moveTo(box, x, top + box.marginTop);
3121
+ // A right float in content measured at no width limit stands at an
3122
+ // infinite x, where its box is not put: a box at infinity cannot be moved
3123
+ // back, so everything in it was laid out again at its next layout — at
3124
+ // every level of a nest of such floats, the innermost as many times as
3125
+ // there were levels. What a measure reads of it is its width, and the
3126
+ // float context keeps where it stands.
3127
+ moveTo(
3128
+ box,
3129
+ Number.isFinite(x) ? x : band.left + box.marginLeft,
3130
+ top + box.marginTop,
3131
+ );
665
3132
  floats.add({
666
3133
  left: x - box.marginLeft,
667
3134
  right: x - box.marginLeft + outerWidth,
@@ -672,21 +3139,26 @@ function layoutFloat(
672
3139
  }
673
3140
 
674
3141
  /**
675
- * An absolutely positioned box, against its containing block.
676
- *
677
- * The static position — where the box would have been in flow — is not
678
- * tracked: a box with neither `top` nor `bottom` is placed at its containing
679
- * block's content top rather than where its markup sat. That is the one
680
- * deliberate simplification in positioning, and it is invisible for the
681
- * overwhelmingly common `position: absolute` with an explicit offset, which
682
- * is how a badge, a tooltip and an overlay are all written.
3142
+ * An absolutely positioned box, against its containing block: a box's
3143
+ * padding box, or an inline box's fragments (`inlineContainingBlock`). An
3144
+ * axis with neither offset takes the static position, where the box would
3145
+ * have been in flow (`placeStatic`, and `staticPositions` in a line).
683
3146
  */
684
3147
  function layoutPositioned(box: Box, containing: Box, ctx: LayoutContext): void {
685
- const cbWidth = containing.contentWidth;
686
- const cbHeight = containing.contentHeight;
687
- const cbX = containing.contentX;
688
- const cbY = containing.contentY;
3148
+ // a grid's is the box's grid area (CSS Grid 1, 9.1), whether the box is
3149
+ // the grid's child or deeper in it
3150
+ const area =
3151
+ containing.kind === 'inline' ? null : gridArea(box, containing, ctx);
3152
+ const {
3153
+ x: cbX,
3154
+ y: cbY,
3155
+ width: cbWidth,
3156
+ height: cbHeight,
3157
+ } = containing.kind === 'inline'
3158
+ ? inlineContainingBlock(containing, box, ctx)
3159
+ : (area ?? containingRect(containing, ctx));
689
3160
  resolveEdges(box, cbWidth);
3161
+ box.percentHeightBase = cbHeight;
690
3162
 
691
3163
  const style = box.style;
692
3164
  const left = resolveOrNull(style.left, cbWidth);
@@ -694,58 +3166,701 @@ function layoutPositioned(box: Box, containing: Box, ctx: LayoutContext): void {
694
3166
  const top = resolveOrNull(style.top, cbHeight);
695
3167
  const bottom = resolveOrNull(style.bottom, cbHeight);
696
3168
 
3169
+ // with neither offset on an axis, the box is where the flow would have
3170
+ // put it (CSS 2.1 10.3.7, 10.6.4): against its start edge, which is the
3171
+ // right one in a right-to-left flow. A grid's own child is aligned in its
3172
+ // grid area instead, where that is its containing block; a box deeper in
3173
+ // the grid is where its own flow put it.
3174
+ const inArea =
3175
+ area !== null &&
3176
+ box.staticPosition?.inside !== undefined &&
3177
+ box.staticPosition.from === containing;
3178
+ const at = inArea ? null : box.staticPosition;
3179
+ const rtl = (at?.from ?? containing).style.direction === 'rtl';
3180
+ // where the box is as a flex box's or a grid's one item, aligned in it —
3181
+ // or in its grid area
3182
+ const room = inArea
3183
+ ? {
3184
+ x: cbX,
3185
+ y: cbY,
3186
+ width: cbWidth,
3187
+ height: cbHeight,
3188
+ ...staticAlignment(box, containing, false),
3189
+ }
3190
+ : at?.inside
3191
+ ? { ...insideRect(at.from, at.inside.box), ...at.inside }
3192
+ : null;
3193
+ // and a shrink-to-fit box's room starts at the offset on the side it has
3194
+ // one, or at its static position: `left: 50%` leaves it half the width
3195
+ const offset =
3196
+ left ??
3197
+ right ??
3198
+ (!at
3199
+ ? 0
3200
+ : rtl
3201
+ ? cbX + cbWidth - (at.from.x + at.right)
3202
+ : at.from.x + at.x - cbX);
3203
+
697
3204
  let width: number;
3205
+ let stretched = NaN;
698
3206
  if (style.width !== AUTO) {
699
- width = blockWidth(box, cbWidth);
3207
+ width = blockWidth(box, cbWidth, cbWidth, ctx);
3208
+ } else if (style.widthKeyword === 'stretch') {
3209
+ // what its offsets and margins leave of its containing block — an
3210
+ // `auto` offset leaving all it has, but where both are, the start is
3211
+ // its static position (CSS Position 3, 4.1)
3212
+ const start = left ?? (right === null ? offset : 0);
3213
+ const room =
3214
+ cbWidth - start - (right ?? 0) - box.marginLeft - box.marginRight;
3215
+ width = clampWidth(box, Math.max(0, room), cbWidth, ctx);
3216
+ } else if (style.widthKeyword || ratioWidth(box) !== null) {
3217
+ // an intrinsic size, which both offsets do not stretch, or a height of
3218
+ // its own through its ratio
3219
+ width = shrinkToFitWidth(box, ctx, cbWidth, offset);
3220
+ } else if (
3221
+ boxRatio(box) &&
3222
+ (left === null || right === null) &&
3223
+ top !== null &&
3224
+ bottom !== null
3225
+ ) {
3226
+ // the height its offsets leave it through its ratio, where its width
3227
+ // has no pair of them to stretch it (CSS Sizing 4, 5.1)
3228
+ const fill = clampHeight(
3229
+ box,
3230
+ Math.max(0, cbHeight - top - bottom - box.marginTop - box.marginBottom),
3231
+ );
3232
+ width = ratioMinimum(
3233
+ box,
3234
+ clampWidth(box, widthFromHeight(box, fill)!, cbWidth, ctx),
3235
+ cbWidth,
3236
+ ctx,
3237
+ );
700
3238
  } else if (left !== null && right !== null) {
701
- width = Math.max(
3239
+ stretched = Math.max(
702
3240
  0,
703
3241
  cbWidth - left - right - box.marginLeft - box.marginRight,
704
3242
  );
3243
+ // within its least and greatest width, which make it a width as one
3244
+ // set would, and the margins' rules run again with it (CSS 2.1 10.4) —
3245
+ // and those of its height, through a ratio (CSS Sizing 4, 5.1)
3246
+ width = clampWidth(box, transferredWidth(box, stretched), cbWidth, ctx);
705
3247
  } else {
706
- width = shrinkToFitWidth(box, ctx, cbWidth);
3248
+ width = shrinkToFitWidth(box, ctx, cbWidth, offset);
707
3249
  }
708
3250
 
709
- if (box.kind === 'replaced') sizeReplaced(box, cbWidth);
710
- else layoutInternals(box, ctx, width, 0, 0);
3251
+ // `stretch` down: what its offsets leave, from its static position where
3252
+ // it has neither
3253
+ if (
3254
+ style.heightKeyword === 'stretch' ||
3255
+ style.minHeightKeyword === 'stretch'
3256
+ ) {
3257
+ STRETCH_TOP.set(
3258
+ box,
3259
+ top ?? (bottom === null && at ? at.from.y + at.y - cbY : 0),
3260
+ );
3261
+ }
3262
+ if (box.kind === 'replaced') {
3263
+ sizeReplaced(box, cbWidth);
3264
+ stretchReplaced(
3265
+ box,
3266
+ style.widthKeyword === 'stretch' ? width : null,
3267
+ cbWidth,
3268
+ );
3269
+ } else layoutOwn(box, ctx, width);
711
3270
 
712
- const x =
713
- left !== null
714
- ? cbX + left + box.marginLeft
715
- : right !== null
3271
+ let x: number;
3272
+ if (
3273
+ left !== null &&
3274
+ right !== null &&
3275
+ (style.width !== AUTO ||
3276
+ style.widthKeyword !== null ||
3277
+ box.kind === 'replaced' ||
3278
+ width !== stretched)
3279
+ ) {
3280
+ // Both offsets and a width: what is left over goes to the margins that
3281
+ // are `auto`, shared where both are, and where none is, the end offset
3282
+ // gives way (10.3.7, 10.3.8).
3283
+ const rest = cbWidth - left - right - box.width;
3284
+ const autoLeft = style.marginLeft === AUTO;
3285
+ const autoRight = style.marginRight === AUTO;
3286
+ const endRtl = containing.style.direction === 'rtl';
3287
+ if (autoLeft && autoRight) {
3288
+ if (rest >= 0) {
3289
+ box.marginLeft = rest / 2;
3290
+ box.marginRight = rest / 2;
3291
+ } else if (endRtl) {
3292
+ box.marginRight = 0;
3293
+ box.marginLeft = rest;
3294
+ } else {
3295
+ box.marginLeft = 0;
3296
+ box.marginRight = rest;
3297
+ }
3298
+ } else if (autoLeft) {
3299
+ box.marginLeft = rest - box.marginRight;
3300
+ } else if (autoRight) {
3301
+ box.marginRight = rest - box.marginLeft;
3302
+ }
3303
+ x =
3304
+ endRtl && !autoLeft && !autoRight
716
3305
  ? cbX + cbWidth - right - box.width - box.marginRight
717
- : cbX;
718
- const y =
719
- top !== null
720
- ? cbY + top + box.marginTop
721
- : bottom !== null
722
- ? cbY + cbHeight - bottom - box.height - box.marginBottom
723
- : cbY;
3306
+ : cbX + left + box.marginLeft;
3307
+ } else {
3308
+ x =
3309
+ left !== null
3310
+ ? cbX + left + box.marginLeft
3311
+ : right !== null
3312
+ ? cbX + cbWidth - right - box.width - box.marginRight
3313
+ : room
3314
+ ? alignedIn(
3315
+ room.x,
3316
+ room.width,
3317
+ room.across,
3318
+ box.width + box.marginLeft + box.marginRight,
3319
+ rtl,
3320
+ ) + box.marginLeft
3321
+ : rtl
3322
+ ? (at ? at.from.x + at.right : cbX + cbWidth) -
3323
+ box.width -
3324
+ box.marginRight
3325
+ : (at ? at.from.x + at.x : cbX) + box.marginLeft;
3326
+ }
3327
+ let y: number;
3328
+ if (top !== null && bottom !== null) {
3329
+ const fill = Math.max(
3330
+ 0,
3331
+ cbHeight - top - bottom - box.marginTop - box.marginBottom,
3332
+ );
3333
+ // — a box with a ratio has its height from its width
3334
+ const stretches =
3335
+ style.height === AUTO && box.kind !== 'replaced' && !boxRatio(box);
3336
+ // both offsets and no height: the box fills what they leave, its
3337
+ // `auto` margins nothing (10.6.4, rule 5) — unless `min-height` or
3338
+ // `max-height` moves that, which makes it a height like one set, and
3339
+ // the rules run again with it (10.7)
3340
+ if (stretches) box.height = clampHeight(box, fill);
3341
+ if (!stretches || box.height !== fill) {
3342
+ // and a height: the `auto` margins share the rest, and where none
3343
+ // is `auto`, `bottom` gives way (10.6.4, 10.6.5)
3344
+ const rest = cbHeight - top - bottom - box.height;
3345
+ const autoTop = style.marginTop === AUTO;
3346
+ const autoBottom = style.marginBottom === AUTO;
3347
+ if (autoTop && autoBottom) {
3348
+ box.marginTop = rest / 2;
3349
+ box.marginBottom = rest / 2;
3350
+ } else if (autoTop) {
3351
+ box.marginTop = rest - box.marginBottom;
3352
+ } else if (autoBottom) {
3353
+ box.marginBottom = rest - box.marginTop;
3354
+ }
3355
+ }
3356
+ y = cbY + top + box.marginTop;
3357
+ } else {
3358
+ y =
3359
+ top !== null
3360
+ ? cbY + top + box.marginTop
3361
+ : bottom !== null
3362
+ ? cbY + cbHeight - bottom - box.height - box.marginBottom
3363
+ : room
3364
+ ? alignedIn(
3365
+ room.y,
3366
+ room.height,
3367
+ room.down,
3368
+ box.height + box.marginTop + box.marginBottom,
3369
+ false,
3370
+ ) + box.marginTop
3371
+ : (at ? at.from.y + at.y : cbY) + box.marginTop;
3372
+ }
724
3373
  moveTo(box, x, y);
725
3374
  }
726
3375
 
3376
+ /** A box's content box or its padding box, as its layout left them. */
3377
+ function insideRect(
3378
+ box: Box,
3379
+ which: 'content' | 'padding',
3380
+ ): { x: number; y: number; width: number; height: number } {
3381
+ if (which === 'content') {
3382
+ return {
3383
+ x: box.contentX,
3384
+ y: box.contentY,
3385
+ width: box.contentWidth,
3386
+ height: Math.max(0, box.height - box.verticalExtra),
3387
+ };
3388
+ }
3389
+ return {
3390
+ x: box.x + box.borderLeft,
3391
+ y: box.y + box.borderTop,
3392
+ width: Math.max(0, box.width - box.borderLeft - box.borderRight),
3393
+ height: Math.max(0, box.height - box.borderTop - box.borderBottom),
3394
+ };
3395
+ }
3396
+
3397
+ /** Where a margin box of a size starts in a room, `along` of the free room
3398
+ * before it from the room's start — its right end where `rtl`. */
3399
+ function alignedIn(
3400
+ start: number,
3401
+ size: number,
3402
+ along: number,
3403
+ outer: number,
3404
+ rtl: boolean,
3405
+ ): number {
3406
+ const free = size - outer;
3407
+ return rtl ? start + size - outer - along * free : start + along * free;
3408
+ }
3409
+
3410
+ /**
3411
+ * The grid area a box positioned in a grid takes for its containing block
3412
+ * (CSS Grid 1, 9.1): between the lines its placement names, where the grid
3413
+ * has them, and the grid's padding edge where a line is `auto`, is no line
3414
+ * of the grid, or is only a `span`. Null for a box whose containing block
3415
+ * is no grid.
3416
+ */
3417
+ function gridArea(
3418
+ box: Box,
3419
+ containing: Box,
3420
+ ctx: LayoutContext,
3421
+ ): CbRect | null {
3422
+ const tracks = GRID_TRACKS.get(containing);
3423
+ if (!tracks) return null;
3424
+ const style = box.style;
3425
+ const pad = containingRect(containing, ctx);
3426
+ const [x, width] = areaSpan(
3427
+ style.gridColumnStart,
3428
+ style.gridColumnEnd,
3429
+ tracks.cols,
3430
+ tracks.colLines,
3431
+ containing.contentX,
3432
+ pad.x,
3433
+ pad.width,
3434
+ );
3435
+ const [y, height] = areaSpan(
3436
+ style.gridRowStart,
3437
+ style.gridRowEnd,
3438
+ tracks.rows,
3439
+ tracks.rowLines,
3440
+ containing.contentY,
3441
+ pad.y,
3442
+ pad.height,
3443
+ );
3444
+ return { x, y, width, height };
3445
+ }
3446
+
3447
+ /** One axis of a grid area: its start and its size in document
3448
+ * coordinates. A line is a track's start where it starts one and a
3449
+ * track's end where it ends one, so a gap is in neither; a line by name
3450
+ * is the one the placement would take. */
3451
+ function areaSpan(
3452
+ start: GridLine,
3453
+ end: GridLine,
3454
+ tracks: readonly [number, number][],
3455
+ lines: GridLines,
3456
+ origin: number,
3457
+ edge: number,
3458
+ size: number,
3459
+ ): [number, number] {
3460
+ const count = tracks.length;
3461
+ // a line the grid has, or null for auto, a span and one it has not
3462
+ const at = (line: GridLine, side: 'start' | 'end'): number | null => {
3463
+ const index = lineOf(lines, line, side);
3464
+ return index !== null && count > 0 && index >= 0 && index <= count
3465
+ ? index
3466
+ : null;
3467
+ };
3468
+ const spanOf = (line: GridLine) => (line && 'span' in line ? line : null);
3469
+ let from = at(start, 'start');
3470
+ let to = at(end, 'end');
3471
+ const startSpan = spanOf(start);
3472
+ const endSpan = spanOf(end);
3473
+ if (from === null && to !== null && startSpan) {
3474
+ const f =
3475
+ startSpan.name !== undefined
3476
+ ? spanToName(lines, startSpan.name, startSpan.span, to, false)
3477
+ : to - startSpan.span;
3478
+ from = f >= 0 ? f : null;
3479
+ }
3480
+ if (to === null && from !== null && endSpan) {
3481
+ const t =
3482
+ endSpan.name !== undefined
3483
+ ? spanToName(lines, endSpan.name, endSpan.span, from, true)
3484
+ : from + endSpan.span;
3485
+ to = t <= count ? t : null;
3486
+ }
3487
+ if (from !== null && to !== null && to < from) [from, to] = [to, from];
3488
+ const a =
3489
+ from === null
3490
+ ? edge
3491
+ : origin + (from < count ? tracks[from][0] : tracks[from - 1][1]);
3492
+ const b =
3493
+ to === null || to === from
3494
+ ? edge + size
3495
+ : origin + (to >= 1 ? tracks[to - 1][1] : tracks[0][0]);
3496
+ return [a, Math.max(0, b - a)];
3497
+ }
3498
+
3499
+ /** A containing block's rectangle, in document coordinates. */
3500
+ interface CbRect {
3501
+ x: number;
3502
+ y: number;
3503
+ width: number;
3504
+ height: number;
3505
+ }
3506
+
3507
+ /**
3508
+ * The containing block a box that lays itself out gives: its padding box
3509
+ * (CSS 2.1 10.1), not its content box — `left: 0` in a padded box is at
3510
+ * its padding edge — and with nothing positioned around it the initial
3511
+ * one, as tall as the viewport rather than as the document, which a fixed
3512
+ * box's is too.
3513
+ */
3514
+ function containingRect(containing: Box, ctx: LayoutContext): CbRect {
3515
+ // a layout that reads the viewport's height lays out differently in
3516
+ // another (`LayoutResult.readsViewportHeight`)
3517
+ if (!containing.parent) ctx.readViewportHeight = true;
3518
+ return {
3519
+ x: containing.x + containing.borderLeft,
3520
+ y: containing.y + containing.captionTop + containing.borderTop,
3521
+ width: Math.max(
3522
+ 0,
3523
+ containing.width - containing.borderLeft - containing.borderRight,
3524
+ ),
3525
+ height: !containing.parent
3526
+ ? ctx.viewportHeight
3527
+ : Math.max(
3528
+ 0,
3529
+ containing.height -
3530
+ containing.captionTop -
3531
+ containing.captionBottom -
3532
+ containing.borderTop -
3533
+ containing.borderBottom,
3534
+ ),
3535
+ };
3536
+ }
3537
+
3538
+ /**
3539
+ * The containing block a `position: relative` inline box gives (CSS 2.1
3540
+ * 10.1, item 4): from the padding edge its first fragment starts at to the
3541
+ * one its last ends at, over the top of the first and the bottom of the
3542
+ * last. The box lays nothing out of its own — its fragments are on its
3543
+ * block's lines — so read as a box that does, it was a rectangle of no size
3544
+ * at the page's corner, and a tooltip under a positioned link came up
3545
+ * there. A box with no fragment at all stands where the box inside it
3546
+ * would have been in flow.
3547
+ */
3548
+ function inlineContainingBlock(
3549
+ inline: Box,
3550
+ box: Box,
3551
+ ctx: LayoutContext,
3552
+ ): CbRect {
3553
+ let block = inline.parent;
3554
+ while (block && block.kind === 'inline') block = block.parent;
3555
+ const face = ctx.fonts ? faceExtentOf(ctx.fonts, inline.style) : null;
3556
+ let first: CbRect | null = null;
3557
+ let last: CbRect | null = null;
3558
+ for (const line of block?.lines ?? []) {
3559
+ const span = spanOn(line, inline);
3560
+ if (!span) continue;
3561
+ let top = line.y;
3562
+ let bottom = line.y + line.height;
3563
+ if (face) {
3564
+ const baseline =
3565
+ line.y +
3566
+ line.baseline -
3567
+ (LINE_BOX_RAISES.get(line)?.get(inline) ?? BOX_RAISES.get(inline) ?? 0);
3568
+ top = baseline - face.ascent - inline.padTop;
3569
+ bottom = baseline + face.descent + inline.padBottom;
3570
+ }
3571
+ const rect = {
3572
+ x: span[0],
3573
+ y: top,
3574
+ width: span[1] - span[0],
3575
+ height: bottom - top,
3576
+ };
3577
+ first ??= rect;
3578
+ last = rect;
3579
+ }
3580
+ if (!first || !last) {
3581
+ const at = box.staticPosition;
3582
+ const x = at ? at.from.x + at.x : 0;
3583
+ const y = at ? at.from.y + at.y : 0;
3584
+ const height = face ? face.ascent + face.descent : 0;
3585
+ return { x, y, width: 0, height };
3586
+ }
3587
+ const rtl = inline.style.direction === 'rtl';
3588
+ const start = rtl ? first.x + first.width : first.x;
3589
+ const end = rtl ? last.x : last.x + last.width;
3590
+ let x = Math.min(start, end);
3591
+ let width = Math.abs(end - start);
3592
+ if (rtl ? end > start : end < start) {
3593
+ // the last fragment ends before the first begins, which CSS 2.1 leaves
3594
+ // undefined: both, then, as a browser takes them
3595
+ x = Math.min(first.x, last.x);
3596
+ width = Math.max(first.x + first.width, last.x + last.width) - x;
3597
+ }
3598
+ return { x, y: first.y, width, height: last.y + last.height - first.y };
3599
+ }
3600
+
3601
+ /** How far an inline box's padding box reaches across a line, left and
3602
+ * right; null where nothing of it is on the line. */
3603
+ function spanOn(line: LineBox, inline: Box): [number, number] | null {
3604
+ let left = Infinity;
3605
+ let right = -Infinity;
3606
+ for (const text of line.texts) {
3607
+ const natural = text.layout.lines[text.layoutLine];
3608
+ const boxAt = text.spans.boxAt;
3609
+ if (!natural || !boxAt) continue;
3610
+ const x = text.drawX + natural.x;
3611
+ for (const run of natural.runs) {
3612
+ const owner = boxAt.call(text.spans, run.start);
3613
+ if (!owner || !holds(inline, owner)) continue;
3614
+ left = Math.min(left, x + run.x);
3615
+ right = Math.max(right, x + run.x + run.width);
3616
+ }
3617
+ }
3618
+ for (const placed of line.atomics) {
3619
+ if (!holds(inline, placed.box)) continue;
3620
+ left = Math.min(left, placed.x - placed.box.marginLeft);
3621
+ right = Math.max(
3622
+ right,
3623
+ placed.x + placed.box.width + placed.box.marginRight,
3624
+ );
3625
+ }
3626
+ for (const edge of line.edges ?? []) {
3627
+ if (edge.box !== inline) {
3628
+ if (!holds(inline, edge.box)) continue;
3629
+ left = Math.min(left, edge.x);
3630
+ right = Math.max(right, edge.x + edge.width);
3631
+ continue;
3632
+ }
3633
+ // its own: the padding edge, inside the margin and the border
3634
+ const onLeft =
3635
+ (edge.side === 'start') !== (inline.style.direction === 'rtl');
3636
+ if (onLeft) {
3637
+ const at = edge.x + inline.marginLeft + inline.borderLeft;
3638
+ left = Math.min(left, at);
3639
+ right = Math.max(right, at);
3640
+ } else {
3641
+ const at = edge.x + edge.width - inline.marginRight - inline.borderRight;
3642
+ left = Math.min(left, at);
3643
+ right = Math.max(right, at);
3644
+ }
3645
+ }
3646
+ return left <= right ? [left, right] : null;
3647
+ }
3648
+
3649
+ /**
3650
+ * Lay out the floats in a block's inline content and give its positioned
3651
+ * boxes their static positions — at any depth of inline box, which lays
3652
+ * nothing out of its own: a float or an absolute box inside a `<span>` is
3653
+ * the paragraph's as much as one beside it. Met only among the block's own
3654
+ * children, one in an inline box was never laid out at all, and stood at
3655
+ * the page's corner with no size.
3656
+ */
3657
+ /** Give the absolute boxes in a block's inline content their static
3658
+ * position, and answer whether the content holds a float. */
3659
+ function placeOutOfLine(
3660
+ parent: Box,
3661
+ block: Box,
3662
+ ctx: LayoutContext,
3663
+ contentTop: number,
3664
+ contentWidth: number,
3665
+ contentLeft: number,
3666
+ ): boolean {
3667
+ let floated = false;
3668
+ for (const child of parent.children) {
3669
+ if (child.outOfFlow) {
3670
+ placeStatic(child, block, contentLeft, contentWidth, contentTop);
3671
+ ctx.positioned.push({
3672
+ box: child,
3673
+ containing: containingBlockFor(child) ?? block,
3674
+ });
3675
+ } else if (child.isFloat) {
3676
+ floated = true;
3677
+ } else if (child.kind === 'inline' && ctx.nestedOutOfLine) {
3678
+ const inner = placeOutOfLine(
3679
+ child,
3680
+ block,
3681
+ ctx,
3682
+ contentTop,
3683
+ contentWidth,
3684
+ contentLeft,
3685
+ );
3686
+ floated ||= inner;
3687
+ }
3688
+ }
3689
+ return floated;
3690
+ }
3691
+
3692
+ /** Where an out-of-flow box would have gone in its parent's flow: its
3693
+ * margin edge's, at either side, kept from the parent's corner, which may
3694
+ * yet move. */
3695
+ function placeStatic(
3696
+ box: Box,
3697
+ parent: Box,
3698
+ x: number,
3699
+ width: number,
3700
+ y: number,
3701
+ ): void {
3702
+ box.staticPosition = {
3703
+ from: parent,
3704
+ x: x - parent.x,
3705
+ right: x + width - parent.x,
3706
+ y: y - parent.y,
3707
+ };
3708
+ }
3709
+
3710
+ /**
3711
+ * An absolutely positioned child of a flex box or a grid, laid out once the
3712
+ * flow is: against its own containing block, which the box is only where
3713
+ * it is positioned, from where it is as the box's one item — at the start
3714
+ * of a flex box's content box (CSS Flexbox 4.1), of a grid's padding box
3715
+ * (CSS Grid 1, 9.1). Pushed against the box whatever it was, one in a
3716
+ * static flex box was placed from the box's corner, and `fixed` one from
3717
+ * a grid.
3718
+ */
3719
+ export function positionOutOfFlow(
3720
+ child: Box,
3721
+ container: Box,
3722
+ ctx: LayoutContext,
3723
+ flex: boolean,
3724
+ ): void {
3725
+ if (flex) {
3726
+ placeStatic(
3727
+ child,
3728
+ container,
3729
+ container.contentX,
3730
+ container.contentWidth,
3731
+ container.contentY,
3732
+ );
3733
+ } else {
3734
+ placeStatic(
3735
+ child,
3736
+ container,
3737
+ container.x + container.borderLeft,
3738
+ container.width - container.borderLeft - container.borderRight,
3739
+ container.y + container.borderTop,
3740
+ );
3741
+ }
3742
+ child.staticPosition!.inside = {
3743
+ box: flex ? 'content' : 'padding',
3744
+ ...staticAlignment(child, container, flex),
3745
+ };
3746
+ ctx.positioned.push({
3747
+ box: child,
3748
+ containing: containingBlockFor(child) ?? container,
3749
+ });
3750
+ }
3751
+
3752
+ /** How far along its free room a flex box's or a grid's one item is set:
3753
+ * 0 at the start, ½ in the middle, 1 at the end, across (from the inline
3754
+ * start) and down. A flex box's main axis by `justify-content`, and the
3755
+ * cross by `align-self`; a grid's by `justify-self` and `align-self`,
3756
+ * each falling back to the container's `-items`. */
3757
+ function staticAlignment(
3758
+ child: Box,
3759
+ container: Box,
3760
+ flex: boolean,
3761
+ ): { across: number; down: number } {
3762
+ const own = child.style;
3763
+ const style = container.style;
3764
+ const factor = (align: string): number =>
3765
+ align === 'flex-end' ? 1 : align === 'center' ? 0.5 : 0;
3766
+ const cross = factor(
3767
+ own.alignSelf === AUTO ? style.alignItems : own.alignSelf,
3768
+ );
3769
+ if (!flex) {
3770
+ const justify =
3771
+ own.justifySelf === 'auto' ? style.justifyItems : own.justifySelf;
3772
+ return { across: factor(justify), down: cross };
3773
+ }
3774
+ const justify = style.justifyContent;
3775
+ let main =
3776
+ justify === 'flex-end' || justify === 'end' || justify === 'right'
3777
+ ? 1
3778
+ : justify === 'center' ||
3779
+ justify === 'space-around' ||
3780
+ justify === 'space-evenly'
3781
+ ? 0.5
3782
+ : 0;
3783
+ if (style.flexDirection.endsWith('-reverse')) main = 1 - main;
3784
+ return style.flexDirection.startsWith('row')
3785
+ ? { across: main, down: cross }
3786
+ : { across: cross, down: main };
3787
+ }
3788
+
727
3789
  /** The nearest positioned ancestor, or null for the initial containing
728
3790
  * block. Walks the box tree rather than the DOM, so an anonymous box in
729
3791
  * between is transparent — which is what the spec means by "the nearest
730
3792
  * positioned ancestor". */
731
3793
  function containingBlockFor(box: Box): Box | null {
3794
+ // a fixed box's is the viewport, whatever is positioned around it — but
3795
+ // a transformed box is one for it too, and one with layout or paint
3796
+ // containment (CSS Containment 2, 3.3)
3797
+ const fixed = box.style.position === 'fixed';
732
3798
  let node = box.parent;
733
3799
  while (node) {
734
- if (node.style.position !== 'static' || node.parent === null) return node;
3800
+ if (
3801
+ (!fixed && node.style.position !== 'static') ||
3802
+ transformed(node.style) ||
3803
+ contained(node, CONTAIN_LAYOUT | CONTAIN_PAINT) ||
3804
+ node.parent === null
3805
+ ) {
3806
+ return node;
3807
+ }
735
3808
  node = node.parent;
736
3809
  }
737
3810
  return null;
738
3811
  }
739
3812
 
3813
+ /** The size containments that hold a box's width: `size` and
3814
+ * `inline-size`. */
3815
+ const CONTAIN_WIDTH = CONTAIN_SIZE | CONTAIN_INLINE_SIZE;
3816
+
3817
+ /**
3818
+ * Whether a box has any of the containments `bits` names, as they apply
3819
+ * to it (CSS Containment 2, 3): none to an inline box that is no atomic
3820
+ * one, and none to a table's inner parts but a cell — nor size
3821
+ * containment to a table or a cell.
3822
+ */
3823
+ export function contained(box: Box, bits: number): boolean {
3824
+ if (!(box.style.contain & bits)) return false;
3825
+ switch (box.kind) {
3826
+ case 'block':
3827
+ case 'flex':
3828
+ case 'replaced':
3829
+ return true;
3830
+ case 'table':
3831
+ case 'table-cell':
3832
+ return !!(box.style.contain & bits & ~CONTAIN_WIDTH);
3833
+ default:
3834
+ return false;
3835
+ }
3836
+ }
3837
+
740
3838
  /** Move a box and everything under it, keeping the subtree's shape. */
741
3839
  export function moveTo(box: Box, x: number, y: number): void {
3840
+ // A position only a probe of an unbounded width comes to — a column after
3841
+ // an infinitely wide one — is no place to move a box to: it stays where
3842
+ // it is, since the probe only wants its size, and the pass after moves it
3843
+ // from somewhere finite (see `placeBlock`).
3844
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return;
742
3845
  translate(box, x - box.x, y - box.y);
743
3846
  }
744
3847
 
3848
+ /** Move what a box holds down, and not the box: a table cell's content
3849
+ * sits where its `vertical-align` puts it, in a box that fills the row. */
3850
+ export function moveContent(box: Box, dy: number): void {
3851
+ translate(box, 0, dy);
3852
+ box.y -= dy;
3853
+ }
3854
+
745
3855
  function translate(box: Box, dx: number, dy: number): void {
746
3856
  if (!dx && !dy) return;
747
3857
  box.x += dx;
748
3858
  box.y += dy;
3859
+ // a list item's marker is placed in the same coordinates as its lines
3860
+ if (box.marker) {
3861
+ box.marker.x += dx;
3862
+ box.marker.y += dy;
3863
+ }
749
3864
  if (box.lines) {
750
3865
  for (const line of box.lines) {
751
3866
  line.x += dx;
@@ -758,6 +3873,7 @@ function translate(box: Box, dx: number, dy: number): void {
758
3873
  placed.x += dx;
759
3874
  placed.y += dy;
760
3875
  }
3876
+ for (const edge of line.edges ?? []) edge.x += dx;
761
3877
  }
762
3878
  }
763
3879
  for (const child of box.children) translate(child, dx, dy);
@@ -784,23 +3900,42 @@ export function resolveEdges(box: Box, containingWidth: number): void {
784
3900
  style.borderLeftStyle === 'none' || style.borderLeftStyle === 'hidden'
785
3901
  ? 0
786
3902
  : style.borderLeftWidth;
787
- box.padTop = edge(style.paddingTop, containingWidth);
788
- box.padRight = edge(style.paddingRight, containingWidth);
789
- box.padBottom = edge(style.paddingBottom, containingWidth);
790
- box.padLeft = edge(style.paddingLeft, containingWidth);
3903
+ // at least zero: a `calc()` may come to less
3904
+ box.padTop = Math.max(0, edge(style.paddingTop, containingWidth));
3905
+ box.padRight = Math.max(0, edge(style.paddingRight, containingWidth));
3906
+ box.padBottom = Math.max(0, edge(style.paddingBottom, containingWidth));
3907
+ box.padLeft = Math.max(0, edge(style.paddingLeft, containingWidth));
791
3908
  box.marginTop = edge(style.marginTop, containingWidth);
792
3909
  box.marginRight = edge(style.marginRight, containingWidth);
793
3910
  box.marginBottom = edge(style.marginBottom, containingWidth);
794
3911
  box.marginLeft = edge(style.marginLeft, containingWidth);
3912
+ box.bottomStrut = null;
3913
+ // A table's parts but its caption have no margins, and its rows and row
3914
+ // groups no padding either (CSS 2.1 8.3, 8.4): a cell set `margin: 50px`
3915
+ // left a gap in its table that no browser draws. The values stay theirs,
3916
+ // for a cell to inherit (`padding: inherit`); they only do nothing here.
3917
+ if (
3918
+ box.kind === 'table-cell' ||
3919
+ box.kind === 'table-row' ||
3920
+ box.kind === 'table-row-group'
3921
+ ) {
3922
+ box.marginTop = 0;
3923
+ box.marginRight = 0;
3924
+ box.marginBottom = 0;
3925
+ box.marginLeft = 0;
3926
+ if (box.kind !== 'table-cell') {
3927
+ box.padTop = 0;
3928
+ box.padRight = 0;
3929
+ box.padBottom = 0;
3930
+ box.padLeft = 0;
3931
+ }
3932
+ }
3933
+ if (box.kind === 'table' || box.kind === 'table-cell') collapseEdges(box);
795
3934
  }
796
3935
 
797
3936
  function edge(len: Len, containingWidth: number): number {
798
3937
  if (len === AUTO) return 0;
799
- if (isPct(len))
800
- return Number.isFinite(containingWidth)
801
- ? (len.pct / 100) * containingWidth
802
- : 0;
803
- return len;
3938
+ return resolve(len, containingWidth, 0);
804
3939
  }
805
3940
 
806
3941
  /** Whether every in-flow child is inline-level, which is what makes this box
@@ -816,9 +3951,6 @@ function establishesInlineContext(box: Box): boolean {
816
3951
  case 'break':
817
3952
  sawInline = true;
818
3953
  break;
819
- case 'replaced':
820
- sawInline = true;
821
- break;
822
3954
  default:
823
3955
  if (isInlineLevel(child)) sawInline = true;
824
3956
  else return false;
@@ -834,7 +3966,9 @@ function isInlineLevel(box: Box): boolean {
834
3966
  d === 'inline' ||
835
3967
  d === 'inline-block' ||
836
3968
  d === 'inline-flex' ||
837
- d === 'inline-table'
3969
+ d === 'inline-table' ||
3970
+ // an image told to be a table's part is inline (CSS Display 3, 2.4)
3971
+ (box.kind === 'replaced' && d.startsWith('table-'))
838
3972
  );
839
3973
  }
840
3974
 
@@ -842,8 +3976,13 @@ function isInlineLevel(box: Box): boolean {
842
3976
  * floats, and does not collapse margins through its edges. */
843
3977
  export function establishesBFC(box: Box): boolean {
844
3978
  const style = box.style;
845
- if (style.overflowX !== 'visible' || style.overflowY !== 'visible')
846
- return true;
3979
+ if (scrolls(style)) return true;
3980
+ if (style.flowRoot) return true;
3981
+ // layout and paint containment make an independent formatting context
3982
+ if (contained(box, CONTAIN_LAYOUT | CONTAIN_PAINT)) return true;
3983
+ // `continue: collapse` makes a block container a formatting context of
3984
+ // its own (CSS Overflow 4, 5.3)
3985
+ if (style.lineClamp !== null) return true;
847
3986
  if (style.float !== 'none') return true;
848
3987
  if (style.position === 'absolute' || style.position === 'fixed') return true;
849
3988
  if (
@@ -853,24 +3992,111 @@ export function establishesBFC(box: Box): boolean {
853
3992
  ) {
854
3993
  return true;
855
3994
  }
856
- if (box.kind === 'table-cell' || box.kind === 'table') return true;
3995
+ if (
3996
+ box.kind === 'table-cell' ||
3997
+ box.kind === 'table-caption' ||
3998
+ box.kind === 'table'
3999
+ ) {
4000
+ return true;
4001
+ }
4002
+ // The root element establishes the document's formatting context. Here it
4003
+ // is a box below the synthetic initial containing block, so it is named:
4004
+ // without it, <html>'s own margin collapsed with <body>'s first block's.
4005
+ if (box.el?.name === 'html') return true;
857
4006
  return box.parent === null;
858
4007
  }
859
4008
 
860
4009
  /** What a box's `position: relative` offset moves it by, applied after
861
4010
  * layout so it does not affect anything else's position — which is the
862
- * whole of what makes it *relative*. */
4011
+ * whole of what makes it *relative* — and its translation with it. */
863
4012
  export function applyRelativeOffsets(box: Box): void {
864
4013
  for (const child of box.children) applyRelativeOffsets(child);
865
4014
  const style = box.style;
4015
+ if (transformed(style) && box.kind !== 'inline') {
4016
+ // a transform moves the box it is on, and an inline box is none
4017
+ const [dx, dy] = translationOf(box);
4018
+ if (dx || dy) translate(box, dx, dy);
4019
+ }
866
4020
  if (style.position !== 'relative' && style.position !== 'sticky') return;
4021
+ const [dx, dy] = relativeOffset(box);
4022
+ translate(box, dx, dy);
4023
+ if (box.kind !== 'inline' || !(dx || dy)) return;
4024
+ offsetInline(box, dx, dy);
4025
+ // the blocks it was broken around move with it (9.2.1.1)
4026
+ const blocks = box.cut ? CUT_BLOCKS.get(box) : undefined;
4027
+ if (blocks) for (const block of blocks) translate(block, dx, dy);
4028
+ }
4029
+
4030
+ /** Whether a box is transformed, which is what makes it a containing
4031
+ * block and a layer as a positioned box is (CSS Transforms 1, 2). */
4032
+ export function transformed(style: ComputedStyle): boolean {
4033
+ return style.translate !== null || style.transformTranslate !== null;
4034
+ }
4035
+
4036
+ /** How far `translate` and a `transform` move a box: a percentage is of
4037
+ * its own border box (CSS Transforms 1, 7). */
4038
+ function translationOf(box: Box): [number, number] {
4039
+ let dx = 0;
4040
+ let dy = 0;
4041
+ for (const moved of [box.style.translate, box.style.transformTranslate]) {
4042
+ if (!moved) continue;
4043
+ dx += resolve(moved[0], box.width, 0);
4044
+ dy += resolve(moved[1], box.height, 0);
4045
+ }
4046
+ return [dx, dy];
4047
+ }
4048
+
4049
+ /** How far `position: relative` moves a box. */
4050
+ function relativeOffset(box: Box): [number, number] {
4051
+ const style = box.style;
867
4052
  const parentWidth = box.parent ? box.parent.contentWidth : 0;
868
- const parentHeight = box.parent ? box.parent.contentHeight : 0;
869
4053
  const left = resolveOrNull(style.left, parentWidth);
870
4054
  const right = resolveOrNull(style.right, parentWidth);
871
- const top = resolveOrNull(style.top, parentHeight);
872
- const bottom = resolveOrNull(style.bottom, parentHeight);
873
- const dx = left ?? (right !== null ? -right : 0);
4055
+ // a percentage down is of a height the containing block sets, as a
4056
+ // percentage height is, and `auto` where its content decides it
4057
+ const top = resolveOrNull(style.top, box.percentHeightBase);
4058
+ const bottom = resolveOrNull(style.bottom, box.percentHeightBase);
4059
+ // both set is over-constrained, and the containing block's direction
4060
+ // says which wins: `left` left to right, `right` right to left (9.4.3)
4061
+ const rtl = box.parent?.style.direction === 'rtl';
4062
+ const dx = right !== null && (left === null || rtl) ? -right : (left ?? 0);
874
4063
  const dy = top ?? (bottom !== null ? -bottom : 0);
875
- translate(box, dx, dy);
4064
+ return [dx, dy];
4065
+ }
4066
+
4067
+ /**
4068
+ * Move an inline box's text where `position: relative` puts the box. Its
4069
+ * text is on its block's lines, laid out apart from the text around it
4070
+ * (`collect`), and those fragments are what move; the lines stay where
4071
+ * they are (CSS 2.1 9.4.3). A box inside it has moved its own already, so
4072
+ * a text in both moves by the two together.
4073
+ */
4074
+ function offsetInline(box: Box, dx: number, dy: number): void {
4075
+ let block = box.parent;
4076
+ while (block && block.kind === 'inline') block = block.parent;
4077
+ const lines = block?.lines;
4078
+ if (!lines) return;
4079
+ INLINE_OFFSETS.set(box, { x: dx, y: dy });
4080
+ for (const line of lines) {
4081
+ for (const text of line.texts) {
4082
+ const owner = text.spans.boxAt?.(text.layoutStart);
4083
+ if (!owner || !holds(box, owner)) continue;
4084
+ text.drawX += dx;
4085
+ text.drawY += dy;
4086
+ const was = TEXT_SHIFTS.get(text);
4087
+ TEXT_SHIFTS.set(text, {
4088
+ x: (was?.x ?? 0) + dx,
4089
+ y: (was?.y ?? 0) + dy,
4090
+ });
4091
+ SHIFTED_LINES.add(line);
4092
+ }
4093
+ }
4094
+ }
4095
+
4096
+ /** Whether `inner` is `outer` or inside it. */
4097
+ function holds(outer: Box, inner: Box): boolean {
4098
+ for (let at: Box | null = inner; at; at = at.parent) {
4099
+ if (at === outer) return true;
4100
+ }
4101
+ return false;
876
4102
  }