@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
@@ -21,12 +21,36 @@ import { Element as DomElement, isTag } from 'domhandler';
21
21
  import type { Element } from 'domhandler';
22
22
 
23
23
  import { attr, tagOf } from '../dom.js';
24
- import { mediaMatches } from './parse.js';
24
+ import { svgSizeHint } from '../svg.js';
25
+ import {
26
+ asciiLower,
27
+ escapeEnd,
28
+ mediaMatches,
29
+ readIdent,
30
+ startsIdent,
31
+ } from './parse.js';
25
32
  import type { Declaration, StyleRule, Stylesheet } from './parse.js';
26
- import { applyDeclaration, blockify, inherit, initialStyle } from './style.js';
33
+ import {
34
+ applyDeclaration,
35
+ blockify,
36
+ settleContentVisibility,
37
+ settleOverflow,
38
+ settleClamp,
39
+ copyStyle,
40
+ decorate,
41
+ inherit,
42
+ initialOne,
43
+ initialStyle,
44
+ isInherited,
45
+ } from './style.js';
27
46
  import type { ComputedStyle, RootLook } from './style.js';
47
+ import type { FontFamilies } from '../fonts.js';
28
48
  import { parseDeclarations } from './parse.js';
49
+ import { CounterStyles, counterStyleRule } from './counter-styles.js';
50
+ import type { CounterStyleRule } from './counter-styles.js';
29
51
  import type { UnitContext } from './values.js';
52
+ import { customProperties, substituteIn } from './vars.js';
53
+ import type { CustomProps } from './vars.js';
30
54
 
31
55
  /** Where a declaration came from. Higher wins before specificity is asked. */
32
56
  const enum Origin {
@@ -40,6 +64,8 @@ const enum Origin {
40
64
 
41
65
  interface Candidate {
42
66
  origin: Origin;
67
+ /** The rule's cascade layer, null for none (`StyleRule.layer`). */
68
+ layer: readonly number[] | null;
43
69
  specificity: number;
44
70
  order: number;
45
71
  declarations: Declaration[];
@@ -59,6 +85,13 @@ type CssSelectAdapter = typeof DomUtils & {
59
85
  isVisited(el: Element): boolean;
60
86
  };
61
87
 
88
+ /** A style handed to every element with the same sharing key, and the key
89
+ * that element's children share under (`Cascade.sharedStyleFor`). */
90
+ export interface SharedStyle {
91
+ style: ComputedStyle;
92
+ key: number;
93
+ }
94
+
62
95
  /** A compiled matcher, kept beside the rule it came from. */
63
96
  interface IndexedRule {
64
97
  rule: StyleRule;
@@ -66,8 +99,26 @@ interface IndexedRule {
66
99
  /** Set once compilation has been attempted, so a selector `css-select`
67
100
  * refuses is not recompiled once per element for the rest of the pass. */
68
101
  compiled: boolean;
102
+ /** Which rule this is, in a sharing key (`Cascade.sharedStyleFor`). */
103
+ id: number;
69
104
  }
70
105
 
106
+ /**
107
+ * What makes a selector's match depend on more than the element's own tag and
108
+ * attributes and its ancestors' — its siblings, where it sits among them,
109
+ * what it contains, or where the document is scrolled to. Two elements that
110
+ * look alike from the root down can still differ under a rule with one of
111
+ * these, so an element such a rule could reach does not share its style
112
+ * (`Cascade.sharedStyleFor`). css-select spells some of them under other
113
+ * names, and those count too: `:checked` and `:selected` read which option
114
+ * comes first, `:disabled` and `:enabled` which legend does, and `:parent`
115
+ * and `:contains()` the element's contents. Over-broad on purpose: a `+` or
116
+ * `~` inside an attribute value, or the `~=` operator, costs sharing and
117
+ * nothing else.
118
+ */
119
+ const UNSHAREABLE =
120
+ /[+~]|:(?:first|last|only)-(?:child|of-type)|:nth-|:empty|:blank|:has\(|:focus-within|:target|:scope|:(?:checked|selected|disabled|enabled|parent)\b|:i?contains\(/;
121
+
71
122
  /**
72
123
  * Rules bucketed by the key of their rightmost compound selector. An element
73
124
  * only ever tries `id`, its classes, its tag and the universal bucket.
@@ -80,13 +131,59 @@ class RuleIndex {
80
131
  /** Whether any rule in here is pointer-sensitive, so the renderer knows
81
132
  * whether a pointer move can change the cascade at all. */
82
133
  hoverSensitive = false;
134
+ /** The buckets holding a rule an element cannot share its style under
135
+ * (`UNSHAREABLE`): the ids, classes and tags whose elements compute
136
+ * their own, and whether the universal bucket makes every element do so. */
137
+ readonly ownStyleIds = new Set<string>();
138
+ readonly ownStyleClasses = new Set<string>();
139
+ readonly ownStyleTags = new Set<string>();
140
+ ownStyleEverywhere = false;
141
+ /** How many rules are in here, so an empty index costs one comparison. */
142
+ size = 0;
143
+ private _nextId = 0;
144
+
145
+ /**
146
+ * Whether a bucket here could hold a rule for `el`, by its tag, id and
147
+ * classes and without matching anything: an index of a rule or two is
148
+ * asked of every element — the user-agent sheet's `q::before` and
149
+ * `q::after` made every element of a document split its `class` twice —
150
+ * and almost none of them has a rule in it.
151
+ */
152
+ reaches(el: Element): boolean {
153
+ if (this.universal.length || this.byTag.has(tagOf(el))) return true;
154
+ if (this.byId.size) {
155
+ const id = attr(el, 'id');
156
+ if (id && this.byId.has(id)) return true;
157
+ }
158
+ if (this.byClass.size) {
159
+ const className = attr(el, 'class');
160
+ if (className) {
161
+ for (const name of className.split(/\s+/)) {
162
+ if (name && this.byClass.has(name)) return true;
163
+ }
164
+ }
165
+ }
166
+ return false;
167
+ }
83
168
 
84
169
  add(rule: StyleRule): void {
85
- const indexed: IndexedRule = { rule, match: null, compiled: false };
170
+ this.size += 1;
171
+ const indexed: IndexedRule = {
172
+ rule,
173
+ match: null,
174
+ compiled: false,
175
+ id: this._nextId++,
176
+ };
86
177
  if (rule.selector.includes(':hover') || rule.selector.includes(':active')) {
87
178
  this.hoverSensitive = true;
88
179
  }
89
180
  const key = rightmostKey(rule.selector);
181
+ if (UNSHAREABLE.test(rule.selector)) {
182
+ if (key.kind === 'id') this.ownStyleIds.add(key.name);
183
+ else if (key.kind === 'class') this.ownStyleClasses.add(key.name);
184
+ else if (key.kind === 'tag') this.ownStyleTags.add(key.name);
185
+ else this.ownStyleEverywhere = true;
186
+ }
90
187
  const bucket =
91
188
  key.kind === 'id'
92
189
  ? mapBucket(this.byId, key.name)
@@ -99,6 +196,204 @@ class RuleIndex {
99
196
  }
100
197
  }
101
198
 
199
+ /** The pseudo-elements a rule can style here. */
200
+ type PseudoElement =
201
+ 'before' | 'after' | 'first-letter' | 'first-line' | 'marker';
202
+
203
+ /** A selector's trailing `::before`, `::after`, `::first-letter`,
204
+ * `::first-line` or `::marker`, or CSS 2's single-colon spelling of any of
205
+ * its four. */
206
+ const PSEUDO_ELEMENT = /::?(before|after|first-letter|first-line)$|::marker$/i;
207
+
208
+ /**
209
+ * A rule for a pseudo-element, as the pseudo-element it styles and a rule
210
+ * for the element it hangs off, which is what gets matched. The specificity
211
+ * is the whole selector's, the pseudo-element counted in. `p::before`
212
+ * matches `p`; `p ::before` and `p > ::before` have nothing left of their
213
+ * last compound and match any child, `p *` and `p > *`.
214
+ */
215
+ function splitPseudoElement(
216
+ rule: StyleRule,
217
+ ): { which: PseudoElement; rule: StyleRule } | null {
218
+ const m = PSEUDO_ELEMENT.exec(rule.selector);
219
+ if (!m) return null;
220
+ const head = rule.selector.slice(0, m.index);
221
+ const trimmed = head.trimEnd();
222
+ const selector = !trimmed
223
+ ? '*'
224
+ : head !== trimmed || /[>+~]$/.test(trimmed)
225
+ ? `${trimmed} *`
226
+ : trimmed;
227
+ return {
228
+ which: (m[1] ?? 'marker').toLowerCase() as PseudoElement,
229
+ rule: { ...rule, selector },
230
+ };
231
+ }
232
+
233
+ /**
234
+ * The compounds of a selector that test `:hover`, each without it (and
235
+ * without an `:active` beside it), and whether one is followed by a sibling
236
+ * combinator, which reaches the element's later siblings. `nested` where a
237
+ * `:hover` sits inside a functional pseudo-class — `:not(:hover)`,
238
+ * `:is(:hover)` — and reaches elements no compound names.
239
+ *
240
+ * A `:hover` in a compound's `:has()` is the one exception, since what it
241
+ * reaches is known: the compound, without its `:has()`, is an ancestor of
242
+ * the element the pointer is over — or an earlier sibling of it or of an
243
+ * ancestor, where the argument starts at a sibling (Selectors 4, 4.5). So
244
+ * it is given as an anchor (`has`) rather than making the rule `nested`.
245
+ *
246
+ * `:active` is not the pointer's here: nothing sets it (`setPointer` is
247
+ * handed none), so a selector that tests only it never changes as the
248
+ * pointer moves — Wikipedia's buttons' `:focus:not(:active)` among them.
249
+ * A press that sets it would have to be counted here too.
250
+ */
251
+ export function pointerCompounds(selector: string): {
252
+ compounds: string[];
253
+ siblings: boolean;
254
+ nested: boolean;
255
+ has: { anchor: string; siblings: boolean }[];
256
+ } {
257
+ const out = {
258
+ compounds: [] as string[],
259
+ siblings: false,
260
+ nested: false,
261
+ has: [] as { anchor: string; siblings: boolean }[],
262
+ };
263
+ // the compounds at the top level, each with the combinator after it
264
+ const parts: { text: string; next: string }[] = [];
265
+ let start = 0;
266
+ let depth = 0;
267
+ let quote = '';
268
+ const split = (end: number, next: string): void => {
269
+ const text = selector.slice(start, end).trim();
270
+ if (text) parts.push({ text, next });
271
+ else if (parts.length && next !== ' ') parts[parts.length - 1].next = next;
272
+ };
273
+ for (let i = 0; i < selector.length; i += 1) {
274
+ const c = selector[i];
275
+ if (quote) {
276
+ if (c === quote && selector[i - 1] !== '\\') quote = '';
277
+ continue;
278
+ }
279
+ if (c === '\\') i = escapeEnd(selector, i) - 1;
280
+ else if (c === '"' || c === "'") quote = c;
281
+ else if (c === '(' || c === '[') depth += 1;
282
+ else if (c === ')' || c === ']') depth = Math.max(0, depth - 1);
283
+ else if (depth === 0 && /[\s>+~]/.test(c)) {
284
+ split(i, c === '+' || c === '~' || c === '>' ? c : ' ');
285
+ start = i + 1;
286
+ }
287
+ }
288
+ split(selector.length, '');
289
+ for (const { text, next } of parts) {
290
+ let bare = '';
291
+ let pointer = false;
292
+ depth = 0;
293
+ quote = '';
294
+ /** Where a top-level `:has(` starts in `bare`, and its argument in
295
+ * `text`, while one is open. */
296
+ let hasAt = -1;
297
+ let argAt = -1;
298
+ let hasPointer = false;
299
+ let anchored = false;
300
+ const args: string[] = [];
301
+ for (let i = 0; i < text.length; i += 1) {
302
+ const c = text[i];
303
+ if (quote) {
304
+ if (c === quote && text[i - 1] !== '\\') quote = '';
305
+ bare += c;
306
+ continue;
307
+ }
308
+ if (c === '\\') {
309
+ const end = escapeEnd(text, i);
310
+ bare += text.slice(i, end);
311
+ i = end - 1;
312
+ continue;
313
+ }
314
+ if (c === '"' || c === "'") quote = c;
315
+ else if (c === '(' || c === '[') {
316
+ if (depth === 0 && c === '(' && /:has$/i.test(bare)) {
317
+ hasAt = bare.length - 4;
318
+ argAt = i + 1;
319
+ hasPointer = false;
320
+ }
321
+ depth += 1;
322
+ } else if (c === ')' || c === ']') {
323
+ depth = Math.max(0, depth - 1);
324
+ if (depth === 0 && hasAt >= 0 && c === ')') {
325
+ // the compound is kept without it: its argument is what the
326
+ // pointer changes
327
+ if (hasPointer) {
328
+ anchored = true;
329
+ args.push(text.slice(argAt, i));
330
+ bare = bare.slice(0, hasAt);
331
+ } else bare += c;
332
+ hasAt = -1;
333
+ continue;
334
+ }
335
+ } else if (c === ':' && text[i + 1] !== ':') {
336
+ const m = POINTER_PSEUDO_AT.exec(text.slice(i));
337
+ if (m) {
338
+ const hover = m[0].length === 6;
339
+ if (depth > 0) {
340
+ // whatever holds it in the argument, the element it tests is
341
+ // in the anchor's subtree or a later sibling's
342
+ if (hover && hasAt >= 0) hasPointer = true;
343
+ else if (hover) out.nested = true;
344
+ } else {
345
+ if (hover) pointer = true;
346
+ i += m[0].length - 1;
347
+ continue;
348
+ }
349
+ }
350
+ }
351
+ bare += c;
352
+ }
353
+ if (anchored) {
354
+ out.has.push({
355
+ anchor: bare.trim() || '*',
356
+ siblings: args.some((arg) =>
357
+ arg.split(',').some((one) => /^\s*[+~]/.test(one)),
358
+ ),
359
+ });
360
+ }
361
+ if (!pointer) continue;
362
+ out.compounds.push(bare.trim() || '*');
363
+ if (next === '+' || next === '~') out.siblings = true;
364
+ }
365
+ return out;
366
+ }
367
+
368
+ /** `:hover` or `:active` at the start of a string, and nothing longer. */
369
+ const POINTER_PSEUDO_AT = /^:(?:hover|active)(?![\w-])/i;
370
+
371
+ /** `:hover` or `:active` anywhere in a selector. */
372
+ const POINTER_PSEUDO = /:(?:hover|active)(?![\w-])/i;
373
+
374
+ /**
375
+ * A selector compiled to a matcher over this adapter. css-select keeps the
376
+ * answers of a `:has()` above the subject, or of the ancestors a
377
+ * descendant combinator tried under one, for as long as the matcher lives
378
+ * (`cacheResults`) — which is the cascade's life, and a pointer move is no
379
+ * new cascade. So a selector that tests the pointer keeps none: with them,
380
+ * `#box:has(a:hover) .m` answered what it did before the first move, for
381
+ * good.
382
+ */
383
+ function compileSelector(
384
+ selector: string,
385
+ adapter: CssSelectAdapter,
386
+ ): (el: Element) => boolean {
387
+ return compile(noEmptyWords(selector), {
388
+ adapter,
389
+ xmlMode: false,
390
+ pseudos: PSEUDOS,
391
+ cacheResults: !POINTER_PSEUDO.test(selector),
392
+ } as unknown as Parameters<typeof compile>[1]) as unknown as (
393
+ node: Element,
394
+ ) => boolean;
395
+ }
396
+
102
397
  function mapBucket(
103
398
  map: Map<string, IndexedRule[]>,
104
399
  key: string,
@@ -132,12 +427,23 @@ function rightmostKey(selector: string): {
132
427
  if (c === quote && selector[i - 1] !== '\\') quote = '';
133
428
  continue;
134
429
  }
135
- if (c === '"' || c === "'") quote = c;
430
+ // an escaped character is part of a name, whatever it is — and a hex
431
+ // escape takes the space after it: `.c\6c ass` is the class `class`
432
+ if (c === '\\') i = escapeEnd(selector, i) - 1;
433
+ else if (c === '"' || c === "'") quote = c;
136
434
  else if (c === '(' || c === '[') depth += 1;
137
435
  else if (c === ')' || c === ']') depth = Math.max(0, depth - 1);
138
436
  else if (
139
437
  depth === 0 &&
140
- (c === ' ' || c === '>' || c === '+' || c === '~')
438
+ (c === ' ' ||
439
+ c === '>' ||
440
+ c === '+' ||
441
+ c === '~' ||
442
+ // CSS's other white space: `div\fp` is a descendant too
443
+ c === '\t' ||
444
+ c === '\n' ||
445
+ c === '\r' ||
446
+ c === '\f')
141
447
  ) {
142
448
  start = i + 1;
143
449
  }
@@ -149,24 +455,23 @@ function rightmostKey(selector: string): {
149
455
  let i = 0;
150
456
  while (i < compound.length) {
151
457
  const c = compound[i];
152
- if (c === '#') {
153
- const end = identEnd(compound, i + 1);
154
- if (!id) id = compound.slice(i + 1, end);
155
- i = end;
156
- } else if (c === '.') {
157
- const end = identEnd(compound, i + 1);
158
- if (!cls) cls = compound.slice(i + 1, end);
159
- i = end;
458
+ // Names as the matcher reads them, escapes resolved: `.md\:flex` is
459
+ // the class `md:flex`, and filed under its first half it never matched.
460
+ if (c === '#' || c === '.') {
461
+ const name = readIdent(compound, i + 1);
462
+ if (c === '#' && !id) id = name.value;
463
+ if (c === '.' && !cls) cls = name.value;
464
+ i = Math.max(name.end, i + 1);
160
465
  } else if (c === '[') {
161
466
  i = balancedEnd(compound, i, '[', ']');
162
467
  } else if (c === ':') {
163
468
  const skip = compound[i + 1] === ':' ? 2 : 1;
164
- const end = identEnd(compound, i + skip);
469
+ const end = Math.max(readIdent(compound, i + skip).end, i + skip);
165
470
  i = compound[end] === '(' ? balancedEnd(compound, end, '(', ')') : end;
166
- } else if (/[a-zA-Z]/.test(c)) {
167
- const end = identEnd(compound, i);
168
- if (!tag) tag = compound.slice(i, end).toLowerCase();
169
- i = end;
471
+ } else if (startsIdent(compound, i)) {
472
+ const name = readIdent(compound, i);
473
+ if (!tag) tag = name.value.toLowerCase();
474
+ i = name.end;
170
475
  } else {
171
476
  i += 1;
172
477
  }
@@ -177,12 +482,6 @@ function rightmostKey(selector: string): {
177
482
  return { kind: 'any', name: '' };
178
483
  }
179
484
 
180
- function identEnd(text: string, from: number): number {
181
- let i = from;
182
- while (i < text.length && /[a-zA-Z0-9_\-\\]/.test(text[i])) i += 1;
183
- return i;
184
- }
185
-
186
485
  function balancedEnd(
187
486
  text: string,
188
487
  from: number,
@@ -200,6 +499,12 @@ function balancedEnd(
200
499
  return text.length;
201
500
  }
202
501
 
502
+ /** What `Cascade.firstLetterRules` found for an element. */
503
+ export interface FirstLetterRules {
504
+ readonly el: Element;
505
+ readonly candidates: readonly unknown[];
506
+ }
507
+
203
508
  /** What the pointer is over, for `:hover`. A chain rather than one element,
204
509
  * because `li:hover a` has to light up while the pointer is on the `li`. */
205
510
  export interface PointerState {
@@ -215,7 +520,34 @@ const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
215
520
  */
216
521
  export class Cascade {
217
522
  private _index = new RuleIndex();
523
+ /** The rules for pseudo-elements, kept apart: they never style the
524
+ * element itself, and a document with none of them asks nothing. */
525
+ private _pseudo: Record<PseudoElement, RuleIndex> = {
526
+ before: new RuleIndex(),
527
+ after: new RuleIndex(),
528
+ 'first-letter': new RuleIndex(),
529
+ 'first-line': new RuleIndex(),
530
+ marker: new RuleIndex(),
531
+ };
218
532
  private _adapter: CssSelectAdapter;
533
+ /** The compounds of the selectors that test the pointer, each without
534
+ * its `:hover` or `:active` (`pointerCompounds`), and their matchers,
535
+ * compiled when a pointer move first asks (`hoverTouches`). */
536
+ private _hoverCompounds = new Set<string>();
537
+ private _hoverMatchers: ((el: Element) => boolean)[] | null = null;
538
+ /** Whether a pointer move can be restyled where it happened
539
+ * (`HtmlViewNode._hoverInPlace`): false where a selector tests the
540
+ * pointer inside a functional pseudo-class, which reaches elements no
541
+ * compound names. */
542
+ hoverLocal = true;
543
+ /** Whether a compound that tests the pointer is followed by a sibling
544
+ * combinator, so an element's later siblings restyle with it. */
545
+ hoverSiblings = false;
546
+ /** The compounds whose `:has()` tests the pointer, each without it, and
547
+ * whether its argument starts at a sibling (`hoverAnchors`). */
548
+ private _hoverAnchors = new Map<string, boolean>();
549
+ private _anchorMatchers:
550
+ { match: (el: Element) => boolean; siblings: boolean }[] | null = null;
219
551
  private _pointer: PointerState = NO_POINTER;
220
552
  readonly initial: ComputedStyle;
221
553
  readonly look: RootLook;
@@ -229,6 +561,37 @@ export class Cascade {
229
561
  /** Every width at which some `@media` rule changes its mind, in CSS
230
562
  * pixels — the unit the author wrote them in. */
231
563
  readonly breakpoints: number[];
564
+ /** The counter styles the sheets define, over the predefined ones. */
565
+ readonly counterStyles: CounterStyles;
566
+
567
+ /** A font's x-height at a size, for `ex`, where the fonts can say. */
568
+ private _xHeightOf: ((family: string, size: number) => number | null) | null;
569
+ private _xHeights = new Map<string, number>();
570
+ /** The advance of a font's "0" at a size, for `ch`, likewise. */
571
+ private _zeroWidthOf:
572
+ ((family: string, size: number) => number | null) | null;
573
+ private _zeroWidths = new Map<string, number>();
574
+ /** A font's own line height at a size, for `lh` where `line-height` is
575
+ * `normal`, likewise. */
576
+ private _normalLineOf:
577
+ ((family: string, size: number) => number | null) | null;
578
+ private _normalLines = new Map<string, number>();
579
+ /** Whether any declaration sets a custom property or reads one: without
580
+ * one, no element's style asks about them. */
581
+ private _vars = false;
582
+ /** Whether any declaration is in a unit of the viewport's width — `vw`,
583
+ * `vmin`, `vmax` — or of its height, `vh` and the same two: a style
584
+ * holds such a length as a number, so it is computed again when that
585
+ * side of the viewport moves (`HtmlViewNode._update`). */
586
+ readsViewportWidth = false;
587
+ readsViewportHeight = false;
588
+ /** The families the document loads itself (`fonts.ts`), or null for a
589
+ * document with no `@font-face`. */
590
+ private _families: FontFamilies | null;
591
+ private _mapFamilies: ((list: string) => string) | undefined;
592
+ /** Whether any declaration has a length in `lh` or `rlh`: only then is
593
+ * the line height settled ahead of the declarations that read it. */
594
+ private _lh = false;
232
595
 
233
596
  constructor(
234
597
  sheets: Stylesheet[],
@@ -236,18 +599,44 @@ export class Cascade {
236
599
  viewportWidth: number,
237
600
  viewportHeight: number,
238
601
  scale = 1,
602
+ xHeight: ((family: string, size: number) => number | null) | null = null,
603
+ zeroWidth: ((family: string, size: number) => number | null) | null = null,
604
+ normalLine: ((family: string, size: number) => number | null) | null = null,
605
+ families: FontFamilies | null = null,
239
606
  ) {
607
+ this._xHeightOf = xHeight;
608
+ this._zeroWidthOf = zeroWidth;
609
+ this._normalLineOf = normalLine;
610
+ this._families = families;
611
+ this._mapFamilies = families
612
+ ? (list: string) => families.map(list)
613
+ : undefined;
240
614
  this.look = look;
241
- this.initial = initialStyle(look);
615
+ this.initial = initialStyle(look, scale);
242
616
  this.viewportWidth = viewportWidth;
243
617
  this.viewportHeight = viewportHeight;
244
618
  this.scale = scale;
245
619
  const breakpoints = new Set<number>();
620
+ const counterStyles = new Map<string, CounterStyleRule>();
246
621
  for (const sheet of sheets) {
247
- for (const rule of sheet.rules) this._index.add(rule);
622
+ // a counter style's rule whole over any before it of its name
623
+ for (const { prelude, declarations } of sheet.counterStyles ?? []) {
624
+ const style = counterStyleRule(prelude, declarations);
625
+ if (style) counterStyles.set(style.name, style.rule);
626
+ }
627
+ for (const rule of sheet.rules) {
628
+ if (!this._vars && usesVars(rule.declarations)) this._vars = true;
629
+ this._noteViewportUnits(rule.declarations);
630
+ if (!this._lh && usesLh(rule.declarations)) this._lh = true;
631
+ const pseudo = splitPseudoElement(rule);
632
+ if (pseudo) this._pseudo[pseudo.which].add(pseudo.rule);
633
+ else this._index.add(rule);
634
+ this._noteHover((pseudo?.rule ?? rule).selector);
635
+ }
248
636
  for (const bp of sheet.breakpoints) breakpoints.add(bp);
249
637
  }
250
638
  this.breakpoints = [...breakpoints].sort((a, b) => a - b);
639
+ this.counterStyles = new CounterStyles(counterStyles);
251
640
  // css-select's default adapter is domutils; `isHovered` and `isActive`
252
641
  // are its documented hooks for exactly this, so `:hover` costs an
253
642
  // adapter field rather than a fork of the matcher.
@@ -258,21 +647,127 @@ export class Cascade {
258
647
  this._adapter = {
259
648
  ...DomUtils,
260
649
  isTag,
650
+ // a type selector sees `tagOf`'s name — XHTML's `<svg:svg>` is an
651
+ // `svg` — asked only of a name that could be one: this runs for every
652
+ // type selector tried, and the parser has lowercased the rest
653
+ getName: (el: Element) =>
654
+ el.name.endsWith(':svg') ? tagOf(el) : el.name,
261
655
  isHovered: (el: Element) => this._pointer.hovered.has(el),
262
656
  isActive: (el: Element) => this._pointer.active.has(el),
263
657
  isVisited: () => false,
264
658
  };
265
659
  }
266
660
 
661
+ /** Whether declarations read the viewport's width or height (`vw`,
662
+ * `vh`, `vmin`, `vmax`), noted on the cascade. */
663
+ private _noteViewportUnits(declarations: readonly Declaration[]): void {
664
+ if (this.readsViewportWidth && this.readsViewportHeight) return;
665
+ for (const d of declarations) {
666
+ if (!VIEWPORT_UNIT.test(d.value)) continue;
667
+ for (const m of d.value.matchAll(VIEWPORT_UNITS)) {
668
+ const unit = m[1].toLowerCase();
669
+ if (unit !== 'vh') this.readsViewportWidth = true;
670
+ if (unit !== 'vw') this.readsViewportHeight = true;
671
+ }
672
+ }
673
+ }
674
+
267
675
  /** Whether a pointer move can change what this cascade produces. */
268
676
  get hoverSensitive(): boolean {
269
- return this._index.hoverSensitive;
677
+ return (
678
+ this._index.hoverSensitive ||
679
+ this._pseudo.before.hoverSensitive ||
680
+ this._pseudo.after.hoverSensitive ||
681
+ this._pseudo['first-letter'].hoverSensitive ||
682
+ this._pseudo['first-line'].hoverSensitive ||
683
+ this._pseudo.marker.hoverSensitive
684
+ );
270
685
  }
271
686
 
272
687
  setPointer(pointer: PointerState): void {
273
688
  this._pointer = pointer;
274
689
  }
275
690
 
691
+ /**
692
+ * Whether the pointer entering or leaving `el` can change a style: one of
693
+ * the compounds a selector tests the pointer in matches it, the pointer
694
+ * aside. A move between two paragraphs of a page whose only such rule is
695
+ * `a:hover` touches nothing, and restyles nothing.
696
+ */
697
+ hoverTouches(el: Element): boolean {
698
+ this._hoverMatchers ??= [...this._hoverCompounds].flatMap((c) => {
699
+ const match = this._compile(c);
700
+ return match ? [match] : [];
701
+ });
702
+ for (const match of this._hoverMatchers) if (match(el)) return true;
703
+ return false;
704
+ }
705
+
706
+ /**
707
+ * The elements whose `:has()` may have changed its answer as `el` was
708
+ * hovered or left: the ancestors of it a compound whose `:has()` tests
709
+ * the pointer names, and their earlier siblings and its own where the
710
+ * argument starts at a sibling — a superset, which is restyled and found
711
+ * the same where it did not. Into `into`.
712
+ */
713
+ hoverAnchors(el: Element, into: Element[]): void {
714
+ if (!this._hoverAnchors.size) return;
715
+ this._anchorMatchers ??= [...this._hoverAnchors].flatMap(
716
+ ([anchor, siblings]) => {
717
+ const match = this._compile(anchor);
718
+ return match ? [{ match, siblings }] : [];
719
+ },
720
+ );
721
+ const siblings = this._anchorMatchers.some((m) => m.siblings);
722
+ for (let at: Element | null = el; at;) {
723
+ if (at !== el) {
724
+ for (const { match } of this._anchorMatchers) {
725
+ if (match(at)) {
726
+ into.push(at);
727
+ break;
728
+ }
729
+ }
730
+ }
731
+ if (siblings) {
732
+ for (let s = at.prev; s; s = s.prev) {
733
+ if (!isTag(s as Element)) continue;
734
+ for (const { match, siblings: apart } of this._anchorMatchers) {
735
+ if (apart && match(s as Element)) {
736
+ into.push(s as Element);
737
+ break;
738
+ }
739
+ }
740
+ }
741
+ }
742
+ const parent: Element['parent'] = at.parent;
743
+ at = parent && isTag(parent as Element) ? (parent as Element) : null;
744
+ }
745
+ }
746
+
747
+ private _noteHover(selector: string): void {
748
+ if (!/:(?:hover|active)/i.test(selector)) return;
749
+ const found = pointerCompounds(selector);
750
+ if (found.nested) this.hoverLocal = false;
751
+ if (found.siblings) this.hoverSiblings = true;
752
+ for (const c of found.compounds) this._hoverCompounds.add(c);
753
+ for (const { anchor, siblings } of found.has) {
754
+ this._hoverAnchors.set(
755
+ anchor,
756
+ siblings || (this._hoverAnchors.get(anchor) ?? false),
757
+ );
758
+ }
759
+ }
760
+
761
+ /** A selector compiled as a rule's is, or null where css-select refuses
762
+ * it, as a rule it refuses drops out of the cascade. */
763
+ private _compile(selector: string): ((el: Element) => boolean) | null {
764
+ try {
765
+ return compileSelector(selector, this._adapter);
766
+ } catch {
767
+ return null;
768
+ }
769
+ }
770
+
276
771
  /** Which media band a device-pixel width falls in. Two widths in the same
277
772
  * band produce identical styles, which is what lets a resize skip
278
773
  * restyling. */
@@ -286,18 +781,361 @@ export class Cascade {
286
781
  return band;
287
782
  }
288
783
 
784
+ /** Computed styles by sharing key, for one build (`sharedStyleFor`): one
785
+ * map for the elements shared without matching, one for the elements
786
+ * shared by what they matched. */
787
+ private _shared = new Map<string, SharedStyle>();
788
+ private _sharedByMatch = new Map<string, SharedStyle>();
789
+ private _nextShareKey = 1;
790
+ /** Custom property sets by what made them, for one build (`_customFor`). */
791
+ private _customs = new Map<string, CustomProps>();
792
+ private _ids = new WeakMap<object, number>();
793
+ private _nextId = 1;
794
+
795
+ /** A box tree is about to be built: the styles shared in the last build
796
+ * were computed against a pointer and a viewport that may have moved. */
797
+ beginSharing(): void {
798
+ this._shared.clear();
799
+ this._sharedByMatch.clear();
800
+ this._customs.clear();
801
+ }
802
+
803
+ /**
804
+ * An element's custom properties: its parent's, or its own over them —
805
+ * shared by every element whose parent has the same ones and whose own
806
+ * come from the same declarations. Tailwind 4 sets thirty-five of them on
807
+ * every element (`*, ::before, ::after { --tw-shadow: 0 0 #0000; … }`),
808
+ * and a set per element was a fifth of building the boxes; it also kept
809
+ * each element's `var()`s from being replaced once for its siblings,
810
+ * which `substituteIn` remembers by the set they were read against.
811
+ */
812
+ private _customFor(
813
+ parentStyle: ComputedStyle,
814
+ candidates: Candidate[],
815
+ ): CustomProps | null {
816
+ let key = '';
817
+ for (const c of candidates) {
818
+ if (!this._hasCustom(c)) continue;
819
+ key += `${this._idOf(c.declarations)}:${c.only},`;
820
+ }
821
+ const parent = parentStyle.custom;
822
+ if (!key) return parent;
823
+ key = `${parent ? this._idOf(parent) : 0}|${key}`;
824
+ const hit = this._customs.get(key);
825
+ if (hit) return hit;
826
+ const own = new Map<string, string>();
827
+ for (const c of candidates) {
828
+ for (const d of pick(c)) if (d.custom) own.set(d.prop, d.value);
829
+ }
830
+ const made = customProperties(own, parent);
831
+ this._customs.set(key, made);
832
+ return made;
833
+ }
834
+
835
+ /** Whether a candidate sets a custom property. */
836
+ private _hasCustom(c: Candidate): boolean {
837
+ if (c.only >= 0) return !!c.declarations[c.only].custom;
838
+ let has = CUSTOM_IN.get(c.declarations);
839
+ if (has === undefined) {
840
+ has = c.declarations.some((d) => !!d.custom);
841
+ CUSTOM_IN.set(c.declarations, has);
842
+ }
843
+ return has;
844
+ }
845
+
846
+ /** A number for an object, to key a string by. */
847
+ private _idOf(of: object): number {
848
+ let id = this._ids.get(of);
849
+ if (id === undefined) {
850
+ id = this._nextId++;
851
+ this._ids.set(of, id);
852
+ }
853
+ return id;
854
+ }
855
+
856
+ /**
857
+ * `styleFor`, shared between the elements that must compute the same
858
+ * style, with the key this element's children share under.
859
+ *
860
+ * A style is a function of the parent's style, the rules that match, the
861
+ * presentational attributes, the inline style and whether the parent is a
862
+ * flex container. Outside the rules `UNSHAREABLE` names, what matches
863
+ * depends only on the element's tag and attributes and its ancestors', and
864
+ * the hints read nothing else either. So the key is the parent's key, the
865
+ * flex flag, the tag and every attribute — plus the pointer state where a
866
+ * rule asks for it — and an element whose key has been computed in this
867
+ * build takes that style object, matching nothing. A long document is a
868
+ * few kinds of element many times over: the benchmark's 600 KB report is
869
+ * 9,039 elements and 110 keys, and computing every style again was a third
870
+ * of an edit.
871
+ *
872
+ * An element a rule `UNSHAREABLE` could reach is matched, since where it
873
+ * sits decides what matches — and then shared by what matched: the same
874
+ * parent, attributes and rules make the same style. That is what keeps a
875
+ * striped table from being a style per row, and it keeps the key its
876
+ * children share under, which an element-per-key would have taken from the
877
+ * whole subtree.
878
+ *
879
+ * The style is handed out shared, so nothing may write to it — nothing
880
+ * does after the cascade, and `rootStyle` does not go through here.
881
+ */
882
+ sharedStyleFor(
883
+ el: Element,
884
+ parentStyle: ComputedStyle,
885
+ parentKey: number,
886
+ inFlexContainer: boolean,
887
+ ): SharedStyle {
888
+ let key = `${parentKey}\u0001${inFlexContainer ? 1 : 0}`;
889
+ const tag = tagOf(el);
890
+ key += `\u0001${tag.length}:${tag}`;
891
+ if (this._index.hoverSensitive) {
892
+ if (this._pointer.hovered.has(el)) key += '\u0001:hover';
893
+ if (this._pointer.active.has(el)) key += '\u0001:active';
894
+ }
895
+ const attribs = el.attribs;
896
+ for (const name in attribs) {
897
+ const value = attribs[name];
898
+ key += `\u0001${name.length}:${name}=${value.length}:${value}`;
899
+ }
900
+ if (!this._shareable(el)) {
901
+ const matched: number[] = [];
902
+ const candidates = this._candidates(el, matched);
903
+ key += `\u0001${matched.join(',')}`;
904
+ let shared = this._sharedByMatch.get(key);
905
+ if (shared === undefined) {
906
+ shared = {
907
+ style: this._computeStyle(
908
+ el,
909
+ parentStyle,
910
+ inFlexContainer,
911
+ candidates,
912
+ ),
913
+ key: this._nextShareKey++,
914
+ };
915
+ this._sharedByMatch.set(key, shared);
916
+ }
917
+ return shared;
918
+ }
919
+ let shared = this._shared.get(key);
920
+ if (shared === undefined) {
921
+ shared = {
922
+ style: this.styleFor(el, parentStyle, inFlexContainer),
923
+ key: this._nextShareKey++,
924
+ };
925
+ this._shared.set(key, shared);
926
+ }
927
+ return shared;
928
+ }
929
+
930
+ /** Whether no rule `UNSHAREABLE` names could reach this element: the same
931
+ * buckets `_candidates` looks in. */
932
+ private _shareable(el: Element): boolean {
933
+ const index = this._index;
934
+ if (index.ownStyleEverywhere) return false;
935
+ if (index.ownStyleTags.size && index.ownStyleTags.has(tagOf(el))) {
936
+ return false;
937
+ }
938
+ if (index.ownStyleIds.size) {
939
+ const id = attr(el, 'id');
940
+ if (id && index.ownStyleIds.has(id)) return false;
941
+ }
942
+ if (index.ownStyleClasses.size) {
943
+ const className = attr(el, 'class');
944
+ if (className) {
945
+ for (const name of className.split(/\s+/)) {
946
+ if (name && index.ownStyleClasses.has(name)) return false;
947
+ }
948
+ }
949
+ }
950
+ return true;
951
+ }
952
+
289
953
  /**
290
- * The computed style for one element, given its parent's. Called once per
291
- * element per style pass, in document order — the box builder drives it,
292
- * so there is no second traversal and no map of styles to allocate.
954
+ * The computed style for one element, given its parent's. The box builder
955
+ * asks for every element in document order, through `sharedStyleFor`, so
956
+ * a style is computed only for the elements with none to share.
293
957
  */
294
958
  styleFor(
295
959
  el: Element,
296
960
  parentStyle: ComputedStyle,
297
961
  inFlexContainer: boolean,
962
+ ): ComputedStyle {
963
+ return this._computeStyle(
964
+ el,
965
+ parentStyle,
966
+ inFlexContainer,
967
+ this._candidates(el),
968
+ );
969
+ }
970
+
971
+ /**
972
+ * The style of an element's `::before` or `::after`, or null when it has
973
+ * none: no rule reaches it, or the rules that do leave `content` at
974
+ * `normal` or `none`, which make no box (CSS 2.1 12.2). It inherits from
975
+ * the element's own style, as a pseudo-element does, and a flex
976
+ * container's are flex items. Not shared: few elements have one, and where
977
+ * a rule gives every element one — a clearfix — the style is the cost of
978
+ * the box it makes.
979
+ */
980
+ pseudoStyleFor(
981
+ el: Element,
982
+ which: 'before' | 'after',
983
+ elementStyle: ComputedStyle,
984
+ ): ComputedStyle | null {
985
+ const index = this._pseudo[which];
986
+ if (!index.size || !index.reaches(el)) return null;
987
+ const candidates: Candidate[] = [];
988
+ this._matchInto(index, el, candidates);
989
+ if (!candidates.length) return null;
990
+ // One that no rule gives a `content` is none, whatever else reaches it —
991
+ // `content` is not inherited, and its initial `normal` is nothing here.
992
+ // Tailwind's `*, ::before, ::after` reaches both of every element's.
993
+ if (!candidates.some(setsContent)) return null;
994
+ candidates.sort(byCascade);
995
+ const style = this._computeStyle(
996
+ el,
997
+ elementStyle,
998
+ elementStyle.display === 'flex' || elementStyle.display === 'inline-flex',
999
+ candidates,
1000
+ );
1001
+ return style.content === 'normal' || style.content === 'none'
1002
+ ? null
1003
+ : style;
1004
+ }
1005
+
1006
+ /**
1007
+ * The rules for an element's `::first-letter`, or null when none reaches
1008
+ * it. They are matched against the element here, and its style computed
1009
+ * later by `firstLetterStyle`, because the pseudo-element inherits from
1010
+ * the box its letter turns out to be in — a `<span>` the block opens with,
1011
+ * a `::before` — which the builder has not reached yet.
1012
+ */
1013
+ firstLetterRules(el: Element): FirstLetterRules | null {
1014
+ const index = this._pseudo['first-letter'];
1015
+ if (!index.size || !index.reaches(el)) return null;
1016
+ const candidates: Candidate[] = [];
1017
+ this._matchInto(index, el, candidates);
1018
+ if (!candidates.length) return null;
1019
+ candidates.sort(byCascade);
1020
+ return { el, candidates };
1021
+ }
1022
+
1023
+ /** Whether any rule styles a `::first-line`: a document with none asks
1024
+ * nothing more of its blocks. */
1025
+ get hasFirstLine(): boolean {
1026
+ return this._pseudo['first-line'].size > 0;
1027
+ }
1028
+
1029
+ /**
1030
+ * A list item's `::marker` style, inheriting from its own, or null when no
1031
+ * rule reaches it (CSS Lists 3, 3.1): the colour and the font a bullet or
1032
+ * a number is set in, and a `content` it is set as instead.
1033
+ */
1034
+ markerStyle(el: Element, style: ComputedStyle): ComputedStyle | null {
1035
+ const index = this._pseudo.marker;
1036
+ if (!index.size || !index.reaches(el)) return null;
1037
+ const candidates: Candidate[] = [];
1038
+ this._matchInto(index, el, candidates);
1039
+ if (!candidates.length) return null;
1040
+ candidates.sort(byCascade);
1041
+ const out = this._computeStyle(el, style, false, candidates);
1042
+ // a marker's direction is its own unless a rule says otherwise (the
1043
+ // HTML style sheet's `::marker { unicode-bidi: isolate }`)
1044
+ const set = candidates.some((c) =>
1045
+ c.declarations.some((d) => d.prop === 'unicode-bidi'),
1046
+ );
1047
+ if (!set) out.unicodeBidi = 'isolate';
1048
+ return out;
1049
+ }
1050
+
1051
+ /**
1052
+ * An element's `::first-line` style, inheriting from its own, or null
1053
+ * when no rule reaches it (CSS 2.1 5.12.1).
1054
+ */
1055
+ firstLineStyle(el: Element, style: ComputedStyle): ComputedStyle | null {
1056
+ const index = this._pseudo['first-line'];
1057
+ if (!index.size) return null;
1058
+ const candidates: Candidate[] = [];
1059
+ this._matchInto(index, el, candidates);
1060
+ if (!candidates.length) return null;
1061
+ candidates.sort(byCascade);
1062
+ return this._computeStyle(el, style, false, candidates);
1063
+ }
1064
+
1065
+ /**
1066
+ * The style of a `::first-letter` inside a box of `parentStyle`. It is an
1067
+ * inline box, or a float where it floats, whatever the rules say of its
1068
+ * `display` or its `position` (CSS 2.1 5.12.2).
1069
+ */
1070
+ firstLetterStyle(
1071
+ rules: FirstLetterRules,
1072
+ parentStyle: ComputedStyle,
1073
+ ): ComputedStyle {
1074
+ const style = this._computeStyle(
1075
+ rules.el,
1076
+ parentStyle,
1077
+ false,
1078
+ rules.candidates as Candidate[],
1079
+ );
1080
+ style.display = style.float === 'none' ? 'inline' : 'block';
1081
+ style.position = 'static';
1082
+ return style;
1083
+ }
1084
+
1085
+ /** The x-height of a style's font: the font's own, asked once per face
1086
+ * and size, or half an em. */
1087
+ private _exOf(style: ComputedStyle): number {
1088
+ const key = `${style.fontFamily}\u0001${style.fontSize}`;
1089
+ let ex = this._xHeights.get(key);
1090
+ if (ex === undefined) {
1091
+ ex = this._xHeightOf?.(style.fontFamily, style.fontSize) ?? NaN;
1092
+ if (!(ex > 0)) ex = style.fontSize * 0.5;
1093
+ this._xHeights.set(key, ex);
1094
+ }
1095
+ return ex;
1096
+ }
1097
+
1098
+ /** The advance of a style's font's "0": the font's own, asked once per
1099
+ * face and size, or half an em. */
1100
+ private _chOf(style: ComputedStyle): number {
1101
+ const key = `${style.fontFamily}\u0001${style.fontSize}`;
1102
+ let ch = this._zeroWidths.get(key);
1103
+ if (ch === undefined) {
1104
+ ch = this._zeroWidthOf?.(style.fontFamily, style.fontSize) ?? NaN;
1105
+ if (!(ch > 0)) ch = style.fontSize * 0.5;
1106
+ this._zeroWidths.set(key, ch);
1107
+ }
1108
+ return ch;
1109
+ }
1110
+
1111
+ /** A style's computed line height as a length, for `lh`: `normal` as
1112
+ * its font's own, asked once per face and size, or 1.2em. */
1113
+ private _lineHeightOf(style: ComputedStyle): number {
1114
+ const set = style.lineHeight;
1115
+ if (set !== 'normal') {
1116
+ return style.lineHeightIsLength ? set : set * style.fontSize;
1117
+ }
1118
+ const key = `${style.fontFamily}\u0001${style.fontSize}`;
1119
+ let line = this._normalLines.get(key);
1120
+ if (line === undefined) {
1121
+ line = this._normalLineOf?.(style.fontFamily, style.fontSize) ?? NaN;
1122
+ if (!(line > 0)) line = style.fontSize * 1.2;
1123
+ this._normalLines.set(key, line);
1124
+ }
1125
+ return line;
1126
+ }
1127
+
1128
+ /** `styleFor`, from the rules and hints already gathered for `el`. */
1129
+ private _computeStyle(
1130
+ el: Element,
1131
+ parentStyle: ComputedStyle,
1132
+ inFlexContainer: boolean,
1133
+ candidates: Candidate[],
298
1134
  ): ComputedStyle {
299
1135
  const style = inherit(parentStyle, this.initial);
300
- const candidates = this._candidates(el);
1136
+ // custom properties first, in cascade order, so every `var()` in the
1137
+ // declarations below finds the one that wins
1138
+ if (this._vars) style.custom = this._customFor(parentStyle, candidates);
301
1139
 
302
1140
  // The unit context has to be built twice: once with the parent's font
303
1141
  // size, so a `font-size: 1.2em` in the cascade resolves against the
@@ -306,29 +1144,128 @@ export class Cascade {
306
1144
  const ctxParent: UnitContext = {
307
1145
  em: parentStyle.fontSize,
308
1146
  rem: this.initial.fontSize,
1147
+ initial: this.initial,
309
1148
  vw: this.viewportWidth,
310
1149
  vh: this.viewportHeight,
311
1150
  scale: this.scale,
1151
+ ex: () => this._exOf(parentStyle),
1152
+ ch: () => this._chOf(parentStyle),
1153
+ families: this._mapFamilies,
1154
+ lh: () => this._lineHeightOf(parentStyle),
1155
+ rlh: () => this._lineHeightOf(this.initial),
312
1156
  };
1157
+ // the family goes with the size, so an `ex` after it is its font's
1158
+ let sized = false;
1159
+ let keyword = false;
313
1160
  for (const c of candidates) {
314
1161
  for (const d of pick(c)) {
315
- if (d.prop === 'font-size' || d.prop === 'font') {
316
- applyDeclaration(style, parentStyle, d.prop, d.value, ctxParent);
1162
+ if (d.prop === 'font-family') {
1163
+ this._apply(style, parentStyle, d, ctxParent);
1164
+ } else if (d.prop === 'font-size' || d.prop === 'font') {
1165
+ // NaN until a size is set, so one the declaration did not take
1166
+ // leaves the size where it was, and says nothing of it
1167
+ const was = style.fontSize;
1168
+ style.fontSize = NaN;
1169
+ this._apply(style, parentStyle, d, ctxParent);
1170
+ if (Number.isNaN(style.fontSize)) {
1171
+ style.fontSize = was;
1172
+ } else {
1173
+ sized = true;
1174
+ keyword = !d.vars && sizeKeyword(d.prop, d.value);
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ // The generic `monospace` on its own has a smaller size than the rest,
1180
+ // 13 pixels to their 16 (the "fixed" font size every browser keeps):
1181
+ // an element whose family becomes it, or stops being it, scales the
1182
+ // size it inherits by that, and a keyword size is read from the
1183
+ // smaller scale — the size it sets itself stays its own. Blink's
1184
+ // CheckForGenericFamilyChange, and why a `<pre>` in a browser is 13px.
1185
+ const mono = monospaceOnly(style.fontFamily);
1186
+ if (
1187
+ sized ? keyword && mono : mono !== monospaceOnly(parentStyle.fontFamily)
1188
+ ) {
1189
+ style.fontSize *= mono ? FIXED_SIZE : 1 / FIXED_SIZE;
1190
+ }
1191
+ const ctx: UnitContext = {
1192
+ ...ctxParent,
1193
+ em: style.fontSize,
1194
+ ex: () => this._exOf(style),
1195
+ ch: () => this._chOf(style),
1196
+ lh: () => this._lineHeightOf(style),
1197
+ };
1198
+ // the line height, which an `lh` in any other declaration reads, ahead
1199
+ // of them: set again with the rest, in its place among them
1200
+ if (this._lh) {
1201
+ for (const c of candidates) {
1202
+ for (const d of pick(c)) {
1203
+ if (d.prop === 'line-height') this._apply(style, parentStyle, d, ctx);
317
1204
  }
318
1205
  }
319
1206
  }
320
- const ctx: UnitContext = { ...ctxParent, em: style.fontSize };
1207
+ const settled = style.fontSize;
321
1208
  for (const c of candidates) {
322
1209
  for (const d of pick(c)) {
323
- if (d.prop === 'font-size') continue;
324
- applyDeclaration(style, parentStyle, d.prop, d.value, ctx);
1210
+ if (d.prop === 'font-size' || d.custom) continue;
1211
+ this._apply(style, parentStyle, d, ctx);
325
1212
  }
326
1213
  }
1214
+ // A `font` is applied again, to keep its other longhands in cascade
1215
+ // order, and it sets the size it names as well: the size is the first
1216
+ // pass's, which the declarations that outrank the `font` had their say
1217
+ // in, and the scale after them. `span { font: 15px/1 Ahem }` under
1218
+ // `.b > span { font-size: 3.75em }` was 15px, its `em`s 3.75 of its
1219
+ // parent's.
1220
+ style.fontSize = settled;
327
1221
 
1222
+ // A table never keeps HTML's alignment, `-webkit-center` and its kin:
1223
+ // the `<td align="center">` every mail centres its body table in
1224
+ // centres the table, and the text in its cells stays at their start —
1225
+ // as Blink resets it ("tables never support the -webkit-* values for
1226
+ // text-align"). An author's own `text-align: center` is not HTML's, and
1227
+ // is inherited into the table as it always was.
1228
+ if (style.alignBlocks !== null && tagOf(el) === 'table') {
1229
+ style.textAlign = 'start';
1230
+ style.alignBlocks = null;
1231
+ }
1232
+ settleClamp(style, parentStyle);
1233
+ settleOverflow(style);
1234
+ settleContentVisibility(style);
328
1235
  blockify(style, inFlexContainer);
1236
+ decorate(style);
1237
+ // which faces of the document's own families this family, weight and
1238
+ // slant ask for — known only now, with all three computed
1239
+ this._families?.note(style);
329
1240
  return style;
330
1241
  }
331
1242
 
1243
+ /** One declaration, its `var()`s replaced first. One that names a
1244
+ * custom property with no value, and has no fallback, is invalid at
1245
+ * computed-value time, and the property is as though `unset`. */
1246
+ private _apply(
1247
+ style: ComputedStyle,
1248
+ parentStyle: ComputedStyle,
1249
+ d: Declaration,
1250
+ ctx: UnitContext,
1251
+ ): void {
1252
+ if (!d.vars) {
1253
+ applyDeclaration(style, parentStyle, d.prop, d.value, ctx);
1254
+ return;
1255
+ }
1256
+ // unset first: a value that does not parse once it is substituted is
1257
+ // invalid at computed-value time, and does not leave the property what
1258
+ // an earlier declaration gave it
1259
+ if (isInherited(d.prop)) {
1260
+ applyDeclaration(style, parentStyle, d.prop, 'inherit', ctx);
1261
+ } else {
1262
+ initialOne(style, this.initial, d.prop);
1263
+ }
1264
+ const value = substituteIn(d.value, style.custom);
1265
+ if (value !== null)
1266
+ applyDeclaration(style, parentStyle, d.prop, value, ctx);
1267
+ }
1268
+
332
1269
  /**
333
1270
  * The root's style — nothing above it to inherit from.
334
1271
  *
@@ -337,24 +1274,41 @@ export class Cascade {
337
1274
  * a `<body>` would have had: the UA sheet's margin and font, and any
338
1275
  * author `body { … }` rule. Without it a fragment renders hard against the
339
1276
  * left edge while the same markup inside `<html><body>` does not, which
340
- * reads as a bug in the renderer rather than as a missing element.
1277
+ * reads as a bug in the renderer rather than as a missing element. With
1278
+ * no `<html>` either, the body inherits from an `<html>` that author
1279
+ * `html { … }` rules have styled, as the one a browser implies would be.
1280
+ *
1281
+ * A document with a `<body>` and no `<html>` — the usual shape of a page
1282
+ * that starts `<!DOCTYPE html><title>` — has the root box standing in for
1283
+ * that implied `<html>` instead, so an `html { font-size }` still reaches
1284
+ * the body. Taken as the initial style, the rule matched nothing and the
1285
+ * whole page came out at the theme's size.
341
1286
  */
342
- rootStyle(hasBody: boolean): ComputedStyle {
343
- const style = { ...this.initial };
1287
+ rootStyle(hasBody: boolean, hasHtml = true): ComputedStyle {
1288
+ const style = copyStyle(this.initial);
344
1289
  style.display = 'block';
345
- if (hasBody) return style;
1290
+ if (hasBody && hasHtml) return style;
346
1291
  const synthetic = new DomElement('body', {}, []);
347
- const bodyStyle = this.styleFor(synthetic, style, false);
1292
+ let parent = style;
1293
+ if (!hasHtml) {
1294
+ const html = new DomElement('html', {}, hasBody ? [] : [synthetic]);
1295
+ synthetic.parent = html;
1296
+ parent = this.styleFor(html, style, false);
1297
+ if (hasBody) return asRoot(parent);
1298
+ }
348
1299
  // Only the box the body would have drawn is taken, not its layout role:
349
1300
  // the root is still the initial containing block.
350
- bodyStyle.display = 'block';
351
- bodyStyle.position = 'static';
352
- bodyStyle.float = 'none';
353
- return bodyStyle;
1301
+ return asRoot(this.styleFor(synthetic, parent, false));
354
1302
  }
355
1303
 
356
- private _candidates(el: Element): Candidate[] {
357
- const out: Candidate[] = [];
1304
+ /** The rules of `index` that match `el`, pushed onto `out` as candidates,
1305
+ * and their ids onto `matched` in the order they were tried. */
1306
+ private _matchInto(
1307
+ index: RuleIndex,
1308
+ el: Element,
1309
+ out: Candidate[],
1310
+ matched?: number[],
1311
+ ): void {
358
1312
  // A media query's width is CSS pixels; the viewport is kept in device.
359
1313
  const width = this.viewportWidth / this.scale;
360
1314
 
@@ -366,12 +1320,7 @@ export class Cascade {
366
1320
  if (!indexed.compiled) {
367
1321
  indexed.compiled = true;
368
1322
  try {
369
- indexed.match = compile(rule.selector, {
370
- adapter: this._adapter,
371
- xmlMode: false,
372
- } as unknown as Parameters<typeof compile>[1]) as unknown as (
373
- node: Element,
374
- ) => boolean;
1323
+ indexed.match = compileSelector(rule.selector, this._adapter);
375
1324
  } catch {
376
1325
  // A selector this matcher does not know (`::-moz-…`, a CSS4 form
377
1326
  // it has not learnt) drops out of the cascade rather than out of
@@ -380,26 +1329,36 @@ export class Cascade {
380
1329
  }
381
1330
  }
382
1331
  if (!indexed.match || !indexed.match(el)) continue;
1332
+ matched?.push(indexed.id);
383
1333
  const origin = rule.order < 0 ? Origin.UserAgent : Origin.Author;
384
1334
  pushRule(out, rule, origin);
385
1335
  }
386
1336
  };
387
1337
 
388
1338
  const id = attr(el, 'id');
389
- if (id) consider(this._index.byId.get(id));
1339
+ if (id) consider(index.byId.get(id));
390
1340
  const className = attr(el, 'class');
391
1341
  if (className) {
392
1342
  for (const name of className.split(/\s+/)) {
393
- if (name) consider(this._index.byClass.get(name));
1343
+ if (name) consider(index.byClass.get(name));
394
1344
  }
395
1345
  }
396
- consider(this._index.byTag.get(tagOf(el)));
397
- consider(this._index.universal);
1346
+ consider(index.byTag.get(tagOf(el)));
1347
+ consider(index.universal);
1348
+ }
1349
+
1350
+ /** Every rule and hint that applies to `el`, in cascade order — and, into
1351
+ * `matched` when it is passed, the ids of the rules, in the order they
1352
+ * were tried. */
1353
+ private _candidates(el: Element, matched?: number[]): Candidate[] {
1354
+ const out: Candidate[] = [];
1355
+ this._matchInto(this._index, el, out, matched);
398
1356
 
399
1357
  const hints = presentationHints(el);
400
1358
  if (hints.length) {
401
1359
  out.push({
402
1360
  origin: Origin.Presentation,
1361
+ layer: null,
403
1362
  specificity: 0,
404
1363
  order: 0,
405
1364
  declarations: hints,
@@ -410,11 +1369,15 @@ export class Cascade {
410
1369
  const inline = attr(el, 'style');
411
1370
  if (inline) {
412
1371
  const declarations = parseDeclarations(inline);
1372
+ if (!this._vars && usesVars(declarations)) this._vars = true;
1373
+ this._noteViewportUnits(declarations);
1374
+ if (!this._lh && usesLh(declarations)) this._lh = true;
413
1375
  const normal = declarations.filter((d) => !d.important);
414
1376
  const important = declarations.filter((d) => d.important);
415
1377
  if (normal.length) {
416
1378
  out.push({
417
1379
  origin: Origin.Inline,
1380
+ layer: null,
418
1381
  specificity: 0,
419
1382
  order: 0,
420
1383
  declarations: normal,
@@ -424,6 +1387,7 @@ export class Cascade {
424
1387
  if (important.length) {
425
1388
  out.push({
426
1389
  origin: Origin.InlineImportant,
1390
+ layer: null,
427
1391
  specificity: 0,
428
1392
  order: 0,
429
1393
  declarations: important,
@@ -437,6 +1401,145 @@ export class Cascade {
437
1401
  }
438
1402
  }
439
1403
 
1404
+ /** An implied element's style as the root box's: its look, not its role —
1405
+ * the root is still a block, in flow, and the initial containing block. */
1406
+ function asRoot(style: ComputedStyle): ComputedStyle {
1407
+ style.display = 'block';
1408
+ style.position = 'static';
1409
+ style.float = 'none';
1410
+ return style;
1411
+ }
1412
+
1413
+ /** `:root` is the `<html>` element: the one a browser implies around a
1414
+ * fragment, whose style the root box takes (`rootStyle`), and never a
1415
+ * fragment's top-level elements, which css-select would take for it. */
1416
+ const PSEUDOS = {
1417
+ root: (el: Element) =>
1418
+ el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
1419
+ // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1420
+ // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1421
+ // Kelvin sign for the K, for `:lang(kl)`
1422
+ lang: (el: Element, code: string | null): boolean => {
1423
+ const ranges = (code ?? '')
1424
+ .split(',')
1425
+ .map((range) => range.trim().replace(/^['"]|['"]$/g, ''))
1426
+ .filter((range) => range.length > 0)
1427
+ .map((range) => asciiLower(range).split('-'));
1428
+ let root = el;
1429
+ for (let node: Element | null = el; node;) {
1430
+ const value = node.attribs['xml:lang'] ?? node.attribs.lang;
1431
+ if (value != null) {
1432
+ if (!value) return ranges.some((range) => range[0] === '');
1433
+ const tag = asciiLower(value).split('-');
1434
+ return ranges.some((range) => langRangeMatches(tag, range));
1435
+ }
1436
+ root = node;
1437
+ const parent: Element['parent'] = node.parent;
1438
+ node = parent && isTag(parent as Element) ? (parent as Element) : null;
1439
+ }
1440
+ // with no element saying, the document's, as its `<meta>` sets it —
1441
+ // searched from the document, where a fragment's `<meta>` is a
1442
+ // sibling of what it covers
1443
+ const pragma = pragmaLanguage(root.parent ?? root);
1444
+ if (!pragma) return ranges.some((range) => range[0] === '');
1445
+ const tag = asciiLower(pragma).split('-');
1446
+ return ranges.some((range) => langRangeMatches(tag, range));
1447
+ },
1448
+ };
1449
+
1450
+ /** A length in a unit of the viewport: a number, then the unit, and no
1451
+ * more of a name after it. */
1452
+ const VIEWPORT_UNIT = /\d(?:vw|vh|vmin|vmax)(?![\w-])/i;
1453
+ const VIEWPORT_UNITS = /\d(vw|vh|vmin|vmax)(?![\w-])/gi;
1454
+
1455
+ /**
1456
+ * A selector with its `[attr~=""]` made one that matches nothing: an empty
1457
+ * string is no word of a list, and such a selector represents nothing
1458
+ * (Selectors 3, 6.3.1) — which the matcher's `~=` is not told, taking the
1459
+ * empty word for one between two spaces, or at either end of none.
1460
+ */
1461
+ function noEmptyWords(selector: string): string {
1462
+ if (!selector.includes('~=')) return selector;
1463
+ return selector.replace(
1464
+ /\[[^\]"']*~=\s*(?:""|'')\s*(?:[is]\s*)?\]/gi,
1465
+ ':not(*)',
1466
+ );
1467
+ }
1468
+
1469
+ /**
1470
+ * A document's language as a `<meta http-equiv="content-language">` sets
1471
+ * it — HTML's pragma-set default language, the last such `<meta>`'s
1472
+ * `content` up to its first white space, and none where it lists more
1473
+ * than one — by the document's root element, found once.
1474
+ */
1475
+ function pragmaLanguage(root: { children: unknown[] }): string {
1476
+ let lang = PRAGMA_LANGUAGE.get(root);
1477
+ if (lang !== undefined) return lang;
1478
+ lang = '';
1479
+ const stack: { children: unknown[] }[] = [root];
1480
+ while (stack.length) {
1481
+ const node = stack.pop()! as Element;
1482
+ if (
1483
+ isTag(node) &&
1484
+ node.name === 'meta' &&
1485
+ node.attribs['http-equiv']?.trim().toLowerCase() === 'content-language'
1486
+ ) {
1487
+ const content = node.attribs.content;
1488
+ if (content !== undefined && !content.includes(',')) {
1489
+ const candidate = content.trim().split(/[\t\n\f\r ]/)[0];
1490
+ if (candidate) lang = candidate;
1491
+ }
1492
+ }
1493
+ // in document order: the last one pushed is taken first
1494
+ for (let i = node.children.length - 1; i >= 0; i -= 1) {
1495
+ const child = node.children[i];
1496
+ if (isTag(child as Element)) stack.push(child as Element);
1497
+ }
1498
+ }
1499
+ PRAGMA_LANGUAGE.set(root, lang);
1500
+ return lang;
1501
+ }
1502
+
1503
+ const PRAGMA_LANGUAGE = new WeakMap<object, string>();
1504
+
1505
+ /** RFC 4647's extended filtering, of a tag's subtags by a range's, as
1506
+ * css-select does it. */
1507
+ function langRangeMatches(tag: string[], range: string[]): boolean {
1508
+ if (range[0] !== '*' && range[0] !== tag[0]) return false;
1509
+ let at = 1;
1510
+ for (let r = 1; r < range.length; r += 1) {
1511
+ if (range[r] === '*') continue;
1512
+ while (at < tag.length && tag[at] !== range[r]) {
1513
+ if (tag[at++].length <= 1) return false;
1514
+ }
1515
+ if (at >= tag.length) return false;
1516
+ at += 1;
1517
+ }
1518
+ return true;
1519
+ }
1520
+
1521
+ /** Whether any of these declarations sets a custom property or reads one. */
1522
+ function usesVars(declarations: readonly Declaration[]): boolean {
1523
+ for (const d of declarations) if (d.custom || d.vars) return true;
1524
+ return false;
1525
+ }
1526
+
1527
+ /** Whether any of the declarations has a length in `lh` or `rlh`. */
1528
+ function usesLh(declarations: readonly Declaration[]): boolean {
1529
+ for (const d of declarations) if (LH.test(d.value)) return true;
1530
+ return false;
1531
+ }
1532
+ const LH = /\d\.?r?lh\b/i;
1533
+
1534
+ /** Whether a candidate declares `content`. */
1535
+ function setsContent(c: Candidate): boolean {
1536
+ for (const d of pick(c)) if (d.prop === 'content') return true;
1537
+ return false;
1538
+ }
1539
+
1540
+ /** Whether a rule's declarations set a custom property, by the array. */
1541
+ const CUSTOM_IN = new WeakMap<Declaration[], boolean>();
1542
+
440
1543
  function pick(c: Candidate): Declaration[] {
441
1544
  return c.only < 0 ? c.declarations : [c.declarations[c.only]];
442
1545
  }
@@ -452,6 +1555,7 @@ function pushRule(out: Candidate[], rule: StyleRule, origin: Origin): void {
452
1555
  if (!hasImportant) {
453
1556
  out.push({
454
1557
  origin,
1558
+ layer: rule.layer,
455
1559
  specificity: rule.specificity,
456
1560
  order: rule.order,
457
1561
  declarations: rule.declarations,
@@ -469,6 +1573,7 @@ function pushRule(out: Candidate[], rule: StyleRule, origin: Origin): void {
469
1573
  ? Origin.UserAgent
470
1574
  : Origin.AuthorImportant
471
1575
  : origin,
1576
+ layer: rule.layer,
472
1577
  specificity: rule.specificity,
473
1578
  order: rule.order,
474
1579
  declarations: rule.declarations,
@@ -479,10 +1584,34 @@ function pushRule(out: Candidate[], rule: StyleRule, origin: Origin): void {
479
1584
 
480
1585
  function byCascade(a: Candidate, b: Candidate): number {
481
1586
  if (a.origin !== b.origin) return a.origin - b.origin;
1587
+ if (a.layer !== b.layer) {
1588
+ // a later layer wins over an earlier one whatever the specificity, and
1589
+ // for `!important` an earlier one does (CSS Cascade 5, 6.4)
1590
+ const by = compareLayers(a.layer, b.layer);
1591
+ if (by !== 0) return a.origin >= Origin.AuthorImportant ? -by : by;
1592
+ }
482
1593
  if (a.specificity !== b.specificity) return a.specificity - b.specificity;
483
1594
  return a.order - b.order;
484
1595
  }
485
1596
 
1597
+ /**
1598
+ * Which of two layers wins for a normal declaration: a later one over an
1599
+ * earlier one, and a rule in no layer over any, as a rule directly in a
1600
+ * layer is over the layers inside it.
1601
+ */
1602
+ function compareLayers(
1603
+ a: readonly number[] | null,
1604
+ b: readonly number[] | null,
1605
+ ): number {
1606
+ if (a === null) return b === null ? 0 : 1;
1607
+ if (b === null) return -1;
1608
+ const n = Math.min(a.length, b.length);
1609
+ for (let i = 0; i < n; i += 1) {
1610
+ if (a[i] !== b[i]) return a[i] - b[i];
1611
+ }
1612
+ return b.length - a.length;
1613
+ }
1614
+
486
1615
  /**
487
1616
  * The presentational attributes, as declarations. They sit above the UA
488
1617
  * sheet and below every author rule, which is where HTML says they go — and
@@ -500,16 +1629,79 @@ function presentationHints(el: Element): Declaration[] {
500
1629
  const align = attr(el, 'align');
501
1630
  if (align) {
502
1631
  const v = align.toLowerCase();
503
- if (tag === 'img' && (v === 'left' || v === 'right')) push('float', v);
504
- else if (v === 'center' || v === 'middle') push('text-align', 'center');
1632
+ const side = v === 'middle' ? 'center' : v;
1633
+ if ((tag === 'img' || tag === 'table') && (v === 'left' || v === 'right')) {
1634
+ push('float', v);
1635
+ } else if (tag === 'img') {
1636
+ // an image's other alignments are its line's
1637
+ const va = IMAGE_ALIGN[v];
1638
+ if (va) push('vertical-align', va);
1639
+ } else if (tag === 'hr') {
1640
+ // a rule narrower than its line stands where it is aligned
1641
+ if (v === 'left') {
1642
+ push('margin-left', '0');
1643
+ push('margin-right', 'auto');
1644
+ } else if (v === 'right') {
1645
+ push('margin-left', 'auto');
1646
+ push('margin-right', '0');
1647
+ }
1648
+ } else if (tag === 'table') {
1649
+ // a table's `align` places the table and leaves its text alone: set
1650
+ // as its text's, `<table align="center">` — the frame of nearly
1651
+ // every mail — stood at the left with its cells' text centred
1652
+ if (side === 'center') {
1653
+ push('margin-left', 'auto');
1654
+ push('margin-right', 'auto');
1655
+ }
1656
+ } else if (
1657
+ ALIGNS_BLOCKS.has(tag) &&
1658
+ (side === 'center' || side === 'left' || side === 'right')
1659
+ ) {
1660
+ // these align the blocks in them as well as their text (HTML's
1661
+ // rendering, "align descendants"): the table a `<td align="center">`
1662
+ // or a `<div align="center">` holds is centred in it
1663
+ push('text-align', `-webkit-${side}`);
1664
+ } else if (side === 'center') push('text-align', 'center');
505
1665
  else if (v === 'left' || v === 'right' || v === 'justify')
506
1666
  push('text-align', v);
507
1667
  }
508
1668
  const valign = attr(el, 'valign');
509
1669
  if (valign) push('vertical-align', valign.toLowerCase());
1670
+ // HTML's `dir` is its element's `direction`: `<table dir="rtl">` starts
1671
+ // at the right, and a Hebrew or an Arabic message is written this way
1672
+ const dir = attr(el, 'dir')?.toLowerCase();
1673
+ if (dir === 'rtl' || dir === 'ltr') push('direction', dir);
1674
+ // and it isolates the element from the text around it, or with `auto`
1675
+ // takes its first strong letter's direction (HTML's UA sheet: `[dir]`,
1676
+ // `[dir=auto i]`, `bdo[dir]`)
1677
+ if (dir !== undefined) {
1678
+ push(
1679
+ 'unicode-bidi',
1680
+ tag === 'bdo'
1681
+ ? 'isolate-override'
1682
+ : dir === 'auto'
1683
+ ? 'plaintext'
1684
+ : 'isolate',
1685
+ );
1686
+ }
510
1687
 
511
1688
  const bgcolor = attr(el, 'bgcolor');
512
1689
  if (bgcolor) push('background-color', bgcolor);
1690
+ if (BACKGROUNDS.has(tag)) {
1691
+ const background = attr(el, 'background')?.trim();
1692
+ if (background) {
1693
+ push('background-image', `url("${background.replace(/["\\\n]/g, '')}")`);
1694
+ }
1695
+ }
1696
+ if (tag === 'body') {
1697
+ // the text's colour, and the links'
1698
+ const text = attr(el, 'text');
1699
+ if (text) push('color', text);
1700
+ } else if (tag === 'a' && attr(el, 'href') !== undefined) {
1701
+ const body = closestBody(el);
1702
+ const link = body ? attr(body, 'link') : undefined;
1703
+ if (link) push('color', link);
1704
+ }
513
1705
  const color = attr(el, 'color');
514
1706
  if (color && (tag === 'font' || tag === 'basefont')) push('color', color);
515
1707
  const face = attr(el, 'face');
@@ -529,6 +1721,24 @@ function presentationHints(el: Element): Declaration[] {
529
1721
  if (width) push('width', lengthAttr(width));
530
1722
  const height = attr(el, 'height');
531
1723
  if (height) push('height', lengthAttr(height));
1724
+ // and, both numbers, an image's the ratio it has before it loads, or
1725
+ // where it has none of its own (HTML 15.4.3, "map to the aspect-ratio
1726
+ // property")
1727
+ if (width && height && RATIO_SIZED.has(tag)) {
1728
+ const w = parseFloat(width);
1729
+ const h = parseFloat(height);
1730
+ if (w > 0 && h > 0 && !/%/.test(width + height)) {
1731
+ push('aspect-ratio', `auto ${w} / ${h}`);
1732
+ }
1733
+ }
1734
+ } else if (tag === 'svg') {
1735
+ // an SVG's are CSS lengths of their own, units and all (SVG 2, 5.1.1)
1736
+ const width = attr(el, 'width');
1737
+ const w = width ? svgSizeHint(width) : null;
1738
+ if (w) push('width', w);
1739
+ const height = attr(el, 'height');
1740
+ const h = height ? svgSizeHint(height) : null;
1741
+ if (h) push('height', h);
532
1742
  }
533
1743
 
534
1744
  if (tag === 'table') {
@@ -550,13 +1760,50 @@ function presentationHints(el: Element): Declaration[] {
550
1760
  if (tag === 'hr') {
551
1761
  const noshade = attr(el, 'noshade');
552
1762
  if (noshade !== undefined) push('border-top-width', '2px');
1763
+ // the rule's colour and its thickness
1764
+ const color = attr(el, 'color');
1765
+ if (color) push('border-top-color', color);
1766
+ const size = attr(el, 'size');
1767
+ if (size && Number(size) > 0) push('border-top-width', `${Number(size)}px`);
1768
+ }
1769
+ if ((tag === 'td' || tag === 'th') && attr(el, 'nowrap') !== undefined) {
1770
+ push('white-space', 'nowrap');
1771
+ }
1772
+ if (tag === 'br') {
1773
+ // below the floats: `<br clear="all">` after a floated image
1774
+ const clear = attr(el, 'clear')?.toLowerCase();
1775
+ if (clear === 'all' || clear === 'both') push('clear', 'both');
1776
+ else if (clear === 'left' || clear === 'right') push('clear', clear);
553
1777
  }
554
1778
  if (tag === 'ol') {
555
1779
  const type = attr(el, 'type');
556
1780
  const mapped = OL_TYPES[type ?? ''];
557
1781
  if (mapped) push('list-style-type', mapped);
558
- const start = attr(el, 'start');
559
- if (start) push('counter-reset', start);
1782
+ // the number a list starts at is its `list-item` counter's, one before
1783
+ // it, or counting down one after it (HTML's rendering, "Lists")
1784
+ const start = htmlInteger(attr(el, 'start'));
1785
+ if (start !== null) {
1786
+ push(
1787
+ 'counter-reset',
1788
+ attr(el, 'reversed') !== undefined
1789
+ ? `reversed(list-item) ${start + 1}`
1790
+ : `list-item ${start - 1}`,
1791
+ );
1792
+ }
1793
+ }
1794
+ if (tag === 'ul') {
1795
+ const type = attr(el, 'type')?.toLowerCase();
1796
+ if (type && BULLETS.has(type)) push('list-style-type', type);
1797
+ }
1798
+ if (tag === 'li') {
1799
+ // an item's `value` is the number it goes on from
1800
+ const value = htmlInteger(attr(el, 'value'));
1801
+ if (value !== null) push('counter-set', `list-item ${value}`);
1802
+ const type = attr(el, 'type');
1803
+ const mapped =
1804
+ OL_TYPES[type ?? ''] ??
1805
+ (type && BULLETS.has(type.toLowerCase()) ? type.toLowerCase() : null);
1806
+ if (mapped) push('list-style-type', mapped);
560
1807
  }
561
1808
  if (tag === 'img' || tag === 'object') {
562
1809
  const hspace = attr(el, 'hspace');
@@ -573,6 +1820,56 @@ function presentationHints(el: Element): Declaration[] {
573
1820
  return out;
574
1821
  }
575
1822
 
1823
+ /** An image's `align` other than a side, as its `vertical-align`. */
1824
+ const IMAGE_ALIGN: Record<string, string> = {
1825
+ top: 'top',
1826
+ texttop: 'text-top',
1827
+ middle: 'middle',
1828
+ absmiddle: 'middle',
1829
+ center: 'middle',
1830
+ bottom: 'baseline',
1831
+ baseline: 'baseline',
1832
+ absbottom: 'bottom',
1833
+ };
1834
+
1835
+ /** The elements whose `align` aligns the blocks in them too. */
1836
+ const ALIGNS_BLOCKS = new Set([
1837
+ 'div',
1838
+ 'caption',
1839
+ 'thead',
1840
+ 'tbody',
1841
+ 'tfoot',
1842
+ 'tr',
1843
+ 'td',
1844
+ 'th',
1845
+ ]);
1846
+
1847
+ /** The elements a `background` attribute gives a background image. */
1848
+ const BACKGROUNDS = new Set([
1849
+ 'body',
1850
+ 'table',
1851
+ 'thead',
1852
+ 'tbody',
1853
+ 'tfoot',
1854
+ 'tr',
1855
+ 'td',
1856
+ 'th',
1857
+ ]);
1858
+
1859
+ function closestBody(el: Element): Element | null {
1860
+ let node = el.parent;
1861
+ while (node) {
1862
+ if (node.type === 'tag' && (node as Element).name === 'body') {
1863
+ return node as Element;
1864
+ }
1865
+ node = node.parent;
1866
+ }
1867
+ return null;
1868
+ }
1869
+
1870
+ /** What the `width` and `height` attributes give a ratio as well. */
1871
+ const RATIO_SIZED = new Set(['img', 'video']);
1872
+
576
1873
  const SIZED = new Set([
577
1874
  'img',
578
1875
  'table',
@@ -582,7 +1879,6 @@ const SIZED = new Set([
582
1879
  'colgroup',
583
1880
  'iframe',
584
1881
  'video',
585
- 'canvas',
586
1882
  'object',
587
1883
  'embed',
588
1884
  'hr',
@@ -596,6 +1892,18 @@ const OL_TYPES: Record<string, string> = {
596
1892
  I: 'upper-roman',
597
1893
  };
598
1894
 
1895
+ /** What a `<ul type>` or an `<li type>` names that is not a number. */
1896
+ const BULLETS = new Set(['disc', 'circle', 'square', 'none']);
1897
+
1898
+ /** An attribute read as HTML reads an integer: its leading digits, signed,
1899
+ * whatever follows them; null where it has none. A counter is a 32-bit
1900
+ * integer in a browser, and one past that is kept to it. */
1901
+ function htmlInteger(value: string | undefined): number | null {
1902
+ const match = value === undefined ? null : /^\s*([+-]?\d+)/.exec(value);
1903
+ if (!match) return null;
1904
+ return Math.max(-2147483648, Math.min(2147483647, Number(match[1])));
1905
+ }
1906
+
599
1907
  const FONT_SIZE_STEPS = [1, 0.63, 0.82, 1, 1.13, 1.5, 2, 3];
600
1908
 
601
1909
  /** A presentational length: bare numbers are pixels, `50%` stays a percent. */
@@ -615,3 +1923,36 @@ function closestTable(el: Element): Element | null {
615
1923
  }
616
1924
  return null;
617
1925
  }
1926
+
1927
+ /** The generic fixed-width family's size to everyone else's: 13 to 16. */
1928
+ const FIXED_SIZE = 13 / 16;
1929
+
1930
+ /** Whether a family list is the generic `monospace` and nothing else,
1931
+ * which has the smaller size; a list with a fallback after it does not,
1932
+ * so `monospace, monospace` keeps the size it had. */
1933
+ function monospaceOnly(family: string): boolean {
1934
+ return family.length === 9 && family.toLowerCase() === 'monospace';
1935
+ }
1936
+
1937
+ /** Whether a `font-size`, or the size in a `font`, is an absolute-size
1938
+ * keyword, or `initial`, which is `medium`. */
1939
+ function sizeKeyword(prop: string, value: string): boolean {
1940
+ const words = value.trim().toLowerCase();
1941
+ if (prop === 'font-size') return ABSOLUTE_SIZES.has(words);
1942
+ for (const word of words.split(/[\s/]+/)) {
1943
+ if (ABSOLUTE_SIZES.has(word)) return true;
1944
+ }
1945
+ return false;
1946
+ }
1947
+
1948
+ const ABSOLUTE_SIZES = new Set([
1949
+ 'xx-small',
1950
+ 'x-small',
1951
+ 'small',
1952
+ 'medium',
1953
+ 'large',
1954
+ 'x-large',
1955
+ 'xx-large',
1956
+ 'xxx-large',
1957
+ 'initial',
1958
+ ]);