@react-x11/components 0.13.0 → 0.14.1
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.
- package/dist/html/controls.d.ts +29 -2
- package/dist/html/controls.d.ts.map +1 -1
- package/dist/html/controls.js +70 -2
- package/dist/html/controls.js.map +1 -1
- package/dist/html/css/cascade.d.ts +42 -1
- package/dist/html/css/cascade.d.ts.map +1 -1
- package/dist/html/css/cascade.js +336 -16
- package/dist/html/css/cascade.js.map +1 -1
- package/dist/html/css/color.d.ts +18 -0
- package/dist/html/css/color.d.ts.map +1 -1
- package/dist/html/css/color.js +27 -0
- package/dist/html/css/color.js.map +1 -1
- package/dist/html/css/parse.d.ts +18 -4
- package/dist/html/css/parse.d.ts.map +1 -1
- package/dist/html/css/parse.js +66 -28
- package/dist/html/css/parse.js.map +1 -1
- package/dist/html/css/shapes.d.ts +44 -0
- package/dist/html/css/shapes.d.ts.map +1 -0
- package/dist/html/css/shapes.js +194 -0
- package/dist/html/css/shapes.js.map +1 -0
- package/dist/html/css/style.d.ts +128 -15
- package/dist/html/css/style.d.ts.map +1 -1
- package/dist/html/css/style.js +547 -144
- package/dist/html/css/style.js.map +1 -1
- package/dist/html/css/transform.d.ts +85 -0
- package/dist/html/css/transform.d.ts.map +1 -0
- package/dist/html/css/transform.js +425 -0
- package/dist/html/css/transform.js.map +1 -0
- package/dist/html/css/ua.d.ts.map +1 -1
- package/dist/html/css/ua.js +14 -2
- package/dist/html/css/ua.js.map +1 -1
- package/dist/html/css/values.d.ts +4 -0
- package/dist/html/css/values.d.ts.map +1 -1
- package/dist/html/css/values.js.map +1 -1
- package/dist/html/fonts.d.ts +55 -5
- package/dist/html/fonts.d.ts.map +1 -1
- package/dist/html/fonts.js +284 -23
- package/dist/html/fonts.js.map +1 -1
- package/dist/html/layout/axes.d.ts +14 -0
- package/dist/html/layout/axes.d.ts.map +1 -0
- package/dist/html/layout/axes.js +28 -0
- package/dist/html/layout/axes.js.map +1 -0
- package/dist/html/layout/block.d.ts +104 -12
- package/dist/html/layout/block.d.ts.map +1 -1
- package/dist/html/layout/block.js +410 -39
- package/dist/html/layout/block.js.map +1 -1
- package/dist/html/layout/boxes.d.ts +72 -1
- package/dist/html/layout/boxes.d.ts.map +1 -1
- package/dist/html/layout/boxes.js +40 -2
- package/dist/html/layout/boxes.js.map +1 -1
- package/dist/html/layout/cache.d.ts +1 -1
- package/dist/html/layout/cache.d.ts.map +1 -1
- package/dist/html/layout/cache.js +6 -0
- package/dist/html/layout/cache.js.map +1 -1
- package/dist/html/layout/css-grid.d.ts.map +1 -1
- package/dist/html/layout/css-grid.js +144 -24
- package/dist/html/layout/css-grid.js.map +1 -1
- package/dist/html/layout/flex.d.ts.map +1 -1
- package/dist/html/layout/flex.js +585 -85
- package/dist/html/layout/flex.js.map +1 -1
- package/dist/html/layout/inline.d.ts +3 -0
- package/dist/html/layout/inline.d.ts.map +1 -1
- package/dist/html/layout/inline.js +144 -54
- package/dist/html/layout/inline.js.map +1 -1
- package/dist/html/layout/multicol.d.ts +42 -0
- package/dist/html/layout/multicol.d.ts.map +1 -0
- package/dist/html/layout/multicol.js +897 -0
- package/dist/html/layout/multicol.js.map +1 -0
- package/dist/html/layout/shaping.d.ts +7 -0
- package/dist/html/layout/shaping.d.ts.map +1 -1
- package/dist/html/layout/shaping.js +25 -20
- package/dist/html/layout/shaping.js.map +1 -1
- package/dist/html/layout/table.d.ts.map +1 -1
- package/dist/html/layout/table.js +97 -31
- package/dist/html/layout/table.js.map +1 -1
- package/dist/html/node.d.ts +4 -0
- package/dist/html/node.d.ts.map +1 -1
- package/dist/html/node.js +319 -47
- package/dist/html/node.js.map +1 -1
- package/dist/html/paint.d.ts +83 -5
- package/dist/html/paint.d.ts.map +1 -1
- package/dist/html/paint.js +1474 -217
- package/dist/html/paint.js.map +1 -1
- package/dist/html/svg.d.ts +39 -9
- package/dist/html/svg.d.ts.map +1 -1
- package/dist/html/svg.js +482 -37
- package/dist/html/svg.js.map +1 -1
- package/dist/html/widgets.js +87 -7
- package/dist/html/widgets.js.map +1 -1
- package/dist/html/woff2.d.ts +14 -0
- package/dist/html/woff2.d.ts.map +1 -0
- package/dist/html/woff2.js +608 -0
- package/dist/html/woff2.js.map +1 -0
- package/dist/maps/controller.d.ts +17 -1
- package/dist/maps/controller.d.ts.map +1 -1
- package/dist/maps/controller.js +27 -7
- package/dist/maps/controller.js.map +1 -1
- package/dist/richtext/node.d.ts +8 -1
- package/dist/richtext/node.d.ts.map +1 -1
- package/dist/richtext/node.js +5 -1
- package/dist/richtext/node.js.map +1 -1
- package/dist/richtext/runs.d.ts +25 -6
- package/dist/richtext/runs.d.ts.map +1 -1
- package/dist/richtext/runs.js +240 -24
- package/dist/richtext/runs.js.map +1 -1
- package/package.json +1 -1
- package/src/html/controls.ts +85 -2
- package/src/html/css/cascade.ts +366 -8
- package/src/html/css/color.ts +29 -0
- package/src/html/css/parse.ts +71 -31
- package/src/html/css/shapes.ts +239 -0
- package/src/html/css/style.ts +652 -147
- package/src/html/css/transform.ts +459 -0
- package/src/html/css/ua.ts +15 -2
- package/src/html/css/values.ts +4 -0
- package/src/html/fonts.ts +312 -29
- package/src/html/layout/axes.ts +62 -0
- package/src/html/layout/block.ts +466 -37
- package/src/html/layout/boxes.ts +95 -3
- package/src/html/layout/cache.ts +6 -0
- package/src/html/layout/css-grid.ts +169 -32
- package/src/html/layout/flex.ts +686 -79
- package/src/html/layout/inline.ts +149 -55
- package/src/html/layout/multicol.ts +977 -0
- package/src/html/layout/shaping.ts +27 -18
- package/src/html/layout/table.ts +105 -35
- package/src/html/node.ts +354 -48
- package/src/html/paint.ts +1804 -315
- package/src/html/svg.ts +573 -36
- package/src/html/widgets.ts +95 -9
- package/src/html/woff2.ts +612 -0
- package/src/maps/controller.ts +28 -7
- package/src/richtext/node.ts +13 -2
- package/src/richtext/runs.ts +313 -35
package/src/html/css/parse.ts
CHANGED
|
@@ -56,6 +56,10 @@ export interface Stylesheet {
|
|
|
56
56
|
* resource seam and splices the result in ahead of this sheet. Absolute
|
|
57
57
|
* when the sheet was parsed with a base. */
|
|
58
58
|
imports: string[];
|
|
59
|
+
/** Each import's media query list, where it has one: the condition the
|
|
60
|
+
* sheet it brings in is under, as though an `@media` block were around
|
|
61
|
+
* all of it (CSS Cascade 4, 2). Null for an import with none. */
|
|
62
|
+
importConditions: (MediaCondition[] | null)[];
|
|
59
63
|
/** Every width a `@media` rule in this sheet switches on. The renderer
|
|
60
64
|
* keeps these so a resize can tell "the layout changed" from "the
|
|
61
65
|
* *cascade* changed", and restyle only when it crossed one. */
|
|
@@ -84,8 +88,9 @@ export interface Stylesheet {
|
|
|
84
88
|
export interface FontFaceRule {
|
|
85
89
|
/** The family as the document names it, unquoted. */
|
|
86
90
|
family: string;
|
|
87
|
-
/** The
|
|
88
|
-
*
|
|
91
|
+
/** The entries of `src`, in order, which is the order they are tried in:
|
|
92
|
+
* a `url()` with its `format()` hint lowercased, or the name a `local()`
|
|
93
|
+
* asks the system for. */
|
|
89
94
|
sources: FontFaceSource[];
|
|
90
95
|
/** The weights the face covers: `[400, 400]` for a static regular, a
|
|
91
96
|
* range for a variable face. */
|
|
@@ -97,10 +102,10 @@ export interface FontFaceRule {
|
|
|
97
102
|
media: MediaCondition[][] | null;
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
export
|
|
101
|
-
url: string;
|
|
102
|
-
|
|
103
|
-
}
|
|
105
|
+
export type FontFaceSource =
|
|
106
|
+
| { url: string; format: string | null }
|
|
107
|
+
/** `local(Arial)`: a face the system has, by name and unquoted. */
|
|
108
|
+
| { local: string };
|
|
104
109
|
|
|
105
110
|
/** The tests this evaluates live: a width, a height, a colour scheme.
|
|
106
111
|
* Anything else — `orientation`, `print`, `prefers-reduced-motion` — is
|
|
@@ -176,16 +181,38 @@ export function parseStylesheet(
|
|
|
176
181
|
* the document's for a `<style>` — or null to leave them as written.
|
|
177
182
|
* Every `url()`, `@import` and `@font-face` source comes out absolute. */
|
|
178
183
|
base: string | null = null,
|
|
184
|
+
/** The media queries the whole sheet is under, outermost first: the ones
|
|
185
|
+
* of the `@import` that brought it in, and of the imports that brought
|
|
186
|
+
* that sheet in. Every rule and face in it is under them, as one inside
|
|
187
|
+
* an `@media` block is under the block's. */
|
|
188
|
+
under: MediaCondition[][] | null = null,
|
|
179
189
|
): Stylesheet {
|
|
180
190
|
text = withoutComments(text);
|
|
181
191
|
const sheet: Stylesheet = {
|
|
182
192
|
rules: [],
|
|
183
193
|
imports: [],
|
|
194
|
+
importConditions: [],
|
|
184
195
|
breakpoints: [],
|
|
185
196
|
fontFaces: [],
|
|
186
197
|
};
|
|
187
198
|
let order = startOrder;
|
|
188
199
|
const breakpoints = new Set<number>();
|
|
200
|
+
/** What a media query list tests that a resize has to restyle for: the
|
|
201
|
+
* widths it changes its mind at, and whether it reads the height. */
|
|
202
|
+
const note = (conditions: MediaCondition[]): void => {
|
|
203
|
+
for (const c of conditions) {
|
|
204
|
+
if (c.min !== undefined) breakpoints.add(c.min);
|
|
205
|
+
if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
|
|
206
|
+
if (c.minHeight !== undefined || c.maxHeight !== undefined) {
|
|
207
|
+
sheet.readsHeight = true;
|
|
208
|
+
}
|
|
209
|
+
if (c.minAspect !== undefined || c.maxAspect !== undefined) {
|
|
210
|
+
sheet.readsHeight = true;
|
|
211
|
+
sheet.readsWidth = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
for (const conditions of under ?? []) note(conditions);
|
|
189
216
|
// `@import` counts only ahead of every other rule, `@charset` aside
|
|
190
217
|
let importsAllowed = true;
|
|
191
218
|
|
|
@@ -233,10 +260,12 @@ export function parseStylesheet(
|
|
|
233
260
|
const name = at.name.toLowerCase();
|
|
234
261
|
if (name === 'charset') continue;
|
|
235
262
|
if (name === 'import') {
|
|
236
|
-
if (importsAllowed && media ===
|
|
263
|
+
if (importsAllowed && media === under) {
|
|
237
264
|
const url = importUrl(at.prelude);
|
|
238
|
-
|
|
265
|
+
const conditions = importConditions(at.prelude);
|
|
266
|
+
if (url && conditions !== false) {
|
|
239
267
|
sheet.imports.push(resolveUrl(url, base));
|
|
268
|
+
sheet.importConditions.push(conditions);
|
|
240
269
|
}
|
|
241
270
|
}
|
|
242
271
|
continue;
|
|
@@ -248,17 +277,7 @@ export function parseStylesheet(
|
|
|
248
277
|
}
|
|
249
278
|
if (name === 'media' && at.block !== null) {
|
|
250
279
|
const conditions = parseMediaQuery(at.prelude);
|
|
251
|
-
|
|
252
|
-
if (c.min !== undefined) breakpoints.add(c.min);
|
|
253
|
-
if (c.max !== undefined) breakpoints.add(c.max + MAX_EDGE);
|
|
254
|
-
if (c.minHeight !== undefined || c.maxHeight !== undefined) {
|
|
255
|
-
sheet.readsHeight = true;
|
|
256
|
-
}
|
|
257
|
-
if (c.minAspect !== undefined || c.maxAspect !== undefined) {
|
|
258
|
-
sheet.readsHeight = true;
|
|
259
|
-
sheet.readsWidth = true;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
280
|
+
note(conditions);
|
|
262
281
|
// A nested `@media` intersects with the one above it; pushing a
|
|
263
282
|
// level rather than merging keeps "all of these blocks hold" exact
|
|
264
283
|
// when two of them overlap.
|
|
@@ -414,7 +433,7 @@ export function parseStylesheet(
|
|
|
414
433
|
}
|
|
415
434
|
};
|
|
416
435
|
|
|
417
|
-
walk(text,
|
|
436
|
+
walk(text, under);
|
|
418
437
|
sheet.breakpoints = [...breakpoints].sort((a, b) => a - b);
|
|
419
438
|
return sheet;
|
|
420
439
|
}
|
|
@@ -1591,18 +1610,28 @@ function importUrl(prelude: string): string | null {
|
|
|
1591
1610
|
return m ? m[1] : null;
|
|
1592
1611
|
}
|
|
1593
1612
|
|
|
1594
|
-
/**
|
|
1595
|
-
*
|
|
1596
|
-
*
|
|
1597
|
-
*
|
|
1598
|
-
|
|
1613
|
+
/**
|
|
1614
|
+
* An `@import`'s media query list, the rest of its prelude after the URL:
|
|
1615
|
+
* null where it has none, and false where no query in it can ever hold
|
|
1616
|
+
* here — a list that only names other media, `print` or `braille` — which
|
|
1617
|
+
* leaves the sheet out, unasked for (CSS 2.1 6.3). Any other list is the
|
|
1618
|
+
* condition the sheet it imports is under (CSS Cascade 4, 2): the sheet is
|
|
1619
|
+
* fetched once, and its rules hold at the widths the list does. Taken to
|
|
1620
|
+
* hold at every width, as it was, a design that imports a sheet a
|
|
1621
|
+
* breakpoint — the Zen Garden's 219, `@import url("219-1367.css") all and
|
|
1622
|
+
* (min-width: 1367px)` — had all of them at once.
|
|
1623
|
+
*/
|
|
1624
|
+
function importConditions(prelude: string): MediaCondition[] | null | false {
|
|
1599
1625
|
const m =
|
|
1600
1626
|
/^\s*(?:url\(\s*(?:"[^"]*"|'[^']*'|[^)]*)\s*\)|"[^"]*"|'[^']*')(.*)$/s.exec(
|
|
1601
1627
|
prelude,
|
|
1602
1628
|
);
|
|
1603
1629
|
const list = m?.[1].trim();
|
|
1604
|
-
if (!list) return
|
|
1605
|
-
|
|
1630
|
+
if (!list) return null;
|
|
1631
|
+
const conditions = parseMediaQuery(list);
|
|
1632
|
+
if (!conditions.some((c) => c.staticPass !== false)) return false;
|
|
1633
|
+
// one that always holds is no condition
|
|
1634
|
+
return conditions.some((c) => c.staticPass === true) ? null : conditions;
|
|
1606
1635
|
}
|
|
1607
1636
|
|
|
1608
1637
|
/**
|
|
@@ -1684,10 +1713,13 @@ const GENERIC_FAMILIES = new Set([
|
|
|
1684
1713
|
]);
|
|
1685
1714
|
|
|
1686
1715
|
/**
|
|
1687
|
-
* A `@font-face` block, or null when it names no family
|
|
1688
|
-
*
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1716
|
+
* A `@font-face` block, or null when it names no family or no source — a
|
|
1717
|
+
* rule CSS Fonts 4 (4.3) drops whole. A rule whose sources are all
|
|
1718
|
+
* `local()`s is a rule like any other: its family is the document's, and
|
|
1719
|
+
* left out here, `"GeistSans Fallback"` — next/font's `src: local("Arial")`
|
|
1720
|
+
* with the metrics adjusted — reached the text engine as a name to look up,
|
|
1721
|
+
* where fontconfig's nearest guess for a family nobody has set a site's
|
|
1722
|
+
* headings in Gill Sans Ultra Bold.
|
|
1691
1723
|
*/
|
|
1692
1724
|
function parseFontFace(
|
|
1693
1725
|
block: string,
|
|
@@ -1720,6 +1752,14 @@ function parseFontFace(
|
|
|
1720
1752
|
const sources: FontFaceSource[] = [];
|
|
1721
1753
|
for (const part of splitTopLevel(src, ',')) {
|
|
1722
1754
|
const item = part.trim();
|
|
1755
|
+
// `local(Gentium Bold)`, `local("Gentium Bold")`: a name as a family's
|
|
1756
|
+
// is written, and like one never a keyword (`local(inherit)`)
|
|
1757
|
+
const local = /^local\(([\s\S]*)\)$/i.exec(item);
|
|
1758
|
+
if (local) {
|
|
1759
|
+
const name = faceFamily(local[1]);
|
|
1760
|
+
if (name) sources.push({ local: name });
|
|
1761
|
+
continue;
|
|
1762
|
+
}
|
|
1723
1763
|
if (!/^url\(/i.test(item)) continue;
|
|
1724
1764
|
const end = urlEnd(item, 3);
|
|
1725
1765
|
const url = parseUrl(item.slice(0, end));
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// What a style sheet says of the shapes in a drawing.
|
|
2
|
+
//
|
|
3
|
+
// An inline `<svg>` is a replaced box: the box builder stops at it, and no
|
|
4
|
+
// computed style is made for what is inside. `fill`, `stroke` and their kin
|
|
5
|
+
// are properties all the same (SVG 2, 6.2 and 13.2), which a rule of the
|
|
6
|
+
// document's sets on a `<path>` as it would on a paragraph — a logo's
|
|
7
|
+
// `.dark .logo path { fill: #fff }` — and a drawing exported with a style
|
|
8
|
+
// sheet of its own, `.st0 { fill: #fff }` on `<path class="st0">`, has all
|
|
9
|
+
// its colours in one. So the cascade is run for them narrowly
|
|
10
|
+
// (`Cascade.shapeStyles`): over the rules that declare one of the
|
|
11
|
+
// properties here, and down to values `SvgView` reads from a `style`
|
|
12
|
+
// attribute, which is where the drawing's copy of an element carries them
|
|
13
|
+
// (`svg.ts`). A `ComputedStyle` per `<path>` would be a few hundred
|
|
14
|
+
// properties for the dozen a shape has, on a page of hundreds of icons.
|
|
15
|
+
import type { Element } from 'domhandler';
|
|
16
|
+
import { LIGHT_DARK, lightDark } from './color.js';
|
|
17
|
+
import { inkColor, parseAlpha, parseColor, parseLength } from './values.js';
|
|
18
|
+
import type { UnitContext } from './values.js';
|
|
19
|
+
|
|
20
|
+
/** What the rules give one element of a drawing: each property they set,
|
|
21
|
+
* as `SvgView` reads it — `inherit` for one they put back to what the
|
|
22
|
+
* element inherits, over its own attribute. */
|
|
23
|
+
export type ShapeStyle = Readonly<Record<string, string>>;
|
|
24
|
+
|
|
25
|
+
/** What the rules give the elements of one drawing, and all of it as a
|
|
26
|
+
* string: what a drawing's tree was last made with (`SvgDrawing`). */
|
|
27
|
+
export interface ShapeStyles {
|
|
28
|
+
readonly of: ReadonlyMap<Element, ShapeStyle>;
|
|
29
|
+
readonly key: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Each property read, and whether it is inherited. */
|
|
33
|
+
const PROPS: Record<string, boolean> = {
|
|
34
|
+
fill: true,
|
|
35
|
+
stroke: true,
|
|
36
|
+
'stroke-width': true,
|
|
37
|
+
'stroke-linecap': true,
|
|
38
|
+
'stroke-linejoin': true,
|
|
39
|
+
'stroke-miterlimit': true,
|
|
40
|
+
'fill-rule': true,
|
|
41
|
+
'fill-opacity': true,
|
|
42
|
+
'stroke-opacity': true,
|
|
43
|
+
color: true,
|
|
44
|
+
visibility: true,
|
|
45
|
+
opacity: false,
|
|
46
|
+
display: false,
|
|
47
|
+
'stop-color': false,
|
|
48
|
+
'stop-opacity': false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Whether a rule with this declaration is one to keep for drawings. */
|
|
52
|
+
export function isShapeProp(prop: string): boolean {
|
|
53
|
+
return PROPS[prop] !== undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The properties the drawing's box has from its computed style already:
|
|
57
|
+
* its paint, which `paintSvg` hands the drawing, and what makes it a box,
|
|
58
|
+
* shown and see-through as one. */
|
|
59
|
+
export const ROOT_BOX_PROPS: ReadonlySet<string> = new Set([
|
|
60
|
+
'fill',
|
|
61
|
+
'stroke',
|
|
62
|
+
'color',
|
|
63
|
+
'display',
|
|
64
|
+
'visibility',
|
|
65
|
+
'opacity',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
/** Each at its initial value (SVG 2, 13; CSS Color 4; CSS Display 3). */
|
|
69
|
+
const INITIAL: Record<string, string> = {
|
|
70
|
+
fill: '#000000',
|
|
71
|
+
stroke: 'none',
|
|
72
|
+
'stroke-width': '1',
|
|
73
|
+
'stroke-linecap': 'butt',
|
|
74
|
+
'stroke-linejoin': 'miter',
|
|
75
|
+
'stroke-miterlimit': '4',
|
|
76
|
+
'fill-rule': 'nonzero',
|
|
77
|
+
'fill-opacity': '1',
|
|
78
|
+
'stroke-opacity': '1',
|
|
79
|
+
visibility: 'visible',
|
|
80
|
+
opacity: '1',
|
|
81
|
+
display: 'inline',
|
|
82
|
+
'stop-color': '#000000',
|
|
83
|
+
'stop-opacity': '1',
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** The elements a rule's subject can be, where it names one by its type,
|
|
87
|
+
* for the rule to reach into a drawing: the ones `SvgView` draws, or
|
|
88
|
+
* reads a paint from. `a` is both languages'. */
|
|
89
|
+
export const SHAPE_TAGS: ReadonlySet<string> = new Set([
|
|
90
|
+
'svg',
|
|
91
|
+
'g',
|
|
92
|
+
'a',
|
|
93
|
+
'path',
|
|
94
|
+
'rect',
|
|
95
|
+
'circle',
|
|
96
|
+
'ellipse',
|
|
97
|
+
'line',
|
|
98
|
+
'polyline',
|
|
99
|
+
'polygon',
|
|
100
|
+
'text',
|
|
101
|
+
'tspan',
|
|
102
|
+
'use',
|
|
103
|
+
'symbol',
|
|
104
|
+
'switch',
|
|
105
|
+
'image',
|
|
106
|
+
'stop',
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
/** What a shape's values are read against: the drawing's own `color` and
|
|
110
|
+
* colour scheme, and its font size for a length in `em`. */
|
|
111
|
+
export interface ShapeContext {
|
|
112
|
+
color: string;
|
|
113
|
+
scheme: 'light' | 'dark';
|
|
114
|
+
units: UnitContext;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A `fill` or a `stroke` (SVG 2, 13.2): `none`, a colour, or a `url()`
|
|
119
|
+
* naming a gradient in the drawing, with what follows it, its fallback,
|
|
120
|
+
* left off. Null for anything else, `context-fill` and `context-stroke`
|
|
121
|
+
* among it, which only a marker or a `<use>` has a context for.
|
|
122
|
+
*/
|
|
123
|
+
export function svgPaint(value: string): string | null {
|
|
124
|
+
const v = value.trim();
|
|
125
|
+
if (v.toLowerCase() === 'none') return 'none';
|
|
126
|
+
const url = /^url\(\s*(['"]?)(#[^'")\s]+)\1\s*\)/i.exec(v);
|
|
127
|
+
if (url) return `url(${url[2]})`;
|
|
128
|
+
return parseColor(v);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const NUMBER = /^[+-]?(?:\d*\.\d+|\d+)(?:e[+-]?\d+)?$/i;
|
|
132
|
+
|
|
133
|
+
/** A word of a `display` that makes a box (CSS Display 3, 2). */
|
|
134
|
+
const DISPLAY =
|
|
135
|
+
/^(?:block|inline|run-in|flow|flow-root|table|flex|grid|ruby|list-item|contents|inline-[a-z]+|table-[a-z-]+|ruby-[a-z-]+|-webkit-[a-z-]+)$/;
|
|
136
|
+
|
|
137
|
+
const LINE_JOINS: Record<string, string> = {
|
|
138
|
+
miter: 'miter',
|
|
139
|
+
round: 'round',
|
|
140
|
+
bevel: 'bevel',
|
|
141
|
+
// the two a canvas has no join for end in a miter, clipped or not
|
|
142
|
+
'miter-clip': 'miter',
|
|
143
|
+
arcs: 'miter',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* One declaration's value as a drawing reads it, or null for a value the
|
|
148
|
+
* property does not take, which leaves the declaration out of the cascade
|
|
149
|
+
* as any invalid one is left. `inherit` for a CSS-wide keyword that puts an
|
|
150
|
+
* inherited property back to what the element inherits.
|
|
151
|
+
*/
|
|
152
|
+
export function shapeValue(
|
|
153
|
+
prop: string,
|
|
154
|
+
raw: string,
|
|
155
|
+
ctx: ShapeContext,
|
|
156
|
+
): string | null {
|
|
157
|
+
let value = raw.trim();
|
|
158
|
+
const wide = value.toLowerCase();
|
|
159
|
+
if (wide === 'inherit') return PROPS[prop] ? 'inherit' : null;
|
|
160
|
+
if (wide === 'initial') {
|
|
161
|
+
return prop === 'color'
|
|
162
|
+
? (ctx.units.initial?.color ?? '#000000')
|
|
163
|
+
: INITIAL[prop];
|
|
164
|
+
}
|
|
165
|
+
if (wide === 'unset' || wide === 'revert' || wide === 'revert-layer') {
|
|
166
|
+
return PROPS[prop] ? 'inherit' : INITIAL[prop];
|
|
167
|
+
}
|
|
168
|
+
if (LIGHT_DARK.test(value)) {
|
|
169
|
+
const picked = lightDark(value, ctx.scheme);
|
|
170
|
+
if (picked === null) return null;
|
|
171
|
+
value = picked;
|
|
172
|
+
}
|
|
173
|
+
switch (prop) {
|
|
174
|
+
case 'fill':
|
|
175
|
+
case 'stroke': {
|
|
176
|
+
const paint = svgPaint(value);
|
|
177
|
+
// `currentColor` is the shape's own `color`, which the drawing knows
|
|
178
|
+
return paint === null || paint === 'currentColor'
|
|
179
|
+
? paint
|
|
180
|
+
: inkColor(paint, ctx.color);
|
|
181
|
+
}
|
|
182
|
+
case 'color':
|
|
183
|
+
case 'stop-color': {
|
|
184
|
+
const color = parseColor(value);
|
|
185
|
+
return color === null ? null : inkColor(color, ctx.color);
|
|
186
|
+
}
|
|
187
|
+
case 'stroke-width': {
|
|
188
|
+
// a number is user units, as a length in pixels is; a percentage is
|
|
189
|
+
// of the viewport's diagonal, which only the drawing has
|
|
190
|
+
const px = NUMBER.test(value)
|
|
191
|
+
? Number(value)
|
|
192
|
+
: lengthIn(value, ctx.units);
|
|
193
|
+
return px !== null && px >= 0 ? String(px) : null;
|
|
194
|
+
}
|
|
195
|
+
case 'stroke-miterlimit': {
|
|
196
|
+
const n = NUMBER.test(value) ? Number(value) : NaN;
|
|
197
|
+
return n >= 1 ? String(n) : null;
|
|
198
|
+
}
|
|
199
|
+
case 'fill-opacity':
|
|
200
|
+
case 'stroke-opacity':
|
|
201
|
+
case 'stop-opacity':
|
|
202
|
+
case 'opacity': {
|
|
203
|
+
const a = parseAlpha(value);
|
|
204
|
+
return a === null ? null : String(a);
|
|
205
|
+
}
|
|
206
|
+
case 'stroke-linecap': {
|
|
207
|
+
const v = value.toLowerCase();
|
|
208
|
+
return v === 'butt' || v === 'round' || v === 'square' ? v : null;
|
|
209
|
+
}
|
|
210
|
+
case 'stroke-linejoin':
|
|
211
|
+
return LINE_JOINS[value.toLowerCase()] ?? null;
|
|
212
|
+
case 'fill-rule': {
|
|
213
|
+
const v = value.toLowerCase();
|
|
214
|
+
return v === 'nonzero' || v === 'evenodd' ? v : null;
|
|
215
|
+
}
|
|
216
|
+
case 'visibility': {
|
|
217
|
+
const v = value.toLowerCase();
|
|
218
|
+
if (v === 'visible') return v;
|
|
219
|
+
return v === 'hidden' || v === 'collapse' ? 'hidden' : null;
|
|
220
|
+
}
|
|
221
|
+
case 'display': {
|
|
222
|
+
// `none`, or some box: which one is nothing to a shape
|
|
223
|
+
const v = value.toLowerCase();
|
|
224
|
+
if (v === 'none') return v;
|
|
225
|
+
return v.split(/\s+/).every((word) => DISPLAY.test(word))
|
|
226
|
+
? 'inline'
|
|
227
|
+
: null;
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** A length in user units, which are CSS pixels; null for none, and for a
|
|
235
|
+
* percentage. */
|
|
236
|
+
function lengthIn(value: string, units: UnitContext): number | null {
|
|
237
|
+
const length = parseLength(value, units);
|
|
238
|
+
return typeof length === 'number' ? length / units.scale : null;
|
|
239
|
+
}
|