@react-x11/components 0.8.0 → 0.10.0

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