@react-x11/components 0.11.0 → 0.12.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 (100) hide show
  1. package/dist/flow/node.d.ts +14 -6
  2. package/dist/flow/node.d.ts.map +1 -1
  3. package/dist/flow/node.js +82 -64
  4. package/dist/flow/node.js.map +1 -1
  5. package/dist/html/controls.d.ts +26 -7
  6. package/dist/html/controls.d.ts.map +1 -1
  7. package/dist/html/controls.js +28 -3
  8. package/dist/html/controls.js.map +1 -1
  9. package/dist/html/css/cascade.d.ts.map +1 -1
  10. package/dist/html/css/cascade.js +19 -6
  11. package/dist/html/css/cascade.js.map +1 -1
  12. package/dist/html/css/color.d.ts +19 -0
  13. package/dist/html/css/color.d.ts.map +1 -1
  14. package/dist/html/css/color.js +86 -0
  15. package/dist/html/css/color.js.map +1 -1
  16. package/dist/html/css/parse.d.ts +11 -0
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +114 -4
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/style.d.ts +38 -1
  21. package/dist/html/css/style.d.ts.map +1 -1
  22. package/dist/html/css/style.js +326 -8
  23. package/dist/html/css/style.js.map +1 -1
  24. package/dist/html/css/ua.js +14 -0
  25. package/dist/html/css/ua.js.map +1 -1
  26. package/dist/html/css/values.d.ts +10 -1
  27. package/dist/html/css/values.d.ts.map +1 -1
  28. package/dist/html/css/values.js +27 -2
  29. package/dist/html/css/values.js.map +1 -1
  30. package/dist/html/css/vars.d.ts +5 -0
  31. package/dist/html/css/vars.d.ts.map +1 -1
  32. package/dist/html/css/vars.js +2 -2
  33. package/dist/html/css/vars.js.map +1 -1
  34. package/dist/html/dom.d.ts.map +1 -1
  35. package/dist/html/dom.js +35 -2
  36. package/dist/html/dom.js.map +1 -1
  37. package/dist/html/index.d.ts.map +1 -1
  38. package/dist/html/index.js +38 -1
  39. package/dist/html/index.js.map +1 -1
  40. package/dist/html/layout/block.d.ts +11 -2
  41. package/dist/html/layout/block.d.ts.map +1 -1
  42. package/dist/html/layout/block.js +181 -18
  43. package/dist/html/layout/block.js.map +1 -1
  44. package/dist/html/layout/boxes.d.ts +34 -6
  45. package/dist/html/layout/boxes.d.ts.map +1 -1
  46. package/dist/html/layout/boxes.js +35 -14
  47. package/dist/html/layout/boxes.js.map +1 -1
  48. package/dist/html/layout/cache.d.ts +2 -2
  49. package/dist/html/layout/cache.d.ts.map +1 -1
  50. package/dist/html/layout/cache.js +8 -3
  51. package/dist/html/layout/cache.js.map +1 -1
  52. package/dist/html/layout/floats.d.ts +4 -0
  53. package/dist/html/layout/floats.d.ts.map +1 -1
  54. package/dist/html/layout/floats.js +26 -0
  55. package/dist/html/layout/floats.js.map +1 -1
  56. package/dist/html/layout/inline.d.ts +26 -2
  57. package/dist/html/layout/inline.d.ts.map +1 -1
  58. package/dist/html/layout/inline.js +1222 -115
  59. package/dist/html/layout/inline.js.map +1 -1
  60. package/dist/html/node.d.ts +33 -0
  61. package/dist/html/node.d.ts.map +1 -1
  62. package/dist/html/node.js +512 -51
  63. package/dist/html/node.js.map +1 -1
  64. package/dist/html/paint.d.ts +53 -1
  65. package/dist/html/paint.d.ts.map +1 -1
  66. package/dist/html/paint.js +546 -180
  67. package/dist/html/paint.js.map +1 -1
  68. package/dist/html/surfaces.d.ts +7 -2
  69. package/dist/html/surfaces.d.ts.map +1 -1
  70. package/dist/html/surfaces.js +16 -0
  71. package/dist/html/surfaces.js.map +1 -1
  72. package/dist/internal/window.d.ts +22 -0
  73. package/dist/internal/window.d.ts.map +1 -1
  74. package/dist/internal/window.js +35 -8
  75. package/dist/internal/window.js.map +1 -1
  76. package/dist/richtext/node.d.ts +7 -0
  77. package/dist/richtext/node.d.ts.map +1 -1
  78. package/dist/richtext/node.js.map +1 -1
  79. package/package.json +5 -3
  80. package/src/flow/node.ts +80 -64
  81. package/src/html/controls.ts +50 -9
  82. package/src/html/css/cascade.ts +19 -6
  83. package/src/html/css/color.ts +87 -0
  84. package/src/html/css/parse.ts +107 -4
  85. package/src/html/css/style.ts +343 -8
  86. package/src/html/css/ua.ts +14 -0
  87. package/src/html/css/values.ts +31 -3
  88. package/src/html/css/vars.ts +2 -2
  89. package/src/html/dom.ts +38 -2
  90. package/src/html/index.ts +39 -1
  91. package/src/html/layout/block.ts +228 -16
  92. package/src/html/layout/boxes.ts +55 -21
  93. package/src/html/layout/cache.ts +8 -3
  94. package/src/html/layout/floats.ts +24 -0
  95. package/src/html/layout/inline.ts +1435 -108
  96. package/src/html/node.ts +535 -54
  97. package/src/html/paint.ts +650 -241
  98. package/src/html/surfaces.ts +21 -1
  99. package/src/internal/window.ts +36 -8
  100. package/src/richtext/node.ts +7 -0
@@ -40,6 +40,7 @@ import {
40
40
  } from './content.js';
41
41
  import type { ContentItem, CounterChange } from './content.js';
42
42
  import type { CustomProps } from './vars.js';
43
+ import { LIGHT_DARK, lightDark, usedColorScheme } from './color.js';
43
44
 
44
45
  export type Display =
45
46
  | 'none'
@@ -202,6 +203,14 @@ export type ContentSize =
202
203
  export interface ComputedStyle {
203
204
  // --- inherited ------------------------------------------------------------
204
205
  color: string;
206
+ /**
207
+ * The colour scheme the element is drawn in (CSS Color Adjust 1, 2.2):
208
+ * its `color-scheme` resolved against the scheme the palette is, which
209
+ * stands for the reader's preference, as it does for `@media
210
+ * (prefers-color-scheme)`. What a `light-dark()` picks by. `normal` is
211
+ * the palette's own, since the palette is this renderer's default look.
212
+ */
213
+ colorScheme: 'light' | 'dark';
205
214
  fontFamily: string;
206
215
  fontSize: number;
207
216
  fontWeight: number;
@@ -320,6 +329,11 @@ export interface ComputedStyle {
320
329
  float: 'none' | 'left' | 'right';
321
330
  clear: 'none' | 'left' | 'right' | 'both';
322
331
  boxSizing: 'content-box' | 'border-box';
332
+ /** `box-decoration-break` (CSS Fragmentation 3, 5.4): how an inline box
333
+ * that goes on to another line places its background images and
334
+ * gradients — `slice`, from its fragments laid end to end as one box,
335
+ * or `clone`, from each fragment's own. */
336
+ boxDecorationBreak: 'slice' | 'clone';
323
337
  /** Whether a block is a line-clamp container, and how many lines of its
324
338
  * formatting context it shows (CSS Overflow 4, 5.3.1): its `max-lines`,
325
339
  * or Infinity for `line-clamp: auto`, which shows as many as its height
@@ -505,6 +519,10 @@ export interface ComputedStyle {
505
519
  * replaced whole when any of them changes, and `NO_BORDER_IMAGE` where
506
520
  * nothing sets them. */
507
521
  borderImage: BorderImage;
522
+ /** `mask-image` and the longhands that place its layers (CSS Masking 1,
523
+ * 7): one object, replaced whole when any of them changes, and
524
+ * `NO_MASK` where nothing sets them. */
525
+ mask: MaskLayers;
508
526
  /** `-webkit-text-fill-color`: what the glyphs are filled with where it
509
527
  * is not the text's `color` — Tailwind's `text-transparent` over a
510
528
  * `bg-clip-text` gradient; null for `color`, as `currentColor` is. */
@@ -608,6 +626,7 @@ export interface ComputedStyle {
608
626
  * same list. */
609
627
  export const INHERITED = [
610
628
  'color',
629
+ 'colorScheme',
611
630
  'textFillColor',
612
631
  'fontFamily',
613
632
  'fontSize',
@@ -689,6 +708,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
689
708
  // a copy of the literal, not the literal: see `copyStyle`
690
709
  return copyStyle({
691
710
  color: look.color,
711
+ colorScheme: look.colorScheme,
692
712
  fontFamily: look.fontFamily,
693
713
  fontSize: look.fontSize,
694
714
  fontWeight: 400,
@@ -747,6 +767,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
747
767
  float: 'none',
748
768
  clear: 'none',
749
769
  boxSizing: 'content-box',
770
+ boxDecorationBreak: 'slice',
750
771
  lineClamp: null,
751
772
  maxLines: null,
752
773
  clampContinue: 'auto',
@@ -846,6 +867,7 @@ export function initialStyle(look: RootLook, scale = 1): ComputedStyle {
846
867
  backgroundClips: null,
847
868
  backgroundOrigins: null,
848
869
  borderImage: NO_BORDER_IMAGE,
870
+ mask: NO_MASK,
849
871
  textFillColor: null,
850
872
 
851
873
  textDecorationLine: 'none',
@@ -941,6 +963,7 @@ export function inherit(
941
963
  // quarters of the cost of every element's style, a store through a
942
964
  // computed name costing thirty times one through a written one
943
965
  out.color = parent.color;
966
+ out.colorScheme = parent.colorScheme;
944
967
  out.textFillColor = parent.textFillColor;
945
968
  out.fontFamily = parent.fontFamily;
946
969
  out.fontSize = parent.fontSize;
@@ -1162,6 +1185,15 @@ export function applyDeclaration(
1162
1185
  if (/!\s*important$/i.test(value)) {
1163
1186
  value = value.replace(/!\s*important$/i, '').trim();
1164
1187
  }
1188
+ // the branch of each `light-dark()` this element's scheme picks, before
1189
+ // anything reads the value: it stands wherever a colour can, in a
1190
+ // shorthand, a shadow, a gradient or a mix, and most often arrives
1191
+ // through a `var()` from a theme on `:root`
1192
+ if (LIGHT_DARK.test(value)) {
1193
+ const picked = lightDark(value, style.colorScheme);
1194
+ if (picked === null) return;
1195
+ value = picked;
1196
+ }
1165
1197
 
1166
1198
  const color = COLOR_PROPS[name];
1167
1199
  if (color) {
@@ -1178,6 +1210,12 @@ export function applyDeclaration(
1178
1210
  }
1179
1211
 
1180
1212
  switch (name) {
1213
+ case 'color-scheme': {
1214
+ const scheme = usedColorScheme(value, ctx.initial?.colorScheme);
1215
+ if (scheme) style.colorScheme = scheme;
1216
+ return;
1217
+ }
1218
+
1181
1219
  // --- box ----------------------------------------------------------------
1182
1220
  case 'display': {
1183
1221
  const v = value.toLowerCase();
@@ -1366,6 +1404,12 @@ export function applyDeclaration(
1366
1404
  if (v === 'border-box' || v === 'content-box') style.boxSizing = v;
1367
1405
  return;
1368
1406
  }
1407
+ case 'box-decoration-break':
1408
+ case '-webkit-box-decoration-break': {
1409
+ const v = value.trim().toLowerCase();
1410
+ if (v === 'slice' || v === 'clone') style.boxDecorationBreak = v;
1411
+ return;
1412
+ }
1369
1413
  case 'aspect-ratio': {
1370
1414
  const parsed = parseAspectRatio(value);
1371
1415
  if (parsed !== undefined) style.aspectRatio = parsed;
@@ -1906,6 +1950,67 @@ export function applyDeclaration(
1906
1950
  return;
1907
1951
  }
1908
1952
 
1953
+ // --- masking --------------------------------------------------------------
1954
+ // Each `-webkit-` name is the same property's (Compatibility, 5): every
1955
+ // engine takes both, and a page writes both — Wikipedia's icons do.
1956
+ case 'mask':
1957
+ case '-webkit-mask': {
1958
+ const mask = readMask(value, ctx);
1959
+ if (mask) style.mask = mask;
1960
+ return;
1961
+ }
1962
+ case 'mask-image':
1963
+ case '-webkit-mask-image': {
1964
+ // `none` a layer as much as an image is; a bad url drops it all
1965
+ const images: BackgroundImage[] = [];
1966
+ for (const part of splitCommas(value)) {
1967
+ const image = maskImageOf(part, ctx);
1968
+ if (image === undefined) return;
1969
+ images.push(image);
1970
+ }
1971
+ if (images.length) style.mask = { ...style.mask, images };
1972
+ return;
1973
+ }
1974
+ case 'mask-repeat':
1975
+ case '-webkit-mask-repeat': {
1976
+ const repeats = layerValues(value, (part) => {
1977
+ const words = splitValue(part.toLowerCase());
1978
+ return words.length <= 2 ? readRepeat(words) : null;
1979
+ });
1980
+ if (repeats) style.mask = { ...style.mask, repeats };
1981
+ return;
1982
+ }
1983
+ case 'mask-size':
1984
+ case '-webkit-mask-size': {
1985
+ const sizes = layerValues(value, (part) =>
1986
+ backgroundSizeOf(splitValue(part), ctx),
1987
+ );
1988
+ if (sizes) style.mask = { ...style.mask, sizes };
1989
+ return;
1990
+ }
1991
+ case 'mask-position':
1992
+ case '-webkit-mask-position': {
1993
+ const positions = layerValues(value, (part) =>
1994
+ positionPair(splitValue(part), ctx),
1995
+ );
1996
+ if (positions) style.mask = { ...style.mask, positions };
1997
+ return;
1998
+ }
1999
+ case 'mask-origin':
2000
+ case '-webkit-mask-origin': {
2001
+ const origins = layerValues(value, (part) => maskBox(part));
2002
+ if (origins) style.mask = { ...style.mask, origins };
2003
+ return;
2004
+ }
2005
+ case 'mask-clip':
2006
+ case '-webkit-mask-clip': {
2007
+ const clips = layerValues(value, (part) =>
2008
+ part.toLowerCase() === 'no-clip' ? 'border-box' : maskBox(part),
2009
+ );
2010
+ if (clips) style.mask = { ...style.mask, clips };
2011
+ return;
2012
+ }
2013
+
1909
2014
  // --- generated content --------------------------------------------------
1910
2015
  case 'content': {
1911
2016
  const parsed = parseContent(value);
@@ -2013,7 +2118,13 @@ export function applyDeclaration(
2013
2118
  style.lineHeight = len;
2014
2119
  style.lineHeightIsLength = true;
2015
2120
  } else if (len && typeof len === 'object') {
2016
- const px = resolve(len, style.fontSize);
2121
+ // a percentage of the element's own font size, the one it is
2122
+ // computed with (CSS 2.1 10.8.1), which `em` already is: not the
2123
+ // size in `style` as this declaration comes up, which a `font`
2124
+ // earlier in the same rule has just set to its own — `p { font:
2125
+ // 12px …; line-height: 100% }` under a `.lead { font-size: 16px }`
2126
+ // set sixteen-pixel text on twelve-pixel lines
2127
+ const px = resolve(len, ctx.em);
2017
2128
  if (px < 0) return;
2018
2129
  style.lineHeight = px;
2019
2130
  style.lineHeightIsLength = true;
@@ -2462,7 +2573,12 @@ export function applyDeclaration(
2462
2573
  return;
2463
2574
  }
2464
2575
  case 'align-content': {
2465
- const v = alignKeyword(value);
2576
+ // `normal` is `stretch` in a flex and a grid container, the only
2577
+ // boxes it moves anything in (CSS Box Alignment 3, 5.1)
2578
+ const v =
2579
+ value.trim().toLowerCase() === 'normal'
2580
+ ? 'stretch'
2581
+ : alignKeyword(value);
2466
2582
  if (v) style.alignContent = v as ComputedStyle['alignContent'];
2467
2583
  return;
2468
2584
  }
@@ -2697,6 +2813,30 @@ export function applyDeclaration(
2697
2813
  }
2698
2814
  return;
2699
2815
  }
2816
+ case 'place-content': {
2817
+ // `<'align-content'> <'justify-content'>?` (CSS Box Alignment 3,
2818
+ // 5.3), either half one keyword or two — `first baseline`, `safe
2819
+ // center` — and the second the first again where it is left out,
2820
+ // but for a baseline, which is no `justify-content`: `start`
2821
+ const words = value.trim().toLowerCase().split(/\s+/);
2822
+ const span = (at: number) =>
2823
+ /^(?:safe|unsafe|first|last)$/.test(words[at] ?? '') ? 2 : 1;
2824
+ const cut = span(0);
2825
+ const align = words.slice(0, cut).join(' ');
2826
+ const rest = words.slice(cut);
2827
+ if (rest.length && rest.length !== span(cut)) return;
2828
+ const justify = rest.length
2829
+ ? rest.join(' ')
2830
+ : /baseline$/.test(align)
2831
+ ? 'start'
2832
+ : align;
2833
+ // a shorthand is one declaration: either half unread drops both
2834
+ if (align !== 'normal' && !alignKeyword(align)) return;
2835
+ if (!justifyKeyword(justify)) return;
2836
+ applyDeclaration(style, parent, 'align-content', align, ctx);
2837
+ applyDeclaration(style, parent, 'justify-content', justify, ctx);
2838
+ return;
2839
+ }
2700
2840
  case 'place-items':
2701
2841
  case 'place-self': {
2702
2842
  const parts = splitValue(value);
@@ -3144,6 +3284,157 @@ export const NO_BORDER_IMAGE: BorderImage = {
3144
3284
  repeat: ['stretch', 'stretch'],
3145
3285
  };
3146
3286
 
3287
+ /**
3288
+ * An element's mask layers (CSS Masking 1, 7), top first, a list a
3289
+ * property as a background's are: each layer takes the images' place in
3290
+ * the others, and they repeat where they are fewer. A layer's image is a
3291
+ * url, a gradient, or null for `none` — which is the one layer of an
3292
+ * element that is not masked.
3293
+ */
3294
+ export interface MaskLayers {
3295
+ images: readonly BackgroundImage[];
3296
+ repeats: readonly BackgroundRepeat[];
3297
+ sizes: readonly ComputedStyle['backgroundSize'][];
3298
+ positions: readonly (readonly [Len, Len])[];
3299
+ /** The box a layer is placed in, and the one it is painted in: the
3300
+ * border box unless a layer says (7.5, 7.6). */
3301
+ origins: readonly VisualBox[];
3302
+ clips: readonly VisualBox[];
3303
+ }
3304
+
3305
+ /** Whether a style masks its element: a layer of its mask has an image. */
3306
+ export function masked(style: ComputedStyle): boolean {
3307
+ const mask = style.mask;
3308
+ if (mask === NO_MASK) return false;
3309
+ for (const image of mask.images) if (image !== null) return true;
3310
+ return false;
3311
+ }
3312
+
3313
+ /**
3314
+ * One layer's `mask-image`: a url, a gradient — or none, for an image
3315
+ * function this does not draw, or a `url(#id)`, which names an SVG
3316
+ * `<mask>` element in the document rather than an image to fetch (7.2):
3317
+ * drawn with no mask rather than as the transparent layer a mask that
3318
+ * cannot be had is, since this draws no `<mask>`. Undefined where it is no
3319
+ * image at all.
3320
+ */
3321
+ function maskImageOf(
3322
+ text: string,
3323
+ ctx: UnitContext,
3324
+ ): BackgroundImage | undefined {
3325
+ const image = backgroundImageOf(text, ctx);
3326
+ return typeof image === 'string' && image[0] === '#' ? null : image;
3327
+ }
3328
+
3329
+ /** A `<geometry-box>` as an HTML element has it (CSS Masking 1, 7.5): the
3330
+ * SVG boxes are its content box and its border box. */
3331
+ function maskBox(text: string): VisualBox | null {
3332
+ const v = text.trim().toLowerCase();
3333
+ if (isVisualBox(v)) return v;
3334
+ if (v === 'fill-box') return 'content-box';
3335
+ if (v === 'stroke-box' || v === 'view-box') return 'border-box';
3336
+ return null;
3337
+ }
3338
+
3339
+ /**
3340
+ * The `mask` shorthand (CSS Masking 1, 7.10): a layer a comma group, each
3341
+ * an image, a position with a size after a `/`, a repeat, one box for the
3342
+ * origin and the clip or two, `no-clip`, a compositing operator and a
3343
+ * mode, each at most once, in any order. The operator and the mode are
3344
+ * read and not honoured: layers are added, and an image masks by its
3345
+ * alpha, which is what `match-source` makes of one. Null where a layer is
3346
+ * not one, which drops the declaration.
3347
+ */
3348
+ function readMask(value: string, ctx: UnitContext): MaskLayers | null {
3349
+ const out = {
3350
+ images: [] as BackgroundImage[],
3351
+ repeats: [] as BackgroundRepeat[],
3352
+ sizes: [] as ComputedStyle['backgroundSize'][],
3353
+ positions: [] as [Len, Len][],
3354
+ origins: [] as VisualBox[],
3355
+ clips: [] as VisualBox[],
3356
+ };
3357
+ for (const text of splitCommas(value)) {
3358
+ const parts = splitValue(text).flatMap(splitSlash);
3359
+ let image: BackgroundImage = null;
3360
+ let repeat = NO_MASK.repeats[0];
3361
+ let size: ComputedStyle['backgroundSize'] = 'auto';
3362
+ let position: [Len, Len] = [0, 0];
3363
+ const boxes: VisualBox[] = [];
3364
+ let seen = 0;
3365
+ const once = (bit: number): boolean => {
3366
+ if (seen & bit) return false;
3367
+ seen |= bit;
3368
+ return true;
3369
+ };
3370
+ for (let i = 0; i < parts.length;) {
3371
+ const part = parts[i];
3372
+ const v = part.toLowerCase();
3373
+ if (v === 'none' || v.startsWith('url(') || IMAGE_FUNCTION.test(v)) {
3374
+ if (!once(1)) return null;
3375
+ const read = maskImageOf(part, ctx);
3376
+ if (read === undefined) return null;
3377
+ image = read;
3378
+ i += 1;
3379
+ } else if (v === 'repeat-x' || v === 'repeat-y' || REPEATS.has(v)) {
3380
+ if (!once(2)) return null;
3381
+ const pair = REPEATS.has(v) && REPEATS.has(parts[i + 1]?.toLowerCase());
3382
+ const read = readRepeat(pair ? [v, parts[i + 1].toLowerCase()] : [v]);
3383
+ if (!read) return null;
3384
+ repeat = read;
3385
+ i += pair ? 2 : 1;
3386
+ } else if (maskBox(v) || v === 'no-clip') {
3387
+ if (boxes.length === 2 || (v === 'no-clip' && !once(4))) return null;
3388
+ boxes.push(maskBox(v) ?? 'border-box');
3389
+ i += 1;
3390
+ } else if (/^(?:add|subtract|intersect|exclude)$/.test(v)) {
3391
+ if (!once(8)) return null;
3392
+ i += 1;
3393
+ } else if (/^(?:alpha|luminance|match-source)$/.test(v)) {
3394
+ if (!once(16)) return null;
3395
+ i += 1;
3396
+ } else if (isPositionPart(v, ctx)) {
3397
+ if (!once(32)) return null;
3398
+ let end = i;
3399
+ while (
3400
+ end < parts.length &&
3401
+ isPositionPart(parts[end].toLowerCase(), ctx)
3402
+ ) {
3403
+ end += 1;
3404
+ }
3405
+ const read = positionPair(parts.slice(i, end), ctx);
3406
+ if (!read) return null;
3407
+ position = read;
3408
+ i = end;
3409
+ if (parts[i] !== '/') continue;
3410
+ const first = parts[i + 1]?.toLowerCase();
3411
+ if (first === 'cover' || first === 'contain') {
3412
+ size = first;
3413
+ i += 2;
3414
+ } else if (first !== undefined && isSizePart(first, ctx)) {
3415
+ const two =
3416
+ i + 2 < parts.length && isSizePart(parts[i + 2].toLowerCase(), ctx);
3417
+ size =
3418
+ backgroundSizeOf(parts.slice(i + 1, i + (two ? 3 : 2)), ctx) ??
3419
+ 'auto';
3420
+ i += two ? 3 : 2;
3421
+ } else {
3422
+ return null;
3423
+ }
3424
+ } else {
3425
+ return null;
3426
+ }
3427
+ }
3428
+ out.images.push(image);
3429
+ out.repeats.push(repeat);
3430
+ out.sizes.push(size);
3431
+ out.positions.push(position);
3432
+ out.origins.push(boxes[0] ?? 'border-box');
3433
+ out.clips.push(boxes[1] ?? boxes[0] ?? 'border-box');
3434
+ }
3435
+ return out.images.length ? out : null;
3436
+ }
3437
+
3147
3438
  /** One to four values, one a side as `margin` takes them; null where one
3148
3439
  * is not a value. */
3149
3440
  function sidesOf<T>(
@@ -3338,6 +3629,15 @@ function repeatPair(x: RepeatMode, y: RepeatMode): BackgroundRepeat {
3338
3629
 
3339
3630
  const REPEAT = repeatPair('repeat', 'repeat');
3340
3631
 
3632
+ export const NO_MASK: MaskLayers = {
3633
+ images: [null],
3634
+ repeats: [REPEAT],
3635
+ sizes: ['auto'],
3636
+ positions: [[0, 0]],
3637
+ origins: ['border-box'],
3638
+ clips: ['border-box'],
3639
+ };
3640
+
3341
3641
  function applyBorderShorthand(
3342
3642
  style: ComputedStyle,
3343
3643
  name: string,
@@ -4026,12 +4326,10 @@ function applyFontShorthand(
4026
4326
  style.lineHeight = 'normal';
4027
4327
  style.lineHeightIsLength = false;
4028
4328
  applyDeclaration(style, parent, 'font-size', sizeText, ctx);
4029
- if (lineText) {
4030
- applyDeclaration(style, parent, 'line-height', lineText, {
4031
- ...ctx,
4032
- em: style.fontSize,
4033
- });
4034
- }
4329
+ // against the element's own size, which `ctx.em` is once the cascade has
4330
+ // settled it: a later rule's `font-size` outranks this one's, and
4331
+ // `font: 12px/150%` under a `font-size: 16px` is on 24px lines
4332
+ if (lineText) applyDeclaration(style, parent, 'line-height', lineText, ctx);
4035
4333
  applyDeclaration(style, parent, 'font-family', family, ctx);
4036
4334
  }
4037
4335
 
@@ -4751,6 +5049,7 @@ function camel(name: string): string {
4751
5049
 
4752
5050
  const INHERITED_NAMES = new Set<string>([
4753
5051
  'color',
5052
+ 'color-scheme',
4754
5053
  '-webkit-text-fill-color',
4755
5054
  'font',
4756
5055
  'font-family',
@@ -4801,6 +5100,11 @@ export function initialOne(
4801
5100
  initial: ComputedStyle,
4802
5101
  name: string,
4803
5102
  ): void {
5103
+ const list = MASK_LISTS[name];
5104
+ if (list) {
5105
+ style.mask = { ...style.mask, [list]: initial.mask[list] };
5106
+ return;
5107
+ }
4804
5108
  const keys = INHERIT_TARGETS[name];
4805
5109
  if (!keys) return;
4806
5110
  for (const key of keys) {
@@ -4813,6 +5117,11 @@ function inheritOne(
4813
5117
  parent: ComputedStyle,
4814
5118
  name: string,
4815
5119
  ): void {
5120
+ const list = MASK_LISTS[name];
5121
+ if (list) {
5122
+ style.mask = { ...style.mask, [list]: parent.mask[list] };
5123
+ return;
5124
+ }
4816
5125
  const keys = INHERIT_TARGETS[name];
4817
5126
  if (!keys) return;
4818
5127
  // A border colour left to `currentColor` inherits as the keyword and
@@ -4828,8 +5137,30 @@ const sides = (
4828
5137
  make: (side: (typeof SIDES)[number]) => keyof ComputedStyle,
4829
5138
  ): (keyof ComputedStyle)[] => SIDES.map(make);
4830
5139
 
5140
+ /**
5141
+ * The list of the mask's layers each longhand is, which `initial`,
5142
+ * `inherit` and a `var()` that fails reset alone: one field holds them all,
5143
+ * and reset whole, a `mask-size: calc(var(--x) - 4px)` put back the
5144
+ * `mask-repeat: no-repeat` before it, and Wikipedia's chevron repeated.
5145
+ */
5146
+ const MASK_LISTS: Record<string, keyof MaskLayers> = {
5147
+ 'mask-image': 'images',
5148
+ '-webkit-mask-image': 'images',
5149
+ 'mask-repeat': 'repeats',
5150
+ '-webkit-mask-repeat': 'repeats',
5151
+ 'mask-size': 'sizes',
5152
+ '-webkit-mask-size': 'sizes',
5153
+ 'mask-position': 'positions',
5154
+ '-webkit-mask-position': 'positions',
5155
+ 'mask-origin': 'origins',
5156
+ '-webkit-mask-origin': 'origins',
5157
+ 'mask-clip': 'clips',
5158
+ '-webkit-mask-clip': 'clips',
5159
+ };
5160
+
4831
5161
  const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4832
5162
  color: ['color'],
5163
+ 'color-scheme': ['colorScheme'],
4833
5164
  '-webkit-text-fill-color': ['textFillColor'],
4834
5165
  'background-clip': [
4835
5166
  'backgroundClipText',
@@ -4843,6 +5174,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4843
5174
  ],
4844
5175
  'background-origin': ['backgroundOrigin', 'backgroundOrigins'],
4845
5176
  'border-image': ['borderImage'],
5177
+ mask: ['mask'],
5178
+ '-webkit-mask': ['mask'],
4846
5179
  'border-image-source': ['borderImage'],
4847
5180
  'border-image-slice': ['borderImage'],
4848
5181
  'border-image-width': ['borderImage'],
@@ -4923,6 +5256,8 @@ const INHERIT_TARGETS: Record<string, readonly (keyof ComputedStyle)[]> = {
4923
5256
  'min-height': ['minHeight', 'minHeightKeyword'],
4924
5257
  'max-height': ['maxHeight', 'maxHeightKeyword'],
4925
5258
  'box-sizing': ['boxSizing'],
5259
+ 'box-decoration-break': ['boxDecorationBreak'],
5260
+ '-webkit-box-decoration-break': ['boxDecorationBreak'],
4926
5261
  margin: sides((s) => `margin${s}`),
4927
5262
  'margin-top': ['marginTop'],
4928
5263
  'margin-right': ['marginRight'],
@@ -188,6 +188,20 @@ input, button, select, textarea, meter, progress {
188
188
  }
189
189
  /* Chrome's own UA margins for the checkables, near enough: they are the
190
190
  controls that sit hard against their label text otherwise. */
191
+ /* HTML's rendering section (15.3.10): a control's text keeps none of the
192
+ spacing, the line height, the case or the indent of the text around it.
193
+ A button in a paragraph of 'line-height: 1.5' is its own font's line
194
+ tall, as it is in a browser. */
195
+ input, button, textarea {
196
+ letter-spacing: initial;
197
+ word-spacing: initial;
198
+ line-height: initial;
199
+ }
200
+ input, select, button, textarea {
201
+ text-transform: initial;
202
+ text-indent: initial;
203
+ text-shadow: initial;
204
+ }
191
205
  input[type=checkbox] { margin: 3px 4px 3px 4px; }
192
206
  input[type=radio] { margin: 3px 4px 3px 5px; }
193
207
  input[type=hidden] { display: none; }
@@ -102,7 +102,7 @@ export interface UnitContext {
102
102
  em: number;
103
103
  /** The root element's `font-size`. */
104
104
  rem: number;
105
- /** Viewport, for `vw`/`vh`/`vmin`/`vmax`. */
105
+ /** Viewport, for `vw`/`vh`/`vmin`/`vmax` and their kin (`viewportUnit`). */
106
106
  vw: number;
107
107
  vh: number;
108
108
  /** Device pixels per CSS pixel — the display scale. */
@@ -131,9 +131,37 @@ export interface UnitContext {
131
131
  const NUMBER_SRC = '[+-]?(?:\\d*\\.\\d+|\\d+)(?:e[+-]?\\d+)?';
132
132
 
133
133
  const LENGTH_RE = new RegExp(
134
- `^(${NUMBER_SRC})(px|em|rem|pt|pc|in|cm|mm|ex|ch|lh|rlh|vw|vh|vmin|vmax|q|%)?$`,
134
+ `^(${NUMBER_SRC})(px|em|rem|pt|pc|in|cm|mm|ex|ch|lh|rlh|[sld]?v(?:w|h|i|b|min|max)|q|%)?$`,
135
135
  );
136
136
 
137
+ /**
138
+ * The viewport unit a viewport-percentage length is (CSS Values 4, 6.1.2),
139
+ * or null for another unit. The small, large and dynamic viewports —
140
+ * `svh`, `lvh`, `dvh` and their kin — are the one viewport here, as they
141
+ * are on a desktop, where no toolbar comes and goes over the page; and the
142
+ * inline and block axes, `vi` and `vb`, are the width and the height of
143
+ * the horizontal writing mode every document here is laid out in.
144
+ */
145
+ export function viewportUnit(
146
+ unit: string,
147
+ ): 'vw' | 'vh' | 'vmin' | 'vmax' | null {
148
+ const u = unit.toLowerCase();
149
+ const bare = /^[sld]v/.test(u) ? u.slice(1) : u;
150
+ switch (bare) {
151
+ case 'vw':
152
+ case 'vi':
153
+ return 'vw';
154
+ case 'vh':
155
+ case 'vb':
156
+ return 'vh';
157
+ case 'vmin':
158
+ case 'vmax':
159
+ return bare;
160
+ default:
161
+ return null;
162
+ }
163
+ }
164
+
137
165
  const NUMBER_RE = new RegExp(`^${NUMBER_SRC}$`, 'i');
138
166
 
139
167
  /** A math function: `calc()`, `min()`, `max()` or `clamp()`. */
@@ -166,7 +194,7 @@ export function parseLength(
166
194
  });
167
195
  }
168
196
  const n = Number(m[1]);
169
- const unit = m[2];
197
+ const unit = m[2] && (viewportUnit(m[2]) ?? m[2]);
170
198
  // a zero needs no unit however it is written — `-0`, `+0`, `0.0`
171
199
  if (!unit) return n === 0 ? 0 : bareIsPx ? n * ctx.scale : null;
172
200
  if (unit === '%') return { pct: n };
@@ -245,7 +245,7 @@ function findVar(value: string, from: number): number {
245
245
 
246
246
  /** The `)` that closes a bracket opened just before `from`, strings and
247
247
  * brackets inside it passed over; -1 where none does. */
248
- function closingParen(value: string, from: number): number {
248
+ export function closingParen(value: string, from: number): number {
249
249
  let depth = 0;
250
250
  for (let i = from; i < value.length; i += 1) {
251
251
  const c = value[i];
@@ -326,7 +326,7 @@ function topLevel(value: string, ch: string): number {
326
326
  }
327
327
 
328
328
  /** The first comma not inside a bracket or a string, or -1. */
329
- function topLevelComma(value: string): number {
329
+ export function topLevelComma(value: string): number {
330
330
  let depth = 0;
331
331
  for (let i = 0; i < value.length; i += 1) {
332
332
  const c = value[i];
package/src/html/dom.ts CHANGED
@@ -572,15 +572,51 @@ function createParser(): { parser: Parser; handler: DomHandler } {
572
572
  withStartIndices: false,
573
573
  withEndIndices: false,
574
574
  });
575
- const parser = new Parser(handler, {
575
+ const parser = new DocumentParser(handler, {
576
576
  lowerCaseTags: true,
577
577
  lowerCaseAttributeNames: true,
578
- recognizeSelfClosing: true,
578
+ // HTML closes an element written `<x/>` only where it is void, and so
579
+ // closed anyway, or in SVG or MathML (HTML 13.2.6.5, "acknowledge the
580
+ // self-closing flag"). A `<div/>` or an `<a href="" /*target="_blank"*/>`
581
+ // — melbcss.com's, a comment that is no comment in a tag — opens an
582
+ // element like any other, and closing it left the link's text outside
583
+ // the link.
584
+ recognizeSelfClosing: false,
579
585
  decodeEntities: true,
580
586
  });
581
587
  return { parser, handler };
582
588
  }
583
589
 
590
+ /**
591
+ * htmlparser2's parser, deciding a `/>` from the context its tag was read
592
+ * in, as HTML's does. htmlparser2 decides from the context the tag opens,
593
+ * and an integration point — SVG's `<title>`, `<desc>` and
594
+ * `<foreignObject>`, MathML's `<mi>` and its kin — opens an HTML one, so
595
+ * an `<svg><title/><path/>` took its path into the title.
596
+ */
597
+ class DocumentParser extends Parser {
598
+ private _foreignTag = false;
599
+ private _acknowledging = false;
600
+
601
+ override isInForeignContext(): boolean {
602
+ return this._acknowledging || super.isInForeignContext();
603
+ }
604
+
605
+ override onopentagname(start: number, endIndex: number): void {
606
+ this._foreignTag = this.isInForeignContext();
607
+ super.onopentagname(start, endIndex);
608
+ }
609
+
610
+ override onselfclosingtag(endIndex: number): void {
611
+ this._acknowledging = this._foreignTag;
612
+ try {
613
+ super.onselfclosingtag(endIndex);
614
+ } finally {
615
+ this._acknowledging = false;
616
+ }
617
+ }
618
+ }
619
+
584
620
  /**
585
621
  * Whether a `media` attribute is one a screen honours. Deliberately not a
586
622
  * media-query engine: `screen`, `all` and an empty list apply, `print` and