@react-x11/components 0.8.0 → 0.10.0

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