@react-x11/components 0.10.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 (155) hide show
  1. package/dist/code/index.d.ts.map +1 -1
  2. package/dist/code/index.js +25 -24
  3. package/dist/code/index.js.map +1 -1
  4. package/dist/code-language/index.d.ts +1 -1
  5. package/dist/code-language/index.d.ts.map +1 -1
  6. package/dist/code-language/index.js +1 -1
  7. package/dist/code-language/index.js.map +1 -1
  8. package/dist/code-language/runs.d.ts +31 -0
  9. package/dist/code-language/runs.d.ts.map +1 -1
  10. package/dist/code-language/runs.js +105 -0
  11. package/dist/code-language/runs.js.map +1 -1
  12. package/dist/codeblock/index.d.ts +13 -0
  13. package/dist/codeblock/index.d.ts.map +1 -1
  14. package/dist/codeblock/index.js +37 -1
  15. package/dist/codeblock/index.js.map +1 -1
  16. package/dist/flow/node.d.ts +14 -6
  17. package/dist/flow/node.d.ts.map +1 -1
  18. package/dist/flow/node.js +82 -64
  19. package/dist/flow/node.js.map +1 -1
  20. package/dist/html/controls.d.ts +26 -7
  21. package/dist/html/controls.d.ts.map +1 -1
  22. package/dist/html/controls.js +28 -3
  23. package/dist/html/controls.js.map +1 -1
  24. package/dist/html/css/cascade.d.ts +32 -10
  25. package/dist/html/css/cascade.d.ts.map +1 -1
  26. package/dist/html/css/cascade.js +89 -31
  27. package/dist/html/css/cascade.js.map +1 -1
  28. package/dist/html/css/color.d.ts +19 -0
  29. package/dist/html/css/color.d.ts.map +1 -1
  30. package/dist/html/css/color.js +86 -0
  31. package/dist/html/css/color.js.map +1 -1
  32. package/dist/html/css/parse.d.ts +11 -0
  33. package/dist/html/css/parse.d.ts.map +1 -1
  34. package/dist/html/css/parse.js +114 -4
  35. package/dist/html/css/parse.js.map +1 -1
  36. package/dist/html/css/style.d.ts +45 -5
  37. package/dist/html/css/style.d.ts.map +1 -1
  38. package/dist/html/css/style.js +363 -14
  39. package/dist/html/css/style.js.map +1 -1
  40. package/dist/html/css/ua.js +22 -5
  41. package/dist/html/css/ua.js.map +1 -1
  42. package/dist/html/css/values.d.ts +10 -1
  43. package/dist/html/css/values.d.ts.map +1 -1
  44. package/dist/html/css/values.js +27 -2
  45. package/dist/html/css/values.js.map +1 -1
  46. package/dist/html/css/vars.d.ts +5 -0
  47. package/dist/html/css/vars.d.ts.map +1 -1
  48. package/dist/html/css/vars.js +2 -2
  49. package/dist/html/css/vars.js.map +1 -1
  50. package/dist/html/dom.d.ts.map +1 -1
  51. package/dist/html/dom.js +35 -2
  52. package/dist/html/dom.js.map +1 -1
  53. package/dist/html/index.d.ts.map +1 -1
  54. package/dist/html/index.js +38 -1
  55. package/dist/html/index.js.map +1 -1
  56. package/dist/html/layout/block.d.ts +11 -2
  57. package/dist/html/layout/block.d.ts.map +1 -1
  58. package/dist/html/layout/block.js +306 -25
  59. package/dist/html/layout/block.js.map +1 -1
  60. package/dist/html/layout/boxes.d.ts +61 -6
  61. package/dist/html/layout/boxes.d.ts.map +1 -1
  62. package/dist/html/layout/boxes.js +65 -16
  63. package/dist/html/layout/boxes.js.map +1 -1
  64. package/dist/html/layout/cache.d.ts +2 -2
  65. package/dist/html/layout/cache.d.ts.map +1 -1
  66. package/dist/html/layout/cache.js +8 -3
  67. package/dist/html/layout/cache.js.map +1 -1
  68. package/dist/html/layout/floats.d.ts +4 -0
  69. package/dist/html/layout/floats.d.ts.map +1 -1
  70. package/dist/html/layout/floats.js +26 -0
  71. package/dist/html/layout/floats.js.map +1 -1
  72. package/dist/html/layout/inline.d.ts +29 -2
  73. package/dist/html/layout/inline.d.ts.map +1 -1
  74. package/dist/html/layout/inline.js +1454 -125
  75. package/dist/html/layout/inline.js.map +1 -1
  76. package/dist/html/node.d.ts +33 -0
  77. package/dist/html/node.d.ts.map +1 -1
  78. package/dist/html/node.js +608 -86
  79. package/dist/html/node.js.map +1 -1
  80. package/dist/html/paint.d.ts +53 -1
  81. package/dist/html/paint.d.ts.map +1 -1
  82. package/dist/html/paint.js +746 -214
  83. package/dist/html/paint.js.map +1 -1
  84. package/dist/html/surfaces.d.ts +7 -2
  85. package/dist/html/surfaces.d.ts.map +1 -1
  86. package/dist/html/surfaces.js +16 -0
  87. package/dist/html/surfaces.js.map +1 -1
  88. package/dist/html/svg.d.ts.map +1 -1
  89. package/dist/html/svg.js +27 -20
  90. package/dist/html/svg.js.map +1 -1
  91. package/dist/internal/codelines.d.ts +42 -0
  92. package/dist/internal/codelines.d.ts.map +1 -0
  93. package/dist/internal/codelines.js +170 -0
  94. package/dist/internal/codelines.js.map +1 -0
  95. package/dist/internal/window.d.ts +22 -0
  96. package/dist/internal/window.d.ts.map +1 -1
  97. package/dist/internal/window.js +35 -8
  98. package/dist/internal/window.js.map +1 -1
  99. package/dist/markdown/index.d.ts.map +1 -1
  100. package/dist/markdown/index.js +98 -26
  101. package/dist/markdown/index.js.map +1 -1
  102. package/dist/rich-text-editor/index.d.ts.map +1 -1
  103. package/dist/rich-text-editor/index.js +3 -8
  104. package/dist/rich-text-editor/index.js.map +1 -1
  105. package/dist/rich-text-editor/markdown.d.ts.map +1 -1
  106. package/dist/rich-text-editor/markdown.js +30 -1
  107. package/dist/rich-text-editor/markdown.js.map +1 -1
  108. package/dist/rich-text-editor/replace.d.ts +13 -0
  109. package/dist/rich-text-editor/replace.d.ts.map +1 -0
  110. package/dist/rich-text-editor/replace.js +64 -0
  111. package/dist/rich-text-editor/replace.js.map +1 -0
  112. package/dist/richtext/node.d.ts +7 -0
  113. package/dist/richtext/node.d.ts.map +1 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/terminal-output/index.d.ts.map +1 -1
  116. package/dist/terminal-output/index.js +100 -25
  117. package/dist/terminal-output/index.js.map +1 -1
  118. package/dist/terminal-output/runs.d.ts +4 -1
  119. package/dist/terminal-output/runs.d.ts.map +1 -1
  120. package/dist/terminal-output/runs.js +5 -1
  121. package/dist/terminal-output/runs.js.map +1 -1
  122. package/package.json +5 -3
  123. package/src/code/index.ts +34 -32
  124. package/src/code-language/index.ts +1 -1
  125. package/src/code-language/runs.ts +111 -1
  126. package/src/codeblock/index.ts +48 -1
  127. package/src/flow/node.ts +80 -64
  128. package/src/html/controls.ts +50 -9
  129. package/src/html/css/cascade.ts +134 -43
  130. package/src/html/css/color.ts +87 -0
  131. package/src/html/css/parse.ts +107 -4
  132. package/src/html/css/style.ts +386 -23
  133. package/src/html/css/ua.ts +22 -5
  134. package/src/html/css/values.ts +31 -3
  135. package/src/html/css/vars.ts +2 -2
  136. package/src/html/dom.ts +38 -2
  137. package/src/html/index.ts +39 -1
  138. package/src/html/layout/block.ts +360 -22
  139. package/src/html/layout/boxes.ts +98 -23
  140. package/src/html/layout/cache.ts +8 -3
  141. package/src/html/layout/floats.ts +24 -0
  142. package/src/html/layout/inline.ts +1690 -119
  143. package/src/html/node.ts +648 -101
  144. package/src/html/paint.ts +883 -283
  145. package/src/html/surfaces.ts +21 -1
  146. package/src/html/svg.ts +30 -19
  147. package/src/internal/codelines.ts +200 -0
  148. package/src/internal/window.ts +36 -8
  149. package/src/markdown/index.ts +145 -26
  150. package/src/rich-text-editor/index.ts +3 -11
  151. package/src/rich-text-editor/markdown.ts +36 -1
  152. package/src/rich-text-editor/replace.ts +85 -0
  153. package/src/richtext/node.ts +7 -0
  154. package/src/terminal-output/index.ts +127 -34
  155. package/src/terminal-output/runs.ts +16 -2
@@ -49,6 +49,7 @@ import { parseDeclarations } from './parse.js';
49
49
  import { CounterStyles, counterStyleRule } from './counter-styles.js';
50
50
  import type { CounterStyleRule } from './counter-styles.js';
51
51
  import type { UnitContext } from './values.js';
52
+ import { viewportUnit } from './values.js';
52
53
  import { customProperties, substituteIn } from './vars.js';
53
54
  import type { CustomProps } from './vars.js';
54
55
 
@@ -514,6 +515,46 @@ export interface PointerState {
514
515
 
515
516
  const NO_POINTER: PointerState = { hovered: new Set(), active: new Set() };
516
517
 
518
+ /**
519
+ * The face a style's font-relative units are measured in: its family list,
520
+ * size, weight and slant, the four that pick one. A family's faces can be
521
+ * different fonts altogether — a page's `@font-face` rules may set its
522
+ * bold in another file — so an `ex` in bold text is the bold face's.
523
+ */
524
+ export interface MetricFace {
525
+ family: string;
526
+ size: number;
527
+ weight: number;
528
+ style: ComputedStyle['fontStyle'];
529
+ }
530
+
531
+ /** What a face measures, where the fonts can say. */
532
+ export type FaceMetric = (face: MetricFace) => number | null;
533
+
534
+ /** What a style's face is read from: a computed style, or the part of one
535
+ * a declaration's font-relative units are measured against. */
536
+ type FaceSource = Pick<
537
+ ComputedStyle,
538
+ 'fontFamily' | 'fontSize' | 'fontWeight' | 'fontStyle'
539
+ >;
540
+
541
+ /** …and its line height, for `lh`. */
542
+ type LineSource = FaceSource &
543
+ Pick<ComputedStyle, 'lineHeight' | 'lineHeightIsLength'>;
544
+
545
+ function faceOf(style: FaceSource): MetricFace {
546
+ return {
547
+ family: style.fontFamily,
548
+ size: style.fontSize,
549
+ weight: style.fontWeight,
550
+ style: style.fontStyle,
551
+ };
552
+ }
553
+
554
+ function faceKey(style: FaceSource): string {
555
+ return `${style.fontFamily}\u0001${style.fontSize}\u0001${style.fontWeight}\u0001${style.fontStyle}`;
556
+ }
557
+
517
558
  /**
518
559
  * A cascade over a fixed set of stylesheets. Rebuilt when the sheets change;
519
560
  * re-run when the DOM, the viewport band or the pointer state does.
@@ -564,17 +605,15 @@ export class Cascade {
564
605
  /** The counter styles the sheets define, over the predefined ones. */
565
606
  readonly counterStyles: CounterStyles;
566
607
 
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;
608
+ /** A face's x-height, for `ex`, where the fonts can say. */
609
+ private _xHeightOf: FaceMetric | null;
569
610
  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;
611
+ /** The advance of a face's "0", for `ch`, likewise. */
612
+ private _zeroWidthOf: FaceMetric | null;
573
613
  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;
614
+ /** A face's own line height, for `lh` where `line-height` is `normal`,
615
+ * likewise. */
616
+ private _normalLineOf: FaceMetric | null;
578
617
  private _normalLines = new Map<string, number>();
579
618
  /** Whether any declaration sets a custom property or reads one: without
580
619
  * one, no element's style asks about them. */
@@ -599,9 +638,9 @@ export class Cascade {
599
638
  viewportWidth: number,
600
639
  viewportHeight: number,
601
640
  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,
641
+ xHeight: FaceMetric | null = null,
642
+ zeroWidth: FaceMetric | null = null,
643
+ normalLine: FaceMetric | null = null,
605
644
  families: FontFamilies | null = null,
606
645
  ) {
607
646
  this._xHeightOf = xHeight;
@@ -665,7 +704,7 @@ export class Cascade {
665
704
  for (const d of declarations) {
666
705
  if (!VIEWPORT_UNIT.test(d.value)) continue;
667
706
  for (const m of d.value.matchAll(VIEWPORT_UNITS)) {
668
- const unit = m[1].toLowerCase();
707
+ const unit = viewportUnit(m[1]);
669
708
  if (unit !== 'vh') this.readsViewportWidth = true;
670
709
  if (unit !== 'vw') this.readsViewportHeight = true;
671
710
  }
@@ -1082,13 +1121,13 @@ export class Cascade {
1082
1121
  return style;
1083
1122
  }
1084
1123
 
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}`;
1124
+ /** The x-height of a style's font: the font's own, asked once per face,
1125
+ * or half an em. */
1126
+ private _exOf(style: FaceSource): number {
1127
+ const key = faceKey(style);
1089
1128
  let ex = this._xHeights.get(key);
1090
1129
  if (ex === undefined) {
1091
- ex = this._xHeightOf?.(style.fontFamily, style.fontSize) ?? NaN;
1130
+ ex = this._xHeightOf?.(faceOf(style)) ?? NaN;
1092
1131
  if (!(ex > 0)) ex = style.fontSize * 0.5;
1093
1132
  this._xHeights.set(key, ex);
1094
1133
  }
@@ -1096,12 +1135,12 @@ export class Cascade {
1096
1135
  }
1097
1136
 
1098
1137
  /** 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}`;
1138
+ * face, or half an em. */
1139
+ private _chOf(style: FaceSource): number {
1140
+ const key = faceKey(style);
1102
1141
  let ch = this._zeroWidths.get(key);
1103
1142
  if (ch === undefined) {
1104
- ch = this._zeroWidthOf?.(style.fontFamily, style.fontSize) ?? NaN;
1143
+ ch = this._zeroWidthOf?.(faceOf(style)) ?? NaN;
1105
1144
  if (!(ch > 0)) ch = style.fontSize * 0.5;
1106
1145
  this._zeroWidths.set(key, ch);
1107
1146
  }
@@ -1109,16 +1148,16 @@ export class Cascade {
1109
1148
  }
1110
1149
 
1111
1150
  /** 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 {
1151
+ * its font's own, asked once per face, or 1.2em. */
1152
+ private _lineHeightOf(style: LineSource): number {
1114
1153
  const set = style.lineHeight;
1115
1154
  if (set !== 'normal') {
1116
1155
  return style.lineHeightIsLength ? set : set * style.fontSize;
1117
1156
  }
1118
- const key = `${style.fontFamily}\u0001${style.fontSize}`;
1157
+ const key = faceKey(style);
1119
1158
  let line = this._normalLines.get(key);
1120
1159
  if (line === undefined) {
1121
- line = this._normalLineOf?.(style.fontFamily, style.fontSize) ?? NaN;
1160
+ line = this._normalLineOf?.(faceOf(style)) ?? NaN;
1122
1161
  if (!(line > 0)) line = style.fontSize * 1.2;
1123
1162
  this._normalLines.set(key, line);
1124
1163
  }
@@ -1154,12 +1193,22 @@ export class Cascade {
1154
1193
  lh: () => this._lineHeightOf(parentStyle),
1155
1194
  rlh: () => this._lineHeightOf(this.initial),
1156
1195
  };
1157
- // the family goes with the size, so an `ex` after it is its font's
1196
+ // The family, the weight and the slant go with the size: together they
1197
+ // pick the face an `ex`, a `ch` or an `lh` in any declaration is
1198
+ // measured in, however the declarations are ordered. A `width: 10ex`
1199
+ // in a sheet under an inline `font-weight: 900` was measured in the
1200
+ // family's regular face. The colour scheme goes ahead of the rest for
1201
+ // the same reason: every `light-dark()` among them is read by it.
1158
1202
  let sized = false;
1159
1203
  let keyword = false;
1160
1204
  for (const c of candidates) {
1161
1205
  for (const d of pick(c)) {
1162
- if (d.prop === 'font-family') {
1206
+ if (
1207
+ d.prop === 'font-family' ||
1208
+ d.prop === 'font-weight' ||
1209
+ d.prop === 'font-style' ||
1210
+ d.prop === 'color-scheme'
1211
+ ) {
1163
1212
  this._apply(style, parentStyle, d, ctxParent);
1164
1213
  } else if (d.prop === 'font-size' || d.prop === 'font') {
1165
1214
  // NaN until a size is set, so one the declaration did not take
@@ -1188,21 +1237,43 @@ export class Cascade {
1188
1237
  ) {
1189
1238
  style.fontSize *= mono ? FIXED_SIZE : 1 / FIXED_SIZE;
1190
1239
  }
1240
+ // The face as the first pass settled it. The second applies every
1241
+ // declaration again in cascade order, the font's among them, so the
1242
+ // style passes through the fonts of every rule on the way: read from
1243
+ // it, the `10ex` in `div { font-family: foo; width: 10ex }` was `foo`'s
1244
+ // under an inline `font-family: Ahem` that came after it.
1245
+ const face: FaceSource = {
1246
+ fontFamily: style.fontFamily,
1247
+ fontSize: style.fontSize,
1248
+ fontWeight: style.fontWeight,
1249
+ fontStyle: style.fontStyle,
1250
+ };
1251
+ let line: LineSource | null = null;
1191
1252
  const ctx: UnitContext = {
1192
1253
  ...ctxParent,
1193
1254
  em: style.fontSize,
1194
- ex: () => this._exOf(style),
1195
- ch: () => this._chOf(style),
1196
- lh: () => this._lineHeightOf(style),
1255
+ ex: () => this._exOf(face),
1256
+ ch: () => this._chOf(face),
1257
+ lh: () => this._lineHeightOf(line ?? style),
1197
1258
  };
1198
1259
  // 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
1260
+ // of them — a `font` sets one too — and read as it is then, for the
1261
+ // same reason as the face: set again with the rest, in its place among
1262
+ // them. A `font` sets the size as well, which the first pass settled.
1200
1263
  if (this._lh) {
1201
1264
  for (const c of candidates) {
1202
1265
  for (const d of pick(c)) {
1203
- if (d.prop === 'line-height') this._apply(style, parentStyle, d, ctx);
1266
+ if (d.prop === 'line-height' || d.prop === 'font') {
1267
+ this._apply(style, parentStyle, d, ctx);
1268
+ }
1204
1269
  }
1205
1270
  }
1271
+ style.fontSize = face.fontSize;
1272
+ line = {
1273
+ ...face,
1274
+ lineHeight: style.lineHeight,
1275
+ lineHeightIsLength: style.lineHeightIsLength,
1276
+ };
1206
1277
  }
1207
1278
  const settled = style.fontSize;
1208
1279
  for (const c of candidates) {
@@ -1283,22 +1354,32 @@ export class Cascade {
1283
1354
  * that implied `<html>` instead, so an `html { font-size }` still reaches
1284
1355
  * the body. Taken as the initial style, the rule matched nothing and the
1285
1356
  * whole page came out at the theme's size.
1357
+ *
1358
+ * `html` is the implied `<html>`'s own style where the root box stands in
1359
+ * for the body inside it, and null otherwise: what an `html { … }` or a
1360
+ * `:root { … }` rule gives it that is not inherited has no box to be
1361
+ * drawn on, and its background is the canvas's (`paintDocument`).
1286
1362
  */
1287
- rootStyle(hasBody: boolean, hasHtml = true): ComputedStyle {
1363
+ rootStyle(
1364
+ hasBody: boolean,
1365
+ hasHtml = true,
1366
+ ): { style: ComputedStyle; html: ComputedStyle | null } {
1288
1367
  const style = copyStyle(this.initial);
1289
1368
  style.display = 'block';
1290
- if (hasBody && hasHtml) return style;
1369
+ if (hasBody && hasHtml) return { style, html: null };
1291
1370
  const synthetic = new DomElement('body', {}, []);
1292
1371
  let parent = style;
1372
+ let html: ComputedStyle | null = null;
1293
1373
  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);
1374
+ const element = new DomElement('html', {}, hasBody ? [] : [synthetic]);
1375
+ synthetic.parent = element;
1376
+ parent = this.styleFor(element, style, false);
1377
+ if (hasBody) return { style: asRoot(parent), html: null };
1378
+ html = parent;
1298
1379
  }
1299
1380
  // Only the box the body would have drawn is taken, not its layout role:
1300
1381
  // the root is still the initial containing block.
1301
- return asRoot(this.styleFor(synthetic, parent, false));
1382
+ return { style: asRoot(this.styleFor(synthetic, parent, false)), html };
1302
1383
  }
1303
1384
 
1304
1385
  /** The rules of `index` that match `el`, pushed onto `out` as candidates,
@@ -1412,10 +1493,20 @@ function asRoot(style: ComputedStyle): ComputedStyle {
1412
1493
 
1413
1494
  /** `:root` is the `<html>` element: the one a browser implies around a
1414
1495
  * fragment, whose style the root box takes (`rootStyle`), and never a
1415
- * fragment's top-level elements, which css-select would take for it. */
1496
+ * fragment's top-level elements, which css-select would take for it.
1497
+ *
1498
+ * `:focus` and its two kin match nothing: no element of the document takes
1499
+ * the focus — a control's widget does, beside it. css-select has none of
1500
+ * the three and throws on them, which dropped every rule that named one,
1501
+ * `:not(:focus)` among them: Wikipedia's skip link hides with
1502
+ * `.mw-jump-link:not(:focus)`, and without it stood in the flow at the top
1503
+ * of every page. */
1416
1504
  const PSEUDOS = {
1417
1505
  root: (el: Element) =>
1418
1506
  el.name === 'html' && !(el.parent && isTag(el.parent as Element)),
1507
+ focus: () => false,
1508
+ 'focus-visible': () => false,
1509
+ 'focus-within': () => false,
1419
1510
  // css-select's, but its ranges and tags lower-cased as ASCII has it, and
1420
1511
  // no other script (CSS 2.1 4.1.3): Unicode's took `:lang(\u212Al)`, a
1421
1512
  // Kelvin sign for the K, for `:lang(kl)`
@@ -1449,8 +1540,8 @@ const PSEUDOS = {
1449
1540
 
1450
1541
  /** A length in a unit of the viewport: a number, then the unit, and no
1451
1542
  * 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;
1543
+ const VIEWPORT_UNIT = /\d[sld]?v(?:w|h|i|b|min|max)(?![\w-])/i;
1544
+ const VIEWPORT_UNITS = /\d([sld]?v(?:w|h|i|b|min|max))(?![\w-])/gi;
1454
1545
 
1455
1546
  /**
1456
1547
  * A selector with its `[attr~=""]` made one that matches nothing: an empty
@@ -1,4 +1,5 @@
1
1
  import * as ntk from 'react-x11/ntk';
2
+ import { closingParen, topLevelComma } from './vars.js';
2
3
 
3
4
  // CSS Color 4's functional colours: read here, and written back in the one
4
5
  // form every drawing context reads — `#rrggbb`, or `rgba(r, g, b, a)` with
@@ -753,3 +754,89 @@ const REC2020_TO_XYZ: readonly Triple[] = [
753
754
  [26158966 / 99577255, 472592308 / 697040785, 8267143 / 139408157],
754
755
  [0, 19567812 / 697040785, 295819943 / 278816314],
755
756
  ];
757
+
758
+ // --- colour schemes ---------------------------------------------------------
759
+
760
+ /** Whether a value has a `light-dark()` in it. */
761
+ export const LIGHT_DARK = /(?:^|[^\w-])light-dark\(/i;
762
+ const LIGHT_DARK_AT = /(?:^|[^\w-])(light-dark\()/gi;
763
+
764
+ /**
765
+ * A value with each `light-dark()` in it replaced by the branch `scheme`
766
+ * picks (CSS Color 5, 7): the first where the element's used colour
767
+ * scheme is light, the second where it is dark. Replaced in the text, as a
768
+ * `var()` is, so a branch can be any colour — or another `light-dark()` —
769
+ * in any value a colour stands in. Null where one is not two arguments,
770
+ * which makes the declaration invalid.
771
+ */
772
+ export function lightDark(
773
+ value: string,
774
+ scheme: 'light' | 'dark',
775
+ ): string | null {
776
+ let out = value;
777
+ // a branch can hold another, which the next round replaces
778
+ for (let round = 0; round < 32; round += 1) {
779
+ LIGHT_DARK_AT.lastIndex = 0;
780
+ const m = LIGHT_DARK_AT.exec(out);
781
+ if (!m) return out;
782
+ const open = m.index + m[0].length;
783
+ const start = open - m[1].length;
784
+ const close = closingParen(out, open);
785
+ if (close < 0) return null;
786
+ const inner = out.slice(open, close);
787
+ const comma = topLevelComma(inner);
788
+ if (comma < 0) return null;
789
+ const light = inner.slice(0, comma).trim();
790
+ const dark = inner.slice(comma + 1).trim();
791
+ if (!light || !dark || topLevelComma(dark) >= 0) return null;
792
+ out =
793
+ out.slice(0, start) +
794
+ (scheme === 'dark' ? dark : light) +
795
+ out.slice(close + 1);
796
+ }
797
+ return null;
798
+ }
799
+
800
+ /**
801
+ * The scheme an element with this `color-scheme` is drawn in (CSS Color
802
+ * Adjust 1, 2.2), given the one `preferred` — the palette's, which stands
803
+ * for the reader's preference: that one where the element supports it,
804
+ * else the first it names that is one, else the one `normal` gives, which
805
+ * is the palette's own too. Null for a value that is not a `color-scheme`.
806
+ */
807
+ export function usedColorScheme(
808
+ value: string,
809
+ preferred: 'light' | 'dark' = 'light',
810
+ ): 'light' | 'dark' | null {
811
+ const words = value.trim().toLowerCase().split(/\s+/);
812
+ if (words.length === 1 && words[0] === 'normal') return preferred;
813
+ // `[ light | dark | <custom-ident> ]+ && only?`: `only` once, first or
814
+ // last, and never on its own
815
+ const only = words.indexOf('only');
816
+ if (only >= 0) {
817
+ if (only !== 0 && only !== words.length - 1) return null;
818
+ words.splice(only, 1);
819
+ if (!words.length) return null;
820
+ }
821
+ let first: 'light' | 'dark' | null = null;
822
+ for (const word of words) {
823
+ if (!SCHEME_IDENT.test(word) || NOT_SCHEMES.has(word)) return null;
824
+ if (word === preferred) return preferred;
825
+ if (!first && (word === 'light' || word === 'dark')) first = word;
826
+ }
827
+ return first ?? preferred;
828
+ }
829
+
830
+ const SCHEME_IDENT = /^-?(?:[a-z_]|--)[\w-]*$/;
831
+ /** What a scheme's name cannot be: the keywords the grammar has, and the
832
+ * CSS-wide ones, which reach here only in a list. */
833
+ const NOT_SCHEMES = new Set([
834
+ 'normal',
835
+ 'only',
836
+ 'inherit',
837
+ 'initial',
838
+ 'unset',
839
+ 'revert',
840
+ 'revert-layer',
841
+ 'default',
842
+ ]);
@@ -242,8 +242,11 @@ export function parseStylesheet(
242
242
  } else if (name === 'supports' && at.block !== null) {
243
243
  // Everything in a `@supports` block is markup this renderer either
244
244
  // understands or ignores per-declaration, so entering it is closer
245
- // to right than skipping it.
246
- walk(at.block, media, layer, path);
245
+ // to right than skipping it — but for a condition it can answer
246
+ // false (`supportsCondition`)
247
+ if (supportsCondition(at.prelude) !== false) {
248
+ walk(at.block, media, layer, path);
249
+ }
247
250
  } else if (name === 'layer') {
248
251
  // `@layer a, b;` names layers, and so fixes their order, and
249
252
  // `@layer a { … }` puts rules in one; one with no name is a layer
@@ -342,7 +345,9 @@ export function parseStylesheet(
342
345
  depth + 1,
343
346
  );
344
347
  } else if (name === 'supports') {
345
- styleRule(selectors, item.block, media, layer, path, depth + 1);
348
+ if (supportsCondition(item.prelude) !== false) {
349
+ styleRule(selectors, item.block, media, layer, path, depth + 1);
350
+ }
346
351
  } else if (name === 'layer' && LAYER_NAME.test(item.prelude.trim())) {
347
352
  const [ranks, full] = enter(item.prelude.trim(), layer, path);
348
353
  styleRule(selectors, item.block, media, ranks, full, depth + 1);
@@ -1775,7 +1780,7 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1775
1780
  sawWidth = true;
1776
1781
  continue;
1777
1782
  }
1778
- const feature = /^\(\s*([a-z-]+)\s*:\s*([^)]+?)\s*\)$/i.exec(term);
1783
+ const feature = mediaFeature(term);
1779
1784
  if (feature) {
1780
1785
  const key = feature[1].toLowerCase();
1781
1786
  const len = parseLength(feature[2], ZERO_UNITS);
@@ -1845,6 +1850,104 @@ export function parseMediaQuery(prelude: string): MediaCondition[] {
1845
1850
  return out.length ? out : [{ staticPass: true }];
1846
1851
  }
1847
1852
 
1853
+ /**
1854
+ * The properties whose support a `@supports` condition is answered for:
1855
+ * the mask's, which pages test before they draw an icon as a masked colour
1856
+ * and keep a background image under `not` for an engine without them.
1857
+ * Drawn here, and the fallback drawn too, an icon was its image in black
1858
+ * under the mask, over the colour it was meant to be.
1859
+ */
1860
+ const ANSWERED = new Set(
1861
+ [
1862
+ 'mask',
1863
+ 'mask-image',
1864
+ 'mask-repeat',
1865
+ 'mask-position',
1866
+ 'mask-size',
1867
+ 'mask-origin',
1868
+ 'mask-clip',
1869
+ ].flatMap((name) => [name, `-webkit-${name}`]),
1870
+ );
1871
+
1872
+ /**
1873
+ * Whether a `@supports` condition holds (CSS Conditional 3, 6): true or
1874
+ * false where it turns on declarations of the properties this answers for
1875
+ * (`ANSWERED`), combined by `not`, `and` and `or`; null where it turns on
1876
+ * anything else, or is none, and the block is entered as every one was.
1877
+ * Answering more is not safe without a list of what this draws: Tailwind 4
1878
+ * keeps its variables' starting values under a test for engines without
1879
+ * `@property`, which this is, and false there would drop every shadow and
1880
+ * gradient it writes.
1881
+ */
1882
+ export function supportsCondition(prelude: string): boolean | null {
1883
+ const text = prelude.trim();
1884
+ if (/^not\b/i.test(text)) {
1885
+ const inner = supportsCondition(text.slice(3));
1886
+ return inner === null ? null : !inner;
1887
+ }
1888
+ // the terms at the top level, and the one operator between them
1889
+ const terms: string[] = [];
1890
+ let op = '';
1891
+ let depth = 0;
1892
+ let start = 0;
1893
+ for (let i = 0; i < text.length; i += 1) {
1894
+ const c = text[i];
1895
+ if (c === '(') depth += 1;
1896
+ else if (c === ')') depth -= 1;
1897
+ else if (depth === 0 && /\s/.test(c)) {
1898
+ const word = /^\s+(and|or)\s+/i.exec(text.slice(i));
1899
+ if (!word) continue;
1900
+ const next = word[1].toLowerCase();
1901
+ // `and` and `or` mixed without parentheses is no condition
1902
+ if (op && op !== next) return null;
1903
+ op = next;
1904
+ terms.push(text.slice(start, i));
1905
+ i += word[0].length - 1;
1906
+ start = i + 1;
1907
+ }
1908
+ if (depth < 0) return null;
1909
+ }
1910
+ if (terms.length) {
1911
+ terms.push(text.slice(start));
1912
+ const values = terms.map(supportsCondition);
1913
+ if (op === 'and') {
1914
+ if (values.includes(false)) return false;
1915
+ return values.includes(null) ? null : true;
1916
+ }
1917
+ if (values.includes(true)) return true;
1918
+ return values.includes(null) ? null : false;
1919
+ }
1920
+ // one term: a condition in parentheses, or a declaration in them
1921
+ if (!(text.startsWith('(') && text.endsWith(')'))) return null;
1922
+ const inner = text.slice(1, -1).trim();
1923
+ if (inner.startsWith('(') || /^not\b/i.test(inner)) {
1924
+ return supportsCondition(inner);
1925
+ }
1926
+ const declaration = /^([a-z-]+)\s*:\s*(\S[\s\S]*)$/i.exec(inner);
1927
+ if (!declaration || !ANSWERED.has(declaration[1].toLowerCase())) return null;
1928
+ return true;
1929
+ }
1930
+
1931
+ /**
1932
+ * A `(name: value)` media feature: the whole term, its name, and its value
1933
+ * read to the parenthesis that closes the feature, or null where the term is
1934
+ * no such thing. A value may hold parentheses of its own: MediaWiki's
1935
+ * breakpoints are `(max-width: calc(640px - 1px))`, and a value read to the
1936
+ * first `)` found no feature there — the term was passed over as though it
1937
+ * said nothing, and every narrow-screen rule held at every width.
1938
+ */
1939
+ function mediaFeature(term: string): [string, string, string] | null {
1940
+ const m = /^\(\s*([a-z-]+)\s*:([\s\S]*)\)$/i.exec(term);
1941
+ if (!m) return null;
1942
+ let depth = 0;
1943
+ for (const c of m[2]) {
1944
+ if (c === '(') depth += 1;
1945
+ else if (c === ')' && --depth < 0) return null;
1946
+ }
1947
+ const value = m[2].trim();
1948
+ return depth === 0 && value ? [term, m[1], value] : null;
1949
+ }
1950
+
1848
1951
  /** A `@media` width is compared with the viewport in CSS pixels — the
1849
1952
  * cascade divides the device width by the scale before asking — so the
1850
1953
  * thresholds parse at scale 1 whatever panel the document lands on. */