@react-x11/components 0.9.0 → 0.11.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 (579) hide show
  1. package/README.md +10 -4
  2. package/dist/charts/node.d.ts +11 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +44 -14
  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 +27 -24
  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/index.d.ts +1 -1
  24. package/dist/code-language/index.d.ts.map +1 -1
  25. package/dist/code-language/index.js +1 -1
  26. package/dist/code-language/index.js.map +1 -1
  27. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  28. package/dist/code-language/languages/glsl.js +10 -2
  29. package/dist/code-language/languages/glsl.js.map +1 -1
  30. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  31. package/dist/code-language/languages/javascript.js +18 -3
  32. package/dist/code-language/languages/javascript.js.map +1 -1
  33. package/dist/code-language/languages/json.d.ts.map +1 -1
  34. package/dist/code-language/languages/json.js +10 -2
  35. package/dist/code-language/languages/json.js.map +1 -1
  36. package/dist/code-language/languages/shell.d.ts.map +1 -1
  37. package/dist/code-language/languages/shell.js +10 -2
  38. package/dist/code-language/languages/shell.js.map +1 -1
  39. package/dist/code-language/languages/sql.d.ts.map +1 -1
  40. package/dist/code-language/languages/sql.js +20 -2
  41. package/dist/code-language/languages/sql.js.map +1 -1
  42. package/dist/code-language/runs.d.ts +31 -0
  43. package/dist/code-language/runs.d.ts.map +1 -1
  44. package/dist/code-language/runs.js +105 -0
  45. package/dist/code-language/runs.js.map +1 -1
  46. package/dist/code-language/stream.d.ts +22 -0
  47. package/dist/code-language/stream.d.ts.map +1 -1
  48. package/dist/code-language/stream.js +259 -9
  49. package/dist/code-language/stream.js.map +1 -1
  50. package/dist/code-language/types.d.ts +5 -4
  51. package/dist/code-language/types.d.ts.map +1 -1
  52. package/dist/codeblock/index.d.ts +26 -2
  53. package/dist/codeblock/index.d.ts.map +1 -1
  54. package/dist/codeblock/index.js +57 -4
  55. package/dist/codeblock/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 +262 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1373 -67
  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 +501 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3729 -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 +65 -16
  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 +3077 -219
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +367 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1742 -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 +154 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3598 -175
  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 +169 -4
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1429 -137
  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 +3635 -107
  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 +442 -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 +1 -1
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js.map +1 -1
  247. package/dist/internal/codelines.d.ts +42 -0
  248. package/dist/internal/codelines.d.ts.map +1 -0
  249. package/dist/internal/codelines.js +170 -0
  250. package/dist/internal/codelines.js.map +1 -0
  251. package/dist/internal/collate.d.ts +3 -0
  252. package/dist/internal/collate.d.ts.map +1 -0
  253. package/dist/internal/collate.js +11 -0
  254. package/dist/internal/collate.js.map +1 -0
  255. package/dist/internal/markdown/ast.d.ts +11 -0
  256. package/dist/internal/markdown/ast.d.ts.map +1 -1
  257. package/dist/internal/markdown/parse.d.ts.map +1 -1
  258. package/dist/internal/markdown/parse.js +109 -8
  259. package/dist/internal/markdown/parse.js.map +1 -1
  260. package/dist/internal/prewarm.d.ts +18 -0
  261. package/dist/internal/prewarm.d.ts.map +1 -0
  262. package/dist/internal/prewarm.js +42 -0
  263. package/dist/internal/prewarm.js.map +1 -0
  264. package/dist/internal/sdf.d.ts +20 -0
  265. package/dist/internal/sdf.d.ts.map +1 -0
  266. package/dist/internal/sdf.js +216 -0
  267. package/dist/internal/sdf.js.map +1 -0
  268. package/dist/internal/splice.d.ts +12 -0
  269. package/dist/internal/splice.d.ts.map +1 -0
  270. package/dist/internal/splice.js +23 -0
  271. package/dist/internal/splice.js.map +1 -0
  272. package/dist/internal/window.d.ts +12 -0
  273. package/dist/internal/window.d.ts.map +1 -1
  274. package/dist/internal/window.js +23 -6
  275. package/dist/internal/window.js.map +1 -1
  276. package/dist/maps/anchors.d.ts +6 -1
  277. package/dist/maps/anchors.d.ts.map +1 -1
  278. package/dist/maps/anchors.js +260 -1
  279. package/dist/maps/anchors.js.map +1 -1
  280. package/dist/maps/controller.d.ts +57 -0
  281. package/dist/maps/controller.d.ts.map +1 -1
  282. package/dist/maps/controller.js +97 -18
  283. package/dist/maps/controller.js.map +1 -1
  284. package/dist/maps/gl/buckets.d.ts +5 -1
  285. package/dist/maps/gl/buckets.d.ts.map +1 -1
  286. package/dist/maps/gl/buckets.js +5 -2
  287. package/dist/maps/gl/buckets.js.map +1 -1
  288. package/dist/maps/gl/build-worker.d.ts +2 -0
  289. package/dist/maps/gl/build-worker.d.ts.map +1 -1
  290. package/dist/maps/gl/build-worker.js +1 -1
  291. package/dist/maps/gl/build-worker.js.map +1 -1
  292. package/dist/maps/gl/cover.d.ts +23 -4
  293. package/dist/maps/gl/cover.d.ts.map +1 -1
  294. package/dist/maps/gl/cover.js +61 -48
  295. package/dist/maps/gl/cover.js.map +1 -1
  296. package/dist/maps/gl/placement.d.ts +21 -8
  297. package/dist/maps/gl/placement.d.ts.map +1 -1
  298. package/dist/maps/gl/placement.js +133 -58
  299. package/dist/maps/gl/placement.js.map +1 -1
  300. package/dist/maps/gl/renderer.d.ts +36 -4
  301. package/dist/maps/gl/renderer.d.ts.map +1 -1
  302. package/dist/maps/gl/renderer.js +81 -24
  303. package/dist/maps/gl/renderer.js.map +1 -1
  304. package/dist/maps/gl/shaders.d.ts +18 -17
  305. package/dist/maps/gl/shaders.d.ts.map +1 -1
  306. package/dist/maps/gl/shaders.js +34 -32
  307. package/dist/maps/gl/shaders.js.map +1 -1
  308. package/dist/maps/gl/store.d.ts +10 -0
  309. package/dist/maps/gl/store.d.ts.map +1 -1
  310. package/dist/maps/gl/store.js +47 -4
  311. package/dist/maps/gl/store.js.map +1 -1
  312. package/dist/maps/gl/text.d.ts +153 -24
  313. package/dist/maps/gl/text.d.ts.map +1 -1
  314. package/dist/maps/gl/text.js +379 -63
  315. package/dist/maps/gl/text.js.map +1 -1
  316. package/dist/maps/gl/view.d.ts +293 -2
  317. package/dist/maps/gl/view.d.ts.map +1 -1
  318. package/dist/maps/gl/view.js +317 -82
  319. package/dist/maps/gl/view.js.map +1 -1
  320. package/dist/maps/icons.d.ts +32 -0
  321. package/dist/maps/icons.d.ts.map +1 -0
  322. package/dist/maps/icons.js +151 -0
  323. package/dist/maps/icons.js.map +1 -0
  324. package/dist/maps/index.d.ts +2 -0
  325. package/dist/maps/index.d.ts.map +1 -1
  326. package/dist/maps/index.js +2 -0
  327. package/dist/maps/index.js.map +1 -1
  328. package/dist/maps/labels.d.ts +56 -4
  329. package/dist/maps/labels.d.ts.map +1 -1
  330. package/dist/maps/labels.js +213 -46
  331. package/dist/maps/labels.js.map +1 -1
  332. package/dist/maps/node.d.ts +17 -0
  333. package/dist/maps/node.d.ts.map +1 -1
  334. package/dist/maps/node.js +64 -30
  335. package/dist/maps/node.js.map +1 -1
  336. package/dist/maps/proj.d.ts +48 -0
  337. package/dist/maps/proj.d.ts.map +1 -1
  338. package/dist/maps/proj.js +73 -0
  339. package/dist/maps/proj.js.map +1 -1
  340. package/dist/maps/sources.d.ts +5 -3
  341. package/dist/maps/sources.d.ts.map +1 -1
  342. package/dist/maps/sources.js +5 -3
  343. package/dist/maps/sources.js.map +1 -1
  344. package/dist/maps/style.d.ts +35 -0
  345. package/dist/maps/style.d.ts.map +1 -1
  346. package/dist/maps/style.js.map +1 -1
  347. package/dist/maps/styles.d.ts +12 -0
  348. package/dist/maps/styles.d.ts.map +1 -1
  349. package/dist/maps/styles.js +120 -6
  350. package/dist/maps/styles.js.map +1 -1
  351. package/dist/maps/tiles.d.ts +31 -11
  352. package/dist/maps/tiles.d.ts.map +1 -1
  353. package/dist/maps/tiles.js +61 -27
  354. package/dist/maps/tiles.js.map +1 -1
  355. package/dist/maps/types.d.ts +28 -4
  356. package/dist/maps/types.d.ts.map +1 -1
  357. package/dist/markdown/index.d.ts.map +1 -1
  358. package/dist/markdown/index.js +209 -68
  359. package/dist/markdown/index.js.map +1 -1
  360. package/dist/markdown/spans.d.ts.map +1 -1
  361. package/dist/markdown/spans.js +6 -0
  362. package/dist/markdown/spans.js.map +1 -1
  363. package/dist/qml/containers.d.ts.map +1 -1
  364. package/dist/qml/containers.js +6 -1
  365. package/dist/qml/containers.js.map +1 -1
  366. package/dist/qml/interaction.d.ts.map +1 -1
  367. package/dist/qml/interaction.js +38 -5
  368. package/dist/qml/interaction.js.map +1 -1
  369. package/dist/qml/layouts.d.ts.map +1 -1
  370. package/dist/qml/layouts.js +21 -6
  371. package/dist/qml/layouts.js.map +1 -1
  372. package/dist/qml/qtquick.d.ts.map +1 -1
  373. package/dist/qml/qtquick.js +9 -2
  374. package/dist/qml/qtquick.js.map +1 -1
  375. package/dist/qml/react.d.ts.map +1 -1
  376. package/dist/qml/react.js +9 -7
  377. package/dist/qml/react.js.map +1 -1
  378. package/dist/reorder/clock.d.ts +7 -0
  379. package/dist/reorder/clock.d.ts.map +1 -0
  380. package/dist/reorder/clock.js +26 -0
  381. package/dist/reorder/clock.js.map +1 -0
  382. package/dist/reorder/index.d.ts +25 -3
  383. package/dist/reorder/index.d.ts.map +1 -1
  384. package/dist/reorder/index.js +64 -19
  385. package/dist/reorder/index.js.map +1 -1
  386. package/dist/rich-text-editor/index.d.ts +0 -11
  387. package/dist/rich-text-editor/index.d.ts.map +1 -1
  388. package/dist/rich-text-editor/index.js +38 -9
  389. package/dist/rich-text-editor/index.js.map +1 -1
  390. package/dist/rich-text-editor/keymap.d.ts.map +1 -1
  391. package/dist/rich-text-editor/keymap.js +2 -1
  392. package/dist/rich-text-editor/keymap.js.map +1 -1
  393. package/dist/rich-text-editor/keys.d.ts +17 -6
  394. package/dist/rich-text-editor/keys.d.ts.map +1 -1
  395. package/dist/rich-text-editor/keys.js +135 -26
  396. package/dist/rich-text-editor/keys.js.map +1 -1
  397. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  398. package/dist/rich-text-editor/markdown.js +30 -1
  399. package/dist/rich-text-editor/markdown.js.map +1 -1
  400. package/dist/rich-text-editor/marks.d.ts +9 -0
  401. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  402. package/dist/rich-text-editor/marks.js +83 -0
  403. package/dist/rich-text-editor/marks.js.map +1 -0
  404. package/dist/rich-text-editor/render.d.ts.map +1 -1
  405. package/dist/rich-text-editor/render.js +28 -10
  406. package/dist/rich-text-editor/render.js.map +1 -1
  407. package/dist/rich-text-editor/replace.d.ts +13 -0
  408. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  409. package/dist/rich-text-editor/replace.js +64 -0
  410. package/dist/rich-text-editor/replace.js.map +1 -0
  411. package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
  412. package/dist/rich-text-editor/toolbar.js +1 -1
  413. package/dist/rich-text-editor/toolbar.js.map +1 -1
  414. package/dist/rich-text-editor/virtual.d.ts.map +1 -1
  415. package/dist/rich-text-editor/virtual.js +17 -0
  416. package/dist/rich-text-editor/virtual.js.map +1 -1
  417. package/dist/richtext/node.d.ts +46 -2
  418. package/dist/richtext/node.d.ts.map +1 -1
  419. package/dist/richtext/node.js +187 -4
  420. package/dist/richtext/node.js.map +1 -1
  421. package/dist/richtext/runs.d.ts +13 -8
  422. package/dist/richtext/runs.d.ts.map +1 -1
  423. package/dist/richtext/runs.js +18 -8
  424. package/dist/richtext/runs.js.map +1 -1
  425. package/dist/table/index.d.ts.map +1 -1
  426. package/dist/table/index.js +23 -0
  427. package/dist/table/index.js.map +1 -1
  428. package/dist/table/rows.d.ts +3 -2
  429. package/dist/table/rows.d.ts.map +1 -1
  430. package/dist/table/rows.js +20 -6
  431. package/dist/table/rows.js.map +1 -1
  432. package/dist/terminal/index.d.ts.map +1 -1
  433. package/dist/terminal/index.js +4 -0
  434. package/dist/terminal/index.js.map +1 -1
  435. package/dist/terminal/vt/pty.d.ts +13 -2
  436. package/dist/terminal/vt/pty.d.ts.map +1 -1
  437. package/dist/terminal/vt/pty.js +18 -3
  438. package/dist/terminal/vt/pty.js.map +1 -1
  439. package/dist/terminal-output/index.d.ts.map +1 -1
  440. package/dist/terminal-output/index.js +103 -23
  441. package/dist/terminal-output/index.js.map +1 -1
  442. package/dist/terminal-output/runs.d.ts +4 -1
  443. package/dist/terminal-output/runs.d.ts.map +1 -1
  444. package/dist/terminal-output/runs.js +5 -1
  445. package/dist/terminal-output/runs.js.map +1 -1
  446. package/dist/three/canvas.d.ts.map +1 -1
  447. package/dist/three/canvas.js +85 -7
  448. package/dist/three/canvas.js.map +1 -1
  449. package/dist/three/events.d.ts +22 -4
  450. package/dist/three/events.d.ts.map +1 -1
  451. package/dist/three/events.js +34 -4
  452. package/dist/three/events.js.map +1 -1
  453. package/dist/tree/index.d.ts.map +1 -1
  454. package/dist/tree/index.js +17 -0
  455. package/dist/tree/index.js.map +1 -1
  456. package/package.json +42 -33
  457. package/src/charts/node.ts +62 -26
  458. package/src/charts/render.ts +120 -30
  459. package/src/code/index.ts +36 -32
  460. package/src/code-editor/completion.ts +3 -2
  461. package/src/code-editor/index.ts +19 -0
  462. package/src/code-editor/node.ts +1574 -184
  463. package/src/code-language/index.ts +1 -1
  464. package/src/code-language/languages/glsl.ts +12 -2
  465. package/src/code-language/languages/javascript.ts +21 -4
  466. package/src/code-language/languages/json.ts +12 -2
  467. package/src/code-language/languages/shell.ts +12 -2
  468. package/src/code-language/languages/sql.ts +22 -2
  469. package/src/code-language/runs.ts +111 -1
  470. package/src/code-language/stream.ts +257 -17
  471. package/src/code-language/types.ts +5 -4
  472. package/src/codeblock/index.ts +68 -4
  473. package/src/flow/draw.ts +167 -41
  474. package/src/flow/gl/color.ts +85 -0
  475. package/src/flow/gl/index.ts +342 -0
  476. package/src/flow/gl/pack.ts +585 -0
  477. package/src/flow/gl/renderer.ts +679 -0
  478. package/src/flow/gl/shaders.ts +371 -0
  479. package/src/flow/gl/text.ts +1295 -0
  480. package/src/flow/index.ts +794 -67
  481. package/src/flow/node.ts +2496 -1256
  482. package/src/flow/paint.ts +511 -0
  483. package/src/flow/paths.ts +80 -15
  484. package/src/flow/scene.ts +2199 -0
  485. package/src/flow/types.ts +113 -1
  486. package/src/html/controls.ts +73 -2
  487. package/src/html/css/calc.ts +385 -0
  488. package/src/html/css/cascade.ts +1481 -62
  489. package/src/html/css/color.ts +755 -0
  490. package/src/html/css/content.ts +417 -0
  491. package/src/html/css/counter-styles.ts +973 -0
  492. package/src/html/css/decode.ts +130 -0
  493. package/src/html/css/parse.ts +1514 -160
  494. package/src/html/css/style.ts +4131 -223
  495. package/src/html/css/ua.ts +66 -17
  496. package/src/html/css/values.ts +150 -50
  497. package/src/html/css/vars.ts +348 -0
  498. package/src/html/dom.ts +284 -17
  499. package/src/html/fonts.ts +641 -0
  500. package/src/html/gif.ts +233 -0
  501. package/src/html/index.ts +180 -20
  502. package/src/html/layout/block.ts +3560 -208
  503. package/src/html/layout/boxes.ts +2141 -190
  504. package/src/html/layout/cache.ts +289 -0
  505. package/src/html/layout/collapse.ts +317 -0
  506. package/src/html/layout/css-grid.ts +1063 -0
  507. package/src/html/layout/flex.ts +1115 -106
  508. package/src/html/layout/floats.ts +128 -14
  509. package/src/html/layout/grid-lines.ts +141 -0
  510. package/src/html/layout/grid.ts +181 -0
  511. package/src/html/layout/inline.ts +4187 -192
  512. package/src/html/layout/shaping.ts +140 -0
  513. package/src/html/layout/table.ts +696 -186
  514. package/src/html/layout/tracks.ts +437 -0
  515. package/src/html/node.ts +1585 -156
  516. package/src/html/paint.ts +4582 -267
  517. package/src/html/resources.ts +161 -50
  518. package/src/html/surfaces.ts +81 -0
  519. package/src/html/svg.ts +504 -0
  520. package/src/html/url.ts +86 -0
  521. package/src/index.ts +2 -0
  522. package/src/internal/codelines.ts +200 -0
  523. package/src/internal/collate.ts +12 -0
  524. package/src/internal/markdown/ast.ts +11 -0
  525. package/src/internal/markdown/parse.ts +154 -11
  526. package/src/internal/prewarm.ts +45 -0
  527. package/src/internal/sdf.ts +227 -0
  528. package/src/internal/splice.ts +25 -0
  529. package/src/internal/window.ts +36 -5
  530. package/src/maps/anchors.ts +307 -1
  531. package/src/maps/controller.ts +116 -31
  532. package/src/maps/gl/buckets.ts +5 -1
  533. package/src/maps/gl/build-worker.ts +13 -2
  534. package/src/maps/gl/cover.ts +79 -44
  535. package/src/maps/gl/placement.ts +221 -69
  536. package/src/maps/gl/renderer.ts +104 -20
  537. package/src/maps/gl/shaders.ts +35 -32
  538. package/src/maps/gl/store.ts +66 -17
  539. package/src/maps/gl/text.ts +444 -71
  540. package/src/maps/gl/view.ts +361 -96
  541. package/src/maps/icons.ts +183 -0
  542. package/src/maps/index.ts +4 -0
  543. package/src/maps/labels.ts +269 -49
  544. package/src/maps/node.ts +82 -49
  545. package/src/maps/proj.ts +87 -0
  546. package/src/maps/sources.ts +5 -3
  547. package/src/maps/style.ts +32 -0
  548. package/src/maps/styles.ts +215 -4
  549. package/src/maps/tiles.ts +64 -31
  550. package/src/maps/types.ts +28 -4
  551. package/src/markdown/index.ts +293 -92
  552. package/src/markdown/spans.ts +5 -0
  553. package/src/qml/containers.tsx +6 -1
  554. package/src/qml/interaction.tsx +42 -5
  555. package/src/qml/layouts.tsx +21 -6
  556. package/src/qml/qtquick.tsx +9 -2
  557. package/src/qml/react.tsx +7 -5
  558. package/src/reorder/clock.ts +27 -0
  559. package/src/reorder/index.ts +100 -27
  560. package/src/rich-text-editor/index.ts +37 -12
  561. package/src/rich-text-editor/keymap.ts +1 -1
  562. package/src/rich-text-editor/keys.ts +135 -26
  563. package/src/rich-text-editor/markdown.ts +36 -1
  564. package/src/rich-text-editor/marks.ts +101 -0
  565. package/src/rich-text-editor/render.ts +49 -12
  566. package/src/rich-text-editor/replace.ts +85 -0
  567. package/src/rich-text-editor/toolbar.ts +1 -1
  568. package/src/rich-text-editor/virtual.ts +25 -0
  569. package/src/richtext/node.ts +235 -14
  570. package/src/richtext/runs.ts +23 -12
  571. package/src/table/index.ts +29 -0
  572. package/src/table/rows.ts +18 -8
  573. package/src/terminal/index.ts +4 -0
  574. package/src/terminal/vt/pty.ts +16 -2
  575. package/src/terminal-output/index.ts +129 -31
  576. package/src/terminal-output/runs.ts +16 -2
  577. package/src/three/canvas.ts +102 -8
  578. package/src/three/events.ts +49 -9
  579. package/src/tree/index.ts +19 -0
@@ -24,11 +24,27 @@ import {
24
24
  childrenOf,
25
25
  isElement,
26
26
  isText,
27
+ inImpliedHead,
27
28
  NON_RENDERED,
28
29
  tagOf,
29
30
  } from '../dom.js';
30
- import type { Cascade } from '../css/cascade.js';
31
+ import type { Cascade, FirstLetterRules } from '../css/cascade.js';
32
+ import type { CollapsedTable } from './collapse.js';
33
+ import { quoteAt } from '../css/content.js';
34
+ import type { CounterStyles } from '../css/counter-styles.js';
35
+ import type { ContentItem } from '../css/content.js';
36
+ import {
37
+ CONTAIN_STYLE,
38
+ FIRST_LINE_INHERITED,
39
+ copyStyle,
40
+ firstLineParent,
41
+ inherit,
42
+ } from '../css/style.js';
43
+ import { AUTO } from '../css/values.js';
44
+ import { svgIntrinsics } from '../svg.js';
45
+ import type { IntrinsicSize } from '../svg.js';
31
46
  import type { ComputedStyle } from '../css/style.js';
47
+ import type { GridLines } from './grid-lines.js';
32
48
 
33
49
  export type BoxKind =
34
50
  | 'block'
@@ -44,6 +60,68 @@ export type BoxKind =
44
60
  | 'marker'
45
61
  | 'break';
46
62
 
63
+ // What only a few boxes carry lives beside the box rather than in it: a box
64
+ // is made for every element and every run of text, and each field on it is
65
+ // memory every walk over the tree goes through — fifteen fields more, never
66
+ // read, made a document's first paint 6% slower.
67
+
68
+ /** A list item's marker: its text, a bullet or the item's number, and
69
+ * where it was laid out. */
70
+ export interface Marker {
71
+ text: string;
72
+ layout: TextLayoutLike | null;
73
+ x: number;
74
+ y: number;
75
+ /** Its own style where a `::marker` rule gives it one; the item's where
76
+ * none does. */
77
+ style: ComputedStyle | null;
78
+ /** The image it is, `list-style-image`'s, at its size in device pixels,
79
+ * where that has arrived; its text is then not drawn. */
80
+ image?: { url: string; width: number; height: number };
81
+ /** Its text is its own — a string `list-style-type`, a `::marker`'s
82
+ * `content` — and ends at the content's edge, with no gap a number
83
+ * or a bullet is set apart by. */
84
+ flush?: true;
85
+ }
86
+
87
+ /** An out-of-flow box's static position, as an offset from the box whose
88
+ * flow it was taken from, which may yet move. */
89
+ export interface StaticPosition {
90
+ from: Box;
91
+ x: number;
92
+ right: number;
93
+ y: number;
94
+ /**
95
+ * For a flex box's or a grid's child, the box it is where it would be as
96
+ * the one item in — `from`'s content box or its padding box, whose size
97
+ * is read once its layout is done — and how far along the free room in
98
+ * it its alignment puts it, from the start across and from the top down.
99
+ */
100
+ inside?: {
101
+ box: 'content' | 'padding';
102
+ across: number;
103
+ down: number;
104
+ };
105
+ }
106
+
107
+ /** The ascent and descent of a decorated inline box's own face. */
108
+ export interface InlineDecoration {
109
+ ascent: number;
110
+ descent: number;
111
+ }
112
+
113
+ /**
114
+ * A set of adjoining margins as CSS 2.1 8.3.1 collapses them: the largest
115
+ * positive and the most negative, whose sum is the margin. Kept apart as
116
+ * margins join, because summing at each join is not associative once the
117
+ * signs mix: 2, -4 and 14 collapse to 10, and taken two at a time to 12,
118
+ * and to 8 with a -4 after.
119
+ */
120
+ export interface MarginStrut {
121
+ pos: number;
122
+ neg: number;
123
+ }
124
+
47
125
  /** A laid-out line inside an inline formatting context. */
48
126
  export interface LineBox {
49
127
  /** Content-box relative, resolved to document coordinates at paint. */
@@ -51,6 +129,8 @@ export interface LineBox {
51
129
  y: number;
52
130
  width: number;
53
131
  height: number;
132
+ /** From the line's top — where a text engine's own lines carry theirs
133
+ * from the top of the layout. */
54
134
  baseline: number;
55
135
  /**
56
136
  * The text on this line, as one or more fragments.
@@ -65,6 +145,16 @@ export interface LineBox {
65
145
  textEnd: number;
66
146
  /** Atomic items sitting on this line — images, inline-blocks, controls. */
67
147
  atomics: AtomicPlacement[];
148
+ /** The inline boxes that open or close on this line, where their own
149
+ * margin, border and padding sit. */
150
+ edges?: EdgePlacement[];
151
+ /** A `::first-line` style with a background, painted behind the line's
152
+ * content over its face's height, as an inline box's is. */
153
+ background?: { style: ComputedStyle } & InlineDecoration;
154
+ /** How wide the floats placed at this line's top are, side by side: what
155
+ * stands beside its content, and so what its content is measured with
156
+ * where a box is as wide as its content (`intrinsicWidth`). */
157
+ floats?: number;
68
158
  }
69
159
 
70
160
  /**
@@ -93,12 +183,42 @@ export interface LineText {
93
183
  /** Code-unit offset within the layout's own text that `textStart` maps to,
94
184
  * so a document index can be turned into a caret index in this layout. */
95
185
  layoutStart: number;
186
+ /** Where in the layout's text an inline box's edge was laid out as a
187
+ * spacer: a unit of the layout that is no text of the document's
188
+ * (`documentOffsetOf`, `layoutOffsetOf`). */
189
+ gaps?: number[];
190
+ /** Any offset in the layout's own text as a document index — how a run
191
+ * under the pointer finds the element whose text it is. Per pass: the
192
+ * layout may be one an earlier pass made (`TextLayoutCache`), and the
193
+ * document index is this parse's. */
194
+ spans: {
195
+ documentAt(offset: number): number;
196
+ /** The text box an offset's text is, where the layout knows. */
197
+ boxAt?(offset: number): Box | null;
198
+ };
96
199
  }
97
200
 
98
201
  export interface AtomicPlacement {
99
202
  box: Box;
100
203
  x: number;
101
204
  y: number;
205
+ /** How far its `vertical-align` raises its baseline above its parent's,
206
+ * worked out as it joined the line; absent for none, and for `top` and
207
+ * `bottom`, which the line box's edges place instead. */
208
+ raise?: number;
209
+ }
210
+
211
+ /**
212
+ * Where an inline box's own margin, border and padding on one side sit on a
213
+ * line: CSS 2.1 puts the start side's before the box's first fragment and the
214
+ * end side's after its last, taking room on those lines (10.3.1). `x` is the
215
+ * edge's outer side, margin included, and `width` all three.
216
+ */
217
+ export interface EdgePlacement {
218
+ box: Box;
219
+ side: 'start' | 'end';
220
+ x: number;
221
+ width: number;
102
222
  }
103
223
 
104
224
  /** The slice of ntk's `TextLayout` this renderer reads. Structural for the
@@ -167,6 +287,10 @@ export class Box {
167
287
  width = 0;
168
288
  height = 0;
169
289
 
290
+ /** For an inline box broken around a block in it (CSS 2.1 9.2.1.1), the
291
+ * sides it goes on from, which have no edge: 1 its start, 2 its end. */
292
+ cut = 0;
293
+
170
294
  /** Resolved edges — border + padding, per side. */
171
295
  borderTop = 0;
172
296
  borderRight = 0;
@@ -180,6 +304,34 @@ export class Box {
180
304
  marginRight = 0;
181
305
  marginBottom = 0;
182
306
  marginLeft = 0;
307
+ /** The margin that came out through the box's bottom edge from its last
308
+ * children, collapsed with its own bottom margin, which is their sum:
309
+ * kept as a strut for the next sibling's to collapse with. Null where
310
+ * none came out, which is most boxes. Set by the block layout. */
311
+ bottomStrut: MarginStrut | null = null;
312
+ /** An inline box with a background or a border to paint behind its
313
+ * fragments: the ascent and descent of its own face, the height CSS
314
+ * paints them over (10.6.1). Set by the inline layout. */
315
+ decoration: InlineDecoration | null = null;
316
+ /** What a percentage `height` resolves against: an absolutely positioned
317
+ * box's containing block's height, which is known before the box is laid
318
+ * out (CSS 2.1 10.5). NaN everywhere else, where that height depends on
319
+ * the content and a percentage is `auto`. */
320
+ percentHeightBase = NaN;
321
+ /** Whether this box last handed its children a percentage base that
322
+ * was a number: until it does, they hold NaN already, and the next
323
+ * pass need not visit them to say so again. */
324
+ gavePercentBase = false;
325
+ /** Whether this box's top margin collapsed through its parent's top edge
326
+ * and was spent placing the parent (CSS 2.1 8.3.1): its own layout puts
327
+ * it at the parent's content top, and applies no margin again. `2` when
328
+ * the box is empty and its bottom margin went the same way, so the next
329
+ * sibling's may have too; `3` for a new formatting context the margin
330
+ * stopped above, separated from the floats it carries, which sits where
331
+ * the margins before it end and applies none of its own. Set by the walk
332
+ * that places the parent, each pass, and read only while every sibling
333
+ * before it was a `2`. */
334
+ topAbsorbed: 0 | 1 | 2 | 3 = 0;
183
335
 
184
336
  /** Text, for a `text` box: already whitespace-processed and transformed. */
185
337
  text = '';
@@ -189,12 +341,17 @@ export class Box {
189
341
 
190
342
  /** Lines, for a box that established an inline formatting context. */
191
343
  lines: LineBox[] | null = null;
192
-
193
- /** Intrinsic size, for a replaced box that knows one. */
194
- intrinsicWidth = 0;
195
- intrinsicHeight = 0;
344
+ /** The widest row of floats its lines placed beside none of them, side
345
+ * by side: what the box is measured with, as a line's floats are
346
+ * (`LineBox.floats`). */
347
+ floatRow = 0;
348
+
349
+ /** A replaced box's intrinsic size and ratio; null on every other box,
350
+ * which is nearly all of them, so it is one field rather than four. */
351
+ intrinsic: Intrinsic | null = null;
196
352
  /**
197
- * Cached min-/max-content widths, for a table cell. -1 until measured.
353
+ * Cached min-/max-content widths, for a table cell and for a box an
354
+ * intrinsic size (`width: fit-content`) sizes. -1 until measured.
198
355
  *
199
356
  * Intrinsic widths are width-independent by definition, so measuring them
200
357
  * per layout pass was this engine breaking its own phase rule — a resize
@@ -205,18 +362,24 @@ export class Box {
205
362
  */
206
363
  intrinsicMinContent = -1;
207
364
  intrinsicMaxContent = -1;
365
+ /** Which layout of the pass this box last had, counted across all boxes:
366
+ * what tells a layout kept for reuse (`layoutOwn`) from one a probe has
367
+ * laid over since. */
368
+ layoutSerial = 0;
208
369
  /** What a replaced box is: the resource seam and the control host both
209
370
  * key on this rather than re-reading the tag. */
210
371
  replaced: ReplacedKind = 'none';
211
372
 
212
- /** The marker text of a `list-item`, if it generated one, and where it
213
- * was laid out. The marker is not a box: it is not in the flow, nothing
214
- * can select it (CSS spells that `::marker`, and no author styles it
215
- * here), and giving it one would put a bullet in every copied list. */
216
- markerText = '';
217
- markerLayout: TextLayoutLike | null = null;
218
- markerX = 0;
219
- markerY = 0;
373
+ /** The marker of a `list-item`, if it generated one. The marker is not a
374
+ * box: it is not in the flow, nothing can select it (CSS spells that
375
+ * `::marker`, and no author styles it here), and giving it one would put
376
+ * a bullet in every copied list. */
377
+ marker: Marker | null = null;
378
+ /** Which of an element's pseudo-elements this box is. Its `el` is null —
379
+ * it is no element — and its text box's is the element it hangs off, so
380
+ * a click on a link's generated text or first letter is a click on the
381
+ * link. */
382
+ pseudo: 'before' | 'after' | 'first-letter' | null = null;
220
383
 
221
384
  /**
222
385
  * The bounds of everything this box and its descendants draw, in document
@@ -247,6 +410,10 @@ export class Box {
247
410
  /** Out-of-flow children in paint order (z-index, then document order),
248
411
  * precomputed so a paint does not filter and sort per box per frame. */
249
412
  positionedPaint: Box[] | null = null;
413
+ /** Whether a positioned box is somewhere under this one: what the pass
414
+ * that gives each stacking context its positioned boxes walks, so that
415
+ * a subtree with none is not walked at all (`stackLayers`). */
416
+ holdsLayers = false;
250
417
  /** The tallest line box under this box — the slack a binary search over
251
418
  * the y-sorted lines needs, since a line's bottom is not monotone. */
252
419
  maxLineHeight = 0;
@@ -265,6 +432,21 @@ export class Box {
265
432
  outOfFlow = false;
266
433
  /** Set on a float, for the same reason. */
267
434
  isFloat = false;
435
+ /** Where an out-of-flow box would have been in the flow it was taken
436
+ * from — its static position (CSS 2.1 10.3.7, 10.6.4). */
437
+ staticPosition: StaticPosition | null = null;
438
+
439
+ /** A table's captions, above and below it: they are in the box's height,
440
+ * and outside the table's own border and background (CSS 2.1 17.4). */
441
+ captionTop = 0;
442
+ captionBottom = 0;
443
+ /** A table whose borders collapse: the border each segment of its grid
444
+ * carries, resolved once per build (`collapseTable`). */
445
+ collapsed: CollapsedTable | null = null;
446
+ /** Set on a table whose borders collapse, and on its cells: their border
447
+ * widths are the halves the collapsing model leaves them, and the table
448
+ * paints the borders rather than the boxes. */
449
+ bordersCollapsed = false;
268
450
 
269
451
  constructor(kind: BoxKind, el: Element | null, style: ComputedStyle) {
270
452
  this.kind = kind;
@@ -323,7 +505,127 @@ export type ReplacedKind =
323
505
  | 'button'
324
506
  | 'checkbox'
325
507
  | 'radio'
326
- | 'hr';
508
+ | 'hr'
509
+ /** An `<iframe>`, `<video>`, `<embed>` or `<canvas>`: what it would
510
+ * show is never loaded or drawn, so it is a box of its size with nothing
511
+ * in it. */
512
+ | 'frame'
513
+ /** An inline `<svg>`: a drawing, sized by what it says of its size. */
514
+ | 'svg';
515
+
516
+ /** A replaced box's intrinsic dimensions, in device pixels. */
517
+ export interface Intrinsic {
518
+ /** Its size — or, on an axis `missing` names, the default object size of
519
+ * 300 by 150 CSS pixels that CSS falls back to there (CSS 2.1 10.3.2,
520
+ * 10.6.2). */
521
+ width: number;
522
+ height: number;
523
+ /** The dimensions it lacks: 1 its width, 2 its height. Only an SVG can. */
524
+ missing: number;
525
+ /** Its ratio, width over height, or 0 where it has none: an image's, or
526
+ * an SVG's from its size or its `viewBox`. */
527
+ ratio: number;
528
+ }
529
+
530
+ /** The `::first-line` style of each block container a rule gives one to,
531
+ * kept beside the boxes: few documents have any (`BoxTree.firstLine`). */
532
+ export const FIRST_LINE = new WeakMap<Box, ComputedStyle>();
533
+
534
+ /**
535
+ * A box's style on the first line of the block it is in, given its
536
+ * parent's style there (CSS Pseudo 4, 2.1.2): an element's own rules over
537
+ * what it inherits on the line, which takes the properties `::first-line`
538
+ * applies to from `parent` and the rest from its parent's own style.
539
+ */
540
+ export type FirstLineStyler = (
541
+ box: Box,
542
+ parent: ComputedStyle,
543
+ ) => ComputedStyle;
544
+
545
+ /** The url of an image generated content put in a pseudo-element: it has
546
+ * no element of its own to name it (`imageUrlOf`). */
547
+ export const CONTENT_IMAGES = new WeakMap<Box, string>();
548
+
549
+ /** How far `position: relative` moved an inline box (`offsetInline`), how
550
+ * far each text on a line moved with the boxes it is in, and the lines
551
+ * holding one: beside the boxes and the lines, since few documents move an
552
+ * inline box. A moved text's `drawX` and `drawY` are where it is drawn;
553
+ * its box's decorations are worked out from where it was, and moved by
554
+ * their own box's offset. */
555
+ export const INLINE_OFFSETS = new WeakMap<Box, { x: number; y: number }>();
556
+ export const TEXT_SHIFTS = new WeakMap<LineText, { x: number; y: number }>();
557
+ export const SHIFTED_LINES = new WeakSet<LineBox>();
558
+
559
+ /** Of a block's lines, by the array they are, those `position: relative`
560
+ * or a translation moved something off — a text in a moved inline box, or
561
+ * an inline-block or an image on it — which is drawn where it went while
562
+ * the line stays where it was: what the paint's cull of a block's lines
563
+ * has to look at past the lines the damage meets (`paintLines`). By the
564
+ * array, so that a layout that makes new lines leaves none of the old. */
565
+ export const MOVED_OFF_LINES = new WeakMap<LineBox[], Set<LineBox>>();
566
+
567
+ /** How far `vertical-align` raised a text above its line's baseline, and an
568
+ * inline box's own baseline, where it did: beside the lines and the boxes
569
+ * for the reason the offsets above are. A line holding a raised text is
570
+ * among `SHIFTED_LINES` too, as its text is off the line's baseline. */
571
+ export const TEXT_RAISES = new WeakMap<LineText, number>();
572
+
573
+ /** The blocks some of whose text casts a shadow (`text-shadow`), which the
574
+ * paint pass looks for in them and in no other. */
575
+ export const SHADOWED_TEXT = new WeakSet<Box>();
576
+ export const BOX_RAISES = new WeakMap<Box, number>();
577
+
578
+ /** And the raise, on one line, of each inline box a `top` or `bottom` box
579
+ * holds, the box itself among them: that box's baseline is where the
580
+ * line's edge puts it, which is a different height on every line, so its
581
+ * background, drawn from its baseline, is put there by line. */
582
+ export const LINE_BOX_RAISES = new WeakMap<LineBox, Map<Box, number>>();
583
+
584
+ /** The table cells wholly in columns `visibility: collapse` took out of
585
+ * their table (CSS 2.1 17.5.5), which the paint pass leaves out: a cell is
586
+ * no descendant of its column and inherits nothing from it, so its own
587
+ * style still says `visible`. */
588
+ export const COLLAPSED_CELLS = new WeakSet<Box>();
589
+ /** And the cells spanning into a column or a row taken out: laid out as
590
+ * they would have been, placed over what is left of their span, and
591
+ * clipped to it. */
592
+ export const CLIPPED_CELLS = new WeakSet<Box>();
593
+
594
+ /** The boxes after a line-clamp container's clamp point (CSS Overflow 4,
595
+ * 5.3.1): invisible, with all they hold, and no taller than nothing to
596
+ * the blocks around them. Marked by the layout of the flow they are in,
597
+ * which leaves them where the last layout put them, and passed over by
598
+ * paint and the paint bounds. */
599
+ export const CLAMPED = new WeakSet<Box>();
600
+
601
+ /** A grid's tracks as its layout left them, each column's and each row's
602
+ * start and end from the content box's corner, with the lines of each
603
+ * axis by number and by name: what an absolutely positioned box's grid
604
+ * area is found in (CSS Grid 1, 9.1). */
605
+ export interface GridTracks {
606
+ cols: [number, number][];
607
+ rows: [number, number][];
608
+ colLines: GridLines;
609
+ rowLines: GridLines;
610
+ }
611
+ export const GRID_TRACKS = new WeakMap<Box, GridTracks>();
612
+
613
+ /** A flex box's children in the order it lays them out and paints them,
614
+ * `order` first and the document's after it (CSS Flexbox 5.4), where
615
+ * `order` moves any: an absolutely positioned child's is 0. */
616
+ export const PAINT_ORDER = new WeakMap<Box, Box[]>();
617
+
618
+ /** The blocks that broke a relatively positioned inline box in pieces,
619
+ * under its first piece: its offset moves them too (CSS 2.1 9.2.1.1),
620
+ * though they stand outside it (`breakAround`). */
621
+ export const CUT_BLOCKS = new WeakMap<Box, Box[]>();
622
+
623
+ /** The blocks that broke an inline box under full opacity in pieces, and
624
+ * the opacity they take from it — from each such inline box around them,
625
+ * multiplied. They stand outside it (CSS 2.1 9.2.1.1) and are still its
626
+ * content, which its opacity fades as a group: a green square in a
627
+ * `<span style="opacity: .5">` was drawn at full strength. */
628
+ export const FADED_BLOCKS = new WeakMap<Box, number>();
327
629
 
328
630
  /** What the builder produced, plus the document-wide text it indexed. */
329
631
  export interface BoxTree {
@@ -333,10 +635,54 @@ export interface BoxTree {
333
635
  text: string;
334
636
  /** Text boxes in document order — the selection binary-searches this. */
335
637
  textBoxes: Box[];
638
+ /** The styles text is set in, each once: the faces a host warms before
639
+ * laying the text out, found here rather than by walking `textBoxes`
640
+ * again after every build. */
641
+ textStyles: Set<ComputedStyle>;
642
+ /** Each element the build gave a style, with it and whether its parent
643
+ * was a flex container: what a pointer move restyles an element from
644
+ * where it did not change the rest (`HtmlViewNode._hoverInPlace`). */
645
+ styles: Map<Element, { style: ComputedStyle; inFlex: boolean }>;
336
646
  /** Every replaced box that needs a real widget, in document order. */
337
647
  controls: Box[];
338
648
  /** Every box carrying an `href`, for click and hover. */
339
649
  links: Box[];
650
+ /** Every element's box with a `background-image` or a border image,
651
+ * for the host to be asked for: a document has a handful, and finding
652
+ * them was a walk over every box after every build. */
653
+ backgrounds: Box[];
654
+ /** Every image generated content names, the element whose
655
+ * pseudo-element names it, for the host to be asked for, and whether its
656
+ * size was known when the box was built. */
657
+ contentImages: { url: string; element: Element; sized: boolean }[];
658
+ /** Whether any box is relatively positioned: where none is, layout skips
659
+ * the walk that moves them. */
660
+ relative: boolean;
661
+ /** Whether any positioned box has a negative `z-index`: where none has,
662
+ * paint has no layer below the flow to find. */
663
+ negative: boolean;
664
+ /** Whether any block has a `::first-line` style (`FIRST_LINE`): where
665
+ * none has, layout looks for none. */
666
+ firstLine: boolean;
667
+ /** Each box's style on a first line (`FirstLineStyler`), where any
668
+ * block has a `::first-line`. */
669
+ firstLineStyler: FirstLineStyler | null;
670
+ /** Whether a float or an out-of-flow box sits in an inline box: where
671
+ * none does, layout looks for them among a block's own children and
672
+ * goes through no inline box to find them. */
673
+ nestedOutOfLine: boolean;
674
+ /** Whether `position: relative` moves an inline box or `vertical-align`
675
+ * raises one: where none does, no text is off its line's baseline, and
676
+ * the bounds walk looks for none. */
677
+ movedInline: boolean;
678
+ /** Whether any box paints its background through its text
679
+ * (`background-clip: text`): where none does, no paragraph looks. */
680
+ clipText: boolean;
681
+ /** The style of the `<html>` a document with neither an `<html>` nor a
682
+ * `<body>` implies around the body the root box stands in for
683
+ * (`Cascade.rootStyle`), whose background is the canvas's; null where
684
+ * the document has either. */
685
+ impliedHtml: ComputedStyle | null;
340
686
  }
341
687
 
342
688
  export interface BuildOptions {
@@ -348,7 +694,9 @@ export interface BuildOptions {
348
694
  /** Intrinsic size for an image the host has already loaded, in the
349
695
  * image's own pixels. `null` when it has not: the box takes the attribute
350
696
  * size, or a placeholder. */
351
- imageSize(el: Element): { width: number; height: number } | null;
697
+ imageSize(el: Element): IntrinsicSize | null;
698
+ /** The same for an image named by url — generated content's. */
699
+ urlSize?(url: string): IntrinsicSize | null;
352
700
  /** The size a real widget wants, so the box in the flow is the size the
353
701
  * control will be drawn at. */
354
702
  controlSize(
@@ -377,18 +725,60 @@ export function buildBoxes(
377
725
  * output, a runaway template) from a stack overflow five phases later.
378
726
  * Blink's parser flattens at 512 for the same reason; content past the cap
379
727
  * is dropped, which beats the alternative of crashing the application.
728
+ * The parser keeps a document to 256 elements (`dom.ts`), so what reaches
729
+ * this now is the anonymous boxes a table builds round each of them.
380
730
  */
381
731
  const MAX_DEPTH = 512;
382
732
 
733
+ /** The sharing key the root box's children share under: one root style per
734
+ * build, and the shared styles last a build (`Cascade.beginSharing`). */
735
+ const ROOT_SHARE_KEY = 0;
736
+
383
737
  class Builder {
384
738
  private _options: BuildOptions;
385
- private _text = '';
739
+ /** The document text, in the pieces it was pushed in — joined once at the
740
+ * end, so that taking back a line's last space is not a copy of it. */
741
+ private _chunks: string[] = [];
742
+ private _length = 0;
386
743
  private _textBoxes: Box[] = [];
744
+ private _textStyles = new Set<ComputedStyle>();
745
+ private _styles = new Map<
746
+ Element,
747
+ { style: ComputedStyle; inFlex: boolean }
748
+ >();
749
+ /** The last text box's style: runs of text share their parent's, so
750
+ * most text boxes repeat it and add nothing to `_textStyles`. */
751
+ private _lastTextStyle: ComputedStyle | null = null;
387
752
  private _controls: Box[] = [];
388
753
  private _links: Box[] = [];
389
- /** Counter stack for `<ol>` numbering, one entry per open list. */
390
- private _counters: number[] = [];
754
+ private _backgrounds: Box[] = [];
755
+ private _contentImages: {
756
+ url: string;
757
+ element: Element;
758
+ sized: boolean;
759
+ }[] = [];
760
+ private _relative = false;
761
+ private _negative = false;
762
+ private _firstLine = false;
763
+ private _nestedOutOfLine = false;
764
+ private _movedInline = false;
765
+ private _clipText = false;
766
+ /** The CSS counters in scope, for `counter()` in generated content. */
767
+ private _scopes = new CounterScopes();
768
+ /** How many quotes generated content has opened and not closed. */
769
+ private _quoteDepth = 0;
770
+ /** Where the inline content being built stands, for collapsing white
771
+ * space across element boundaries. */
772
+ private _ws: Collapse = 'start';
773
+ /** Whether the word the inline content being built is in has had its
774
+ * first letter, for `text-transform: capitalize`: a word runs on across
775
+ * element boundaries, as white space collapses across them. */
776
+ private _lettered = false;
391
777
  private _depth = 0;
778
+ /** The `::first-letter` whose letter is still to come, for the block
779
+ * container whose first line has not begun. Null when there is none,
780
+ * and once anything but a letter begins that line. */
781
+ private _firstLetter: LetterSearch | null = null;
392
782
 
393
783
  constructor(options: BuildOptions) {
394
784
  this._options = options;
@@ -396,20 +786,72 @@ class Builder {
396
786
 
397
787
  run(root: Element): BoxTree {
398
788
  const cascade = this._options.cascade;
399
- const rootStyle = cascade.rootStyle(hasBody(root));
789
+ cascade.beginSharing();
790
+ const { style: rootStyle, html: impliedHtml } = cascade.rootStyle(
791
+ hasBody(root),
792
+ hasHtml(root),
793
+ );
400
794
  const rootBox = new Box('block', null, rootStyle);
795
+ // a fragment's root stands in for a `<body>`, counters and all
796
+ this._scopes.open();
797
+ this._counterChanges(rootStyle, {
798
+ node: root,
799
+ style: rootStyle,
800
+ key: ROOT_SHARE_KEY,
801
+ parent: null,
802
+ });
401
803
  // The DOM's `<html>`/`<body>` are ordinary elements with ordinary styles;
402
804
  // the box above them exists only to be the initial containing block, so
403
805
  // it carries no margins of its own and cannot collapse with anything.
404
- this._children(root, rootBox, rootStyle, false);
405
- fixUp(rootBox);
806
+ this._children(root, rootBox, rootStyle, false, null, ROOT_SHARE_KEY);
807
+ this._endLine();
808
+ fixUp(rootBox, anonymousStyles(cascade.initial));
406
809
  assignSubtreeRanges(rootBox);
407
810
  return {
408
811
  root: rootBox,
409
- text: this._text,
812
+ text: this._chunks.join(''),
410
813
  textBoxes: this._textBoxes,
814
+ textStyles: this._textStyles,
815
+ styles: this._styles,
411
816
  controls: this._controls,
412
817
  links: this._links,
818
+ backgrounds: this._backgrounds,
819
+ contentImages: this._contentImages,
820
+ relative: this._relative || isRelative(rootStyle),
821
+ negative: this._negative,
822
+ firstLine: this._firstLine,
823
+ firstLineStyler: this._firstLine ? this._firstLineStyler() : null,
824
+ nestedOutOfLine: this._nestedOutOfLine,
825
+ movedInline: this._movedInline,
826
+ clipText: this._clipText,
827
+ impliedHtml,
828
+ };
829
+ }
830
+
831
+ /** `BoxTree.firstLineStyler`, over this build's cascade. */
832
+ private _firstLineStyler(): FirstLineStyler {
833
+ const cascade = this._options.cascade;
834
+ const styles = this._styles;
835
+ return (box, parent) => {
836
+ const up = box.parent?.style ?? box.style;
837
+ const inherits = firstLineParent(up, parent);
838
+ const el = box.kind === 'text' ? null : box.el;
839
+ const own = el ? styles.get(el) : undefined;
840
+ if (el && own && own.style === box.style) {
841
+ return cascade.styleFor(el, inherits, own.inFlex);
842
+ }
843
+ // text, an anonymous box, a pseudo-element's: no rules of its own to
844
+ // be asked again, so what it took from its parent it takes from the
845
+ // parent's style on the line, and what it set it keeps
846
+ const out = copyStyle(box.style);
847
+ const to = out as unknown as Record<string, unknown>;
848
+ const mine = box.style as unknown as Record<string, unknown>;
849
+ const theirs = up as unknown as Record<string, unknown>;
850
+ const line = inherits as unknown as Record<string, unknown>;
851
+ for (const name of FIRST_LINE_INHERITED) {
852
+ if (mine[name] === theirs[name]) to[name] = line[name];
853
+ }
854
+ return out;
413
855
  };
414
856
  }
415
857
 
@@ -419,15 +861,33 @@ class Builder {
419
861
  into: Box,
420
862
  parentStyle: ComputedStyle,
421
863
  inFlex: boolean,
422
- owner: Element | null = null,
864
+ owner: Element | null,
865
+ parentKey: number,
423
866
  ): void {
867
+ // CSS 2.1 17.2.1: a column group holds columns, and anything else in it
868
+ // is not rendered
869
+ const onlyColumns = parentStyle.display === 'table-column-group';
870
+ // A closed `<details>` shows its first `<summary>` and nothing else:
871
+ // HTML renders the rest into a slot that is out of the box tree until
872
+ // the element is `open`.
873
+ const closed =
874
+ owner !== null &&
875
+ tagOf(owner) === 'details' &&
876
+ owner.attribs.open === undefined;
877
+ let summarised = false;
424
878
  for (const child of childrenOf(node as Element)) {
879
+ if (closed) {
880
+ if (!isElement(child) || summarised || tagOf(child) !== 'summary') {
881
+ continue;
882
+ }
883
+ summarised = true;
884
+ }
425
885
  if (isText(child)) {
426
- this._textNode(child.data, into, parentStyle, owner);
886
+ if (!onlyColumns) this._textNode(child.data, into, parentStyle, owner);
427
887
  continue;
428
888
  }
429
889
  if (!isElement(child)) continue;
430
- this._element(child, into, parentStyle, inFlex);
890
+ this._element(child, into, parentStyle, inFlex, parentKey, onlyColumns);
431
891
  }
432
892
  }
433
893
 
@@ -436,23 +896,116 @@ class Builder {
436
896
  into: Box,
437
897
  parentStyle: ComputedStyle,
438
898
  inFlex: boolean,
899
+ parentKey: number,
900
+ onlyColumns = false,
439
901
  ): void {
440
902
  const tag = tagOf(el);
441
- if (NON_RENDERED.has(tag)) return;
442
-
443
- const style = this._options.cascade.styleFor(el, parentStyle, inFlex);
903
+ if (NON_RENDERED.has(tag) || inImpliedHead(el, tag)) return;
904
+
905
+ // shared with every element that must compute the same style, which in
906
+ // a long document is most of them (`Cascade.sharedStyleFor`)
907
+ const { style, key } = this._options.cascade.sharedStyleFor(
908
+ el,
909
+ parentStyle,
910
+ parentKey,
911
+ inFlex,
912
+ );
913
+ this._styles.set(el, { style, inFlex });
444
914
  if (style.display === 'none') return;
915
+ if (onlyColumns && style.display !== 'table-column') return;
916
+ // what an element counts is in scope for it and what it holds, and for
917
+ // what follows it where its parent has no such counter
918
+ // (`CounterScopes`)
919
+ this._scopes.open();
920
+ // style containment keeps what the element's subtree does to counters
921
+ // and quotes in it (CSS Containment 2, 3.4)
922
+ const contains =
923
+ (style.contain & CONTAIN_STYLE) !== 0 && style.display !== 'contents';
924
+ const quoteDepth = this._quoteDepth;
925
+ if (contains) this._scopes.contain();
926
+ this._elementIn(el, tag, style, key, into, inFlex, {
927
+ node: el,
928
+ style,
929
+ key,
930
+ parent: { style: parentStyle, key: parentKey, inFlex },
931
+ });
932
+ if (contains) this._quoteDepth = quoteDepth;
933
+ this._scopes.close();
934
+ }
935
+
936
+ private _elementIn(
937
+ el: Element,
938
+ tag: string,
939
+ style: ComputedStyle,
940
+ key: number,
941
+ into: Box,
942
+ inFlex: boolean,
943
+ place: CounterPlace,
944
+ ): void {
945
+ if (style.display === 'contents') {
946
+ // no box of its own (CSS Display 3, 2.5): its `::before`, its
947
+ // children and its `::after` are its parent's, in its style — the
948
+ // items of a flex row still, through a wrapper Tailwind's `contents`
949
+ // takes out — and a replaced element, which has nothing to hand on,
950
+ // is not rendered
951
+ if (tag === 'br' || replacedKind(el, tag) !== 'none') return;
952
+ this._counterChanges(style, place);
953
+ this._pseudo(el, 'before', style, into);
954
+ this._children(el, into, style, inFlex, el, key);
955
+ this._pseudo(el, 'after', style, into);
956
+ return;
957
+ }
958
+ if (isRelative(style)) {
959
+ this._relative = true;
960
+ if (style.display === 'inline' && isOffset(style)) {
961
+ this._movedInline = true;
962
+ }
963
+ }
964
+ // a translation is moved by the same pass
965
+ if (style.translate || style.transformTranslate) this._relative = true;
966
+ if (style.backgroundClipText) this._clipText = true;
967
+ if (style.verticalAlign !== 'baseline' && style.display === 'inline') {
968
+ this._movedInline = true;
969
+ }
970
+ if (isNegative(style)) this._negative = true;
971
+ // before anything else of the element's, including its `::before`,
972
+ // and for the element whatever box it makes (CSS 2.1 12.4)
973
+ this._counterChanges(style, place);
445
974
 
446
975
  // `<br>` is a line break rather than a box, and it is the one element
447
976
  // whose *absence* of a box still has to reach the inline layout.
448
977
  if (tag === 'br') {
978
+ this._endLine();
979
+ // the first line ends with no letter on it (CSS 2.1 5.12.2)
980
+ this._abandonLetter();
981
+ if (style.clear !== 'none') {
982
+ // a break that clears the floats — `<br clear="all">` after a
983
+ // floated image — puts what follows it below them: an empty block
984
+ // that clears them, which the inline content around it is broken
985
+ // for (`breakInlines`), where a line break went on beside them
986
+ const clearing = copyStyle(style);
987
+ clearing.display = 'block';
988
+ into.append(new Box('block', el, clearing));
989
+ this._ws = 'start';
990
+ return;
991
+ }
449
992
  const box = new Box('break', el, style);
450
993
  into.append(box);
451
994
  this._push('\n', box);
995
+ this._ws = 'start';
452
996
  return;
453
997
  }
454
998
 
455
- const replaced = replacedKind(el, tag);
999
+ // an `<object>` is its image once it has one, and its content until;
1000
+ // an `<embed>` its image, and a `<video>` its poster, or a frame
1001
+ const replaced =
1002
+ tag === 'object'
1003
+ ? this._options.imageSize(el)
1004
+ ? 'image'
1005
+ : 'none'
1006
+ : (tag === 'embed' || tag === 'video') && this._options.imageSize(el)
1007
+ ? 'image'
1008
+ : replacedKind(el, tag);
456
1009
  if (replaced !== 'none') {
457
1010
  this._replaced(el, tag, replaced, style, into);
458
1011
  return;
@@ -462,29 +1015,137 @@ class Builder {
462
1015
  const kind = boxKindFor(style.display);
463
1016
  const box = new Box(kind, el, style);
464
1017
  into.append(box);
1018
+ if (
1019
+ (kind === 'block' || kind === 'table-cell' || kind === 'table-caption') &&
1020
+ this._options.cascade.hasFirstLine
1021
+ ) {
1022
+ const firstLine = this._options.cascade.firstLineStyle(el, style);
1023
+ if (firstLine) {
1024
+ FIRST_LINE.set(box, firstLine);
1025
+ this._firstLine = true;
1026
+ }
1027
+ }
1028
+ if (
1029
+ style.backgroundImage ||
1030
+ style.backgroundImages ||
1031
+ typeof style.borderImage.source === 'string'
1032
+ ) {
1033
+ this._backgrounds.push(box);
1034
+ }
465
1035
  if (style.position === 'absolute' || style.position === 'fixed')
466
1036
  box.outOfFlow = true;
467
1037
  else if (style.float !== 'none') box.isFloat = true;
1038
+ if (into.kind === 'inline' && (box.outOfFlow || box.isFloat))
1039
+ this._nestedOutOfLine = true;
468
1040
 
469
1041
  if (attr(el, 'href') && (tag === 'a' || tag === 'area'))
470
1042
  this._links.push(box);
471
1043
 
472
- if (style.display === 'list-item') {
473
- box.markerText = markerFor(el, style, this._counters);
1044
+ // a column's content is not rendered at all (CSS 2.1 17.2.1)
1045
+ if (style.display === 'table-column') return;
1046
+
1047
+ let insideMarker: string | null = null;
1048
+ let markerStyle: ComputedStyle | null = null;
1049
+ let ownMarker = false;
1050
+ let markerImage: { url: string; size: IntrinsicSize } | null = null;
1051
+ let insideImage: string | null = null;
1052
+ if (style.display === 'list-item' && style.listStyleImage) {
1053
+ // asked for as generated content's images are, and the tree built
1054
+ // again when it arrives; until it has, and where it never does, the
1055
+ // item's marker is its `list-style-type`'s
1056
+ const url = style.listStyleImage;
1057
+ const size = this._options.urlSize?.(url) ?? null;
1058
+ this._contentImages.push({ url, element: el, sized: !!size });
1059
+ if (size) markerImage = { url, size };
474
1060
  }
475
- const opensCounter = tag === 'ol' || tag === 'ul';
476
- if (opensCounter) {
477
- const start = Number(attr(el, 'start') ?? '1');
478
- this._counters.push(Number.isFinite(start) ? start : 1);
1061
+ if (style.display === 'list-item') {
1062
+ // the item's number, the `list-item` counter it has just counted
1063
+ const written = markerFor(
1064
+ style,
1065
+ this._scopes.value('list-item'),
1066
+ this._options.cascade.counterStyles,
1067
+ );
1068
+ let text = written.text;
1069
+ let flush = written.flush;
1070
+ markerStyle = this._options.cascade.markerStyle(el, style);
1071
+ // a `::marker` with a `content` is set as that, as it is written —
1072
+ // its strings and its counters, where one of `none` is no marker
1073
+ const content = markerStyle?.content;
1074
+ if (content === 'none') text = '';
1075
+ else if (Array.isArray(content) && markerStyle) {
1076
+ text = this._generated(content, markerStyle, el)
1077
+ .filter((piece) => typeof piece === 'string')
1078
+ .join('');
1079
+ ownMarker = true;
1080
+ flush = true;
1081
+ } else if (style.listStyleType.startsWith('"')) {
1082
+ ownMarker = true;
1083
+ flush = true;
1084
+ }
1085
+ if (markerImage && !ownMarker) {
1086
+ if (style.listStylePosition === 'inside') insideImage = markerImage.url;
1087
+ else {
1088
+ const scale = this._options.scale ?? 1;
1089
+ box.marker = {
1090
+ text: '',
1091
+ layout: null,
1092
+ x: 0,
1093
+ y: 0,
1094
+ style: markerStyle,
1095
+ image: {
1096
+ url: markerImage.url,
1097
+ width: (markerImage.size.width ?? 0) * scale,
1098
+ height: (markerImage.size.height ?? 0) * scale,
1099
+ },
1100
+ };
1101
+ }
1102
+ } else if (text && style.listStylePosition === 'inside') {
1103
+ insideMarker = flush ? text : `${text} `;
1104
+ } else if (text) {
1105
+ box.marker = { text, layout: null, x: 0, y: 0, style: markerStyle };
1106
+ if (flush) box.marker.flush = true;
1107
+ }
479
1108
  }
480
-
481
1109
  const childInFlex =
482
1110
  style.display === 'flex' || style.display === 'inline-flex';
1111
+ const flow = flowOf(style, box);
1112
+ const around = this._ws;
1113
+ const aroundWord = this._lettered;
1114
+ if (flow === 'block') this._endLine();
1115
+ if (flow !== 'inline') this._ws = 'start';
1116
+ // A first letter is looked for in the first line of a block container,
1117
+ // down through its inline content and its first blocks. A float, a
1118
+ // positioned box and a flex container are no part of that line, and
1119
+ // an atomic inline is something other than a letter at its start
1120
+ // (CSS 2.1 5.12.2). A block container with rules of its own takes the
1121
+ // search over.
1122
+ const outerLetter = this._firstLetter;
1123
+ const skipped = flow === 'out' || flow === 'atomic' || kind === 'flex';
1124
+ if (skipped) this._firstLetter = null;
1125
+ // a block starts a line, and punctuation before it was on another
1126
+ else if (flow === 'block' && outerLetter) giveBack(outerLetter);
1127
+ const ownRules = hasFirstLetter(style.display)
1128
+ ? this._options.cascade.firstLetterRules(el)
1129
+ : null;
1130
+ const ownLetter = ownRules ? { rules: ownRules, punctuation: [] } : null;
1131
+ if (ownLetter) this._firstLetter = ownLetter;
483
1132
  this._depth += 1;
484
- this._children(el, box, style, childInFlex, el);
1133
+ if (insideImage) {
1134
+ // an inline image at the start of the first line, as a generated
1135
+ // image is, and the space a marker's text ends in
1136
+ this._contentImage(insideImage, box, style, el);
1137
+ this._textNode(' ', box, style, el);
1138
+ } else if (insideMarker) {
1139
+ this._insideMarker(insideMarker, style, markerStyle, box, el);
1140
+ }
1141
+ this._pseudo(el, 'before', style, box);
1142
+ this._children(el, box, style, childInFlex, el, key);
1143
+ this._pseudo(el, 'after', style, box);
485
1144
  this._depth -= 1;
486
-
487
- if (opensCounter) this._counters.pop();
1145
+ this._letterAfter(flow, skipped, outerLetter, ownLetter);
1146
+ if (flow !== 'inline') this._endLine();
1147
+ this._ws = after(flow, this._ws, around);
1148
+ this._lettered = letteredAfterFlow(flow, this._lettered, aroundWord);
488
1149
  }
489
1150
 
490
1151
  private _replaced(
@@ -497,9 +1158,23 @@ class Builder {
497
1158
  const box = new Box('replaced', el, style);
498
1159
  box.replaced = replaced;
499
1160
  into.append(box);
1161
+ if (
1162
+ style.backgroundImage ||
1163
+ style.backgroundImages ||
1164
+ typeof style.borderImage.source === 'string'
1165
+ ) {
1166
+ this._backgrounds.push(box);
1167
+ }
500
1168
  if (style.position === 'absolute' || style.position === 'fixed')
501
1169
  box.outOfFlow = true;
502
1170
  else if (style.float !== 'none') box.isFloat = true;
1171
+ if (into.kind === 'inline' && (box.outOfFlow || box.isFloat))
1172
+ this._nestedOutOfLine = true;
1173
+ const flow = flowOf(style, box);
1174
+ if (flow === 'block') this._endLine();
1175
+ this._ws = after(flow, this._ws, this._ws);
1176
+ this._lettered = letteredAfterFlow(flow, this._lettered, this._lettered);
1177
+ if (flow === 'atomic') this._abandonLetter();
503
1178
 
504
1179
  if (replaced === 'image') {
505
1180
  // Both sources are CSS pixels — an image pixel is one, and so is an
@@ -507,15 +1182,20 @@ class Builder {
507
1182
  const scale = this._options.scale ?? 1;
508
1183
  const loaded = this._options.imageSize(el);
509
1184
  if (loaded) {
510
- box.intrinsicWidth = loaded.width * scale;
511
- box.intrinsicHeight = loaded.height * scale;
1185
+ setIntrinsics(box, loaded, scale);
512
1186
  } else {
513
1187
  // An image that has not arrived still needs a box, or the document
514
1188
  // reflows under the reader when it does. The attributes are the
515
1189
  // author telling us the size in advance; without them the box is a
516
1190
  // small placeholder rather than nothing.
517
- box.intrinsicWidth = (numberAttr(el, 'width') ?? 0) * scale;
518
- box.intrinsicHeight = (numberAttr(el, 'height') ?? 0) * scale;
1191
+ const width = numberAttr(el, 'width') ?? 0;
1192
+ const height = numberAttr(el, 'height') ?? 0;
1193
+ box.intrinsic = {
1194
+ width: width * scale,
1195
+ height: height * scale,
1196
+ missing: 0,
1197
+ ratio: width > 0 && height > 0 ? width / height : 0,
1198
+ };
519
1199
  }
520
1200
  // The alt text joins the document text, so a document read with the
521
1201
  // images blocked still copies as prose.
@@ -525,16 +1205,434 @@ class Builder {
525
1205
  }
526
1206
 
527
1207
  if (replaced === 'hr') return;
1208
+ if (replaced === 'svg') {
1209
+ // its `em` is its own font size, in CSS pixels like the rest of it
1210
+ const scale = this._options.scale ?? 1;
1211
+ setIntrinsics(box, svgIntrinsics(el, style.fontSize / scale), scale);
1212
+ return;
1213
+ }
1214
+ if (replaced === 'frame') {
1215
+ const scale = this._options.scale ?? 1;
1216
+ if (tag === 'canvas') {
1217
+ // A canvas is the size of its bitmap, which its `width` and
1218
+ // `height` attributes give, 300 by 150 where they do not, and
1219
+ // keeps its proportions (HTML 4.12.5): no script draws in it, so it
1220
+ // is that box with nothing in it. Taken for an element with no box
1221
+ // of its own, it took no room, and one set a height kept no width.
1222
+ const width = canvasSize(el, 'width', 300);
1223
+ const height = canvasSize(el, 'height', 150);
1224
+ box.intrinsic = {
1225
+ width: width * scale,
1226
+ height: height * scale,
1227
+ missing: 0,
1228
+ ratio: width > 0 && height > 0 ? width / height : 0,
1229
+ };
1230
+ return;
1231
+ }
1232
+ // HTML's default object size, in CSS pixels; `width` and `height`
1233
+ // attributes reach the style as presentational hints and win
1234
+ box.intrinsic = {
1235
+ width: 300 * scale,
1236
+ height: 150 * scale,
1237
+ missing: 0,
1238
+ ratio: 0,
1239
+ };
1240
+ return;
1241
+ }
528
1242
 
529
1243
  const size = this._options.controlSize(el, replaced, style);
530
- box.intrinsicWidth = size.width;
531
- box.intrinsicHeight = size.height;
1244
+ box.intrinsic = {
1245
+ width: size.width,
1246
+ height: size.height,
1247
+ missing: 0,
1248
+ ratio: 0,
1249
+ };
532
1250
  this._controls.push(box);
533
1251
  // A control's value is the widget's, not the document's: putting it in
534
1252
  // the selection index would make Ctrl+A copy the contents of every text
535
1253
  // field, which no document viewer does.
536
1254
  }
537
1255
 
1256
+ /**
1257
+ * An element's `::before` or `::after`, when a rule gives it content: a
1258
+ * box of its own `display`, holding the text its `content` comes to, which
1259
+ * goes through the same white-space processing as the document's text.
1260
+ */
1261
+ private _pseudo(
1262
+ el: Element,
1263
+ which: 'before' | 'after',
1264
+ elementStyle: ComputedStyle,
1265
+ into: Box,
1266
+ ): void {
1267
+ const style = this._options.cascade.pseudoStyleFor(el, which, elementStyle);
1268
+ if (!style || style.display === 'none') return;
1269
+ if (isRelative(style)) {
1270
+ this._relative = true;
1271
+ if (style.display === 'inline' && isOffset(style)) {
1272
+ this._movedInline = true;
1273
+ }
1274
+ }
1275
+ // a translation is moved by the same pass
1276
+ if (style.translate || style.transformTranslate) this._relative = true;
1277
+ if (style.backgroundClipText) this._clipText = true;
1278
+ if (style.verticalAlign !== 'baseline' && style.display === 'inline') {
1279
+ this._movedInline = true;
1280
+ }
1281
+ if (isNegative(style)) this._negative = true;
1282
+ // a column renders no content, and generated content is all it would
1283
+ // hold; in a column group it is not a column either (CSS 2.1 17.2.1)
1284
+ if (
1285
+ style.display === 'table-column' ||
1286
+ style.display === 'table-column-group' ||
1287
+ elementStyle.display === 'table-column-group'
1288
+ ) {
1289
+ return;
1290
+ }
1291
+ // a scope of its own, which what it counts is in where its element has
1292
+ // the counter already (`CounterScopes`)
1293
+ this._scopes.open();
1294
+ this._counterChanges(style, null);
1295
+ const box = new Box(boxKindFor(style.display), null, style);
1296
+ box.pseudo = which;
1297
+ into.append(box);
1298
+ // a list item it generates has a marker as an element's has, of the
1299
+ // `list-item` counter it has just counted (CSS 2.1 12.5): outside it,
1300
+ // or at the start of its content
1301
+ const written =
1302
+ style.display === 'list-item'
1303
+ ? markerFor(
1304
+ style,
1305
+ this._scopes.value('list-item'),
1306
+ this._options.cascade.counterStyles,
1307
+ )
1308
+ : null;
1309
+ let marker = written?.text ?? '';
1310
+ if (marker && style.listStylePosition !== 'inside') {
1311
+ box.marker = { text: marker, layout: null, x: 0, y: 0, style: null };
1312
+ if (written?.flush) box.marker.flush = true;
1313
+ marker = '';
1314
+ }
1315
+ if (style.position === 'absolute' || style.position === 'fixed')
1316
+ box.outOfFlow = true;
1317
+ else if (style.float !== 'none') box.isFloat = true;
1318
+ if (into.kind === 'inline' && (box.outOfFlow || box.isFloat))
1319
+ this._nestedOutOfLine = true;
1320
+ const pieces = this._generated(style.content as ContentItem[], style, el);
1321
+ if (marker) pieces.unshift(written?.flush ? marker : `${marker} `);
1322
+ this._scopes.close();
1323
+ const flow = flowOf(style, box);
1324
+ const around = this._ws;
1325
+ const aroundWord = this._lettered;
1326
+ if (flow === 'block') this._endLine();
1327
+ if (flow !== 'inline') this._ws = 'start';
1328
+ // the first letter can be generated, and is looked for here as in an
1329
+ // element of the same display
1330
+ const outerLetter = this._firstLetter;
1331
+ const skipped = flow === 'out' || flow === 'atomic';
1332
+ if (skipped) this._firstLetter = null;
1333
+ else if (flow === 'block' && outerLetter) giveBack(outerLetter);
1334
+ for (const piece of pieces) {
1335
+ if (typeof piece === 'string') this._textNode(piece, box, style, el);
1336
+ else this._contentImage(piece.url, box, style, el);
1337
+ }
1338
+ this._letterAfter(flow, skipped, outerLetter, null);
1339
+ if (flow !== 'inline') this._endLine();
1340
+ this._ws = after(flow, this._ws, around);
1341
+ this._lettered = letteredAfterFlow(flow, this._lettered, aroundWord);
1342
+ }
1343
+
1344
+ /**
1345
+ * An `inside` list marker: an inline box at the start of the item, the
1346
+ * marker and a space, which takes its room on the first line as a
1347
+ * `::marker` does (CSS Lists 3, 3.1), where a marker set beside the line
1348
+ * was drawn over its first letters — every `list-style-position: inside`
1349
+ * list, and every `<summary>`. It inherits the item's text style and has
1350
+ * none of its box.
1351
+ */
1352
+ private _insideMarker(
1353
+ text: string,
1354
+ itemStyle: ComputedStyle,
1355
+ markerStyle: ComputedStyle | null,
1356
+ into: Box,
1357
+ el: Element,
1358
+ ): void {
1359
+ const style = inherit(
1360
+ markerStyle ?? itemStyle,
1361
+ this._options.cascade.initial,
1362
+ );
1363
+ style.display = 'inline';
1364
+ // a marker's direction is its own, whatever the text after it, unless
1365
+ // its rules say otherwise (the HTML style sheet's `::marker`)
1366
+ style.unicodeBidi = markerStyle?.unicodeBidi ?? 'isolate';
1367
+ const box = new Box('inline', null, style);
1368
+ box.pseudo = 'before';
1369
+ into.append(box);
1370
+ this._textNode(text, box, style, el);
1371
+ }
1372
+
1373
+ /**
1374
+ * `counter-reset`, then `counter-increment`, then `counter-set`, as CSS
1375
+ * Lists 3 orders them (4.2). A list item counts `list-item` besides,
1376
+ * unless its `counter-increment` names it: one up, or one down in a list
1377
+ * that counts down. `place` is where an element is, for a reversed
1378
+ * counter to be counted from; a pseudo-element has none.
1379
+ */
1380
+ private _counterChanges(
1381
+ style: ComputedStyle,
1382
+ place: CounterPlace | null,
1383
+ ): void {
1384
+ const resets = style.counterReset;
1385
+ const increments = style.counterIncrement;
1386
+ const sets = style.counterSet;
1387
+ const listItem = style.display === 'list-item';
1388
+ if (!resets && !increments && !sets && !listItem) return;
1389
+ if (resets) {
1390
+ for (let i = 0; i < resets.length; i += 1) {
1391
+ const change = resets[i];
1392
+ // a name reset twice is reset by the last
1393
+ if (resets.some((c, j) => j > i && c.name === change.name)) continue;
1394
+ const value = change.counted
1395
+ ? this._countedStart(change.name, style, place)
1396
+ : change.value;
1397
+ this._scopes.reset(change.name, value, !!change.reversed);
1398
+ }
1399
+ }
1400
+ let counted = false;
1401
+ for (const { name, value } of increments ?? []) {
1402
+ this._scopes.increment(name, value);
1403
+ if (name === 'list-item') counted = true;
1404
+ }
1405
+ if (listItem && !counted) {
1406
+ this._scopes.increment(
1407
+ 'list-item',
1408
+ this._scopes.reversed('list-item') ? -1 : 1,
1409
+ );
1410
+ }
1411
+ for (const { name, value } of sets ?? []) this._scopes.set(name, value);
1412
+ }
1413
+
1414
+ /**
1415
+ * Where a reversed counter written with no number starts (CSS Lists 3,
1416
+ * 4.4.2): minus what the elements and pseudo-elements in its scope add to
1417
+ * it, as far as the first that sets it, and what that sets it to, and as
1418
+ * much again as the last of them that counted took away — 1 for a list
1419
+ * item. `<ol reversed>`'s items count down to 1, and to an item's
1420
+ * `value` before it. The scope is walked ahead of the builder, from the styles it will
1421
+ * find shared when it gets there (`Cascade.sharedStyleFor`), and only for
1422
+ * such a counter.
1423
+ */
1424
+ private _countedStart(
1425
+ name: string,
1426
+ style: ComputedStyle,
1427
+ place: CounterPlace | null,
1428
+ ): number {
1429
+ const cascade = this._options.cascade;
1430
+ /** What each element and pseudo-element in the scope does to the
1431
+ * counter, in document order: what it adds, and what it sets it to. */
1432
+ const steps: { by: number; set: number | null }[] = [];
1433
+ const note = (st: ComputedStyle): void => {
1434
+ let by = 0;
1435
+ let increments = false;
1436
+ for (const change of st.counterIncrement ?? []) {
1437
+ if (change.name !== name) continue;
1438
+ by += change.value;
1439
+ increments = true;
1440
+ }
1441
+ if (!increments && name === 'list-item' && st.display === 'list-item') {
1442
+ by = -1;
1443
+ increments = true;
1444
+ }
1445
+ let set: number | null = null;
1446
+ for (const change of st.counterSet ?? []) {
1447
+ if (change.name === name) set = change.value;
1448
+ }
1449
+ if (increments || set !== null) steps.push({ by, set });
1450
+ };
1451
+ const resets = (st: ComputedStyle): boolean =>
1452
+ st.counterReset?.some((change) => change.name === name) ?? false;
1453
+ const pseudo = (
1454
+ el: Element,
1455
+ which: 'before' | 'after',
1456
+ st: ComputedStyle,
1457
+ ): void => {
1458
+ const own = cascade.pseudoStyleFor(el, which, st);
1459
+ if (own && own.display !== 'none' && !resets(own)) note(own);
1460
+ };
1461
+ /** The elements from `from` on of a parent, and what is in them: a
1462
+ * reset of the counter in one is a counter of its own, and a later
1463
+ * sibling's reset at the scope's own level ends it (`stopAtReset`). */
1464
+ const walk = (
1465
+ children: ReturnType<typeof childrenOf>,
1466
+ from: number,
1467
+ parentStyle: ComputedStyle,
1468
+ parentKey: number,
1469
+ inFlex: boolean,
1470
+ stopAtReset: boolean,
1471
+ ): void => {
1472
+ for (let i = from; i < children.length; i += 1) {
1473
+ const child = children[i];
1474
+ if (!isElement(child)) continue;
1475
+ const tag = tagOf(child);
1476
+ if (NON_RENDERED.has(tag) || inImpliedHead(child, tag)) continue;
1477
+ const shared = cascade.sharedStyleFor(
1478
+ child,
1479
+ parentStyle,
1480
+ parentKey,
1481
+ inFlex,
1482
+ );
1483
+ const st = shared.style;
1484
+ if (st.display === 'none') continue;
1485
+ if (resets(st)) {
1486
+ if (stopAtReset) return;
1487
+ continue;
1488
+ }
1489
+ note(st);
1490
+ inside(child, st, shared.key);
1491
+ }
1492
+ };
1493
+ const inside = (el: Element, st: ComputedStyle, key: number): void => {
1494
+ pseudo(el, 'before', st);
1495
+ walk(
1496
+ childrenOf(el),
1497
+ 0,
1498
+ st,
1499
+ key,
1500
+ st.display === 'flex' || st.display === 'inline-flex',
1501
+ false,
1502
+ );
1503
+ pseudo(el, 'after', st);
1504
+ };
1505
+
1506
+ // the element that resets it counts in it too, after the reset
1507
+ note(style);
1508
+ if (place) {
1509
+ const siblings = this._scopes.reachesSiblings(name);
1510
+ inside(place.node, place.style, place.key);
1511
+ const parent = place.parent;
1512
+ const node = place.node as Element & { parent?: Element | null };
1513
+ if (siblings && parent && node.parent) {
1514
+ const children = childrenOf(node.parent);
1515
+ walk(
1516
+ children,
1517
+ children.indexOf(place.node) + 1,
1518
+ parent.style,
1519
+ parent.key,
1520
+ parent.inFlex,
1521
+ true,
1522
+ );
1523
+ }
1524
+ }
1525
+ let num = 0;
1526
+ let last = 0;
1527
+ for (const { by, set } of steps) {
1528
+ if (by !== 0) last = -by;
1529
+ if (set !== null) {
1530
+ num += set;
1531
+ break;
1532
+ }
1533
+ num -= by;
1534
+ }
1535
+ return num + last;
1536
+ }
1537
+
1538
+ /** A counter's value in a style the document can name, as `counter()`
1539
+ * writes it; `none` writes nothing. */
1540
+ private _counterText(n: number, name: string, style: ComputedStyle): string {
1541
+ if (name === 'none') return '';
1542
+ return this._options.cascade.counterStyles.text(
1543
+ n,
1544
+ name,
1545
+ style.direction === 'rtl',
1546
+ );
1547
+ }
1548
+
1549
+ /** What `content` comes to here, in document order: its text, broken
1550
+ * where it names an image. The quotes it opens and closes count for
1551
+ * everything after it. */
1552
+ private _generated(
1553
+ items: ContentItem[],
1554
+ style: ComputedStyle,
1555
+ el: Element,
1556
+ ): (string | { url: string })[] {
1557
+ const pieces: (string | { url: string })[] = [];
1558
+ let text = '';
1559
+ for (const item of items) {
1560
+ switch (item.kind) {
1561
+ case 'string':
1562
+ text += item.text;
1563
+ break;
1564
+ case 'url':
1565
+ if (text) pieces.push(text);
1566
+ text = '';
1567
+ pieces.push({ url: item.url });
1568
+ break;
1569
+ case 'attr':
1570
+ text += attr(el, item.name) ?? '';
1571
+ break;
1572
+ case 'counter':
1573
+ text += this._counterText(
1574
+ this._scopes.value(item.name),
1575
+ item.style,
1576
+ style,
1577
+ );
1578
+ break;
1579
+ case 'counters':
1580
+ text += this._scopes
1581
+ .values(item.name)
1582
+ .map((v) => this._counterText(v, item.style, style))
1583
+ .join(item.separator);
1584
+ break;
1585
+ case 'open-quote':
1586
+ text += quoteAt(style.quotes, this._quoteDepth, 0);
1587
+ this._quoteDepth += 1;
1588
+ break;
1589
+ case 'close-quote':
1590
+ // a close with nothing open writes nothing and closes nothing
1591
+ if (this._quoteDepth > 0) {
1592
+ this._quoteDepth -= 1;
1593
+ text += quoteAt(style.quotes, this._quoteDepth, 1);
1594
+ }
1595
+ break;
1596
+ case 'no-open-quote':
1597
+ this._quoteDepth += 1;
1598
+ break;
1599
+ case 'no-close-quote':
1600
+ if (this._quoteDepth > 0) this._quoteDepth -= 1;
1601
+ break;
1602
+ }
1603
+ }
1604
+ if (text) pieces.push(text);
1605
+ return pieces;
1606
+ }
1607
+
1608
+ /**
1609
+ * An image generated content names: an inline replaced box in the
1610
+ * pseudo-element, of the style it inherits and no other (CSS 2.1 12.2),
1611
+ * the image's size once the host has it and none before. An image that
1612
+ * never arrives takes no room, as no image does.
1613
+ */
1614
+ private _contentImage(
1615
+ url: string,
1616
+ into: Box,
1617
+ style: ComputedStyle,
1618
+ owner: Element,
1619
+ ): void {
1620
+ const box = new Box('replaced', null, {
1621
+ ...inherit(style, this._options.cascade.initial),
1622
+ display: 'inline',
1623
+ });
1624
+ box.replaced = 'image';
1625
+ into.append(box);
1626
+ CONTENT_IMAGES.set(box, url);
1627
+ const size = this._options.urlSize?.(url) ?? null;
1628
+ this._contentImages.push({ url, element: owner, sized: !!size });
1629
+ if (size) setIntrinsics(box, size, this._options.scale ?? 1);
1630
+ else box.intrinsic = { width: 0, height: 0, missing: 0, ratio: 0 };
1631
+ this._ws = after('atomic', this._ws, this._ws);
1632
+ this._lettered = false;
1633
+ this._abandonLetter();
1634
+ }
1635
+
538
1636
  /** A text node, whitespace-processed per the inherited `white-space`. */
539
1637
  private _textNode(
540
1638
  data: string,
@@ -542,21 +1640,128 @@ class Builder {
542
1640
  style: ComputedStyle,
543
1641
  owner: Element | null,
544
1642
  ): void {
1643
+ // text set at no size draws nothing and takes no room, so it needs no
1644
+ // box — and it is no part of the white space around it either
1645
+ if (!(style.fontSize > 0)) return;
1646
+ // a word does not run on over the start of a line
1647
+ if (this._ws === 'start') this._lettered = false;
545
1648
  const ws = style.whiteSpace;
546
1649
  let text: string;
547
1650
  if (ws === 'pre' || ws === 'pre-wrap') {
548
1651
  text = data;
549
- } else if (ws === 'pre-line') {
550
- text = data.replace(/[ \t]+/g, ' ').replace(/ *\n */g, '\n');
1652
+ if (!text) return;
1653
+ // preserved spaces do not collapse with the ones after them
1654
+ this._ws = text.endsWith('\n') ? 'start' : 'content';
551
1655
  } else {
552
- text = data.replace(/[\t\n\r\f ]+/g, ' ');
553
- // A run of whitespace against the start of a block collapses away
554
- // entirely; between two inline boxes it collapses to one space, which
555
- // the replace above already did.
556
- if (text === ' ' && !hasInlineContent(into)) return;
557
- }
558
- if (!text) return;
559
- text = transformText(text, style.textTransform);
1656
+ text =
1657
+ ws === 'pre-line'
1658
+ ? data.replace(/[ \t]+/g, ' ').replace(/ *\n */g, '\n')
1659
+ : COLLAPSIBLE.test(data)
1660
+ ? data.replace(/[\t\n\r\f ]+/g, ' ')
1661
+ : data;
1662
+ // A space at the start of a line goes, and so does one after another
1663
+ // space, across element boundaries (CSS 2.1 16.6.1): `<p>\n Hi` has
1664
+ // no space before the H, and `Hi <b> there</b>` has one between.
1665
+ if (text.charCodeAt(0) === 32 && this._ws !== 'content') {
1666
+ text = text.slice(1);
1667
+ }
1668
+ if (!text) return;
1669
+ const last = text.charCodeAt(text.length - 1);
1670
+ this._ws = last === 32 ? 'space' : last === 10 ? 'start' : 'content';
1671
+ }
1672
+ text = transformText(text, style.textTransform, this._lettered);
1673
+ this._lettered = letteredAfter(text, this._lettered);
1674
+ const search = this._firstLetter;
1675
+ const letter = search ? FIRST_LETTER.exec(text) : null;
1676
+ if (!search || !letter) {
1677
+ const punctuation = search ? PUNCTUATION_ONLY.exec(text) : null;
1678
+ if (!search || !punctuation) {
1679
+ this._textBox(text, into, style, owner);
1680
+ return;
1681
+ }
1682
+ // punctuation the letter comes after, in a text of its own —
1683
+ // `<q>`'s open quote — takes the letter's style, and gives it back
1684
+ // if no letter follows on the line
1685
+ const start = punctuation[1].length;
1686
+ if (start > 0) this._textBox(text.slice(0, start), into, style, owner);
1687
+ search.punctuation.push(
1688
+ this._letterBox(search, text.slice(start), into, style, owner),
1689
+ );
1690
+ return;
1691
+ }
1692
+ // The first letter, with the punctuation around it, in a box of its
1693
+ // own inside the box it was found in, and so inheriting from that
1694
+ // (CSS 2.1 5.12.2): `<p><b>T</b>his` has a bold first letter.
1695
+ this._firstLetter = null;
1696
+ const start = letter[1].length;
1697
+ const end = letter[0].length;
1698
+ if (start > 0) this._textBox(text.slice(0, start), into, style, owner);
1699
+ this._letterBox(search, text.slice(start, end), into, style, owner);
1700
+ if (end < text.length) {
1701
+ this._textBox(text.slice(end), into, style, owner);
1702
+ }
1703
+ }
1704
+
1705
+ /** A box of the first letter's style around `text`, in `into`. */
1706
+ private _letterBox(
1707
+ search: LetterSearch,
1708
+ text: string,
1709
+ into: Box,
1710
+ style: ComputedStyle,
1711
+ owner: Element | null,
1712
+ ): { box: Box; text: Box; style: ComputedStyle } {
1713
+ const cascade = this._options.cascade;
1714
+ const letterStyle = cascade.firstLetterStyle(search.rules, style);
1715
+ const box = new Box(boxKindFor(letterStyle.display), null, letterStyle);
1716
+ box.pseudo = 'first-letter';
1717
+ if (letterStyle.float !== 'none') {
1718
+ box.isFloat = true;
1719
+ if (into.kind === 'inline') this._nestedOutOfLine = true;
1720
+ }
1721
+ into.append(box);
1722
+ if (letterStyle.textTransform !== style.textTransform) {
1723
+ text = transformText(text, letterStyle.textTransform);
1724
+ }
1725
+ return { box, text: this._textBox(text, box, letterStyle, owner), style };
1726
+ }
1727
+
1728
+ /**
1729
+ * Where the search for a first letter stands after a box. A box that was
1730
+ * no part of the line — a float, a positioned box — leaves it where it
1731
+ * was, and one that was something other than a letter on it ends it. A
1732
+ * block that looked for a letter of its own and found its first line
1733
+ * ended its parent's too, and one that found no line leaves the parent
1734
+ * looking. A block ends the line it is on.
1735
+ */
1736
+ private _letterAfter(
1737
+ flow: 'inline' | 'atomic' | 'block' | 'out',
1738
+ skipped: boolean,
1739
+ outer: LetterSearch | null,
1740
+ own: LetterSearch | null,
1741
+ ): void {
1742
+ const lined = own !== null && this._firstLetter !== own;
1743
+ if (own && !lined) this._abandonLetter();
1744
+ if (skipped || own) this._firstLetter = skipped || !lined ? outer : null;
1745
+ if (lined && !skipped && outer) giveBack(outer);
1746
+ if (flow === 'atomic') this._abandonLetter();
1747
+ else if (flow === 'block' && this._firstLetter) giveBack(this._firstLetter);
1748
+ }
1749
+
1750
+ /** The first line ended, or began with something other than a letter:
1751
+ * there is no first letter, and punctuation that took its style gives
1752
+ * it back. */
1753
+ private _abandonLetter(): void {
1754
+ const search = this._firstLetter;
1755
+ this._firstLetter = null;
1756
+ if (search) giveBack(search);
1757
+ }
1758
+
1759
+ private _textBox(
1760
+ text: string,
1761
+ into: Box,
1762
+ style: ComputedStyle,
1763
+ owner: Element | null,
1764
+ ): Box {
560
1765
  // The owning element rides on the text box, and from there onto the
561
1766
  // `TextRun`: hit testing inside a paragraph has no rectangle to test —
562
1767
  // an inline box is the runs on its lines — so the run is what has to
@@ -565,20 +1770,228 @@ class Builder {
565
1770
  box.text = text;
566
1771
  into.append(box);
567
1772
  this._push(text, box);
1773
+ return box;
1774
+ }
1775
+
1776
+ /**
1777
+ * A line ends here — a block's inline content is over, or a `<br>` or a
1778
+ * block interrupts it — and a collapsible space it ends on goes (CSS 2.1
1779
+ * 16.6.1). Content standing on a space means that space was the last
1780
+ * thing pushed, so taking it back is a character off the last chunk; a
1781
+ * text box it empties goes with it.
1782
+ */
1783
+ private _endLine(): void {
1784
+ if (this._ws !== 'space') return;
1785
+ this._ws = 'start';
1786
+ const box = this._textBoxes[this._textBoxes.length - 1];
1787
+ if (!box || box.textEnd !== this._length || !box.text.endsWith(' ')) {
1788
+ return;
1789
+ }
1790
+ box.text = box.text.slice(0, -1);
1791
+ box.textEnd -= 1;
1792
+ this._length -= 1;
1793
+ const last = this._chunks.length - 1;
1794
+ this._chunks[last] = this._chunks[last].slice(0, -1);
1795
+ if (!box.text) {
1796
+ this._textBoxes.pop();
1797
+ const siblings = box.parent?.children;
1798
+ const at = siblings ? siblings.lastIndexOf(box) : -1;
1799
+ if (at >= 0) siblings!.splice(at, 1);
1800
+ }
568
1801
  }
569
1802
 
570
1803
  /** Give a box its slice of the document text index. */
571
1804
  private _push(text: string, box: Box): void {
572
- box.textStart = this._text.length;
573
- this._text += text;
574
- box.textEnd = this._text.length;
575
- if (box.kind === 'text') this._textBoxes.push(box);
1805
+ box.textStart = this._length;
1806
+ this._chunks.push(text);
1807
+ this._length += text.length;
1808
+ box.textEnd = this._length;
1809
+ if (box.kind === 'text') {
1810
+ this._textBoxes.push(box);
1811
+ if (box.style !== this._lastTextStyle) {
1812
+ this._lastTextStyle = box.style;
1813
+ this._textStyles.add(box.style);
1814
+ }
1815
+ }
576
1816
  }
577
1817
  }
578
1818
 
1819
+ /**
1820
+ * The CSS counters in scope as the builder walks the document (CSS Lists 3,
1821
+ * 4.5). Every element and pseudo-element opens a level, which closes after
1822
+ * what it holds. A `counter-reset` makes an instance that reaches the
1823
+ * element's descendants and — where its parent has no counter of the name
1824
+ * — its later siblings too, so that instance belongs to the level the
1825
+ * element is on, its parent's, and goes when that closes; a later sibling's
1826
+ * reset takes its place. Where its parent has one, the new instance nests
1827
+ * in it and reaches the element's descendants alone, on the element's own
1828
+ * level: a list in a list item, a counter reset in a `::before`. `counter()`
1829
+ * reads the innermost instance and `counters()` all of them, outermost
1830
+ * first. A counter used where none is in scope is reset to 0 there, as
1831
+ * though the element had asked.
1832
+ */
1833
+ class CounterScopes {
1834
+ /** Per name, its instances, outermost first: the level each goes with,
1835
+ * whether it is the own one of the element that level is, and whether it
1836
+ * counts down. */
1837
+ private _instances = new Map<string, CounterInstance[]>();
1838
+ /** The names each open level made an instance of, so closing it drops
1839
+ * exactly those: null for a level that made none, which is nearly every
1840
+ * one, so that opening an element allocates nothing. */
1841
+ private _made: (string[] | null)[] = [null];
1842
+ /** The levels of the open elements with style containment, innermost
1843
+ * last: below one, a counter made further out is not counted on. */
1844
+ private _contained: number[] = [];
1845
+
1846
+ open(): void {
1847
+ this._made.push(null);
1848
+ }
1849
+
1850
+ close(): void {
1851
+ const level = this._made.length - 1;
1852
+ const made = this._made.pop();
1853
+ if (made) {
1854
+ for (const name of made) {
1855
+ const stack = this._instances.get(name);
1856
+ if (stack && stack[stack.length - 1]?.level === level) stack.pop();
1857
+ }
1858
+ }
1859
+ const contained = this._contained;
1860
+ if (contained[contained.length - 1] === level) contained.pop();
1861
+ }
1862
+
1863
+ /** The element whose level is open has style containment. */
1864
+ contain(): void {
1865
+ this._contained.push(this._made.length - 1);
1866
+ }
1867
+
1868
+ /** Whether the parent of the element whose level is open has a counter
1869
+ * of this name: its own, or one from further out — not one a sibling
1870
+ * before the element made. */
1871
+ private _parentHas(name: string): boolean {
1872
+ const stack = this._instances.get(name);
1873
+ if (!stack?.length) return false;
1874
+ const parent = this._made.length - 2;
1875
+ for (let i = stack.length - 1; i >= 0; i -= 1) {
1876
+ const instance = stack[i];
1877
+ if (instance.level < parent) return true;
1878
+ if (instance.level === parent && instance.own) return true;
1879
+ }
1880
+ return false;
1881
+ }
1882
+
1883
+ /** Whether a reset of `name` here reaches the element's later siblings,
1884
+ * or its descendants alone. */
1885
+ reachesSiblings(name: string): boolean {
1886
+ return this._made.length < 2 || !this._parentHas(name);
1887
+ }
1888
+
1889
+ reset(name: string, value: number, reversed: boolean): void {
1890
+ const own = !this.reachesSiblings(name);
1891
+ const level = own ? this._made.length - 1 : this._made.length - 2;
1892
+ let stack = this._instances.get(name);
1893
+ if (!stack) {
1894
+ stack = [];
1895
+ this._instances.set(name, stack);
1896
+ }
1897
+ const top = stack[stack.length - 1];
1898
+ if (top?.level === level && top.own === own) {
1899
+ top.value = value;
1900
+ top.reversed = reversed;
1901
+ return;
1902
+ }
1903
+ stack.push({ level, own, value, reversed });
1904
+ (this._made[level] ??= []).push(name);
1905
+ }
1906
+
1907
+ /** The innermost instance, made here at 0 where there is none. */
1908
+ private _innermost(name: string): CounterInstance {
1909
+ let stack = this._instances.get(name);
1910
+ if (!stack?.length) {
1911
+ this.reset(name, 0, false);
1912
+ stack = this._instances.get(name)!;
1913
+ }
1914
+ return stack[stack.length - 1];
1915
+ }
1916
+
1917
+ increment(name: string, by: number): void {
1918
+ const instance = this._counted(name);
1919
+ instance.value = clampCounter(instance.value + by);
1920
+ }
1921
+
1922
+ set(name: string, value: number): void {
1923
+ this._counted(name).value = value;
1924
+ }
1925
+
1926
+ /** The instance an increment or a set counts on: the innermost, unless
1927
+ * it was made outside an element with style containment that this is
1928
+ * inside, where a new one is made instead, as though this reset it for
1929
+ * itself and its later siblings — which one outside the element's parent
1930
+ * then does not see (CSS Containment 2, 3.4). */
1931
+ private _counted(name: string): CounterInstance {
1932
+ const instance = this._innermost(name);
1933
+ const bound = this._contained[this._contained.length - 1];
1934
+ const level = this._made.length - 1;
1935
+ if (bound === undefined || level <= bound || instance.level >= bound) {
1936
+ return instance;
1937
+ }
1938
+ const parent = level - 1;
1939
+ const fresh = { level: parent, own: false, value: 0, reversed: false };
1940
+ this._instances.get(name)!.push(fresh);
1941
+ (this._made[parent] ??= []).push(name);
1942
+ return fresh;
1943
+ }
1944
+
1945
+ /** Whether the innermost instance counts down: a list item takes one
1946
+ * from it rather than adding one. */
1947
+ reversed(name: string): boolean {
1948
+ return this._innermost(name).reversed;
1949
+ }
1950
+
1951
+ value(name: string): number {
1952
+ return this._innermost(name).value;
1953
+ }
1954
+
1955
+ values(name: string): number[] {
1956
+ this._innermost(name);
1957
+ return this._instances.get(name)!.map((instance) => instance.value);
1958
+ }
1959
+ }
1960
+
1961
+ interface CounterInstance {
1962
+ level: number;
1963
+ own: boolean;
1964
+ value: number;
1965
+ reversed: boolean;
1966
+ }
1967
+
1968
+ /** A counter is a 32-bit integer in a browser, and saturates there. */
1969
+ function clampCounter(value: number): number {
1970
+ return Math.max(-2147483648, Math.min(2147483647, value));
1971
+ }
1972
+
1973
+ /** Where the element whose counters change is: what the walk that counts
1974
+ * a reversed counter's scope starts from (`BoxBuilder._countedStart`). */
1975
+ interface CounterPlace {
1976
+ node: Element;
1977
+ style: ComputedStyle;
1978
+ key: number;
1979
+ /** The element's parent's style and share key, and whether it is a flex
1980
+ * container: what its later siblings' styles are computed from. Null
1981
+ * for the root. */
1982
+ parent: { style: ComputedStyle; key: number; inFlex: boolean } | null;
1983
+ }
1984
+
579
1985
  /** Whether the parsed document has a `<body>`. htmlparser2 does not
580
1986
  * synthesise one — it parses what it was given — so a fragment has none,
581
1987
  * and the root box stands in for it. */
1988
+ function hasHtml(root: Element | { children: unknown }): boolean {
1989
+ for (const child of childrenOf(root as Element)) {
1990
+ if (isElement(child) && tagOf(child) === 'html') return true;
1991
+ }
1992
+ return false;
1993
+ }
1994
+
582
1995
  function hasBody(root: Element | { children: unknown }): boolean {
583
1996
  for (const child of childrenOf(root as Element)) {
584
1997
  if (!isElement(child)) continue;
@@ -589,6 +2002,14 @@ function hasBody(root: Element | { children: unknown }): boolean {
589
2002
  return false;
590
2003
  }
591
2004
 
2005
+ /** A canvas's bitmap dimension: a non-negative integer, as HTML parses
2006
+ * one, or its default where the attribute is missing or no number. */
2007
+ function canvasSize(el: Element, name: string, fallback: number): number {
2008
+ const raw = attr(el, name);
2009
+ const n = raw == null ? NaN : parseInt(raw.trim(), 10);
2010
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
2011
+ }
2012
+
592
2013
  function numberAttr(el: Element, name: string): number | null {
593
2014
  const raw = attr(el, name);
594
2015
  if (!raw) return null;
@@ -596,19 +2017,10 @@ function numberAttr(el: Element, name: string): number | null {
596
2017
  return Number.isFinite(n) ? n : null;
597
2018
  }
598
2019
 
599
- function hasInlineContent(box: Box): boolean {
600
- for (let i = box.children.length - 1; i >= 0; i -= 1) {
601
- const kind = box.children[i].kind;
602
- if (kind === 'text' || kind === 'inline' || kind === 'replaced')
603
- return true;
604
- if (kind === 'block' || kind === 'flex' || kind === 'table') return false;
605
- }
606
- return false;
607
- }
608
-
609
2020
  function transformText(
610
2021
  text: string,
611
2022
  transform: ComputedStyle['textTransform'],
2023
+ lettered = false,
612
2024
  ): string {
613
2025
  switch (transform) {
614
2026
  case 'uppercase':
@@ -616,15 +2028,270 @@ function transformText(
616
2028
  case 'lowercase':
617
2029
  return text.toLowerCase();
618
2030
  case 'capitalize':
619
- return text.replace(
620
- /(^|\s)(\S)/g,
621
- (_, sp: string, c: string) => sp + c.toUpperCase(),
622
- );
2031
+ return capitalize(text, lettered);
623
2032
  default:
624
2033
  return text;
625
2034
  }
626
2035
  }
627
2036
 
2037
+ /**
2038
+ * `text` transformed as `transform` says, where it stands for text laid out
2039
+ * already — a first line's, whose `::first-line` transforms it (CSS 2.1
2040
+ * 5.12.1): a character at a time where the transform would change its
2041
+ * length, which moves every offset after it — `ß` is `SS` in capitals —
2042
+ * and that character kept as it is.
2043
+ */
2044
+ export function transformInPlace(
2045
+ text: string,
2046
+ transform: ComputedStyle['textTransform'],
2047
+ lettered = false,
2048
+ ): string {
2049
+ const whole = transformText(text, transform, lettered);
2050
+ if (whole.length === text.length) return whole;
2051
+ let out = '';
2052
+ let state = lettered;
2053
+ for (const ch of text) {
2054
+ const one = transformText(ch, transform, state);
2055
+ out += one.length === ch.length ? one : ch;
2056
+ state = letteredAfter(ch, state);
2057
+ }
2058
+ return out;
2059
+ }
2060
+
2061
+ /** A letter or a number: what a word's first typographic letter unit is
2062
+ * (CSS Text 3, 2.1). */
2063
+ const LETTER_UNIT = /[\p{L}\p{N}]/u;
2064
+
2065
+ /**
2066
+ * What does not end a word (UAX #29): a combining mark, and the
2067
+ * punctuation that joins letters into one — `x.y`, `don't`, `a:b`. Any
2068
+ * other character that is not a letter or a number is between words: a
2069
+ * space, a no-break space, a hyphen, a bracket.
2070
+ */
2071
+ const IN_WORD =
2072
+ /[\p{M}'.:\u00b7\u0387\u05f4\u2018\u2019\u2024\u2027\ufe13\ufe52\ufe55\uff07\uff0e\uff1a]/u;
2073
+
2074
+ /**
2075
+ * `text-transform: capitalize` (CSS Text 3, 2.1): the first letter or
2076
+ * number of each word in upper case, and nothing else. Punctuation a word
2077
+ * starts with is not its first letter — `(p.p.)` is `(P.p.)` — and a word
2078
+ * the text continues, `lettered`, has had its own: `<b>fo</b>o` is `Foo`.
2079
+ */
2080
+ function capitalize(text: string, lettered: boolean): string {
2081
+ let out = '';
2082
+ let from = 0;
2083
+ let at = 0;
2084
+ for (const ch of text) {
2085
+ if (LETTER_UNIT.test(ch)) {
2086
+ if (!lettered) {
2087
+ const title = titleCase(ch);
2088
+ if (title !== ch) {
2089
+ out += text.slice(from, at) + title;
2090
+ from = at + ch.length;
2091
+ }
2092
+ lettered = true;
2093
+ }
2094
+ } else if (!IN_WORD.test(ch)) {
2095
+ lettered = false;
2096
+ }
2097
+ at += ch.length;
2098
+ }
2099
+ return from ? out + text.slice(from) : text;
2100
+ }
2101
+
2102
+ /**
2103
+ * A letter in title case (Unicode's SpecialCasing), which is its upper case
2104
+ * but for a letter that is two: a digraph is capitalized as its first
2105
+ * letter alone — `dž` as `Dž`, where upper case is `DŽ` — and a Greek vowel
2106
+ * with a subscript iota keeps the iota subscript, where upper case writes
2107
+ * it out. A letter whose upper case is two letters, as `ß` is `SS`, is the
2108
+ * first of them: `Ss`.
2109
+ */
2110
+ function titleCase(ch: string): string {
2111
+ const cp = ch.codePointAt(0)!;
2112
+ if (cp >= 0x1c4 && cp <= 0x1cc) {
2113
+ return String.fromCharCode(0x1c5 + 3 * Math.floor((cp - 0x1c4) / 3));
2114
+ }
2115
+ if (cp >= 0x1f1 && cp <= 0x1f3) return '\u01f2';
2116
+ if (cp >= 0x1f80 && cp <= 0x1faf) return String.fromCharCode(cp | 0x08);
2117
+ if (cp === 0x1fb3 || cp === 0x1fc3 || cp === 0x1ff3) {
2118
+ return String.fromCharCode(cp + 9);
2119
+ }
2120
+ if (cp === 0x1fbc || cp === 0x1fcc || cp === 0x1ffc) return ch;
2121
+ const upper = ch.toUpperCase();
2122
+ if (upper.length <= ch.length) return upper;
2123
+ const first = String.fromCodePoint(upper.codePointAt(0)!);
2124
+ return first + upper.slice(first.length).toLowerCase();
2125
+ }
2126
+
2127
+ /**
2128
+ * Whether a word is under way after `text`, as `capitalize` reads words: its
2129
+ * last letter, number or character between words decides, and a text of
2130
+ * nothing but joining punctuation leaves it as it was. Asked of every text,
2131
+ * from the end, where it is almost always decided by the last character.
2132
+ */
2133
+ export function letteredAfter(text: string, lettered: boolean): boolean {
2134
+ for (let i = text.length - 1; i >= 0; i -= 1) {
2135
+ const c = text.charCodeAt(i);
2136
+ if (c < 0x80) {
2137
+ if (
2138
+ (c >= 0x61 && c <= 0x7a) ||
2139
+ (c >= 0x41 && c <= 0x5a) ||
2140
+ (c >= 0x30 && c <= 0x39)
2141
+ ) {
2142
+ return true;
2143
+ }
2144
+ if (c === 0x27 || c === 0x2e || c === 0x3a) continue;
2145
+ return false;
2146
+ }
2147
+ // a low surrogate is read with the high one before it
2148
+ const start =
2149
+ c >= 0xdc00 && c <= 0xdfff && i > 0 && isHighSurrogate(text, i - 1)
2150
+ ? i - 1
2151
+ : i;
2152
+ const ch = text.slice(start, i + 1);
2153
+ if (LETTER_UNIT.test(ch)) return true;
2154
+ if (!IN_WORD.test(ch)) return false;
2155
+ i = start;
2156
+ }
2157
+ return lettered;
2158
+ }
2159
+
2160
+ function isHighSurrogate(text: string, i: number): boolean {
2161
+ const c = text.charCodeAt(i);
2162
+ return c >= 0xd800 && c <= 0xdbff;
2163
+ }
2164
+
2165
+ /** Whether a word is under way after a box, as `after` says where white
2166
+ * space stands: an inline box leaves it as its content did, an atomic
2167
+ * inline and a block end it, and a float or a positioned box leaves the
2168
+ * word around it as it was. */
2169
+ function letteredAfterFlow(
2170
+ flow: 'inline' | 'atomic' | 'block' | 'out',
2171
+ inside: boolean,
2172
+ before: boolean,
2173
+ ): boolean {
2174
+ if (flow === 'inline') return inside;
2175
+ if (flow === 'out') return before;
2176
+ return false;
2177
+ }
2178
+
2179
+ /** Where the inline content being built stands, for white space: at the
2180
+ * start of a line, just after a space that may collapse, or after anything
2181
+ * else. */
2182
+ type Collapse = 'start' | 'space' | 'content';
2183
+
2184
+ /**
2185
+ * A text's first letter, with the punctuation before and after it that CSS
2186
+ * 2.1 5.12.2 counts in — the Ps, Pe, Pi, Pf and Po classes — and the white
2187
+ * space before it in the first group. No match when the text ends, or
2188
+ * reaches a space, before any letter: its first letter is in a later text,
2189
+ * as browsers read it.
2190
+ */
2191
+ const FIRST_LETTER =
2192
+ /^([ \t\n\r\f\u00a0]*)(?:[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]\p{M}*)*[^ \t\n\r\f\u00a0\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]\p{M}*(?:[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]\p{M}*)*/u;
2193
+
2194
+ /** A text that is punctuation and nothing else, but for the white space
2195
+ * before it in the first group. */
2196
+ const PUNCTUATION_ONLY =
2197
+ /^([ \t\n\r\f\u00a0]*)(?:[\p{Ps}\p{Pe}\p{Pi}\p{Pf}\p{Po}]\p{M}*)+$/u;
2198
+
2199
+ /** A `::first-letter` looking for its letter. */
2200
+ interface LetterSearch {
2201
+ rules: FirstLetterRules;
2202
+ /** Punctuation already in the letter's style, from texts that ended
2203
+ * before the letter came. */
2204
+ punctuation: { box: Box; text: Box; style: ComputedStyle }[];
2205
+ }
2206
+
2207
+ /** Put punctuation a search styled back in its own box and style: the
2208
+ * letter it was waiting for is not on its line. */
2209
+ function giveBack(search: LetterSearch): void {
2210
+ for (const { box, text, style } of search.punctuation) {
2211
+ const parent = box.parent;
2212
+ if (!parent) continue;
2213
+ const at = parent.children.indexOf(box);
2214
+ if (at < 0) continue;
2215
+ parent.children[at] = text;
2216
+ text.parent = parent;
2217
+ text.style = style;
2218
+ }
2219
+ search.punctuation.length = 0;
2220
+ }
2221
+
2222
+ /** Whether a box of this display is a block container, which is what can
2223
+ * have a first letter. */
2224
+ function hasFirstLetter(display: ComputedStyle['display']): boolean {
2225
+ switch (display) {
2226
+ case 'block':
2227
+ case 'inline-block':
2228
+ case 'list-item':
2229
+ case 'table-cell':
2230
+ case 'table-caption':
2231
+ return true;
2232
+ default:
2233
+ return false;
2234
+ }
2235
+ }
2236
+
2237
+ /**
2238
+ * How a box sits in its parent's inline content, for white space. An inline
2239
+ * box's content is its parent's own; an atomic inline — an inline-block, an
2240
+ * image — is one piece of it; a block ends the line it interrupts and starts
2241
+ * another; a float or a positioned box is no part of it at all.
2242
+ */
2243
+ function flowOf(
2244
+ style: ComputedStyle,
2245
+ box: Box,
2246
+ ): 'inline' | 'atomic' | 'block' | 'out' {
2247
+ if (box.outOfFlow || box.isFloat) return 'out';
2248
+ // an image told to be a table's part is an inline one (`isBlockLevel`)
2249
+ if (box.kind === 'replaced' && style.display.startsWith('table-')) {
2250
+ return 'atomic';
2251
+ }
2252
+ switch (style.display) {
2253
+ case 'inline':
2254
+ return box.kind === 'replaced' ? 'atomic' : 'inline';
2255
+ case 'inline-block':
2256
+ case 'inline-table':
2257
+ case 'inline-flex':
2258
+ return 'atomic';
2259
+ case 'table-row-group':
2260
+ case 'table-header-group':
2261
+ case 'table-footer-group':
2262
+ case 'table-row':
2263
+ case 'table-cell':
2264
+ case 'table-caption':
2265
+ case 'table-column':
2266
+ case 'table-column-group':
2267
+ // table parts in an inline box are given an inline table around them
2268
+ // (CSS 2.1 17.2.1), which sits in the line as an inline-block does:
2269
+ // the white space either side of it is the line's
2270
+ return box.parent?.kind === 'inline' ? 'atomic' : 'block';
2271
+ default:
2272
+ return 'block';
2273
+ }
2274
+ }
2275
+
2276
+ /** Where the inline content stands after a box, from where it stood inside
2277
+ * the box and before it. */
2278
+ function after(
2279
+ flow: 'inline' | 'atomic' | 'block' | 'out',
2280
+ inside: Collapse,
2281
+ before: Collapse,
2282
+ ): Collapse {
2283
+ switch (flow) {
2284
+ case 'inline':
2285
+ return inside;
2286
+ case 'atomic':
2287
+ return 'content';
2288
+ case 'out':
2289
+ return before;
2290
+ default:
2291
+ return 'start';
2292
+ }
2293
+ }
2294
+
628
2295
  function boxKindFor(display: ComputedStyle['display']): BoxKind {
629
2296
  switch (display) {
630
2297
  case 'inline':
@@ -658,18 +2325,38 @@ function boxKindFor(display: ComputedStyle['display']): BoxKind {
658
2325
  }
659
2326
  }
660
2327
 
2328
+ /** A replaced box's intrinsic size from what its content says, in CSS
2329
+ * pixels: an axis it does not give takes the default object size. */
2330
+ function setIntrinsics(box: Box, size: IntrinsicSize, scale: number): void {
2331
+ box.intrinsic = {
2332
+ width: (size.width ?? 300) * scale,
2333
+ height: (size.height ?? 150) * scale,
2334
+ missing: (size.width === null ? 1 : 0) | (size.height === null ? 2 : 0),
2335
+ ratio: size.ratio,
2336
+ };
2337
+ }
2338
+
661
2339
  function replacedKind(el: Element, tag: string): ReplacedKind {
662
2340
  switch (tag) {
663
2341
  case 'img':
664
2342
  return 'image';
2343
+ case 'svg':
2344
+ // XHTML's `<svg:svg>` too, which `tagOf` names `svg`
2345
+ return 'svg';
665
2346
  case 'hr':
666
2347
  return 'hr';
2348
+ case 'iframe':
2349
+ case 'video':
2350
+ case 'embed':
2351
+ case 'canvas':
2352
+ return 'frame';
667
2353
  case 'textarea':
668
2354
  return 'textarea';
669
2355
  case 'select':
670
2356
  return 'select';
671
- case 'button':
672
- return 'button';
2357
+ // A `<button>` is not one: its content is the document's, laid out and
2358
+ // drawn like any box's (the UA sheet's `button` rule), where an
2359
+ // `<input type=submit>` has only a value to show and is a widget.
673
2360
  case 'input': {
674
2361
  const type = (attr(el, 'type') ?? 'text').toLowerCase();
675
2362
  if (type === 'checkbox') return 'checkbox';
@@ -684,87 +2371,34 @@ function replacedKind(el: Element, tag: string): ReplacedKind {
684
2371
  }
685
2372
  }
686
2373
 
687
- const ROMAN: [number, string][] = [
688
- [1000, 'm'],
689
- [900, 'cm'],
690
- [500, 'd'],
691
- [400, 'cd'],
692
- [100, 'c'],
693
- [90, 'xc'],
694
- [50, 'l'],
695
- [40, 'xl'],
696
- [10, 'x'],
697
- [9, 'ix'],
698
- [5, 'v'],
699
- [4, 'iv'],
700
- [1, 'i'],
701
- ];
702
-
703
2374
  /**
704
- * The marker a `list-item` draws. The counter is the *builder's*, not the
705
- * element's, because `value` on an `<li>` restarts it and a nested list has
706
- * its own — both of which are lost if the number is derived from the index
707
- * of the child in its parent.
2375
+ * The marker a `list-item` draws: its `list-item` counter written in its
2376
+ * `list-style-type`, between the style's prefix and suffix, or the string
2377
+ * the type is. The counter is the builder's (`CounterScopes`), which is
2378
+ * what lets `value` on an `<li>`, `start` and `reversed` on an `<ol>`, a
2379
+ * list in a list and an author's own `counter-reset` and `counter-set` all
2380
+ * count as a browser counts them. A suffix that ends in a space — `. `, a
2381
+ * bullet's — is written without it, and the layout sets the marker off by
2382
+ * its own gap; one that does not, `、`, sets the marker against the text.
708
2383
  */
709
2384
  function markerFor(
710
- el: Element,
711
2385
  style: ComputedStyle,
712
- counters: number[],
713
- ): string {
2386
+ n: number,
2387
+ styles: CounterStyles,
2388
+ ): { text: string; flush: boolean } {
714
2389
  const type = style.listStyleType;
715
- if (type === 'none') return '';
716
- const depth = counters.length;
717
- if (depth) {
718
- const value = numberAttr(el, 'value');
719
- if (value !== null) counters[depth - 1] = value;
720
- }
721
- const n = depth ? counters[depth - 1]++ : 1;
722
- switch (type) {
723
- case 'decimal':
724
- return `${n}.`;
725
- case 'decimal-leading-zero':
726
- return `${n < 10 ? '0' : ''}${n}.`;
727
- case 'lower-alpha':
728
- case 'lower-latin':
729
- return `${alpha(n).toLowerCase()}.`;
730
- case 'upper-alpha':
731
- case 'upper-latin':
732
- return `${alpha(n)}.`;
733
- case 'lower-roman':
734
- return `${roman(n)}.`;
735
- case 'upper-roman':
736
- return `${roman(n).toUpperCase()}.`;
737
- case 'circle':
738
- return '◦';
739
- case 'square':
740
- return '▪';
741
- case 'disc':
742
- default:
743
- return '•';
744
- }
745
- }
746
-
747
- function alpha(n: number): string {
748
- let out = '';
749
- let v = Math.max(1, n);
750
- while (v > 0) {
751
- const rem = (v - 1) % 26;
752
- out = String.fromCharCode(65 + rem) + out;
753
- v = Math.floor((v - 1) / 26);
754
- }
755
- return out;
756
- }
757
-
758
- function roman(n: number): string {
759
- let v = Math.max(1, Math.min(3999, n));
760
- let out = '';
761
- for (const [value, sym] of ROMAN) {
762
- while (v >= value) {
763
- out += sym;
764
- v -= value;
765
- }
766
- }
767
- return out;
2390
+ if (type === 'none') return { text: '', flush: false };
2391
+ if (type.startsWith('"')) return { text: type.slice(1), flush: true };
2392
+ const { prefix, text, suffix } = styles.marker(
2393
+ n,
2394
+ type,
2395
+ style.direction === 'rtl',
2396
+ );
2397
+ const spaced = /\s$/.test(suffix);
2398
+ return {
2399
+ text: prefix + text + (spaced ? suffix.trimEnd() : suffix),
2400
+ flush: !spaced,
2401
+ };
768
2402
  }
769
2403
 
770
2404
  /**
@@ -810,23 +2444,35 @@ function assignSubtreeRanges(box: Box): { start: number; end: number } {
810
2444
  * straight to `<tr>`, and CSS says the missing row group is generated
811
2445
  * rather than the rows being dropped.
812
2446
  */
813
- function fixUp(box: Box): void {
814
- for (const child of box.children) fixUp(child);
2447
+ function fixUp(box: Box, anonymous: AnonymousStyle): void {
2448
+ for (const child of box.children) fixUp(child, anonymous);
2449
+ fixUpOwn(box, anonymous);
2450
+ }
815
2451
 
2452
+ /** `fixUp` of a box whose children have had theirs. */
2453
+ function fixUpOwn(box: Box, anonymous: AnonymousStyle): void {
816
2454
  if (box.kind === 'table') {
817
- fixUpTable(box);
2455
+ fixUpTable(box, anonymous);
818
2456
  return;
819
2457
  }
820
2458
  if (box.kind === 'table-row-group') {
821
- wrapOrphans(box, 'table-row', (k) => k === 'table-row');
2459
+ wrapOrphans(box, 'table-row', (k) => k === 'table-row', anonymous);
822
2460
  return;
823
2461
  }
824
2462
  if (box.kind === 'table-row') {
825
- wrapOrphans(box, 'table-cell', (k) => k === 'table-cell');
2463
+ wrapOrphans(box, 'table-cell', (k) => k === 'table-cell', anonymous);
826
2464
  return;
827
2465
  }
2466
+ // a column group holds its columns, which are where they belong, and the
2467
+ // builder kept nothing else in it
2468
+ if (box.style.display === 'table-column-group') return;
828
2469
 
829
2470
  if (!box.children.length) return;
2471
+ wrapTableParts(box, anonymous);
2472
+ // An inline box holds no block: a block in one is its block container's,
2473
+ // which breaks the inline box around it
2474
+ if (box.kind === 'inline') return;
2475
+ if (box.kind !== 'flex') breakInlines(box);
830
2476
  let hasBlockLevel = false;
831
2477
  let hasInlineLevel = false;
832
2478
  for (const child of box.children) {
@@ -851,7 +2497,7 @@ function fixUp(box: Box): void {
851
2497
  // finds itself; it does not force an anonymous block on its own.
852
2498
  if (isBlockLevel(child) && !child.outOfFlow && !child.isFloat) {
853
2499
  if (run) {
854
- next.push(anonymousBlock(box, run));
2500
+ next.push(anonymousOf(box, 'block', run, anonymous));
855
2501
  run = null;
856
2502
  }
857
2503
  next.push(child);
@@ -859,27 +2505,152 @@ function fixUp(box: Box): void {
859
2505
  }
860
2506
  // Whitespace between two blocks is not content and must not generate a
861
2507
  // line box — `<div><p>a</p> <p>b</p></div>` has no blank line in it.
862
- if (!run && child.kind === 'text' && !child.text.trim()) continue;
2508
+ if (!run && child.kind === 'text' && isBlank(child.text)) continue;
863
2509
  (run ??= []).push(child);
864
2510
  }
865
2511
  if (run) {
866
- if (run.every((c) => c.kind === 'text' && !c.text.trim())) {
2512
+ if (run.every((c) => c.kind === 'text' && isBlank(c.text))) {
867
2513
  // trailing whitespace after the last block: same rule
868
2514
  } else {
869
- next.push(anonymousBlock(box, run));
2515
+ next.push(anonymousOf(box, 'block', run, anonymous));
870
2516
  }
871
2517
  }
872
2518
  box.children = next;
873
2519
  }
874
2520
 
875
- function anonymousBlock(parent: Box, run: Box[]): Box {
876
- const box = new Box('block', null, parent.style);
877
- box.parent = parent;
878
- for (const child of run) {
879
- child.parent = box;
880
- box.children.push(child);
2521
+ /**
2522
+ * Break a block container's inline children around the blocks in them
2523
+ * (CSS 2.1 9.2.1.1): an inline box with an in-flow block in it — at any
2524
+ * depth of inline box — becomes the pieces of it before, between and after
2525
+ * its blocks, and the blocks stand beside them as the container's own, for
2526
+ * the mixed-content rule to wrap the pieces in anonymous blocks.
2527
+ * `<font><p>One</p><p>Two</p></font>` laid its paragraphs side by side as
2528
+ * inline-blocks, and a link around a card's blocks set them in a line.
2529
+ */
2530
+ function breakInlines(box: Box): void {
2531
+ let next: Box[] | null = null;
2532
+ for (let i = 0; i < box.children.length; i += 1) {
2533
+ const child = box.children[i];
2534
+ const pieces =
2535
+ child.kind === 'inline' && holdsBlock(child) ? breakAround(child) : null;
2536
+ if (!pieces) {
2537
+ next?.push(child);
2538
+ continue;
2539
+ }
2540
+ next ??= box.children.slice(0, i);
2541
+ for (const piece of pieces) {
2542
+ piece.parent = box;
2543
+ next.push(piece);
2544
+ }
881
2545
  }
882
- return box;
2546
+ if (next) box.children = next;
2547
+ }
2548
+
2549
+ /** Whether an inline box has an in-flow block in it, at any depth of
2550
+ * inline box: asked first, and without making anything, since nearly
2551
+ * every inline box has none. */
2552
+ function holdsBlock(inline: Box): boolean {
2553
+ for (const child of inline.children) {
2554
+ if (child.kind === 'inline') {
2555
+ if (holdsBlock(child)) return true;
2556
+ } else if (isBlockLevel(child) && !child.outOfFlow && !child.isFloat) {
2557
+ return true;
2558
+ }
2559
+ }
2560
+ return false;
2561
+ }
2562
+
2563
+ /**
2564
+ * An inline box broken around the in-flow blocks in it: its pieces, each a
2565
+ * box of its element and style holding the content between two blocks,
2566
+ * with the blocks between them; null where it has none, which is nearly
2567
+ * every inline box. A piece has no edge on a side a block cut: the start of
2568
+ * the first and the end of the last are the box's own.
2569
+ */
2570
+ function breakAround(inline: Box): Box[] | null {
2571
+ let out: Box[] | null = null;
2572
+ const pieces: Box[] = [];
2573
+ let run: Box[] = [];
2574
+ const close = (): void => {
2575
+ const piece = new Box('inline', inline.el, inline.style);
2576
+ piece.pseudo = inline.pseudo;
2577
+ for (const child of run) piece.append(child);
2578
+ pieces.push(piece);
2579
+ (out ??= []).push(piece);
2580
+ run = [];
2581
+ };
2582
+ const blocks: Box[] = [];
2583
+ for (const child of inline.children) {
2584
+ if (isBlockLevel(child) && !child.outOfFlow && !child.isFloat) {
2585
+ close();
2586
+ out!.push(child);
2587
+ blocks.push(child);
2588
+ continue;
2589
+ }
2590
+ const inner =
2591
+ child.kind === 'inline' && holdsBlock(child) ? breakAround(child) : null;
2592
+ if (!inner) {
2593
+ run.push(child);
2594
+ continue;
2595
+ }
2596
+ for (const piece of inner) {
2597
+ if (piece.kind === 'inline') {
2598
+ run.push(piece);
2599
+ } else {
2600
+ close();
2601
+ out!.push(piece);
2602
+ blocks.push(piece);
2603
+ }
2604
+ }
2605
+ }
2606
+ if (!out) return null;
2607
+ close();
2608
+ for (let i = 0; i < pieces.length; i += 1) {
2609
+ pieces[i].cut = (i > 0 ? 1 : 0) | (i < pieces.length - 1 ? 2 : 0);
2610
+ }
2611
+ if (isRelative(inline.style)) CUT_BLOCKS.set(pieces[0], blocks);
2612
+ const fade = inline.style.opacity;
2613
+ if (fade < 1) {
2614
+ for (const block of blocks) {
2615
+ FADED_BLOCKS.set(block, (FADED_BLOCKS.get(block) ?? 1) * fade);
2616
+ }
2617
+ }
2618
+ return out;
2619
+ }
2620
+
2621
+ /**
2622
+ * The style of an anonymous box inside a parent: what the parent passes on
2623
+ * by inheritance, and every other property at its initial value, as CSS 2.1
2624
+ * gives anonymous boxes theirs (9.2.1.1, 17.2.1). An anonymous box that took
2625
+ * the parent's style itself took its height, its padding and borders, its
2626
+ * background, its relative offset and its opacity a second time — the text
2627
+ * in `<div style="padding: 20px">text<p>…</p></div>` sat 40px in, and the
2628
+ * paragraph after it the height of the div further down. One per parent
2629
+ * style, which the cascade already shares between elements.
2630
+ */
2631
+ type AnonymousStyle = (
2632
+ parent: Box,
2633
+ display: ComputedStyle['display'],
2634
+ ) => ComputedStyle;
2635
+
2636
+ function anonymousStyles(initial: ComputedStyle): AnonymousStyle {
2637
+ const made = new WeakMap<ComputedStyle, Map<string, ComputedStyle>>();
2638
+ return (parent, display) => {
2639
+ let byDisplay = made.get(parent.style);
2640
+ if (!byDisplay) {
2641
+ byDisplay = new Map();
2642
+ made.set(parent.style, byDisplay);
2643
+ }
2644
+ let style = byDisplay.get(display);
2645
+ if (!style) {
2646
+ // `display` is the one property it does not start from: it is the
2647
+ // box the fix-up made, and layout asks the style what a box is
2648
+ style = inherit(parent.style, initial);
2649
+ style.display = display;
2650
+ byDisplay.set(display, style);
2651
+ }
2652
+ return style;
2653
+ };
883
2654
  }
884
2655
 
885
2656
  function isBlockLevel(box: Box): boolean {
@@ -894,6 +2665,17 @@ function isBlockLevel(box: Box): boolean {
894
2665
  // An `inline-block` or `inline-flex` is a block *container* with an
895
2666
  // inline-level outer role, so it belongs to the inline run around it.
896
2667
  return !isInlineLevelDisplay(box.style.display);
2668
+ case 'replaced':
2669
+ // an image is inline unless it is told otherwise, and then it is a
2670
+ // block: `img { display: block }`, which mail writes to lose the gap
2671
+ // under its images, stacks them. A table's part is no display it
2672
+ // can take, and one told to be a cell is inline (CSS Display 3,
2673
+ // 2.4), which is what a table wraps a cell around.
2674
+ return (
2675
+ box.style.display !== 'inline' &&
2676
+ !isInlineLevelDisplay(box.style.display) &&
2677
+ !box.style.display.startsWith('table-')
2678
+ );
897
2679
  default:
898
2680
  return false;
899
2681
  }
@@ -912,6 +2694,7 @@ function wrapOrphans(
912
2694
  box: Box,
913
2695
  kind: BoxKind,
914
2696
  accept: (k: BoxKind) => boolean,
2697
+ anonymous: AnonymousStyle,
915
2698
  ): void {
916
2699
  let needed = false;
917
2700
  for (const child of box.children) {
@@ -921,26 +2704,56 @@ function wrapOrphans(
921
2704
  }
922
2705
  }
923
2706
  if (!needed) return;
2707
+ // White space goes only between two of the parts, or beside one at an
2708
+ // end (CSS 2.1 17.2.1, rule 1): beside a child the fix-up wraps, it is
2709
+ // that child's run's, so `<span>a</span> <span>b</span>` in a row is one
2710
+ // anonymous cell of `a b`, where it was `ab`
924
2711
  const next: Box[] = [];
925
2712
  let run: Box[] | null = null;
2713
+ let space: Box[] = [];
926
2714
  for (const child of box.children) {
927
2715
  if (accept(child.kind)) {
928
2716
  if (run) {
929
- next.push(anonymousOf(box, kind, run));
2717
+ run.push(...space);
2718
+ next.push(anonymousOf(box, kind, run, anonymous));
930
2719
  run = null;
931
2720
  }
2721
+ space = [];
932
2722
  next.push(child);
933
2723
  continue;
934
2724
  }
935
- if (isDroppableWhitespace(child)) continue;
936
- (run ??= []).push(child);
2725
+ if (isDroppableWhitespace(child)) {
2726
+ space.push(child);
2727
+ continue;
2728
+ }
2729
+ (run ??= []).push(...space, child);
2730
+ space = [];
2731
+ }
2732
+ if (run) {
2733
+ run.push(...space);
2734
+ next.push(anonymousOf(box, kind, run, anonymous));
937
2735
  }
938
- if (run) next.push(anonymousOf(box, kind, run));
939
2736
  box.children = next;
2737
+ // A cell made here is a block container the fix-up has not been to: its
2738
+ // children had theirs, as the table's, before it held them. Text beside a
2739
+ // block in it goes in an anonymous block as in any other (CSS 2.1
2740
+ // 9.2.1.1) — `<span style="display: inline-table">bcd<div>x</div>` laid
2741
+ // `bcd` out as nothing, and drew the table as wide as the `x`.
2742
+ if (kind === 'table-cell') {
2743
+ for (const child of next) {
2744
+ if (child.kind === 'table-cell' && !child.el) fixUpOwn(child, anonymous);
2745
+ }
2746
+ }
940
2747
  }
941
2748
 
942
- function anonymousOf(parent: Box, kind: BoxKind, run: Box[]): Box {
943
- const box = new Box(kind, null, parent.style);
2749
+ function anonymousOf(
2750
+ parent: Box,
2751
+ kind: BoxKind,
2752
+ run: Box[],
2753
+ anonymous: AnonymousStyle,
2754
+ display: ComputedStyle['display'] = kind as ComputedStyle['display'],
2755
+ ): Box {
2756
+ const box = new Box(kind, null, anonymous(parent, display));
944
2757
  box.parent = parent;
945
2758
  for (const child of run) {
946
2759
  child.parent = box;
@@ -949,43 +2762,181 @@ function anonymousOf(parent: Box, kind: BoxKind, run: Box[]): Box {
949
2762
  return box;
950
2763
  }
951
2764
 
2765
+ /** White space alone, which the table fix-up drops between the parts of a
2766
+ * table whether it is kept or not (CSS 2.1 17.2.1, rule 1). Kept, under
2767
+ * `white-space: pre`, it made a cell of the line break before every row. */
952
2768
  function isDroppableWhitespace(box: Box): boolean {
953
- return box.kind === 'text' && !box.text.trim();
2769
+ return box.kind === 'text' && isBlank(box.text);
2770
+ }
2771
+
2772
+ /**
2773
+ * Whether a text is white space alone: CSS's white space, which is the
2774
+ * space, the tab and the three line breaks and nothing else (CSS Text 3,
2775
+ * 4.1). JavaScript's `\s` and `trim` take in the no-break space and the
2776
+ * other Unicode spaces too, so a `<p>&nbsp;</p>` spacer or a
2777
+ * `<td>&nbsp;</td>` beside a block was dropped as if it held nothing. Tested
2778
+ * rather than trimmed, which copies a paragraph's text to find out.
2779
+ */
2780
+ export function isBlank(text: string): boolean {
2781
+ return BLANK.test(text);
2782
+ }
2783
+
2784
+ const BLANK = /^[ \t\n\r\f]*$/;
2785
+
2786
+ /** White space that collapsing would change: anything but a lone space.
2787
+ * Most of a document's text has none, and is its own collapsed form. */
2788
+ const COLLAPSIBLE = /[\t\n\r\f]| {2}/;
2789
+
2790
+ function isRelative(style: ComputedStyle): boolean {
2791
+ return style.position === 'relative' || style.position === 'sticky';
2792
+ }
2793
+
2794
+ /** Whether `position: relative` moves a box: an inset is set. An inline box
2795
+ * that is moved has its text laid out apart (`collect`), so one whose
2796
+ * insets resolve to nothing still costs its paragraph the layout of a line
2797
+ * at a time, which a box with every inset `auto` does not. */
2798
+ export function isOffset(style: ComputedStyle): boolean {
2799
+ if (!isRelative(style)) return false;
2800
+ return (
2801
+ style.top !== AUTO ||
2802
+ style.right !== AUTO ||
2803
+ style.bottom !== AUTO ||
2804
+ style.left !== AUTO
2805
+ );
2806
+ }
2807
+
2808
+ /** Whether a style may put its box under its stacking context's flow: a
2809
+ * negative `z-index` on a positioned box, or on a flex item, which that
2810
+ * makes a stacking context as it is (`stacksLayers`). */
2811
+ function isNegative(style: ComputedStyle): boolean {
2812
+ return typeof style.zIndex === 'number' && style.zIndex < 0;
954
2813
  }
955
2814
 
956
- function fixUpTable(table: Box): void {
2815
+ /** A box that belongs inside a table: a row group, a row, a cell, a
2816
+ * caption or a column. Asked of every child of every box in a build, so
2817
+ * a text or an inline box is answered from its kind, without its style. */
2818
+ function isTablePart(box: Box): boolean {
2819
+ switch (box.kind) {
2820
+ case 'table-row-group':
2821
+ case 'table-row':
2822
+ case 'table-cell':
2823
+ case 'table-caption':
2824
+ return !box.outOfFlow && !box.isFloat;
2825
+ case 'block': {
2826
+ if (box.outOfFlow || box.isFloat) return false;
2827
+ const display = box.style.display;
2828
+ return display === 'table-column' || display === 'table-column-group';
2829
+ }
2830
+ default:
2831
+ return false;
2832
+ }
2833
+ }
2834
+
2835
+ /**
2836
+ * Table parts outside a table get one around them (CSS 2.1 17.2.1, rule 3):
2837
+ * each run of them — the white space between them no part of it — is
2838
+ * wrapped in an anonymous table, an inline one where the parent is inline,
2839
+ * and the table is then completed as any other. Laid out on their own, two
2840
+ * cells side by side in a line were two inline-blocks with a space between
2841
+ * them, and in a block two blocks, one above the other.
2842
+ */
2843
+ function wrapTableParts(box: Box, anonymous: AnonymousStyle): void {
2844
+ if (!box.children.some(isTablePart)) return;
2845
+ const display = box.kind === 'inline' ? 'inline-table' : 'table';
2846
+ const next: Box[] = [];
2847
+ let run: Box[] | null = null;
2848
+ let space: Box[] = [];
2849
+ const flush = (): void => {
2850
+ if (!run) return;
2851
+ const table = anonymousOf(box, 'table', run, anonymous, display);
2852
+ fixUpTable(table, anonymous);
2853
+ next.push(table);
2854
+ run = null;
2855
+ };
2856
+ for (const child of box.children) {
2857
+ if (isTablePart(child)) {
2858
+ run ??= [];
2859
+ run.push(child);
2860
+ space = [];
2861
+ continue;
2862
+ }
2863
+ if (run && isDroppableWhitespace(child)) {
2864
+ // held back: between two parts it goes, after the last it stays
2865
+ space.push(child);
2866
+ continue;
2867
+ }
2868
+ flush();
2869
+ next.push(...space, child);
2870
+ space = [];
2871
+ }
2872
+ flush();
2873
+ next.push(...space);
2874
+ box.children = next;
2875
+ for (const child of next) child.parent = box;
2876
+ }
2877
+
2878
+ function fixUpTable(table: Box, anonymous: AnonymousStyle): void {
957
2879
  const groups: Box[] = [];
958
2880
  const captions: Box[] = [];
2881
+ const columns: Box[] = [];
959
2882
  let looseRows: Box[] | null = null;
2883
+ let looseCells: Box[] | null = null;
2884
+ const flushCells = (): void => {
2885
+ if (!looseCells) return;
2886
+ (looseRows ??= []).push(
2887
+ anonymousOf(table, 'table-row', looseCells, anonymous),
2888
+ );
2889
+ looseCells = null;
2890
+ };
960
2891
  for (const child of table.children) {
961
- if (child.kind === 'table-row-group') {
2892
+ const display = child.style.display;
2893
+ // an image told to be a column is an inline image, as one told to be a
2894
+ // cell is (`flowOf`): taken for a column, it was never drawn
2895
+ if (
2896
+ (display === 'table-column' || display === 'table-column-group') &&
2897
+ child.kind !== 'replaced'
2898
+ ) {
2899
+ // A column is the table's, beside its rows: it lays out nothing and
2900
+ // paints nothing (17.2.1). Taken for a stray child, it was wrapped in
2901
+ // a row of its own and drawn as a cell.
2902
+ columns.push(child);
2903
+ } else if (child.kind === 'table-row-group') {
2904
+ flushCells();
962
2905
  if (looseRows) {
963
- groups.push(anonymousOf(table, 'table-row-group', looseRows));
2906
+ groups.push(
2907
+ anonymousOf(table, 'table-row-group', looseRows, anonymous),
2908
+ );
964
2909
  looseRows = null;
965
2910
  }
966
2911
  groups.push(child);
967
2912
  } else if (child.kind === 'table-caption') {
968
2913
  captions.push(child);
969
2914
  } else if (isDroppableWhitespace(child)) {
2915
+ // after a loose child it goes with the loose children, and the cells
2916
+ // they are wrapped in say whether it stays (`wrapOrphans`)
2917
+ if (looseCells) looseCells.push(child);
970
2918
  continue;
2919
+ } else if (child.kind === 'table-row') {
2920
+ flushCells();
2921
+ (looseRows ??= []).push(child);
971
2922
  } else {
972
- // A `<tr>`, or anything else that ended up here: rows go into an
973
- // anonymous group, and anything that is not a row becomes a cell in
974
- // one, which is how a browser rescues `<table>text</table>`.
975
- const row =
976
- child.kind === 'table-row'
977
- ? child
978
- : anonymousOf(table, 'table-row', [child]);
979
- (looseRows ??= []).push(row);
2923
+ // Anything else that ended up here is a cell, or goes in one, and a
2924
+ // run of them shares an anonymous row — which is how a browser
2925
+ // rescues `<table>text</table>`, and how three cells in a table
2926
+ // with no row are one row of three rather than three rows of one.
2927
+ (looseCells ??= []).push(child);
980
2928
  }
981
2929
  }
982
- if (looseRows) groups.push(anonymousOf(table, 'table-row-group', looseRows));
2930
+ flushCells();
2931
+ if (looseRows) {
2932
+ groups.push(anonymousOf(table, 'table-row-group', looseRows, anonymous));
2933
+ }
983
2934
  for (const group of groups) {
984
- wrapOrphans(group, 'table-row', (k) => k === 'table-row');
2935
+ wrapOrphans(group, 'table-row', (k) => k === 'table-row', anonymous);
985
2936
  for (const row of group.children) {
986
- wrapOrphans(row, 'table-cell', (k) => k === 'table-cell');
2937
+ wrapOrphans(row, 'table-cell', (k) => k === 'table-cell', anonymous);
987
2938
  }
988
2939
  }
989
- table.children = [...captions, ...groups];
2940
+ table.children = [...captions, ...columns, ...groups];
990
2941
  for (const child of table.children) child.parent = table;
991
2942
  }