@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
@@ -1,5 +1,39 @@
1
- import { codeUnitOffsets } from '../../internal/text.js';
2
- import { inkColor, isTransparent } from '../css/values.js';
1
+ // Inline layout: text, and the atomic things that sit in the middle of it.
2
+ //
3
+ // The whole file turns on one decision — **how many times ntk's `TextLayout`
4
+ // is called per paragraph** — because that is where a document's first paint
5
+ // is won or lost.
6
+ //
7
+ // `TextLayout` does the expensive and difficult half already: UAX#14 break
8
+ // opportunities, OpenType shaping with a memo per inter-break segment, font
9
+ // fallback, bidi, and per-line UAX#9 reordering. Calling it **once** for a
10
+ // whole inline formatting context is therefore both the fastest and the most
11
+ // correct thing this can do, and it is what the common case does: a
12
+ // paragraph, a heading, a list item's line, a table cell.
13
+ //
14
+ // Two things break that. An **atomic** in the middle of the text — an image,
15
+ // an inline-block, a form control — which the text layout knows nothing
16
+ // about; and a **float**, which makes the available width differ line by
17
+ // line while `TextLayout` takes one `maxWidth`. Either drops this into a
18
+ // line-at-a-time loop that re-lays the remaining text per line. That loop is
19
+ // quadratic, and that is fine, because it *leaves* itself the moment nothing
20
+ // ahead can vary the width again: the tail of a long paragraph beside a
21
+ // floated image is one call, not one per line.
22
+ //
23
+ // The runs handed to `TextLayout` are richtext's `TextRun`s, and that is
24
+ // deliberate rather than incidental: ntk lets unknown span fields ride along
25
+ // and hands them back on the laid-out runs, so the decoration a `<span>`
26
+ // carries reaches paint on the same object the glyphs did — which is exactly
27
+ // the trick `<richtext>` is built on, and it is why paint reuses richtext's
28
+ // decoration pass unchanged instead of reimplementing it. What a run does
29
+ // *not* carry is its element: a layout outlives the parse it was made for
30
+ // (`TextLayoutCache`), so the element under a run is found from its text's
31
+ // place in the document (`LineText.spans`) instead.
32
+ import { codePointAtOffset, codeUnitOffsets } from '../../internal/text.js';
33
+ import { CONTAIN_LAYOUT, INLINE_BEFORE_ABSOLUTE, scrolls, } from '../css/style.js';
34
+ import { inkColor, isTransparent, resolve } from '../css/values.js';
35
+ import { BOX_RAISES, LINE_BOX_RAISES, isOffset, letteredAfter, SHADOWED_TEXT, SHIFTED_LINES, TEXT_RAISES, transformInPlace, } from './boxes.js';
36
+ import { tableGrid } from './grid.js';
3
37
  /**
4
38
  * The text behind a layout, for the one caller that needs it: a selection
5
39
  * band has to turn a document offset into a **code point** index, which is
@@ -11,6 +45,119 @@ import { inkColor, isTransparent } from '../css/values.js';
11
45
  * most documents are read without using.
12
46
  */
13
47
  const LAYOUT_RUNS = new WeakMap();
48
+ const WIDEST_WORD = new WeakMap();
49
+ /**
50
+ * The widest word between spaces in a layout's text: its runs laid out
51
+ * again with every space a line break, at no width limit. Where a text's
52
+ * only breaks are spaces that is its min-content width, and where it has
53
+ * others — a hyphen, a slash — more: an upper bound of it either way. The
54
+ * other way to ask, a layout of no width, has ntk search every word for a
55
+ * place to cut it, since none fits, which costs fifteen layouts of this
56
+ * one. Infinity where the runs are no longer kept.
57
+ */
58
+ export function widestWord(fonts, layout, measured = true) {
59
+ if (!measured)
60
+ return longestWord(layout);
61
+ let widest = WIDEST_WORD.get(layout);
62
+ if (widest !== undefined)
63
+ return widest;
64
+ const held = LAYOUT_RUNS.get(layout);
65
+ if (!held || (held.length > 0 && typeof held[0] === 'number')) {
66
+ return Infinity;
67
+ }
68
+ const words = fonts.layout(held.map((run) => ({
69
+ ...run,
70
+ text: run.text.replace(/[ \t\u200b]+/g, '\n'),
71
+ })), {}, {});
72
+ widest = 0;
73
+ for (const line of words.lines)
74
+ widest = Math.max(widest, line.width);
75
+ WIDEST_WORD.set(layout, widest);
76
+ return widest;
77
+ }
78
+ const SPACE_BREAKS = new WeakMap();
79
+ /**
80
+ * Whether a layout's text breaks only at its spaces: letters and digits of
81
+ * the Latin, Greek and Cyrillic scripts, and the punctuation that UAX #14
82
+ * never breaks after between two letters — no hyphen, slash, `!`, `?`,
83
+ * `|`, `}`, soft hyphen or ellipsis, and nothing of a script that breaks
84
+ * between its letters or by dictionary. Where it does, its widest word
85
+ * (`widestWord`) is exactly its min-content width.
86
+ */
87
+ export function spaceBreaksOnly(layout) {
88
+ let only = SPACE_BREAKS.get(layout);
89
+ if (only !== undefined)
90
+ return only;
91
+ const held = LAYOUT_RUNS.get(layout);
92
+ only = !!held && (held.length === 0 || typeof held[0] !== 'number');
93
+ if (only) {
94
+ outer: for (const run of held) {
95
+ const text = run.text;
96
+ for (let i = 0; i < text.length; i += 1) {
97
+ if (!breaksOnlyAtSpace(text.charCodeAt(i))) {
98
+ only = false;
99
+ break outer;
100
+ }
101
+ }
102
+ }
103
+ }
104
+ SPACE_BREAKS.set(layout, only);
105
+ return only;
106
+ }
107
+ function breaksOnlyAtSpace(c) {
108
+ if (c < 0x80) {
109
+ // `-` `/` `!` `?` `|` `}`: a break may follow each
110
+ return (c >= 0x20 &&
111
+ c !== 0x2d &&
112
+ c !== 0x2f &&
113
+ c !== 0x21 &&
114
+ c !== 0x3f &&
115
+ c !== 0x7c &&
116
+ c !== 0x7d &&
117
+ c !== 0x7f);
118
+ }
119
+ // Latin-1 but the soft hyphen and the acute accent, which a break may
120
+ // precede; Latin Extended, the combining marks, Greek and Cyrillic
121
+ if (c >= 0xa0 && c < 0x530)
122
+ return c !== 0xad && c !== 0xb4 && c !== 0x2c8;
123
+ // the curly quotes
124
+ return c >= 0x2018 && c <= 0x201f;
125
+ }
126
+ const LONGEST_WORD = new WeakMap();
127
+ /**
128
+ * `widestWord` estimated from the text alone, with no layout: each word's
129
+ * characters at half again their size, which no glyph a document sets
130
+ * words in is wider than, and their letter spacing. What is under it
131
+ * needs no measuring, and most words are.
132
+ */
133
+ function longestWord(layout) {
134
+ let longest = LONGEST_WORD.get(layout);
135
+ if (longest !== undefined)
136
+ return longest;
137
+ const held = LAYOUT_RUNS.get(layout);
138
+ if (!held || (held.length > 0 && typeof held[0] === 'number')) {
139
+ return Infinity;
140
+ }
141
+ longest = 0;
142
+ let word = 0;
143
+ for (const run of held) {
144
+ const per = (run.size ?? 16) * 1.5 + Math.max(0, run.letterSpacing ?? 0);
145
+ const text = run.text;
146
+ for (let i = 0; i < text.length; i += 1) {
147
+ const c = text.charCodeAt(i);
148
+ if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x200b)
149
+ word = 0;
150
+ else if (c < 0xdc00 || c > 0xdfff) {
151
+ // a surrogate pair is one character
152
+ word += per;
153
+ if (word > longest)
154
+ longest = word;
155
+ }
156
+ }
157
+ }
158
+ LONGEST_WORD.set(layout, longest);
159
+ return longest;
160
+ }
14
161
  /** Code-unit → code-point offsets for a layout's text, built once. */
15
162
  export function layoutOffsets(layout) {
16
163
  const held = LAYOUT_RUNS.get(layout);
@@ -23,17 +170,131 @@ export function layoutOffsets(layout) {
23
170
  return offsets;
24
171
  }
25
172
  const EMPTY_OFFSETS = [0];
173
+ function isText(item) {
174
+ return item.kind === 'text';
175
+ }
26
176
  /**
27
177
  * Lay out one inline formatting context. Coordinates in the result are
28
178
  * relative to the containing block's content box; the caller translates.
29
179
  */
30
180
  export function layoutInline(block, options) {
31
- const fonts = options.fonts;
181
+ const result = layoutLines(block, options);
182
+ const clamp = options.clamp?.lines;
183
+ // A clamped block shows its first lines and is as tall as they are. The
184
+ // one-layout paths below had the engine cut them, with its ellipsis; a
185
+ // block laid out a line at a time — an image on a line, a float beside
186
+ // it — is cut here, and ends where its last line does, with none.
187
+ if (clamp !== undefined && result.lines.length > clamp) {
188
+ const lines = result.lines.slice(0, clamp);
189
+ const last = lines[clamp - 1];
190
+ let widest = 0;
191
+ for (const line of lines)
192
+ widest = Math.max(widest, line.width);
193
+ return {
194
+ lines,
195
+ height: last ? last.y + last.height : 0,
196
+ width: widest,
197
+ cut: true,
198
+ };
199
+ }
200
+ return result;
201
+ }
202
+ function cutOf(style, clamp) {
203
+ // the ellipsis a clamp ends in is placed apart (`ellipsized`)
204
+ if (clamp)
205
+ return { maxLines: clamp.lines, overflow: 'clip' };
206
+ if (style.textOverflow === 'ellipsis' &&
207
+ !wraps(style) &&
208
+ style.overflowX !== 'visible') {
209
+ return { maxLines: 1, overflow: 'ellipsis', wrap: false };
210
+ }
211
+ return null;
212
+ }
213
+ function layoutLines(block, options) {
32
214
  const items = [];
33
- collect(block, items);
34
- if (!items.length || !fonts)
35
- return EMPTY;
215
+ const statics = [];
216
+ const floatCount = collect(block, items, options.width, options.fonts, block.style, !!options.floatBoxes, statics);
217
+ const result = linesOf(block, options, items, floatCount);
218
+ // whichever way the lines were made, one pass over them
219
+ if (statics.length) {
220
+ staticPositions(statics, items, result.lines, block, options);
221
+ }
222
+ return result;
223
+ }
224
+ /** The lines of a block's inline content, gathered into `items`. */
225
+ function linesOf(block, options, items, floatCount) {
226
+ let fonts = options.fonts;
227
+ const placing = options.floatBoxes;
228
+ if (items.length === floatCount || !fonts) {
229
+ // no line to wait for: the floats go at the top
230
+ const placed = [];
231
+ for (const item of items) {
232
+ if (item.kind === 'float') {
233
+ placing.size(item.box);
234
+ placing.place(item.box, options.startY);
235
+ placed.push(item.box);
236
+ }
237
+ }
238
+ if (!placed.length)
239
+ return EMPTY;
240
+ return { ...EMPTY, floatRow: besideLines([], placed, options.startY) };
241
+ }
242
+ // text a box's background shows through (`background-clip: text`): its
243
+ // layouts are made through a recorder, so paint can lay the same text out
244
+ // again with no ink of its own and fill it with that background
245
+ if (options.clipText && clipsText(block, items)) {
246
+ CLIPPED_TEXT.add(block);
247
+ fonts = recording(fonts);
248
+ }
249
+ // small capitals the face does not have, made of its capitals
250
+ synthesizeSmallCaps(items, fonts);
251
+ if (wraps(block.style))
252
+ holdNoWrap(items, block);
253
+ // one walk for what few paragraphs have: text that casts a shadow, and a
254
+ // tab, which only a `white-space` that keeps it leaves — so no text is
255
+ // searched for one where it cannot be
256
+ let tabbed = false;
257
+ let shadowed = false;
258
+ for (const item of items) {
259
+ if (item.kind !== 'text' || item.control)
260
+ continue;
261
+ const style = item.box.style;
262
+ if (style.textShadow)
263
+ shadowed = true;
264
+ if (!tabbed &&
265
+ (style.whiteSpace === 'pre' || style.whiteSpace === 'pre-wrap') &&
266
+ item.run.text.includes('\t')) {
267
+ tabbed = true;
268
+ }
269
+ }
270
+ if (shadowed)
271
+ SHADOWED_TEXT.add(block);
272
+ if (tabbed) {
273
+ setTabs(items, fonts, block.style, blockIndent(block, options.width));
274
+ }
275
+ // an override on the block is one on all of its inline content (CSS 2.1
276
+ // 9.10); its embedding or isolation is the paragraph's own direction
277
+ const own = block.style.unicodeBidi === 'bidi-override' ||
278
+ block.style.unicodeBidi === 'isolate-override'
279
+ ? bidiControls(block.style, true)
280
+ : null;
281
+ if (own) {
282
+ const first = items.find(isText);
283
+ let last;
284
+ for (const item of items)
285
+ if (item.kind === 'text')
286
+ last = item;
287
+ if (first && last) {
288
+ const opening = [];
289
+ pushControls(opening, own[0], block, first.start);
290
+ items.unshift(...opening);
291
+ pushControls(items, own[1], block, last.start + last.length);
292
+ }
293
+ }
294
+ if (options.firstLine)
295
+ firstLineColour(items, options.firstLine);
36
296
  const style = block.style;
297
+ const wrapWords = overflowWrapOf(style, items);
37
298
  const base = {
38
299
  family: style.fontFamily,
39
300
  size: style.fontSize,
@@ -42,14 +303,86 @@ export function layoutInline(block, options) {
42
303
  color: style.color,
43
304
  };
44
305
  const lineHeightMul = lineHeightMultiplier(fonts, style);
45
- const align = alignFor(style);
46
- const indent = indentOf(style, options.width);
47
- const hasAtomics = items.some((i) => i.kind === 'atomic');
48
- const floated = options.floats?.intersects(options.startY, Infinity) ?? false;
306
+ // The lines the end of the text or a forced break ends take
307
+ // `text-align-last` (CSS Text 3, 7.2), and a text that does not wrap is
308
+ // all such lines. Where they are aligned otherwise than the rest, and
309
+ // neither is justified — which fills a line whatever its alignment — the
310
+ // lines are made one at a time, each aligned its own way.
311
+ const lastAlign = lastAlignOf(style);
312
+ const align = engineAlign(wraps(style) ? style.textAlign : lastAlign);
313
+ const rtl = style.direction === 'rtl';
314
+ const alignedApart = wraps(style) &&
315
+ style.textAlign !== 'justify' &&
316
+ lastAlign !== 'justify' &&
317
+ alignShift(style.textAlign, rtl) !== alignShift(lastAlign, rtl);
318
+ const indent = blockIndent(block, options.width);
319
+ let hasAtomics = false;
320
+ let hasEdges = false;
321
+ let hasOffset = false;
322
+ let raised = false;
323
+ for (const item of items) {
324
+ if (item.kind === 'atomic')
325
+ hasAtomics = true;
326
+ else if (item.kind === 'text') {
327
+ // text in an inline box with a line height of its own, which an
328
+ // inline box of no edges has as much as one with them
329
+ if (!raised && fonts && inBoxOwningLeading(fonts, block, item.box)) {
330
+ hasOffset = true;
331
+ raised = true;
332
+ }
333
+ }
334
+ else if (item.kind === 'edge') {
335
+ hasEdges = true;
336
+ if (item.box.style.verticalAlign !== 'baseline' ||
337
+ ownsLeading(fonts, block.style, item.box.style) ||
338
+ tallerStrut(fonts, block.style, item.box)) {
339
+ hasOffset = true;
340
+ raised = true;
341
+ }
342
+ else if (!hasOffset && isOffset(item.box.style))
343
+ hasOffset = true;
344
+ }
345
+ }
346
+ // a float in the content is placed on the line it is on, which only the
347
+ // line-at-a-time loop knows, as it does whatever floats are beside it
348
+ const floated = floatCount > 0 ||
349
+ (options.floats?.intersects(options.startY, Infinity) ?? false);
350
+ // An inline box's edges take room on their lines. Where nothing else on
351
+ // the lines has to be placed a piece at a time, they go into the one
352
+ // layout as spacers (`spacerRun`) — which only holds where no bidi
353
+ // reordering can move one off the side of its box it belongs to, and no
354
+ // box between edges is moved afterwards, which needs its text apart.
355
+ const spaced = hasEdges && !hasAtomics && !hasOffset && spacersHold(style, items);
356
+ // a first line in fonts of its own is found a piece at a time, in them
357
+ const restyled = options.firstLineStyle !== undefined;
358
+ // and where one layout makes a line shorter than the strut, its text all
359
+ // smaller than the block's, the lines are made a piece at a time too,
360
+ // where every line holds it (`shortOfStrut`)
361
+ const strut = fonts ? strutOf(fonts, style) : null;
362
+ // and the first line holds what it is handed besides: a marker's room
363
+ const firstStrut = withRoom(strut, options.firstStrut);
364
+ let strutted = false;
365
+ // Inline boxes' edges as spacers in one layout, where the engine left
366
+ // every edge on the line of the content it belongs to (`layoutSpaced`);
367
+ // the lines a piece at a time, below, where it did not.
368
+ if (spaced && !floated && !indent && !alignedApart && !restyled) {
369
+ const laid = layoutSpaced(items, base, style, options.width, lineHeightMul, align, fonts);
370
+ if (laid && !shortOfStrut(laid.lines, strut, firstStrut))
371
+ return laid;
372
+ if (laid)
373
+ strutted = true;
374
+ }
49
375
  // The text-only, float-free, unindented case: one call, every line — or
50
376
  // one call per *chunk*, when the text is long and carries hard breaks.
51
- if (!hasAtomics && !floated && !indent) {
52
- // `hasAtomics` is false, so every item is text; the cast is that fact.
377
+ fast: if (!hasAtomics &&
378
+ !hasEdges &&
379
+ !raised &&
380
+ !floated &&
381
+ !indent &&
382
+ !alignedApart &&
383
+ !restyled &&
384
+ !strutted) {
385
+ // no atomics and no edges, so everything is text
53
386
  const textItems = items;
54
387
  let total = 0;
55
388
  let hasNewline = false;
@@ -60,25 +393,101 @@ export function layoutInline(block, options) {
60
393
  }
61
394
  if (!total)
62
395
  return EMPTY;
63
- if (total > CHUNK_TRIGGER_CHARS && hasNewline) {
64
- return layoutChunked(textItems, base, style, options.width, lineHeightMul, align, fonts);
396
+ // `text-overflow` cuts every line that overflows, and a layout can cut
397
+ // only its last: one that a forced break ends is laid out apart, a hard
398
+ // line at a time, or the lines after the first were lost. A clamp is
399
+ // the paragraph's, and stays one layout.
400
+ const clamp = options.clamp;
401
+ let cut = cutOf(style, clamp);
402
+ const perLine = cut !== null && !clamp && hasNewline;
403
+ // an embedding cannot be cut into chunks
404
+ if (perLine && !hasControls(items)) {
405
+ return layoutChunked(textItems, base, style, options.width, lineHeightMul, align, fonts, { chars: Infinity, hardLines: 1, cut });
65
406
  }
66
- const runs = [];
67
- const spans = new SpanMap();
407
+ if (perLine)
408
+ cut = null;
409
+ if (total > CHUNK_TRIGGER_CHARS && hasNewline && !hasControls(items)) {
410
+ const chunked = layoutChunked(textItems, base, style, options.width, lineHeightMul, align, fonts);
411
+ if (!shortOfStrut(chunked.lines, strut, firstStrut))
412
+ return chunked;
413
+ strutted = true;
414
+ break fast;
415
+ }
416
+ let runs = [];
417
+ let spans = new SpanMap();
68
418
  for (const item of textItems) {
69
- spans.add(item.box.textStart, item.run.text.length);
419
+ spans.add(item.start, item.run.text.length, item.control ? null : item.box);
70
420
  runs.push(item.run);
71
421
  }
72
- const layout = fonts.layout(runs, base, {
73
- maxWidth: wraps(style) ? options.width : undefined,
422
+ // justified lines fill their width, so where all but the last are,
423
+ // the layout is aligned as those are
424
+ const justifies = wraps(style) && style.textJustify !== 'none';
425
+ const justifyRest = style.textAlign === 'justify' && justifies;
426
+ const justifyLast = lastAlign === 'justify' && justifies;
427
+ const layoutOptions = {
428
+ maxWidth: wraps(style) || cut ? options.width : undefined,
74
429
  lineHeight: lineHeightMul,
75
- align,
430
+ align: justifyRest && !justifyLast ? engineAlign(lastAlign) : align,
76
431
  direction: style.direction,
77
- });
432
+ overflowWrap: cutWrap(style, cut, wrapWords),
433
+ ...cut,
434
+ };
435
+ let layout = fonts.layout(runs, base, layoutOptions);
78
436
  LAYOUT_RUNS.set(layout, runs);
437
+ // A clamp that cut the text short says so, for the clamp point after
438
+ // it, and its last line ends in an ellipsis — as it does where the
439
+ // clamp point falls just after the lines, more of the container after
440
+ // them (CSS Overflow 4, 4.2).
441
+ let clampCut = false;
442
+ if (clamp) {
443
+ const shown = layout.lines.length;
444
+ clampCut =
445
+ shown > 0 &&
446
+ shown >= clamp.lines &&
447
+ (layout.truncated ?? inkBeyond(runs, layout.lines[shown - 1].end));
448
+ if (clamp.ellipsis &&
449
+ shown > 0 &&
450
+ shown === clamp.lines &&
451
+ (clampCut || clamp.follows)) {
452
+ const ended = ellipsized(fonts, textItems, runs, base, layoutOptions, layout, options.width, style);
453
+ layout = ended.layout;
454
+ runs = ended.runs;
455
+ spans = ended.spans;
456
+ LAYOUT_RUNS.set(layout, runs);
457
+ }
458
+ }
459
+ // `text-wrap: balance`: the lines broken at the narrowest width that
460
+ // keeps as many of them, and then set in the whole width
461
+ let balanced = false;
462
+ if (style.textWrapStyle === 'balance' &&
463
+ layoutOptions.maxWidth !== undefined &&
464
+ !cut &&
465
+ style.textAlign !== 'justify' &&
466
+ layout.lines.length > 1 &&
467
+ layout.lines.length <= BALANCE_LINES) {
468
+ const width = layoutOptions.maxWidth;
469
+ const at = balancedWidth(fonts, runs, base, layoutOptions, width, layout);
470
+ if (at < width) {
471
+ layout = fonts.layout(runs, base, { ...layoutOptions, maxWidth: at });
472
+ LAYOUT_RUNS.set(layout, runs);
473
+ balanced = true;
474
+ }
475
+ }
476
+ layout = justifiedLayout(fonts, runs, base, layoutOptions, layout, options.width, { rest: justifyRest, last: justifyLast }).layout;
79
477
  const lines = [];
80
- const widest = emitLayout(layout, spans, 0, 0, lines);
81
- return { lines, height: layout.height, width: widest };
478
+ const widest = emitLayout(layout, spans, 0, 0, lines, layoutOptions.maxWidth === undefined
479
+ ? unwrappedPlacer(style, options.width, true)
480
+ : balanced
481
+ ? unwrappedPlacer(style, options.width, false)
482
+ : null);
483
+ // a clamp and a cut are the one layout's to make
484
+ if (!clamp && !cut && shortOfStrut(lines, strut, firstStrut)) {
485
+ strutted = true;
486
+ break fast;
487
+ }
488
+ return clampCut
489
+ ? { lines, height: layout.height, width: widest, cut: true }
490
+ : { lines, height: layout.height, width: widest };
82
491
  }
83
492
  // --- the general case: line at a time -------------------------------------
84
493
  const lines = [];
@@ -87,34 +496,213 @@ export function layoutInline(block, options) {
87
496
  let index = 0;
88
497
  let offset = 0;
89
498
  let open = openLine(indent);
90
- const close = () => {
91
- const line = finishLine(open, y);
499
+ /** Start edges waiting for the content they belong to: an inline box's
500
+ * opening margin, border and padding go on the line its first content
501
+ * goes on, so they are placed with it, never left at a line's end. */
502
+ let pending = [];
503
+ let pendingWidth = 0;
504
+ const placePending = (left) => {
505
+ if (pending.length)
506
+ open.left = left;
507
+ for (const edge of pending) {
508
+ const placed = {
509
+ box: edge.box,
510
+ side: 'start',
511
+ x: left + open.x,
512
+ width: edge.width,
513
+ };
514
+ open.edges.push(placed);
515
+ open.order.push({ kind: 'edge', at: open.x, item: placed });
516
+ open.x += edge.width;
517
+ }
518
+ pending = [];
519
+ pendingWidth = 0;
520
+ };
521
+ /** Floats met on a line they did not fit beside: they go at the top of
522
+ * the next line, as the line they were met on closes. */
523
+ let deferred = [];
524
+ /** Every float placed, in order, for the lines they stand beside. */
525
+ const floatsPlaced = [];
526
+ const placeDeferred = () => {
527
+ for (const box of deferred) {
528
+ options.floatBoxes.place(box, options.startY + y);
529
+ floatsPlaced.push(box);
530
+ }
531
+ deferred = [];
532
+ };
533
+ // How tall a line is taken to be before it is made, for its room beside
534
+ // the floats: the paragraph's strut, a line of its text. An item taller
535
+ // asks over its own height, and the line made over its whole (`close`).
536
+ // A 1.4em guess put a line of `line-height: 0` below floats it fitted
537
+ // beside.
538
+ const guess = strut ? strut.ascent + strut.descent : style.fontSize * 1.4;
539
+ const lifts = raised && fonts ? new Lifts(fonts, block.style) : null;
540
+ const restShift = alignShift(style.textAlign, rtl);
541
+ const lastShift = alignShift(lastAlign, rtl);
542
+ // a justified line fills its room, whatever made it a piece at a time
543
+ const justify = justification(style);
544
+ const lineJustify = fonts && (justify.rest || justify.last)
545
+ ? {
546
+ fonts,
547
+ base,
548
+ options: {
549
+ lineHeight: lineHeightMul,
550
+ align: 'left',
551
+ direction: style.direction,
552
+ overflowWrap: wrapWords,
553
+ },
554
+ }
555
+ : null;
556
+ // The first line in its `::first-line` fonts, until a line is closed
557
+ const lineOne = options.firstLineStyle
558
+ ? firstLineSetting(block, items, options.firstLineStyle, fonts, strut, lineJustify)
559
+ : null;
560
+ /** Close the open line; `last` where the text or a forced break ends it,
561
+ * which `text-align-last` aligns. */
562
+ const close = (last = false) => {
563
+ const onFirst = lineOne !== null && !lines.length;
564
+ const line = finishLine(open, y,
565
+ // the room beside the floats over the whole line box, which an
566
+ // inline-block can make taller than its text (CSS 2.1 9.5)
567
+ (height) => bandAt(options, y, Math.max(height, guess)), last || !wraps(style) ? lastShift : restShift, rtl, lines.length
568
+ ? strut
569
+ : withRoom(onFirst ? lineOne.strut : strut, options.firstStrut), onFirst ? lineOne.lifts : lifts, (last || !wraps(style) ? justify.last : justify.rest)
570
+ ? onFirst
571
+ ? lineOne.justify
572
+ : lineJustify
573
+ : null);
92
574
  if (!line) {
93
575
  open = openLine(0);
576
+ placeDeferred();
94
577
  return;
95
578
  }
96
579
  lines.push(line);
97
580
  widest = Math.max(widest, line.width);
98
581
  y += line.height;
99
582
  open = openLine(0);
583
+ placeDeferred();
100
584
  };
101
585
  while (index < items.length) {
102
- const band = bandAt(options, y, style.fontSize * 1.4);
586
+ const band = bandAt(options, y, guess);
103
587
  const available = band.right - band.left;
104
588
  const item = items[index];
589
+ const onFirst = lineOne !== null && !lines.length;
590
+ const lineItems = onFirst ? lineOne.items : items;
591
+ const lineBase = onFirst ? lineOne.base : base;
592
+ if (item.kind === 'float') {
593
+ // No higher than the top of the line it is met on (CSS 2.1 9.5.1):
594
+ // at that top where it fits beside what the line holds already, and
595
+ // what the line holds moves over for it; under the line where it
596
+ // does not, with the rest of the line's content still on the line.
597
+ // Where the line cannot break at the float, what follows it up to
598
+ // where it can is on the line too, and has to fit beside it as well,
599
+ // as browsers place one: `nowrap` text it is in the middle of runs on
600
+ // past it otherwise, under it.
601
+ const outer = options.floatBoxes.size(item.box);
602
+ // and after a float that waits for the next line, since none goes
603
+ // higher than one before it (9.5.1, rule 5)
604
+ let fits = !deferred.length &&
605
+ (isEmpty(open) || open.x + pendingWidth + outer <= available);
606
+ if (fits && !isEmpty(open) && !breaksAtEnd(open, style)) {
607
+ const after = unbreakableAfter(lineItems, index + 1, style, fonts, lineBase);
608
+ fits = open.x + pendingWidth + after + outer <= available;
609
+ }
610
+ if (fits) {
611
+ options.floatBoxes.place(item.box, options.startY + y);
612
+ floatsPlaced.push(item.box);
613
+ const left = bandAt(options, y, guess).left;
614
+ if (!isEmpty(open) && left !== open.left) {
615
+ const dx = left - open.left;
616
+ for (const text of open.texts)
617
+ text.drawX += dx;
618
+ for (const placed of open.atomics)
619
+ placed.x += dx;
620
+ for (const edge of open.edges)
621
+ edge.x += dx;
622
+ open.left = left;
623
+ }
624
+ }
625
+ else {
626
+ deferred.push(item.box);
627
+ }
628
+ index += 1;
629
+ continue;
630
+ }
631
+ if (item.kind === 'edge') {
632
+ if (item.side === 'start') {
633
+ pending.push(item);
634
+ pendingWidth += item.width;
635
+ }
636
+ else {
637
+ // an end edge follows the content it closes onto its line; it may
638
+ // run past the line's end, as a trailing space does, rather than
639
+ // leave the content it closes
640
+ placePending(band.left);
641
+ const placed = {
642
+ box: item.box,
643
+ side: 'end',
644
+ x: band.left + open.x,
645
+ width: item.width,
646
+ };
647
+ open.edges.push(placed);
648
+ open.order.push({ kind: 'edge', at: open.x, item: placed });
649
+ open.x += item.width;
650
+ open.left = band.left;
651
+ }
652
+ index += 1;
653
+ continue;
654
+ }
105
655
  if (item.kind === 'atomic') {
106
656
  const box = item.box;
107
657
  const outer = box.width + box.marginLeft + box.marginRight;
108
- if (open.x > 0 && open.x + outer > available) {
658
+ // An inline-block makes its line as tall as itself, and the line box
659
+ // must not run into a float over that height either (CSS 2.1 9.5):
660
+ // the room is what the floats leave beside all of it.
661
+ const tall = box.marginTop + box.height + box.marginBottom;
662
+ const lineTall = Math.max(guess, tall);
663
+ const room = tall > guess ? bandAt(options, y, tall) : band;
664
+ const roomWidth = room.right - room.left;
665
+ // a line with nothing on it keeps what comes first, indent or not:
666
+ // there is no break before it (a first line's `text-indent` is room
667
+ // taken, and no content)
668
+ if (open.x !== open.indent && open.x + pendingWidth + outer > roomWidth) {
109
669
  close();
110
670
  continue;
111
671
  }
112
- open.atomics.push({ box, x: band.left + open.x + box.marginLeft, y: 0 });
672
+ // an inline-block wider than the room a float left goes below it,
673
+ // as a word does (below)
674
+ if (isEmpty(open) &&
675
+ pendingWidth + outer > roomWidth &&
676
+ roomWidth < options.width) {
677
+ const below = belowFloats(options, y, lineTall);
678
+ if (below !== null) {
679
+ y = below;
680
+ continue;
681
+ }
682
+ }
683
+ placePending(room.left);
684
+ const placed = {
685
+ box,
686
+ x: room.left + open.x + box.marginLeft,
687
+ y: 0,
688
+ };
689
+ const raise = atomicRaise(fonts, box, (box.parent ?? block).style);
690
+ if (raise)
691
+ placed.raise = raise;
692
+ open.atomics.push(placed);
693
+ open.order.push({ kind: 'atomic', at: open.x, item: placed });
113
694
  open.x += outer;
695
+ open.hang = 0;
696
+ open.left = room.left;
114
697
  index += 1;
115
698
  continue;
116
699
  }
117
- const segment = segmentFrom(items, index, offset);
700
+ const plain = segmentFrom(lineItems, index, offset);
701
+ // measured as it will be drawn where it is justified, its spaces
702
+ // spaced apart (`HAIR`) — the same text, so every offset holds
703
+ const segment = lineJustify
704
+ ? { ...plain, runs: spacedApart(plain.runs) }
705
+ : plain;
118
706
  // `segmentFrom` always passes at least the text item it started on, so
119
707
  // this advances even when the slice came out empty — which is what stops
120
708
  // a zero-length tail from spinning the loop.
@@ -128,18 +716,36 @@ export function layoutInline(block, options) {
128
716
  // one layout, every line of it. This is the exit that keeps the loop
129
717
  // below from being quadratic on an ordinary paragraph: it runs line by
130
718
  // line only while it has to.
131
- const tailIsPlain = segment.nextIndex >= items.length &&
719
+ // An inline box's edges of no width on the open line are no room it
720
+ // takes, and are still on it: an empty `<span>` with a tall line height
721
+ // before the text was left on a line of its own after it, and the
722
+ // text's line was as short as the paragraph's
723
+ // (and its lines all aligned alike, which the last is not where
724
+ // `text-align-last` sets it apart)
725
+ const tailIsPlain = !alignedApart &&
726
+ !onFirst &&
727
+ !strutted &&
728
+ segment.nextIndex >= items.length &&
132
729
  open.x === 0 &&
133
730
  !open.atomics.length &&
731
+ !open.edges.length &&
732
+ !pending.length &&
733
+ !deferred.length &&
134
734
  !(options.floats?.intersects(options.startY + y, Infinity) ?? false);
135
735
  if (tailIsPlain) {
136
- const layout = fonts.layout(segment.runs, base, {
736
+ const tailOptions = {
137
737
  maxWidth: wraps(style) ? Math.max(1, available) : undefined,
138
738
  lineHeight: lineHeightMul,
139
739
  align,
140
740
  direction: style.direction,
141
- });
741
+ overflowWrap: wrapWords,
742
+ };
743
+ let layout = fonts.layout(segment.runs, base, tailOptions);
142
744
  LAYOUT_RUNS.set(layout, segment.runs);
745
+ // the lines past the floats are justified as the lines beside them
746
+ // were (`finishLine`), in the room they have
747
+ layout = justifiedLayout(fonts, segment.runs, base, tailOptions, layout, Math.max(1, available), justify).layout;
748
+ const tail = [];
143
749
  for (let i = 0; i < layout.lines.length; i += 1) {
144
750
  const natural = layout.lines[i];
145
751
  const text = {
@@ -150,24 +756,33 @@ export function layoutInline(block, options) {
150
756
  textStart: segment.spans.documentAt(natural.start),
151
757
  textEnd: segment.spans.documentAt(natural.end),
152
758
  layoutStart: natural.start,
759
+ spans: segment.spans,
153
760
  };
154
- lines.push({
761
+ segment.spans.giveGaps(text, natural.start, natural.end);
762
+ tail.push({
155
763
  x: band.left + natural.x,
156
764
  y: y + natural.y,
157
765
  width: natural.width,
158
766
  height: natural.height,
159
- baseline: natural.baseline,
767
+ baseline: natural.baseline - natural.y,
160
768
  texts: [text],
161
769
  textStart: text.textStart,
162
770
  textEnd: text.textEnd,
163
771
  atomics: [],
164
772
  });
165
- widest = Math.max(widest, natural.width);
166
773
  }
167
- y += layout.height;
168
- index = segment.nextIndex;
169
- offset = 0;
170
- continue;
774
+ // or a line at a time, where one of them is shorter than the strut
775
+ if (!shortOfStrut(tail, strut, lines.length ? null : firstStrut)) {
776
+ for (const line of tail) {
777
+ lines.push(line);
778
+ widest = Math.max(widest, line.width);
779
+ }
780
+ y += layout.height;
781
+ index = segment.nextIndex;
782
+ offset = 0;
783
+ continue;
784
+ }
785
+ strutted = true;
171
786
  }
172
787
  // One line: `maxLines: 1` cuts the layout at the first break and its
173
788
  // `truncated` flag answers "did the segment wrap" — so a line costs one
@@ -175,15 +790,29 @@ export function layoutInline(block, options) {
175
790
  // re-cut (which is what an earlier shape paid, per line, beside every
176
791
  // float). ntk's shaping memo makes the successive cuts cheap; only the
177
792
  // line breaker re-runs.
178
- const room = Math.max(1, available - open.x);
179
- const fragment = fonts.layout(segment.runs, base, {
180
- maxWidth: wraps(style) ? room : undefined,
181
- lineHeight: lineHeightMul,
182
- // A fragment that does not start at the line's left edge cannot be
183
- // aligned by the text layout: the alignment belongs to the whole line,
184
- // which only this loop can see.
185
- align: open.x > 0 ? 'left' : align,
793
+ const room = Math.max(1, available - open.x - pendingWidth);
794
+ // A float met inside a word, or inside a `nowrap` element, is no place
795
+ // for the line to break: the text after it up to where it may break
796
+ // is on this line too, so this line keeps room for it — or breaks
797
+ // before the word it is tied to. It overran the line otherwise, the
798
+ // float having gone below, and the break it could have taken passed.
799
+ const tied = wraps(style) &&
800
+ items[segment.nextIndex]?.kind === 'float' &&
801
+ !BREAKS_AFTER.test(segment.runs[segment.runs.length - 1].text)
802
+ ? unbreakableAfter(lineItems, segment.nextIndex, style, fonts, lineBase)
803
+ : 0;
804
+ const fragment = fonts.layout(segment.runs, lineBase, {
805
+ maxWidth: wraps(style) ? Math.max(1, room - tied) : undefined,
806
+ lineHeight: onFirst ? lineOne.lineHeight : lineHeightMul,
807
+ // Never aligned by the text layout: the alignment belongs to the whole
808
+ // line — its text, its atomics and its inline boxes' edges together —
809
+ // which only this loop can see, and `finishLine` shifts it all. Laid
810
+ // out aligned, the first fragment of a centred line was centred alone
811
+ // and whatever followed it on the line was placed as though it had
812
+ // not been, over it.
813
+ align: 'left',
186
814
  direction: style.direction,
815
+ overflowWrap: wrapWords,
187
816
  maxLines: 1,
188
817
  });
189
818
  LAYOUT_RUNS.set(fragment, segment.runs);
@@ -193,42 +822,91 @@ export function layoutInline(block, options) {
193
822
  offset = 0;
194
823
  continue;
195
824
  }
196
- open.texts.push({
825
+ // Nothing fits beside the floats — the first word is wider than the
826
+ // room they leave, and was either run past it or cut inside itself to
827
+ // fit — so the line moves down to where a float ends, and tries again
828
+ // there (CSS 2.1 9.5). Only where a float took some of the line's width:
829
+ // past the floats the line has the whole of it, and a word wider still
830
+ // is `overflow-wrap`'s to break or to let run past.
831
+ if (isEmpty(open) &&
832
+ available < options.width &&
833
+ tooNarrow(segment.runs, first.end, first.width, room)) {
834
+ const below = belowFloats(options, y, guess);
835
+ if (below !== null) {
836
+ y = below;
837
+ continue;
838
+ }
839
+ }
840
+ // An inline box's opening edge goes with the first word after it: where
841
+ // the word does not fit the room the edge leaves, both go to the next
842
+ // line, rather than the edge being left at this one's end. And a word
843
+ // that may start a line — after a space the line ends on, or after an
844
+ // atomic, which has a break after it (CSS Text 3, 5.1) — goes to the
845
+ // next line whole where it does not fit what is left of this one,
846
+ // rather than being broken inside itself to fit it: the text after an
847
+ // inline-block, or after a float it was cut at, was its first letter
848
+ // at the line's end and the rest on the next.
849
+ const startsLine = wraps(style) &&
850
+ (open.hang > 0 || open.order[open.order.length - 1]?.kind === 'atomic');
851
+ if (!isEmpty(open) &&
852
+ (pendingWidth > 0
853
+ ? tooNarrow(segment.runs, first.end, first.width, room)
854
+ : startsLine &&
855
+ // run past the room — its white space is no part of the width —
856
+ // or cut inside itself to fit it
857
+ (first.width > room + 0.5 ||
858
+ tooNarrow(segment.runs, first.end, first.width, room)))) {
859
+ close();
860
+ continue;
861
+ }
862
+ placePending(band.left);
863
+ const placed = {
197
864
  layout: fragment,
198
865
  layoutLine: 0,
199
- // The layout's own `first.x` carries the alignment offset, so the
200
- // origin is the band edge plus the cursor — subtracting `first.x`
201
- // here would left-align a centred line. Mid-line continuations were
202
- // laid out `left`, where `first.x` is zero and the two agree.
866
+ // laid out `left`, so the fragment starts at the cursor
203
867
  drawX: band.left + open.x,
204
868
  drawY: 0, // filled in by `finishLine`, which is where the top is known
205
869
  textStart: segment.spans.documentAt(first.start),
206
870
  textEnd: segment.spans.documentAt(first.end),
207
871
  layoutStart: first.start,
872
+ spans: segment.spans,
873
+ };
874
+ segment.spans.giveGaps(placed, first.start, first.end);
875
+ open.texts.push(placed);
876
+ open.order.push({
877
+ kind: 'text',
878
+ at: open.x,
879
+ item: placed,
880
+ reads: readingOf(segment.runs, first.start, first.end),
208
881
  });
209
882
  open.x += first.width;
210
883
  open.left = band.left;
884
+ open.hang = 0;
211
885
  // Did the segment wrap? ntk answers with `truncated`. A layout that does
212
886
  // not carry the flag — react-x11's Cocoa engine reports none — is asked
213
887
  // the same of its line ends instead: it wrapped if anything but
214
888
  // whitespace follows the first line. Read as "fitted", a cut fragment
215
889
  // advanced past the whole segment, and a paragraph beside a float lost
216
- // every line after its first.
217
- const wrapped = fragment.truncated ?? inkBeyond(segment.runs, first.end);
890
+ // every line after its first. And a line that ends at a `<br>` is over
891
+ // whether or not anything followed it in this segment: what comes next
892
+ // — an atomic, an element's edge — is the next line's.
893
+ const wrapped = breakBefore(segment.runs, first.end) ||
894
+ (fragment.truncated ?? inkBeyond(segment.runs, first.end));
218
895
  if (!wrapped) {
219
896
  // It fitted: the cursor stays on this line for whatever comes next.
220
897
  //
221
- // With one correction first. ntk strips a line's trailing whitespace —
222
- // right at a real line end, wrong here, where the "line" is only a
223
- // fragment and an atomic follows on the same one: `Name <input>` laid
224
- // the space out to width zero and the control sat flush against the
225
- // label (nbsp included; ntk's whitespace set has U+00A0 in it). The
226
- // stripped advance is measured back and added to the cursor.
898
+ // With one correction first. A text engine strips a line's trailing
899
+ // whitespace — right at a real line end, wrong here, where the "line"
900
+ // is only a fragment and an atomic follows on the same one: `Name
901
+ // <input>` laid the space out to width zero and the control sat flush
902
+ // against the label. The stripped advance is measured back and added
903
+ // to the cursor: what the engine strips, and no more (`hungSpaces`).
227
904
  if (segment.nextIndex < items.length) {
905
+ const hung = hungSpaces(fonts, segment.runs[segment.runs.length - 1]);
228
906
  let trailing = '';
229
907
  for (let i = segment.runs.length - 1; i >= 0; i -= 1) {
230
908
  const text = segment.runs[i].text;
231
- const m = /[ \t\u00A0]+$/.exec(text);
909
+ const m = hung.exec(text);
232
910
  if (!m)
233
911
  break;
234
912
  trailing = m[0] + trailing;
@@ -236,45 +914,294 @@ export function layoutInline(block, options) {
236
914
  break;
237
915
  }
238
916
  if (trailing) {
239
- open.x +=
917
+ // and noted: if what follows goes to the next line after all,
918
+ // these spaces end this one, where CSS removes them (16.6.1)
919
+ open.hang =
240
920
  spaceAdvance(fonts, segment.runs[segment.runs.length - 1]) *
241
921
  trailing.length;
922
+ open.x += open.hang;
242
923
  }
243
924
  }
244
925
  index = segment.nextIndex;
245
926
  offset = 0;
246
927
  continue;
247
928
  }
248
- close();
929
+ close(breakBefore(segment.runs, first.end));
249
930
  const advanced = advance(items, index, offset, first.end);
250
931
  index = advanced.index;
251
932
  offset = advanced.offset;
252
933
  }
253
- if (open.texts.length || open.atomics.length)
254
- close();
255
- return { lines, height: y, width: widest };
934
+ if (pending.length)
935
+ placePending(bandAt(options, y, guess).left);
936
+ if (open.texts.length || open.atomics.length || open.edges.length) {
937
+ close(true);
938
+ }
939
+ placeDeferred();
940
+ if (!floatsPlaced.length)
941
+ return { lines, height: y, width: widest };
942
+ const floatRow = besideLines(Number.isFinite(options.width) ? [] : lines, floatsPlaced, options.startY);
943
+ return { lines, height: y, width: widest, floatRow };
256
944
  }
257
945
  const EMPTY = { lines: [], height: 0, width: 0 };
258
- /** Append one layout's lines as LineBoxes at an offset; returns the widest. */
259
- function emitLayout(layout, spans, xOff, yOff, lines) {
946
+ /**
947
+ * Which line each float a paragraph placed stands beside: the one whose top
948
+ * it went at, whose content moved over for it — added up on the line, for
949
+ * what the line is measured with (`LineBox.floats`). The widest row of the
950
+ * rest, side by side where they went at one top, is the answer. Both lines
951
+ * and floats come in order down the paragraph, since a float goes no
952
+ * higher than the one before it.
953
+ *
954
+ * The lines are handed over only where the paragraph had no width limit,
955
+ * where its content is measured at its widest and a float is beside a line
956
+ * because nothing made the line go under it. At a width, the least a
957
+ * paragraph can be is its widest word or float, not the two side by side
958
+ * (where the word ran past the room a float left it), and its floats are
959
+ * rows of their own.
960
+ */
961
+ function besideLines(lines, placed, startY) {
962
+ let row = 0;
963
+ let rowTop = NaN;
964
+ let widest = 0;
965
+ let at = 0;
966
+ for (const box of placed) {
967
+ const top = box.y - box.marginTop - startY;
968
+ const width = box.width + box.marginLeft + box.marginRight;
969
+ while (at < lines.length && lines[at].y < top - 0.01)
970
+ at += 1;
971
+ const line = lines[at];
972
+ if (line && Math.abs(line.y - top) <= 0.01) {
973
+ line.floats = (line.floats ?? 0) + width;
974
+ }
975
+ else if (Math.abs(top - rowTop) <= 0.01)
976
+ row += width;
977
+ else {
978
+ widest = Math.max(widest, row);
979
+ row = width;
980
+ rowTop = top;
981
+ }
982
+ }
983
+ return Math.max(widest, row);
984
+ }
985
+ /** A no-break space in a paragraph's face, and its advance, per font
986
+ * manager and per style — which the cascade shares between every element
987
+ * of a kind, so a document of paragraphs asks once. */
988
+ const SPACERS = new WeakMap();
989
+ /**
990
+ * An inline box's edge as a run of the one layout: a no-break space in the
991
+ * paragraph's face, letter-spaced to the edge's width. A no-break space
992
+ * glues to the text on either side, so a start edge goes to whichever line
993
+ * the box's first word goes to, and an end edge stays with its last; a line
994
+ * may still break after a space before it (UAX #14, LB12a). It is drawn as
995
+ * nothing, and is no part of the document's text: the line's text notes
996
+ * where it is (`LineText.gaps`). Laid out piece by piece instead, a
997
+ * paragraph with an inline `<code>` in it — whose padding is an edge —
998
+ * cost five layouts where one does, and a document of them reflowed two
999
+ * and a half times slower.
1000
+ */
1001
+ function spacerRun(fonts, style, width) {
1002
+ let byStyle = SPACERS.get(fonts);
1003
+ if (!byStyle) {
1004
+ byStyle = new WeakMap();
1005
+ SPACERS.set(fonts, byStyle);
1006
+ }
1007
+ let spacer = byStyle.get(style);
1008
+ if (!spacer) {
1009
+ const face = {
1010
+ family: style.fontFamily,
1011
+ size: style.fontSize,
1012
+ weight: style.fontWeight,
1013
+ style: style.fontStyle === 'normal'
1014
+ ? 'normal'
1015
+ : 'italic',
1016
+ };
1017
+ const run = { text: '\u00a0', ...face };
1018
+ spacer = { run, advance: fonts.layout([run], face, {}).width };
1019
+ byStyle.set(style, spacer);
1020
+ }
1021
+ return { ...spacer.run, letterSpacing: width - spacer.advance };
1022
+ }
1023
+ /** Text that bidi could reorder around a spacer: any right-to-left
1024
+ * letter, and the explicit embedding and isolate controls. */
1025
+ const REORDERS = /[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc\u202a-\u202e\u2066-\u2069]|[\ud802\ud803\ud83a\ud83b]/;
1026
+ /** Whether a paragraph's edges can be spacers: it reads left to right, and
1027
+ * nothing in it can be reordered. */
1028
+ function spacersHold(style, items) {
1029
+ if (style.direction !== 'ltr')
1030
+ return false;
1031
+ for (const item of items) {
1032
+ if (item.kind === 'text' && REORDERS.test(item.run.text))
1033
+ return false;
1034
+ // A negative margin takes room back. As a spacer it is a space with
1035
+ // negative letter spacing, which ntk's line fill counts and CoreText's
1036
+ // typesetter breaks the line before: the line at a time does the sum.
1037
+ if (item.kind === 'edge' &&
1038
+ (item.box.style.direction !== 'ltr' || item.width < 0))
1039
+ return false;
1040
+ }
1041
+ return true;
1042
+ }
1043
+ /**
1044
+ * A paragraph whose inline boxes have edges, as one layout with the edges
1045
+ * in it as spacers (`spacerRun`), each line told where its spacers are and
1046
+ * where they put the edges. Null where the engine began a line with an
1047
+ * edge that closes a box: a break after a box's last character is after
1048
+ * its end edge, which stays on the line of the content it closes (CSS Text
1049
+ * 3, 5.1), and a line break after a space may come before a no-break space
1050
+ * (UAX #14, LB12a), which is what a spacer is. The lines a piece at a time
1051
+ * keep it there.
1052
+ */
1053
+ function layoutSpaced(items, base, style, width, lineHeightMul, align, fonts) {
1054
+ const wrapWords = overflowWrapOf(style, items);
1055
+ const runs = [];
1056
+ const spans = new SpanMap();
1057
+ const spacers = [];
1058
+ let doc = 0;
1059
+ for (const item of items) {
1060
+ if (item.kind === 'text') {
1061
+ doc = item.start;
1062
+ break;
1063
+ }
1064
+ }
1065
+ for (const item of items) {
1066
+ if (item.kind === 'edge') {
1067
+ spacers.push({ at: spans.laidOut, edge: item });
1068
+ spans.add(doc, 1, null);
1069
+ runs.push(spacerRun(fonts, style, item.width));
1070
+ }
1071
+ else if (item.kind === 'text') {
1072
+ spans.add(item.start, item.run.text.length, item.control ? null : item.box);
1073
+ runs.push(item.run);
1074
+ if (!item.control)
1075
+ doc = item.start + item.run.text.length;
1076
+ }
1077
+ }
1078
+ const layoutOptions = {
1079
+ maxWidth: wraps(style) ? width : undefined,
1080
+ lineHeight: lineHeightMul,
1081
+ align,
1082
+ direction: style.direction,
1083
+ overflowWrap: wrapWords,
1084
+ };
1085
+ let layout = fonts.layout(runs, base, layoutOptions);
1086
+ LAYOUT_RUNS.set(layout, runs);
1087
+ // justified with its spacers in it, which are edges and not spaces
1088
+ layout = justifiedLayout(fonts, runs, base, layoutOptions, layout, width, justification(style), new Set(spacers.map((spacer) => spacer.at))).layout;
1089
+ const place = wraps(style) ? null : unwrappedPlacer(style, width);
1090
+ let offsets = null;
1091
+ const lines = [];
1092
+ let widest = 0;
1093
+ let next = 0;
1094
+ for (let i = 0; i < layout.lines.length; i += 1) {
1095
+ const natural = layout.lines[i];
1096
+ const dx = place ? place(natural) : 0;
1097
+ const edges = [];
1098
+ while (next < spacers.length && spacers[next].at < natural.end) {
1099
+ const { at, edge } = spacers[next];
1100
+ next += 1;
1101
+ if (at < natural.start)
1102
+ continue;
1103
+ if (i > 0 && edge.side === 'end' && at === natural.start)
1104
+ return null;
1105
+ const run = natural.runs?.find((r) => r.start === at);
1106
+ const x = run
1107
+ ? natural.x + run.x
1108
+ : layout.caretPosition(codePointAt((offsets ??= layoutOffsets(layout)), at)).x;
1109
+ edges.push({
1110
+ box: edge.box,
1111
+ side: edge.side,
1112
+ x: x + dx,
1113
+ width: edge.width,
1114
+ });
1115
+ }
1116
+ const text = {
1117
+ layout,
1118
+ layoutLine: i,
1119
+ drawX: dx,
1120
+ drawY: 0,
1121
+ textStart: spans.documentAt(natural.start),
1122
+ textEnd: spans.documentAt(natural.end),
1123
+ layoutStart: natural.start,
1124
+ spans,
1125
+ };
1126
+ spans.giveGaps(text, natural.start, natural.end);
1127
+ lines.push({
1128
+ x: natural.x + dx,
1129
+ y: natural.y,
1130
+ width: natural.width,
1131
+ height: natural.height,
1132
+ baseline: natural.baseline - natural.y,
1133
+ texts: [text],
1134
+ textStart: text.textStart,
1135
+ textEnd: text.textEnd,
1136
+ atomics: [],
1137
+ ...(edges.length ? { edges } : null),
1138
+ });
1139
+ widest = Math.max(widest, natural.width);
1140
+ }
1141
+ return { lines, height: layout.height, width: widest };
1142
+ }
1143
+ /** The code point a code-unit offset into a layout's text starts. */
1144
+ function codePointAt(offsets, units) {
1145
+ if (!offsets.length)
1146
+ return units;
1147
+ let lo = 0;
1148
+ let hi = offsets.length - 1;
1149
+ while (lo < hi) {
1150
+ const mid = (lo + hi + 1) >> 1;
1151
+ if (offsets[mid] <= units)
1152
+ lo = mid;
1153
+ else
1154
+ hi = mid - 1;
1155
+ }
1156
+ return lo;
1157
+ }
1158
+ /**
1159
+ * A code-unit offset in a line's layout as a document index, and back:
1160
+ * `textStart` is `layoutStart`, and every spacer before the offset is a
1161
+ * unit of the layout that is no text of the document's. A document offset
1162
+ * with a spacer right at it is past the spacer, where a caret or a range's
1163
+ * start goes — inside a padded box, after its padding — and before it as
1164
+ * a range's `end`, so a selection does not take in the padding after it.
1165
+ */
1166
+ export function documentOffsetOf(text, units) {
1167
+ let doc = text.textStart + (units - text.layoutStart);
1168
+ for (const gap of text.gaps ?? [])
1169
+ if (gap < units)
1170
+ doc -= 1;
1171
+ return doc;
1172
+ }
1173
+ export function layoutOffsetOf(text, doc, end = false) {
1174
+ let units = text.layoutStart + (doc - text.textStart);
1175
+ for (const gap of text.gaps ?? []) {
1176
+ if (end ? gap < units : gap <= units)
1177
+ units += 1;
1178
+ }
1179
+ return units;
1180
+ }
1181
+ /** Append one layout's lines as LineBoxes at an offset, each moved where
1182
+ * `place` says; returns the widest. */
1183
+ function emitLayout(layout, spans, xOff, yOff, lines, place = null) {
260
1184
  let widest = 0;
261
1185
  for (let i = 0; i < layout.lines.length; i += 1) {
262
1186
  const natural = layout.lines[i];
1187
+ const dx = place ? place(natural) : 0;
263
1188
  const text = {
264
1189
  layout,
265
1190
  layoutLine: i,
266
- drawX: xOff,
1191
+ drawX: xOff + dx,
267
1192
  drawY: yOff,
268
1193
  textStart: spans.documentAt(natural.start),
269
1194
  textEnd: spans.documentAt(natural.end),
270
1195
  layoutStart: natural.start,
1196
+ spans,
271
1197
  };
1198
+ spans.giveGaps(text, natural.start, natural.end);
272
1199
  lines.push({
273
- x: xOff + natural.x,
1200
+ x: xOff + dx + natural.x,
274
1201
  y: yOff + natural.y,
275
1202
  width: natural.width,
276
1203
  height: natural.height,
277
- baseline: natural.baseline,
1204
+ baseline: natural.baseline - natural.y,
278
1205
  texts: [text],
279
1206
  textStart: text.textStart,
280
1207
  textEnd: text.textEnd,
@@ -292,6 +1219,11 @@ function emitLayout(layout, spans, xOff, yOff, lines) {
292
1219
  const CHUNK_TRIGGER_CHARS = 16384;
293
1220
  const CHUNK_MAX_CHARS = 8192;
294
1221
  const CHUNK_MAX_HARD_LINES = 64;
1222
+ const CHUNKS = {
1223
+ chars: CHUNK_MAX_CHARS,
1224
+ hardLines: CHUNK_MAX_HARD_LINES,
1225
+ cut: null,
1226
+ };
295
1227
  /**
296
1228
  * A long hard-broken text — a `<pre>` holding a log, a wall of `<br>`s — laid
297
1229
  * out as a sequence of layouts split at newline boundaries.
@@ -305,7 +1237,8 @@ const CHUNK_MAX_HARD_LINES = 64;
305
1237
  * breaking never crosses a required break), so the chunk seams are
306
1238
  * invisible; each chunk stays a single batch a viewport can keep whole.
307
1239
  */
308
- function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
1240
+ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts, chunking = CHUNKS) {
1241
+ const wrapWords = overflowWrapOf(style, items);
309
1242
  const lines = [];
310
1243
  let widest = 0;
311
1244
  let y = 0;
@@ -313,17 +1246,25 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
313
1246
  let chunkSpans = new SpanMap();
314
1247
  let chars = 0;
315
1248
  let hardLines = 0;
1249
+ const { cut } = chunking;
1250
+ const place = wraps(style) || cut ? null : unwrappedPlacer(style, width);
316
1251
  const flush = () => {
317
1252
  if (!chunkRuns.length)
318
1253
  return;
319
- const layout = fonts.layout(chunkRuns, base, {
320
- maxWidth: wraps(style) ? width : undefined,
1254
+ const chunkOptions = {
1255
+ maxWidth: wraps(style) || cut ? width : undefined,
321
1256
  lineHeight: lineHeightMul,
322
1257
  align,
323
1258
  direction: style.direction,
324
- });
1259
+ overflowWrap: cutWrap(style, cut, wrapWords),
1260
+ ...cut,
1261
+ };
1262
+ let layout = fonts.layout(chunkRuns, base, chunkOptions);
325
1263
  LAYOUT_RUNS.set(layout, chunkRuns);
326
- widest = Math.max(widest, emitLayout(layout, chunkSpans, 0, y, lines));
1264
+ // a chunk ends at a forced break or at the paragraph's end, which is
1265
+ // where `justifiedRuns` takes a chunk's last line to end
1266
+ layout = justifiedLayout(fonts, chunkRuns, base, chunkOptions, layout, width, justification(style)).layout;
1267
+ widest = Math.max(widest, emitLayout(layout, chunkSpans, 0, y, lines, place));
327
1268
  y += layout.height;
328
1269
  chunkRuns = [];
329
1270
  chunkSpans = new SpanMap();
@@ -332,7 +1273,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
332
1273
  };
333
1274
  for (const item of items) {
334
1275
  let text = item.run.text;
335
- let docStart = item.box.textStart;
1276
+ let docStart = item.start;
336
1277
  while (text.length) {
337
1278
  // Scan up to the budgets, remembering the last newline: the split has
338
1279
  // to land just after one for the chunking to be exact.
@@ -341,8 +1282,8 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
341
1282
  let lines2 = hardLines;
342
1283
  let taken = chars;
343
1284
  while (i < text.length &&
344
- taken < CHUNK_MAX_CHARS &&
345
- lines2 < CHUNK_MAX_HARD_LINES) {
1285
+ taken < chunking.chars &&
1286
+ lines2 < chunking.hardLines) {
346
1287
  if (text.charCodeAt(i) === 10) {
347
1288
  lines2 += 1;
348
1289
  lastBreak = i;
@@ -353,7 +1294,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
353
1294
  if (i >= text.length) {
354
1295
  // the rest of this run fits the open chunk
355
1296
  chunkRuns.push(text === item.run.text ? item.run : { ...item.run, text });
356
- chunkSpans.add(docStart, text.length);
1297
+ chunkSpans.add(docStart, text.length, item.box);
357
1298
  chars = taken;
358
1299
  hardLines = lines2;
359
1300
  break;
@@ -369,7 +1310,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
369
1310
  const next = text.indexOf('\n', i);
370
1311
  const end = next < 0 ? text.length : next + 1;
371
1312
  chunkRuns.push({ ...item.run, text: text.slice(0, end) });
372
- chunkSpans.add(docStart, end);
1313
+ chunkSpans.add(docStart, end, item.box);
373
1314
  flush();
374
1315
  docStart += end;
375
1316
  text = text.slice(end);
@@ -377,7 +1318,7 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
377
1318
  }
378
1319
  const cut = lastBreak + 1;
379
1320
  chunkRuns.push({ ...item.run, text: text.slice(0, cut) });
380
- chunkSpans.add(docStart, cut);
1321
+ chunkSpans.add(docStart, cut, item.box);
381
1322
  flush();
382
1323
  docStart += cut;
383
1324
  text = text.slice(cut);
@@ -387,30 +1328,207 @@ function layoutChunked(items, base, style, width, lineHeightMul, align, fonts) {
387
1328
  return { lines, height: y, width: widest };
388
1329
  }
389
1330
  function openLine(indent) {
390
- return { x: indent, left: 0, texts: [], atomics: [] };
1331
+ return {
1332
+ x: indent,
1333
+ left: 0,
1334
+ texts: [],
1335
+ atomics: [],
1336
+ edges: [],
1337
+ order: [],
1338
+ hang: 0,
1339
+ indent,
1340
+ };
1341
+ }
1342
+ /** Whether a line may break where it has got to: after the white space it
1343
+ * ends on, which hangs, or after an inline-block, where its block wraps. */
1344
+ function breaksAtEnd(open, style) {
1345
+ return (wraps(style) &&
1346
+ (open.hang > 0 || open.order[open.order.length - 1]?.kind === 'atomic'));
1347
+ }
1348
+ /**
1349
+ * How wide the content from `from` on is up to where its line may break:
1350
+ * its text up to a space it may break at — one `holdNoWrap` left a space —
1351
+ * or, in a block that does not wrap, up to a line's end, with the edges of
1352
+ * inline boxes on the way, and an inline-block, which has a break before it
1353
+ * where the block wraps. Floats take no room on the line.
1354
+ */
1355
+ function unbreakableAfter(items, from, style, fonts, base) {
1356
+ const wrapping = wraps(style);
1357
+ const runs = [];
1358
+ let width = 0;
1359
+ for (let i = from; i < items.length; i += 1) {
1360
+ const item = items[i];
1361
+ if (item.kind === 'float')
1362
+ continue;
1363
+ if (item.kind === 'edge') {
1364
+ width += item.width;
1365
+ continue;
1366
+ }
1367
+ if (item.kind === 'atomic') {
1368
+ if (wrapping)
1369
+ break;
1370
+ width += item.box.width + item.box.marginLeft + item.box.marginRight;
1371
+ continue;
1372
+ }
1373
+ const text = item.run.text;
1374
+ const stop = wrapping ? text.search(/[ \t\n]/) : text.indexOf('\n');
1375
+ if (stop < 0) {
1376
+ runs.push(item.run);
1377
+ continue;
1378
+ }
1379
+ if (stop > 0)
1380
+ runs.push({ ...item.run, text: text.slice(0, stop) });
1381
+ break;
1382
+ }
1383
+ return runs.length ? width + fonts.layout(runs, base, {}).width : width;
391
1384
  }
392
- function finishLine(open, y) {
393
- if (!open.texts.length && !open.atomics.length)
1385
+ /**
1386
+ * Close a line: its height and baseline from what is on it, then where it
1387
+ * sits — in the room `bandFor` answers for a line that tall, and aligned in
1388
+ * it by `shift` of the room it did not use (0 flush left, 1 flush right, ½
1389
+ * centred), moved onto every text, atomic and edge on it at once.
1390
+ */
1391
+ function finishLine(open, y, bandFor, shift, rtl, strut, lifts = null, justify = null) {
1392
+ if (!open.texts.length && !open.atomics.length && !open.edges.length) {
394
1393
  return null;
395
- let ascent = 0;
396
- let descent = 0;
397
- let height = 0;
398
- for (const text of open.texts) {
399
- const natural = text.layout.lines[text.layoutLine];
400
- ascent = Math.max(ascent, natural.baseline);
401
- descent = Math.max(descent, natural.height - natural.baseline);
402
- height = Math.max(height, natural.height);
403
1394
  }
1395
+ const reordered = open.order.some((p) => needsOrdering(p, rtl));
1396
+ if (reordered)
1397
+ reorderLine(open, rtl);
1398
+ // Every line box starts with the block's strut, its face at its line
1399
+ // height, so a line of images alone is still as tall as `line-height`
1400
+ // makes a line (CSS 2.1 10.8.1). Not one with nothing on it that takes
1401
+ // room, which has no height at all (9.4.2), nor one that holds a block
1402
+ // an inline box was split around, which is no line of text.
1403
+ const held = strut &&
1404
+ (open.texts.length > 0 ||
1405
+ open.atomics.some((placed) => placed.box.style.display.startsWith('inline')) ||
1406
+ open.edges.some((edge) => edge.width !== 0 || hasEdgeOn(edge.box, edge.side)));
1407
+ let ascent = held ? strut.ascent : 0;
1408
+ let descent = held ? strut.descent : 0;
1409
+ // A text `vertical-align` raises takes its own box's room about where it
1410
+ // is raised to. A `top` or `bottom` box takes the room of all it holds
1411
+ // on the line about its baseline, and the line is as tall as that.
1412
+ const lifted = lifts ? open.texts.map((text) => lifts.of(text)) : null;
1413
+ let edges = null;
1414
+ for (let i = 0; i < open.texts.length; i += 1) {
1415
+ const lift = lifted?.[i];
1416
+ if (lift) {
1417
+ if (lift.edge) {
1418
+ edges ??= new Map();
1419
+ let room = edges.get(lift.edge.box);
1420
+ if (!room) {
1421
+ room = {
1422
+ to: lift.edge.to,
1423
+ ascent: lift.edge.ascent,
1424
+ descent: lift.edge.descent,
1425
+ };
1426
+ edges.set(lift.edge.box, room);
1427
+ }
1428
+ room.ascent = Math.max(room.ascent, lift.ascent + lift.raise);
1429
+ room.descent = Math.max(room.descent, lift.descent - lift.raise);
1430
+ }
1431
+ else {
1432
+ ascent = Math.max(ascent, lift.ascent + lift.raise);
1433
+ descent = Math.max(descent, lift.descent - lift.raise);
1434
+ }
1435
+ continue;
1436
+ }
1437
+ const natural = open.texts[i].layout.lines[open.texts[i].layoutLine];
1438
+ const own = natural.baseline - natural.y;
1439
+ ascent = Math.max(ascent, own);
1440
+ descent = Math.max(descent, natural.height - own);
1441
+ }
1442
+ // an inline box with no text on the line is on it all the same, as tall
1443
+ // as its own face and line height make it (CSS 2.1 10.8): an empty one,
1444
+ // or one whose text is on another line — raised, or with a line height
1445
+ // of its own, which an empty `<span>` made the line's no taller for.
1446
+ // Only on a line that is one at all, which is none for boxes of no
1447
+ // edges and no text (9.4.2)
1448
+ if (lifts && held) {
1449
+ for (const placed of open.edges) {
1450
+ if (placed.side !== 'start' || placed.box.kind !== 'inline')
1451
+ continue;
1452
+ const room = lifts.ofBox(placed.box);
1453
+ if (!room)
1454
+ continue;
1455
+ ascent = Math.max(ascent, room.ascent + room.raise);
1456
+ descent = Math.max(descent, room.descent - room.raise);
1457
+ }
1458
+ }
1459
+ // `top` and `bottom` boxes are set against the line's edges rather than
1460
+ // its baseline, so all they ask of it is to be as tall as they are
1461
+ let top = 0;
1462
+ let bottom = 0;
404
1463
  for (const placed of open.atomics) {
405
- const h = placed.box.height + placed.box.marginTop + placed.box.marginBottom;
406
- const va = placed.box.style.verticalAlign;
407
- if (va === 'top' || va === 'bottom' || va === 'middle')
408
- height = Math.max(height, h);
409
- else
410
- ascent = Math.max(ascent, h);
1464
+ const box = placed.box;
1465
+ const h = box.height + box.marginTop + box.marginBottom;
1466
+ const va = box.style.verticalAlign;
1467
+ if (va === 'top' || va === 'bottom') {
1468
+ if (va === 'top')
1469
+ top = Math.max(top, h);
1470
+ else
1471
+ bottom = Math.max(bottom, h);
1472
+ continue;
1473
+ }
1474
+ // on the line's baseline by its own, raised from it by its
1475
+ // `vertical-align`, and below it by the rest of it
1476
+ const raise = placed.raise ?? 0;
1477
+ const b = atomicBaseline(box);
1478
+ ascent = Math.max(ascent, b + raise);
1479
+ descent = Math.max(descent, h - b - raise);
1480
+ }
1481
+ if (edges) {
1482
+ for (const room of edges.values()) {
1483
+ const h = room.ascent + room.descent;
1484
+ if (room.to === 'top')
1485
+ top = Math.max(top, h);
1486
+ else
1487
+ bottom = Math.max(bottom, h);
1488
+ }
1489
+ }
1490
+ // Where the baseline goes in a line one of them made taller than the
1491
+ // rest is left open (CSS 2.1 10.8.1). Browsers keep it where the rest
1492
+ // puts it, under the line's top, and only a taller `bottom` box moves it
1493
+ // down: an image set `top` beside a line of text has the text at its
1494
+ // top, not halfway down it.
1495
+ let height = ascent + descent;
1496
+ let baseline = ascent;
1497
+ if (bottom > height) {
1498
+ baseline += bottom - height;
1499
+ height = bottom;
1500
+ }
1501
+ height = Math.max(height, top);
1502
+ // Placed beside the floats at the text's height; a float the whole line
1503
+ // reaches moves it, and the alignment divides the room that is left.
1504
+ // The indent is at the line's start, which is its right in a
1505
+ // right-to-left paragraph: the content moves back over it there, and
1506
+ // the room it takes is at the other end (CSS 2.1 16.1).
1507
+ const band = bandFor(height);
1508
+ const indent = rtl ? open.indent : 0;
1509
+ let used = open.x - open.hang - indent;
1510
+ let free = band.right - indent - band.left - used;
1511
+ // A justified line fills its room (CSS Text 3, 7.4). Not yet one that
1512
+ // reads right to left, whose pieces are in visual order by now and would
1513
+ // take their shares from the wrong end: that is left at its start, as it
1514
+ // was.
1515
+ if (justify && !rtl && !reordered && Number.isFinite(free) && free > 0) {
1516
+ const grew = justifyLine(open, free, justify);
1517
+ used += grew;
1518
+ free -= grew;
1519
+ }
1520
+ let dx = band.left - open.left - indent;
1521
+ if (shift > 0 && Number.isFinite(free) && free > 0)
1522
+ dx += free * shift;
1523
+ if (dx) {
1524
+ for (const text of open.texts)
1525
+ text.drawX += dx;
1526
+ for (const placed of open.atomics)
1527
+ placed.x += dx;
1528
+ for (const edge of open.edges)
1529
+ edge.x += dx;
1530
+ open.left += dx;
411
1531
  }
412
- height = Math.max(height, ascent + descent);
413
- const baseline = Math.max(ascent, (height - ascent - descent) / 2 + ascent);
414
1532
  let textStart = Infinity;
415
1533
  let textEnd = 0;
416
1534
  for (const text of open.texts) {
@@ -418,63 +1536,597 @@ function finishLine(open, y) {
418
1536
  textEnd = Math.max(textEnd, text.textEnd);
419
1537
  }
420
1538
  const line = {
421
- x: open.left,
1539
+ x: open.left + indent,
422
1540
  y,
423
- width: open.x,
1541
+ width: used,
424
1542
  height,
425
1543
  baseline,
426
1544
  texts: open.texts,
427
1545
  textStart: Number.isFinite(textStart) ? textStart : 0,
428
1546
  textEnd,
429
1547
  atomics: open.atomics,
1548
+ ...(open.edges.length ? { edges: open.edges } : null),
430
1549
  };
431
1550
  for (const placed of open.atomics)
432
- placed.y = y + alignAtomic(placed.box, line);
1551
+ placed.y = y + alignAtomic(placed, line);
433
1552
  // Every fragment on this line shares the line's baseline, whatever its own
434
- // layout thinks: that is what makes a small `<sup>` beside body text sit on
435
- // the same baseline rather than on its own.
436
- for (const text of open.texts) {
1553
+ // layout thinks: that is what makes a small `<span>` beside body text sit
1554
+ // on the same baseline rather than on its own. One that `vertical-align`
1555
+ // raises is drawn that far above it.
1556
+ for (let i = 0; i < open.texts.length; i += 1) {
1557
+ const text = open.texts[i];
437
1558
  const natural = text.layout.lines[text.layoutLine];
438
- text.drawY = y + baseline - natural.baseline - natural.y;
1559
+ let at = y + baseline;
1560
+ const lift = lifted?.[i];
1561
+ if (lift) {
1562
+ const room = lift.edge && edges.get(lift.edge.box);
1563
+ if (lift.edge?.to === 'top')
1564
+ at = y + room.ascent - lift.raise;
1565
+ else if (lift.edge)
1566
+ at = y + height - room.descent - lift.raise;
1567
+ else
1568
+ at -= lift.raise;
1569
+ if (at !== y + baseline) {
1570
+ TEXT_RAISES.set(text, y + baseline - at);
1571
+ SHIFTED_LINES.add(line);
1572
+ }
1573
+ // and the boxes it is in, up to the `top` or `bottom` one, whose
1574
+ // backgrounds are drawn from their own baselines
1575
+ if (lift.edge && lifts) {
1576
+ const owner = text.spans.boxAt?.(text.layoutStart);
1577
+ for (let at2 = owner?.parent; at2?.kind === 'inline'; at2 = at2.parent) {
1578
+ const placed = lifts.edgeOf(at2);
1579
+ if (!placed)
1580
+ break;
1581
+ const room = edges.get(placed.edge);
1582
+ if (!room)
1583
+ break;
1584
+ const own = placed.edge.style.verticalAlign === 'top'
1585
+ ? y + room.ascent
1586
+ : y + height - room.descent;
1587
+ let raises = LINE_BOX_RAISES.get(line);
1588
+ if (!raises)
1589
+ LINE_BOX_RAISES.set(line, (raises = new Map()));
1590
+ raises.set(at2, y + baseline - (own - placed.raise));
1591
+ SHIFTED_LINES.add(line);
1592
+ }
1593
+ }
1594
+ }
1595
+ text.drawY = at - natural.baseline;
439
1596
  }
440
1597
  return line;
441
1598
  }
1599
+ /**
1600
+ * Put a line's pieces in visual order. The text and the atomics first, by
1601
+ * UAX #9's L2 over pieces rather than characters: a fragment is in order
1602
+ * inside itself already — the engine laid it out bidirectionally — so what
1603
+ * is left to order is the fragments and the atomics between them, each at
1604
+ * a level — a fragment the direction its letters read in, or the
1605
+ * paragraph's where they read both ways, and a neutral (an atomic, a
1606
+ * fragment of spaces and digits) its neighbours' where they agree and the
1607
+ * paragraph's where they do not (N1, N2).
1608
+ *
1609
+ * Then each element's edges, innermost first, around what of it is on the
1610
+ * line: CSS 2.1 8.6 puts an ltr element's left margin, border and padding
1611
+ * on its leftmost box and a rtl one's right ones on its rightmost, which is
1612
+ * a matter of its `direction` rather than of which way its text reads — so
1613
+ * an edge is placed beside its element's pieces, never ordered among them.
1614
+ * Each piece keeps the room it took, and a right-to-left fragment's hung
1615
+ * trailing space goes to its left, where its logical end is.
1616
+ */
1617
+ function reorderLine(open, rtl) {
1618
+ const order = open.order;
1619
+ const n = order.length;
1620
+ if (!n)
1621
+ return;
1622
+ const content = [];
1623
+ for (let i = 0; i < n; i += 1)
1624
+ if (order[i].kind !== 'edge')
1625
+ content.push(i);
1626
+ const strong = content.map((i) => {
1627
+ const p = order[i];
1628
+ if (p.kind !== 'text' || p.reads === null)
1629
+ return null;
1630
+ return p.reads === 'mixed' ? rtl : p.reads === 'rtl';
1631
+ });
1632
+ const strongAt = (from, step) => {
1633
+ for (let k = from + step; k >= 0 && k < content.length; k += step) {
1634
+ if (strong[k] !== null)
1635
+ return strong[k];
1636
+ }
1637
+ return rtl;
1638
+ };
1639
+ const isRtl = strong.map((r, k) => {
1640
+ if (r !== null)
1641
+ return r;
1642
+ const before = strongAt(k, -1);
1643
+ return before === strongAt(k, 1) ? before : rtl;
1644
+ });
1645
+ // R is level 1 in either paragraph; L is 0 in a left-to-right one and 2
1646
+ // in a right-to-left one
1647
+ const levels = isRtl.map((r) => (r ? 1 : rtl ? 2 : 0));
1648
+ const visual = content.map((_, k) => k);
1649
+ for (let level = Math.max(0, ...levels); level >= 1; level -= 1) {
1650
+ for (let a = 0; a < visual.length;) {
1651
+ if (levels[visual[a]] < level) {
1652
+ a += 1;
1653
+ continue;
1654
+ }
1655
+ let b = a;
1656
+ while (b < visual.length && levels[visual[b]] >= level)
1657
+ b += 1;
1658
+ for (let i = a, j = b - 1; i < j; i += 1, j -= 1) {
1659
+ [visual[i], visual[j]] = [visual[j], visual[i]];
1660
+ }
1661
+ a = b;
1662
+ }
1663
+ }
1664
+ const seq = visual.map((k) => content[k]);
1665
+ const elements = new Map();
1666
+ for (let i = 0; i < n; i += 1) {
1667
+ const p = order[i];
1668
+ if (p.kind !== 'edge')
1669
+ continue;
1670
+ const sides = elements.get(p.item.box) ?? { start: null, end: null };
1671
+ sides[p.item.side] = i;
1672
+ elements.set(p.item.box, sides);
1673
+ }
1674
+ const inside = [...elements].sort(([a], [b]) => depthOf(b) - depthOf(a));
1675
+ for (const [box, { start, end }] of inside) {
1676
+ // what of the element is on this line: everything between its edges,
1677
+ // or the line's end where one of them is on another line
1678
+ const from = start ?? -1;
1679
+ const to = end ?? n;
1680
+ let lo = -1;
1681
+ let hi = -1;
1682
+ for (let k = 0; k < seq.length; k += 1) {
1683
+ if (seq[k] > from && seq[k] < to) {
1684
+ if (lo < 0)
1685
+ lo = k;
1686
+ hi = k;
1687
+ }
1688
+ }
1689
+ if (lo < 0) {
1690
+ // Nothing of it here but its edges: they go where it is in the text,
1691
+ // after what comes before it, in the paragraph's direction.
1692
+ let at = rtl ? seq.length : 0;
1693
+ for (let i = from - 1; i >= 0; i -= 1) {
1694
+ const k = seq.indexOf(i);
1695
+ if (k >= 0) {
1696
+ at = rtl ? k : k + 1;
1697
+ break;
1698
+ }
1699
+ }
1700
+ lo = at;
1701
+ hi = at - 1;
1702
+ }
1703
+ const ltr = box.style.direction !== 'rtl';
1704
+ const left = ltr ? start : end;
1705
+ const right = ltr ? end : start;
1706
+ if (right !== null)
1707
+ seq.splice(hi + 1, 0, right);
1708
+ if (left !== null)
1709
+ seq.splice(lo, 0, left);
1710
+ }
1711
+ let x = order[0].at;
1712
+ for (const i of seq) {
1713
+ const piece = order[i];
1714
+ const room = (i + 1 < n ? order[i + 1].at : open.x) - piece.at;
1715
+ let dx = x - piece.at;
1716
+ if (piece.kind === 'text') {
1717
+ const natural = piece.item.layout.lines[piece.item.layoutLine];
1718
+ const k = content.indexOf(i);
1719
+ if (isRtl[k] && natural)
1720
+ dx += Math.max(0, room - natural.width);
1721
+ piece.item.drawX += dx;
1722
+ }
1723
+ else {
1724
+ piece.item.x += dx;
1725
+ }
1726
+ x += room;
1727
+ }
1728
+ }
1729
+ /** Whether a piece can be out of place in logical order: anything on a
1730
+ * right-to-left line, a fragment that reads right to left, or the edge of
1731
+ * an element whose direction is not the line's. A line of left-to-right
1732
+ * text, the common one, is placed as it was built. */
1733
+ function needsOrdering(piece, rtl) {
1734
+ if (rtl)
1735
+ return true;
1736
+ if (piece.kind === 'text')
1737
+ return piece.reads === 'rtl';
1738
+ return piece.kind === 'edge' && piece.item.box.style.direction === 'rtl';
1739
+ }
1740
+ /** How deep a box is in the tree — an element inside another is deeper. */
1741
+ export function depthOf(box) {
1742
+ let depth = 0;
1743
+ for (let at = box.parent; at; at = at.parent)
1744
+ depth += 1;
1745
+ return depth;
1746
+ }
1747
+ /**
1748
+ * Which way the text from `start` to `end` of a segment's runs reads, from
1749
+ * its letters. Asked of the text rather than of the engine's runs: a run of
1750
+ * the space before a word reads the paragraph's way wherever it starts a
1751
+ * fragment, which says nothing about the word, and an engine may not say
1752
+ * which way its runs went at all.
1753
+ */
1754
+ function readingOf(runs, start, end) {
1755
+ let right = false;
1756
+ let left = false;
1757
+ let at = 0;
1758
+ for (const run of runs) {
1759
+ const length = run.text.length;
1760
+ const from = Math.max(start, at);
1761
+ const to = Math.min(end, at + length);
1762
+ if (to > from) {
1763
+ const text = to - from === length ? run.text : run.text.slice(from - at, to - at);
1764
+ right ||= RIGHT_TO_LEFT.test(text);
1765
+ left ||= LEFT_TO_RIGHT.test(text);
1766
+ }
1767
+ at += length;
1768
+ if (at >= end)
1769
+ break;
1770
+ }
1771
+ return right ? (left ? 'mixed' : 'rtl') : left ? 'ltr' : null;
1772
+ }
1773
+ /** A letter of a script written right to left — Hebrew, Arabic, Syriac,
1774
+ * Thaana, N'Ko and their neighbours, their presentation forms, and the
1775
+ * supplementary planes' right-to-left blocks — and any other letter. */
1776
+ const RIGHT_TO_LEFT = /[\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}]/u;
1777
+ const LEFT_TO_RIGHT = /(?![\u0590-\u08FF\uFB1D-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10FFF}\u{1E800}-\u{1EFFF}])\p{L}/u;
442
1778
  /** Where an atomic's top edge sits, relative to the line box top. */
443
- function alignAtomic(box, line) {
444
- const h = box.height + box.marginTop + box.marginBottom;
1779
+ function alignAtomic(placed, line) {
1780
+ const box = placed.box;
445
1781
  switch (box.style.verticalAlign) {
446
1782
  case 'top':
447
1783
  return 0;
448
1784
  case 'bottom':
449
- return line.height - h;
450
- case 'middle':
451
- return (line.height - h) / 2;
452
- case 'sub':
453
- return line.baseline - h + line.height * 0.1;
454
- case 'super':
455
- return line.baseline - h - line.height * 0.25;
1785
+ return line.height - (box.height + box.marginTop + box.marginBottom);
456
1786
  default:
457
- if (typeof box.style.verticalAlign === 'number') {
458
- return line.baseline - h - box.style.verticalAlign;
1787
+ return line.baseline - atomicBaseline(box) - (placed.raise ?? 0);
1788
+ }
1789
+ }
1790
+ /**
1791
+ * How far an atomic's `vertical-align` raises its baseline above its
1792
+ * parent's (CSS 2.1 10.8.1), as `Lifts` raises an inline box's: `sub` and
1793
+ * `super` by a fifth and a third of the parent's font size and a pixel,
1794
+ * `text-top` and `text-bottom` to the edges of the parent's font, `middle`
1795
+ * by its middle to half the parent's x-height above the baseline, a length
1796
+ * by itself and a percentage of its own line height. `top` and `bottom` are
1797
+ * the line box's edges, and raise nothing from a baseline.
1798
+ *
1799
+ * `middle` is the parent's baseline, not the middle of the line: an image
1800
+ * set `middle` beside text in a line an image before it made tall sat
1801
+ * where that image left room, and drew the text beside it a pixel off.
1802
+ * Read before the line moves the atomic, as `atomicBaseline` is.
1803
+ */
1804
+ function atomicRaise(fonts, box, parent) {
1805
+ const va = box.style.verticalAlign;
1806
+ switch (va) {
1807
+ case 'baseline':
1808
+ case 'top':
1809
+ case 'bottom':
1810
+ return 0;
1811
+ case 'sub':
1812
+ return -(parent.fontSize / 5 + parent.fontSize / 16);
1813
+ case 'super':
1814
+ return parent.fontSize / 3 + parent.fontSize / 16;
1815
+ }
1816
+ const h = box.height + box.marginTop + box.marginBottom;
1817
+ const b = atomicBaseline(box);
1818
+ switch (va) {
1819
+ case 'text-top':
1820
+ return faceExtent(fonts, parent).ascent - b;
1821
+ case 'text-bottom':
1822
+ return h - b - faceExtent(fonts, parent).descent;
1823
+ case 'middle':
1824
+ return xHeightOf(fonts, parent) / 2 + h / 2 - b;
1825
+ default:
1826
+ return typeof va === 'number'
1827
+ ? va
1828
+ : resolve(va, lineHeightOf(fonts, box.style));
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Where an atomic's baseline is, from the top of its margin box (CSS 2.1
1833
+ * 10.8.1, 17.5.3): an inline-block's is its last line box's, an
1834
+ * inline-table's its first row's — its first line box's — and the bottom of
1835
+ * its margin box where it has none, is replaced, or clips what overflows
1836
+ * it. Set bottom-on-baseline, an inline-block's text sat its descent above
1837
+ * the text beside it, and a line holding one grew by as much.
1838
+ *
1839
+ * Read while the atomic is still where it was laid out, before the line
1840
+ * moves it: its lines are then at the same offset from its top as they will
1841
+ * be after.
1842
+ */
1843
+ function atomicBaseline(box) {
1844
+ const bottom = box.height + box.marginTop + box.marginBottom;
1845
+ if (box.kind === 'replaced' ||
1846
+ scrolls(box.style) ||
1847
+ // layout containment keeps its baseline in (CSS Containment 2, 3.3)
1848
+ box.style.contain & CONTAIN_LAYOUT) {
1849
+ return bottom;
1850
+ }
1851
+ const baseline = box.kind === 'table'
1852
+ ? tableBaseline(box)
1853
+ : // an inline flex box sits on its first (CSS Flexbox 8.5), where an
1854
+ // inline block sits on its last line's
1855
+ box.kind === 'flex'
1856
+ ? firstBaselineIn(box)
1857
+ : lastBaselineIn(box);
1858
+ return baseline === null ? bottom : box.marginTop + (baseline - box.y);
1859
+ }
1860
+ /**
1861
+ * A flex container's first baseline, or its last (CSS Flexbox 8.5): that
1862
+ * of the first item on its first line that is aligned by its baseline — the
1863
+ * last on its last line — or where none is, of its first item, or last,
1864
+ * in `order`. An item gives its own, or where it has none, its border
1865
+ * box's bottom edge. Not its last child's: an `inline-flex` of a small
1866
+ * item and a big one sat on the big one's baseline. Null for a box with no
1867
+ * item.
1868
+ */
1869
+ function flexBaseline(box, first) {
1870
+ let items = [];
1871
+ let reordered = false;
1872
+ for (const child of box.children) {
1873
+ if (child.outOfFlow || child.isFloat)
1874
+ continue;
1875
+ if (child.kind === 'text' || child.kind === 'break')
1876
+ continue;
1877
+ if (child.style.order !== 0)
1878
+ reordered = true;
1879
+ items.push(child);
1880
+ }
1881
+ if (!items.length)
1882
+ return null;
1883
+ if (reordered)
1884
+ items.sort((a, b) => a.style.order - b.style.order);
1885
+ if (!first)
1886
+ items = items.reverse();
1887
+ const style = box.style;
1888
+ if (style.flexDirection.startsWith('row') && !style.grid) {
1889
+ // a line ends where the next item does not go on along the main axis,
1890
+ // which from the last item is where it goes on
1891
+ const back = ((style.flexDirection === 'row-reverse') !==
1892
+ (style.direction === 'rtl')) !==
1893
+ !first;
1894
+ let last = NaN;
1895
+ for (const item of items) {
1896
+ if (!Number.isNaN(last) && (back ? item.x >= last : item.x <= last)) {
1897
+ break;
459
1898
  }
460
- return line.baseline - h;
1899
+ last = item.x;
1900
+ const align = item.style.alignSelf === 'auto'
1901
+ ? style.alignItems
1902
+ : item.style.alignSelf;
1903
+ if (align === 'baseline')
1904
+ return itemBaselineOf(item, first, style);
1905
+ }
1906
+ }
1907
+ return itemBaselineOf(items[0], first, style);
1908
+ }
1909
+ /** A flex item's baseline, in document coordinates: its first line's or
1910
+ * its last's — its bottom margin edge where it clips what it holds — or
1911
+ * where it has none, its border box's bottom edge, which a grid does not
1912
+ * take from its item, and has none of its own then (CSS Grid 1, 9). */
1913
+ function itemBaselineOf(item, first, container) {
1914
+ const found = childBaseline(item, first ? firstBaselineIn : lastBaselineIn);
1915
+ if (found !== null)
1916
+ return found;
1917
+ return container.grid ? null : item.y + item.height;
1918
+ }
1919
+ /**
1920
+ * A table's baseline: its first row's (CSS 2.1 17.5.3), which is the first
1921
+ * line of a cell in the row, or where no cell in it has a line, the bottom
1922
+ * content edge of the row's lowest cell. Not the table's bottom: a table
1923
+ * of empty rows stands on its first one, and the rest hang below the line.
1924
+ */
1925
+ function tableBaseline(table) {
1926
+ const { cells } = tableGrid(table);
1927
+ let lowest = -Infinity;
1928
+ for (const cell of cells) {
1929
+ if (cell.row > 0)
1930
+ break;
1931
+ const found = firstBaselineIn(cell.box);
1932
+ if (found !== null)
1933
+ return found;
1934
+ const b = cell.box;
1935
+ lowest = Math.max(lowest, b.y + b.height - b.padBottom - b.borderBottom);
461
1936
  }
1937
+ return lowest === -Infinity ? null : lowest;
1938
+ }
1939
+ /**
1940
+ * The baseline a box's content gives it, in document coordinates: its last
1941
+ * line box's, or its last in-flow child's with one — and a child that clips
1942
+ * what overflows it gives its bottom margin edge and no deeper, whatever
1943
+ * text it holds (CSS Box Alignment's synthesized baseline). Null where
1944
+ * nothing in it has one.
1945
+ */
1946
+ function lastBaselineIn(box) {
1947
+ // a flex box's is its items', in the order it lays them out
1948
+ if (box.kind === 'flex')
1949
+ return flexBaseline(box, false);
1950
+ if (box.lines?.length) {
1951
+ const line = box.lines[box.lines.length - 1];
1952
+ return line.y + line.baseline;
1953
+ }
1954
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1955
+ const found = childBaseline(box.children[i], lastBaselineIn);
1956
+ if (found !== null)
1957
+ return found;
1958
+ }
1959
+ return null;
1960
+ }
1961
+ /** The same, from the first line box or child: a table's baseline is its
1962
+ * first row's, and so is a table cell's. */
1963
+ export function firstBaselineIn(box) {
1964
+ if (box.kind === 'flex')
1965
+ return flexBaseline(box, true);
1966
+ if (box.lines?.length)
1967
+ return box.lines[0].y + box.lines[0].baseline;
1968
+ for (const child of box.children) {
1969
+ const found = childBaseline(child, firstBaselineIn);
1970
+ if (found !== null)
1971
+ return found;
1972
+ }
1973
+ return null;
1974
+ }
1975
+ function childBaseline(child, inside) {
1976
+ if (child.outOfFlow || child.isFloat || child.kind === 'replaced') {
1977
+ return null;
1978
+ }
1979
+ if (scrolls(child.style)) {
1980
+ return child.y + child.height + child.marginBottom;
1981
+ }
1982
+ return inside(child);
1983
+ }
1984
+ /**
1985
+ * The static position of each absolutely positioned box among a
1986
+ * paragraph's content (CSS 2.1 10.3.7, 10.6.4): where it would have been
1987
+ * in flow, found from the content before it. A block-level one would have
1988
+ * broken the line that content is on, so it goes under that line at the
1989
+ * line's start; an inline-level one goes on it, where the pen stood after
1990
+ * that content, or under it where a forced break ends the text; either goes
1991
+ * at the top where nothing comes before it. Taken as the block's content
1992
+ * top for every one of them, a box after a line of text was drawn over that
1993
+ * line, and a menu under its link came up on it. Not from a probe of
1994
+ * intrinsic width, whose room is no place for one.
1995
+ */
1996
+ function staticPositions(statics, items, lines, block, options) {
1997
+ if (!Number.isFinite(options.width))
1998
+ return;
1999
+ for (const { index, box } of statics) {
2000
+ const inline = INLINE_BEFORE_ABSOLUTE.has(box.style);
2001
+ let top = lines.length ? lines[0].y : 0;
2002
+ let x = 0;
2003
+ const after = lines.length ? penAfter(items, index, lines) : null;
2004
+ if (after) {
2005
+ const { line, pen, broken } = after;
2006
+ if (inline && !broken) {
2007
+ top = line.y;
2008
+ x = pen;
2009
+ }
2010
+ else {
2011
+ top = line.y + line.height;
2012
+ }
2013
+ }
2014
+ box.staticPosition = {
2015
+ from: block,
2016
+ x: options.originX + x - block.x,
2017
+ right: options.originX + options.width - block.x,
2018
+ y: options.startY + top - block.y,
2019
+ };
2020
+ }
2021
+ }
2022
+ /** The line the content before `items[index]` ended on, where the pen
2023
+ * stood after it, and whether it ended in a forced break. Null where no
2024
+ * text or atomic comes before. */
2025
+ function penAfter(items, index, lines) {
2026
+ for (let i = index - 1; i >= 0; i -= 1) {
2027
+ const item = items[i];
2028
+ if (item.kind === 'edge') {
2029
+ // an inline box's margin, border or padding is content, which keeps
2030
+ // its line from being one of no height (CSS 2.1 9.4.2), even where
2031
+ // they add up to no width
2032
+ if (!hasEdgeOn(item.box, item.side))
2033
+ continue;
2034
+ for (const line of lines) {
2035
+ const placed = line.edges?.find((e) => e.box === item.box && e.side === item.side);
2036
+ if (placed) {
2037
+ return { line, pen: placed.x + placed.width, broken: false };
2038
+ }
2039
+ }
2040
+ continue;
2041
+ }
2042
+ if (item.kind === 'atomic') {
2043
+ for (const line of lines) {
2044
+ const placed = line.atomics.find((a) => a.box === item.box);
2045
+ if (!placed)
2046
+ continue;
2047
+ const pen = placed.x + item.box.width + item.box.marginRight;
2048
+ return { line, pen, broken: false };
2049
+ }
2050
+ return null;
2051
+ }
2052
+ if (item.kind !== 'text' || item.control)
2053
+ continue;
2054
+ const end = item.start + item.length;
2055
+ const line = lines.find((l) => l.textStart < end && end <= l.textEnd) ??
2056
+ lines[lines.length - 1];
2057
+ const broken = item.run.text.endsWith('\n');
2058
+ return { line, pen: broken ? 0 : caretX(line, end), broken };
2059
+ }
2060
+ return null;
2061
+ }
2062
+ /** Whether an inline box has margin, border or padding on a side. */
2063
+ function hasEdgeOn(box, side) {
2064
+ const left = (side === 'start') !== (box.style.direction === 'rtl');
2065
+ return left
2066
+ ? box.marginLeft !== 0 || box.borderLeft !== 0 || box.padLeft !== 0
2067
+ : box.marginRight !== 0 || box.borderRight !== 0 || box.padRight !== 0;
2068
+ }
2069
+ /** Where the caret at document offset `at` stands on a line, in the
2070
+ * paragraph's coordinates: the line's end where no text of it holds the
2071
+ * offset. */
2072
+ function caretX(line, at) {
2073
+ for (const text of line.texts) {
2074
+ if (at < text.textStart || at > text.textEnd)
2075
+ continue;
2076
+ const offsets = layoutOffsets(text.layout);
2077
+ const units = layoutOffsetOf(text, at, true);
2078
+ const caret = text.layout.caretPosition(codePointAtOffset(offsets, units));
2079
+ return text.drawX + caret.x;
2080
+ }
2081
+ return line.x + line.width;
462
2082
  }
463
2083
  // --- gathering --------------------------------------------------------------
464
- /** Flatten an inline subtree into a stream of runs, atomics and breaks. */
465
- function collect(box, out) {
2084
+ /** Flatten an inline subtree into a stream of runs, atomics, breaks and the
2085
+ * edges of the inline boxes the text sits in. */
2086
+ /** Gather an inline formatting context's content into `out`, and answer
2087
+ * how many floats it gathered — none unless `floats` asks for them. */
2088
+ function collect(box, out, width, fonts, block, floats,
2089
+ /** Where each absolutely positioned box among the content is: before the
2090
+ * item at `index`. No item of the stream, which reads none of them. */
2091
+ statics,
2092
+ /** Whether the text is an inline box's that has a margin, border or
2093
+ * padding at a side, which parts it from the text beside it. */
2094
+ apart = false) {
2095
+ let floated = 0;
466
2096
  for (const child of box.children) {
467
- if (child.outOfFlow || child.isFloat)
2097
+ if (child.outOfFlow) {
2098
+ statics?.push({ index: out.length, box: child });
468
2099
  continue;
2100
+ }
2101
+ if (child.isFloat) {
2102
+ if (floats) {
2103
+ out.push({ kind: 'float', box: child });
2104
+ floated += 1;
2105
+ }
2106
+ continue;
2107
+ }
469
2108
  switch (child.kind) {
470
2109
  case 'text':
471
2110
  if (child.text) {
472
- out.push({
473
- kind: 'text',
474
- run: runFor(child.text, child.style, child.el),
475
- box: child,
476
- length: child.text.length,
477
- });
2111
+ const run = runFor(heldText(child), child.style);
2112
+ // shaping is broken across the edge (CSS Text 3, 7.3), and the
2113
+ // engine shapes a word that runs across spans shaped alike as one:
2114
+ // its text is shaped on its own. At both of its sides, the one
2115
+ // with no edge among them, as the engine takes it
2116
+ if (apart)
2117
+ run.shapeApart = true;
2118
+ if (child.style.wordSpacing) {
2119
+ wordSpaced(out, child, run, child.style.wordSpacing);
2120
+ }
2121
+ else {
2122
+ out.push({
2123
+ kind: 'text',
2124
+ run,
2125
+ box: child,
2126
+ length: child.text.length,
2127
+ start: child.textStart,
2128
+ });
2129
+ }
478
2130
  }
479
2131
  break;
480
2132
  case 'break':
@@ -495,11 +2147,52 @@ function collect(box, out) {
495
2147
  },
496
2148
  box: child,
497
2149
  length: 1,
2150
+ start: child.textStart,
498
2151
  });
499
2152
  break;
500
- case 'inline':
501
- collect(child, out);
2153
+ case 'inline': {
2154
+ const [start, end] = inlineEdges(child, width, fonts);
2155
+ // Both edges or neither, so that an element with any is bounded on
2156
+ // the line: its pieces are what lies between them, which is how a
2157
+ // right-to-left line finds them (`reorderLine`), and how a rounded
2158
+ // background knows the fragments that open and close it, to round
2159
+ // only those corners — so it gets them even at width zero.
2160
+ // And a box that `position: relative` moves or `vertical-align`
2161
+ // raises has them, so that its text is laid out apart from the text
2162
+ // around it, to be drawn where the box goes (`offsetInline`,
2163
+ // `Lifts`)
2164
+ // and a margin a border cancels is still an edge, which keeps its
2165
+ // line from being one of no height (CSS 2.1 9.4.2)
2166
+ const edged = start !== 0 ||
2167
+ end !== 0 ||
2168
+ hasEdgeOn(child, 'start') ||
2169
+ hasEdgeOn(child, 'end') ||
2170
+ (child.decoration !== null &&
2171
+ child.style.borderRadius.some((r) => r !== 0)) ||
2172
+ isOffset(child.style) ||
2173
+ child.style.verticalAlign !== 'baseline' ||
2174
+ (fonts !== null && ownsLeading(fonts, block, child.style));
2175
+ if (edged) {
2176
+ out.push({ kind: 'edge', box: child, side: 'start', width: start });
2177
+ }
2178
+ // `unicode-bidi` as the controls it stands for, inside the box's
2179
+ // edges and around its text, where it has any
2180
+ const controls = child.style.unicodeBidi !== 'normal' &&
2181
+ child.subtreeTextEnd > child.subtreeTextStart
2182
+ ? bidiControls(child.style, false)
2183
+ : null;
2184
+ if (controls) {
2185
+ pushControls(out, controls[0], child, child.subtreeTextStart);
2186
+ }
2187
+ floated += collect(child, out, width, fonts, block, floats, statics, start !== 0 || end !== 0);
2188
+ if (controls) {
2189
+ pushControls(out, controls[1], child, child.subtreeTextEnd);
2190
+ }
2191
+ if (edged) {
2192
+ out.push({ kind: 'edge', box: child, side: 'end', width: end });
2193
+ }
502
2194
  break;
2195
+ }
503
2196
  default:
504
2197
  // Everything else that reached an inline context is inline-level and
505
2198
  // atomic: a replaced element, an `inline-block`, an `inline-table`.
@@ -507,45 +2200,885 @@ function collect(box, out) {
507
2200
  break;
508
2201
  }
509
2202
  }
2203
+ return floated;
2204
+ }
2205
+ /**
2206
+ * What a paragraph does with a word too long for its line: cuts it where
2207
+ * its style or any text in it says a word may be cut — `overflow-wrap`, or
2208
+ * `word-break` or `line-break: anywhere`, which cut one whatever
2209
+ * `overflow-wrap` says (CSS Text 3, 5.2 and 5.3) — and lets it run past the
2210
+ * line's end where nothing does, as a browser does. The engine takes one answer for the paragraph, so a span
2211
+ * that breaks its words breaks the paragraph's.
2212
+ */
2213
+ function overflowWrapOf(style, items) {
2214
+ if (breaksWords(style))
2215
+ return 'break-word';
2216
+ for (const item of items) {
2217
+ if (item.kind === 'text' && breaksWords(item.box.style)) {
2218
+ return 'break-word';
2219
+ }
2220
+ }
2221
+ return 'normal';
2222
+ }
2223
+ /** A `text-overflow` cut on a line that does not wrap puts its ellipsis at
2224
+ * the box's edge, inside a word where one runs past it, as a browser
2225
+ * does — which is the engine cutting the word there. */
2226
+ function cutWrap(style, cut, wrapWords) {
2227
+ return cut && !wraps(style) ? 'break-word' : wrapWords;
2228
+ }
2229
+ function breaksWords(style) {
2230
+ return (style.overflowWrap !== 'normal' ||
2231
+ style.wordBreak === 'break-all' ||
2232
+ style.wordBreak === 'break-word' ||
2233
+ style.lineBreakAnywhere);
2234
+ }
2235
+ /**
2236
+ * Keep the text of a `nowrap` element together where its block wraps (CSS
2237
+ * 2.1 16.6). Its runs share the element as their `nowrap`, which ntk makes
2238
+ * no break inside or between; and, for an engine that does not read it,
2239
+ * a space it may not break after is a no-break space, as wide and as many,
2240
+ * so every offset holds. Whether a line breaks between two characters is
2241
+ * the call of the nearest element holding both (CSS Text 3, 5.1), so the
2242
+ * space such an element ends on stays one to break at where the text after
2243
+ * it is not the same element's: text that wraps, or another `nowrap`
2244
+ * element's — two tags side by side, a collapsed space between them.
2245
+ */
2246
+ function holdNoWrap(items, block) {
2247
+ let last = true;
2248
+ let nextGroup = null;
2249
+ for (let i = items.length - 1; i >= 0; i -= 1) {
2250
+ const item = items[i];
2251
+ if (item.kind !== 'text')
2252
+ continue;
2253
+ const group = wraps(item.box.style) ? null : noWrapGroup(item.box, block);
2254
+ if (group) {
2255
+ const text = item.run.text;
2256
+ let held = text;
2257
+ // `pre`'s are no-break spaces already (`runFor`)
2258
+ if (item.box.style.whiteSpace === 'nowrap' && text.includes(' ')) {
2259
+ held = text.replace(/ /g, '\u00a0');
2260
+ if (text.endsWith(' ') && (last || nextGroup !== group)) {
2261
+ held = held.slice(0, -1) + ' ';
2262
+ }
2263
+ }
2264
+ item.run = { ...item.run, text: held, nowrap: group };
2265
+ }
2266
+ last = false;
2267
+ nextGroup = group;
2268
+ }
2269
+ }
2270
+ /** The element a text that does not wrap is held together by: the outermost
2271
+ * one inside the block that does not wrap either. */
2272
+ function noWrapGroup(box, block) {
2273
+ let group = box;
2274
+ for (let up = box.parent; up && up !== block; up = up.parent) {
2275
+ if (wraps(up.style))
2276
+ break;
2277
+ group = up;
2278
+ }
2279
+ return group;
2280
+ }
2281
+ /** The bidi controls `unicode-bidi` stands for, opening and closing (CSS
2282
+ * Writing Modes 3, 2.4.2), in the direction of the box; null for `normal`.
2283
+ * On a block only an override opens anything: the block's embedding is
2284
+ * its paragraph's direction. */
2285
+ function bidiControls(style, block) {
2286
+ const rtl = style.direction === 'rtl';
2287
+ const override = rtl ? '\u202E' : '\u202D';
2288
+ switch (style.unicodeBidi) {
2289
+ case 'embed':
2290
+ return [rtl ? '\u202B' : '\u202A', '\u202C'];
2291
+ case 'bidi-override':
2292
+ return [override, '\u202C'];
2293
+ case 'isolate':
2294
+ return [rtl ? '\u2067' : '\u2066', '\u2069'];
2295
+ case 'isolate-override':
2296
+ return block
2297
+ ? [override, '\u202C']
2298
+ : [(rtl ? '\u2067' : '\u2066') + override, '\u202C\u2069'];
2299
+ case 'plaintext':
2300
+ return ['\u2068', '\u2069'];
2301
+ default:
2302
+ return null;
2303
+ }
2304
+ }
2305
+ /** Bidi controls as items of their own, one a unit, at a document index
2306
+ * none of them takes up. */
2307
+ function pushControls(out, controls, box, start) {
2308
+ for (const control of controls) {
2309
+ out.push({
2310
+ kind: 'text',
2311
+ run: runFor(control, box.style),
2312
+ box,
2313
+ length: 1,
2314
+ start,
2315
+ control: true,
2316
+ });
2317
+ }
2318
+ }
2319
+ function hasControls(items) {
2320
+ for (const item of items)
2321
+ if (item.kind === 'text' && item.control)
2322
+ return true;
2323
+ return false;
2324
+ }
2325
+ /**
2326
+ * A text box with `word-spacing`, as runs of its words and of the spaces
2327
+ * between them, each space run carrying the extra as letter spacing: the
2328
+ * engines space letters, not words, and a space's letter spacing is exactly
2329
+ * the room CSS adds after a word separator (16.4 — the space and the
2330
+ * no-break space). Only text that asks for it is split.
2331
+ */
2332
+ function wordSpaced(out, box, run, spacing) {
2333
+ const text = box.text;
2334
+ const push = (from, to, extra) => {
2335
+ if (to <= from)
2336
+ return;
2337
+ out.push({
2338
+ kind: 'text',
2339
+ run: {
2340
+ ...run,
2341
+ text: text.slice(from, to),
2342
+ ...(extra ? { letterSpacing: (run.letterSpacing ?? 0) + extra } : null),
2343
+ },
2344
+ box,
2345
+ length: to - from,
2346
+ start: box.textStart + from,
2347
+ });
2348
+ };
2349
+ let from = 0;
2350
+ for (const m of text.matchAll(/[ \u00A0]+/g)) {
2351
+ push(from, m.index, 0);
2352
+ push(m.index, m.index + m[0].length, spacing);
2353
+ from = m.index + m[0].length;
2354
+ }
2355
+ push(from, text.length, 0);
2356
+ }
2357
+ /**
2358
+ * An inline box's margin, border and padding, resolved onto it — percentages
2359
+ * of the containing block's width, as a block's are — and the widths of its
2360
+ * two edges on the line: the start side's before its first fragment, the
2361
+ * end side's after its last. One with a background or a border to paint
2362
+ * also notes its face's ascent and descent, the height CSS paints them over
2363
+ * (10.6.1, the content area), so paint needs no font.
2364
+ */
2365
+ function inlineEdges(box, width, fonts) {
2366
+ const s = box.style;
2367
+ box.borderTop = usedBorder(s.borderTopWidth, s.borderTopStyle);
2368
+ box.borderRight = usedBorder(s.borderRightWidth, s.borderRightStyle);
2369
+ box.borderBottom = usedBorder(s.borderBottomWidth, s.borderBottomStyle);
2370
+ box.borderLeft = usedBorder(s.borderLeftWidth, s.borderLeftStyle);
2371
+ box.padTop = resolve(s.paddingTop, width);
2372
+ box.padRight = resolve(s.paddingRight, width);
2373
+ box.padBottom = resolve(s.paddingBottom, width);
2374
+ box.padLeft = resolve(s.paddingLeft, width);
2375
+ box.marginLeft = resolve(s.marginLeft, width);
2376
+ box.marginRight = resolve(s.marginRight, width);
2377
+ if (box.cut) {
2378
+ // a piece of a box broken around a block has no edge where it was cut
2379
+ const rtl = s.direction === 'rtl';
2380
+ if (box.cut & (rtl ? 2 : 1)) {
2381
+ box.marginLeft = box.borderLeft = box.padLeft = 0;
2382
+ }
2383
+ if (box.cut & (rtl ? 1 : 2)) {
2384
+ box.marginRight = box.borderRight = box.padRight = 0;
2385
+ }
2386
+ }
2387
+ box.decoration = inlineDecoration(fonts, box, s);
2388
+ const left = box.marginLeft + box.borderLeft + box.padLeft;
2389
+ const right = box.padRight + box.borderRight + box.marginRight;
2390
+ return s.direction === 'rtl' ? [right, left] : [left, right];
2391
+ }
2392
+ /**
2393
+ * What an inline box draws around its text, in `s`: the extent of its face
2394
+ * where it has a background, a border or an outline, and null where it
2395
+ * draws none. Its borders are the ones `inlineEdges` just used. Shared with
2396
+ * a pointer move that gives a link a background (`HtmlViewNode`).
2397
+ */
2398
+ export function inlineDecoration(fonts, box, s) {
2399
+ // a background is a colour, a gradient or an image: a span a gradient
2400
+ // underlines, a highlighter's, was painted only where it had a border
2401
+ const decorated = !isTransparent(s.backgroundColor) ||
2402
+ s.backgroundGradient !== null ||
2403
+ s.backgroundImage !== null ||
2404
+ s.backgroundImages !== null ||
2405
+ box.borderTop + box.borderRight + box.borderBottom + box.borderLeft > 0 ||
2406
+ s.outlineStyle !== 'none';
2407
+ return !decorated ? null : fonts ? faceExtent(fonts, s) : NO_EXTENT;
2408
+ }
2409
+ /** A border's width, or none where its style draws none. */
2410
+ function usedBorder(width, style) {
2411
+ return style === 'none' || style === 'hidden' ? 0 : width;
2412
+ }
2413
+ /** A face's ascent and descent at a style's size, kept per font manager
2414
+ * and shared by the boxes set in it: by the style, which the cascade
2415
+ * shares between every element of a kind, and by the face, which styles
2416
+ * that differ in anything else share. */
2417
+ const FACE_EXTENTS = new WeakMap();
2418
+ /** The extent of a decorated box where there are no fonts to ask. */
2419
+ const NO_EXTENT = { ascent: 0, descent: 0 };
2420
+ /** A style's face's ascent and descent, which an inline box's background
2421
+ * is painted over. */
2422
+ export function faceExtentOf(fonts, style) {
2423
+ return faceExtent(fonts, style);
2424
+ }
2425
+ function faceExtent(fonts, style) {
2426
+ let cache = FACE_EXTENTS.get(fonts);
2427
+ if (!cache) {
2428
+ cache = { byStyle: new WeakMap(), byFace: new Map() };
2429
+ FACE_EXTENTS.set(fonts, cache);
2430
+ }
2431
+ let extent = cache.byStyle.get(style);
2432
+ if (extent)
2433
+ return extent;
2434
+ const key = `${style.fontFamily}|${style.fontSize}|${style.fontWeight}|${style.fontStyle}`;
2435
+ extent = cache.byFace.get(key);
2436
+ if (!extent) {
2437
+ try {
2438
+ const m = fonts
2439
+ .match(style.fontFamily, {
2440
+ size: style.fontSize,
2441
+ weight: style.fontWeight,
2442
+ style: style.fontStyle,
2443
+ })
2444
+ .metrics(style.fontSize);
2445
+ extent = { ascent: m.ascent, descent: m.descent };
2446
+ }
2447
+ catch {
2448
+ extent = { ascent: style.fontSize * 0.8, descent: style.fontSize * 0.2 };
2449
+ }
2450
+ if (cache.byFace.size > 64)
2451
+ cache.byFace.clear();
2452
+ cache.byFace.set(key, extent);
2453
+ }
2454
+ cache.byStyle.set(style, extent);
2455
+ return extent;
2456
+ }
2457
+ /**
2458
+ * What a first line is made with where its `::first-line` sets its fonts
2459
+ * (CSS 2.1 5.12.1). Its text is in the styles its boxes have on it — each
2460
+ * element's own rules over what it inherits there, which takes the fonts
2461
+ * from `::first-line` (CSS Pseudo 4, 2.1.2), so a `<small>` is smaller
2462
+ * than the line's font and not than the block's — and it is laid out at
2463
+ * the pseudo-element's line height. The pseudo-element is an inline box
2464
+ * around the line's content, so its face at its line height is on the line
2465
+ * with the block's strut, which stays: a `::first-line` of a smaller line
2466
+ * height leaves the line as tall as the block's.
2467
+ */
2468
+ function firstLineSetting(block, items, how, fonts, strut, justify) {
2469
+ const style = how.style;
2470
+ const of = (box) => box === block || !box.parent
2471
+ ? style
2472
+ : styleOnFirstLine(box, of(box.parent), how.styler);
2473
+ // and in its `text-transform` where the line has one of its own, which
2474
+ // the boxes' text was not made with: a word carried from one run into
2475
+ // the next, as the box builder carries it (`letteredAfter`)
2476
+ let lettered = false;
2477
+ const restyled = items.map((item) => {
2478
+ if (item.kind === 'atomic')
2479
+ lettered = false;
2480
+ if (item.kind !== 'text')
2481
+ return item;
2482
+ const own = item.box.style;
2483
+ const on = of(item.box);
2484
+ let run = restyledRun(item.run, own, on);
2485
+ const transform = on.textTransform;
2486
+ if (!item.control &&
2487
+ transform !== own.textTransform &&
2488
+ transform !== 'none') {
2489
+ const text = transformInPlace(run.text, transform, lettered);
2490
+ if (text !== run.text)
2491
+ run = { ...run, text };
2492
+ }
2493
+ lettered = letteredAfter(run.text, lettered);
2494
+ return run === item.run ? item : { ...item, run };
2495
+ });
2496
+ const lineHeight = lineHeightMultiplier(fonts, style);
2497
+ const base = {
2498
+ family: style.fontFamily,
2499
+ size: style.fontSize,
2500
+ weight: style.fontWeight,
2501
+ style: style.fontStyle,
2502
+ color: style.color,
2503
+ };
2504
+ const own = strutOf(fonts, style);
2505
+ return {
2506
+ items: restyled,
2507
+ base,
2508
+ lineHeight,
2509
+ strut: strut && {
2510
+ ascent: Math.max(strut.ascent, own.ascent),
2511
+ descent: Math.max(strut.descent, own.descent),
2512
+ },
2513
+ lifts: new Lifts(fonts, style, of),
2514
+ justify: justify && {
2515
+ ...justify,
2516
+ base,
2517
+ options: { ...justify.options, lineHeight },
2518
+ },
2519
+ };
2520
+ }
2521
+ /**
2522
+ * Each box's style on a first line (`FirstLineStyler`), kept for the style
2523
+ * its parent has there: every pass asks again, and a style that is a new
2524
+ * object each time is a new face to every cache that keys on one.
2525
+ */
2526
+ const ON_FIRST_LINE = new WeakMap();
2527
+ /** A box's style on a first line, where its parent's there is `parent`. */
2528
+ function styleOnFirstLine(box, parent, styler) {
2529
+ const known = ON_FIRST_LINE.get(box);
2530
+ if (known && known.parent === parent)
2531
+ return known.style;
2532
+ const style = styler(box, parent);
2533
+ ON_FIRST_LINE.set(box, { parent, style });
2534
+ return style;
2535
+ }
2536
+ /** What a run takes from its style (`runFor`). */
2537
+ const RUN_STYLE = [
2538
+ 'family',
2539
+ 'size',
2540
+ 'weight',
2541
+ 'style',
2542
+ 'color',
2543
+ 'letterSpacing',
2544
+ 'features',
2545
+ 'underline',
2546
+ 'underlineStyle',
2547
+ 'underlineOffset',
2548
+ 'underlineThickness',
2549
+ 'strike',
2550
+ ];
2551
+ /**
2552
+ * A run of text in `style` rather than `own`, the style it was made in:
2553
+ * what it has of neither — the room a tab or `word-spacing` adds to its
2554
+ * letter spacing, a `nowrap` group, where its shaping is broken — kept.
2555
+ */
2556
+ function restyledRun(run, own, style) {
2557
+ if (style === own)
2558
+ return run;
2559
+ const fresh = runFor(run.text, style);
2560
+ const out = { ...run };
2561
+ for (const name of RUN_STYLE) {
2562
+ if (fresh[name] === undefined)
2563
+ delete out[name];
2564
+ else
2565
+ out[name] = fresh[name];
2566
+ }
2567
+ const added = (run.letterSpacing ?? 0) - (own.letterSpacing || 0);
2568
+ if (added)
2569
+ out.letterSpacing = (out.letterSpacing ?? 0) + added;
2570
+ return out;
2571
+ }
2572
+ /**
2573
+ * The first line's text in its `::first-line` colour: the runs before the
2574
+ * line's end whose colour is the block's own, the one that ends past it cut
2575
+ * there. A run in a colour of its own — a link's — keeps it, as an element
2576
+ * inside the pseudo-element does. Colour moves no glyph, so the lines break
2577
+ * where they did.
2578
+ */
2579
+ function firstLineColour(items, firstLine) {
2580
+ for (let i = 0; i < items.length; i += 1) {
2581
+ const item = items[i];
2582
+ if (item.kind !== 'text')
2583
+ continue;
2584
+ // past the line: a run in a colour of its own may have been the one
2585
+ // across its end, so this is where the walk stops, not at the cut
2586
+ if (item.start >= firstLine.end)
2587
+ break;
2588
+ if (item.run.color !== firstLine.from)
2589
+ continue;
2590
+ const cut = firstLine.end - item.start;
2591
+ if (cut < item.length) {
2592
+ items.splice(i + 1, 0, {
2593
+ ...item,
2594
+ run: { ...item.run, text: item.run.text.slice(cut) },
2595
+ start: item.start + cut,
2596
+ length: item.length - cut,
2597
+ });
2598
+ items[i] = {
2599
+ ...item,
2600
+ run: {
2601
+ ...item.run,
2602
+ text: item.run.text.slice(0, cut),
2603
+ color: firstLine.color,
2604
+ },
2605
+ length: cut,
2606
+ };
2607
+ break;
2608
+ }
2609
+ items[i] = { ...item, run: { ...item.run, color: firstLine.color } };
2610
+ }
2611
+ }
2612
+ /** The `TextRun` one styled piece of text becomes: its text, and what paint
2613
+ * needs of its style — nothing that names a box or an element, so that a
2614
+ * layout made from it can be kept for the next parse (`TextLayoutCache`). */
2615
+ /**
2616
+ * A text box's text as it is laid out. `pre` keeps its spaces whole: none
2617
+ * is broken after, and none hangs at a line's end, where a trailing space
2618
+ * counts in the line's width. Laid out, they are no-break spaces, as wide
2619
+ * and as many, so every offset holds (CSS Text 3, 4.1.3). Kept per box: a
2620
+ * pass makes no new string, and the kept layouts find their text by
2621
+ * identity before they compare it.
2622
+ */
2623
+ /**
2624
+ * Tabs a `white-space` keeps, set at their stops (CSS Text 3, 4.2): a stop
2625
+ * every `tab-size` spaces of the block's font from the line's start, and a
2626
+ * tab that would reach one less than half a `ch` on goes to the next.
2627
+ * Neither engine has them — ntk draws a tab a space wide, and CoreText
2628
+ * sets it at stops of its own every 28 points — so a tab is laid out as a
2629
+ * space spaced out to its stop: text as the engine sees it and not as the
2630
+ * document holds it, the way `heldText` holds a `pre`'s spaces. Where each
2631
+ * tab starts is read off one layout of the paragraph with its tabs as
2632
+ * spaces, and its inline boxes' edges and its atomics as the room they
2633
+ * take, unwrapped: a line `pre-wrap` wraps sets the tabs after the wrap as
2634
+ * though it had not, and a right-to-left one sets them a space wide.
2635
+ *
2636
+ * Read off each tab's run, which the tab is given of its own, and not off
2637
+ * a caret: CoreText puts a caret after a spaced glyph part of the way
2638
+ * into its spacing, so a tab after `word-spacing` was set from a place
2639
+ * half the spacing short of where it is drawn.
2640
+ */
2641
+ function setTabs(items, fonts, block, indent) {
2642
+ // the paragraph as it will be laid out, an inline box's edges and an
2643
+ // atomic as the room they take, each tab a run of its own, and where
2644
+ // each text item starts in it
2645
+ const runs = [];
2646
+ const starts = new Map();
2647
+ const tabs = new Set();
2648
+ let units = 0;
2649
+ for (const item of items) {
2650
+ // a float is beside the line, and takes none of it
2651
+ if (item.kind === 'float')
2652
+ continue;
2653
+ if (item.kind !== 'text') {
2654
+ const box = item.box;
2655
+ runs.push(spacerRun(fonts, block, item.kind === 'edge'
2656
+ ? item.width
2657
+ : box.width + box.marginLeft + box.marginRight));
2658
+ units += 1;
2659
+ continue;
2660
+ }
2661
+ starts.set(item, units);
2662
+ const text = item.run.text;
2663
+ if (item.control || !text.includes('\t')) {
2664
+ runs.push(item.run);
2665
+ units += text.length;
2666
+ continue;
2667
+ }
2668
+ let done = 0;
2669
+ for (let p = text.indexOf('\t'); p >= 0; p = text.indexOf('\t', p + 1)) {
2670
+ if (p > done)
2671
+ runs.push({ ...item.run, text: text.slice(done, p) });
2672
+ // spaced a hair apart from its neighbours, which an engine that
2673
+ // merges runs alike (CoreText) would otherwise take it into
2674
+ tabs.add(units + p);
2675
+ runs.push({
2676
+ ...item.run,
2677
+ text: spaceIn(item.box),
2678
+ letterSpacing: (item.run.letterSpacing ?? 0) + tabs.size * HAIR,
2679
+ });
2680
+ done = p + 1;
2681
+ }
2682
+ if (done < text.length)
2683
+ runs.push({ ...item.run, text: text.slice(done) });
2684
+ units += text.length;
2685
+ }
2686
+ const natural = fonts.layout(runs, fontOf(block), {
2687
+ direction: block.direction,
2688
+ });
2689
+ /** Where each tab starts, by its place in the layout's text. */
2690
+ const found = new Map();
2691
+ natural.lines.forEach((line, i) => {
2692
+ for (const run of line.runs ?? []) {
2693
+ if (tabs.has(run.start))
2694
+ found.set(run.start, { x: line.x + run.x, line: i });
2695
+ }
2696
+ });
2697
+ let offsets;
2698
+ const space = advanceOf(fonts, fontOf(block), ' ') +
2699
+ block.letterSpacing +
2700
+ block.wordSpacing;
2701
+ const every = block.tabSizeIsLength ? block.tabSize : block.tabSize * space;
2702
+ const half = advanceOf(fonts, fontOf(block), '0') / 2;
2703
+ const rtl = block.direction === 'rtl';
2704
+ /** The room tabs have added to each line so far. */
2705
+ const added = new Map();
2706
+ const next = [];
2707
+ for (const item of items) {
2708
+ if (item.kind !== 'text' || item.control || !item.run.text.includes('\t')) {
2709
+ next.push(item);
2710
+ continue;
2711
+ }
2712
+ const text = item.run.text;
2713
+ const at = starts.get(item);
2714
+ // what the space a tab is laid out as takes before it is spaced out
2715
+ const own = advanceOf(fonts, item.run, spaceIn(item.box)) +
2716
+ (item.run.letterSpacing ?? 0);
2717
+ const piece = (start, end, run) => {
2718
+ next.push({
2719
+ ...item,
2720
+ run,
2721
+ length: end - start,
2722
+ start: item.start + start,
2723
+ });
2724
+ };
2725
+ let done = 0;
2726
+ for (let p = text.indexOf('\t'); p >= 0; p = text.indexOf('\t', p + 1)) {
2727
+ if (p > done)
2728
+ piece(done, p, { ...item.run, text: text.slice(done, p) });
2729
+ let advance = own;
2730
+ if (!rtl) {
2731
+ const unit = at + p;
2732
+ let caret = found.get(unit);
2733
+ if (!caret) {
2734
+ // an engine that says nothing of its runs has a caret to go by
2735
+ if (offsets === undefined) {
2736
+ const joined = runs.map((run) => run.text).join('');
2737
+ offsets = /[\uD800-\uDFFF]/.test(joined)
2738
+ ? codeUnitOffsets(joined)
2739
+ : null;
2740
+ }
2741
+ caret = natural.caretPosition(offsets ? codePointAt(offsets, unit) : unit);
2742
+ }
2743
+ const before = added.get(caret.line) ?? 0;
2744
+ const x = caret.x + before + (caret.line === 0 ? indent : 0);
2745
+ if (every > 0) {
2746
+ let stop = (Math.floor(x / every) + 1) * every;
2747
+ if (stop - x < half)
2748
+ stop += every;
2749
+ advance = stop - x;
2750
+ }
2751
+ else
2752
+ advance = 0;
2753
+ added.set(caret.line, before + advance - own);
2754
+ }
2755
+ piece(p, p + 1, {
2756
+ ...item.run,
2757
+ text: spaceIn(item.box),
2758
+ letterSpacing: (item.run.letterSpacing ?? 0) + advance - own,
2759
+ });
2760
+ done = p + 1;
2761
+ }
2762
+ if (done < text.length) {
2763
+ piece(done, text.length, { ...item.run, text: text.slice(done) });
2764
+ }
2765
+ }
2766
+ items.splice(0, items.length, ...next);
2767
+ }
2768
+ /** The space a tab in a box is laid out as: a `pre`'s spaces are held
2769
+ * together (`heldText`), and so are its tabs. */
2770
+ function spaceIn(box) {
2771
+ return box.style.whiteSpace === 'pre' ? '\u00a0' : ' ';
2772
+ }
2773
+ /** A block's font, as its runs name it (`runFor`). */
2774
+ function fontOf(style) {
2775
+ return {
2776
+ family: style.fontFamily,
2777
+ size: style.fontSize,
2778
+ weight: style.fontWeight,
2779
+ style: style.fontStyle === 'normal' ? 'normal' : 'italic',
2780
+ };
2781
+ }
2782
+ /** One character's advance in a font, kept per font manager. */
2783
+ function advanceOf(fonts, font, char) {
2784
+ let kept = ADVANCES.get(fonts);
2785
+ if (!kept)
2786
+ ADVANCES.set(fonts, (kept = new Map()));
2787
+ const key = `${font.family}|${font.size}|${font.weight}|${font.style}|${char}`;
2788
+ let advance = kept.get(key);
2789
+ if (advance === undefined) {
2790
+ const face = {
2791
+ family: font.family,
2792
+ size: font.size,
2793
+ weight: font.weight,
2794
+ style: font.style,
2795
+ };
2796
+ // between two letters, so that no engine drops it as a line's end
2797
+ advance =
2798
+ fonts.layout([{ ...face, text: `x${char}x` }], face, {}).width -
2799
+ fonts.layout([{ ...face, text: 'xx' }], face, {}).width;
2800
+ kept.set(key, advance);
2801
+ }
2802
+ return advance;
2803
+ }
2804
+ const ADVANCES = new WeakMap();
2805
+ function heldText(box) {
2806
+ if (box.style.whiteSpace !== 'pre' || !box.text.includes(' ')) {
2807
+ return box.text;
2808
+ }
2809
+ let held = HELD.get(box);
2810
+ if (held === undefined) {
2811
+ held = box.text.replace(/ /g, '\u00a0');
2812
+ HELD.set(box, held);
2813
+ }
2814
+ return held;
2815
+ }
2816
+ const HELD = new WeakMap();
2817
+ /**
2818
+ * Whether a line one layout made is shorter about its baseline than the
2819
+ * block's strut. Every line box starts with the strut, the block's font at
2820
+ * its line height (CSS 2.1 10.8.1), and a layout has none: it makes a line
2821
+ * as tall as the text on it, at the block's line height as a multiple of
2822
+ * each face's natural one. So a line of `<small>` text alone came out as
2823
+ * short as the small text, and overlapped what followed. A paragraph with
2824
+ * such a line is made a line at a time instead, where `finishLine` puts
2825
+ * the strut on every line; one whose lines all hold text of the block's
2826
+ * own face is kept as it was laid out.
2827
+ */
2828
+ function shortOfStrut(lines, strut,
2829
+ /** The first line's, where it holds more: a marker's room. */
2830
+ first = null) {
2831
+ for (let i = 0; i < lines.length; i += 1) {
2832
+ const line = lines[i];
2833
+ const holds = i === 0 && first ? first : strut;
2834
+ if (!holds)
2835
+ continue;
2836
+ if (line.baseline < holds.ascent - 0.5 ||
2837
+ line.height - line.baseline < holds.descent - 0.5) {
2838
+ return true;
2839
+ }
2840
+ }
2841
+ return false;
2842
+ }
2843
+ /** A strut with room besides it about the same baseline, or the strut
2844
+ * where there is none. */
2845
+ function withRoom(strut, room) {
2846
+ if (!room)
2847
+ return strut;
2848
+ if (!strut)
2849
+ return room;
2850
+ return {
2851
+ ascent: Math.max(strut.ascent, room.ascent),
2852
+ descent: Math.max(strut.descent, room.descent),
2853
+ };
510
2854
  }
511
- /** The `TextRun` one styled piece of text becomes. */
512
- function runFor(text, style, owner) {
513
- // ntk hands unknown span fields back on the laid-out runs untouched, which
514
- // is how the element reaches the paint and hit-test passes without a
515
- // parallel structure to keep in step.
2855
+ /** The share of the size a synthesized small capital is set at, rounded to
2856
+ * a whole pixel as Blink rounds it (`SimpleFontData`'s scaled font data):
2857
+ * CSS Fonts 4 (6.2) leaves the size to the user agent. */
2858
+ const SMALL_CAPS_SCALE = 0.7;
2859
+ /**
2860
+ * Small capitals a face does not have, made of its capitals set smaller
2861
+ * (CSS Fonts 4, 6.2): where `font-variant-caps` asks for `smcp` — or
2862
+ * `c2sc` too, for `all-small-caps` — and the face does not answer it,
2863
+ * each letter that would be a small capital is its capital at 70% of the
2864
+ * size, in a run of its own: a lower-case letter, and for `all-small-caps`
2865
+ * the rest of the text too. Under `small-caps` what has no case — a space,
2866
+ * a digit, a mark of punctuation — keeps its size, as Blink keeps it. Only
2867
+ * a letter whose capital is one character is made one, so every offset
2868
+ * holds.
2869
+ */
2870
+ function synthesizeSmallCaps(items, fonts) {
2871
+ for (let i = 0; i < items.length; i++) {
2872
+ const item = items[i];
2873
+ if (item.kind !== 'text' || item.control)
2874
+ continue;
2875
+ // `font-variant-caps`'s, which is synthesized where the face has none;
2876
+ // not `font-feature-settings`', which asks for the feature as it is
2877
+ const caps = item.box.style.fontVariantCaps;
2878
+ const features = item.run.features;
2879
+ if (!caps.includes('smcp') || !features?.smcp)
2880
+ continue;
2881
+ const all = caps.includes('c2sc');
2882
+ if (hasSmallCaps(fonts, item.run, all))
2883
+ continue;
2884
+ const pieces = smallCapsPieces(item, all, withoutCaps(features));
2885
+ if (!pieces)
2886
+ continue;
2887
+ items.splice(i, 1, ...pieces);
2888
+ i += pieces.length - 1;
2889
+ }
2890
+ }
2891
+ /** A text item cut where its letters turn to synthesized small capitals
2892
+ * and back, or null where none does. */
2893
+ function smallCapsPieces(item, all, features) {
2894
+ const text = item.run.text;
2895
+ const pieces = [];
2896
+ let from = 0;
2897
+ let small = false;
2898
+ let capitals = '';
2899
+ const cut = (to) => {
2900
+ if (to === from)
2901
+ return;
2902
+ const run = small
2903
+ ? {
2904
+ ...item.run,
2905
+ text: capitals,
2906
+ size: Math.round((item.run.size ?? 16) * SMALL_CAPS_SCALE),
2907
+ }
2908
+ : { ...item.run, text: text.slice(from, to) };
2909
+ if (small) {
2910
+ if (features)
2911
+ run.features = features;
2912
+ else
2913
+ delete run.features;
2914
+ }
2915
+ pieces.push({ ...item, run, start: item.start + from, length: to - from });
2916
+ from = to;
2917
+ capitals = '';
2918
+ };
2919
+ let any = false;
2920
+ for (let at = 0; at < text.length;) {
2921
+ const ch = String.fromCodePoint(text.codePointAt(at));
2922
+ const upper = ch.toUpperCase();
2923
+ // everything, for `all-small-caps`: its spaces are small too
2924
+ const becomes = (all || upper !== ch) && upper.length === ch.length;
2925
+ if (becomes !== small) {
2926
+ cut(at);
2927
+ small = becomes;
2928
+ }
2929
+ if (becomes) {
2930
+ capitals += upper;
2931
+ any = true;
2932
+ }
2933
+ at += ch.length;
2934
+ }
2935
+ cut(text.length);
2936
+ return any ? pieces : null;
2937
+ }
2938
+ /** Features without the small capitals a synthesis stands in for, one
2939
+ * object for each set, as `featuresOf` keeps them. */
2940
+ function withoutCaps(features) {
2941
+ let kept = WITHOUT_CAPS.get(features);
2942
+ if (kept === undefined) {
2943
+ const out = {};
2944
+ for (const [tag, value] of Object.entries(features)) {
2945
+ if (tag !== 'smcp' && tag !== 'c2sc')
2946
+ out[tag] = value;
2947
+ }
2948
+ kept = Object.keys(out).length ? out : null;
2949
+ WITHOUT_CAPS.set(features, kept);
2950
+ }
2951
+ return kept;
2952
+ }
2953
+ const WITHOUT_CAPS = new WeakMap();
2954
+ /**
2955
+ * Whether a run's face has small capitals of its own — and capitals to
2956
+ * small capitals, for `all-small-caps`: the alphabet set with the feature
2957
+ * and without it comes out another width where it does. Asked of the
2958
+ * engine's layout rather than of the font's tables, which a CoreText face
2959
+ * does not hand over. Kept per face.
2960
+ */
2961
+ function hasSmallCaps(fonts, run, all) {
2962
+ let known = SMALL_CAPS.get(fonts);
2963
+ if (!known)
2964
+ SMALL_CAPS.set(fonts, (known = new Map()));
2965
+ const key = `${run.family}|${run.weight}|${run.style}|${all}`;
2966
+ let has = known.get(key);
2967
+ if (has === undefined) {
2968
+ const face = {
2969
+ family: run.family,
2970
+ weight: run.weight,
2971
+ style: run.style,
2972
+ size: 16,
2973
+ };
2974
+ const width = (text, features) => fonts.layout([{ text, ...face, ...(features ? { features } : null) }], face, {}).width;
2975
+ const lower = 'abcdefghijklmnopqrstuvwxyz';
2976
+ has = Math.abs(width(lower, { smcp: 1 }) - width(lower, null)) > 0.01;
2977
+ if (has && all) {
2978
+ const upper = lower.toUpperCase();
2979
+ has = Math.abs(width(upper, { c2sc: 1 }) - width(upper, null)) > 0.01;
2980
+ }
2981
+ known.set(key, has);
2982
+ }
2983
+ return has;
2984
+ }
2985
+ const SMALL_CAPS = new WeakMap();
2986
+ /**
2987
+ * The OpenType features a style's text is shaped with: the `font-variant`
2988
+ * longhands' and `font-kerning`'s, then `font-feature-settings`, which has
2989
+ * the last word (CSS Fonts 3, 7.2). One object for each set of them, kept,
2990
+ * because a run's fields are compared by identity — a new object a pass
2991
+ * would find no layout the last pass made.
2992
+ */
2993
+ export function featuresOf(style) {
2994
+ const { fontKerning: kerning, fontVariantLigatures: ligatures, fontVariantNumeric: numeric, fontVariantCaps: caps, fontVariantPosition: position, fontFeatureSettings: settings, } = style;
2995
+ if (!kerning && !ligatures && !numeric && !caps && !position && !settings) {
2996
+ return null;
2997
+ }
2998
+ const key = `${kerning}|${ligatures}|${numeric}|${caps}|${position}|${settings}`;
2999
+ let features = FEATURES.get(key);
3000
+ if (!features) {
3001
+ const out = {};
3002
+ for (const part of [
3003
+ kerning,
3004
+ ligatures,
3005
+ numeric,
3006
+ caps,
3007
+ position,
3008
+ settings,
3009
+ ]) {
3010
+ if (!part)
3011
+ continue;
3012
+ for (const pair of part.split(',')) {
3013
+ const at = pair.indexOf('=');
3014
+ out[pair.slice(0, at)] = Number(pair.slice(at + 1));
3015
+ }
3016
+ }
3017
+ features = Object.freeze(out);
3018
+ FEATURES.set(key, features);
3019
+ }
3020
+ return features;
3021
+ }
3022
+ const FEATURES = new Map();
3023
+ /** A run of `text` in `style`: its face, its ink and its rules. Every
3024
+ * paint field of a run of the document's text is this function's, which
3025
+ * is what lets a pointer move re-ink a paragraph without laying it out
3026
+ * again (`HtmlViewNode._hoverInPlace`). */
3027
+ export function runFor(text, style) {
516
3028
  const run = {
517
3029
  text,
518
3030
  family: style.fontFamily,
519
3031
  size: style.fontSize,
520
3032
  weight: style.fontWeight,
521
3033
  style: style.fontStyle === 'normal' ? 'normal' : 'italic',
522
- color: style.color,
3034
+ // the glyphs' fill, which `-webkit-text-fill-color` sets apart from
3035
+ // `color` — the decorations keep `color`
3036
+ color: style.textFillColor === null
3037
+ ? style.color
3038
+ : inkColor(style.textFillColor, style.color),
3039
+ // set here, and made true later for an inline box's text (`collect`):
3040
+ // added to a run after it was made, it gave the runs of a document two
3041
+ // shapes where the layout cache compares them, and a pass over 600 KB
3042
+ // took 3 ms longer finding every paragraph's layout again
3043
+ shapeApart: false,
523
3044
  };
524
- // An inline background fills the line box rather than hugging the ink:
525
- // `<span style="background: yellow">` is a highlighter, and two adjacent
526
- // highlighted spans must not leave a seam between them. That is exactly
527
- // what richtext's `bgFill: 'line'` was added for.
528
- if (!isTransparent(style.backgroundColor)) {
529
- run.bg = inkColor(style.backgroundColor, style.color);
530
- run.bgFill = 'line';
531
- }
532
- if (owner)
533
- run.element = owner;
534
- if (style.textDecorationLine === 'underline') {
535
- run.underline = inkColor(style.textDecorationColor ?? 'currentColor', style.color);
3045
+ if (style.letterSpacing)
3046
+ run.letterSpacing = style.letterSpacing;
3047
+ const features = featuresOf(style);
3048
+ if (features)
3049
+ run.features = features;
3050
+ // Hidden text keeps its place on the line and draws nothing, rules
3051
+ // included, and the text of a visible element inside it is its own run
3052
+ // (CSS 2.1 11.2): drawn in no ink, it is laid out as it was
3053
+ if (style.visibility !== 'visible') {
3054
+ run.color = 'transparent';
3055
+ return run;
3056
+ }
3057
+ // A background is not the run's: the inline box it belongs to paints it,
3058
+ // behind every fragment of the box — nested elements' text included — and
3059
+ // out to its padding (`paintInlineBoxes`).
3060
+ // the element's own decorations and the ones propagated to it, in the
3061
+ // colours of the elements that set them (`decorate`)
3062
+ if (style.underline) {
3063
+ run.underline = style.underline;
536
3064
  // CSS's five rule styles and SGR 4's five are the same set under two
537
3065
  // names; richtext speaks SGR's, so `solid` is `single` and `wavy` is
538
3066
  // `curly`. The other three are spelled identically.
539
3067
  run.underlineStyle =
540
- style.textDecorationStyle === 'wavy'
3068
+ style.underlineStyle === 'wavy'
541
3069
  ? 'curly'
542
- : style.textDecorationStyle === 'solid'
3070
+ : style.underlineStyle === 'solid'
543
3071
  ? 'single'
544
- : style.textDecorationStyle;
545
- }
546
- else if (style.textDecorationLine === 'line-through') {
547
- run.strike = inkColor(style.textDecorationColor ?? 'currentColor', style.color);
3072
+ : style.underlineStyle;
3073
+ if (style.underlineOffset !== null) {
3074
+ run.underlineOffset = style.underlineOffset;
3075
+ }
3076
+ if (style.underlineThickness !== null) {
3077
+ run.underlineThickness = style.underlineThickness;
3078
+ }
548
3079
  }
3080
+ if (style.lineThrough)
3081
+ run.strike = style.lineThrough;
549
3082
  return run;
550
3083
  }
551
3084
  /**
@@ -556,16 +3089,46 @@ function runFor(text, style, owner) {
556
3089
  class SpanMap {
557
3090
  _laid = [];
558
3091
  _doc = [];
3092
+ _boxes = [];
3093
+ /** Where the laid-out text has a unit that is no text of the document's:
3094
+ * a spacer, a bidi control. */
3095
+ _gaps = null;
559
3096
  laidOut = 0;
560
- add(documentStart, length) {
3097
+ add(documentStart, length, box = null) {
561
3098
  this._laid.push(this.laidOut);
562
3099
  this._doc.push(documentStart);
3100
+ this._boxes.push(box);
3101
+ if (!box) {
3102
+ this._gaps ??= [];
3103
+ for (let i = 0; i < length; i += 1)
3104
+ this._gaps.push(this.laidOut + i);
3105
+ }
563
3106
  this.laidOut += length;
564
3107
  }
3108
+ /** Give a line's text the gaps in its part of the laid-out text, where
3109
+ * it has any. Set after the text is made rather than spread into it: a
3110
+ * spread in an object literal costs every line that has none. */
3111
+ giveGaps(text, start, end) {
3112
+ if (!this._gaps)
3113
+ return;
3114
+ const gaps = this._gaps.filter((at) => at >= start && at < end);
3115
+ if (gaps.length)
3116
+ text.gaps = gaps;
3117
+ }
3118
+ /** The text box whose text an offset in the laid-out text is. */
3119
+ boxAt(offset) {
3120
+ const i = this._entryAt(offset);
3121
+ return i < 0 ? null : this._boxes[i];
3122
+ }
565
3123
  documentAt(offset) {
3124
+ const i = this._entryAt(offset);
3125
+ return i < 0 ? 0 : this._doc[i] + (offset - this._laid[i]);
3126
+ }
3127
+ /** The entry an offset falls in, or -1 when there are none. */
3128
+ _entryAt(offset) {
566
3129
  const laid = this._laid;
567
3130
  if (!laid.length)
568
- return 0;
3131
+ return -1;
569
3132
  let lo = 0;
570
3133
  let hi = laid.length - 1;
571
3134
  while (lo < hi) {
@@ -575,7 +3138,7 @@ class SpanMap {
575
3138
  else
576
3139
  hi = mid - 1;
577
3140
  }
578
- return this._doc[lo] + (offset - laid[lo]);
3141
+ return lo;
579
3142
  }
580
3143
  }
581
3144
  /** The maximal run of text items from `(index, offset)` to the next atomic. */
@@ -590,13 +3153,16 @@ function segmentFrom(items, index, offset) {
590
3153
  break;
591
3154
  const text = skip > 0 ? item.run.text.slice(skip) : item.run.text;
592
3155
  if (text) {
593
- spans.add(item.box.textStart + skip, text.length);
3156
+ spans.add(item.start + skip, text.length, item.control ? null : item.box);
594
3157
  runs.push(skip > 0 ? { ...item.run, text } : item.run);
595
3158
  }
596
3159
  skip = 0;
597
3160
  }
598
3161
  return { runs, spans, nextIndex: i };
599
3162
  }
3163
+ /** A text a line may break after where its block wraps: one that ends in
3164
+ * white space, which a no-break space a `nowrap` element holds is not. */
3165
+ const BREAKS_AFTER = /[ \t\n]$/;
600
3166
  /** Move `(index, offset)` forward by `consumed` code units of text. */
601
3167
  function advance(items, index, offset, consumed) {
602
3168
  let i = index;
@@ -618,8 +3184,9 @@ function advance(items, index, offset, consumed) {
618
3184
  /**
619
3185
  * One space's advance in a run's style, measured as the difference between
620
3186
  * `x x` and `xx` — ntk strips a lone trailing space from a line, so it
621
- * cannot be measured directly. Cached per font manager per style; the
622
- * measurement itself hits ntk's shaping memo.
3187
+ * cannot be measured directly — plus the run's letter spacing, which a
3188
+ * space takes as every other character does. Cached per font manager per
3189
+ * face; the measurement itself hits ntk's shaping memo.
623
3190
  */
624
3191
  const SPACE_ADVANCE = new WeakMap();
625
3192
  function spaceAdvance(fonts, run) {
@@ -628,10 +3195,11 @@ function spaceAdvance(fonts, run) {
628
3195
  cache = new Map();
629
3196
  SPACE_ADVANCE.set(fonts, cache);
630
3197
  }
3198
+ const spacing = run.letterSpacing ?? 0;
631
3199
  const key = `${run.family}|${run.size}|${run.weight}|${run.style}`;
632
3200
  const hit = cache.get(key);
633
3201
  if (hit !== undefined)
634
- return hit;
3202
+ return hit + spacing;
635
3203
  const style = {
636
3204
  family: run.family,
637
3205
  size: run.size,
@@ -643,7 +3211,110 @@ function spaceAdvance(fonts, run) {
643
3211
  if (cache.size > 64)
644
3212
  cache.clear();
645
3213
  cache.set(key, advance);
646
- return advance;
3214
+ return advance + spacing;
3215
+ }
3216
+ /** Whether the runs' joined text has a forced break just before a
3217
+ * code-unit offset — a line's end, which an engine counts the break in. */
3218
+ function breakBefore(runs, offset) {
3219
+ let at = 0;
3220
+ for (const run of runs) {
3221
+ const next = at + run.text.length;
3222
+ if (offset - 1 < next)
3223
+ return run.text.charCodeAt(offset - 1 - at) === 10;
3224
+ at = next;
3225
+ }
3226
+ return false;
3227
+ }
3228
+ /**
3229
+ * The white space a text engine strips from a line's end, as a pattern for
3230
+ * the end of a run: spaces and tabs, and the no-break space too where the
3231
+ * engine strips that — ntk to 8.12.9 and CoreText through @windowkit/appkit
3232
+ * 0.15.0 do, where CSS measures it (sidorares/ntk#395, windowkit/appkit#83).
3233
+ * Asked of the engine once per font manager, because what the line adds back
3234
+ * has to be what the engine took: added back after an engine that kept it, a
3235
+ * no-break space before an image counted twice.
3236
+ */
3237
+ const HUNG = new WeakMap();
3238
+ export function hungSpaces(fonts, run) {
3239
+ let hung = HUNG.get(fonts);
3240
+ if (!hung) {
3241
+ const style = { family: run.family, size: run.size };
3242
+ const width = (text) => fonts.layout([{ ...style, text }], style, {}).lines[0]?.width ?? 0;
3243
+ hung = width('x\u00A0') > width('x') ? /[ \t]+$/ : /[ \t\u00A0]+$/;
3244
+ HUNG.set(fonts, hung);
3245
+ }
3246
+ return hung;
3247
+ }
3248
+ /**
3249
+ * A clamped paragraph with an ellipsis at the end of its last line (CSS
3250
+ * Overflow 4, 4.2): its text up to where that line breaks in the room the
3251
+ * ellipsis leaves it — at a soft wrap opportunity, so the words that do
3252
+ * not fit beside the ellipsis go to the lines the clamp hides, and inside
3253
+ * a word only where the line has no other — and the ellipsis after it, in
3254
+ * the block's own style. The engine's own ellipsis cuts inside the last
3255
+ * word, as `text-overflow` does.
3256
+ */
3257
+ function ellipsized(fonts, items, runs, base, options, layout, width, style) {
3258
+ const mark = runFor('\u2026', style);
3259
+ const room = width - fonts.layout([mark], base, {}).width;
3260
+ const last = layout.lines[layout.lines.length - 1];
3261
+ let end = last.end;
3262
+ if (last.width > room) {
3263
+ const probe = fonts.layout(sliceRuns(runs, last.start, last.end), base, {
3264
+ ...options,
3265
+ maxWidth: Math.max(0, room),
3266
+ maxLines: 1,
3267
+ overflow: 'clip',
3268
+ overflowWrap: 'break-word',
3269
+ });
3270
+ end = last.start + (probe.lines[0]?.end ?? 0);
3271
+ }
3272
+ // the text to the break, less the white space the line ends in — an
3273
+ // empty line keeps the break before it, and is the ellipsis alone
3274
+ const line = sliceRuns(runs, last.start, end);
3275
+ let trailing = 0;
3276
+ for (let i = line.length - 1; i >= 0; i -= 1) {
3277
+ const text = line[i].text;
3278
+ const trimmed = text.replace(/[ \t\n\r\f]+$/, '').length;
3279
+ trailing += text.length - trimmed;
3280
+ if (trimmed)
3281
+ break;
3282
+ }
3283
+ const kept = sliceRuns(runs, 0, end - trailing);
3284
+ // and the spans to match, the ellipsis no text of the document's
3285
+ const spans = new SpanMap();
3286
+ let at = items.length ? items[0].start : 0;
3287
+ let left = 0;
3288
+ for (const run of kept)
3289
+ left += run.text.length;
3290
+ for (const item of items) {
3291
+ if (left <= 0)
3292
+ break;
3293
+ const length = Math.min(item.run.text.length, left);
3294
+ spans.add(item.start, length, item.control ? null : item.box);
3295
+ left -= length;
3296
+ at = item.start + length;
3297
+ }
3298
+ spans.add(at, 1, null);
3299
+ kept.push(mark);
3300
+ return { layout: fonts.layout(kept, base, options), runs: kept, spans };
3301
+ }
3302
+ /** The runs' text from one code-unit offset into their joined text to
3303
+ * another, each piece in its own run's style. */
3304
+ function sliceRuns(runs, from, to) {
3305
+ const out = [];
3306
+ let at = 0;
3307
+ for (const run of runs) {
3308
+ const next = at + run.text.length;
3309
+ if (next > from && at < to) {
3310
+ const text = run.text.slice(Math.max(0, from - at), to - at);
3311
+ out.push(text === run.text ? run : { ...run, text });
3312
+ }
3313
+ at = next;
3314
+ if (at >= to)
3315
+ break;
3316
+ }
3317
+ return out;
647
3318
  }
648
3319
  /** Whether anything but whitespace lies past a code-unit offset into the
649
3320
  * runs' joined text — "did `maxLines` drop content", for a layout that
@@ -652,7 +3323,8 @@ function inkBeyond(runs, offset) {
652
3323
  let at = 0;
653
3324
  for (const run of runs) {
654
3325
  const next = at + run.text.length;
655
- if (next > offset && /\S/.test(run.text.slice(Math.max(0, offset - at)))) {
3326
+ if (next > offset &&
3327
+ /[^ \t\n\r\f]/.test(run.text.slice(Math.max(0, offset - at)))) {
656
3328
  return true;
657
3329
  }
658
3330
  at = next;
@@ -663,18 +3335,419 @@ function inkBeyond(runs, offset) {
663
3335
  function wraps(style) {
664
3336
  return style.whiteSpace !== 'nowrap' && style.whiteSpace !== 'pre';
665
3337
  }
666
- function alignFor(style) {
667
- // ntk has no justification; `start` is closer than a silent left on an RTL
668
- // paragraph, and closer than a ragged-right lie about what was drawn.
3338
+ /** How much of a line's unused room goes before it: 0 for a line set flush
3339
+ * left, 1 flush right, ½ centred. `start` and `end` follow the direction;
3340
+ * `justify` is set as `start`, as `alignFor` sets it. */
3341
+ /** How far into the room a line an alignment sets goes: 0 at the left, 1
3342
+ * at the right; `justify` at the start, since a line not justified is
3343
+ * set there. */
3344
+ function alignShift(align, rtl) {
3345
+ switch (align) {
3346
+ case 'center':
3347
+ return 0.5;
3348
+ case 'right':
3349
+ return 1;
3350
+ case 'left':
3351
+ return 0;
3352
+ case 'end':
3353
+ return rtl ? 0 : 1;
3354
+ default:
3355
+ return rtl ? 1 : 0;
3356
+ }
3357
+ }
3358
+ /** The blocks whose lines hold text a box's background shows through. */
3359
+ export const CLIPPED_TEXT = new WeakSet();
3360
+ /** How each of those blocks' layouts was made, to make it again. */
3361
+ const RECORDED = new WeakMap();
3362
+ const RECORDERS = new WeakMap();
3363
+ const INKLESS = new WeakMap();
3364
+ /** The fonts, noting how each layout they make was made. One recorder a
3365
+ * fonts object, so what is kept per fonts object is kept for it too. */
3366
+ function recording(fonts) {
3367
+ let recorder = RECORDERS.get(fonts);
3368
+ if (!recorder) {
3369
+ recorder = {
3370
+ layout(content, style, options) {
3371
+ const layout = fonts.layout(content, style, options);
3372
+ RECORDED.set(layout, { fonts, content, style, options });
3373
+ return layout;
3374
+ },
3375
+ match: (family, style) => fonts.match(family, style),
3376
+ };
3377
+ RECORDERS.set(fonts, recorder);
3378
+ }
3379
+ return recorder;
3380
+ }
3381
+ /**
3382
+ * A layout of the same text with no ink of its own, which an engine draws
3383
+ * in the context's fill — a gradient's, for text a background shows
3384
+ * through; the same runs, so the same glyphs where the first put them.
3385
+ * Null for a layout that was not recorded.
3386
+ */
3387
+ export function inklessLayout(layout) {
3388
+ let inkless = INKLESS.get(layout);
3389
+ if (inkless !== undefined)
3390
+ return inkless;
3391
+ const made = RECORDED.get(layout);
3392
+ inkless = made
3393
+ ? made.fonts.layout(made.content.map((run) => ({ ...run, color: undefined })), { ...made.style, color: undefined }, made.options)
3394
+ : null;
3395
+ INKLESS.set(layout, inkless);
3396
+ return inkless;
3397
+ }
3398
+ /** Whether any of a block's text is text a background shows through. */
3399
+ function clipsText(block, items) {
3400
+ for (const item of items) {
3401
+ if (item.kind === 'text' && !item.control && clipBoxOf(item.box, block)) {
3402
+ return true;
3403
+ }
3404
+ }
3405
+ return false;
3406
+ }
3407
+ /** The box whose background a text box's glyphs show: the nearest element
3408
+ * box from it up to the block its lines are in with `background-clip:
3409
+ * text`, or null. */
3410
+ export function clipBoxOf(box, block) {
3411
+ for (let at = box; at; at = at.parent) {
3412
+ if (at.kind !== 'text' && at.style.backgroundClipText)
3413
+ return at;
3414
+ if (at === block)
3415
+ break;
3416
+ }
3417
+ return null;
3418
+ }
3419
+ /** The most lines `text-wrap: balance` evens out, as in Chrome: a heading,
3420
+ * a caption, a pull quote — not a paragraph, whose last line is its own. */
3421
+ const BALANCE_LINES = 6;
3422
+ /**
3423
+ * The narrowest width a paragraph's text breaks into no more lines than
3424
+ * `layout` has (CSS Text 4, 6.2), found by halving the width between the
3425
+ * least that could hold them — their widths over their number — and the
3426
+ * width it was laid out at. The engine breaks the lines, so each step
3427
+ * asks it; eight steps come to within a pixel of the answer for a heading
3428
+ * across a page.
3429
+ */
3430
+ function balancedWidth(fonts, runs, base, options, width, layout) {
3431
+ const count = layout.lines.length;
3432
+ let total = 0;
3433
+ for (const line of layout.lines)
3434
+ total += line.width;
3435
+ let lo = total / count;
3436
+ let hi = width;
3437
+ for (let step = 0; step < 8 && hi - lo > 1; step += 1) {
3438
+ const mid = (lo + hi) / 2;
3439
+ const probe = fonts.layout(runs, base, { ...options, maxWidth: mid });
3440
+ if (probe.lines.length <= count)
3441
+ hi = mid;
3442
+ else
3443
+ lo = mid;
3444
+ }
3445
+ return Math.ceil(hi);
3446
+ }
3447
+ /**
3448
+ * Where `text-align` puts the lines of a layout that was given no width to
3449
+ * align them in — text that does not wrap, which has no `maxWidth` — as how
3450
+ * far each moves: the engine aligns those lines within the widest of them,
3451
+ * which for a single line is no alignment at all, so a centred `<td
3452
+ * nowrap>` or a `white-space: nowrap` button label was set flush left. A
3453
+ * line too long for its box is set at its start, overflowing its end (CSS
3454
+ * Text 3, 7.1), which is its left in a right-to-left paragraph, as in
3455
+ * Blink. Null where every line is already where it belongs: flush left in
3456
+ * a left-to-right paragraph, or measured for a width it has not been given.
3457
+ */
3458
+ /** How the lines of a layout made with no width to fill are placed in
3459
+ * the room: each is its text's last or a forced break's, and takes
3460
+ * `text-align-last`, but where `last` is false — a balanced paragraph's,
3461
+ * laid out narrower than its room. */
3462
+ function unwrappedPlacer(style, width, last = true) {
3463
+ const rtl = style.direction === 'rtl';
3464
+ const shift = alignShift(last ? lastAlignOf(style) : style.textAlign, rtl);
3465
+ if ((shift === 0 && !rtl) || !Number.isFinite(width))
3466
+ return null;
3467
+ return (line) => {
3468
+ const free = width - line.width;
3469
+ return (free >= 0 ? free * shift : rtl ? free : 0) - line.x;
3470
+ };
3471
+ }
3472
+ /**
3473
+ * The spacing a space is given to be laid out as one that may be spaced
3474
+ * apart, too little to move a glyph. It is not nothing, because neither
3475
+ * engine measures a space that is a spaced run of its own as it measures
3476
+ * the same space inside its run: ntk shapes each run apart, so the kerning
3477
+ * pair a space made with the letter beside it is gone (Arial's `A` and
3478
+ * `T` have them), and CoreText spaces a glyph with its kerning attribute,
3479
+ * which takes the place of the font's pairs. A line justified from a
3480
+ * measure of its spaces as they were came out wider than its box and
3481
+ * broke a word early: on macOS, in Helvetica, half of a paragraph did.
3482
+ */
3483
+ const HAIR = 1e-6;
3484
+ /** The runs with each space a run of its own, spaced a hair apart. */
3485
+ function spacedApart(runs) {
3486
+ const out = [];
3487
+ for (const run of runs) {
3488
+ const text = run.text;
3489
+ let piece = 0;
3490
+ for (let at = 0; at < text.length; at += 1) {
3491
+ const c = text.charCodeAt(at);
3492
+ if (c !== 0x20 && c !== 0xa0)
3493
+ continue;
3494
+ if (at > piece)
3495
+ out.push({ ...run, text: text.slice(piece, at) });
3496
+ out.push({
3497
+ ...run,
3498
+ text: text[at],
3499
+ letterSpacing: (run.letterSpacing ?? 0) + HAIR,
3500
+ });
3501
+ piece = at + 1;
3502
+ }
3503
+ if (piece === 0)
3504
+ out.push(run);
3505
+ else if (piece < text.length)
3506
+ out.push({ ...run, text: text.slice(piece) });
3507
+ }
3508
+ return out;
3509
+ }
3510
+ /**
3511
+ * `text-align: justify` (CSS Text 3, 7.4), as neither engine has it: the
3512
+ * paragraph's runs again, with each space inside a line that is to be
3513
+ * justified widened by its share of what the line leaves of `width` — the
3514
+ * `letter-spacing` a `word-spacing` is drawn with — so the same breaks fill
3515
+ * their lines. A line that is the paragraph's last, or that a forced break
3516
+ * ends, is not justified, nor one with no space inside it; the spaces a
3517
+ * line ends on hang, and take no share. `skip` is the offsets of spaces
3518
+ * that are no word separators — an inline box's edge laid out as a spacer
3519
+ * (`layoutSpaced`) — which take no share either. Null where no line is
3520
+ * justified.
3521
+ */
3522
+ function justifiedRuns(runs, layout, width, rest = true, last = false, skip) {
3523
+ const text = runs.map((run) => run.text).join('');
3524
+ // each space's extra, by its offset in the paragraph
3525
+ const extra = new Map();
3526
+ const lines = layout.lines;
3527
+ for (let i = 0; i < lines.length; i += 1) {
3528
+ const line = lines[i];
3529
+ let end = line.end;
3530
+ // the paragraph's last line, or one a forced break ends, is
3531
+ // `text-align-last`'s to justify
3532
+ const ends = i === lines.length - 1 || text[end - 1] === '\n' || text[end] === '\n';
3533
+ if (!(ends ? last : rest))
3534
+ continue;
3535
+ while (end > line.start &&
3536
+ (text[end - 1] === ' ' ||
3537
+ text[end - 1] === '\u00a0' ||
3538
+ !!skip?.has(end - 1)))
3539
+ end -= 1;
3540
+ const spaces = [];
3541
+ for (let at = line.start; at < end; at += 1) {
3542
+ if ((text[at] === ' ' || text[at] === '\u00a0') && !skip?.has(at)) {
3543
+ spaces.push(at);
3544
+ }
3545
+ }
3546
+ // a hair short, so that the engine breaks where it did
3547
+ const slack = width - line.width - 0.01;
3548
+ if (!spaces.length || !(slack > 0))
3549
+ continue;
3550
+ for (const at of spaces)
3551
+ extra.set(at, slack / spaces.length);
3552
+ }
3553
+ return extra.size ? widenedAt(runs, extra) : null;
3554
+ }
3555
+ /** The runs with the character at each offset of `extra` a run of its
3556
+ * own, spaced that much more. */
3557
+ function widenedAt(runs, extra) {
3558
+ const text = runs.map((run) => run.text).join('');
3559
+ const out = [];
3560
+ let from = 0;
3561
+ for (const run of runs) {
3562
+ const end = from + run.text.length;
3563
+ let piece = from;
3564
+ for (let at = from; at < end; at += 1) {
3565
+ const add = extra.get(at);
3566
+ if (add === undefined)
3567
+ continue;
3568
+ if (at > piece)
3569
+ out.push({ ...run, text: text.slice(piece, at) });
3570
+ out.push({
3571
+ ...run,
3572
+ text: text[at],
3573
+ letterSpacing: (run.letterSpacing ?? 0) + add,
3574
+ });
3575
+ piece = at + 1;
3576
+ }
3577
+ if (end > piece)
3578
+ out.push({ ...run, text: text.slice(piece, end) });
3579
+ from = end;
3580
+ }
3581
+ return out;
3582
+ }
3583
+ /** A word separator, as justification spaces them out (CSS Text 3, 7.4):
3584
+ * a space, or a no-break one. */
3585
+ function isSeparator(code) {
3586
+ return code === 0x20 || code === 0xa0;
3587
+ }
3588
+ /**
3589
+ * A line made a piece at a time — beside a float, around an inline-block,
3590
+ * between an inline box's edges — justified where it is (CSS Text 3, 7.4):
3591
+ * the room it leaves, `free`, shared out equally among its word
3592
+ * separators, as `justifiedRuns` shares out a paragraph's. A space inside a
3593
+ * piece of text takes its share as letter spacing, the piece laid out again
3594
+ * with it; a space a piece ends on, with something after it on the line,
3595
+ * takes it as room before what follows. The spaces the line ends on hang
3596
+ * (7.3), whatever inline box closes after them, and take none. Whatever
3597
+ * follows a share on the line moves over by it. How much wider the line
3598
+ * is.
3599
+ */
3600
+ function justifyLine(open, free, how) {
3601
+ const order = open.order;
3602
+ let lastContent = -1;
3603
+ for (let i = order.length - 1; i >= 0; i -= 1) {
3604
+ if (order[i].kind !== 'edge') {
3605
+ lastContent = i;
3606
+ break;
3607
+ }
3608
+ }
3609
+ const plans = new Map();
3610
+ let count = 0;
3611
+ for (let i = 0; i < order.length; i += 1) {
3612
+ const placed = order[i];
3613
+ if (placed.kind !== 'text')
3614
+ continue;
3615
+ const text = placed.item;
3616
+ // the runs it was laid out from, where they are still what is kept
3617
+ // (a selection keeps its offsets in their place)
3618
+ const kept = LAYOUT_RUNS.get(text.layout);
3619
+ const runs = kept?.length && typeof kept[0] !== 'number' ? kept : null;
3620
+ const line = text.layout.lines[text.layoutLine];
3621
+ if (!runs || !line)
3622
+ continue;
3623
+ const all = runs.map((run) => run.text).join('');
3624
+ let end = line.end;
3625
+ while (end > line.start && isSeparator(all.charCodeAt(end - 1)))
3626
+ end -= 1;
3627
+ const inside = [];
3628
+ for (let at = line.start; at < end; at += 1) {
3629
+ if (isSeparator(all.charCodeAt(at)))
3630
+ inside.push(at);
3631
+ }
3632
+ // the spaces it ends on, before what follows it on the line
3633
+ let after = 0;
3634
+ if (i < lastContent) {
3635
+ for (let at = end; at < all.length; at += 1) {
3636
+ if (!isSeparator(all.charCodeAt(at)))
3637
+ break;
3638
+ after += 1;
3639
+ }
3640
+ }
3641
+ count += inside.length + after;
3642
+ plans.set(text, { runs, inside, after });
3643
+ }
3644
+ if (!count)
3645
+ return 0;
3646
+ const share = free / count;
3647
+ let moved = 0;
3648
+ for (const placed of order) {
3649
+ if (placed.kind !== 'text') {
3650
+ placed.item.x += moved;
3651
+ continue;
3652
+ }
3653
+ const text = placed.item;
3654
+ text.drawX += moved;
3655
+ const plan = plans.get(text);
3656
+ if (!plan)
3657
+ continue;
3658
+ if (plan.inside.length) {
3659
+ const line = text.layout.lines[text.layoutLine];
3660
+ const widened = widenedAt(plan.runs, new Map(plan.inside.map((at) => [at, share])));
3661
+ // as wide as the piece is to be, and the piece alone: the text after
3662
+ // it did not fit beside it before it was any wider
3663
+ const layout = how.fonts.layout(widened, how.base, {
3664
+ ...how.options,
3665
+ maxWidth: line.width + plan.inside.length * share + 0.5,
3666
+ maxLines: 1,
3667
+ });
3668
+ const first = layout.lines[0];
3669
+ if (first && first.start === line.start && first.end === line.end) {
3670
+ LAYOUT_RUNS.set(layout, widened);
3671
+ text.layout = layout;
3672
+ text.layoutLine = 0;
3673
+ moved += first.width - line.width;
3674
+ }
3675
+ }
3676
+ moved += plan.after * share;
3677
+ }
3678
+ return moved;
3679
+ }
3680
+ /** Which of a paragraph's lines are justified (CSS Text 3, 7.4): those
3681
+ * its end or a forced break does not end, where `text-align` is
3682
+ * `justify`, and those it does, where `text-align-last` is — neither
3683
+ * where the text does not wrap, nor under `text-justify: none`. */
3684
+ function justification(style) {
3685
+ const on = wraps(style) && style.textJustify !== 'none';
3686
+ return {
3687
+ rest: on && style.textAlign === 'justify',
3688
+ last: on && lastAlignOf(style) === 'justify',
3689
+ };
3690
+ }
3691
+ /**
3692
+ * A layout of `runs` whose justified lines fill `width`: measured as it
3693
+ * will be drawn, its spaces spaced apart (`HAIR`), then laid out again with
3694
+ * each space inside a justified line widened by its share
3695
+ * (`justifiedRuns`), which keeps the lines where they broke. The layout
3696
+ * that is given where no line is to be justified. Every path that lays a
3697
+ * paragraph out as one layout justifies through this, so that a paragraph
3698
+ * is justified whichever it took.
3699
+ */
3700
+ function justifiedLayout(fonts, runs, base, options, layout, width, { rest, last }, skip) {
3701
+ if (!Number.isFinite(width) ||
3702
+ !((rest && layout.lines.length > 1) || (last && layout.lines.length > 0))) {
3703
+ return { layout, runs };
3704
+ }
3705
+ const spaced = spacedApart(runs);
3706
+ const measured = fonts.layout(spaced, base, options);
3707
+ const justified = justifiedRuns(spaced, measured, width, rest, last, skip);
3708
+ const out = justified ? fonts.layout(justified, base, options) : measured;
3709
+ const outRuns = justified ?? spaced;
3710
+ LAYOUT_RUNS.set(out, outRuns);
3711
+ return { layout: out, runs: outRuns };
3712
+ }
3713
+ /** An alignment as the text engine takes it. It has no justification:
3714
+ * `start` is closer than a silent left on an RTL paragraph, and closer
3715
+ * than a ragged-right lie about what was drawn — justified lines are
3716
+ * spaced to fill their width before they get here (`justifiedRuns`). */
3717
+ function engineAlign(align) {
3718
+ return align === 'justify' || align === 'auto' ? 'start' : align;
3719
+ }
3720
+ /** How the lines the text or a forced break ends are aligned (CSS Text 3,
3721
+ * 7.2): `text-align-last`, or where that is `auto`, `text-align`, but
3722
+ * `justify`'s, which leaves them at the start. */
3723
+ function lastAlignOf(style) {
3724
+ const last = style.textAlignLast;
3725
+ if (last !== 'auto')
3726
+ return last;
669
3727
  return style.textAlign === 'justify' ? 'start' : style.textAlign;
670
3728
  }
3729
+ /**
3730
+ * The `text-indent` a block's first line takes: none where that line is
3731
+ * not its element's first formatted line (CSS 2.1 16.1). An anonymous
3732
+ * block's is only where the block is its parent's first child: the text
3733
+ * after a `<div>` in a `<span>`, or after a paragraph in a `<div>`, starts
3734
+ * a line of no indent.
3735
+ */
3736
+ function blockIndent(block, width) {
3737
+ if (!block.el && block.parent && block.parent.children[0] !== block) {
3738
+ return 0;
3739
+ }
3740
+ return indentOf(block.style, width);
3741
+ }
671
3742
  function indentOf(style, width) {
672
3743
  const indent = style.textIndent;
673
3744
  if (typeof indent === 'number')
674
3745
  return indent;
675
3746
  if (indent === 'auto')
676
3747
  return 0;
677
- return Number.isFinite(width) ? (indent.pct / 100) * width : 0;
3748
+ // of zero where the width is not known yet, as when measuring the
3749
+ // content's width: a `calc()` keeps its pixels (CSS Sizing 3 5.2.1)
3750
+ return resolve(indent, Number.isFinite(width) ? width : 0);
678
3751
  }
679
3752
  /**
680
3753
  * CSS's `line-height` expressed as ntk's multiplier.
@@ -684,8 +3757,14 @@ function indentOf(style, width) {
684
3757
  * rather than passing the CSS number through is the difference between
685
3758
  * `line-height: 1.5` meaning 1.5 × 16px and it meaning 1.5 × 19px, which is
686
3759
  * a visibly looser document than the author asked for.
3760
+ *
3761
+ * `line-height: 0` is legal and means what it says — lines of no height,
3762
+ * the glyphs overflowing evenly — and ntk's half-leading does exactly that
3763
+ * with a multiplier of 0. The floor is a hair above it rather than the 0.1
3764
+ * it was, which set such lines a tenth of a line apart: the Cocoa engine
3765
+ * takes a zero multiple for none at all.
687
3766
  */
688
- function lineHeightMultiplier(fonts, style) {
3767
+ export function lineHeightMultiplier(fonts, style) {
689
3768
  if (style.lineHeight === 'normal')
690
3769
  return 1;
691
3770
  const target = style.lineHeightIsLength
@@ -694,25 +3773,369 @@ function lineHeightMultiplier(fonts, style) {
694
3773
  const natural = naturalLineHeight(fonts, style);
695
3774
  if (!natural)
696
3775
  return 1;
697
- return Math.max(0.1, target / natural);
3776
+ return Math.max(1e-4, target / natural);
698
3777
  }
3778
+ /**
3779
+ * The font's natural line height in a style, which CSS's number form is
3780
+ * converted against. Kept per font manager per style, like a space's
3781
+ * advance: a pass asks once a paragraph — 8,800 times at 600 KB — and on
3782
+ * CoreText every answer was a call to the native side. Found by the style
3783
+ * object first, as `faceExtent` is: the paragraphs of a pass share their
3784
+ * styles, and spelling a face into a string key for each was a millisecond
3785
+ * of every pass over a long document.
3786
+ */
3787
+ const NATURAL_LINE_HEIGHT = new WeakMap();
699
3788
  function naturalLineHeight(fonts, style) {
3789
+ let cache = NATURAL_LINE_HEIGHT.get(fonts);
3790
+ if (!cache) {
3791
+ cache = { byStyle: new WeakMap(), byFace: new Map() };
3792
+ NATURAL_LINE_HEIGHT.set(fonts, cache);
3793
+ }
3794
+ const known = cache.byStyle.get(style);
3795
+ if (known !== undefined)
3796
+ return known;
3797
+ const key = `${style.fontFamily}|${style.fontSize}|${style.fontWeight}|${style.fontStyle}`;
3798
+ let height = cache.byFace.get(key);
3799
+ if (height === undefined) {
3800
+ try {
3801
+ const font = fonts.match(style.fontFamily, {
3802
+ size: style.fontSize,
3803
+ weight: style.fontWeight,
3804
+ style: style.fontStyle,
3805
+ });
3806
+ height = font.metrics(style.fontSize).lineHeight;
3807
+ }
3808
+ catch {
3809
+ height = style.fontSize * 1.2;
3810
+ }
3811
+ if (cache.byFace.size > 64)
3812
+ cache.byFace.clear();
3813
+ cache.byFace.set(key, height);
3814
+ }
3815
+ cache.byStyle.set(style, height);
3816
+ return height;
3817
+ }
3818
+ /**
3819
+ * A block's strut: the ascent and descent of a line of nothing in its face,
3820
+ * the half-leading its `line-height` adds shared above and below, as the
3821
+ * text engines set a line (CSS 2.1 10.8.1).
3822
+ */
3823
+ export function strutOf(fonts, style) {
3824
+ // text set at no size takes no room, and CoreText reads a size of 0 as
3825
+ // its default twelve points
3826
+ if (!(style.fontSize > 0))
3827
+ return NO_EXTENT;
3828
+ const face = faceExtent(fonts, style);
3829
+ const target = style.lineHeight === 'normal'
3830
+ ? naturalLineHeight(fonts, style)
3831
+ : style.lineHeightIsLength
3832
+ ? style.lineHeight
3833
+ : style.lineHeight * style.fontSize;
3834
+ const half = (target - face.ascent - face.descent) / 2;
3835
+ return { ascent: face.ascent + half, descent: face.descent + half };
3836
+ }
3837
+ /**
3838
+ * The raises `vertical-align` gives a paragraph's inline boxes (CSS 2.1
3839
+ * 10.8.1): each from its parent's baseline, so a box inside a raised box is
3840
+ * raised with it. `sub` and `super` by a fifth and a third of the parent's
3841
+ * font size and a pixel, as browsers set them; `text-top` and `text-bottom`
3842
+ * to the edges of the parent's font; `middle` by its middle to half the
3843
+ * parent's x-height above its baseline; a length by itself and a
3844
+ * percentage of the box's own line height. Worked out once a box a pass.
3845
+ */
3846
+ class Lifts {
3847
+ fonts;
3848
+ block;
3849
+ styleOf;
3850
+ _boxes = new Map();
3851
+ constructor(fonts, block,
3852
+ /** A box's style on the lines these are for: its own, or the one it
3853
+ * has on a first line (`firstLineSetting`). */
3854
+ styleOf = ownStyle) {
3855
+ this.fonts = fonts;
3856
+ this.block = block;
3857
+ this.styleOf = styleOf;
3858
+ }
3859
+ /** Where a text goes, or null for one that is on the line's baseline and
3860
+ * in no box with a line height of its own. */
3861
+ of(text) {
3862
+ const owner = text.spans.boxAt?.(text.layoutStart);
3863
+ const parent = owner?.parent;
3864
+ if (!owner || !parent || parent.kind !== 'inline')
3865
+ return null;
3866
+ const { raise, edge, lead } = this._box(parent);
3867
+ if (!raise && !edge && !lead)
3868
+ return null;
3869
+ // the room of every box whose text is in it: no raised box starts or
3870
+ // ends inside a fragment, but one it holds may be set larger
3871
+ const own = { ...strutOf(this.fonts, this.styleOf(owner)) };
3872
+ // and of the inline boxes around it, each about its own baseline: each
3873
+ // is on the line with its line height, which the text's may not reach.
3874
+ // Not only where one sets a line height of its own: a raised `<sup>`
3875
+ // holding a smaller `<a>` — every footnote mark — is as tall on the
3876
+ // line as its own font and line height make it (CSS 2.1 10.8)
3877
+ if (lead || raise) {
3878
+ for (let at = parent; at?.kind === 'inline'; at = at.parent) {
3879
+ const box = this._box(at);
3880
+ const room = strutOf(this.fonts, this.styleOf(at));
3881
+ own.ascent = Math.max(own.ascent, room.ascent + box.raise - raise);
3882
+ own.descent = Math.max(own.descent, room.descent - box.raise + raise);
3883
+ if (box.edge === at)
3884
+ break;
3885
+ }
3886
+ }
3887
+ const natural = text.layout.lines[text.layoutLine];
3888
+ let seen = this.styleOf(owner);
3889
+ for (const run of natural?.runs ?? []) {
3890
+ const at = text.spans.boxAt?.(run.start);
3891
+ const style = at && this.styleOf(at);
3892
+ if (!style || style === seen)
3893
+ continue;
3894
+ seen = style;
3895
+ const room = strutOf(this.fonts, style);
3896
+ own.ascent = Math.max(own.ascent, room.ascent);
3897
+ own.descent = Math.max(own.descent, room.descent);
3898
+ }
3899
+ let to = null;
3900
+ if (edge) {
3901
+ const room = strutOf(this.fonts, this.styleOf(edge));
3902
+ to = {
3903
+ box: edge,
3904
+ to: this.styleOf(edge).verticalAlign === 'top' ? 'top' : 'bottom',
3905
+ ascent: room.ascent,
3906
+ descent: room.descent,
3907
+ };
3908
+ }
3909
+ return { raise, ascent: own.ascent, descent: own.descent, edge: to };
3910
+ }
3911
+ /** Where an inline box's own room is about the line's baseline — its
3912
+ * face at its line height, raised as it is — or null for one on the
3913
+ * baseline, or one the line's edge sets. */
3914
+ ofBox(box) {
3915
+ const { raise, edge, lead } = this._box(box);
3916
+ if (edge)
3917
+ return null;
3918
+ if (!raise && !lead && !tallerStrut(this.fonts, this.block, box)) {
3919
+ return null;
3920
+ }
3921
+ const room = strutOf(this.fonts, this.styleOf(box));
3922
+ return { raise, ascent: room.ascent, descent: room.descent };
3923
+ }
3924
+ /** The `top` or `bottom` box a box is in, itself included, and how far
3925
+ * its baseline is raised from that box's; null outside one. */
3926
+ edgeOf(box) {
3927
+ const lift = this._box(box);
3928
+ return lift.edge ? { edge: lift.edge, raise: lift.raise } : null;
3929
+ }
3930
+ _box(box) {
3931
+ if (box.kind !== 'inline' || !box.parent)
3932
+ return NO_LIFT;
3933
+ const known = this._boxes.get(box);
3934
+ if (known)
3935
+ return known;
3936
+ const va = this.styleOf(box).verticalAlign;
3937
+ const own = ownsLeading(this.fonts, this.block, this.styleOf(box));
3938
+ let lift;
3939
+ if (va === 'top' || va === 'bottom') {
3940
+ // what is in it is raised from its baseline, which the line's edge
3941
+ // sets, whatever is around it
3942
+ lift = { raise: 0, edge: box, lead: own };
3943
+ }
3944
+ else {
3945
+ const up = this._box(box.parent);
3946
+ lift = {
3947
+ raise: up.raise + this._own(box, box.parent),
3948
+ edge: up.edge,
3949
+ lead: own || up.lead,
3950
+ };
3951
+ if (lift.raise && !lift.edge)
3952
+ BOX_RAISES.set(box, lift.raise);
3953
+ }
3954
+ this._boxes.set(box, lift);
3955
+ return lift;
3956
+ }
3957
+ /** How far a box's own `vertical-align` raises it from its parent's
3958
+ * baseline. */
3959
+ _own(box, parent) {
3960
+ const va = this.styleOf(box).verticalAlign;
3961
+ const size = this.styleOf(parent).fontSize;
3962
+ switch (va) {
3963
+ case 'baseline':
3964
+ case 'top':
3965
+ case 'bottom':
3966
+ return 0;
3967
+ case 'sub':
3968
+ return -(size / 5 + size / 16);
3969
+ case 'super':
3970
+ return size / 3 + size / 16;
3971
+ case 'text-top':
3972
+ return (faceExtent(this.fonts, this.styleOf(parent)).ascent -
3973
+ strutOf(this.fonts, this.styleOf(box)).ascent);
3974
+ case 'text-bottom':
3975
+ return (strutOf(this.fonts, this.styleOf(box)).descent -
3976
+ faceExtent(this.fonts, this.styleOf(parent)).descent);
3977
+ case 'middle': {
3978
+ const own = strutOf(this.fonts, this.styleOf(box));
3979
+ return (xHeightOf(this.fonts, this.styleOf(parent)) / 2 -
3980
+ (own.ascent - own.descent) / 2);
3981
+ }
3982
+ default:
3983
+ return typeof va === 'number'
3984
+ ? va
3985
+ : resolve(va, lineHeightOf(this.fonts, this.styleOf(box)));
3986
+ }
3987
+ }
3988
+ }
3989
+ const ownStyle = (box) => box.style;
3990
+ const NO_LIFT = { raise: 0, edge: null, lead: false };
3991
+ /**
3992
+ * Whether an inline box's own line height sets its line otherwise than the
3993
+ * one a paragraph's layout gives its text. That layout sets every run at
3994
+ * the block's line height, as a multiple of the run's font's natural one;
3995
+ * CSS gives each inline box its own, and the line box holds them all (CSS
3996
+ * 2.1 10.8.1). So a box whose own is more makes its line taller, and a box
3997
+ * whose own is less makes it shorter than the multiple does, where the
3998
+ * multiple is more than the block's line height too: a larger face under
3999
+ * a `line-height` length, which every inline box inherits as that length —
4000
+ * `font: 11px/15px` on the body and a 14px heading run inline, which the
4001
+ * multiple set 19px tall on a line CSS makes 16.5. One whose own is less
4002
+ * but which the block's line height holds anyway, a `<code>` in a font
4003
+ * with taller natural lines, is left to the one layout: its lines come out
4004
+ * the same, and the line at a time costs a long document dear.
4005
+ *
4006
+ * Asked of every inline box in every paragraph, so it has to cost nothing
4007
+ * for the ones that are nothing: a box with its block's line height, family
4008
+ * and size is past at once, and any other answer is kept on its style.
4009
+ */
4010
+ function ownsLeading(fonts, block, style) {
4011
+ // a bold or an italic face keeps its family's line metrics, and a
4012
+ // paragraph is thick with <strong>, <em> and <a>
4013
+ if (style.lineHeight === block.lineHeight &&
4014
+ style.lineHeightIsLength === block.lineHeightIsLength &&
4015
+ style.fontSize === block.fontSize &&
4016
+ style.fontFamily === block.fontFamily) {
4017
+ return false;
4018
+ }
4019
+ const known = LEADS.get(style);
4020
+ if (known && known.block === block && known.fonts === fonts) {
4021
+ return known.own;
4022
+ }
4023
+ const multiple = lineHeightMultiplier(fonts, block) * naturalLineHeight(fonts, style);
4024
+ const height = lineHeightOf(fonts, style);
4025
+ const own = height > multiple + 0.5 ||
4026
+ multiple > Math.max(height, lineHeightOf(fonts, block)) + 0.5;
4027
+ LEADS.set(style, { fonts, block, own });
4028
+ return own;
4029
+ }
4030
+ /** Whether a text is in an inline box that owns its leading
4031
+ * (`ownsLeading`), below the block it is laid out in. */
4032
+ function inBoxOwningLeading(fonts, block, text) {
4033
+ for (let at = text.parent; at && at !== block; at = at.parent) {
4034
+ if (at.kind !== 'inline')
4035
+ break;
4036
+ if (ownsLeading(fonts, block.style, at.style))
4037
+ return true;
4038
+ }
4039
+ return false;
4040
+ }
4041
+ const LEADS = new WeakMap();
4042
+ /**
4043
+ * Whether an inline box with no text is taller on its line than the
4044
+ * paragraph's own line height: what it holds there, which the engine,
4045
+ * which lays out text, gives no room (CSS 2.1 10.8). A box with text is
4046
+ * given its face's room with the text, at the paragraph's leading, which
4047
+ * `ownsLeading` compares its own line height with; asked of one with none,
4048
+ * the border of an empty `<span>` in a large face was drawn over the text
4049
+ * above its line, which was as short as the paragraph's. Not asked of a
4050
+ * box with text, which the one-layout paths lay out whole: a padded
4051
+ * `<code>` in a paragraph would have been laid out a line at a time.
4052
+ */
4053
+ function tallerStrut(fonts, block, box) {
4054
+ if (box.subtreeTextEnd > box.subtreeTextStart)
4055
+ return false;
4056
+ const style = box.style;
4057
+ if (style.fontSize === block.fontSize &&
4058
+ style.lineHeight === block.lineHeight) {
4059
+ return false;
4060
+ }
4061
+ const own = strutOf(fonts, style);
4062
+ const theirs = strutOf(fonts, block);
4063
+ return own.ascent + own.descent > theirs.ascent + theirs.descent + 0.5;
4064
+ }
4065
+ /** A style's line height in pixels. */
4066
+ function lineHeightOf(fonts, style) {
4067
+ if (style.lineHeight === 'normal')
4068
+ return naturalLineHeight(fonts, style);
4069
+ return style.lineHeightIsLength
4070
+ ? style.lineHeight
4071
+ : style.lineHeight * style.fontSize;
4072
+ }
4073
+ /**
4074
+ * A style's x-height, where its font says, or half its size, as `ex` takes
4075
+ * it (CSS 2.1 4.3.2). A face whose OS/2 table is older than version 2 —
4076
+ * DejaVu's — states none, and the engine's answer is then NaN, which is a
4077
+ * number: taken for one, it put an image set `middle` nowhere at all.
4078
+ */
4079
+ function xHeightOf(fonts, style) {
700
4080
  try {
701
- const font = fonts.match(style.fontFamily, {
4081
+ const metrics = fonts
4082
+ .match(style.fontFamily, {
702
4083
  size: style.fontSize,
703
4084
  weight: style.fontWeight,
704
4085
  style: style.fontStyle,
705
- });
706
- return font.metrics(style.fontSize).lineHeight;
4086
+ })
4087
+ .metrics(style.fontSize);
4088
+ const x = metrics.xHeight;
4089
+ if (typeof x === 'number' && x > 0)
4090
+ return x;
707
4091
  }
708
4092
  catch {
709
- return style.fontSize * 1.2;
4093
+ // no font to ask
4094
+ }
4095
+ return style.fontSize / 2;
4096
+ }
4097
+ /** Whether a line has nothing on it yet. */
4098
+ function isEmpty(open) {
4099
+ return open.x === open.indent && !open.texts.length && !open.atomics.length;
4100
+ }
4101
+ /** Where a line at `y` next has more room, in the block's own space: the
4102
+ * nearest bottom edge of a float beside it, or null when none is. */
4103
+ function belowFloats(options, y, height) {
4104
+ const edge = options.floats?.nextEdgeBelow(options.startY + y, height);
4105
+ return edge == null ? null : Math.max(y + 1, edge - options.startY);
4106
+ }
4107
+ /**
4108
+ * Whether a line's first word did not fit its room: the line runs past the
4109
+ * room — a line's width leaves out the white space it ends on, which hangs,
4110
+ * so this is the word, kept whole where `overflow-wrap` says so — or it
4111
+ * ends inside a word — between two letters or digits, where no break is
4112
+ * allowed, so only a line too narrow for the word put one there. Ideographs
4113
+ * and kana break between any two, and are not counted as a word's letters.
4114
+ */
4115
+ function tooNarrow(runs, end, width, room) {
4116
+ let at = 0;
4117
+ let before = '';
4118
+ let after = '';
4119
+ for (const run of runs) {
4120
+ const text = run.text;
4121
+ if (end > at && end <= at + text.length)
4122
+ before = text[end - at - 1];
4123
+ if (end >= at && end < at + text.length)
4124
+ after = text[end - at];
4125
+ at += text.length;
710
4126
  }
4127
+ if (WORD_CHAR.test(before) && WORD_CHAR.test(after))
4128
+ return true;
4129
+ return width > room + 0.5 && before !== '';
711
4130
  }
4131
+ const WORD_CHAR = /^(?![\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}])[\p{L}\p{N}\p{M}]$/u;
712
4132
  function bandAt(options, y, height) {
713
4133
  if (!options.floats)
714
4134
  return { left: 0, right: options.width };
715
- const band = options.floats.bandAt(options.startY + y, height);
4135
+ // Floats narrow the block's own line, where they reach into it; the
4136
+ // formatting context's edges are not this block's, and a block wider than
4137
+ // its context — a fixed width in a narrow body — keeps its lines whole.
4138
+ const band = options.floats.bandAt(options.startY + y, height, options.originX, options.originX + options.width);
716
4139
  return {
717
4140
  left: Math.max(0, band.left - options.originX),
718
4141
  right: Math.min(options.width, band.right - options.originX),