@react-x11/components 0.14.1 → 0.16.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 (96) hide show
  1. package/dist/code-editor/node.d.ts +13 -0
  2. package/dist/code-editor/node.d.ts.map +1 -1
  3. package/dist/code-editor/node.js +23 -5
  4. package/dist/code-editor/node.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +171 -26
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +616 -216
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +25 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +307 -19
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/content.d.ts +11 -6
  14. package/dist/html/css/content.d.ts.map +1 -1
  15. package/dist/html/css/content.js +171 -12
  16. package/dist/html/css/content.js.map +1 -1
  17. package/dist/html/css/parse.d.ts.map +1 -1
  18. package/dist/html/css/parse.js +62 -9
  19. package/dist/html/css/parse.js.map +1 -1
  20. package/dist/html/css/shapes.d.ts +18 -0
  21. package/dist/html/css/shapes.d.ts.map +1 -1
  22. package/dist/html/css/shapes.js +21 -1
  23. package/dist/html/css/shapes.js.map +1 -1
  24. package/dist/html/css/style.d.ts +42 -5
  25. package/dist/html/css/style.d.ts.map +1 -1
  26. package/dist/html/css/style.js +74 -12
  27. package/dist/html/css/style.js.map +1 -1
  28. package/dist/html/css/ua.js +15 -0
  29. package/dist/html/css/ua.js.map +1 -1
  30. package/dist/html/css/values.d.ts +5 -0
  31. package/dist/html/css/values.d.ts.map +1 -1
  32. package/dist/html/css/values.js +18 -9
  33. package/dist/html/css/values.js.map +1 -1
  34. package/dist/html/index.d.ts.map +1 -1
  35. package/dist/html/index.js +7 -0
  36. package/dist/html/index.js.map +1 -1
  37. package/dist/html/layout/block.d.ts +29 -9
  38. package/dist/html/layout/block.d.ts.map +1 -1
  39. package/dist/html/layout/block.js +141 -52
  40. package/dist/html/layout/block.js.map +1 -1
  41. package/dist/html/layout/boxes.d.ts +16 -2
  42. package/dist/html/layout/boxes.d.ts.map +1 -1
  43. package/dist/html/layout/boxes.js +144 -23
  44. package/dist/html/layout/boxes.js.map +1 -1
  45. package/dist/html/layout/css-grid.d.ts.map +1 -1
  46. package/dist/html/layout/css-grid.js +11 -9
  47. package/dist/html/layout/css-grid.js.map +1 -1
  48. package/dist/html/layout/flex.d.ts.map +1 -1
  49. package/dist/html/layout/flex.js +1631 -221
  50. package/dist/html/layout/flex.js.map +1 -1
  51. package/dist/html/layout/inline.d.ts +15 -3
  52. package/dist/html/layout/inline.d.ts.map +1 -1
  53. package/dist/html/layout/inline.js +23 -8
  54. package/dist/html/layout/inline.js.map +1 -1
  55. package/dist/html/layout/table.d.ts +27 -1
  56. package/dist/html/layout/table.d.ts.map +1 -1
  57. package/dist/html/layout/table.js +886 -324
  58. package/dist/html/layout/table.js.map +1 -1
  59. package/dist/html/node.d.ts +35 -4
  60. package/dist/html/node.d.ts.map +1 -1
  61. package/dist/html/node.js +118 -26
  62. package/dist/html/node.js.map +1 -1
  63. package/dist/html/paint.d.ts.map +1 -1
  64. package/dist/html/paint.js +558 -178
  65. package/dist/html/paint.js.map +1 -1
  66. package/dist/html/resources.js +14 -1
  67. package/dist/html/resources.js.map +1 -1
  68. package/dist/html/svg.d.ts +23 -7
  69. package/dist/html/svg.d.ts.map +1 -1
  70. package/dist/html/svg.js +264 -59
  71. package/dist/html/svg.js.map +1 -1
  72. package/dist/html/widgets.d.ts.map +1 -1
  73. package/dist/html/widgets.js +57 -4
  74. package/dist/html/widgets.js.map +1 -1
  75. package/package.json +3 -3
  76. package/src/code-editor/node.ts +24 -5
  77. package/src/html/css/cascade.ts +697 -221
  78. package/src/html/css/color.ts +345 -18
  79. package/src/html/css/content.ts +173 -15
  80. package/src/html/css/parse.ts +60 -8
  81. package/src/html/css/shapes.ts +35 -1
  82. package/src/html/css/style.ts +122 -17
  83. package/src/html/css/ua.ts +15 -0
  84. package/src/html/css/values.ts +23 -9
  85. package/src/html/index.ts +7 -0
  86. package/src/html/layout/block.ts +149 -58
  87. package/src/html/layout/boxes.ts +171 -27
  88. package/src/html/layout/css-grid.ts +12 -9
  89. package/src/html/layout/flex.ts +1843 -212
  90. package/src/html/layout/inline.ts +27 -7
  91. package/src/html/layout/table.ts +961 -332
  92. package/src/html/node.ts +151 -27
  93. package/src/html/paint.ts +692 -234
  94. package/src/html/resources.ts +13 -1
  95. package/src/html/svg.ts +299 -56
  96. package/src/html/widgets.ts +61 -4
@@ -1,4 +1,5 @@
1
1
  import * as ntk from 'react-x11/ntk';
2
+ import { parseMathNumber } from './calc.js';
2
3
  import { closingParen, topLevelComma } from './vars.js';
3
4
 
4
5
  // CSS Color 4's functional colours: read here, and written back in the one
@@ -151,9 +152,7 @@ function readRgbaFunction(value: string): Rgba | null {
151
152
  // `rgb(0, 128, 0` as a sheet's last words is green
152
153
  if (body.endsWith(')')) body = body.slice(0, -1);
153
154
  if (name === 'color-mix') return mix(body);
154
- // `calc()`, `var()`, a relative colour's channel keywords: not read here,
155
- // and a declaration this cannot read is dropped
156
- if (body.includes('(') || body.includes(')')) return null;
155
+ if (RELATIVE.test(body)) return relative(name, body);
157
156
 
158
157
  let channels: Component[];
159
158
  let alpha: Component | null = null;
@@ -161,13 +160,13 @@ function readRgbaFunction(value: string): Rgba | null {
161
160
  if (body.includes(',')) {
162
161
  // the legacy comma form, which only these four have
163
162
  if (!LEGACY.has(name)) return null;
164
- const parts = body.split(',');
163
+ const parts = topLevelParts(body, ',');
165
164
  if (parts.length !== 3 && parts.length !== 4) return null;
166
165
  const read: Component[] = [];
167
166
  for (const part of parts) {
168
167
  const token = part.trim();
169
- if (!token || /\s/.test(token)) return null;
170
- const c = component(token);
168
+ if (!token || topLevelParts(token, ' ').length > 1) return null;
169
+ const c = channel(token);
171
170
  if (!c || c.unit === 'none') return null;
172
171
  read.push(c);
173
172
  }
@@ -182,22 +181,22 @@ function readRgbaFunction(value: string): Rgba | null {
182
181
  return null;
183
182
  }
184
183
  } else {
185
- const slash = body.split('/');
184
+ const slash = topLevelParts(body, '/');
186
185
  if (slash.length > 2) return null;
187
- const tokens = slash[0].trim().split(/\s+/);
186
+ const tokens = words(slash[0]);
188
187
  if (name === 'color') space = (tokens.shift() ?? '').toLowerCase();
189
188
  if (tokens.length !== 3) return null;
190
189
  const read: Component[] = [];
191
190
  for (const token of tokens) {
192
- const c = component(token);
191
+ const c = channel(token);
193
192
  if (!c) return null;
194
193
  read.push(c);
195
194
  }
196
195
  channels = read;
197
196
  if (slash.length === 2) {
198
- const token = slash[1].trim();
199
- if (!token || /\s/.test(token)) return null;
200
- alpha = component(token);
197
+ const token = words(slash[1]);
198
+ if (token.length !== 1) return null;
199
+ alpha = channel(token[0]);
201
200
  if (!alpha) return null;
202
201
  }
203
202
  }
@@ -215,6 +214,158 @@ function readRgbaFunction(value: string): Rgba | null {
215
214
 
216
215
  const LEGACY = new Set(['rgb', 'rgba', 'hsl', 'hsla']);
217
216
 
217
+ /** A space-separated list's items, a function's arguments kept whole. */
218
+ function words(text: string): string[] {
219
+ return topLevelParts(text.trim(), ' ').filter(Boolean);
220
+ }
221
+
222
+ /** A channel: a number, a percentage, an angle or `none`, or a math
223
+ * function standing for a number — `rgb(calc(255 / 2) 0 0)`. */
224
+ function channel(token: string): Component | null {
225
+ if (!token.includes('(')) return component(token);
226
+ const n = parseMathNumber(token);
227
+ return n === null ? null : { value: n, unit: '' };
228
+ }
229
+
230
+ // --- relative colours (CSS Color 5, 4) -------------------------------------
231
+
232
+ const RELATIVE = /^\s*from\s/i;
233
+
234
+ /**
235
+ * A relative colour, `rgb(from <colour> r g b / alpha)` and its kind in
236
+ * each function: the origin colour taken into the function's space, and
237
+ * each channel a number, a keyword that is one of the origin's channels,
238
+ * or a calculation with them in it. The keywords are numbers in the
239
+ * function's own range (`r` from 0 to 255, `s` and `l` from 0 to 100, an
240
+ * angle's degrees), and an alpha left out is the origin's, not 1. The
241
+ * origin is any colour but `currentColor`, which `parseColor` keeps the
242
+ * colour to be resolved where it is used.
243
+ */
244
+ function relative(name: string, body: string): Rgba | null {
245
+ const slash = topLevelParts(body, '/');
246
+ if (slash.length > 2) return null;
247
+ const tokens = words(slash[0]);
248
+ if (tokens.length < 2 || tokens[0].toLowerCase() !== 'from') return null;
249
+ const origin = readRgba(tokens[1]);
250
+ if (!origin) return null;
251
+ const rest = tokens.slice(2);
252
+ const space = name === 'color' ? (rest.shift() ?? '').toLowerCase() : '';
253
+ if (rest.length !== 3) return null;
254
+ const coords = originIn(name, space, origin.rgb);
255
+ const keys = CHANNEL_NAMES[name] ?? (XYZ_SPACES.has(space) ? 'xyz' : 'rgb');
256
+ if (!coords) return null;
257
+ const known = new Map<string, number>([['alpha', origin.a]]);
258
+ for (let i = 0; i < 3; i += 1) {
259
+ // a hue a grey has none of is `none`, which is 0
260
+ known.set(keys[i], Number.isNaN(coords[i]) ? 0 : coords[i]);
261
+ }
262
+ const channels: Component[] = [];
263
+ for (const token of rest) {
264
+ const c = relativeChannel(token, known);
265
+ if (!c) return null;
266
+ channels.push(c);
267
+ }
268
+ let a = origin.a;
269
+ if (slash.length === 2) {
270
+ const token = words(slash[1]);
271
+ if (token.length !== 1) return null;
272
+ const alpha = relativeChannel(token[0], known);
273
+ if (!alpha || alpha.unit === 'deg' || alpha.unit === 'grad') return null;
274
+ if (alpha.unit === 'rad' || alpha.unit === 'turn') return null;
275
+ a = clamp01(alpha.unit === '%' ? alpha.value / 100 : alpha.value);
276
+ }
277
+ const rgb = toSrgb(name, space, channels);
278
+ return rgb ? { rgb, a } : null;
279
+ }
280
+
281
+ /** Each function's channel keywords, in order. */
282
+ const CHANNEL_NAMES: Record<string, string> = {
283
+ rgb: 'rgb',
284
+ rgba: 'rgb',
285
+ hsl: 'hsl',
286
+ hsla: 'hsl',
287
+ hwb: 'hwb',
288
+ lab: 'lab',
289
+ lch: 'lch',
290
+ oklab: 'lab',
291
+ oklch: 'lch',
292
+ };
293
+
294
+ const XYZ_SPACES = new Set(['xyz', 'xyz-d50', 'xyz-d65']);
295
+
296
+ /** A channel of a relative colour: a keyword is the origin's channel, and
297
+ * a calculation is worked out with the keywords' values put in. */
298
+ function relativeChannel(
299
+ token: string,
300
+ known: Map<string, number>,
301
+ ): Component | null {
302
+ const value = known.get(token.toLowerCase());
303
+ if (value !== undefined) return { value, unit: '' };
304
+ if (!token.includes('(')) return component(token);
305
+ let unknown = false;
306
+ const text = token.replace(KEYWORD, (word) => {
307
+ const v = known.get(word.toLowerCase());
308
+ // a function's name, `pi`, `e`, `infinity`: the calculation's own
309
+ if (v === undefined) {
310
+ if (!/^(?:pi|e|-?infinity|nan)$/i.test(word)) unknown = true;
311
+ return word;
312
+ }
313
+ return `(${v})`;
314
+ });
315
+ if (unknown) return null;
316
+ return channel(text);
317
+ }
318
+
319
+ /** A word in a calculation that is not a function's name. */
320
+ const KEYWORD = /(?<![\w.-])-?[a-z][\w-]*(?![\w(-])/gi;
321
+
322
+ /** The origin colour's channels in a relative colour's function, in the
323
+ * ranges its keywords take. */
324
+ function originIn(name: string, space: string, rgb: Triple): Triple | null {
325
+ switch (name) {
326
+ case 'rgb':
327
+ case 'rgba':
328
+ return scaled(rgb, 255);
329
+ case 'hsl':
330
+ case 'hsla':
331
+ return srgbToHsl(rgb);
332
+ case 'hwb':
333
+ case 'lab':
334
+ case 'lch':
335
+ case 'oklab':
336
+ case 'oklch':
337
+ return toSpace(name, rgb);
338
+ case 'color':
339
+ return toSpace(space, rgb) ?? toPredefined(space, rgb);
340
+ default:
341
+ return null;
342
+ }
343
+ }
344
+
345
+ /** Gamma-encoded sRGB in one of `color()`'s RGB spaces, as `predefined`
346
+ * reads them back. */
347
+ function toPredefined(space: string, rgb: Triple): Triple | null {
348
+ const xyz = multiply(LINEAR_SRGB_TO_XYZ, linearize(rgb));
349
+ switch (space) {
350
+ case 'display-p3':
351
+ return multiply(XYZ_TO_P3, xyz).map(srgbEncode) as Triple;
352
+ case 'display-p3-linear':
353
+ return multiply(XYZ_TO_P3, xyz);
354
+ case 'a98-rgb':
355
+ return multiply(XYZ_TO_A98, xyz).map(
356
+ (x) => Math.sign(x) * Math.abs(x) ** (256 / 563),
357
+ ) as Triple;
358
+ case 'prophoto-rgb':
359
+ return multiply(XYZ_D50_TO_PROPHOTO, multiply(D65_TO_D50, xyz)).map(
360
+ prophotoEncode,
361
+ ) as Triple;
362
+ case 'rec2020':
363
+ return multiply(XYZ_TO_REC2020, xyz).map(rec2020Encode) as Triple;
364
+ default:
365
+ return null;
366
+ }
367
+ }
368
+
218
369
  /** A channel as a number, or null where its unit is not one it takes:
219
370
  * `pct` is what 100% means for it. */
220
371
  function amount(c: Component, pct: number): number | null {
@@ -674,12 +825,14 @@ function srgbLinear(x: number): number {
674
825
  }
675
826
 
676
827
  function gamma(rgb: Triple): Triple {
677
- return rgb.map((x) => {
678
- const abs = Math.abs(x);
679
- return abs > 0.0031308
680
- ? Math.sign(x) * (1.055 * abs ** (1 / 2.4) - 0.055)
681
- : 12.92 * x;
682
- }) as Triple;
828
+ return rgb.map(srgbEncode) as Triple;
829
+ }
830
+
831
+ function srgbEncode(x: number): number {
832
+ const abs = Math.abs(x);
833
+ return abs > 0.0031308
834
+ ? Math.sign(x) * (1.055 * abs ** (1 / 2.4) - 0.055)
835
+ : 12.92 * x;
683
836
  }
684
837
 
685
838
  function prophotoLinear(x: number): number {
@@ -687,6 +840,20 @@ function prophotoLinear(x: number): number {
687
840
  return abs <= 16 / 512 ? x / 16 : Math.sign(x) * abs ** 1.8;
688
841
  }
689
842
 
843
+ function prophotoEncode(x: number): number {
844
+ const abs = Math.abs(x);
845
+ return abs < 1 / 512 ? x * 16 : Math.sign(x) * abs ** (1 / 1.8);
846
+ }
847
+
848
+ function rec2020Encode(x: number): number {
849
+ const alpha = 1.09929682680944;
850
+ const beta = 0.018053968510807;
851
+ const abs = Math.abs(x);
852
+ return abs < beta
853
+ ? x * 4.5
854
+ : Math.sign(x) * (alpha * abs ** 0.45 - (alpha - 1));
855
+ }
856
+
690
857
  function rec2020Linear(x: number): number {
691
858
  const alpha = 1.09929682680944;
692
859
  const beta = 0.018053968510807;
@@ -755,6 +922,33 @@ const REC2020_TO_XYZ: readonly Triple[] = [
755
922
  [0, 19567812 / 697040785, 295819943 / 278816314],
756
923
  ];
757
924
 
925
+ // The way back from XYZ to each RGB space, which a relative colour's origin
926
+ // is taken into.
927
+
928
+ const XYZ_TO_P3: readonly Triple[] = [
929
+ [446124 / 178915, -333277 / 357830, -72051 / 178915],
930
+ [-14852 / 17905, 63121 / 35810, 423 / 17905],
931
+ [11844 / 330415, -50337 / 660830, 316169 / 330415],
932
+ ];
933
+
934
+ const XYZ_TO_A98: readonly Triple[] = [
935
+ [1829569 / 896150, -506331 / 896150, -308931 / 896150],
936
+ [-851781 / 878810, 1648619 / 878810, 36519 / 878810],
937
+ [16779 / 1248040, -147721 / 1248040, 1266979 / 1248040],
938
+ ];
939
+
940
+ const XYZ_D50_TO_PROPHOTO: readonly Triple[] = [
941
+ [1.3457868816471583, -0.25557208737979464, -0.05110186497554526],
942
+ [-0.5446307051249019, 1.5082477428451468, 0.02052744743642139],
943
+ [0, 0, 1.2119675456389452],
944
+ ];
945
+
946
+ const XYZ_TO_REC2020: readonly Triple[] = [
947
+ [30757411 / 17917100, -6372589 / 17917100, -4539589 / 17917100],
948
+ [-19765991 / 29648200, 47925759 / 29648200, 467509 / 29648200],
949
+ [792561 / 44930125, -1921689 / 44930125, 42328811 / 44930125],
950
+ ];
951
+
758
952
  // --- colour schemes ---------------------------------------------------------
759
953
 
760
954
  /** Whether a value has a `light-dark()` in it. */
@@ -856,6 +1050,139 @@ export const SCHEME_COLORS = {
856
1050
  dark: { canvas: '#121212', text: '#ffffff', link: '#9e9eff' },
857
1051
  } as const;
858
1052
 
1053
+ // --- system colours (CSS Color 4, 6.2) -------------------------------------
1054
+
1055
+ /**
1056
+ * The system colours, in light and in dark, as Chrome has them where the
1057
+ * platform says nothing (Blink's `LayoutTheme::DefaultSystemColor`): the
1058
+ * colours of a browser's own page and controls, which a page names to
1059
+ * look like one and which follow its colour scheme.
1060
+ */
1061
+ const SYSTEM_COLORS: Record<string, readonly [string, string]> = {
1062
+ accentcolor: ['#0075ff', '#0075ff'],
1063
+ accentcolortext: ['#ffffff', '#ffffff'],
1064
+ activetext: ['#ff0000', '#ff0000'],
1065
+ buttonborder: ['#767676', '#6b6b6b'],
1066
+ buttonface: ['#efefef', '#6b6b6b'],
1067
+ buttontext: ['#000000', '#ffffff'],
1068
+ canvas: ['#ffffff', '#121212'],
1069
+ canvastext: ['#000000', '#ffffff'],
1070
+ field: ['#ffffff', '#3b3b3b'],
1071
+ fieldtext: ['#000000', '#ffffff'],
1072
+ graytext: ['#808080', '#808080'],
1073
+ highlight: ['#b3d7ff', '#99c8ff'],
1074
+ highlighttext: ['#000000', '#000000'],
1075
+ linktext: ['#0000ee', '#9e9eff'],
1076
+ mark: ['#ffff00', '#ffff00'],
1077
+ marktext: ['#000000', '#000000'],
1078
+ selecteditem: ['#b3d7ff', '#99c8ff'],
1079
+ selecteditemtext: ['#000000', '#3b3b3b'],
1080
+ visitedtext: ['#551a8b', '#d0adf0'],
1081
+ };
1082
+
1083
+ /** The deprecated system colours, each the same as one of the rest
1084
+ * (CSS Color 4, 6.3). */
1085
+ const DEPRECATED_COLORS: Record<string, string> = {
1086
+ activeborder: 'buttonborder',
1087
+ inactiveborder: 'buttonborder',
1088
+ threeddarkshadow: 'buttonborder',
1089
+ threedhighlight: 'buttonborder',
1090
+ threedlightshadow: 'buttonborder',
1091
+ threedshadow: 'buttonborder',
1092
+ windowframe: 'buttonborder',
1093
+ buttonhighlight: 'buttonface',
1094
+ buttonshadow: 'buttonface',
1095
+ threedface: 'buttonface',
1096
+ activecaption: 'canvas',
1097
+ appworkspace: 'canvas',
1098
+ background: 'canvas',
1099
+ inactivecaption: 'canvas',
1100
+ infobackground: 'canvas',
1101
+ menu: 'canvas',
1102
+ scrollbar: 'canvas',
1103
+ window: 'canvas',
1104
+ captiontext: 'canvastext',
1105
+ infotext: 'canvastext',
1106
+ menutext: 'canvastext',
1107
+ windowtext: 'canvastext',
1108
+ inactivecaptiontext: 'graytext',
1109
+ };
1110
+
1111
+ /** The palette's colours a system colour is, in the palette's scheme. */
1112
+ export interface PaletteColors {
1113
+ color: string;
1114
+ background: string;
1115
+ linkColor: string;
1116
+ surface: string;
1117
+ borderColor: string;
1118
+ mutedColor: string;
1119
+ colorScheme: 'light' | 'dark';
1120
+ }
1121
+
1122
+ /** Which of the palette's colours each system colour is: the window's
1123
+ * ground and ink, its links and accent, its controls'. */
1124
+ const FROM_PALETTE: Record<string, keyof PaletteColors> = {
1125
+ accentcolor: 'linkColor',
1126
+ buttonborder: 'borderColor',
1127
+ buttonface: 'surface',
1128
+ buttontext: 'color',
1129
+ canvas: 'background',
1130
+ canvastext: 'color',
1131
+ field: 'background',
1132
+ fieldtext: 'color',
1133
+ graytext: 'mutedColor',
1134
+ linktext: 'linkColor',
1135
+ };
1136
+
1137
+ /**
1138
+ * Each system colour's name, lower-cased, and the `light-dark()` it is.
1139
+ * The palette is this renderer's platform, so in the palette's scheme the
1140
+ * colours it has are its own: `Canvas` is the window's ground and
1141
+ * `CanvasText` its text, as an unstyled document is drawn. In the other
1142
+ * scheme, and in an SVG image, which has no palette, they are Chrome's.
1143
+ */
1144
+ export function systemColorTable(
1145
+ palette: PaletteColors | null,
1146
+ ): Map<string, string> {
1147
+ const table = new Map<string, string>();
1148
+ const own = palette?.colorScheme === 'dark' ? 1 : 0;
1149
+ const pairOf = (name: string): string => {
1150
+ const pair = [...SYSTEM_COLORS[name]];
1151
+ const token = palette && FROM_PALETTE[name];
1152
+ if (token) pair[own] = palette[token] as string;
1153
+ return `light-dark(${pair[0]}, ${pair[1]})`;
1154
+ };
1155
+ for (const name of Object.keys(SYSTEM_COLORS)) table.set(name, pairOf(name));
1156
+ for (const [name, same] of Object.entries(DEPRECATED_COLORS)) {
1157
+ table.set(name, table.get(same)!);
1158
+ }
1159
+ return table;
1160
+ }
1161
+
1162
+ /** Chrome's own, for a cascade with no palette. */
1163
+ let CHROME_TABLE: Map<string, string> | null = null;
1164
+
1165
+ /** Whether a value names a system colour: a word of its own, not a part
1166
+ * of a name, a path or a string. Written out, as a pattern built from the
1167
+ * tables would be a call at load. */
1168
+ export const SYSTEM_COLOR =
1169
+ /(?<![\w.#/'"-])(?:inactivecaptiontext|threedlightshadow|selecteditemtext|threeddarkshadow|accentcolortext|threedhighlight|buttonhighlight|inactivecaption|inactiveborder|infobackground|highlighttext|activecaption|buttonborder|selecteditem|activeborder|threedshadow|buttonshadow|appworkspace|accentcolor|visitedtext|windowframe|captiontext|activetext|buttonface|buttontext|canvastext|threedface|background|windowtext|fieldtext|highlight|scrollbar|graytext|linktext|marktext|infotext|menutext|canvas|window|field|mark|menu)(?![\w.(/'"-])/i;
1170
+ const SYSTEM_COLOR_AT =
1171
+ /(?<![\w.#/'"-])(?:inactivecaptiontext|threedlightshadow|selecteditemtext|threeddarkshadow|accentcolortext|threedhighlight|buttonhighlight|inactivecaption|inactiveborder|infobackground|highlighttext|activecaption|buttonborder|selecteditem|activeborder|threedshadow|buttonshadow|appworkspace|accentcolor|visitedtext|windowframe|captiontext|activetext|buttonface|buttontext|canvastext|threedface|background|windowtext|fieldtext|highlight|scrollbar|graytext|linktext|marktext|infotext|menutext|canvas|window|field|mark|menu)(?![\w.(/'"-])/gi;
1172
+
1173
+ /** A value with each system colour in it replaced by the `light-dark()` it
1174
+ * is, which the element's scheme then picks a branch of. */
1175
+ export function systemColors(
1176
+ value: string,
1177
+ table: Map<string, string> | undefined,
1178
+ ): string {
1179
+ const colors = table ?? (CHROME_TABLE ??= systemColorTable(null));
1180
+ return value.replace(
1181
+ SYSTEM_COLOR_AT,
1182
+ (word) => colors.get(word.toLowerCase()) ?? word,
1183
+ );
1184
+ }
1185
+
859
1186
  const SCHEME_IDENT = /^-?(?:[a-z_]|--)[\w-]*$/;
860
1187
  /** What a scheme's name cannot be: the keywords the grammar has, and the
861
1188
  * CSS-wide ones, which reach here only in a list. */
@@ -35,10 +35,167 @@ export interface CounterChange {
35
35
  counted?: true;
36
36
  }
37
37
 
38
- /** English's quotation marks, outer pair first: `quotes`' initial value,
39
- * which CSS 2.1 leaves to the user agent. */
38
+ /** English's quotation marks, outer pair first: what `quotes: auto` is for
39
+ * a language with none of its own, and for none. */
40
40
  export const DEFAULT_QUOTES: readonly string[] = ['“', '”', '‘', '’'];
41
41
 
42
+ /**
43
+ * Each language's quotation marks, outer pair then inner, where they are
44
+ * not its parent's — `fr-ch`'s where they are not `fr`'s, `fr`'s where
45
+ * they are not English's — from CLDR's delimiters, which Chrome reads
46
+ * through ICU. What `quotes: auto` makes of the element's language (CSS
47
+ * Content 3, 2.1): `<q>` in a French page is «un ‹deux› trois».
48
+ */
49
+ const QUOTES_BY_LANGUAGE: Record<string, string> = {
50
+ agq: '„”‚’',
51
+ am: '«»‹›',
52
+ ar: '”“’‘',
53
+ ast: '«»“”',
54
+ 'az-arab': '«»‹›',
55
+ 'az-cyrl': '«»‹›',
56
+ bas: '«»„“',
57
+ be: '«»„“',
58
+ bg: '„“„“',
59
+ blo: '«»“”',
60
+ bm: '«»“”',
61
+ 'bm-nkoo': '“”‘’',
62
+ br: '«»“”',
63
+ bs: '„”‘’',
64
+ 'bs-cyrl': '„“‚‘',
65
+ bua: '«»„“',
66
+ ca: '«»“”',
67
+ cs: '„“‚‘',
68
+ cv: '«»“”',
69
+ de: '„“‚‘',
70
+ dsb: '„“‚‘',
71
+ dua: '«»‘’',
72
+ dyo: '«»“”',
73
+ el: '«»“”',
74
+ 'el-polyton': '«»‘’',
75
+ eo: '“”«»',
76
+ 'es-us': '«»“”',
77
+ et: '„“‚‘',
78
+ eu: '«»“”',
79
+ ewo: '«»“”',
80
+ fa: '«»‹›',
81
+ ff: '„”‚’',
82
+ 'ff-adlm': '“”‘’',
83
+ fi: '””’’',
84
+ fr: '«»«»',
85
+ 'fr-ca': '«»”“',
86
+ 'fr-ch': '«»‹›',
87
+ fur: '‘’“”',
88
+ gsw: '«»‹›',
89
+ he: '””’’',
90
+ hr: '„“‚‘',
91
+ hsb: '„“‚‘',
92
+ ht: '«»«»',
93
+ hu: '„”»«',
94
+ hy: '«»«»',
95
+ ia: '‘’“”',
96
+ ie: '«»“”',
97
+ is: '„“‚‘',
98
+ it: '«»“”',
99
+ ja: '「」『』',
100
+ jgo: '«»‹›',
101
+ ka: '„“«»',
102
+ kab: '«»“”',
103
+ kk: '«»“”',
104
+ 'kk-arab': '»«›‹',
105
+ kkj: '«»‹›',
106
+ ksf: '«»‘’',
107
+ ksh: '„“‚‘',
108
+ ky: '«»„“',
109
+ lag: '””’’',
110
+ lb: '„“‚‘',
111
+ lij: '«»“”',
112
+ lld: '”“’‘',
113
+ lt: '„“„“',
114
+ luy: '„“‚‘',
115
+ mg: '«»“”',
116
+ mk: '„“‚‘',
117
+ 'ms-arab': '”“’‘',
118
+ mua: '«»“”',
119
+ mzn: '«»‹›',
120
+ nb: '«»‘’',
121
+ nds: '„“‚‘',
122
+ nl: '‘’‘’',
123
+ nmg: '„”«»',
124
+ nn: '«»‘’',
125
+ nnh: '«»“”',
126
+ no: '«»‘’',
127
+ oc: '«»«»',
128
+ os: '«»„“',
129
+ pl: '„”«»',
130
+ pms: '«»“”',
131
+ prg: '„“„“',
132
+ 'pt-ao': '«»“”',
133
+ 'pt-ch': '«»“”',
134
+ 'pt-cv': '«»“”',
135
+ 'pt-gq': '«»“”',
136
+ 'pt-gw': '«»“”',
137
+ 'pt-lu': '«»“”',
138
+ 'pt-mo': '«»“”',
139
+ 'pt-mz': '«»“”',
140
+ 'pt-pt': '«»“”',
141
+ 'pt-st': '«»“”',
142
+ 'pt-tl': '«»“”',
143
+ rm: '«»‹›',
144
+ rn: '””’’',
145
+ ro: '„”«»',
146
+ ru: '«»„“',
147
+ rw: '«»‘’',
148
+ sah: '«»„“',
149
+ sc: '«»“”',
150
+ sdh: '«»‹›',
151
+ se: '””’’',
152
+ sg: '«»“”',
153
+ sgs: '„“„“',
154
+ shi: '«»„”',
155
+ sk: '„“‚‘',
156
+ sl: '„“‚‘',
157
+ sn: '””’’',
158
+ sr: '„”’’',
159
+ st: '“’“”',
160
+ sv: '””’’',
161
+ syr: '”“’‘',
162
+ szl: '„”»«',
163
+ ti: '«»“”',
164
+ 'ti-er': '‘’“”',
165
+ tk: '“”“”',
166
+ tn: '‘’“”',
167
+ tyv: '«»„“',
168
+ ug: '»«›‹',
169
+ uk: '«»„“',
170
+ ur: '”“’‘',
171
+ uz: '“”’‘',
172
+ 'uz-arab': '“”‘’',
173
+ 'uz-cyrl': '“”‘’',
174
+ wae: '«»‹›',
175
+ yav: '«»«»',
176
+ yi: '””’’',
177
+ yue: '「」『』',
178
+ zgh: '«»„”',
179
+ 'zh-hk': '「」『』',
180
+ 'zh-hant': '「」『』',
181
+ 'zh-mo': '「」『』',
182
+ 'zh-tw': '「」『』',
183
+ };
184
+
185
+ /** The marks `quotes: auto` gives an element of a language: its tag's, or
186
+ * the nearest of the tags it narrows, or English's. */
187
+ export function languageQuotes(lang: string): readonly string[] {
188
+ let tag = lang.trim().toLowerCase().replace(/_/g, '-');
189
+ while (tag) {
190
+ if (Object.hasOwn(QUOTES_BY_LANGUAGE, tag)) {
191
+ return [...QUOTES_BY_LANGUAGE[tag]];
192
+ }
193
+ const cut = tag.lastIndexOf('-');
194
+ tag = cut < 0 ? '' : tag.slice(0, cut);
195
+ }
196
+ return DEFAULT_QUOTES;
197
+ }
198
+
42
199
  export type Token =
43
200
  | { kind: 'string'; text: string }
44
201
  | { kind: 'url'; url: string }
@@ -227,17 +384,14 @@ export function parseListStyleType(value: string): string | null {
227
384
  return lower === 'none' ? 'none' : counterStyleName(token.name);
228
385
  }
229
386
 
230
- /** `quotes`: `none`, or pairs of strings, outermost first. Null when the
231
- * value cannot be read — an odd number of strings among them. */
232
- export function parseQuotes(value: string): string[] | 'none' | null {
387
+ /** `quotes`: `none`, `auto`, or pairs of strings, outermost first. Null
388
+ * when the value cannot be read — an odd number of strings among them. */
389
+ export function parseQuotes(value: string): string[] | 'none' | 'auto' | null {
233
390
  const tokens = tokenize(value);
234
391
  if (!tokens?.length) return null;
235
- if (
236
- tokens.length === 1 &&
237
- tokens[0].kind === 'ident' &&
238
- tokens[0].name.toLowerCase() === 'none'
239
- ) {
240
- return 'none';
392
+ if (tokens.length === 1 && tokens[0].kind === 'ident') {
393
+ const name = tokens[0].name.toLowerCase();
394
+ return name === 'none' || name === 'auto' ? name : null;
241
395
  }
242
396
  if (tokens.length % 2 !== 0) return null;
243
397
  const out: string[] = [];
@@ -251,13 +405,17 @@ export function parseQuotes(value: string): string[] | 'none' | null {
251
405
  /** The mark an `open-quote` (side 0) or `close-quote` (side 1) at a nesting
252
406
  * depth writes: the pair for that depth, or the innermost pair past it. */
253
407
  export function quoteAt(
254
- quotes: readonly string[] | 'none',
408
+ quotes: readonly string[] | 'none' | 'auto',
255
409
  depth: number,
256
410
  side: 0 | 1,
411
+ /** The element's language, for `auto`. */
412
+ lang = '',
257
413
  ): string {
258
- if (quotes === 'none' || quotes.length < 2) return '';
259
- const pair = Math.min(depth, quotes.length / 2 - 1);
260
- return quotes[pair * 2 + side];
414
+ if (quotes === 'none') return '';
415
+ const marks = quotes === 'auto' ? languageQuotes(lang) : quotes;
416
+ if (marks.length < 2) return '';
417
+ const pair = Math.min(depth, marks.length / 2 - 1);
418
+ return marks[pair * 2 + side];
261
419
  }
262
420
 
263
421
  // --- counter styles -----------------------------------------------------------