@react-x11/components 0.12.0 → 0.14.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 (150) hide show
  1. package/dist/html/controls.d.ts +41 -5
  2. package/dist/html/controls.d.ts.map +1 -1
  3. package/dist/html/controls.js +85 -57
  4. package/dist/html/controls.js.map +1 -1
  5. package/dist/html/css/cascade.d.ts +134 -20
  6. package/dist/html/css/cascade.d.ts.map +1 -1
  7. package/dist/html/css/cascade.js +545 -126
  8. package/dist/html/css/cascade.js.map +1 -1
  9. package/dist/html/css/color.d.ts +18 -0
  10. package/dist/html/css/color.d.ts.map +1 -1
  11. package/dist/html/css/color.js +27 -0
  12. package/dist/html/css/color.js.map +1 -1
  13. package/dist/html/css/parse.d.ts +47 -6
  14. package/dist/html/css/parse.d.ts.map +1 -1
  15. package/dist/html/css/parse.js +336 -30
  16. package/dist/html/css/parse.js.map +1 -1
  17. package/dist/html/css/style.d.ts +142 -15
  18. package/dist/html/css/style.d.ts.map +1 -1
  19. package/dist/html/css/style.js +563 -147
  20. package/dist/html/css/style.js.map +1 -1
  21. package/dist/html/css/transform.d.ts +85 -0
  22. package/dist/html/css/transform.d.ts.map +1 -0
  23. package/dist/html/css/transform.js +425 -0
  24. package/dist/html/css/transform.js.map +1 -0
  25. package/dist/html/css/ua.d.ts +11 -1
  26. package/dist/html/css/ua.d.ts.map +1 -1
  27. package/dist/html/css/ua.js +73 -9
  28. package/dist/html/css/ua.js.map +1 -1
  29. package/dist/html/css/values.d.ts +4 -0
  30. package/dist/html/css/values.d.ts.map +1 -1
  31. package/dist/html/css/values.js.map +1 -1
  32. package/dist/html/dom.d.ts.map +1 -1
  33. package/dist/html/dom.js +3 -1
  34. package/dist/html/dom.js.map +1 -1
  35. package/dist/html/fonts.d.ts +78 -4
  36. package/dist/html/fonts.d.ts.map +1 -1
  37. package/dist/html/fonts.js +373 -19
  38. package/dist/html/fonts.js.map +1 -1
  39. package/dist/html/form.d.ts +200 -0
  40. package/dist/html/form.d.ts.map +1 -0
  41. package/dist/html/form.js +801 -0
  42. package/dist/html/form.js.map +1 -0
  43. package/dist/html/index.d.ts +12 -0
  44. package/dist/html/index.d.ts.map +1 -1
  45. package/dist/html/index.js +30 -301
  46. package/dist/html/index.js.map +1 -1
  47. package/dist/html/layout/axes.d.ts +14 -0
  48. package/dist/html/layout/axes.d.ts.map +1 -0
  49. package/dist/html/layout/axes.js +28 -0
  50. package/dist/html/layout/axes.js.map +1 -0
  51. package/dist/html/layout/block.d.ts +93 -12
  52. package/dist/html/layout/block.d.ts.map +1 -1
  53. package/dist/html/layout/block.js +393 -39
  54. package/dist/html/layout/block.js.map +1 -1
  55. package/dist/html/layout/boxes.d.ts +90 -2
  56. package/dist/html/layout/boxes.d.ts.map +1 -1
  57. package/dist/html/layout/boxes.js +74 -10
  58. package/dist/html/layout/boxes.js.map +1 -1
  59. package/dist/html/layout/cache.d.ts +1 -1
  60. package/dist/html/layout/cache.d.ts.map +1 -1
  61. package/dist/html/layout/cache.js +6 -0
  62. package/dist/html/layout/cache.js.map +1 -1
  63. package/dist/html/layout/css-grid.d.ts.map +1 -1
  64. package/dist/html/layout/css-grid.js +90 -18
  65. package/dist/html/layout/css-grid.js.map +1 -1
  66. package/dist/html/layout/flex.d.ts.map +1 -1
  67. package/dist/html/layout/flex.js +574 -84
  68. package/dist/html/layout/flex.js.map +1 -1
  69. package/dist/html/layout/inline.d.ts +3 -0
  70. package/dist/html/layout/inline.d.ts.map +1 -1
  71. package/dist/html/layout/inline.js +154 -54
  72. package/dist/html/layout/inline.js.map +1 -1
  73. package/dist/html/layout/multicol.d.ts +42 -0
  74. package/dist/html/layout/multicol.d.ts.map +1 -0
  75. package/dist/html/layout/multicol.js +897 -0
  76. package/dist/html/layout/multicol.js.map +1 -0
  77. package/dist/html/layout/shaping.d.ts +7 -0
  78. package/dist/html/layout/shaping.d.ts.map +1 -1
  79. package/dist/html/layout/shaping.js +25 -20
  80. package/dist/html/layout/shaping.js.map +1 -1
  81. package/dist/html/layout/table.d.ts.map +1 -1
  82. package/dist/html/layout/table.js +62 -18
  83. package/dist/html/layout/table.js.map +1 -1
  84. package/dist/html/node.d.ts +90 -15
  85. package/dist/html/node.d.ts.map +1 -1
  86. package/dist/html/node.js +771 -186
  87. package/dist/html/node.js.map +1 -1
  88. package/dist/html/paint.d.ts +101 -5
  89. package/dist/html/paint.d.ts.map +1 -1
  90. package/dist/html/paint.js +1502 -160
  91. package/dist/html/paint.js.map +1 -1
  92. package/dist/html/svg.d.ts +15 -2
  93. package/dist/html/svg.d.ts.map +1 -1
  94. package/dist/html/svg.js +249 -17
  95. package/dist/html/svg.js.map +1 -1
  96. package/dist/html/widgets.d.ts +29 -0
  97. package/dist/html/widgets.d.ts.map +1 -0
  98. package/dist/html/widgets.js +700 -0
  99. package/dist/html/widgets.js.map +1 -0
  100. package/dist/html/woff2.d.ts +14 -0
  101. package/dist/html/woff2.d.ts.map +1 -0
  102. package/dist/html/woff2.js +608 -0
  103. package/dist/html/woff2.js.map +1 -0
  104. package/dist/index.d.ts +1 -1
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js.map +1 -1
  107. package/dist/maps/controller.d.ts +17 -1
  108. package/dist/maps/controller.d.ts.map +1 -1
  109. package/dist/maps/controller.js +27 -7
  110. package/dist/maps/controller.js.map +1 -1
  111. package/dist/richtext/node.d.ts +8 -1
  112. package/dist/richtext/node.d.ts.map +1 -1
  113. package/dist/richtext/node.js +5 -1
  114. package/dist/richtext/node.js.map +1 -1
  115. package/dist/richtext/runs.d.ts +25 -6
  116. package/dist/richtext/runs.d.ts.map +1 -1
  117. package/dist/richtext/runs.js +240 -24
  118. package/dist/richtext/runs.js.map +1 -1
  119. package/package.json +2 -2
  120. package/src/html/controls.ts +114 -51
  121. package/src/html/css/cascade.ts +614 -140
  122. package/src/html/css/color.ts +29 -0
  123. package/src/html/css/parse.ts +388 -33
  124. package/src/html/css/style.ts +688 -150
  125. package/src/html/css/transform.ts +459 -0
  126. package/src/html/css/ua.ts +77 -10
  127. package/src/html/css/values.ts +4 -0
  128. package/src/html/dom.ts +3 -1
  129. package/src/html/fonts.ts +417 -24
  130. package/src/html/form.ts +962 -0
  131. package/src/html/index.ts +49 -338
  132. package/src/html/layout/axes.ts +62 -0
  133. package/src/html/layout/block.ts +447 -37
  134. package/src/html/layout/boxes.ts +143 -14
  135. package/src/html/layout/cache.ts +6 -0
  136. package/src/html/layout/css-grid.ts +95 -22
  137. package/src/html/layout/flex.ts +675 -78
  138. package/src/html/layout/inline.ts +159 -55
  139. package/src/html/layout/multicol.ts +977 -0
  140. package/src/html/layout/shaping.ts +27 -18
  141. package/src/html/layout/table.ts +62 -19
  142. package/src/html/node.ts +798 -172
  143. package/src/html/paint.ts +1742 -241
  144. package/src/html/svg.ts +277 -19
  145. package/src/html/widgets.ts +821 -0
  146. package/src/html/woff2.ts +612 -0
  147. package/src/index.ts +1 -0
  148. package/src/maps/controller.ts +28 -7
  149. package/src/richtext/node.ts +13 -2
  150. package/src/richtext/runs.ts +313 -35
@@ -0,0 +1,459 @@
1
+ // `transform`, and the properties that are one function of it each —
2
+ // `translate`, `rotate` and `scale` (CSS Transforms 1 and 2) — read down to
3
+ // what is drawn: a matrix in the plane.
4
+ //
5
+ // A transform moves nothing but the box it is on and what that holds, so it
6
+ // is no part of layout: the box is laid out where it would be, moved by the
7
+ // matrix's translation (`applyRelativeOffsets`), and painted through the
8
+ // rest of it, about its `transform-origin` (`paintTransformed`). The two
9
+ // halves are one matrix: with `L` its linear part, `t` its translation and
10
+ // `O` the origin, a point goes to `O + L(p - O) + t`, which is `L` about the
11
+ // origin of the box `t` moved. So the translation stays the layout's, where
12
+ // a hit test, a control's rectangle and the ink bounds already find it, and
13
+ // only a box that turns, scales or skews is painted through a matrix.
14
+ //
15
+ // The functions out of the plane — `rotateX()`, `translateZ()`,
16
+ // `perspective()` — are read and are no part of the matrix: a declaration
17
+ // that names one is kept, and drawn as the rest of its list.
18
+
19
+ import { parseMath } from './calc.js';
20
+ import {
21
+ AUTO,
22
+ parseLength,
23
+ parseNumber,
24
+ resolve,
25
+ splitCommas,
26
+ splitValue,
27
+ } from './values.js';
28
+ import type { Len, UnitContext } from './values.js';
29
+ import type { ComputedStyle } from './style.js';
30
+
31
+ /** `matrix(a, b, c, d, e, f)`: the point (x, y) goes to
32
+ * (ax + cy + e, bx + dy + f), as a 2d context's `transform` has it. */
33
+ export type Matrix = readonly [number, number, number, number, number, number];
34
+
35
+ /** One function of a `transform` list: a translation by lengths, a
36
+ * percentage of the box's own width across and its height down, or a
37
+ * matrix — a run of the functions that are numbers alone, multiplied. */
38
+ export type TransformFunction = { by: [Len, Len] } | { matrix: Matrix };
39
+
40
+ export const IDENTITY: Matrix = [1, 0, 0, 1, 0, 0];
41
+
42
+ /** `m` after `n`: the matrix that does `n` and then `m`, which is what two
43
+ * functions of a list come to, the first written being `m`. */
44
+ export function multiply(m: Matrix, n: Matrix): Matrix {
45
+ return [
46
+ m[0] * n[0] + m[2] * n[1],
47
+ m[1] * n[0] + m[3] * n[1],
48
+ m[0] * n[2] + m[2] * n[3],
49
+ m[1] * n[2] + m[3] * n[3],
50
+ m[0] * n[4] + m[2] * n[5] + m[4],
51
+ m[1] * n[4] + m[3] * n[5] + m[5],
52
+ ];
53
+ }
54
+
55
+ /** The matrix that undoes `m`, or null where none does: one that flattens
56
+ * the plane to a line or a point, which draws nothing (CSS Transforms 1,
57
+ * 6). */
58
+ export function invert(m: Matrix): Matrix | null {
59
+ const det = m[0] * m[3] - m[1] * m[2];
60
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-12) return null;
61
+ const a = m[3] / det;
62
+ const b = -m[1] / det;
63
+ const c = -m[2] / det;
64
+ const d = m[0] / det;
65
+ return [a, b, c, d, -(a * m[4] + c * m[5]), -(b * m[4] + d * m[5])];
66
+ }
67
+
68
+ /** Where a point goes. */
69
+ export function mapPoint(m: Matrix, x: number, y: number): [number, number] {
70
+ return [m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5]];
71
+ }
72
+
73
+ /** The rectangle around where a rectangle goes: its four corners'. */
74
+ export function mapRect(
75
+ m: Matrix,
76
+ x: number,
77
+ y: number,
78
+ width: number,
79
+ height: number,
80
+ ): { x: number; y: number; width: number; height: number } {
81
+ let x1 = Infinity;
82
+ let y1 = Infinity;
83
+ let x2 = -Infinity;
84
+ let y2 = -Infinity;
85
+ for (let i = 0; i < 4; i += 1) {
86
+ const px = i & 1 ? x + width : x;
87
+ const py = i & 2 ? y + height : y;
88
+ const qx = m[0] * px + m[2] * py + m[4];
89
+ const qy = m[1] * px + m[3] * py + m[5];
90
+ if (qx < x1) x1 = qx;
91
+ if (qx > x2) x2 = qx;
92
+ if (qy < y1) y1 = qy;
93
+ if (qy > y2) y2 = qy;
94
+ }
95
+ return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
96
+ }
97
+
98
+ /** A rotation by degrees, clockwise as the page is seen. A quarter turn is
99
+ * its own numbers exactly: the cosine of ninety degrees computed is 6e-17,
100
+ * and a box turned on its side was a sliver of a pixel off the grid. */
101
+ export function rotation(degrees: number): Matrix {
102
+ const quarters = degrees / 90;
103
+ if (Number.isInteger(quarters)) {
104
+ const at = ((quarters % 4) + 4) % 4;
105
+ const cos = at === 0 ? 1 : at === 2 ? -1 : 0;
106
+ const sin = at === 1 ? 1 : at === 3 ? -1 : 0;
107
+ // no negative zero: it is a zero that compares unequal to one
108
+ return [cos, sin, sin === 0 ? 0 : -sin, cos, 0, 0];
109
+ }
110
+ const angle = (degrees * Math.PI) / 180;
111
+ const cos = Math.cos(angle);
112
+ const sin = Math.sin(angle);
113
+ return [cos, sin, -sin, cos, 0, 0];
114
+ }
115
+
116
+ const DEGREES: Record<string, number> = {
117
+ deg: 1,
118
+ grad: 0.9,
119
+ rad: 180 / Math.PI,
120
+ turn: 360,
121
+ };
122
+
123
+ const ANGLE = /^([+-]?(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?)(deg|grad|rad|turn)$/;
124
+
125
+ /** An angle in degrees: a number with its unit, or a `calc()` of them —
126
+ * and a zero without one, where `bare` lets it be, as a transform
127
+ * function's argument does (CSS Values 4, 7.1). Null for anything else. */
128
+ export function parseAngle(value: string, bare = true): number | null {
129
+ const v = value.trim().toLowerCase();
130
+ const m = ANGLE.exec(v);
131
+ if (m) return Number(m[1]) * DEGREES[m[2]];
132
+ if (bare && /^[+-]?0*\.?0+$/.test(v)) return 0;
133
+ if (!v.includes('(')) return null;
134
+ const sum = parseMath(v, (token) => {
135
+ const unit = ANGLE.exec(token);
136
+ return unit ? Number(unit[1]) * DEGREES[unit[2]] : null;
137
+ });
138
+ return typeof sum === 'number' ? sum : null;
139
+ }
140
+
141
+ /** A scale factor: a number, or a percentage of one (CSS Transforms 2, 5),
142
+ * or a `calc()` of either. */
143
+ function parseFactor(value: string): number | null {
144
+ const v = value.trim();
145
+ if (v.endsWith('%')) {
146
+ const n = parseNumber(v.slice(0, -1));
147
+ return n === null ? null : n / 100;
148
+ }
149
+ const n = parseNumber(v);
150
+ if (n !== null) return n;
151
+ if (!v.includes('(')) return null;
152
+ const sum = parseMath(v.toLowerCase(), () => null);
153
+ if (sum === null || sum === AUTO || typeof sum === 'number') return null;
154
+ return sum.of ? null : sum.pct / 100;
155
+ }
156
+
157
+ function parseOffset(value: string | undefined, ctx: UnitContext): Len | null {
158
+ if (value === undefined) return null;
159
+ const len = parseLength(value, ctx);
160
+ return len === AUTO ? null : len;
161
+ }
162
+
163
+ /**
164
+ * A `transform`: its functions in the order written. Null for `none`, and
165
+ * undefined for what is no transform list, which drops the declaration. A
166
+ * list that comes to nothing in the plane — `translateZ(0)`, which pages
167
+ * write to have a layer made — is a list of none, and a transform still: the
168
+ * box is a containing block and a stacking context as any transformed box
169
+ * is.
170
+ */
171
+ export function parseTransform(
172
+ value: string,
173
+ ctx: UnitContext,
174
+ ): TransformFunction[] | null | undefined {
175
+ if (value.trim().toLowerCase() === 'none') return null;
176
+ const parts = splitValue(value);
177
+ if (!parts.length) return undefined;
178
+ const out: TransformFunction[] = [];
179
+ for (const part of parts) {
180
+ const m = /^([a-z0-9]+)\((.*)\)$/is.exec(part.trim());
181
+ if (!m) return undefined;
182
+ const args = splitCommas(m[2]).map((a) => a.trim());
183
+ const fn = functionOf(m[1].toLowerCase(), args, ctx);
184
+ if (fn === undefined) return undefined;
185
+ if (fn === null) continue;
186
+ const last = out[out.length - 1];
187
+ if ('matrix' in fn && last && 'matrix' in last) {
188
+ out[out.length - 1] = { matrix: multiply(last.matrix, fn.matrix) };
189
+ } else out.push(fn);
190
+ }
191
+ return out;
192
+ }
193
+
194
+ /** One function: what it is in the plane, null where that is nothing, and
195
+ * undefined where it is no function or its arguments are not its own. */
196
+ function functionOf(
197
+ name: string,
198
+ args: string[],
199
+ ctx: UnitContext,
200
+ ): TransformFunction | null | undefined {
201
+ const count = args.length;
202
+ const angles = (): number[] | null => {
203
+ const out = args.map((a) => parseAngle(a));
204
+ return out.some((a) => a === null) ? null : (out as number[]);
205
+ };
206
+ const factors = (): number[] | null => {
207
+ const out = args.map(parseFactor);
208
+ return out.some((n) => n === null) ? null : (out as number[]);
209
+ };
210
+ const tan = (degrees: number): number => Math.tan((degrees * Math.PI) / 180);
211
+ switch (name) {
212
+ case 'translate': {
213
+ if (count < 1 || count > 2) return undefined;
214
+ const x = parseOffset(args[0], ctx);
215
+ const y = count === 2 ? parseOffset(args[1], ctx) : 0;
216
+ return x === null || y === null ? undefined : { by: [x, y] };
217
+ }
218
+ case 'translate3d': {
219
+ if (count !== 3) return undefined;
220
+ const x = parseOffset(args[0], ctx);
221
+ const y = parseOffset(args[1], ctx);
222
+ if (x === null || y === null) return undefined;
223
+ // a depth is a length, and no percentage
224
+ return typeof parseOffset(args[2], ctx) === 'number'
225
+ ? { by: [x, y] }
226
+ : undefined;
227
+ }
228
+ case 'translatex':
229
+ case 'translatey': {
230
+ const by = count === 1 ? parseOffset(args[0], ctx) : null;
231
+ if (by === null) return undefined;
232
+ return { by: name === 'translatex' ? [by, 0] : [0, by] };
233
+ }
234
+ case 'translatez':
235
+ return count === 1 && typeof parseOffset(args[0], ctx) === 'number'
236
+ ? null
237
+ : undefined;
238
+ case 'scale': {
239
+ const f = count >= 1 && count <= 2 ? factors() : null;
240
+ return f ? { matrix: [f[0], 0, 0, f[1] ?? f[0], 0, 0] } : undefined;
241
+ }
242
+ case 'scale3d': {
243
+ const f = count === 3 ? factors() : null;
244
+ return f ? { matrix: [f[0], 0, 0, f[1], 0, 0] } : undefined;
245
+ }
246
+ case 'scalex':
247
+ case 'scaley': {
248
+ const f = count === 1 ? factors() : null;
249
+ if (!f) return undefined;
250
+ return {
251
+ matrix:
252
+ name === 'scalex' ? [f[0], 0, 0, 1, 0, 0] : [1, 0, 0, f[0], 0, 0],
253
+ };
254
+ }
255
+ case 'scalez':
256
+ return count === 1 && factors() ? null : undefined;
257
+ case 'rotate':
258
+ case 'rotatez': {
259
+ const a = count === 1 ? angles() : null;
260
+ return a ? { matrix: rotation(a[0]) } : undefined;
261
+ }
262
+ case 'rotatex':
263
+ case 'rotatey':
264
+ return count === 1 && angles() ? null : undefined;
265
+ case 'rotate3d': {
266
+ if (count !== 4) return undefined;
267
+ const axis = args.slice(0, 3).map(parseNumber);
268
+ const angle = parseAngle(args[3]);
269
+ if (angle === null || axis.some((n) => n === null)) return undefined;
270
+ return axisRotation(axis as number[], angle);
271
+ }
272
+ case 'skew': {
273
+ const a = count >= 1 && count <= 2 ? angles() : null;
274
+ return a
275
+ ? { matrix: [1, tan(a[1] ?? 0), tan(a[0]), 1, 0, 0] }
276
+ : undefined;
277
+ }
278
+ case 'skewx':
279
+ case 'skewy': {
280
+ const a = count === 1 ? angles() : null;
281
+ if (!a) return undefined;
282
+ return {
283
+ matrix:
284
+ name === 'skewx'
285
+ ? [1, 0, tan(a[0]), 1, 0, 0]
286
+ : [1, tan(a[0]), 0, 1, 0, 0],
287
+ };
288
+ }
289
+ case 'matrix':
290
+ case 'matrix3d': {
291
+ const n = args.map(parseNumber);
292
+ const flat = name === 'matrix';
293
+ if (n.length !== (flat ? 6 : 16) || n.some((v) => v === null)) {
294
+ return undefined;
295
+ }
296
+ const v = n as number[];
297
+ // the plane's part of a 4×4, whose columns are written in turn; a
298
+ // matrix's translation is in CSS pixels, as a length without its unit
299
+ const [a, b, c, d, e, f] = flat
300
+ ? v
301
+ : [v[0], v[1], v[4], v[5], v[12], v[13]];
302
+ return { matrix: [a, b, c, d, e * ctx.scale, f * ctx.scale] };
303
+ }
304
+ case 'perspective': {
305
+ if (count !== 1) return undefined;
306
+ if (args[0].toLowerCase() === 'none') return null;
307
+ return typeof parseOffset(args[0], ctx) === 'number' ? null : undefined;
308
+ }
309
+ default:
310
+ return undefined;
311
+ }
312
+ }
313
+
314
+ /** A turn about an axis, where the axis is the one out of the page — the
315
+ * other way where it points into it; about any other it is out of the
316
+ * plane, and nothing here. */
317
+ function axisRotation(axis: number[], angle: number): TransformFunction | null {
318
+ const [x, y, z] = axis;
319
+ if (x !== 0 || y !== 0 || z === 0) return null;
320
+ return { matrix: rotation(z > 0 ? angle : -angle) };
321
+ }
322
+
323
+ /**
324
+ * `rotate` (CSS Transforms 2, 4): an angle in degrees, about the axis out of
325
+ * the page where one is named. Null for `none`, and for a turn about any
326
+ * other axis, which is out of the plane; undefined for what is no value of
327
+ * it.
328
+ */
329
+ export function parseRotate(value: string): number | null | undefined {
330
+ const parts = splitValue(value.trim().toLowerCase());
331
+ if (parts.length === 1) {
332
+ if (parts[0] === 'none') return null;
333
+ return parseAngle(parts[0], false) ?? undefined;
334
+ }
335
+ // an axis and an angle, in either order; the axis a letter or a vector
336
+ const at = parts.findIndex((p) => parseAngle(p, false) !== null);
337
+ const angle = at < 0 ? null : parseAngle(parts[at], false);
338
+ if (angle === null || (at !== 0 && at !== parts.length - 1)) return undefined;
339
+ const axis = parts.filter((_, i) => i !== at);
340
+ if (axis.length === 1) {
341
+ if (axis[0] === 'z') return angle;
342
+ return axis[0] === 'x' || axis[0] === 'y' ? null : undefined;
343
+ }
344
+ const vector = axis.map(parseNumber);
345
+ if (vector.length !== 3 || vector.some((n) => n === null)) return undefined;
346
+ const turned = axisRotation(vector as number[], 1);
347
+ if (!turned) return null;
348
+ return (vector[2] as number) > 0 ? angle : -angle;
349
+ }
350
+
351
+ /** `scale` (CSS Transforms 2, 5): across and down, the one factor both
352
+ * where one is written, and a depth that is nothing here. Null for
353
+ * `none`. */
354
+ export function parseScale(value: string): [number, number] | null | undefined {
355
+ const parts = splitValue(value.trim());
356
+ if (parts.length === 1 && parts[0].toLowerCase() === 'none') return null;
357
+ if (parts.length < 1 || parts.length > 3) return undefined;
358
+ const factors = parts.map(parseFactor);
359
+ if (factors.some((f) => f === null)) return undefined;
360
+ const [x, y] = factors as number[];
361
+ return [x, y ?? x];
362
+ }
363
+
364
+ type Transformed = Pick<
365
+ ComputedStyle,
366
+ 'translate' | 'rotate' | 'scale' | 'transform'
367
+ >;
368
+
369
+ /** Whether a style transforms its box: any of the four properties at
370
+ * anything but `none`, whatever it comes to — which makes the box a
371
+ * containing block for all it holds and a stacking context (CSS
372
+ * Transforms 1, 3). */
373
+ export function transformed(style: Transformed): boolean {
374
+ return (
375
+ style.transform !== null ||
376
+ style.translate !== null ||
377
+ style.rotate !== null ||
378
+ style.scale !== null
379
+ );
380
+ }
381
+
382
+ /**
383
+ * A style's whole transform, for a box `width` by `height`, which its
384
+ * percentages are of: `translate`, then `rotate`, then `scale`, then the
385
+ * `transform` list (CSS Transforms 2, 7) — without its origin, which is
386
+ * where the box is.
387
+ */
388
+ export function matrixOf(
389
+ style: Transformed,
390
+ width: number,
391
+ height: number,
392
+ ): Matrix {
393
+ let m = IDENTITY;
394
+ const moved = style.translate;
395
+ if (moved) {
396
+ m = [1, 0, 0, 1, resolve(moved[0], width, 0), resolve(moved[1], height, 0)];
397
+ }
398
+ if (style.rotate !== null) m = multiply(m, rotation(style.rotate));
399
+ if (style.scale) {
400
+ m = multiply(m, [style.scale[0], 0, 0, style.scale[1], 0, 0]);
401
+ }
402
+ if (style.transform) {
403
+ for (const fn of style.transform) {
404
+ m = multiply(
405
+ m,
406
+ 'matrix' in fn
407
+ ? fn.matrix
408
+ : [
409
+ 1,
410
+ 0,
411
+ 0,
412
+ 1,
413
+ resolve(fn.by[0], width, 0),
414
+ resolve(fn.by[1], height, 0),
415
+ ],
416
+ );
417
+ }
418
+ }
419
+ return m;
420
+ }
421
+
422
+ /** The part of a matrix that turns, scales and skews. */
423
+ export type Linear = readonly [number, number, number, number];
424
+
425
+ const LINEAR = new WeakMap<object, Linear | null>();
426
+
427
+ /**
428
+ * What a style's transform does besides move the box: null where that is
429
+ * nothing, as it is for every box but a few. The same for a box of any
430
+ * size — a translation's percentages are no part of it — so kept by the
431
+ * style.
432
+ */
433
+ export function linearOf(style: Transformed): Linear | null {
434
+ if (
435
+ style.transform === null &&
436
+ style.rotate === null &&
437
+ style.scale === null
438
+ ) {
439
+ return null;
440
+ }
441
+ let linear = LINEAR.get(style);
442
+ if (linear === undefined) {
443
+ const m = matrixOf(style, 0, 0);
444
+ const near = (a: number, b: number) => Math.abs(a - b) < 1e-9;
445
+ linear =
446
+ near(m[0], 1) && near(m[1], 0) && near(m[2], 0) && near(m[3], 1)
447
+ ? null
448
+ : [m[0], m[1], m[2], m[3]];
449
+ LINEAR.set(style, linear);
450
+ }
451
+ return linear;
452
+ }
453
+
454
+ /** `linear` about a point: the matrix a box at that origin is drawn
455
+ * through, in the coordinates the point is in. */
456
+ export function about(linear: Linear, x: number, y: number): Matrix {
457
+ const [a, b, c, d] = linear;
458
+ return [a, b, c, d, x - (a * x + c * y), y - (b * x + d * y)];
459
+ }
@@ -8,16 +8,34 @@
8
8
  // already using, so an unstyled document reads as part of the window. An
9
9
  // author stylesheet still overrides all of it — that is what makes it a UA
10
10
  // sheet and not a skin.
11
+ //
12
+ // The theme has the colours of one scheme. A document whose root is of the
13
+ // other — `color-scheme: light` under a dark palette — is a browser's in
14
+ // that one: the text and the links a `light-dark()` of the theme's and
15
+ // Chrome's (`inScheme`), over a canvas of that scheme (`paintDocument`).
16
+ import { SCHEME_COLORS } from './color.js';
11
17
  import { parseStylesheet } from './parse.js';
12
- import type { Stylesheet } from './parse.js';
18
+ import type { Declaration, Stylesheet } from './parse.js';
13
19
  import type { RootLook } from './style.js';
14
20
 
21
+ /**
22
+ * The declaration blocks that draw a control as the palette's widget rather
23
+ * than as the web's UA sheet has it. The cascade drops them from an element
24
+ * whose page styles its background or border (`Cascade`'s
25
+ * `dropPaletteChrome`), as Blink drops a control's native appearance
26
+ * (`LayoutTheme::IsControlStyled`), since a page that styles a control
27
+ * builds on the UA values a browser gives it. Keyed by the array, which is
28
+ * what a cascade candidate carries.
29
+ */
30
+ export const PALETTE_CHROME = new WeakSet<Declaration[]>();
31
+
15
32
  /** Cache key → parsed sheet. The text depends only on the look, so two
16
33
  * documents in one themed window parse this once between them. */
17
34
  const CACHE = new Map<string, Stylesheet>();
18
35
 
19
36
  export function lookKey(look: RootLook): string {
20
37
  return [
38
+ look.colorScheme,
21
39
  look.color,
22
40
  look.fontFamily,
23
41
  look.fontSize,
@@ -30,6 +48,8 @@ export function lookKey(look: RootLook): string {
30
48
  look.controlPadY,
31
49
  look.controlBorder,
32
50
  look.controlRadius,
51
+ look.controlFontSize,
52
+ look.controlFontFamily,
33
53
  ].join('|');
34
54
  }
35
55
 
@@ -38,6 +58,13 @@ export function uaStylesheet(look: RootLook): Stylesheet {
38
58
  const hit = CACHE.get(key);
39
59
  if (hit) return hit;
40
60
  const sheet = parseStylesheet(uaText(look), -1_000_000);
61
+ // after the rest, so its values win over theirs while it applies
62
+ const last = sheet.rules[sheet.rules.length - 1];
63
+ const chrome = parseStylesheet(chromeText(look), last.order + 1);
64
+ for (const rule of chrome.rules) {
65
+ PALETTE_CHROME.add(rule.declarations);
66
+ sheet.rules.push(rule);
67
+ }
41
68
  if (CACHE.size > 8) CACHE.clear();
42
69
  CACHE.set(key, sheet);
43
70
  return sheet;
@@ -52,6 +79,12 @@ export function uaStylesheet(look: RootLook): Stylesheet {
52
79
  */
53
80
  function uaText(look: RootLook): string {
54
81
  const mono = look.monoFamily;
82
+ // the theme's colour where the element's scheme is the theme's, and a
83
+ // browser's own for the other scheme where it is not
84
+ const inScheme = (own: string, which: 'text' | 'link'): string =>
85
+ look.colorScheme === 'dark'
86
+ ? `light-dark(${SCHEME_COLORS.light[which]}, ${own})`
87
+ : `light-dark(${own}, ${SCHEME_COLORS.dark[which]})`;
55
88
  return `
56
89
  html, body, div, p, h1, h2, h3, h4, h5, h6, ol, ul, li, dl, dt, dd,
57
90
  blockquote, pre, hr, table, form, fieldset, figure, figcaption, address,
@@ -74,7 +107,7 @@ bdo { unicode-bidi: isolate-override; }
74
107
  /* the theme's colour and font are the root's (\`initialStyle\`), and the
75
108
  body inherits them, from an author's \`html\` rule too */
76
109
  body { margin: 8px; }
77
- html { color: ${look.color}; }
110
+ html { color: ${inScheme(look.color, 'text')}; }
78
111
 
79
112
  p { margin: 1em 0; }
80
113
  h1 { font-size: 2em; font-weight: bold; margin: 0.67em 0; }
@@ -139,7 +172,7 @@ q::after { content: close-quote; }
139
172
  pointer moves, and a rule that changes nothing (the link is already
140
173
  underlined) would make every plain document pay that. An 'a' with no
141
174
  'href' is an anchor, not a link, and is drawn as the text around it. */
142
- a[href] { color: ${look.linkColor}; text-decoration: underline; cursor: pointer; }
175
+ a[href] { color: ${inScheme(look.linkColor, 'link')}; text-decoration: underline; cursor: pointer; }
143
176
 
144
177
  hr {
145
178
  margin: 0.5em auto;
@@ -181,11 +214,23 @@ col { display: table-column; }
181
214
  makes a label and its input share a line. */
182
215
  input, button, select, textarea, meter, progress {
183
216
  display: inline-block;
184
- font-family: ${look.fontFamily};
185
- font-size: 1em;
186
217
  vertical-align: middle;
187
218
  margin: 3px 2px;
188
219
  }
220
+ /* A control's text is a system font's, not its parent's: Chrome gives these
221
+ four \`font: -webkit-small-control\`, Arial at the default size less 2pt
222
+ (13.33px) whatever the text around it is set in, and Gecko \`-moz-field\`
223
+ the same. The system here is the palette, and its family and size are the
224
+ ones core's widgets are set in — so a document's form is its window's,
225
+ and the box a control is measured into is the size the widget mounted in
226
+ it draws. A <textarea> is then \`monospace\` in Chrome, the sheet's code
227
+ face here; a <meter> and a <progress> keep their parent's font, as they
228
+ do in Chrome. */
229
+ input, button, select, textarea {
230
+ font-family: ${look.controlFontFamily ?? look.fontFamily};
231
+ font-size: ${look.controlFontSize ?? look.fontSize}px;
232
+ }
233
+ textarea { font-family: ${mono}; }
189
234
  /* Chrome's own UA margins for the checkables, near enough: they are the
190
235
  controls that sit hard against their label text otherwise. */
191
236
  /* HTML's rendering section (15.3.10): a control's text keeps none of the
@@ -208,13 +253,16 @@ input[type=hidden] { display: none; }
208
253
  /* A <button> is drawn rather than mounted: its content is the document's —
209
254
  an icon, a label in spans, a pill of the page's own design, which is what
210
255
  most buttons on the web are — and the page restyles it as it restyles
211
- anything. What the sheet gives one the page left alone is the palette's
212
- control, the chrome a mounted widget around it has. */
256
+ anything. The edges here are Chrome's, which a page that styles its
257
+ buttons builds on: Codex gives Wikipedia's search button its side padding
258
+ and a 32px min-height, and leaves the 1px above and below the label, and
259
+ the border box the minimum holds, to the browser. One the page left alone
260
+ is the palette's control instead, below the rest of this sheet. */
213
261
  button {
214
262
  vertical-align: baseline;
215
- padding: ${look.controlPadY}px 0.75em;
216
- border: ${look.controlBorder}px solid ${look.borderColor};
217
- border-radius: ${look.controlRadius}px;
263
+ box-sizing: border-box;
264
+ padding: 1px 6px;
265
+ border: 2px outset ${look.borderColor};
218
266
  background-color: ${look.surface};
219
267
  color: ${look.color};
220
268
  text-align: center;
@@ -235,3 +283,22 @@ details[open] > summary:first-of-type { list-style-type: disclosure-open; }
235
283
  [hidden] { display: none; }
236
284
  `;
237
285
  }
286
+
287
+ /**
288
+ * The palette's control, for a `<button>` whose page left its background and
289
+ * border alone: the chrome a mounted widget has, so a form in a document is
290
+ * the size of the window's own. The rules land in `PALETTE_CHROME`, and a
291
+ * page that styles either loses all of them — Blink counts every background
292
+ * and border longhand, radius included — along with the native look it
293
+ * would lose in a browser. Nothing stays half the palette's and half the
294
+ * page's.
295
+ */
296
+ function chromeText(look: RootLook): string {
297
+ return `
298
+ button {
299
+ padding: ${look.controlPadY}px 0.75em;
300
+ border: ${look.controlBorder}px solid ${look.borderColor};
301
+ border-radius: ${look.controlRadius}px;
302
+ }
303
+ `;
304
+ }
@@ -117,6 +117,10 @@ export interface UnitContext {
117
117
  * itself, with `@font-face`, put as the text engine knows them — see
118
118
  * `fonts.ts`. Absent, the list is the one the author wrote. */
119
119
  families?: (list: string) => string;
120
+ /** The family a `font-family` list with no generic family at its end
121
+ * goes on to: the document's own, for a list an author wrote, and none
122
+ * for the UA sheet's, which names the host's faces. */
123
+ fallbackFamily?: () => string | null;
120
124
  /** The element's line height and the root's, for `lh` and `rlh`, asked
121
125
  * only for a length in them; 1.2em where they are missing. */
122
126
  lh?: () => number;
package/src/html/dom.ts CHANGED
@@ -342,7 +342,9 @@ export class HtmlSource {
342
342
  tag === 'image' ||
343
343
  tag === 'object' ||
344
344
  tag === 'embed' ||
345
- tag === 'video'
345
+ tag === 'video' ||
346
+ (tag === 'input' &&
347
+ (attr(el, 'type') ?? '').trim().toLowerCase() === 'image')
346
348
  ) {
347
349
  if (imageUrlOf(el)) facts.resources.push(el);
348
350
  } else if (tag === 'title' && facts.title === null) {