@react-x11/components 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (579) hide show
  1. package/README.md +10 -4
  2. package/dist/charts/node.d.ts +11 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +44 -14
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/charts/render.d.ts +12 -3
  7. package/dist/charts/render.d.ts.map +1 -1
  8. package/dist/charts/render.js +94 -25
  9. package/dist/charts/render.js.map +1 -1
  10. package/dist/code/index.d.ts.map +1 -1
  11. package/dist/code/index.js +27 -24
  12. package/dist/code/index.js.map +1 -1
  13. package/dist/code-editor/completion.d.ts.map +1 -1
  14. package/dist/code-editor/completion.js +3 -2
  15. package/dist/code-editor/completion.js.map +1 -1
  16. package/dist/code-editor/index.d.ts.map +1 -1
  17. package/dist/code-editor/index.js +19 -0
  18. package/dist/code-editor/index.js.map +1 -1
  19. package/dist/code-editor/node.d.ts +177 -7
  20. package/dist/code-editor/node.d.ts.map +1 -1
  21. package/dist/code-editor/node.js +1329 -164
  22. package/dist/code-editor/node.js.map +1 -1
  23. package/dist/code-language/index.d.ts +1 -1
  24. package/dist/code-language/index.d.ts.map +1 -1
  25. package/dist/code-language/index.js +1 -1
  26. package/dist/code-language/index.js.map +1 -1
  27. package/dist/code-language/languages/glsl.d.ts.map +1 -1
  28. package/dist/code-language/languages/glsl.js +10 -2
  29. package/dist/code-language/languages/glsl.js.map +1 -1
  30. package/dist/code-language/languages/javascript.d.ts.map +1 -1
  31. package/dist/code-language/languages/javascript.js +18 -3
  32. package/dist/code-language/languages/javascript.js.map +1 -1
  33. package/dist/code-language/languages/json.d.ts.map +1 -1
  34. package/dist/code-language/languages/json.js +10 -2
  35. package/dist/code-language/languages/json.js.map +1 -1
  36. package/dist/code-language/languages/shell.d.ts.map +1 -1
  37. package/dist/code-language/languages/shell.js +10 -2
  38. package/dist/code-language/languages/shell.js.map +1 -1
  39. package/dist/code-language/languages/sql.d.ts.map +1 -1
  40. package/dist/code-language/languages/sql.js +20 -2
  41. package/dist/code-language/languages/sql.js.map +1 -1
  42. package/dist/code-language/runs.d.ts +31 -0
  43. package/dist/code-language/runs.d.ts.map +1 -1
  44. package/dist/code-language/runs.js +105 -0
  45. package/dist/code-language/runs.js.map +1 -1
  46. package/dist/code-language/stream.d.ts +22 -0
  47. package/dist/code-language/stream.d.ts.map +1 -1
  48. package/dist/code-language/stream.js +259 -9
  49. package/dist/code-language/stream.js.map +1 -1
  50. package/dist/code-language/types.d.ts +5 -4
  51. package/dist/code-language/types.d.ts.map +1 -1
  52. package/dist/codeblock/index.d.ts +26 -2
  53. package/dist/codeblock/index.d.ts.map +1 -1
  54. package/dist/codeblock/index.js +57 -4
  55. package/dist/codeblock/index.js.map +1 -1
  56. package/dist/flow/draw.d.ts +50 -0
  57. package/dist/flow/draw.d.ts.map +1 -1
  58. package/dist/flow/draw.js +132 -33
  59. package/dist/flow/draw.js.map +1 -1
  60. package/dist/flow/gl/color.d.ts +13 -0
  61. package/dist/flow/gl/color.d.ts.map +1 -0
  62. package/dist/flow/gl/color.js +82 -0
  63. package/dist/flow/gl/color.js.map +1 -0
  64. package/dist/flow/gl/index.d.ts +60 -0
  65. package/dist/flow/gl/index.d.ts.map +1 -0
  66. package/dist/flow/gl/index.js +283 -0
  67. package/dist/flow/gl/index.js.map +1 -0
  68. package/dist/flow/gl/pack.d.ts +133 -0
  69. package/dist/flow/gl/pack.d.ts.map +1 -0
  70. package/dist/flow/gl/pack.js +408 -0
  71. package/dist/flow/gl/pack.js.map +1 -0
  72. package/dist/flow/gl/renderer.d.ts +95 -0
  73. package/dist/flow/gl/renderer.d.ts.map +1 -0
  74. package/dist/flow/gl/renderer.js +459 -0
  75. package/dist/flow/gl/renderer.js.map +1 -0
  76. package/dist/flow/gl/shaders.d.ts +79 -0
  77. package/dist/flow/gl/shaders.d.ts.map +1 -0
  78. package/dist/flow/gl/shaders.js +346 -0
  79. package/dist/flow/gl/shaders.js.map +1 -0
  80. package/dist/flow/gl/text.d.ts +318 -0
  81. package/dist/flow/gl/text.d.ts.map +1 -0
  82. package/dist/flow/gl/text.js +1103 -0
  83. package/dist/flow/gl/text.js.map +1 -0
  84. package/dist/flow/index.d.ts +1 -1
  85. package/dist/flow/index.d.ts.map +1 -1
  86. package/dist/flow/index.js +619 -64
  87. package/dist/flow/index.js.map +1 -1
  88. package/dist/flow/node.d.ts +621 -72
  89. package/dist/flow/node.d.ts.map +1 -1
  90. package/dist/flow/node.js +2253 -1018
  91. package/dist/flow/node.js.map +1 -1
  92. package/dist/flow/paint.d.ts +35 -0
  93. package/dist/flow/paint.d.ts.map +1 -0
  94. package/dist/flow/paint.js +393 -0
  95. package/dist/flow/paint.js.map +1 -0
  96. package/dist/flow/paths.d.ts +14 -0
  97. package/dist/flow/paths.d.ts.map +1 -1
  98. package/dist/flow/paths.js +45 -19
  99. package/dist/flow/paths.js.map +1 -1
  100. package/dist/flow/scene.d.ts +584 -0
  101. package/dist/flow/scene.d.ts.map +1 -0
  102. package/dist/flow/scene.js +1453 -0
  103. package/dist/flow/scene.js.map +1 -0
  104. package/dist/flow/types.d.ts +111 -1
  105. package/dist/flow/types.d.ts.map +1 -1
  106. package/dist/html/controls.d.ts +28 -0
  107. package/dist/html/controls.d.ts.map +1 -1
  108. package/dist/html/controls.js +52 -2
  109. package/dist/html/controls.js.map +1 -1
  110. package/dist/html/css/calc.d.ts +14 -0
  111. package/dist/html/css/calc.d.ts.map +1 -0
  112. package/dist/html/css/calc.js +344 -0
  113. package/dist/html/css/calc.js.map +1 -0
  114. package/dist/html/css/cascade.d.ts +262 -6
  115. package/dist/html/css/cascade.d.ts.map +1 -1
  116. package/dist/html/css/cascade.js +1373 -67
  117. package/dist/html/css/cascade.js.map +1 -1
  118. package/dist/html/css/color.d.ts +23 -0
  119. package/dist/html/css/color.d.ts.map +1 -0
  120. package/dist/html/css/color.js +740 -0
  121. package/dist/html/css/color.js.map +1 -0
  122. package/dist/html/css/content.d.ts +92 -0
  123. package/dist/html/css/content.d.ts.map +1 -0
  124. package/dist/html/css/content.js +388 -0
  125. package/dist/html/css/content.js.map +1 -0
  126. package/dist/html/css/counter-styles.d.ts +76 -0
  127. package/dist/html/css/counter-styles.d.ts.map +1 -0
  128. package/dist/html/css/counter-styles.js +893 -0
  129. package/dist/html/css/counter-styles.js.map +1 -0
  130. package/dist/html/css/decode.d.ts +12 -0
  131. package/dist/html/css/decode.d.ts.map +1 -0
  132. package/dist/html/css/decode.js +116 -0
  133. package/dist/html/css/decode.js.map +1 -0
  134. package/dist/html/css/parse.d.ts +110 -10
  135. package/dist/html/css/parse.d.ts.map +1 -1
  136. package/dist/html/css/parse.js +1473 -180
  137. package/dist/html/css/parse.js.map +1 -1
  138. package/dist/html/css/style.d.ts +501 -15
  139. package/dist/html/css/style.d.ts.map +1 -1
  140. package/dist/html/css/style.js +3729 -206
  141. package/dist/html/css/style.js.map +1 -1
  142. package/dist/html/css/ua.d.ts.map +1 -1
  143. package/dist/html/css/ua.js +65 -16
  144. package/dist/html/css/ua.js.map +1 -1
  145. package/dist/html/css/values.d.ts +37 -3
  146. package/dist/html/css/values.d.ts.map +1 -1
  147. package/dist/html/css/values.js +122 -48
  148. package/dist/html/css/values.js.map +1 -1
  149. package/dist/html/css/vars.d.ts +52 -0
  150. package/dist/html/css/vars.d.ts.map +1 -0
  151. package/dist/html/css/vars.js +358 -0
  152. package/dist/html/css/vars.js.map +1 -0
  153. package/dist/html/dom.d.ts +30 -4
  154. package/dist/html/dom.d.ts.map +1 -1
  155. package/dist/html/dom.js +278 -17
  156. package/dist/html/dom.js.map +1 -1
  157. package/dist/html/fonts.d.ts +106 -0
  158. package/dist/html/fonts.d.ts.map +1 -0
  159. package/dist/html/fonts.js +565 -0
  160. package/dist/html/fonts.js.map +1 -0
  161. package/dist/html/gif.d.ts +15 -0
  162. package/dist/html/gif.d.ts.map +1 -0
  163. package/dist/html/gif.js +229 -0
  164. package/dist/html/gif.js.map +1 -0
  165. package/dist/html/index.d.ts +50 -11
  166. package/dist/html/index.d.ts.map +1 -1
  167. package/dist/html/index.js +115 -9
  168. package/dist/html/index.js.map +1 -1
  169. package/dist/html/layout/block.d.ts +226 -3
  170. package/dist/html/layout/block.d.ts.map +1 -1
  171. package/dist/html/layout/block.js +3077 -219
  172. package/dist/html/layout/block.js.map +1 -1
  173. package/dist/html/layout/boxes.d.ts +367 -17
  174. package/dist/html/layout/boxes.d.ts.map +1 -1
  175. package/dist/html/layout/boxes.js +1742 -182
  176. package/dist/html/layout/boxes.js.map +1 -1
  177. package/dist/html/layout/cache.d.ts +58 -0
  178. package/dist/html/layout/cache.d.ts.map +1 -0
  179. package/dist/html/layout/cache.js +224 -0
  180. package/dist/html/layout/cache.js.map +1 -0
  181. package/dist/html/layout/collapse.d.ts +45 -0
  182. package/dist/html/layout/collapse.d.ts.map +1 -0
  183. package/dist/html/layout/collapse.js +246 -0
  184. package/dist/html/layout/collapse.js.map +1 -0
  185. package/dist/html/layout/css-grid.d.ts +7 -0
  186. package/dist/html/layout/css-grid.d.ts.map +1 -0
  187. package/dist/html/layout/css-grid.js +900 -0
  188. package/dist/html/layout/css-grid.js.map +1 -0
  189. package/dist/html/layout/flex.d.ts.map +1 -1
  190. package/dist/html/layout/flex.js +1025 -89
  191. package/dist/html/layout/flex.js.map +1 -1
  192. package/dist/html/layout/floats.d.ts +45 -6
  193. package/dist/html/layout/floats.d.ts.map +1 -1
  194. package/dist/html/layout/floats.js +113 -14
  195. package/dist/html/layout/floats.js.map +1 -1
  196. package/dist/html/layout/grid-lines.d.ts +39 -0
  197. package/dist/html/layout/grid-lines.d.ts.map +1 -0
  198. package/dist/html/layout/grid-lines.js +119 -0
  199. package/dist/html/layout/grid-lines.js.map +1 -0
  200. package/dist/html/layout/grid.d.ts +53 -0
  201. package/dist/html/layout/grid.d.ts.map +1 -0
  202. package/dist/html/layout/grid.js +153 -0
  203. package/dist/html/layout/grid.js.map +1 -0
  204. package/dist/html/layout/inline.d.ts +154 -1
  205. package/dist/html/layout/inline.d.ts.map +1 -1
  206. package/dist/html/layout/inline.js +3598 -175
  207. package/dist/html/layout/inline.js.map +1 -1
  208. package/dist/html/layout/shaping.d.ts +6 -0
  209. package/dist/html/layout/shaping.d.ts.map +1 -0
  210. package/dist/html/layout/shaping.js +100 -0
  211. package/dist/html/layout/shaping.js.map +1 -0
  212. package/dist/html/layout/table.d.ts +3 -0
  213. package/dist/html/layout/table.d.ts.map +1 -1
  214. package/dist/html/layout/table.js +681 -175
  215. package/dist/html/layout/table.js.map +1 -1
  216. package/dist/html/layout/tracks.d.ts +44 -0
  217. package/dist/html/layout/tracks.d.ts.map +1 -0
  218. package/dist/html/layout/tracks.js +349 -0
  219. package/dist/html/layout/tracks.js.map +1 -0
  220. package/dist/html/node.d.ts +169 -4
  221. package/dist/html/node.d.ts.map +1 -1
  222. package/dist/html/node.js +1429 -137
  223. package/dist/html/node.js.map +1 -1
  224. package/dist/html/paint.d.ts +92 -3
  225. package/dist/html/paint.d.ts.map +1 -1
  226. package/dist/html/paint.js +3635 -107
  227. package/dist/html/paint.js.map +1 -1
  228. package/dist/html/resources.d.ts +60 -14
  229. package/dist/html/resources.d.ts.map +1 -1
  230. package/dist/html/resources.js +122 -36
  231. package/dist/html/resources.js.map +1 -1
  232. package/dist/html/surfaces.d.ts +23 -0
  233. package/dist/html/surfaces.d.ts.map +1 -0
  234. package/dist/html/surfaces.js +68 -0
  235. package/dist/html/surfaces.js.map +1 -0
  236. package/dist/html/svg.d.ts +80 -0
  237. package/dist/html/svg.d.ts.map +1 -0
  238. package/dist/html/svg.js +442 -0
  239. package/dist/html/svg.js.map +1 -0
  240. package/dist/html/url.d.ts +23 -0
  241. package/dist/html/url.d.ts.map +1 -0
  242. package/dist/html/url.js +80 -0
  243. package/dist/html/url.js.map +1 -0
  244. package/dist/index.d.ts +1 -1
  245. package/dist/index.d.ts.map +1 -1
  246. package/dist/index.js.map +1 -1
  247. package/dist/internal/codelines.d.ts +42 -0
  248. package/dist/internal/codelines.d.ts.map +1 -0
  249. package/dist/internal/codelines.js +170 -0
  250. package/dist/internal/codelines.js.map +1 -0
  251. package/dist/internal/collate.d.ts +3 -0
  252. package/dist/internal/collate.d.ts.map +1 -0
  253. package/dist/internal/collate.js +11 -0
  254. package/dist/internal/collate.js.map +1 -0
  255. package/dist/internal/markdown/ast.d.ts +11 -0
  256. package/dist/internal/markdown/ast.d.ts.map +1 -1
  257. package/dist/internal/markdown/parse.d.ts.map +1 -1
  258. package/dist/internal/markdown/parse.js +109 -8
  259. package/dist/internal/markdown/parse.js.map +1 -1
  260. package/dist/internal/prewarm.d.ts +18 -0
  261. package/dist/internal/prewarm.d.ts.map +1 -0
  262. package/dist/internal/prewarm.js +42 -0
  263. package/dist/internal/prewarm.js.map +1 -0
  264. package/dist/internal/sdf.d.ts +20 -0
  265. package/dist/internal/sdf.d.ts.map +1 -0
  266. package/dist/internal/sdf.js +216 -0
  267. package/dist/internal/sdf.js.map +1 -0
  268. package/dist/internal/splice.d.ts +12 -0
  269. package/dist/internal/splice.d.ts.map +1 -0
  270. package/dist/internal/splice.js +23 -0
  271. package/dist/internal/splice.js.map +1 -0
  272. package/dist/internal/window.d.ts +12 -0
  273. package/dist/internal/window.d.ts.map +1 -1
  274. package/dist/internal/window.js +23 -6
  275. package/dist/internal/window.js.map +1 -1
  276. package/dist/maps/anchors.d.ts +6 -1
  277. package/dist/maps/anchors.d.ts.map +1 -1
  278. package/dist/maps/anchors.js +260 -1
  279. package/dist/maps/anchors.js.map +1 -1
  280. package/dist/maps/controller.d.ts +57 -0
  281. package/dist/maps/controller.d.ts.map +1 -1
  282. package/dist/maps/controller.js +97 -18
  283. package/dist/maps/controller.js.map +1 -1
  284. package/dist/maps/gl/buckets.d.ts +5 -1
  285. package/dist/maps/gl/buckets.d.ts.map +1 -1
  286. package/dist/maps/gl/buckets.js +5 -2
  287. package/dist/maps/gl/buckets.js.map +1 -1
  288. package/dist/maps/gl/build-worker.d.ts +2 -0
  289. package/dist/maps/gl/build-worker.d.ts.map +1 -1
  290. package/dist/maps/gl/build-worker.js +1 -1
  291. package/dist/maps/gl/build-worker.js.map +1 -1
  292. package/dist/maps/gl/cover.d.ts +23 -4
  293. package/dist/maps/gl/cover.d.ts.map +1 -1
  294. package/dist/maps/gl/cover.js +61 -48
  295. package/dist/maps/gl/cover.js.map +1 -1
  296. package/dist/maps/gl/placement.d.ts +21 -8
  297. package/dist/maps/gl/placement.d.ts.map +1 -1
  298. package/dist/maps/gl/placement.js +133 -58
  299. package/dist/maps/gl/placement.js.map +1 -1
  300. package/dist/maps/gl/renderer.d.ts +36 -4
  301. package/dist/maps/gl/renderer.d.ts.map +1 -1
  302. package/dist/maps/gl/renderer.js +81 -24
  303. package/dist/maps/gl/renderer.js.map +1 -1
  304. package/dist/maps/gl/shaders.d.ts +18 -17
  305. package/dist/maps/gl/shaders.d.ts.map +1 -1
  306. package/dist/maps/gl/shaders.js +34 -32
  307. package/dist/maps/gl/shaders.js.map +1 -1
  308. package/dist/maps/gl/store.d.ts +10 -0
  309. package/dist/maps/gl/store.d.ts.map +1 -1
  310. package/dist/maps/gl/store.js +47 -4
  311. package/dist/maps/gl/store.js.map +1 -1
  312. package/dist/maps/gl/text.d.ts +153 -24
  313. package/dist/maps/gl/text.d.ts.map +1 -1
  314. package/dist/maps/gl/text.js +379 -63
  315. package/dist/maps/gl/text.js.map +1 -1
  316. package/dist/maps/gl/view.d.ts +293 -2
  317. package/dist/maps/gl/view.d.ts.map +1 -1
  318. package/dist/maps/gl/view.js +317 -82
  319. package/dist/maps/gl/view.js.map +1 -1
  320. package/dist/maps/icons.d.ts +32 -0
  321. package/dist/maps/icons.d.ts.map +1 -0
  322. package/dist/maps/icons.js +151 -0
  323. package/dist/maps/icons.js.map +1 -0
  324. package/dist/maps/index.d.ts +2 -0
  325. package/dist/maps/index.d.ts.map +1 -1
  326. package/dist/maps/index.js +2 -0
  327. package/dist/maps/index.js.map +1 -1
  328. package/dist/maps/labels.d.ts +56 -4
  329. package/dist/maps/labels.d.ts.map +1 -1
  330. package/dist/maps/labels.js +213 -46
  331. package/dist/maps/labels.js.map +1 -1
  332. package/dist/maps/node.d.ts +17 -0
  333. package/dist/maps/node.d.ts.map +1 -1
  334. package/dist/maps/node.js +64 -30
  335. package/dist/maps/node.js.map +1 -1
  336. package/dist/maps/proj.d.ts +48 -0
  337. package/dist/maps/proj.d.ts.map +1 -1
  338. package/dist/maps/proj.js +73 -0
  339. package/dist/maps/proj.js.map +1 -1
  340. package/dist/maps/sources.d.ts +5 -3
  341. package/dist/maps/sources.d.ts.map +1 -1
  342. package/dist/maps/sources.js +5 -3
  343. package/dist/maps/sources.js.map +1 -1
  344. package/dist/maps/style.d.ts +35 -0
  345. package/dist/maps/style.d.ts.map +1 -1
  346. package/dist/maps/style.js.map +1 -1
  347. package/dist/maps/styles.d.ts +12 -0
  348. package/dist/maps/styles.d.ts.map +1 -1
  349. package/dist/maps/styles.js +120 -6
  350. package/dist/maps/styles.js.map +1 -1
  351. package/dist/maps/tiles.d.ts +31 -11
  352. package/dist/maps/tiles.d.ts.map +1 -1
  353. package/dist/maps/tiles.js +61 -27
  354. package/dist/maps/tiles.js.map +1 -1
  355. package/dist/maps/types.d.ts +28 -4
  356. package/dist/maps/types.d.ts.map +1 -1
  357. package/dist/markdown/index.d.ts.map +1 -1
  358. package/dist/markdown/index.js +209 -68
  359. package/dist/markdown/index.js.map +1 -1
  360. package/dist/markdown/spans.d.ts.map +1 -1
  361. package/dist/markdown/spans.js +6 -0
  362. package/dist/markdown/spans.js.map +1 -1
  363. package/dist/qml/containers.d.ts.map +1 -1
  364. package/dist/qml/containers.js +6 -1
  365. package/dist/qml/containers.js.map +1 -1
  366. package/dist/qml/interaction.d.ts.map +1 -1
  367. package/dist/qml/interaction.js +38 -5
  368. package/dist/qml/interaction.js.map +1 -1
  369. package/dist/qml/layouts.d.ts.map +1 -1
  370. package/dist/qml/layouts.js +21 -6
  371. package/dist/qml/layouts.js.map +1 -1
  372. package/dist/qml/qtquick.d.ts.map +1 -1
  373. package/dist/qml/qtquick.js +9 -2
  374. package/dist/qml/qtquick.js.map +1 -1
  375. package/dist/qml/react.d.ts.map +1 -1
  376. package/dist/qml/react.js +9 -7
  377. package/dist/qml/react.js.map +1 -1
  378. package/dist/reorder/clock.d.ts +7 -0
  379. package/dist/reorder/clock.d.ts.map +1 -0
  380. package/dist/reorder/clock.js +26 -0
  381. package/dist/reorder/clock.js.map +1 -0
  382. package/dist/reorder/index.d.ts +25 -3
  383. package/dist/reorder/index.d.ts.map +1 -1
  384. package/dist/reorder/index.js +64 -19
  385. package/dist/reorder/index.js.map +1 -1
  386. package/dist/rich-text-editor/index.d.ts +0 -11
  387. package/dist/rich-text-editor/index.d.ts.map +1 -1
  388. package/dist/rich-text-editor/index.js +38 -9
  389. package/dist/rich-text-editor/index.js.map +1 -1
  390. package/dist/rich-text-editor/keymap.d.ts.map +1 -1
  391. package/dist/rich-text-editor/keymap.js +2 -1
  392. package/dist/rich-text-editor/keymap.js.map +1 -1
  393. package/dist/rich-text-editor/keys.d.ts +17 -6
  394. package/dist/rich-text-editor/keys.d.ts.map +1 -1
  395. package/dist/rich-text-editor/keys.js +135 -26
  396. package/dist/rich-text-editor/keys.js.map +1 -1
  397. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  398. package/dist/rich-text-editor/markdown.js +30 -1
  399. package/dist/rich-text-editor/markdown.js.map +1 -1
  400. package/dist/rich-text-editor/marks.d.ts +9 -0
  401. package/dist/rich-text-editor/marks.d.ts.map +1 -0
  402. package/dist/rich-text-editor/marks.js +83 -0
  403. package/dist/rich-text-editor/marks.js.map +1 -0
  404. package/dist/rich-text-editor/render.d.ts.map +1 -1
  405. package/dist/rich-text-editor/render.js +28 -10
  406. package/dist/rich-text-editor/render.js.map +1 -1
  407. package/dist/rich-text-editor/replace.d.ts +13 -0
  408. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  409. package/dist/rich-text-editor/replace.js +64 -0
  410. package/dist/rich-text-editor/replace.js.map +1 -0
  411. package/dist/rich-text-editor/toolbar.d.ts.map +1 -1
  412. package/dist/rich-text-editor/toolbar.js +1 -1
  413. package/dist/rich-text-editor/toolbar.js.map +1 -1
  414. package/dist/rich-text-editor/virtual.d.ts.map +1 -1
  415. package/dist/rich-text-editor/virtual.js +17 -0
  416. package/dist/rich-text-editor/virtual.js.map +1 -1
  417. package/dist/richtext/node.d.ts +46 -2
  418. package/dist/richtext/node.d.ts.map +1 -1
  419. package/dist/richtext/node.js +187 -4
  420. package/dist/richtext/node.js.map +1 -1
  421. package/dist/richtext/runs.d.ts +13 -8
  422. package/dist/richtext/runs.d.ts.map +1 -1
  423. package/dist/richtext/runs.js +18 -8
  424. package/dist/richtext/runs.js.map +1 -1
  425. package/dist/table/index.d.ts.map +1 -1
  426. package/dist/table/index.js +23 -0
  427. package/dist/table/index.js.map +1 -1
  428. package/dist/table/rows.d.ts +3 -2
  429. package/dist/table/rows.d.ts.map +1 -1
  430. package/dist/table/rows.js +20 -6
  431. package/dist/table/rows.js.map +1 -1
  432. package/dist/terminal/index.d.ts.map +1 -1
  433. package/dist/terminal/index.js +4 -0
  434. package/dist/terminal/index.js.map +1 -1
  435. package/dist/terminal/vt/pty.d.ts +13 -2
  436. package/dist/terminal/vt/pty.d.ts.map +1 -1
  437. package/dist/terminal/vt/pty.js +18 -3
  438. package/dist/terminal/vt/pty.js.map +1 -1
  439. package/dist/terminal-output/index.d.ts.map +1 -1
  440. package/dist/terminal-output/index.js +103 -23
  441. package/dist/terminal-output/index.js.map +1 -1
  442. package/dist/terminal-output/runs.d.ts +4 -1
  443. package/dist/terminal-output/runs.d.ts.map +1 -1
  444. package/dist/terminal-output/runs.js +5 -1
  445. package/dist/terminal-output/runs.js.map +1 -1
  446. package/dist/three/canvas.d.ts.map +1 -1
  447. package/dist/three/canvas.js +85 -7
  448. package/dist/three/canvas.js.map +1 -1
  449. package/dist/three/events.d.ts +22 -4
  450. package/dist/three/events.d.ts.map +1 -1
  451. package/dist/three/events.js +34 -4
  452. package/dist/three/events.js.map +1 -1
  453. package/dist/tree/index.d.ts.map +1 -1
  454. package/dist/tree/index.js +17 -0
  455. package/dist/tree/index.js.map +1 -1
  456. package/package.json +42 -33
  457. package/src/charts/node.ts +62 -26
  458. package/src/charts/render.ts +120 -30
  459. package/src/code/index.ts +36 -32
  460. package/src/code-editor/completion.ts +3 -2
  461. package/src/code-editor/index.ts +19 -0
  462. package/src/code-editor/node.ts +1574 -184
  463. package/src/code-language/index.ts +1 -1
  464. package/src/code-language/languages/glsl.ts +12 -2
  465. package/src/code-language/languages/javascript.ts +21 -4
  466. package/src/code-language/languages/json.ts +12 -2
  467. package/src/code-language/languages/shell.ts +12 -2
  468. package/src/code-language/languages/sql.ts +22 -2
  469. package/src/code-language/runs.ts +111 -1
  470. package/src/code-language/stream.ts +257 -17
  471. package/src/code-language/types.ts +5 -4
  472. package/src/codeblock/index.ts +68 -4
  473. package/src/flow/draw.ts +167 -41
  474. package/src/flow/gl/color.ts +85 -0
  475. package/src/flow/gl/index.ts +342 -0
  476. package/src/flow/gl/pack.ts +585 -0
  477. package/src/flow/gl/renderer.ts +679 -0
  478. package/src/flow/gl/shaders.ts +371 -0
  479. package/src/flow/gl/text.ts +1295 -0
  480. package/src/flow/index.ts +794 -67
  481. package/src/flow/node.ts +2496 -1256
  482. package/src/flow/paint.ts +511 -0
  483. package/src/flow/paths.ts +80 -15
  484. package/src/flow/scene.ts +2199 -0
  485. package/src/flow/types.ts +113 -1
  486. package/src/html/controls.ts +73 -2
  487. package/src/html/css/calc.ts +385 -0
  488. package/src/html/css/cascade.ts +1481 -62
  489. package/src/html/css/color.ts +755 -0
  490. package/src/html/css/content.ts +417 -0
  491. package/src/html/css/counter-styles.ts +973 -0
  492. package/src/html/css/decode.ts +130 -0
  493. package/src/html/css/parse.ts +1514 -160
  494. package/src/html/css/style.ts +4131 -223
  495. package/src/html/css/ua.ts +66 -17
  496. package/src/html/css/values.ts +150 -50
  497. package/src/html/css/vars.ts +348 -0
  498. package/src/html/dom.ts +284 -17
  499. package/src/html/fonts.ts +641 -0
  500. package/src/html/gif.ts +233 -0
  501. package/src/html/index.ts +180 -20
  502. package/src/html/layout/block.ts +3560 -208
  503. package/src/html/layout/boxes.ts +2141 -190
  504. package/src/html/layout/cache.ts +289 -0
  505. package/src/html/layout/collapse.ts +317 -0
  506. package/src/html/layout/css-grid.ts +1063 -0
  507. package/src/html/layout/flex.ts +1115 -106
  508. package/src/html/layout/floats.ts +128 -14
  509. package/src/html/layout/grid-lines.ts +141 -0
  510. package/src/html/layout/grid.ts +181 -0
  511. package/src/html/layout/inline.ts +4187 -192
  512. package/src/html/layout/shaping.ts +140 -0
  513. package/src/html/layout/table.ts +696 -186
  514. package/src/html/layout/tracks.ts +437 -0
  515. package/src/html/node.ts +1585 -156
  516. package/src/html/paint.ts +4582 -267
  517. package/src/html/resources.ts +161 -50
  518. package/src/html/surfaces.ts +81 -0
  519. package/src/html/svg.ts +504 -0
  520. package/src/html/url.ts +86 -0
  521. package/src/index.ts +2 -0
  522. package/src/internal/codelines.ts +200 -0
  523. package/src/internal/collate.ts +12 -0
  524. package/src/internal/markdown/ast.ts +11 -0
  525. package/src/internal/markdown/parse.ts +154 -11
  526. package/src/internal/prewarm.ts +45 -0
  527. package/src/internal/sdf.ts +227 -0
  528. package/src/internal/splice.ts +25 -0
  529. package/src/internal/window.ts +36 -5
  530. package/src/maps/anchors.ts +307 -1
  531. package/src/maps/controller.ts +116 -31
  532. package/src/maps/gl/buckets.ts +5 -1
  533. package/src/maps/gl/build-worker.ts +13 -2
  534. package/src/maps/gl/cover.ts +79 -44
  535. package/src/maps/gl/placement.ts +221 -69
  536. package/src/maps/gl/renderer.ts +104 -20
  537. package/src/maps/gl/shaders.ts +35 -32
  538. package/src/maps/gl/store.ts +66 -17
  539. package/src/maps/gl/text.ts +444 -71
  540. package/src/maps/gl/view.ts +361 -96
  541. package/src/maps/icons.ts +183 -0
  542. package/src/maps/index.ts +4 -0
  543. package/src/maps/labels.ts +269 -49
  544. package/src/maps/node.ts +82 -49
  545. package/src/maps/proj.ts +87 -0
  546. package/src/maps/sources.ts +5 -3
  547. package/src/maps/style.ts +32 -0
  548. package/src/maps/styles.ts +215 -4
  549. package/src/maps/tiles.ts +64 -31
  550. package/src/maps/types.ts +28 -4
  551. package/src/markdown/index.ts +293 -92
  552. package/src/markdown/spans.ts +5 -0
  553. package/src/qml/containers.tsx +6 -1
  554. package/src/qml/interaction.tsx +42 -5
  555. package/src/qml/layouts.tsx +21 -6
  556. package/src/qml/qtquick.tsx +9 -2
  557. package/src/qml/react.tsx +7 -5
  558. package/src/reorder/clock.ts +27 -0
  559. package/src/reorder/index.ts +100 -27
  560. package/src/rich-text-editor/index.ts +37 -12
  561. package/src/rich-text-editor/keymap.ts +1 -1
  562. package/src/rich-text-editor/keys.ts +135 -26
  563. package/src/rich-text-editor/markdown.ts +36 -1
  564. package/src/rich-text-editor/marks.ts +101 -0
  565. package/src/rich-text-editor/render.ts +49 -12
  566. package/src/rich-text-editor/replace.ts +85 -0
  567. package/src/rich-text-editor/toolbar.ts +1 -1
  568. package/src/rich-text-editor/virtual.ts +25 -0
  569. package/src/richtext/node.ts +235 -14
  570. package/src/richtext/runs.ts +23 -12
  571. package/src/table/index.ts +29 -0
  572. package/src/table/rows.ts +18 -8
  573. package/src/terminal/index.ts +4 -0
  574. package/src/terminal/vt/pty.ts +16 -2
  575. package/src/terminal-output/index.ts +129 -31
  576. package/src/terminal-output/runs.ts +16 -2
  577. package/src/three/canvas.ts +102 -8
  578. package/src/three/events.ts +49 -9
  579. package/src/tree/index.ts +19 -0
package/src/html/node.ts CHANGED
@@ -48,27 +48,61 @@ import type { Style } from 'react-x11/style';
48
48
  import type { Element } from 'domhandler';
49
49
 
50
50
  import { codePointAtOffset, codeUnitOffsets } from '../internal/text.js';
51
- import { attr, HtmlSource, isElement, tagOf } from './dom.js';
51
+ import { attr, HtmlSource, imageUrlOf, isElement, tagOf } from './dom.js';
52
52
  import type { Document } from './dom.js';
53
53
  import { Cascade } from './css/cascade.js';
54
- import { parseStylesheet } from './css/parse.js';
54
+ import type { MetricFace } from './css/cascade.js';
55
+ import { mediaMatches, parseStylesheet } from './css/parse.js';
55
56
  import type { Stylesheet } from './css/parse.js';
56
57
  import { uaStylesheet } from './css/ua.js';
57
- import type { RootLook } from './css/style.js';
58
- import { buildBoxes } from './layout/boxes.js';
59
- import type { Box, BoxTree, ReplacedKind } from './layout/boxes.js';
58
+ import type { ComputedStyle, RootLook } from './css/style.js';
59
+ import {
60
+ BOX_RAISES,
61
+ buildBoxes,
62
+ CONTENT_IMAGES,
63
+ LINE_BOX_RAISES,
64
+ SHIFTED_LINES,
65
+ } from './layout/boxes.js';
66
+ import type {
67
+ Box,
68
+ BoxTree,
69
+ LineBox,
70
+ LineText,
71
+ ReplacedKind,
72
+ TextLayoutLike,
73
+ } from './layout/boxes.js';
60
74
  import { layoutDocument } from './layout/block.js';
75
+ import { TextLayoutCache } from './layout/cache.js';
76
+ import { shapingSafe } from './layout/shaping.js';
77
+ import { SurfaceCache } from './surfaces.js';
78
+ import { faceExtentOf, inlineDecoration, runFor } from './layout/inline.js';
61
79
  import type { FontsLike } from './layout/inline.js';
80
+ import type { TextRun } from '../richtext/index.js';
62
81
  // Through the inline module rather than a second cache: the offsets table for
63
82
  // a layout is built once, on the first selection that needs it.
64
- import { layoutOffsets as layoutOffsetsOf } from './layout/inline.js';
83
+ import {
84
+ documentOffsetOf,
85
+ layoutOffsetOf,
86
+ layoutOffsets as layoutOffsetsOf,
87
+ } from './layout/inline.js';
65
88
  import { lineBands as bandsFor } from '../richtext/runs.js';
66
- import { paintDocument, queryChildIndex } from './paint.js';
89
+ import {
90
+ clipsOverflow,
91
+ containingBlockOf,
92
+ hasRect,
93
+ holds,
94
+ holdsAbsolute,
95
+ paintDocument,
96
+ queryChildIndex,
97
+ } from './paint.js';
67
98
  import type { PaintContext } from './paint.js';
68
99
  import { controlRectsOf, measureControl } from './controls.js';
69
- import type { ControlRect } from './controls.js';
100
+ import type { BareField, ControlRect } from './controls.js';
70
101
  import { ResourceStore } from './resources.js';
71
102
  import type { ResourceRequest, ResourceResult } from './resources.js';
103
+ import { WebFonts } from './fonts.js';
104
+ import type { DeclaredFace } from './fonts.js';
105
+ import { resolveUrl, UrlResolver } from './url.js';
72
106
 
73
107
  /** The element name — registration key, `node.kind` and JSX tag alike. */
74
108
  export const ELEMENT = 'htmlview';
@@ -95,6 +129,12 @@ export interface HtmlViewProps {
95
129
  complete?: boolean;
96
130
  /** Author stylesheets applied after the document's own. */
97
131
  stylesheet?: string | string[];
132
+ /** The encoding the host decoded `source` from: what a stylesheet handed
133
+ * over as bytes falls back to. */
134
+ charset?: string;
135
+ /** The URL the document came from, which its relative URLs resolve
136
+ * against — see `url.ts`. */
137
+ baseUrl?: string | null;
98
138
  look: RootLook;
99
139
  selectionColor?: string;
100
140
  onResource?: (
@@ -118,11 +158,48 @@ export function registerHtmlView(): void {
118
158
  // `source` and `look` are this element's own vocabulary and neither is a
119
159
  // style name today; declaring them keeps the DEV flat-style-prop
120
160
  // assertion honest if core's vocabulary grows underneath us.
121
- semanticNames: ['source', 'look', 'stylesheet', 'complete'],
161
+ semanticNames: [
162
+ 'source',
163
+ 'look',
164
+ 'stylesheet',
165
+ 'charset',
166
+ 'complete',
167
+ 'baseUrl',
168
+ ],
122
169
  childrenAllowed: false,
123
170
  });
124
171
  }
125
172
 
173
+ /** How many widths' sizes a document keeps (`HtmlViewNode._sizeAt`): the
174
+ * two a frame of a resize asks, and a little slack for a drag that turns
175
+ * back. */
176
+ const SIZES_KEPT = 4;
177
+
178
+ /**
179
+ * A size in the whole pixels core lays out in, rounded up — less the float
180
+ * noise a sum of Yoga's single-precision positions carries: a page exactly
181
+ * `100vh` tall came to 737.0000076 under a 737-pixel viewport, and a
182
+ * rounding that took it for 738 scrolled it by a pixel under a scrollbar.
183
+ */
184
+ function wholePixels(n: number): number {
185
+ return Math.max(0, Math.ceil(n - 1 / 64));
186
+ }
187
+
188
+ /** How deep `@import`s are followed: an import in an import in an import is
189
+ * a stylesheet; sixteen of them is a loop that changes its URL each time. */
190
+ const MAX_IMPORT_DEPTH = 16;
191
+
192
+ /** A scheme at the start: an absolute URL, a base that means something. */
193
+ const ABSOLUTE_URL = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
194
+
195
+ /** What warming a document's faces needs of the fonts: ntk's
196
+ * `FontManager#prewarm`, which names faces from the release that added
197
+ * them (8.14) and warms the family's four before it. An engine without it
198
+ * has nothing to look up. */
199
+ interface WarmingFonts {
200
+ prewarm?(family: string, faces?: { weight: number; style: string }[]): void;
201
+ }
202
+
126
203
  /** What changed, and therefore how far back up the pipeline to go. */
127
204
  const enum Stale {
128
205
  Nothing = 0,
@@ -134,16 +211,43 @@ const enum Stale {
134
211
 
135
212
  export class HtmlViewNode extends Node {
136
213
  private _source = new HtmlSource();
214
+ /** Where the document's relative URLs resolve: its `<base href>` against
215
+ * the `baseUrl` prop, or the prop, or nowhere (`url.ts`). */
216
+ private _urls = new UrlResolver();
137
217
  private _resources: ResourceStore;
218
+ /** The families the document's `@font-face` rules declare (`fonts.ts`). */
219
+ private _webFonts: WebFonts;
220
+ /** The sheets the last restyle read, and the cascade built from them. */
221
+ private _sheetsRead: SheetsRead | null = null;
222
+ /** Blurred shadows, drawn once each. */
223
+ private _shadowCache: SurfaceCache | null = null;
138
224
  private _cascade: Cascade | null = null;
139
225
  private _tree: BoxTree | null = null;
226
+ /** The faces `_warmFaces` has asked the font matcher for, and of which
227
+ * fonts. */
228
+ private _warmedFaces = new Set<string>();
229
+ private _warmedFor: unknown = null;
140
230
  private _stale: Stale = Stale.Everything;
141
231
  /** The width the document was last laid out at. Not `_laidOutAt`: core's
142
232
  * `Node` has a method of that name, which a field would hide. */
143
233
  private _laidOutWidth = -1;
144
234
  private _mediaBand = -1;
235
+ /** The viewport the box tree's styles were computed at, for the ones
236
+ * that read it (`vw`, `vh`). */
237
+ private _styledWidth = -1;
238
+ private _styledHeight = -1;
145
239
  private _documentHeight = 0;
146
240
  private _documentWidth = 0;
241
+ /** The viewport height the document was last laid out under, and whether
242
+ * that layout read it (`LayoutResult.readsViewportHeight`). */
243
+ private _laidOutUnder = -1;
244
+ private _layoutReadsViewport = false;
245
+ /** Whether core is asking for this element's size right now — inside
246
+ * its layout pass, where a node must not ask to be measured again. */
247
+ private _measuring = false;
248
+ /** What the document came to at the widths it was last laid out at, while
249
+ * nothing a layout reads has changed (`_sizeAt`). */
250
+ private _sizes = new Map<number, { width: number; height: number }>();
147
251
  private _textPoints: number[] | null = null;
148
252
  /** Whether code points and code units are the same index — true unless
149
253
  * the text carries surrogate pairs. null until checked. */
@@ -155,14 +259,32 @@ export class HtmlViewNode extends Node {
155
259
 
156
260
  constructor(props: Record<string, unknown>, app: NtkApp) {
157
261
  super(ELEMENT, props, app);
262
+ const ask = (request: ResourceRequest) =>
263
+ this._props().onResource?.(request) ?? null;
158
264
  this._resources = new ResourceStore(
159
- (request) => this._props().onResource?.(request) ?? null,
265
+ ask,
266
+ (what) => {
267
+ // A late stylesheet is a new cascade: its rules, what it imports,
268
+ // the faces it declares. A late image changes intrinsic sizes, so
269
+ // the box tree is what has to be rebuilt — not merely repainted.
270
+ // Either arriving after first paint is the ordinary case, not an
271
+ // error path: a host on a network answers every request that way.
272
+ this._invalidate(what === 'stylesheet' ? Stale.Style : Stale.Boxes);
273
+ },
274
+ this._urls,
275
+ );
276
+ this._webFonts = new WebFonts(
277
+ app,
278
+ ask,
160
279
  () => {
161
- // A late resource changes intrinsic sizes, so the box tree is what
162
- // has to be rebuilt — not merely repainted. An image that arrives
163
- // after first paint is the ordinary case, not an error path.
280
+ // A face arrived: the family lists change, so the styles do, and
281
+ // every text layout kept from before was set in the face it
282
+ // replaces — under the same list, where a family of the document's
283
+ // had loaded one weight and now has another.
284
+ this._layouts = null;
164
285
  this._invalidate(Stale.Boxes);
165
286
  },
287
+ 'sans-serif',
166
288
  );
167
289
  this._read();
168
290
  }
@@ -256,9 +378,24 @@ export class HtmlViewNode extends Node {
256
378
  props.onDocument?.(this._source.document);
257
379
  this._reportedDomRevision = props.domRevision ?? 0;
258
380
  }
381
+ this._updateBase();
259
382
  this._sweep();
260
383
  }
261
384
 
385
+ /**
386
+ * The document's base URL: its first `<base href>`, resolved against the
387
+ * `baseUrl` prop, or the prop — and only an absolute one, since a relative
388
+ * base resolves nothing. Everything resolved against the old one is
389
+ * stale when it moves, the sheets' URLs with it, so the cascade is.
390
+ */
391
+ private _updateBase(): void {
392
+ const given = this._props().baseUrl || null;
393
+ const href = this._source.facts().base;
394
+ let base = href ? resolveUrl(href, given) : given;
395
+ if (base && !ABSOLUTE_URL.test(base)) base = null;
396
+ if (this._urls.setBase(base)) this._invalidate(Stale.Style);
397
+ }
398
+
262
399
  /**
263
400
  * Hand the document's own declarations to the host: every `<script>` once,
264
401
  * and every resource that has not been asked for.
@@ -286,7 +423,7 @@ export class HtmlViewNode extends Node {
286
423
  }
287
424
  for (const el of facts.resources) {
288
425
  const tag = tagOf(el);
289
- const url = tag === 'link' ? attr(el, 'href') : attr(el, 'src');
426
+ const url = tag === 'link' ? attr(el, 'href') : imageUrlOf(el);
290
427
  if (!url) continue;
291
428
  this._resources.request({
292
429
  url,
@@ -296,49 +433,190 @@ export class HtmlViewNode extends Node {
296
433
  }
297
434
  }
298
435
 
436
+ /**
437
+ * Ask for every `background-image` the styles name. Known only once the
438
+ * cascade has run, where an `<img>` is known from the markup; the store
439
+ * asks once a URL, so a tree built again asks nothing new.
440
+ */
441
+ private _requestBackgrounds(tree: BoxTree): void {
442
+ for (const box of tree.backgrounds) {
443
+ if (!box.el) continue;
444
+ // each layer's, where there is more than one
445
+ for (const url of box.style.backgroundImages ?? [
446
+ box.style.backgroundImage,
447
+ ]) {
448
+ if (typeof url === 'string') {
449
+ this._resources.request({ url, kind: 'image', element: box.el });
450
+ }
451
+ }
452
+ const border = box.style.borderImage.source;
453
+ if (typeof border === 'string') {
454
+ this._resources.request({
455
+ url: border,
456
+ kind: 'image',
457
+ element: box.el,
458
+ });
459
+ }
460
+ }
461
+ // and every image generated content names, which is only known there
462
+ // too
463
+ for (const { url, element } of tree.contentImages) {
464
+ this._resources.request({ url, kind: 'image', element });
465
+ }
466
+ }
467
+
468
+ /** Whether an image generated content names arrived as it was asked for,
469
+ * the tree built without its size: a host that answers at once is told
470
+ * nothing later, where an `<img>` is asked for before the build. */
471
+ private _contentImagesArrived(tree: BoxTree): boolean {
472
+ for (const { url, sized } of tree.contentImages) {
473
+ if (!sized && this._resources.imageSize(url)) return true;
474
+ }
475
+ return false;
476
+ }
477
+
478
+ /**
479
+ * Ask the font matcher for the faces the document's text is set in, as
480
+ * soon as its boxes say which and while its layout is still ahead. A face
481
+ * nothing had warmed was a synchronous fc-match inside that layout: the
482
+ * benchmark report's `th`, at 600, waited 38 ms before its first paint.
483
+ * One child answers every face a call names, off the event loop (ntk's
484
+ * `FontManager#prewarm`), and a layout reaching a face still on its way
485
+ * takes the answer rather than asking again. Each face is asked once, by
486
+ * the family, weight and style the layout hands the engine.
487
+ */
488
+ private _warmFaces(tree: BoxTree): void {
489
+ const fonts = this._fonts() as WarmingFonts | null;
490
+ if (typeof fonts?.prewarm !== 'function') return;
491
+ if (this._warmedFor !== fonts) {
492
+ this._warmedFor = fonts;
493
+ this._warmedFaces.clear();
494
+ }
495
+ let faces: Map<string, { weight: number; style: string }[]> | null = null;
496
+ for (const style of tree.textStyles) {
497
+ const key = `${style.fontFamily}|${style.fontWeight}|${style.fontStyle}`;
498
+ if (this._warmedFaces.has(key)) continue;
499
+ this._warmedFaces.add(key);
500
+ faces ??= new Map();
501
+ let named = faces.get(style.fontFamily);
502
+ if (!named) faces.set(style.fontFamily, (named = []));
503
+ named.push({ weight: style.fontWeight, style: style.fontStyle });
504
+ }
505
+ if (faces)
506
+ for (const [family, named] of faces) fonts.prewarm(family, named);
507
+ }
508
+
299
509
  /** Rebuild the cascade — the document's sheets plus the host's. */
300
510
  private _restyle(width: number): void {
301
511
  const props = this._props();
302
512
  const look = this._deviceLook();
303
- const sheets: Stylesheet[] = [uaStylesheet(look)];
304
- let order = 0;
513
+ const documentBase = this._urls.base;
514
+ // What the sheets are read from, in order: a `<style>`'s text or a
515
+ // fetched `<link>`'s, and after them the host's — each with the URL its
516
+ // own relative URLs resolve against: the document's for a `<style>`,
517
+ // the sheet's own for a `<link>`.
518
+ const read: SheetText[] = [];
305
519
  for (const ref of this._source.facts().sheets) {
306
- const text =
520
+ // A sheet handed over as bytes that names no encoding of its own is in
521
+ // its referrer's: a `<link charset>`, then the document's (CSS 2.1
522
+ // 4.4), and an import is in the encoding of the sheet importing it.
523
+ const linked =
307
524
  ref.kind === 'inline'
308
- ? ref.text
309
- : this._resources.stylesheetText(ref.href);
525
+ ? null
526
+ : this._resources.stylesheet(ref.href, [
527
+ attr(ref.element, 'charset') || undefined,
528
+ props.charset,
529
+ ]);
530
+ const text = ref.kind === 'inline' ? ref.text : linked?.text;
310
531
  if (!text) continue;
311
- const sheet = parseStylesheet(text, order);
312
- order += sheet.rules.length + 1;
313
- // `@import` is a resource like any other, and its rules sit *before*
314
- // the importing sheet's — so a fetched import is spliced in ahead.
315
- for (const url of sheet.imports) {
316
- this._resources.request({
317
- url,
318
- kind: 'stylesheet',
319
- element: ref.element,
320
- });
321
- const imported = this._resources.stylesheetText(url);
322
- if (imported) {
323
- const parsed = parseStylesheet(imported, order);
324
- order += parsed.rules.length + 1;
325
- sheets.push(parsed);
326
- }
327
- }
328
- sheets.push(sheet);
532
+ const encoding = linked ? linked.encoding : props.charset;
533
+ const base =
534
+ ref.kind === 'inline'
535
+ ? documentBase
536
+ : this._resources.sheetBase(ref.href);
537
+ read.push({ text, encoding, element: ref.element, base });
329
538
  }
330
539
  const extra = props.stylesheet;
331
- for (const text of Array.isArray(extra) ? extra : extra ? [extra] : []) {
332
- const sheet = parseStylesheet(text, order);
333
- order += sheet.rules.length + 1;
334
- sheets.push(sheet);
335
- }
336
- this._cascade = new Cascade(
337
- sheets,
338
- look,
339
- width,
340
- this._viewportHeight(),
341
- this._scale,
540
+ const extras = Array.isArray(extra) ? extra : extra ? [extra] : [];
541
+ const fonts = this._fonts();
542
+ // Sheets that read as they did last time are the same rules: an append
543
+ // to a streamed document, or a width across a `@media` breakpoint,
544
+ // restyles the elements without parsing a framework's stylesheet again
545
+ // or indexing its thousand rules again.
546
+ const kept = this._sheetsRead;
547
+ let faces: DeclaredFace[];
548
+ if (
549
+ kept &&
550
+ kept.look === look &&
551
+ kept.scale === this._scale &&
552
+ kept.fonts === fonts &&
553
+ this._sameSheets(kept, read, extras)
554
+ ) {
555
+ this._cascade = kept.cascade;
556
+ kept.cascade.viewportWidth = width;
557
+ kept.cascade.viewportHeight = this._viewportHeight();
558
+ faces = kept.faces;
559
+ } else {
560
+ const sheets: Stylesheet[] = [uaStylesheet(look)];
561
+ const imports: ImportRead[][] = [];
562
+ faces = [];
563
+ let order = 0;
564
+ const layers = new Map<string, number>();
565
+ // A sheet takes its place in the cascade's order once everything it
566
+ // imports has taken theirs: `@import` is a resource like any other,
567
+ // and an imported sheet's rules sit *before* the importing sheet's
568
+ // (CSS 2.1 6.4.1), or an imported rule would win a tie against the
569
+ // sheet importing it.
570
+ const place = (sheet: Stylesheet, element: Element): void => {
571
+ for (const rule of sheet.rules) rule.order = order++;
572
+ order += 1;
573
+ sheets.push(sheet);
574
+ for (const rule of sheet.fontFaces) faces.push({ rule, element });
575
+ };
576
+ for (const { text, encoding, element, base } of read) {
577
+ const sheet = parseStylesheet(text, 0, layers, base);
578
+ const seen: ImportRead[] = [];
579
+ this._placeImports(sheet, encoding, element, layers, seen, place);
580
+ imports.push(seen);
581
+ place(sheet, element);
582
+ }
583
+ for (const text of extras) {
584
+ const sheet = parseStylesheet(text, order, layers);
585
+ order += sheet.rules.length + 1;
586
+ sheets.push(sheet);
587
+ }
588
+ this._cascade = new Cascade(
589
+ sheets,
590
+ look,
591
+ width,
592
+ this._viewportHeight(),
593
+ this._scale,
594
+ fonts ? (face) => xHeightOf(fonts, face) : null,
595
+ fonts ? (face) => zeroWidthOf(fonts, face) : null,
596
+ fonts ? (face) => normalLineOf(fonts, face) : null,
597
+ faces.length ? this._webFonts : null,
598
+ );
599
+ this._sheetsRead = {
600
+ look,
601
+ scale: this._scale,
602
+ fonts,
603
+ texts: read.map((r) => r.text),
604
+ encodings: read.map((r) => r.encoding),
605
+ bases: read.map((r) => r.base),
606
+ imports,
607
+ extras,
608
+ faces,
609
+ cascade: this._cascade,
610
+ };
611
+ }
612
+ // The faces this width and scheme declare: a `@font-face` may sit in a
613
+ // `@media` block like any rule.
614
+ const cssWidth = width / this._scale;
615
+ this._webFonts.setFallback(look.fontFamily);
616
+ this._webFonts.setFaces(
617
+ faces.filter((f) =>
618
+ mediaMatches(f.rule.media, cssWidth, look.colorScheme),
619
+ ),
342
620
  );
343
621
  this._cascade.setPointer({
344
622
  hovered: new Set(this._hovered),
@@ -347,10 +625,101 @@ export class HtmlViewNode extends Node {
347
625
  this._mediaBand = this._cascade.mediaBand(width);
348
626
  }
349
627
 
628
+ /**
629
+ * Place what a sheet imports, and what each of those imports, depth
630
+ * first — asking for each through the seam, as a parse of the sheet has
631
+ * to. `seen` records every import read, in order, which is what tells the
632
+ * next restyle whether the sheets read the same (`_sameSheets`). A sheet
633
+ * that imports itself, or one of the sheets importing it, is read once.
634
+ */
635
+ private _placeImports(
636
+ sheet: Stylesheet,
637
+ encoding: string | undefined,
638
+ element: Element,
639
+ layers: Map<string, number>,
640
+ seen: ImportRead[],
641
+ place: (sheet: Stylesheet, element: Element) => void,
642
+ depth = 0,
643
+ chain: Set<string> = new Set(),
644
+ ): void {
645
+ for (const url of sheet.imports) {
646
+ this._resources.request({ url, kind: 'stylesheet', element });
647
+ const fetched = this._resources.stylesheet(url, [encoding]);
648
+ const base = this._resources.sheetBase(url);
649
+ seen.push({ url, text: fetched?.text ?? null, base });
650
+ const key = base ?? url;
651
+ if (!fetched || depth >= MAX_IMPORT_DEPTH || chain.has(key)) continue;
652
+ const imported = parseStylesheet(fetched.text, 0, layers, base);
653
+ chain.add(key);
654
+ this._placeImports(
655
+ imported,
656
+ fetched.encoding,
657
+ element,
658
+ layers,
659
+ seen,
660
+ place,
661
+ depth + 1,
662
+ chain,
663
+ );
664
+ chain.delete(key);
665
+ place(imported, element);
666
+ }
667
+ }
668
+
669
+ /** Whether the sheets read the same as the ones `kept` was built from,
670
+ * down to what each imports — asking for the imports again, as a parse
671
+ * of the sheet would have. */
672
+ private _sameSheets(
673
+ kept: SheetsRead,
674
+ read: SheetText[],
675
+ extras: string[],
676
+ ): boolean {
677
+ if (read.length !== kept.texts.length) return false;
678
+ if (extras.length !== kept.extras.length) return false;
679
+ for (let i = 0; i < extras.length; i += 1) {
680
+ if (extras[i] !== kept.extras[i]) return false;
681
+ }
682
+ for (let i = 0; i < read.length; i += 1) {
683
+ const r = read[i];
684
+ if (
685
+ r.text !== kept.texts[i] ||
686
+ r.encoding !== kept.encodings[i] ||
687
+ r.base !== kept.bases[i]
688
+ ) {
689
+ return false;
690
+ }
691
+ // The encodings an import falls back to are its importer's, which
692
+ // only a parse knows; the text an import decoded to under them is what
693
+ // was kept, and bytes that decode differently now are a new sheet.
694
+ for (const { url, text, base } of kept.imports[i]) {
695
+ this._resources.request({
696
+ url,
697
+ kind: 'stylesheet',
698
+ element: r.element,
699
+ });
700
+ const fetched = this._resources.stylesheet(url, [r.encoding]);
701
+ if ((fetched?.text ?? null) !== text) return false;
702
+ if (this._resources.sheetBase(url) !== base) return false;
703
+ }
704
+ }
705
+ return true;
706
+ }
707
+
350
708
  private _viewportHeight(): number {
351
- // The viewport a `vh` resolves against is the window's, not the
352
- // document's — a document taller than the window does not make `100vh`
353
- // taller with it.
709
+ // The viewport a `vh` resolves against is the one the document is seen
710
+ // through, not the document — a document taller than it does not make
711
+ // `100vh` taller with it. That is the box that scrolls the element,
712
+ // where one does: a browser's page area, below its tab strip and its
713
+ // toolbar, where `100vh` measured by the window put a page's footer the
714
+ // toolbar's height below the fold. The window, where nothing scrolls it.
715
+ for (let node = this.parent; node; node = node.parent) {
716
+ // `Scrollable`'s, and not every node's: asked the way core asks it
717
+ const scroller = node as { isScroller?: () => boolean };
718
+ if (!scroller.isScroller?.()) continue;
719
+ const height = node.contentBox().height;
720
+ if (height > 0) return height;
721
+ break;
722
+ }
354
723
  const root = this.root;
355
724
  const height = root?.abs?.height;
356
725
  return height && height > 0 ? height : 600;
@@ -358,20 +727,101 @@ export class HtmlViewNode extends Node {
358
727
 
359
728
  private _fonts(): FontsLike | null {
360
729
  const fonts = (this.app as { fonts?: FontsLike } | null)?.fonts;
361
- return fonts ?? null;
730
+ // a face the engine cannot shape from costs its characters, not the
731
+ // document (`shaping.ts`)
732
+ return fonts ? shapingSafe(fonts) : null;
733
+ }
734
+
735
+ /** The text layouts the last pass made, for this one to reuse. */
736
+ private _layouts: TextLayoutCache | null = null;
737
+
738
+ /** The fonts a layout pass lays text out with: the engine's, through the
739
+ * layouts the pass before made (`TextLayoutCache`). */
740
+ private _layoutFonts(): FontsLike | null {
741
+ const fonts = this._fonts();
742
+ if (!fonts) return null;
743
+ if (this._layouts?.engine !== fonts)
744
+ this._layouts = new TextLayoutCache(fonts);
745
+ this._layouts.begin();
746
+ return this._layouts.fonts;
362
747
  }
363
748
 
364
- /** Bring the pipeline up to date for a width. */
749
+ /**
750
+ * Bring the pipeline up to date for a width — or, where that throws, leave
751
+ * the document blank. From a paint a throw is the application's end, for
752
+ * a document it did not write: a stack a thousand nested elements run
753
+ * down, or a limit of the text engine's or the server's that one more
754
+ * document finds. Reported once for each thing that went wrong, and tried
755
+ * again only when something changes, or the width does.
756
+ */
365
757
  private _prepare(width: number): void {
366
758
  const target = Math.max(1, Math.floor(width));
759
+ if (this._failedAt === target && this._stale === Stale.Nothing) return;
760
+ try {
761
+ this._update(target);
762
+ this._failedAt = -1;
763
+ } catch (error) {
764
+ this._fail(error, target);
765
+ }
766
+ }
767
+
768
+ /** The width the pipeline threw at, until something changes (`_prepare`). */
769
+ private _failedAt = -1;
770
+ private _failures: Set<string> | null = null;
771
+
772
+ private _fail(error: unknown, width: number): void {
773
+ this._tree = null;
774
+ this._stale = Stale.Nothing;
775
+ this._failedAt = width;
776
+ this._laidOutWidth = -1;
777
+ this._documentWidth = 0;
778
+ this._documentHeight = 0;
779
+ const message = String((error as { stack?: unknown })?.stack ?? error);
780
+ this._failures ??= new Set();
781
+ if (this._failures.has(message)) return;
782
+ this._failures.add(message);
783
+ const g = globalThis as {
784
+ process?: { env?: Record<string, string | undefined> };
785
+ console?: { error(message: string): void };
786
+ };
787
+ if (g.process?.env?.NODE_ENV === 'production') return;
788
+ g.console?.error(
789
+ '@react-x11/components: <Html> could not lay out or paint its ' +
790
+ `document, and leaves it blank.\n${message}`,
791
+ );
792
+ }
793
+
794
+ private _update(target: number): void {
367
795
  const props = this._props();
368
796
  if ((props.domRevision ?? 0) !== this._reportedDomRevision) {
369
797
  this._reportedDomRevision = props.domRevision ?? 0;
370
798
  this._source.touch();
799
+ this._updateBase();
371
800
  this._sweep();
372
801
  if (this._stale < Stale.Style) this._stale = Stale.Style;
373
802
  }
374
- if (this._stale === Stale.Nothing && this._laidOutWidth === target) return;
803
+ // A document that reads its viewport's height — a `vh`, a percentage
804
+ // height on the root, a box placed against the initial containing block
805
+ // — is laid out again when that height moves. One that reads none, which
806
+ // is most, goes on skipping layout when the window only grew taller.
807
+ const viewport = this._viewportHeight();
808
+ const viewportMoved =
809
+ viewport !== this._laidOutUnder && this._readsViewportHeight();
810
+ // the sizes other widths came to were read from what just changed
811
+ if (this._stale !== Stale.Nothing || viewportMoved) this._sizes.clear();
812
+ if (
813
+ this._stale === Stale.Nothing &&
814
+ this._laidOutWidth === target &&
815
+ !viewportMoved
816
+ ) {
817
+ return;
818
+ }
819
+ // Nothing but the viewport: the size core laid this element out at came
820
+ // from a layout under the old one, and nothing else will tell it so.
821
+ const onlyViewport =
822
+ this._stale === Stale.Nothing && this._laidOutWidth === target;
823
+ const wasWidth = this._documentWidth;
824
+ const wasHeight = this._documentHeight;
375
825
 
376
826
  if (this._stale >= Stale.Style || !this._cascade) {
377
827
  this._restyle(target);
@@ -387,17 +837,43 @@ export class HtmlViewNode extends Node {
387
837
  const cascade = this._cascade;
388
838
  if (!cascade) return;
389
839
  cascade.viewportWidth = target;
390
- cascade.viewportHeight = this._viewportHeight();
840
+ cascade.viewportHeight = viewport;
841
+ // A `vw` or a `vh` is a number by the time a style holds it, so the
842
+ // styles computed for another viewport are wrong for this one: built
843
+ // again, where some style reads the side that moved. A document that
844
+ // reads neither — most — goes on skipping the cascade on a resize.
845
+ if (
846
+ this._stale < Stale.Boxes &&
847
+ ((cascade.readsViewportWidth && this._styledWidth !== target) ||
848
+ (cascade.readsViewportHeight && this._styledHeight !== viewport))
849
+ ) {
850
+ this._stale = Stale.Boxes;
851
+ }
391
852
 
392
853
  if (this._stale >= Stale.Boxes || !this._tree) {
393
854
  const look = this._deviceLook();
394
- this._tree = buildBoxes(this._source.document, {
395
- cascade,
396
- scale: this._scale,
397
- imageSize: (el) => this._resources.imageSize(attr(el, 'src') ?? ''),
398
- controlSize: (el, kind, style) =>
399
- measureControl(el, kind, style, this._fonts(), look),
400
- });
855
+ const build = () =>
856
+ buildBoxes(this._source.document, {
857
+ cascade,
858
+ scale: this._scale,
859
+ imageSize: (el) => this._resources.imageSize(imageUrlOf(el) ?? ''),
860
+ urlSize: (url) => this._resources.imageSize(url),
861
+ controlSize: (el, kind, style) =>
862
+ measureControl(el, kind, style, this._fonts(), look),
863
+ });
864
+ this._tree = build();
865
+ this._styledWidth = target;
866
+ this._styledHeight = viewport;
867
+ this._requestBackgrounds(this._tree);
868
+ let again = this._contentImagesArrived(this._tree);
869
+ // the faces the styles just asked for, of the families the document
870
+ // loads itself — which, answered at once, change the styles asking
871
+ if (this._webFonts.request(this._tree.text)) {
872
+ this._layouts = null;
873
+ again = true;
874
+ }
875
+ if (again) this._tree = build();
876
+ this._warmFaces(this._tree);
401
877
  this._textPoints = null;
402
878
  this._pointsAreUnits = null;
403
879
  this._laidOutWidth = -1;
@@ -405,20 +881,47 @@ export class HtmlViewNode extends Node {
405
881
 
406
882
  if (
407
883
  this._tree &&
408
- (this._laidOutWidth !== target || this._stale >= Stale.Layout)
884
+ (this._laidOutWidth !== target ||
885
+ viewportMoved ||
886
+ this._stale >= Stale.Layout)
409
887
  ) {
410
888
  const result = layoutDocument(
411
889
  this._tree,
412
- this._fonts(),
890
+ this._layoutFonts(),
413
891
  target,
414
- this._viewportHeight(),
892
+ viewport,
415
893
  );
416
894
  this._documentWidth = result.width;
417
895
  this._documentHeight = result.height;
418
896
  this._laidOutWidth = target;
897
+ this._laidOutUnder = viewport;
898
+ this._layoutReadsViewport = result.readsViewportHeight;
899
+ this._sizes.delete(target);
900
+ this._sizes.set(target, { width: result.width, height: result.height });
901
+ if (this._sizes.size > SIZES_KEPT) {
902
+ this._sizes.delete(this._sizes.keys().next().value!);
903
+ }
419
904
  this._reportControls();
420
905
  }
421
906
  this._stale = Stale.Nothing;
907
+ // Read after core's layout pass — the viewport is the box around this
908
+ // one, whose new height only that pass decides — so it is a paint that
909
+ // finds it moved, and the next frame that measures this element again.
910
+ // Inside a measure the answer already is the new size.
911
+ if (
912
+ onlyViewport &&
913
+ !this._measuring &&
914
+ (this._documentWidth !== wasWidth || this._documentHeight !== wasHeight)
915
+ ) {
916
+ this.invalidateMeasure('content');
917
+ }
918
+ }
919
+
920
+ /** Whether the document as laid out reads its viewport's height. */
921
+ private _readsViewportHeight(): boolean {
922
+ return (
923
+ this._layoutReadsViewport || this._cascade?.readsViewportHeight === true
924
+ );
422
925
  }
423
926
 
424
927
  private _reportControls(): void {
@@ -437,6 +940,16 @@ export class HtmlViewNode extends Node {
437
940
  y: r.y / s,
438
941
  width: r.width / s,
439
942
  height: r.height / s,
943
+ ...(r.bare && {
944
+ bare: {
945
+ ...r.bare,
946
+ x: r.bare.x / s,
947
+ y: r.bare.y / s,
948
+ width: r.bare.width / s,
949
+ height: r.bare.height / s,
950
+ fontSize: r.bare.fontSize / s,
951
+ },
952
+ }),
440
953
  },
441
954
  );
442
955
  if (sameRects(rects, this._controls)) return;
@@ -457,13 +970,52 @@ export class HtmlViewNode extends Node {
457
970
  */
458
971
  override measureContent({ width }: MeasureConstraints): MeasuredSize {
459
972
  const offered = Number.isFinite(width) ? width : 800;
460
- this._prepare(offered);
973
+ this._measuring = true;
974
+ let size: { width: number; height: number };
975
+ try {
976
+ size = this._sizeAt(offered);
977
+ } finally {
978
+ this._measuring = false;
979
+ }
980
+ // core takes finite numbers only, and throws on any other from the
981
+ // layout — a document of lengths no clamp foresaw is none too tall
982
+ const finite = (n: number): number => (Number.isFinite(n) ? n : 0);
461
983
  return {
462
- width: Math.ceil(Math.min(this._documentWidth, offered)),
463
- height: Math.ceil(this._documentHeight),
984
+ width: wholePixels(Math.min(finite(size.width), offered)),
985
+ height: wholePixels(finite(size.height)),
464
986
  };
465
987
  }
466
988
 
989
+ /**
990
+ * The size the document comes to at `width`.
991
+ *
992
+ * Core asks a leaf for its height at the width it was measured at as well
993
+ * as at the one it has now (`probeHeightFloors`), to learn whether a
994
+ * relayout changed what the leaf needs — so every frame of a resize asks
995
+ * two widths, and the boxes hold one. Laying the document out at the old
996
+ * width to answer, and then at the new one again for the pass after, was
997
+ * three whole passes a frame. The size a width came to is kept instead,
998
+ * and answers until anything a layout reads changes: the source, the
999
+ * styles, a resource, the viewport height. Nothing but a size comes from
1000
+ * it — paint and the text accessors read the boxes, which are only ever
1001
+ * laid out for real (`_prepare`).
1002
+ */
1003
+ private _sizeAt(width: number): { width: number; height: number } {
1004
+ const target = Math.max(1, Math.floor(width));
1005
+ if (
1006
+ target !== this._laidOutWidth &&
1007
+ this._stale === Stale.Nothing &&
1008
+ (this._props().domRevision ?? 0) === this._reportedDomRevision &&
1009
+ (!this._readsViewportHeight() ||
1010
+ this._viewportHeight() === this._laidOutUnder)
1011
+ ) {
1012
+ const known = this._sizes.get(target);
1013
+ if (known) return known;
1014
+ }
1015
+ this._prepare(width);
1016
+ return { width: this._documentWidth, height: this._documentHeight };
1017
+ }
1018
+
467
1019
  override applyProps(
468
1020
  nextProps: Record<string, unknown>,
469
1021
  prevProps: Record<string, unknown>,
@@ -471,19 +1023,31 @@ export class HtmlViewNode extends Node {
471
1023
  super.applyProps(nextProps, prevProps);
472
1024
  const next = nextProps as unknown as HtmlViewProps;
473
1025
  const prev = prevProps as unknown as HtmlViewProps;
474
- if (next.look !== prev.look || next.stylesheet !== prev.stylesheet) {
1026
+ if (
1027
+ next.look !== prev.look ||
1028
+ next.stylesheet !== prev.stylesheet ||
1029
+ next.charset !== prev.charset
1030
+ ) {
475
1031
  this._invalidate(Stale.Style);
476
1032
  }
477
1033
  if (next.source !== prev.source || next.complete !== prev.complete) {
478
1034
  this._read();
479
1035
  } else if ((next.domRevision ?? 0) !== (prev.domRevision ?? 0)) {
480
1036
  this._invalidate(Stale.Style);
1037
+ } else if ((next.baseUrl ?? null) !== (prev.baseUrl ?? null)) {
1038
+ // the same document somewhere else: every URL in it is another one
1039
+ this._updateBase();
1040
+ this._sweep();
481
1041
  }
482
1042
  }
483
1043
 
484
1044
  override destroySubtree(): void {
485
1045
  this._resources.destroy();
1046
+ this._webFonts.destroy();
486
1047
  this._source.destroy();
1048
+ this._shadowCache?.destroy();
1049
+ this._shadowCache = null;
1050
+ this._sheetsRead = null;
487
1051
  this._tree = null;
488
1052
  this._cascade = null;
489
1053
  super.destroySubtree();
@@ -586,26 +1150,129 @@ export class HtmlViewNode extends Node {
586
1150
  // handler hand over — and are the one place the two units meet on the way
587
1151
  // in. `_toDocument` multiplies.
588
1152
 
589
- /** The link under a logical window point, if any. Not part of the
590
- * selection seam: core deliberately left hover and `cursorAt` out of
591
- * #291, so following a link stays this package's. */
1153
+ /** The link under a logical window point, if any — resolved against the
1154
+ * document's base where it has one. Not part of the selection seam: core
1155
+ * deliberately left hover and `cursorAt` out of #291, so following a
1156
+ * link stays this package's. */
592
1157
  hrefAtPoint(x: number, y: number): string | null {
593
1158
  const el = this.elementAtPoint(x, y);
594
1159
  let node: Element | null = el;
595
1160
  while (node) {
596
1161
  const href = attr(node, 'href');
597
- if (href && (tagOf(node) === 'a' || tagOf(node) === 'area')) return href;
1162
+ if (href && (tagOf(node) === 'a' || tagOf(node) === 'area')) {
1163
+ return this._urls.resolve(href);
1164
+ }
598
1165
  node = isElement(node.parent) ? node.parent : null;
599
1166
  }
600
1167
  return null;
601
1168
  }
602
1169
 
1170
+ /** The URL the document's relative URLs resolve against, or null where
1171
+ * it has none (`_updateBase`). */
1172
+ get documentBase(): string | null {
1173
+ return this._urls.base;
1174
+ }
1175
+
1176
+ /**
1177
+ * Where an element is, in document coordinates: logical pixels from this
1178
+ * element's top left, the space the scroll offset of a box around it is
1179
+ * in — so scrolling to a fragment is `scrollTo({ y: rect.y })`. A block's
1180
+ * border box. An inline element's fragments, from its first line to its
1181
+ * last, each across its text, the inline-blocks and inline boxes' edges
1182
+ * it holds, and its own padding and border where it starts or ends — its
1183
+ * border box across, as `getBoundingClientRect` is (CSSOM View 6.1) — and
1184
+ * as tall as its lines; or where its text would start when it has none,
1185
+ * as `<a name>` has none. Null for an element with no box — `display:
1186
+ * none`, or not in the document.
1187
+ */
1188
+ elementRect(element: Element): Rect | null {
1189
+ this._prepare(this.abs.width || 1);
1190
+ const tree = this._tree;
1191
+ if (!tree) return null;
1192
+ const box = boxFor(tree.root, element);
1193
+ if (!box) return null;
1194
+ let rect: Rect | null = null;
1195
+ if (box.kind !== 'inline' && box.kind !== 'text') {
1196
+ rect = { x: box.x, y: box.y, width: box.width, height: box.height };
1197
+ } else {
1198
+ const bands: Rect[] = [];
1199
+ // an inline box's fragments are on the lines of the block it is laid
1200
+ // out in, and no text of a float or a positioned box inside it — on
1201
+ // lines of their own — is one of them (CSSOM View 6.1): a list item
1202
+ // made inline around an absolute link measured as the link
1203
+ let block: Box | null = box.parent;
1204
+ while (block && !block.lines) block = block.parent;
1205
+ if (box.subtreeTextEnd > box.subtreeTextStart) {
1206
+ if (box.kind === 'inline' && block?.lines) {
1207
+ lineBands(
1208
+ block.lines,
1209
+ box.subtreeTextStart,
1210
+ box.subtreeTextEnd,
1211
+ 0,
1212
+ 0,
1213
+ bands,
1214
+ );
1215
+ } else {
1216
+ collectBands(
1217
+ tree.root,
1218
+ box.subtreeTextStart,
1219
+ box.subtreeTextEnd,
1220
+ 0,
1221
+ 0,
1222
+ bands,
1223
+ );
1224
+ }
1225
+ }
1226
+ if (box.kind === 'inline') {
1227
+ fragmentReach(box, bands);
1228
+ const fonts = this._layouts?.fonts;
1229
+ if (fonts) fragmentHeights(box, bands, fonts);
1230
+ }
1231
+ for (const band of bands) rect = rect ? unionRect(rect, band) : band;
1232
+ if (!rect) {
1233
+ // an empty subtree's range is `[0, 0)`, wherever it stands: where
1234
+ // its text would be is where the text after it starts
1235
+ const caret = caretAt(tree.root, textAfter(tree.root, box));
1236
+ if (!caret) return null;
1237
+ rect = { x: caret.x, y: caret.y, width: 0, height: caret.height };
1238
+ }
1239
+ }
1240
+ const s = this._scale;
1241
+ return {
1242
+ x: rect.x / s,
1243
+ y: rect.y / s,
1244
+ width: rect.width / s,
1245
+ height: rect.height / s,
1246
+ };
1247
+ }
1248
+
603
1249
  /** The deepest element whose box contains a logical window point. */
604
1250
  elementAtPoint(x: number, y: number): Element | null {
605
1251
  const tree = this._tree;
606
1252
  if (!tree) return null;
607
1253
  const local = this._toDocument(x, y);
608
- return deepestAt(tree.root, local.x, local.y);
1254
+ return deepestAt(tree, local.x, local.y);
1255
+ }
1256
+
1257
+ /**
1258
+ * The cursor for a point of this element's, in device pixels: what core
1259
+ * asks a drawn element as the pointer moves over it (`cursorAt`,
1260
+ * react-x11#757). The `cursor` the document's styles give what is under
1261
+ * it — a link's `pointer`, the user-agent sheet's — and where they say
1262
+ * nothing, text's I-beam over text and the arrow elsewhere, as a browser
1263
+ * shows them. The arrow is named rather than left to null: null lets
1264
+ * core fall through to the element's `defaultCursor`, which is the
1265
+ * I-beam on a selectable surface, and every `<Html>` is one.
1266
+ */
1267
+ override cursorAt(x: number, y: number): string | null {
1268
+ const tree = this._tree;
1269
+ if (!tree) return null;
1270
+ const hit = { text: false };
1271
+ const el = deepestAt(tree, x - this.abs.x, y - this.abs.y, hit);
1272
+ const cursor = el ? tree.styles.get(el)?.style.cursor : null;
1273
+ // a keyword; a `url()` this cannot load falls back, as its list would
1274
+ if (cursor && cursor !== 'auto' && /^[a-z-]+$/.test(cursor)) return cursor;
1275
+ return hit.text ? 'text' : 'default';
609
1276
  }
610
1277
 
611
1278
  /**
@@ -624,20 +1291,207 @@ export class HtmlViewNode extends Node {
624
1291
  node = isElement(node.parent) ? node.parent : null;
625
1292
  }
626
1293
  if (sameChain(chain, this._hovered)) return false;
1294
+ const was = this._hovered;
627
1295
  this._hovered = chain;
628
1296
  cascade.setPointer({ hovered: new Set(chain), active: EMPTY_SET });
629
- this._invalidate(Stale.Boxes);
1297
+ this._restyleHover(was, chain);
630
1298
  return true;
631
1299
  }
632
1300
 
633
1301
  clearHover(): boolean {
634
1302
  if (!this._hovered.length) return false;
1303
+ const was = this._hovered;
635
1304
  this._hovered = [];
636
1305
  this._cascade?.setPointer({ hovered: new Set(), active: EMPTY_SET });
637
- this._invalidate(Stale.Boxes);
1306
+ this._restyleHover(was, []);
638
1307
  return true;
639
1308
  }
640
1309
 
1310
+ /** The hovered chain moved: restyle where it did, or the document. */
1311
+ private _restyleHover(was: readonly Element[], now: readonly Element[]) {
1312
+ const done = this._hoverInPlace(was, now);
1313
+ if (done === 'painted') this.invalidate(false, this, 'props');
1314
+ else if (done === false) this._invalidate(Stale.Boxes);
1315
+ }
1316
+
1317
+ /**
1318
+ * A pointer move restyled where it happened, when all it changed is ink.
1319
+ *
1320
+ * A move to another element is a change to the cascade, and the pipeline
1321
+ * answers one by building every box again and laying the document out:
1322
+ * 270 ms on X11 and twice that on Cocoa for a Wikipedia article, on every
1323
+ * link the pointer crossed. But the rules a page writes for `:hover`
1324
+ * nearly all change a colour, an underline, a background or a border's
1325
+ * colour — 77 of that article's 79 — and those move nothing. So:
1326
+ *
1327
+ * - Only an element whose hover state flipped and that a compound testing
1328
+ * the pointer could match can change (`Cascade.hoverTouches`), with its
1329
+ * subtree, and its later siblings where a sibling combinator follows.
1330
+ * A move between two paragraphs under `a:hover` touches nothing.
1331
+ * - Those are styled again from their parents. Where anything but ink
1332
+ * differs (`PAINT_ONLY`), or a box is not the element's own — an
1333
+ * anonymous or a pseudo-element box, a marker, a control — this is
1334
+ * not the move to take.
1335
+ * - The boxes take their new styles, and each layout of their text is
1336
+ * made again from the runs it was made from (`TextLayoutCache.inputsOf`)
1337
+ * with the new ink (`runFor`), where its geometry comes out the same.
1338
+ *
1339
+ * Everything is checked before anything is changed. 'none' where nothing
1340
+ * could change, 'painted' where it was restyled here, false where the
1341
+ * document has to be built again.
1342
+ */
1343
+ private _hoverInPlace(
1344
+ was: readonly Element[],
1345
+ now: readonly Element[],
1346
+ ): 'none' | 'painted' | false {
1347
+ const cascade = this._cascade;
1348
+ const tree = this._tree;
1349
+ const layouts = this._layouts;
1350
+ if (!cascade || !tree || !layouts || !cascade.hoverLocal) return false;
1351
+ if (this._stale !== Stale.Nothing || this._laidOutWidth < 0) return false;
1352
+
1353
+ const before = new Set(was);
1354
+ const after = new Set(now);
1355
+ const roots: Element[] = [];
1356
+ // and the elements a `:has()` testing the pointer may flip, which
1357
+ // are around the ones that changed; the chains are ancestor chains,
1358
+ // so the deepest that changed on each side is where to look from
1359
+ const flipped = (el: Element, other: Set<Element>): void => {
1360
+ if (other.has(el)) return;
1361
+ if (cascade.hoverTouches(el)) roots.push(el);
1362
+ };
1363
+ for (const el of was) flipped(el, after);
1364
+ for (const el of now) flipped(el, before);
1365
+ const deepest = (chain: readonly Element[], other: Set<Element>) => {
1366
+ const el = chain[0];
1367
+ if (el && !other.has(el)) cascade.hoverAnchors(el, roots);
1368
+ };
1369
+ deepest(was, after);
1370
+ deepest(now, before);
1371
+ if (!roots.length) return 'none';
1372
+
1373
+ // what may restyle: the roots' subtrees, which inherit from them and a
1374
+ // descendant combinator reaches, and their later siblings' too where a
1375
+ // sibling combinator follows a compound that tests the pointer
1376
+ const reach = new Set<Element>();
1377
+ const collect = (el: Element): boolean => {
1378
+ const stack: Element[] = [el];
1379
+ while (stack.length) {
1380
+ const at = stack.pop()!;
1381
+ if (reach.has(at)) continue;
1382
+ reach.add(at);
1383
+ if (reach.size > HOVER_RESTYLE_LIMIT) return false;
1384
+ for (const child of at.children)
1385
+ if (isElement(child)) stack.push(child);
1386
+ }
1387
+ return true;
1388
+ };
1389
+ for (const root of roots) {
1390
+ if (!collect(root)) return false;
1391
+ if (!cascade.hoverSiblings) continue;
1392
+ for (let s = root.nextSibling; s; s = s.nextSibling) {
1393
+ if (isElement(s) && !collect(s)) return false;
1394
+ }
1395
+ }
1396
+
1397
+ // styled again from their parents, a parent first
1398
+ const fresh = new Map<Element, ComputedStyle>();
1399
+ const changed = new Map<Element, ComputedStyle>();
1400
+ let refused = false;
1401
+ const styleOf = (el: Element): ComputedStyle | null => {
1402
+ const kept = tree.styles.get(el);
1403
+ if (!kept) return null;
1404
+ if (!reach.has(el)) return kept.style;
1405
+ const done = fresh.get(el);
1406
+ if (done) return done;
1407
+ // an element at the top of the document is styled from the box above
1408
+ // it all, as the builder styled it (`BoxBuilder.run`)
1409
+ const parent = isElement(el.parent) ? el.parent : null;
1410
+ const parentStyle = parent ? styleOf(parent) : tree.root.style;
1411
+ if (!parentStyle) {
1412
+ refused = true;
1413
+ return null;
1414
+ }
1415
+ const style = cascade.styleFor(el, parentStyle, kept.inFlex);
1416
+ fresh.set(el, style);
1417
+ const diff = inkOnly(kept.style, style);
1418
+ if (diff === false) refused = true;
1419
+ else if (diff) changed.set(el, style);
1420
+ return style;
1421
+ };
1422
+ for (const el of reach) {
1423
+ styleOf(el);
1424
+ if (refused) return false;
1425
+ }
1426
+ if (!changed.size) return 'none';
1427
+ for (const el of changed.keys()) {
1428
+ const tag = tagOf(el);
1429
+ // their backgrounds are the canvas's
1430
+ if (tag === 'html' || tag === 'body') return false;
1431
+ }
1432
+
1433
+ // the boxes: each changed element's own, and nothing that takes its
1434
+ // style from one without being it
1435
+ const fonts = layouts.fonts;
1436
+ const restyled: [Box, ComputedStyle][] = [];
1437
+ const redecorated: [Box, Box['decoration']][] = [];
1438
+ const walk: Box[] = [tree.root];
1439
+ while (walk.length) {
1440
+ const box = walk.pop()!;
1441
+ for (const child of box.children) walk.push(child);
1442
+ const owner = box.el ?? nearestElement(box);
1443
+ if (!owner) continue;
1444
+ const style = changed.get(owner);
1445
+ if (!style) continue;
1446
+ const kept = tree.styles.get(owner)!;
1447
+ // an anonymous box, a pseudo-element, a marker, a control: a style
1448
+ // derived from the element's, or drawn from it somewhere else
1449
+ if (!box.el || box.style !== kept.style) return false;
1450
+ if (box.marker || box.replaced !== 'none') return false;
1451
+ restyled.push([box, style]);
1452
+ if (box.kind === 'inline') {
1453
+ const decoration = inlineDecoration(fonts, box, style);
1454
+ if ((decoration === null) !== (box.decoration === null)) {
1455
+ // a rounded box with a background is laid out with its edges
1456
+ if (style.borderRadius.some((r) => r !== 0)) return false;
1457
+ }
1458
+ if (decoration !== box.decoration) redecorated.push([box, decoration]);
1459
+ }
1460
+ }
1461
+
1462
+ // the text: each layout holding a run of a changed element's, made
1463
+ // again with its new ink, where it comes out the same shape
1464
+ const relaid = new Map<TextLayoutLike, TextLayoutLike | null>();
1465
+ const texts: LineText[] = [];
1466
+ walk.push(tree.root);
1467
+ while (walk.length) {
1468
+ const box = walk.pop()!;
1469
+ for (const child of box.children) walk.push(child);
1470
+ if (!box.lines) continue;
1471
+ for (const line of box.lines) {
1472
+ for (const text of line.texts) {
1473
+ texts.push(text);
1474
+ if (relaid.has(text.layout)) continue;
1475
+ const next = reinked(text, layouts, changed, tree.styles);
1476
+ if (next === false) return false;
1477
+ relaid.set(text.layout, next);
1478
+ }
1479
+ }
1480
+ }
1481
+
1482
+ // and only now, all of it
1483
+ for (const [box, style] of restyled) box.style = style;
1484
+ for (const [box, decoration] of redecorated) box.decoration = decoration;
1485
+ for (const text of texts) {
1486
+ const next = relaid.get(text.layout);
1487
+ if (next) text.layout = next;
1488
+ }
1489
+ for (const [el, style] of changed) {
1490
+ tree.styles.set(el, { style, inFlex: tree.styles.get(el)!.inFlex });
1491
+ }
1492
+ return 'painted';
1493
+ }
1494
+
641
1495
  /** The document, for an application that wants to read or change it. */
642
1496
  get document(): Document {
643
1497
  return this._source.document;
@@ -652,6 +1506,7 @@ export class HtmlViewNode extends Node {
652
1506
  */
653
1507
  touchDocument(): void {
654
1508
  this._source.touch();
1509
+ this._updateBase();
655
1510
  this._sweep();
656
1511
  this._invalidate(Stale.Style);
657
1512
  }
@@ -708,23 +1563,133 @@ export class HtmlViewNode extends Node {
708
1563
  damage = { x: 0, y: 0, width: window.width, height: window.height };
709
1564
  }
710
1565
  }
1566
+ try {
1567
+ this._paint(ctx, tree, range, damage);
1568
+ } catch (error) {
1569
+ this._fail(error, this._laidOutWidth);
1570
+ }
1571
+ }
1572
+
1573
+ private _paint(
1574
+ ctx: Context2D,
1575
+ tree: BoxTree,
1576
+ range: { start: number; end: number } | null,
1577
+ damage: { x: number; y: number; width: number; height: number } | null,
1578
+ ): void {
711
1579
  paintDocument(ctx as PaintContext, tree, {
712
1580
  originX: this.abs.x,
713
1581
  originY: this.abs.y,
1582
+ // the root's background covers the whole element, not only the
1583
+ // document: an element grown past its content is canvas too
1584
+ canvas: this.abs,
714
1585
  scale: this._scale,
715
1586
  damage,
716
1587
  selection: range
717
1588
  ? { start: this._toUnits(range.start), end: this._toUnits(range.end) }
718
1589
  : null,
719
1590
  selectionColor: this.selectionColor,
720
- imageFor: (box) =>
721
- box.el ? this._resources.image(attr(box.el, 'src') ?? '') : null,
1591
+ imageFor: (box) => {
1592
+ const url = box.el ? imageUrlOf(box.el) : CONTENT_IMAGES.get(box);
1593
+ return url === undefined ? null : this._resources.image(url);
1594
+ },
1595
+ backgroundImageFor: (url) => {
1596
+ const image = this._resources.image(url);
1597
+ const size = image ? this._resources.imageSize(url) : null;
1598
+ return image && size ? { image, ...size } : null;
1599
+ },
1600
+ cached: (key, width, height, draw) =>
1601
+ (this._shadowCache ??= new SurfaceCache(this.app)).get(
1602
+ key,
1603
+ width,
1604
+ height,
1605
+ draw as (ctx: unknown) => void,
1606
+ ),
722
1607
  });
723
1608
  }
724
1609
  }
725
1610
 
726
1611
  const EMPTY_SET: ReadonlySet<Element> = new Set();
727
1612
 
1613
+ /** A sheet as the restyle reads it: its text, the encoding it was decoded
1614
+ * from, the element that brought it, and what its URLs resolve against. */
1615
+ interface SheetText {
1616
+ text: string;
1617
+ encoding?: string;
1618
+ element: Element;
1619
+ base: string | null;
1620
+ }
1621
+
1622
+ /** One `@import` a sheet read, and what it read as. */
1623
+ interface ImportRead {
1624
+ url: string;
1625
+ text: string | null;
1626
+ base: string | null;
1627
+ }
1628
+
1629
+ /** What a cascade was built from, to tell whether the next would be the
1630
+ * same one: the look, scale and fonts, each sheet's text, encoding and
1631
+ * base, the texts of everything each imports, and the host's own — and
1632
+ * the faces all of them declare. */
1633
+ interface SheetsRead {
1634
+ look: RootLook;
1635
+ scale: number;
1636
+ fonts: unknown;
1637
+ texts: string[];
1638
+ encodings: (string | undefined)[];
1639
+ bases: (string | null)[];
1640
+ imports: ImportRead[][];
1641
+ extras: string[];
1642
+ faces: DeclaredFace[];
1643
+ cascade: Cascade;
1644
+ }
1645
+
1646
+ /** The first box an element made, depth first. */
1647
+ function boxFor(root: Box, element: Element): Box | null {
1648
+ const stack: Box[] = [root];
1649
+ while (stack.length) {
1650
+ const box = stack.pop()!;
1651
+ if (box.el === element) return box;
1652
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1653
+ stack.push(box.children[i]);
1654
+ }
1655
+ }
1656
+ return null;
1657
+ }
1658
+
1659
+ /** Where the document's text goes on after a box with none: the start of
1660
+ * the next text in document order, or the end of the last before it. */
1661
+ function textAfter(root: Box, target: Box): number {
1662
+ const stack: Box[] = [root];
1663
+ let passed = false;
1664
+ let before = 0;
1665
+ while (stack.length) {
1666
+ const box = stack.pop()!;
1667
+ if (box === target) {
1668
+ passed = true;
1669
+ continue;
1670
+ }
1671
+ if (box.kind === 'text' && box.textEnd > box.textStart) {
1672
+ if (passed) return box.textStart;
1673
+ before = box.textEnd;
1674
+ }
1675
+ for (let i = box.children.length - 1; i >= 0; i -= 1) {
1676
+ stack.push(box.children[i]);
1677
+ }
1678
+ }
1679
+ return before;
1680
+ }
1681
+
1682
+ function unionRect(a: Rect, b: Rect): Rect {
1683
+ const x = Math.min(a.x, b.x);
1684
+ const y = Math.min(a.y, b.y);
1685
+ return {
1686
+ x,
1687
+ y,
1688
+ width: Math.max(a.x + a.width, b.x + b.width) - x,
1689
+ height: Math.max(a.y + a.height, b.y + b.height) - y,
1690
+ };
1691
+ }
1692
+
728
1693
  function textOf(el: Element): string {
729
1694
  let out = '';
730
1695
  for (const child of el.children) {
@@ -749,7 +1714,8 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
749
1714
  p.x !== q.x ||
750
1715
  p.y !== q.y ||
751
1716
  p.width !== q.width ||
752
- p.height !== q.height
1717
+ p.height !== q.height ||
1718
+ !sameBare(p.bare, q.bare)
753
1719
  ) {
754
1720
  return false;
755
1721
  }
@@ -757,6 +1723,19 @@ function sameRects(a: ControlRect[], b: ControlRect[]): boolean {
757
1723
  return true;
758
1724
  }
759
1725
 
1726
+ function sameBare(a?: BareField, b?: BareField): boolean {
1727
+ if (!a || !b) return a === b;
1728
+ return (
1729
+ a.x === b.x &&
1730
+ a.y === b.y &&
1731
+ a.width === b.width &&
1732
+ a.height === b.height &&
1733
+ a.color === b.color &&
1734
+ a.fontFamily === b.fontFamily &&
1735
+ a.fontSize === b.fontSize
1736
+ );
1737
+ }
1738
+
760
1739
  // --- walks over the laid-out tree -------------------------------------------
761
1740
 
762
1741
  /** Distance from a coordinate to an interval, zero inside it. */
@@ -797,7 +1776,7 @@ function nearestText(box: Box, x: number, y: number): number | null {
797
1776
  const units = offsets.length
798
1777
  ? offsets[Math.max(0, Math.min(local, offsets.length - 1))]
799
1778
  : local;
800
- best = text.textStart + Math.max(0, units - text.layoutStart);
1779
+ best = Math.max(text.textStart, documentOffsetOf(text, units));
801
1780
  }
802
1781
  };
803
1782
 
@@ -908,7 +1887,7 @@ function caretAt(
908
1887
  for (const text of line.texts) {
909
1888
  if (units < text.textStart || units > text.textEnd) continue;
910
1889
  const offsets = layoutOffsetsOf(text.layout);
911
- const layoutUnits = text.layoutStart + (units - text.textStart);
1890
+ const layoutUnits = layoutOffsetOf(text, units);
912
1891
  const caret = text.layout.caretPosition(
913
1892
  codePointAtOffset(offsets, layoutUnits),
914
1893
  );
@@ -930,56 +1909,152 @@ function caretAt(
930
1909
  /** Every band a document range covers, in window coordinates. The subtree
931
1910
  * text ranges prune the walk to the boxes the range actually crosses, so a
932
1911
  * drag's small range costs its own paragraphs; only Ctrl+A pays for all. */
933
- function collectBands(
934
- box: Box,
1912
+ /**
1913
+ * What an inline box reaches on each line besides its text, as `out`
1914
+ * rectangles as tall as the line: its own padding and border where it
1915
+ * starts or ends there — its edge less its margin, which is outside it —
1916
+ * and the inline-blocks and inline boxes' edges inside it, whole. The rest
1917
+ * of its border box across, which its text alone missed: a padded link was
1918
+ * as wide as its words (`elementRect`).
1919
+ */
1920
+ function fragmentReach(box: Box, out: Rect[]): void {
1921
+ let block = box.parent;
1922
+ while (block && !block.lines) block = block.parent;
1923
+ if (!block?.lines) return;
1924
+ const inside = (b: Box | null): boolean => {
1925
+ for (let at = b; at && at !== block; at = at.parent) {
1926
+ if (at === box) return true;
1927
+ }
1928
+ return false;
1929
+ };
1930
+ const rtl = box.style.direction === 'rtl';
1931
+ for (const line of block.lines) {
1932
+ let left = Infinity;
1933
+ let right = -Infinity;
1934
+ for (const edge of line.edges ?? []) {
1935
+ if (edge.box === box) {
1936
+ // the element's `direction` says which side its start is on, as
1937
+ // paint reads it (`paintInlineBoxes`)
1938
+ const onLeft = (edge.side === 'start') !== rtl;
1939
+ left = Math.min(left, onLeft ? edge.x + box.marginLeft : edge.x);
1940
+ right = Math.max(
1941
+ right,
1942
+ onLeft ? edge.x + edge.width : edge.x + edge.width - box.marginRight,
1943
+ );
1944
+ } else if (inside(edge.box)) {
1945
+ left = Math.min(left, edge.x);
1946
+ right = Math.max(right, edge.x + edge.width);
1947
+ }
1948
+ }
1949
+ for (const placed of line.atomics) {
1950
+ if (!inside(placed.box)) continue;
1951
+ left = Math.min(left, placed.x - placed.box.marginLeft);
1952
+ right = Math.max(
1953
+ right,
1954
+ placed.x + placed.box.width + placed.box.marginRight,
1955
+ );
1956
+ }
1957
+ if (right > left) {
1958
+ out.push({
1959
+ x: left,
1960
+ y: line.y,
1961
+ width: right - left,
1962
+ height: line.height,
1963
+ });
1964
+ }
1965
+ }
1966
+ }
1967
+
1968
+ /**
1969
+ * An inline element's bands made as tall as its fragments' border boxes,
1970
+ * as CSSOM View takes an inline box's client rects (6.1): its font's
1971
+ * content area about its own baseline, with the padding and the border
1972
+ * above and below it, which take no room on the line (CSS 2.1 10.6.1) and
1973
+ * which paint draws the background over (`paintInlineBoxes`). A band is a
1974
+ * line's height across until then: a link padded 10px below measured as
1975
+ * tall as its line and no taller.
1976
+ */
1977
+ function fragmentHeights(box: Box, bands: Rect[], fonts: FontsLike): void {
1978
+ let block = box.parent;
1979
+ while (block && !block.lines) block = block.parent;
1980
+ if (!block?.lines?.length) return;
1981
+ const face = faceExtentOf(fonts, box.style);
1982
+ const byTop = new Map<number, LineBox>();
1983
+ for (const line of block.lines) byTop.set(line.y, line);
1984
+ for (let i = 0; i < bands.length; i += 1) {
1985
+ const band = bands[i];
1986
+ const line = byTop.get(band.y);
1987
+ if (!line) continue;
1988
+ // on its own baseline, which `vertical-align` may raise off the line's
1989
+ const raise = SHIFTED_LINES.has(line)
1990
+ ? (LINE_BOX_RAISES.get(line)?.get(box) ?? BOX_RAISES.get(box) ?? 0)
1991
+ : 0;
1992
+ const baseline = line.y + line.baseline - raise;
1993
+ const top = baseline - face.ascent - box.padTop - box.borderTop;
1994
+ const bottom = baseline + face.descent + box.padBottom + box.borderBottom;
1995
+ bands[i] = { x: band.x, y: top, width: band.width, height: bottom - top };
1996
+ }
1997
+ }
1998
+
1999
+ /** The bands of a text range on one block's own lines. */
2000
+ function lineBands(
2001
+ lines: readonly LineBox[],
935
2002
  from: number,
936
2003
  to: number,
937
2004
  dx: number,
938
2005
  dy: number,
939
2006
  out: Rect[],
940
2007
  ): void {
941
- if (box.subtreeTextEnd <= box.subtreeTextStart) return;
942
- if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
943
- if (box.lines) {
944
- const lines = box.lines;
945
- // First line whose text can reach `from`, by binary search over the
946
- // sorted text starts; stop at the first line past `to`.
947
- let lo = 0;
948
- let hi = lines.length;
949
- while (lo < hi) {
950
- const mid = (lo + hi) >> 1;
951
- if (lines[mid].textEnd > from) hi = mid;
952
- else lo = mid + 1;
953
- }
954
- for (let i = lo; i < lines.length; i += 1) {
955
- const line = lines[i];
956
- if (line.textStart >= to) break;
957
- if (line.textEnd > from && line.textStart < to) {
958
- for (const text of line.texts) {
959
- const natural = text.layout.lines[text.layoutLine];
960
- if (!natural) continue;
961
- const a = Math.max(from, text.textStart);
962
- const b = Math.min(to, text.textEnd);
963
- if (b <= a) continue;
964
- const offsets = layoutOffsetsOf(text.layout);
965
- for (const band of bandsFor(
966
- text.layout,
967
- natural,
968
- offsets,
969
- text.layoutStart + (a - text.textStart),
970
- text.layoutStart + (b - text.textStart),
971
- )) {
972
- out.push({
973
- x: dx + band.x + text.drawX,
974
- y: dy + line.y,
975
- width: band.width,
976
- height: line.height,
977
- });
978
- }
2008
+ // First line whose text can reach `from`, by binary search over the
2009
+ // sorted text starts; stop at the first line past `to`.
2010
+ let lo = 0;
2011
+ let hi = lines.length;
2012
+ while (lo < hi) {
2013
+ const mid = (lo + hi) >> 1;
2014
+ if (lines[mid].textEnd > from) hi = mid;
2015
+ else lo = mid + 1;
2016
+ }
2017
+ for (let i = lo; i < lines.length; i += 1) {
2018
+ const line = lines[i];
2019
+ if (line.textStart >= to) break;
2020
+ if (line.textEnd > from && line.textStart < to) {
2021
+ for (const text of line.texts) {
2022
+ const natural = text.layout.lines[text.layoutLine];
2023
+ if (!natural) continue;
2024
+ const a = Math.max(from, text.textStart);
2025
+ const b = Math.min(to, text.textEnd);
2026
+ if (b <= a) continue;
2027
+ const offsets = layoutOffsetsOf(text.layout);
2028
+ for (const band of bandsFor(
2029
+ text.layout,
2030
+ natural,
2031
+ offsets,
2032
+ layoutOffsetOf(text, a),
2033
+ layoutOffsetOf(text, b, true),
2034
+ )) {
2035
+ out.push({
2036
+ x: dx + band.x + text.drawX,
2037
+ y: dy + line.y,
2038
+ width: band.width,
2039
+ height: line.height,
2040
+ });
979
2041
  }
980
2042
  }
981
2043
  }
982
2044
  }
2045
+ }
2046
+
2047
+ function collectBands(
2048
+ box: Box,
2049
+ from: number,
2050
+ to: number,
2051
+ dx: number,
2052
+ dy: number,
2053
+ out: Rect[],
2054
+ ): void {
2055
+ if (box.subtreeTextEnd <= box.subtreeTextStart) return;
2056
+ if (box.subtreeTextEnd <= from || box.subtreeTextStart >= to) return;
2057
+ if (box.lines) lineBands(box.lines, from, to, dx, dy, out);
983
2058
  // Atomics are ordinary children, reached below.
984
2059
  for (const child of box.children) {
985
2060
  if (child.kind === 'text' || child.kind === 'break') continue;
@@ -987,10 +2062,118 @@ function collectBands(
987
2062
  }
988
2063
  }
989
2064
 
2065
+ /** The layers of a hit test, in the order CSS paints them within a
2066
+ * context (`deepestAt`). */
2067
+ const HIT_NEGATIVE = -1;
2068
+ const HIT_BLOCK = 0;
2069
+ const HIT_FLOAT = 1;
2070
+ const HIT_INLINE = 2;
2071
+ const HIT_POSITIONED = 3;
2072
+
990
2073
  /** The deepest element box containing a document-space point. */
991
- function deepestAt(box: Box, x: number, y: number): Element | null {
2074
+ function deepestAt(
2075
+ tree: BoxTree,
2076
+ x: number,
2077
+ y: number,
2078
+ /** Told whether what was found was found under text: a run's, rather
2079
+ * than a box's. */
2080
+ hit?: { text: boolean },
2081
+ ): Element | null {
2082
+ const box = tree.root;
992
2083
  let found: Element | null = box.el;
993
- const visit = (node: Box): void => {
2084
+ let foundKey: readonly number[] = [];
2085
+ let viaText = false;
2086
+ // What is under a point is what was painted there last (CSS 2.1 Appendix
2087
+ // E): within a context, an in-flow block's own box, then a float over it,
2088
+ // then a line's text and atomics, then a positioned box — and a float, an
2089
+ // atomic or a positioned box paints its own content whole in its turn,
2090
+ // those layers again inside it. So a hit carries the layers down to it,
2091
+ // and takes the place of the one before where those come after them, or
2092
+ // tie: an infobox floated out of one section hangs over the next, whose
2093
+ // own box took every link in it, and a skin that puts the article in a
2094
+ // `position: relative` box put all of it in one layer. Positioned boxes
2095
+ // are painted in `z-index` order and then the document's (`byZIndex`),
2096
+ // the negative ones under the flow: the Zen Garden's `›` has
2097
+ // `z-index: 3` over the bar the "View All Designs" link fills after it.
2098
+ const take = (el: Element, key: readonly number[], text: boolean) => {
2099
+ if (compareKeys(key, foundKey) < 0) return;
2100
+ found = el;
2101
+ foundKey = key;
2102
+ viaText = text;
2103
+ };
2104
+ // A box is walked into wherever it draws — its reach, overflow and all
2105
+ // (`computePaintBounds`) — and named only where its own rectangle is. A
2106
+ // page's `html, body { height: 100% }` is one viewport tall and its
2107
+ // article overflows it, and a walk that went no further than a box's own
2108
+ // rectangle found nothing below the first screen: once Wikipedia was
2109
+ // scrolled, no link lit up. One whose reach is not known yet — before its
2110
+ // first paint — is walked into at its rectangle, as before.
2111
+ //
2112
+ // A box that clips what overflows it hides what it holds past its edge —
2113
+ // but not the positioned boxes whose containing block is outside it (CSS
2114
+ // 2.1 11.1.1), which paint puts off until its clip ends
2115
+ // (`paintPositioned`). So past the edge the walk goes on only into a box
2116
+ // that holds positioned ones, carrying the clips the point is outside
2117
+ // of, and names nothing until a positioned box escapes them all: the
2118
+ // Zen Garden's archive links are absolute `<li>`s in an `overflow:
2119
+ // hidden` list with no height of its own, and not one of them could be
2120
+ // hovered or pressed.
2121
+ const enter = (
2122
+ child: Box,
2123
+ context: readonly number[],
2124
+ clipped: readonly Box[],
2125
+ ): void => {
2126
+ const style = child.style;
2127
+ if (
2128
+ style.position !== 'static' ||
2129
+ (child.parent?.kind === 'flex' && typeof style.zIndex === 'number')
2130
+ ) {
2131
+ // a flex item with a `z-index` is layered unpositioned (`layered`)
2132
+ const z = style.zIndex === 'auto' ? 0 : style.zIndex;
2133
+ context = [...context, z < 0 ? HIT_NEGATIVE : HIT_POSITIONED, z];
2134
+ } else if (style.float !== 'none') context = [...context, HIT_FLOAT];
2135
+ if (clipped.length !== 0 && child.outOfFlow) {
2136
+ const containing = containingBlockOf(child);
2137
+ clipped = containing
2138
+ ? clipped.filter((clip) => holds(clip, containing))
2139
+ : [];
2140
+ }
2141
+ const inside =
2142
+ x >= child.x &&
2143
+ x < child.x + child.width &&
2144
+ y >= child.y &&
2145
+ y < child.y + child.height;
2146
+ const own = inside && hasRect(child);
2147
+ if (!own) {
2148
+ const reach = child.boundsY;
2149
+ const known =
2150
+ Number.isFinite(reach) &&
2151
+ (child.boundsWidth > 0 || child.boundsHeight > 0);
2152
+ if (!known) {
2153
+ if (!inside) return;
2154
+ } else if (
2155
+ x < child.boundsX ||
2156
+ x >= child.boundsX + child.boundsWidth ||
2157
+ y < reach ||
2158
+ y >= reach + child.boundsHeight
2159
+ ) {
2160
+ return;
2161
+ }
2162
+ if (clipsOverflow(child)) {
2163
+ if (!holdsAbsolute(child)) return;
2164
+ clipped = [...clipped, child];
2165
+ }
2166
+ }
2167
+ if (own && child.el && clipped.length === 0) {
2168
+ take(child.el, [...context, HIT_BLOCK], false);
2169
+ }
2170
+ visit(child, context, clipped);
2171
+ };
2172
+ const visit = (
2173
+ node: Box,
2174
+ context: readonly number[],
2175
+ clipped: readonly Box[],
2176
+ ): void => {
994
2177
  // The paint index answers a point query too — the wide level of a flat
995
2178
  // document is the root's child list, and a hit test that walked all of
996
2179
  // it would run per pointer move once hover is in the picture.
@@ -999,55 +2182,37 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
999
2182
  : node.children;
1000
2183
  for (const child of candidates) {
1001
2184
  if (child.kind === 'text' || child.kind === 'break') continue;
1002
- if (
1003
- x >= child.x &&
1004
- x < child.x + child.width &&
1005
- y >= child.y &&
1006
- y < child.y + child.height
1007
- ) {
1008
- if (child.el) found = child.el;
1009
- visit(child);
1010
- }
2185
+ enter(child, context, clipped);
1011
2186
  }
1012
2187
  if (node.paintIndex && node.positionedPaint) {
1013
2188
  for (const child of node.positionedPaint) {
1014
- if (
1015
- x >= child.x &&
1016
- x < child.x + child.width &&
1017
- y >= child.y &&
1018
- y < child.y + child.height
1019
- ) {
1020
- if (child.el) found = child.el;
1021
- visit(child);
1022
- }
2189
+ enter(child, context, clipped);
1023
2190
  }
1024
2191
  }
1025
2192
  if (node.lines) {
1026
2193
  for (const line of node.lines) {
1027
2194
  for (const placed of line.atomics) {
1028
- if (
1029
- x >= placed.box.x &&
1030
- x < placed.box.x + placed.box.width &&
1031
- y >= placed.box.y &&
1032
- y < placed.box.y + placed.box.height
1033
- ) {
1034
- if (placed.box.el) found = placed.box.el;
1035
- visit(placed.box);
1036
- }
2195
+ enter(placed.box, [...context, HIT_INLINE], clipped);
1037
2196
  }
1038
2197
  // An inline box has no box of its own — its extent is the runs on
1039
2198
  // this line — so the element under a point inside a paragraph is
1040
- // found from the run rather than from a rectangle.
1041
- if (y >= line.y && y < line.y + line.height) {
2199
+ // found from the run rather than from a rectangle: from where its
2200
+ // text sits in the document, whose text boxes know their element.
2201
+ // Not from the run itself, whose layout may be one an earlier parse
2202
+ // made (`TextLayoutCache`), and which an engine may hand back with
2203
+ // nothing on it but its extent.
2204
+ if (clipped.length === 0 && y >= line.y && y < line.y + line.height) {
1042
2205
  for (const text of line.texts) {
1043
2206
  const natural = text.layout.lines[text.layoutLine];
1044
2207
  if (!natural) continue;
1045
2208
  for (const run of natural.runs) {
1046
2209
  const left = text.drawX + natural.x + run.x;
1047
2210
  if (x >= left && x < left + run.width) {
1048
- const owner = (run.span as { element?: Element } | undefined)
1049
- ?.element;
1050
- if (owner) found = owner;
2211
+ const owner = ownerOf(
2212
+ tree.textBoxes,
2213
+ text.spans.documentAt(run.start),
2214
+ );
2215
+ if (owner) take(owner, [...context, HIT_INLINE], true);
1051
2216
  }
1052
2217
  }
1053
2218
  }
@@ -1055,8 +2220,272 @@ function deepestAt(box: Box, x: number, y: number): Element | null {
1055
2220
  }
1056
2221
  }
1057
2222
  };
1058
- visit(box);
2223
+ visit(box, [], []);
2224
+ if (hit) hit.text = viaText;
1059
2225
  return found;
1060
2226
  }
1061
2227
 
2228
+ /** Paint order between two hits' layers, outermost first: negative where
2229
+ * `a` was painted under `b`. */
2230
+ function compareKeys(a: readonly number[], b: readonly number[]): number {
2231
+ const n = Math.min(a.length, b.length);
2232
+ for (let i = 0; i < n; i += 1) if (a[i] !== b[i]) return a[i] - b[i];
2233
+ return a.length - b.length;
2234
+ }
2235
+
2236
+ // --- a pointer move restyled where it happened (`_hoverInPlace`) -----------
2237
+
2238
+ /** How many elements a pointer move restyles in place before the document
2239
+ * is built again instead: a link's subtree is a handful, and a compound
2240
+ * that matches a container reaches everything in it. */
2241
+ const HOVER_RESTYLE_LIMIT = 300;
2242
+
2243
+ /**
2244
+ * The computed properties a pointer move may change in place: ink, which
2245
+ * moves nothing. A background colour is drawn inside the box it colours
2246
+ * and a border's colour on the border it has; `box-shadow`, `text-shadow`
2247
+ * and an outline's width reach past it, where the paint index has already
2248
+ * looked, and a background image has to be fetched.
2249
+ */
2250
+ const PAINT_ONLY = new Set([
2251
+ 'color',
2252
+ 'textFillColor',
2253
+ 'cursor',
2254
+ 'backgroundColor',
2255
+ 'borderTopColor',
2256
+ 'borderRightColor',
2257
+ 'borderBottomColor',
2258
+ 'borderLeftColor',
2259
+ 'outlineColor',
2260
+ 'textDecorationLine',
2261
+ 'textDecorationColor',
2262
+ 'textDecorationStyle',
2263
+ 'textDecorationThickness',
2264
+ 'textUnderlineOffset',
2265
+ 'underline',
2266
+ 'underlineStyle',
2267
+ 'underlineThickness',
2268
+ 'underlineOffset',
2269
+ 'lineThrough',
2270
+ ]);
2271
+
2272
+ /** The fields of a run its ink is: what `runFor` takes from `PAINT_ONLY`. */
2273
+ const INK_FIELDS = [
2274
+ 'color',
2275
+ 'underline',
2276
+ 'underlineStyle',
2277
+ 'underlineOffset',
2278
+ 'underlineThickness',
2279
+ 'strike',
2280
+ ] as const;
2281
+
2282
+ /** The fields of a run its shape is, which ink never changes. */
2283
+ const FACE_FIELDS = [
2284
+ 'family',
2285
+ 'size',
2286
+ 'weight',
2287
+ 'style',
2288
+ 'letterSpacing',
2289
+ 'features',
2290
+ ] as const;
2291
+
2292
+ /** Whether two styles differ in ink alone: true where they do, null where
2293
+ * they do not differ, false where something else does. Custom properties
2294
+ * are read through the properties that use them. */
2295
+ function inkOnly(was: ComputedStyle, now: ComputedStyle): boolean | null {
2296
+ const a = was as unknown as Record<string, unknown>;
2297
+ const b = now as unknown as Record<string, unknown>;
2298
+ let ink = false;
2299
+ for (const key in b) {
2300
+ if (key === 'custom' || sameValue(a[key], b[key])) continue;
2301
+ if (!PAINT_ONLY.has(key)) return false;
2302
+ ink = true;
2303
+ }
2304
+ return ink ? true : null;
2305
+ }
2306
+
2307
+ /** Structural equality for a computed value: a number, a string, or the
2308
+ * plain objects and arrays a length or a shadow list is. */
2309
+ function sameValue(x: unknown, y: unknown): boolean {
2310
+ if (x === y) return true;
2311
+ if (typeof x === 'number' && typeof y === 'number') {
2312
+ return Number.isNaN(x) && Number.isNaN(y);
2313
+ }
2314
+ if (!x || !y || typeof x !== 'object' || typeof y !== 'object') return false;
2315
+ if (Array.isArray(x) !== Array.isArray(y)) return false;
2316
+ const a = x as Record<string, unknown>;
2317
+ const b = y as Record<string, unknown>;
2318
+ const keys = Object.keys(a);
2319
+ if (keys.length !== Object.keys(b).length) return false;
2320
+ for (const key of keys) if (!sameValue(a[key], b[key])) return false;
2321
+ return true;
2322
+ }
2323
+
2324
+ /** The element an anonymous box takes its style from: its nearest
2325
+ * ancestor's that has one. */
2326
+ function nearestElement(box: Box): Element | null {
2327
+ for (let at = box.parent; at; at = at.parent) if (at.el) return at.el;
2328
+ return null;
2329
+ }
2330
+
2331
+ /**
2332
+ * `text`'s layout made again with the ink of the elements a pointer move
2333
+ * restyled: from the runs it was made from, each of a changed element's
2334
+ * given the ink its new style makes (`runFor`) and nothing else. Null where
2335
+ * none of its runs is theirs; false where that cannot be told — a run this
2336
+ * cannot place in the document, one some other pass inked (a first line's),
2337
+ * or a layout that comes out another shape.
2338
+ */
2339
+ function reinked(
2340
+ text: LineText,
2341
+ layouts: TextLayoutCache,
2342
+ changed: ReadonlyMap<Element, ComputedStyle>,
2343
+ styles: BoxTree['styles'],
2344
+ ): TextLayoutLike | null | false {
2345
+ const spans = text.spans;
2346
+ const ownerAt = (offset: number): Element | null =>
2347
+ spans.boxAt ? (spans.boxAt(offset)?.el ?? null) : null;
2348
+ const inputs = layouts.inputsOf(text.layout);
2349
+ if (!inputs || !spans.boxAt) {
2350
+ // not one this could make again: none of its text may be theirs
2351
+ for (const line of text.layout.lines) {
2352
+ for (const run of line.runs) {
2353
+ const owner = spans.boxAt ? ownerAt(run.start) : null;
2354
+ if (!spans.boxAt || (owner && changed.has(owner))) return false;
2355
+ }
2356
+ }
2357
+ return null;
2358
+ }
2359
+ let runs: TextRun[] | null = null;
2360
+ let offset = 0;
2361
+ const content = inputs.content;
2362
+ for (let i = 0; i < content.length; i += 1) {
2363
+ const run = content[i];
2364
+ const length = run.text.length;
2365
+ const owner = length ? ownerAt(offset) : null;
2366
+ const style = owner ? changed.get(owner) : undefined;
2367
+ if (owner && style) {
2368
+ if (ownerAt(offset + length - 1) !== owner) return false;
2369
+ const was = runFor(run.text, styles.get(owner)!.style);
2370
+ const now = runFor(run.text, style);
2371
+ for (const f of FACE_FIELDS) if (!sameValue(was[f], now[f])) return false;
2372
+ let next: Record<string, unknown> | null = null;
2373
+ for (const f of INK_FIELDS) {
2374
+ // a run that is not what its style made — a first line's colour
2375
+ if (!sameValue(run[f], was[f])) return false;
2376
+ if (sameValue(was[f], now[f])) continue;
2377
+ next ??= { ...run };
2378
+ if (now[f] === undefined) delete next[f];
2379
+ else next[f] = now[f];
2380
+ }
2381
+ if (next) {
2382
+ runs ??= content.slice();
2383
+ runs[i] = next as unknown as TextRun;
2384
+ }
2385
+ }
2386
+ offset += length;
2387
+ }
2388
+ if (!runs) return null;
2389
+ const layout = layouts.fonts.layout(runs, inputs.style, inputs.options);
2390
+ return sameShape(text.layout, layout) ? layout : false;
2391
+ }
2392
+
2393
+ /** Whether two layouts put the same text in the same places: ink aside,
2394
+ * the same layout. */
2395
+ function sameShape(a: TextLayoutLike, b: TextLayoutLike): boolean {
2396
+ if (a.width !== b.width || a.height !== b.height) return false;
2397
+ if (a.lines.length !== b.lines.length) return false;
2398
+ for (let i = 0; i < a.lines.length; i += 1) {
2399
+ const x = a.lines[i];
2400
+ const y = b.lines[i];
2401
+ if (
2402
+ x.x !== y.x ||
2403
+ x.y !== y.y ||
2404
+ x.width !== y.width ||
2405
+ x.height !== y.height ||
2406
+ x.baseline !== y.baseline ||
2407
+ x.ascent !== y.ascent ||
2408
+ x.descent !== y.descent ||
2409
+ x.start !== y.start ||
2410
+ x.end !== y.end ||
2411
+ x.runs.length !== y.runs.length
2412
+ ) {
2413
+ return false;
2414
+ }
2415
+ for (let j = 0; j < x.runs.length; j += 1) {
2416
+ const r = x.runs[j];
2417
+ const t = y.runs[j];
2418
+ if (
2419
+ r.x !== t.x ||
2420
+ r.width !== t.width ||
2421
+ r.start !== t.start ||
2422
+ r.end !== t.end
2423
+ ) {
2424
+ return false;
2425
+ }
2426
+ }
2427
+ }
2428
+ return true;
2429
+ }
2430
+
2431
+ /** The element whose text holds a document index: the text box around it,
2432
+ * found by bisecting the boxes in document order. */
2433
+ function ownerOf(boxes: readonly Box[], index: number): Element | null {
2434
+ let lo = 0;
2435
+ let hi = boxes.length - 1;
2436
+ while (lo < hi) {
2437
+ const mid = (lo + hi + 1) >> 1;
2438
+ if (boxes[mid].textStart <= index) lo = mid;
2439
+ else hi = mid - 1;
2440
+ }
2441
+ const box = boxes[lo];
2442
+ return box && index >= box.textStart && index < box.textEnd ? box.el : null;
2443
+ }
2444
+
1062
2445
  export type { ControlRect, ReplacedKind, ResourceRequest, ResourceResult };
2446
+
2447
+ /** The advance of a face's "0", laid out: null where the engine cannot
2448
+ * lay it out. */
2449
+ function zeroWidthOf(fonts: FontsLike, face: MetricFace): number | null {
2450
+ const { family, size, weight } = face;
2451
+ // a run is upright or slanted, as `inline.ts` sets one
2452
+ const style = face.style === 'normal' ? 'normal' : 'italic';
2453
+ try {
2454
+ const run = { family, size, weight, style } as const;
2455
+ return fonts.layout([{ text: '0', ...run }], run, {}).width;
2456
+ } catch {
2457
+ return null;
2458
+ }
2459
+ }
2460
+
2461
+ /** A face's own line height, as the engine reports it: null where it
2462
+ * cannot say. */
2463
+ function normalLineOf(fonts: FontsLike, face: MetricFace): number | null {
2464
+ const { family, size, weight, style } = face;
2465
+ try {
2466
+ const line = fonts
2467
+ .match(family, { size, weight, style })
2468
+ .metrics(size).lineHeight;
2469
+ return line > 0 ? line : null;
2470
+ } catch {
2471
+ return null;
2472
+ }
2473
+ }
2474
+
2475
+ /** A face's x-height, as the engine reports it: null where the face
2476
+ * states none, or the engine does not say. */
2477
+ function xHeightOf(fonts: FontsLike, face: MetricFace): number | null {
2478
+ const { family, size, weight, style } = face;
2479
+ try {
2480
+ const metrics = fonts
2481
+ .match(family, { size, weight, style })
2482
+ .metrics(size) as {
2483
+ xHeight?: number | null;
2484
+ };
2485
+ const x = metrics.xHeight;
2486
+ // NaN where the face states none, which is a number
2487
+ return typeof x === 'number' && x > 0 ? x : null;
2488
+ } catch {
2489
+ return null;
2490
+ }
2491
+ }