buninu-browser 0.2.3 → 0.2.4
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/lib/cdp-server.js +47 -14
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +15 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +19 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +9 -6
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +213 -91
- package/lib/media/byte-pipe.js +2 -2
- package/lib/media/position-audio.js +10 -1
- package/lib/media/source-snapshots.js +126 -0
- package/lib/media/video-keyframe.js +17 -4
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +8 -28
- package/lib/paint/frame-cache.js +25 -0
- package/lib/paint/frame.js +42 -13
- package/lib/paint/paragraph-text.js +24 -0
- package/lib/paint/raster-readback.js +41 -0
- package/lib/paint/rounded-rect.js +82 -0
- package/lib/paint/shared-frame-pool.js +105 -0
- package/lib/render-tree/index.js +38 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +233 -80
- package/lib/renderer/broadcast-channel.js +76 -0
- package/lib/renderer/canvas-2d.js +257 -14
- package/lib/renderer/canvas-text.js +120 -0
- package/lib/renderer/compositor.js +13 -11
- package/lib/renderer/custom-element-reactions.js +25 -1
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/host.js +28 -4
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +30 -4
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +270 -98
- package/lib/renderer/page-renderer.js +647 -235
- package/lib/renderer/prepared-script-cache.js +50 -0
- package/lib/renderer/rendered-text.js +108 -0
- package/lib/renderer/rewrite-worker.js +4 -3
- package/lib/renderer/script-rewrite.js +13 -2
- package/lib/renderer/scroll-into-view.js +28 -0
- package/lib/renderer/slot-assignment.js +60 -0
- package/lib/renderer/video-presentation.js +23 -4
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +611 -104
- package/lib/style/selector-traits.js +49 -5
- package/package.json +1 -1
- package/test/broadcast-channel.test.js +26 -0
- package/test/canvas-2d.test.js +83 -2
- package/test/canvas-paint.test.js +9 -0
- package/test/canvas-text.test.js +41 -0
- package/test/cdp-stream.test.js +56 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +258 -1
- package/test/display-list.test.js +12 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/fetch-lifecycle.test.js +161 -0
- package/test/fixtures/canvas-paint.js +117 -0
- package/test/fixtures/canvas-text.js +42 -0
- package/test/fixtures/image-lifetime.js +90 -0
- package/test/fixtures/media-seek-snapshot.js +77 -0
- package/test/fixtures/mse-audio-chirp.webm +0 -0
- package/test/fixtures/mse-switch-large.mp4 +0 -0
- package/test/fixtures/mse-switch-small.mp4 +0 -0
- package/test/fixtures/renderer-memory.js +110 -0
- package/test/fixtures/summary-style.js +28 -0
- package/test/font-loading.test.js +97 -0
- package/test/grid.test.js +58 -0
- package/test/hit-test.test.js +21 -1
- package/test/image-lifetime.test.js +12 -0
- package/test/image-resources.test.js +94 -0
- package/test/local-media.test.js +84 -0
- package/test/media-seek-snapshot.test.js +11 -0
- package/test/media-source-config.test.js +98 -0
- package/test/media-source-webm-accounting.test.js +98 -0
- package/test/media-source-webm.test.js +108 -0
- package/test/navigation.test.js +1 -0
- package/test/network-service.test.js +388 -0
- package/test/page-scripts.test.js +1 -1
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/position-audio.test.js +24 -0
- package/test/positioning.test.js +22 -0
- package/test/prepared-script-cache.test.js +45 -0
- package/test/pulse-client.test.js +10 -3
- package/test/raster-readback.test.js +40 -0
- package/test/render-tree.test.js +28 -0
- package/test/renderer-memory.test.js +11 -0
- package/test/selector-traits.test.js +24 -0
- package/test/shared-frame-pool.test.js +63 -0
- package/test/style-sheets.test.js +51 -0
- package/test/svg.test.js +29 -0
- package/test/text-layout.test.js +106 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +69 -0
- package/tools/browser-lab.js +78 -0
- package/tools/browser-soak.js +75 -0
- package/tools/network-reference.js +49 -0
- package/tools/process-memory.js +38 -0
- package/tools/script-benchmark.js +25 -0
- package/ai-debug.log +0 -5537
|
@@ -15,25 +15,32 @@ const BLOCK_TAGS = new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
const NONE_TAGS = new Set(["HEAD", "LINK", "META", "NOFRAMES", "SCRIPT", "STYLE", "TEMPLATE", "TITLE"]);
|
|
17
17
|
const INHERITED = new Set([
|
|
18
|
-
"color", "color-scheme", "font-family", "font-size", "font-weight", "line-height", "list-style-type", "text-align", "white-space",
|
|
19
|
-
"visibility", "overflow-wrap", "word-break", "border-spacing", "border-collapse", "pointer-events",
|
|
18
|
+
"color", "color-scheme", "font-family", "font-size", "font-weight", "font-style", "font-stretch", "line-height", "list-style-type", "text-align", "white-space",
|
|
19
|
+
"visibility", "overflow-wrap", "word-break", "border-spacing", "border-collapse", "pointer-events", "direction",
|
|
20
|
+
"fill", "stroke", "stroke-width", "fill-opacity", "stroke-opacity", "fill-rule",
|
|
20
21
|
]);
|
|
21
22
|
// Inherited properties whose computed value is copied from the parent as is.
|
|
22
23
|
// line-height is not: a unitless factor resolves against each element's own
|
|
23
24
|
// font size, so it goes through its specified form (copyProperty).
|
|
24
25
|
const INHERITED_FIELDS = new Map([
|
|
25
26
|
["color", "color"], ["font-family", "fontFamily"], ["font-size", "fontSize"], ["font-weight", "fontWeight"],
|
|
27
|
+
["font-style", "fontStyle"],
|
|
28
|
+
["font-stretch", "fontStretch"],
|
|
26
29
|
["list-style-type", "listStyleType"], ["text-align", "textAlign"], ["white-space", "whiteSpace"],
|
|
27
30
|
["visibility", "visibility"], ["overflow-wrap", "overflowWrap"], ["word-break", "wordBreak"],
|
|
28
31
|
["border-spacing", "borderSpacing"], ["border-collapse", "borderCollapse"], ["pointer-events", "pointerEvents"],
|
|
32
|
+
["direction", "direction"],
|
|
33
|
+
["fill", "fill"], ["stroke", "stroke"], ["stroke-width", "strokeWidth"],
|
|
34
|
+
["fill-opacity", "fillOpacity"], ["stroke-opacity", "strokeOpacity"], ["fill-rule", "fillRule"],
|
|
29
35
|
]);
|
|
30
36
|
const LIST_STYLE_TYPES = new Set([
|
|
31
37
|
"none", "disc", "circle", "square", "decimal", "decimal-leading-zero",
|
|
32
38
|
"lower-alpha", "upper-alpha", "lower-latin", "upper-latin", "lower-roman", "upper-roman",
|
|
39
|
+
"disclosure-open", "disclosure-closed",
|
|
33
40
|
]);
|
|
34
41
|
const DISPLAYS = new Set([
|
|
35
42
|
"-webkit-box", "-webkit-inline-box",
|
|
36
|
-
"none", "block", "flex", "grid", "inline", "inline-block", "inline-flex", "inline-grid", "list-item",
|
|
43
|
+
"none", "contents", "block", "flex", "grid", "inline", "inline-block", "inline-flex", "inline-grid", "list-item",
|
|
37
44
|
"table", "inline-table", "table-row-group", "table-header-group", "table-footer-group", "table-row",
|
|
38
45
|
"table-cell", "table-caption", "table-column", "table-column-group",
|
|
39
46
|
]);
|
|
@@ -41,6 +48,8 @@ const DISPLAYS = new Set([
|
|
|
41
48
|
const FONT_SIZE_KEYWORDS = Object.freeze({
|
|
42
49
|
"xx-small": 9, "x-small": 10, small: 13, medium: 16, large: 18, "x-large": 24, "xx-large": 32, "xxx-large": 48,
|
|
43
50
|
});
|
|
51
|
+
const FONT_STRETCH_KEYWORDS = Object.freeze({ "ultra-condensed": 50, "extra-condensed": 62.5, condensed: 75,
|
|
52
|
+
"semi-condensed": 87.5, normal: 100, "semi-expanded": 112.5, expanded: 125, "extra-expanded": 150, "ultra-expanded": 200 });
|
|
44
53
|
/** HTML §15.3.8 table elements and their display types. */
|
|
45
54
|
const TABLE_DISPLAYS = new Map(Object.entries({
|
|
46
55
|
TABLE: "table", CAPTION: "table-caption", COLGROUP: "table-column-group", COL: "table-column",
|
|
@@ -57,6 +66,8 @@ const BACKGROUND_REPEATS = new Set(["repeat", "repeat-x", "repeat-y", "no-repeat
|
|
|
57
66
|
const BACKGROUND_BOXES = new Set(["border-box", "padding-box", "content-box"]);
|
|
58
67
|
const BACKGROUND_ATTACHMENTS = new Set(["scroll", "fixed", "local"]);
|
|
59
68
|
const BOX_SIDES = Object.freeze(["top", "right", "bottom", "left"]);
|
|
69
|
+
const CORNER_FIELDS = Object.freeze(["borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", "borderBottomLeftRadius"]);
|
|
70
|
+
const ZERO_RADIUS = Object.freeze([0, 0]);
|
|
60
71
|
const WHITE_SPACES = new Set(["normal", "nowrap", "pre", "pre-wrap"]);
|
|
61
72
|
// CSS Cascade 4 §7.3 CSS-wide keywords; revert is treated as unset (no user/UA origin split).
|
|
62
73
|
const CSS_WIDE_KEYWORDS = new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
@@ -68,20 +79,27 @@ const PROPERTY_FIELDS = Object.freeze({
|
|
|
68
79
|
"overflow-x": ["overflowX", "overflowScrollX"],
|
|
69
80
|
"overflow-y": ["overflowY", "overflowScrollY"],
|
|
70
81
|
background: ["backgroundColor", "backgroundImage", "backgroundPosition", "backgroundSize", "backgroundRepeat"],
|
|
82
|
+
font: ["fontSize", "fontFamily", "fontWeight", "fontStyle", "fontStretch", "lineHeight", "lineHeightFactor"],
|
|
83
|
+
"font-width": ["fontStretch"],
|
|
84
|
+
"font-parts": ["lineHeight", "lineHeightFactor"],
|
|
71
85
|
flex: ["flexGrow", "flexShrink", "flexBasis"],
|
|
72
86
|
"flex-flow": ["flexDirection", "flexWrap"],
|
|
73
|
-
gap: ["rowGap", "columnGap", "gap"],
|
|
74
|
-
"grid-gap": ["rowGap", "columnGap", "gap"],
|
|
87
|
+
gap: ["rowGap", "columnGap", "gap", "rowGapNormal", "columnGapNormal"],
|
|
88
|
+
"grid-gap": ["rowGap", "columnGap", "gap", "rowGapNormal", "columnGapNormal"],
|
|
89
|
+
"row-gap": ["rowGap", "rowGapNormal"],
|
|
90
|
+
"column-gap": ["columnGap", "columnGapNormal"],
|
|
91
|
+
"grid-row-gap": ["rowGap", "rowGapNormal"],
|
|
92
|
+
"grid-column-gap": ["columnGap", "columnGapNormal"],
|
|
75
93
|
"grid-area": ["gridRowStart", "gridColumnStart", "gridRowEnd", "gridColumnEnd"],
|
|
76
94
|
"grid-row": ["gridRowStart", "gridRowEnd"],
|
|
77
95
|
"grid-column": ["gridColumnStart", "gridColumnEnd"],
|
|
78
|
-
"grid-template": ["gridTemplateColumns", "gridTemplateRows"],
|
|
96
|
+
"grid-template": ["gridTemplateColumns", "gridTemplateRows", "gridTemplateAreas"],
|
|
79
97
|
"place-content": ["alignContent", "justifyContent"],
|
|
80
98
|
"place-items": ["alignItems", "justifyItems"],
|
|
81
99
|
"place-self": ["alignSelf", "justifySelf"],
|
|
82
100
|
"line-height": ["lineHeight", "lineHeightFactor"],
|
|
83
101
|
"list-style": ["listStyleType"],
|
|
84
|
-
"border-radius": ["borderRadius"],
|
|
102
|
+
"border-radius": ["borderRadius", ...CORNER_FIELDS],
|
|
85
103
|
"text-decoration": ["textDecorationLine", "textDecorationColor"],
|
|
86
104
|
"text-decoration-line": ["textDecorationLine"],
|
|
87
105
|
"text-decoration-color": ["textDecorationColor"],
|
|
@@ -91,12 +109,19 @@ const BORDER_FIELDS = Object.freeze(["borderSpecifiedWidths", "borderStyles", "b
|
|
|
91
109
|
|
|
92
110
|
const INITIAL = Object.freeze({
|
|
93
111
|
display: "inline",
|
|
112
|
+
direction: "ltr",
|
|
94
113
|
// text-decoration: this element's own lines ("none" or an array) and color
|
|
95
114
|
// (null = currentColor); textDecorations adds what ancestors propagate.
|
|
96
115
|
textDecorationLine: "none",
|
|
97
116
|
textDecorationColor: null,
|
|
98
117
|
textDecorations: null,
|
|
99
118
|
color: "rgba(0, 0, 0, 1)",
|
|
119
|
+
fill: "rgba(0, 0, 0, 1)",
|
|
120
|
+
stroke: "none",
|
|
121
|
+
strokeWidth: 1,
|
|
122
|
+
fillOpacity: 1,
|
|
123
|
+
strokeOpacity: 1,
|
|
124
|
+
fillRule: "nonzero",
|
|
100
125
|
colorScheme: "light",
|
|
101
126
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
102
127
|
backgroundImage: null,
|
|
@@ -109,6 +134,8 @@ const INITIAL = Object.freeze({
|
|
|
109
134
|
fontFamily: Object.freeze(["sans-serif"]),
|
|
110
135
|
fontSize: 16,
|
|
111
136
|
fontWeight: 400,
|
|
137
|
+
fontStyle: "normal",
|
|
138
|
+
fontStretch: 100,
|
|
112
139
|
// null means `line-height: normal`, which layout resolves from font metrics.
|
|
113
140
|
lineHeight: null,
|
|
114
141
|
lineHeightFactor: null,
|
|
@@ -132,6 +159,10 @@ const INITIAL = Object.freeze({
|
|
|
132
159
|
borderStyles: Object.freeze(["none", "none", "none", "none"]),
|
|
133
160
|
borderSpecifiedWidths: Object.freeze([3, 3, 3, 3]),
|
|
134
161
|
borderRadius: 0,
|
|
162
|
+
borderTopLeftRadius: ZERO_RADIUS,
|
|
163
|
+
borderTopRightRadius: ZERO_RADIUS,
|
|
164
|
+
borderBottomRightRadius: ZERO_RADIUS,
|
|
165
|
+
borderBottomLeftRadius: ZERO_RADIUS,
|
|
135
166
|
flexDirection: "row",
|
|
136
167
|
flexWrap: "nowrap",
|
|
137
168
|
justifyContent: "normal",
|
|
@@ -141,6 +172,10 @@ const INITIAL = Object.freeze({
|
|
|
141
172
|
gap: 0,
|
|
142
173
|
rowGap: 0,
|
|
143
174
|
columnGap: 0,
|
|
175
|
+
// Layout uses zero for normal in flex/grid, but CSSOM must distinguish it
|
|
176
|
+
// from an explicitly specified zero length.
|
|
177
|
+
rowGapNormal: true,
|
|
178
|
+
columnGapNormal: true,
|
|
144
179
|
flexGrow: 0,
|
|
145
180
|
flexShrink: 1,
|
|
146
181
|
flexBasis: "auto",
|
|
@@ -150,12 +185,14 @@ const INITIAL = Object.freeze({
|
|
|
150
185
|
inset: Object.freeze(["auto", "auto", "auto", "auto"]),
|
|
151
186
|
zIndex: "auto",
|
|
152
187
|
transform: null,
|
|
188
|
+
translate: null,
|
|
153
189
|
// Raw text (default "50% 50%"); paint resolves it against the border box.
|
|
154
190
|
transformOrigin: null,
|
|
155
191
|
justifyItems: "normal",
|
|
156
192
|
justifySelf: "auto",
|
|
157
193
|
gridTemplateColumns: null,
|
|
158
194
|
gridTemplateRows: null,
|
|
195
|
+
gridTemplateAreas: null,
|
|
159
196
|
gridAutoColumns: Object.freeze([AUTO_TRACK]),
|
|
160
197
|
gridAutoRows: Object.freeze([AUTO_TRACK]),
|
|
161
198
|
gridAutoFlow: Object.freeze({ column: false, dense: false }),
|
|
@@ -189,6 +226,18 @@ const INITIAL = Object.freeze({
|
|
|
189
226
|
clipPath: null,
|
|
190
227
|
});
|
|
191
228
|
|
|
229
|
+
// `all` resets every supported CSS property except direction (and custom
|
|
230
|
+
// properties). Derived rendering fields are recomputed from their owners.
|
|
231
|
+
const RESET_EXCLUDED_FIELDS = new Set([
|
|
232
|
+
"direction", "textDecorations", "lineHeightFactor", "borderWidths", "borderSpecifiedWidths",
|
|
233
|
+
"borderStyles", "borderColors", "inset", "overflowScrollX", "overflowScrollY",
|
|
234
|
+
"rowGapNormal", "columnGapNormal",
|
|
235
|
+
]);
|
|
236
|
+
const ALL_PROPERTIES = Object.freeze(Object.keys(INITIAL)
|
|
237
|
+
.filter((field) => !RESET_EXCLUDED_FIELDS.has(field))
|
|
238
|
+
.map((field) => field.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`))
|
|
239
|
+
.concat(["border-width", "border-style", "border-color", "top", "right", "bottom", "left"]));
|
|
240
|
+
|
|
192
241
|
/**
|
|
193
242
|
* User-agent defaults from the HTML Standard's suggested rendering rules
|
|
194
243
|
* (WHATWG HTML §15.3, CC BY 4.0) for the elements the renderer lays out.
|
|
@@ -243,6 +292,7 @@ const UA_CONTROL_DECLARATIONS = Object.freeze({
|
|
|
243
292
|
select: parseDeclarations(`${FIELD}; border-radius: 2px; padding: 0 4px; white-space: pre`),
|
|
244
293
|
});
|
|
245
294
|
const BUTTON_INPUTS = new Set(["button", "submit", "reset", "image", "file", "color"]);
|
|
295
|
+
const SVG_PRESENTATION_PROPERTIES = ["fill", "stroke", "stroke-width", "fill-opacity", "stroke-opacity", "fill-rule", "color", "opacity", "display", "visibility"];
|
|
246
296
|
|
|
247
297
|
/** Which UA control defaults an element gets, or null. */
|
|
248
298
|
export function controlKind(element) {
|
|
@@ -271,6 +321,7 @@ const DEFAULT_ENVIRONMENT = Object.freeze({ viewportWidth: 800, viewportHeight:
|
|
|
271
321
|
let environment = DEFAULT_ENVIRONMENT;
|
|
272
322
|
|
|
273
323
|
export class StyleEngine {
|
|
324
|
+
#measureText;
|
|
274
325
|
#styles = new WeakMap();
|
|
275
326
|
#customProperties = new WeakMap();
|
|
276
327
|
#pseudoStyles = new WeakMap();
|
|
@@ -286,6 +337,10 @@ export class StyleEngine {
|
|
|
286
337
|
#parsedSheets = new Map();
|
|
287
338
|
#parsedSheetsKey = "";
|
|
288
339
|
|
|
340
|
+
constructor({ measureText = null } = {}) {
|
|
341
|
+
this.#measureText = measureText;
|
|
342
|
+
}
|
|
343
|
+
|
|
289
344
|
/**
|
|
290
345
|
* Computes the document's styles. With `dirty` (elements whose subtree's
|
|
291
346
|
* styles may have changed), only those subtrees are restyled and every
|
|
@@ -300,6 +355,7 @@ export class StyleEngine {
|
|
|
300
355
|
viewportHeight: viewport.height ?? DEFAULT_ENVIRONMENT.viewportHeight,
|
|
301
356
|
rootFontSize: DEFAULT_ENVIRONMENT.rootFontSize,
|
|
302
357
|
mobile: viewport.mobile === true,
|
|
358
|
+
measureText: this.#measureText,
|
|
303
359
|
};
|
|
304
360
|
this.restyled = 0;
|
|
305
361
|
try {
|
|
@@ -643,9 +699,37 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
643
699
|
else if (BLOCK_TAGS.has(element.tagName)) values.display = element.tagName === "LI" ? "list-item" : "block";
|
|
644
700
|
else if (["BUTTON", "INPUT", "SELECT", "TEXTAREA"].includes(element.tagName)) values.display = "inline-block";
|
|
645
701
|
if (["B", "STRONG"].includes(element.tagName)) values.fontWeight = 700;
|
|
702
|
+
if (["I", "EM", "CITE", "DFN", "VAR", "ADDRESS"].includes(element.tagName)) values.fontStyle = "italic";
|
|
646
703
|
if (element.tagName === "PRE") values.whiteSpace = "pre";
|
|
704
|
+
if (element.tagName === "SUMMARY") {
|
|
705
|
+
const details = element.parentElement;
|
|
706
|
+
const principal = details?.tagName === "DETAILS"
|
|
707
|
+
&& [...details.children].find((child) => child.tagName === "SUMMARY") === element;
|
|
708
|
+
values.display = principal ? "list-item" : "block";
|
|
709
|
+
if (principal) values.listStyleType = details.hasAttribute("open") ? "disclosure-open" : "disclosure-closed";
|
|
710
|
+
}
|
|
711
|
+
const direction = element.getAttribute("dir")?.toLowerCase();
|
|
712
|
+
if (direction === "ltr" || direction === "rtl") values.direction = direction;
|
|
647
713
|
|
|
648
714
|
const declarations = new Map(authorDeclarations ?? []);
|
|
715
|
+
// HTML dimension attributes are presentational hints, below author CSS.
|
|
716
|
+
// Percentages are supported by the legacy dimension parsing rules, even
|
|
717
|
+
// though the modern authoring syntax normally uses non-negative integers.
|
|
718
|
+
if (["IMG", "VIDEO", "IFRAME"].includes(element.tagName)) {
|
|
719
|
+
for (const name of ["width", "height"]) {
|
|
720
|
+
const match = /^[\t\n\f\r ]*(\d+(?:\.\d+)?)(%?)/.exec(element.getAttribute(name) ?? "");
|
|
721
|
+
if (match && !declarations.has(name)) declarations.set(name, {
|
|
722
|
+
name, value: `${match[1]}${match[2] || "px"}`, specificity: 0, order: -1, layer: -Infinity,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
// SVG presentation attributes precede stylesheet rules at author origin.
|
|
727
|
+
if (element.namespaceURI === "http://www.w3.org/2000/svg") {
|
|
728
|
+
for (const name of SVG_PRESENTATION_PROPERTIES) {
|
|
729
|
+
const value = element.getAttribute(name);
|
|
730
|
+
if (value != null && !declarations.has(name)) declarations.set(name, { name, value, specificity: 0, order: -1, layer: -Infinity });
|
|
731
|
+
}
|
|
732
|
+
}
|
|
649
733
|
const isLink = (element.tagName === "A" || element.tagName === "AREA") && element.hasAttribute("href");
|
|
650
734
|
const control = controlKind(element);
|
|
651
735
|
if (control === "hidden") values.display = "none";
|
|
@@ -663,23 +747,58 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
663
747
|
declarations.set(declaration.name, { ...declaration, specificity: 1_000_000, order: Infinity });
|
|
664
748
|
}
|
|
665
749
|
}
|
|
750
|
+
const reset = declarations.get("all");
|
|
751
|
+
if (reset && CSS_WIDE_KEYWORDS.has(resolveVariables(reset.value, customProperties))) {
|
|
752
|
+
// Expand before resolving font prerequisites. Competing declarations
|
|
753
|
+
// keep their normal importance, specificity and source-order priority.
|
|
754
|
+
for (const name of ALL_PROPERTIES) {
|
|
755
|
+
const current = declarations.get(name);
|
|
756
|
+
if (!current || compareDeclarations(reset, current) >= 0) declarations.set(name, { ...reset, name });
|
|
757
|
+
}
|
|
758
|
+
}
|
|
666
759
|
// The font size comes first (em lengths use it), from whichever of
|
|
667
760
|
// font-size and the font shorthand wins the cascade.
|
|
668
761
|
const fontSize = declarations.get("font-size");
|
|
669
762
|
const font = declarations.get("font");
|
|
670
763
|
if (font && (!fontSize || compareDeclarations(font, fontSize) > 0)) {
|
|
671
|
-
const
|
|
672
|
-
|
|
764
|
+
const fontValue = resolveVariables(font.value, customProperties);
|
|
765
|
+
const parsed = parseFontShorthand(fontValue);
|
|
766
|
+
if (CSS_WIDE_KEYWORDS.has(fontValue)) setProperty(values, "font-size", fontValue, parentStyle);
|
|
767
|
+
else if (parsed) setProperty(values, "font-size", parsed.size, parentStyle);
|
|
673
768
|
} else if (fontSize) {
|
|
674
769
|
applyDeclaration(values, fontSize, customProperties, parentStyle);
|
|
675
770
|
}
|
|
676
771
|
// light-dark() depends on the used scheme regardless of declaration order.
|
|
677
772
|
const colorScheme = declarations.get("color-scheme");
|
|
678
773
|
if (colorScheme) applyDeclaration(values, colorScheme, customProperties, parentStyle);
|
|
774
|
+
const directionDeclaration = declarations.get("direction");
|
|
775
|
+
if (directionDeclaration) applyDeclaration(values, directionDeclaration, customProperties, parentStyle);
|
|
679
776
|
// Apply in cascade order so a later shorthand overrides earlier longhands and vice versa.
|
|
680
777
|
const ordered = [...declarations.values()].sort(compareDeclarations);
|
|
778
|
+
// Font-relative lengths such as ch use this element's final font, even
|
|
779
|
+
// when width/padding appear before font-family or the font shorthand.
|
|
780
|
+
for (const declaration of ordered) {
|
|
781
|
+
if (declaration.name === "font") {
|
|
782
|
+
const fontValue = resolveVariables(declaration.value, customProperties);
|
|
783
|
+
const parsed = parseFontShorthand(fontValue);
|
|
784
|
+
if (CSS_WIDE_KEYWORDS.has(fontValue)) {
|
|
785
|
+
setProperty(values, "font-family", fontValue, parentStyle);
|
|
786
|
+
setProperty(values, "font-weight", fontValue, parentStyle);
|
|
787
|
+
setProperty(values, "font-style", fontValue, parentStyle);
|
|
788
|
+
setProperty(values, "font-stretch", fontValue, parentStyle);
|
|
789
|
+
}
|
|
790
|
+
if (parsed) {
|
|
791
|
+
setProperty(values, "font-family", parsed.family, parentStyle);
|
|
792
|
+
setProperty(values, "font-weight", parsed.weight ?? "normal", parentStyle);
|
|
793
|
+
setProperty(values, "font-style", parsed.style ?? "normal", parentStyle);
|
|
794
|
+
setProperty(values, "font-stretch", parsed.stretch ?? "normal", parentStyle);
|
|
795
|
+
}
|
|
796
|
+
} else if (["font-family", "font-weight", "font-style", "font-stretch", "font-width"].includes(declaration.name)) {
|
|
797
|
+
applyDeclaration(values, declaration, customProperties, parentStyle);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
681
800
|
for (const declaration of ordered) {
|
|
682
|
-
if (declaration.name.startsWith("--") ||
|
|
801
|
+
if (declaration.name.startsWith("--") || ["font-size", "font-family", "font-weight", "font-style", "font-stretch", "font-width", "color-scheme", "direction"].includes(declaration.name)) continue;
|
|
683
802
|
// The shorthand's size was settled above; its other parts apply in order.
|
|
684
803
|
applyDeclaration(values, declaration.name === "font" ? { ...declaration, name: "font-parts" } : declaration, customProperties, parentStyle);
|
|
685
804
|
}
|
|
@@ -689,7 +808,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
689
808
|
const parentDisplay = parentStyle?.display ?? "block";
|
|
690
809
|
const flexOrGridItem = ["flex", "inline-flex", "grid", "inline-grid"].includes(parentDisplay);
|
|
691
810
|
if (values.float !== "none" && (["absolute", "fixed"].includes(values.position) || flexOrGridItem)) values.float = "none";
|
|
692
|
-
if (values.float !== "none" && values.display
|
|
811
|
+
if (values.float !== "none" && !["none", "contents"].includes(values.display)) {
|
|
693
812
|
values.display = { flex: "inline-flex", grid: "inline-grid", "inline-flex": "inline-flex", "inline-grid": "inline-grid" }[values.display]
|
|
694
813
|
?? "inline-block";
|
|
695
814
|
}
|
|
@@ -866,6 +985,110 @@ export function parseStyleSheet(source, viewport = environment, counter = { orde
|
|
|
866
985
|
return rules;
|
|
867
986
|
}
|
|
868
987
|
|
|
988
|
+
/** Font resource descriptors, separate from selector rules (CSS Fonts §4). */
|
|
989
|
+
export function extractFontFaces(source, baseURL, viewport = environment) {
|
|
990
|
+
const css = String(source).replace(/\/\*[\s\S]*?\*\//g, "");
|
|
991
|
+
const faces = [];
|
|
992
|
+
function visit(start, end) {
|
|
993
|
+
let index = start;
|
|
994
|
+
while (index < end) {
|
|
995
|
+
const open = findTopLevel(css, "{", index, end);
|
|
996
|
+
const semicolon = findTopLevel(css, ";", index, end);
|
|
997
|
+
if (semicolon !== -1 && (open === -1 || semicolon < open)) {
|
|
998
|
+
index = semicolon + 1;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (open === -1) break;
|
|
1002
|
+
const close = matchingBrace(css, open, end);
|
|
1003
|
+
const prelude = css.slice(index, open).trim();
|
|
1004
|
+
const name = /^@([\w-]+)/.exec(prelude)?.[1]?.toLowerCase();
|
|
1005
|
+
if (name === "font-face") {
|
|
1006
|
+
const descriptors = parseDeclarations(css.slice(open + 1, close));
|
|
1007
|
+
const value = (name) => descriptors.get(name)?.important ? undefined : descriptors.get(name)?.value;
|
|
1008
|
+
const family = fontFaceFamily(value("font-family"));
|
|
1009
|
+
const sources = fontFaceSources(value("src"), baseURL);
|
|
1010
|
+
if (family && sources.length) {
|
|
1011
|
+
const weights = (value("font-weight") ?? "normal").split(/\s+/).map((part) =>
|
|
1012
|
+
part === "normal" ? 400 : part === "bold" ? 700 : Number(part));
|
|
1013
|
+
const weight = weights.length <= 2 && weights.every((part) => part >= 1 && part <= 1000)
|
|
1014
|
+
? [Math.min(...weights), Math.max(...weights)] : [400, 400];
|
|
1015
|
+
const style = /^(normal|italic|oblique)(?:\s|$)/.exec(value("font-style") ?? "normal")?.[1] ?? "normal";
|
|
1016
|
+
const stretches = (value("font-width") ?? value("font-stretch") ?? "normal").split(/\s+/).map(fontStretch);
|
|
1017
|
+
const stretch = stretches.length <= 2 && stretches.every(Number.isFinite)
|
|
1018
|
+
? [Math.min(...stretches), Math.max(...stretches)] : [100, 100];
|
|
1019
|
+
const display = value("font-display");
|
|
1020
|
+
faces.push({ family, sources, weight, style, stretch,
|
|
1021
|
+
unicodeRanges: fontFaceUnicodeRanges(value("unicode-range")),
|
|
1022
|
+
display: ["auto", "block", "swap", "fallback", "optional"].includes(display) ? display : "auto" });
|
|
1023
|
+
}
|
|
1024
|
+
} else if (name === "layer" || name === "supports"
|
|
1025
|
+
|| name === "media" && mediaQueryMatches(prelude.slice(6).trim(), viewport)) {
|
|
1026
|
+
visit(open + 1, close);
|
|
1027
|
+
}
|
|
1028
|
+
index = close + 1;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
visit(0, css.length);
|
|
1032
|
+
return faces;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function decodeCssString(value) {
|
|
1036
|
+
const text = String(value ?? "").trim();
|
|
1037
|
+
const quoted = /^(?:"((?:[^"\\]|\\[\s\S])*)"|'((?:[^'\\]|\\[\s\S])*)')$/.exec(text);
|
|
1038
|
+
return (quoted ? quoted[1] ?? quoted[2] : text).replace(/\\(?:([\da-f]{1,6})\s?|([\s\S]))/gi,
|
|
1039
|
+
(_, hex, literal) => hex ? String.fromCodePoint(Math.min(0x10ffff, parseInt(hex, 16)) || 0xfffd)
|
|
1040
|
+
: /[\r\n\f]/.test(literal) ? "" : literal);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function fontFaceFamily(value) {
|
|
1044
|
+
if (!value) return null;
|
|
1045
|
+
const text = value.trim();
|
|
1046
|
+
if (/^["']/.test(text)) {
|
|
1047
|
+
return /^(?:"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')$/.test(text) ? decodeCssString(text) : null;
|
|
1048
|
+
}
|
|
1049
|
+
if (!/^(?:-?(?:[a-z_\u0080-\uFFFF]|--)[\w\u0080-\uFFFF-]*)(?:\s+-?(?:[a-z_\u0080-\uFFFF]|--)[\w\u0080-\uFFFF-]*)*$/i.test(text)) return null;
|
|
1050
|
+
if (CSS_WIDE_KEYWORDS.has(text.toLowerCase()) || ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui"].includes(text.toLowerCase())) return null;
|
|
1051
|
+
return text.replace(/\s+/g, " ");
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function fontFaceSources(value, baseURL) {
|
|
1055
|
+
if (!value) return [];
|
|
1056
|
+
const result = [];
|
|
1057
|
+
for (const source of splitTopLevel(value, ",")) {
|
|
1058
|
+
const match = /^(url|local)\(\s*((?:"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|[^)]*?))\s*\)\s*(.*)$/i.exec(source);
|
|
1059
|
+
if (!match) continue;
|
|
1060
|
+
const target = decodeCssString(match[2]);
|
|
1061
|
+
if (!target) continue;
|
|
1062
|
+
if (match[1].toLowerCase() === "local") {
|
|
1063
|
+
if (!match[3]) result.push({ local: target });
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
let url;
|
|
1067
|
+
try { url = new URL(target, baseURL).href; } catch { continue; }
|
|
1068
|
+
const format = /^format\(\s*([^)]*)\s*\)/i.exec(match[3]);
|
|
1069
|
+
const rest = format ? match[3].slice(format[0].length).trim() : match[3];
|
|
1070
|
+
if (rest && !/^tech\([^)]*\)$/i.test(rest)) continue;
|
|
1071
|
+
result.push({ url, ...(format ? { format: decodeCssString(format[1]).toLowerCase() } : {}),
|
|
1072
|
+
...(rest ? { tech: rest.slice(5, -1).split(",").map((part) => part.trim().toLowerCase()) } : {}) });
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function fontFaceUnicodeRanges(value) {
|
|
1078
|
+
const fallback = [[0, 0x10ffff]];
|
|
1079
|
+
if (!value) return fallback;
|
|
1080
|
+
const ranges = [];
|
|
1081
|
+
for (const token of value.split(",")) {
|
|
1082
|
+
const match = /^\s*U\+([\da-f]{1,6})(?:-([\da-f]{1,6}))?\s*$|^\s*U\+([\da-f]{0,5}\?{1,6})\s*$/i.exec(token);
|
|
1083
|
+
if (!match || match[3]?.length > 6) return fallback;
|
|
1084
|
+
const from = parseInt(match[3]?.replaceAll("?", "0") ?? match[1], 16);
|
|
1085
|
+
const to = Math.min(0x10ffff, parseInt(match[3]?.replaceAll("?", "F") ?? match[2] ?? match[1], 16));
|
|
1086
|
+
if (from > to || from > 0x10ffff) return fallback;
|
|
1087
|
+
ranges.push([from, to]);
|
|
1088
|
+
}
|
|
1089
|
+
return ranges.length ? ranges : fallback;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
869
1092
|
function newLayer(parent) {
|
|
870
1093
|
return { parent, children: new Map(), rank: Infinity };
|
|
871
1094
|
}
|
|
@@ -921,13 +1144,17 @@ function rankLayerTree(root) {
|
|
|
921
1144
|
}
|
|
922
1145
|
|
|
923
1146
|
/** Splits a `font` shorthand value into size, line height, weight and family. */
|
|
924
|
-
function parseFontShorthand(value) {
|
|
1147
|
+
export function parseFontShorthand(value) {
|
|
925
1148
|
const tokens = splitTopLevel(value, " ").filter(Boolean);
|
|
926
1149
|
let weight = null;
|
|
1150
|
+
let style = "normal";
|
|
1151
|
+
let stretch = "normal";
|
|
927
1152
|
for (let index = 0; index < tokens.length; index++) {
|
|
928
1153
|
const token = tokens[index];
|
|
929
1154
|
const lower = token.toLowerCase();
|
|
930
|
-
if (
|
|
1155
|
+
if (lower === "italic" || lower === "oblique") { style = lower; continue; }
|
|
1156
|
+
if (lower !== "normal" && Object.hasOwn(FONT_STRETCH_KEYWORDS, lower)) { stretch = lower; continue; }
|
|
1157
|
+
if (["normal", "small-caps"].includes(lower)) continue;
|
|
931
1158
|
if (["bold", "bolder", "lighter"].includes(lower) || /^[1-9]00$/.test(lower)) {
|
|
932
1159
|
weight = lower === "bolder" ? "700" : lower === "lighter" ? "300" : lower;
|
|
933
1160
|
continue;
|
|
@@ -943,7 +1170,7 @@ function parseFontShorthand(value) {
|
|
|
943
1170
|
} else if (lineHeight === undefined && tokens[rest]?.startsWith("/")) lineHeight = tokens[rest++].slice(1); // "14px /1.5"
|
|
944
1171
|
const family = tokens.slice(rest).join(" ").trim();
|
|
945
1172
|
if (!family || !(FONT_SIZE_KEYWORDS[size] || /^[\d.]/.test(size) || /^(?:calc|clamp|min|max)\(/.test(size))) return null;
|
|
946
|
-
return { size, lineHeight: lineHeight || null, weight, family };
|
|
1173
|
+
return { size, lineHeight: lineHeight || null, weight, style, stretch, family };
|
|
947
1174
|
}
|
|
948
1175
|
return null;
|
|
949
1176
|
}
|
|
@@ -960,7 +1187,8 @@ function findTopLevel(source, character, start, end) {
|
|
|
960
1187
|
if (quote) {
|
|
961
1188
|
if (current === BACKSLASH) index++;
|
|
962
1189
|
else if (current === quote) quote = 0;
|
|
963
|
-
} else if (current ===
|
|
1190
|
+
} else if (current === BACKSLASH) index++;
|
|
1191
|
+
else if (current === QUOTE || current === APOSTROPHE) quote = current;
|
|
964
1192
|
else if (current === OPEN_PAREN) depth++;
|
|
965
1193
|
else if (current === CLOSE_PAREN) depth = Math.max(0, depth - 1);
|
|
966
1194
|
else if (current === target && depth === 0) return index;
|
|
@@ -977,7 +1205,8 @@ function matchingBrace(source, open, end) {
|
|
|
977
1205
|
if (quote) {
|
|
978
1206
|
if (current === BACKSLASH) index++;
|
|
979
1207
|
else if (current === quote) quote = 0;
|
|
980
|
-
} else if (current ===
|
|
1208
|
+
} else if (current === BACKSLASH) index++;
|
|
1209
|
+
else if (current === QUOTE || current === APOSTROPHE) quote = current;
|
|
981
1210
|
else if (current === OPEN_BRACE) depth++;
|
|
982
1211
|
else if (current === CLOSE_BRACE && --depth === 0) return index;
|
|
983
1212
|
}
|
|
@@ -1046,6 +1275,7 @@ function mediaLength(value) {
|
|
|
1046
1275
|
|
|
1047
1276
|
export function parseDeclarations(source) {
|
|
1048
1277
|
const declarations = new Map();
|
|
1278
|
+
let declarationIndex = 0;
|
|
1049
1279
|
for (const part of splitDeclarations(String(source))) {
|
|
1050
1280
|
const colon = part.indexOf(":");
|
|
1051
1281
|
if (colon < 1) continue;
|
|
@@ -1056,7 +1286,7 @@ export function parseDeclarations(source) {
|
|
|
1056
1286
|
const important = /\s*!important\s*$/i.test(value);
|
|
1057
1287
|
value = value.replace(/\s*!important\s*$/i, "").trim();
|
|
1058
1288
|
if (!name || !value || (declarations.get(name)?.important && !important)) continue;
|
|
1059
|
-
declarations.set(name, { name, value, important });
|
|
1289
|
+
declarations.set(name, { name, value, important, declarationIndex: declarationIndex++ });
|
|
1060
1290
|
}
|
|
1061
1291
|
return declarations;
|
|
1062
1292
|
}
|
|
@@ -1079,7 +1309,46 @@ function applyDeclaration(style, declaration, customProperties, parentStyle) {
|
|
|
1079
1309
|
}
|
|
1080
1310
|
}
|
|
1081
1311
|
|
|
1312
|
+
const LOGICAL_SIZES = Object.freeze({
|
|
1313
|
+
"inline-size": "width", "block-size": "height",
|
|
1314
|
+
"min-inline-size": "min-width", "max-inline-size": "max-width",
|
|
1315
|
+
"min-block-size": "min-height", "max-block-size": "max-height",
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1082
1318
|
function setProperty(style, name, value, parentStyle = null) {
|
|
1319
|
+
if (name === "font-width") return setProperty(style, "font-stretch", value, parentStyle);
|
|
1320
|
+
const logicalCorner = /^border-(start|end)-(start|end)-radius$/.exec(name);
|
|
1321
|
+
if (logicalCorner) {
|
|
1322
|
+
const block = logicalCorner[1] === "start" ? "top" : "bottom";
|
|
1323
|
+
const inline = (logicalCorner[2] === "start") !== (style.direction === "rtl") ? "left" : "right";
|
|
1324
|
+
return setProperty(style, `border-${block}-${inline}-radius`, value, parentStyle);
|
|
1325
|
+
}
|
|
1326
|
+
// The layout engine uses horizontal writing. Resolve logical declarations
|
|
1327
|
+
// into their physical slots in cascade order, after direction is computed.
|
|
1328
|
+
if (Object.hasOwn(LOGICAL_SIZES, name)) return setProperty(style, LOGICAL_SIZES[name], value, parentStyle);
|
|
1329
|
+
if (name.includes("-inline") || name.includes("-block")) {
|
|
1330
|
+
const logical = /^(margin|padding|inset)-(inline|block)(?:-(start|end))?$/.exec(name);
|
|
1331
|
+
if (logical) {
|
|
1332
|
+
const [, property, axis, edge] = logical;
|
|
1333
|
+
const sides = axis === "block" ? ["top", "bottom"]
|
|
1334
|
+
: style.direction === "rtl" ? ["right", "left"] : ["left", "right"];
|
|
1335
|
+
const physical = (side) => property === "inset" ? side : `${property}-${side}`;
|
|
1336
|
+
if (edge) return setProperty(style, physical(sides[edge === "start" ? 0 : 1]), value, parentStyle);
|
|
1337
|
+
if (CSS_WIDE_KEYWORDS.has(value)) {
|
|
1338
|
+
for (const side of sides) setProperty(style, physical(side), value, parentStyle);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
const parts = splitTopLevel(value, " ").filter(Boolean);
|
|
1342
|
+
if (!parts.length || parts.length > 2) return;
|
|
1343
|
+
const lengths = parts.map((part) => property !== "padding" && part === "auto" ? "auto"
|
|
1344
|
+
: computedLength(part, style, property !== "margin"));
|
|
1345
|
+
if (lengths.some((length) => length !== "auto" && !Number.isFinite(length) && !length?.unit
|
|
1346
|
+
|| property === "padding" && (typeof length === "number" ? length < 0 : length?.unit === "%" && length.value < 0))) return;
|
|
1347
|
+
setProperty(style, physical(sides[0]), parts[0], parentStyle);
|
|
1348
|
+
setProperty(style, physical(sides[1]), parts[1] ?? parts[0], parentStyle);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1083
1352
|
if (CSS_WIDE_KEYWORDS.has(value)) {
|
|
1084
1353
|
const inherits = value === "inherit" || (value !== "initial" && INHERITED.has(name));
|
|
1085
1354
|
copyProperty(style, name, inherits && parentStyle ? parentStyle : INITIAL);
|
|
@@ -1087,6 +1356,9 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1087
1356
|
}
|
|
1088
1357
|
if (typeof value !== "string") return;
|
|
1089
1358
|
switch (name) {
|
|
1359
|
+
case "direction":
|
|
1360
|
+
if (value === "ltr" || value === "rtl") style.direction = value;
|
|
1361
|
+
break;
|
|
1090
1362
|
case "color-scheme": {
|
|
1091
1363
|
const schemes = value.toLowerCase().split(/\s+/).filter((part) => part !== "only");
|
|
1092
1364
|
if (schemes.every((part) => ["normal", "light", "dark"].includes(part))) {
|
|
@@ -1123,7 +1395,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1123
1395
|
if (!parsed) return;
|
|
1124
1396
|
lines = parsed;
|
|
1125
1397
|
} else if (DECORATION_STYLES.has(lower) || lower === "auto" || lower === "from-font"
|
|
1126
|
-
|| Number.isFinite(computedLength(token, style
|
|
1398
|
+
|| Number.isFinite(computedLength(token, style, true)) || /%$/.test(token)) {
|
|
1127
1399
|
continue;
|
|
1128
1400
|
} else if (lower === "currentcolor") {
|
|
1129
1401
|
color = null;
|
|
@@ -1178,7 +1450,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1178
1450
|
if (value === "auto") style.clip = null;
|
|
1179
1451
|
const rect = /^rect\(\s*(.*)\s*\)$/i.exec(value);
|
|
1180
1452
|
if (rect) {
|
|
1181
|
-
const edges = rect[1].split(/\s*,\s*|\s+/).map((part) => (part === "auto" ? "auto" : computedLength(part, style
|
|
1453
|
+
const edges = rect[1].split(/\s*,\s*|\s+/).map((part) => (part === "auto" ? "auto" : computedLength(part, style)));
|
|
1182
1454
|
if (edges.length === 4 && edges.every((edge) => edge === "auto" || Number.isFinite(edge))) style.clip = Object.freeze(edges);
|
|
1183
1455
|
}
|
|
1184
1456
|
break;
|
|
@@ -1188,7 +1460,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1188
1460
|
if (value === "none") style.clipPath = null;
|
|
1189
1461
|
const inset = /^inset\(\s*([^)]*?)\s*(?:round\s[^)]*)?\)$/i.exec(value);
|
|
1190
1462
|
if (inset) {
|
|
1191
|
-
const box = boxLengths(inset[1], false, style
|
|
1463
|
+
const box = boxLengths(inset[1], false, style, false, true);
|
|
1192
1464
|
if (box) style.clipPath = Object.freeze({ inset: box });
|
|
1193
1465
|
} else if (/^rect\(\s*0(?:px)?\s+0(?:px)?\s+0(?:px)?\s+0(?:px)?\s*\)$/i.test(value)) {
|
|
1194
1466
|
style.clipPath = Object.freeze({ empty: true });
|
|
@@ -1200,6 +1472,25 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1200
1472
|
if (color) style.color = color;
|
|
1201
1473
|
break;
|
|
1202
1474
|
}
|
|
1475
|
+
case "fill":
|
|
1476
|
+
case "stroke": {
|
|
1477
|
+
const paint = ["none", "currentcolor"].includes(value.toLowerCase()) ? value.toLowerCase()
|
|
1478
|
+
: /^url\(/i.test(value) ? value : resolvedColor(value, style.colorScheme);
|
|
1479
|
+
if (paint) style[name] = paint;
|
|
1480
|
+
break;
|
|
1481
|
+
}
|
|
1482
|
+
case "stroke-width": {
|
|
1483
|
+
const width = /^[\d.]+$/.test(value) ? Number(value) : computedLength(value, style);
|
|
1484
|
+
if (Number.isFinite(width) && width >= 0) style.strokeWidth = width;
|
|
1485
|
+
break;
|
|
1486
|
+
}
|
|
1487
|
+
case "fill-opacity":
|
|
1488
|
+
case "stroke-opacity": {
|
|
1489
|
+
const opacity = value.endsWith("%") ? Number(value.slice(0, -1)) / 100 : Number(value);
|
|
1490
|
+
if (Number.isFinite(opacity)) style[name === "fill-opacity" ? "fillOpacity" : "strokeOpacity"] = Math.max(0, Math.min(1, opacity));
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
case "fill-rule": if (["nonzero", "evenodd"].includes(value)) style.fillRule = value; break;
|
|
1203
1494
|
case "background-color": {
|
|
1204
1495
|
const color = resolvedColor(value, style.colorScheme);
|
|
1205
1496
|
if (color) style.backgroundColor = color;
|
|
@@ -1238,7 +1529,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1238
1529
|
break;
|
|
1239
1530
|
}
|
|
1240
1531
|
case "box-shadow": {
|
|
1241
|
-
const shadows = value === "none" ? null : parseBoxShadows(value, style
|
|
1532
|
+
const shadows = value === "none" ? null : parseBoxShadows(value, style);
|
|
1242
1533
|
if (value === "none" || shadows) style.boxShadow = shadows;
|
|
1243
1534
|
break;
|
|
1244
1535
|
}
|
|
@@ -1248,7 +1539,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1248
1539
|
break;
|
|
1249
1540
|
case "font-size": {
|
|
1250
1541
|
const parentSize = parentStyle?.fontSize ?? INITIAL.fontSize;
|
|
1251
|
-
const specified = computedLength(value,
|
|
1542
|
+
const specified = computedLength(value, parentStyle ?? INITIAL, true);
|
|
1252
1543
|
const relativeSize = specified?.unit === "%" ? parentSize * specified.value / 100
|
|
1253
1544
|
: specified?.unit === "math" && specified.kind === "calc" ? specified.px + parentSize * specified.percent / 100 : specified;
|
|
1254
1545
|
const length = FONT_SIZE_KEYWORDS[value] ?? (value === "larger" ? parentSize * 1.2 : value === "smaller" ? parentSize / 1.2
|
|
@@ -1265,8 +1556,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1265
1556
|
if (!parsed) break;
|
|
1266
1557
|
if (name === "font") setProperty(style, "font-size", parsed.size, parentStyle);
|
|
1267
1558
|
setProperty(style, "line-height", parsed.lineHeight ?? "normal", parentStyle);
|
|
1268
|
-
|
|
1269
|
-
|
|
1559
|
+
if (name === "font") {
|
|
1560
|
+
setProperty(style, "font-weight", parsed.weight ?? "normal", parentStyle);
|
|
1561
|
+
setProperty(style, "font-style", parsed.style ?? "normal", parentStyle);
|
|
1562
|
+
setProperty(style, "font-stretch", parsed.stretch ?? "normal", parentStyle);
|
|
1563
|
+
setProperty(style, "font-family", parsed.family, parentStyle);
|
|
1564
|
+
}
|
|
1270
1565
|
break;
|
|
1271
1566
|
}
|
|
1272
1567
|
case "position":
|
|
@@ -1276,13 +1571,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1276
1571
|
case "right":
|
|
1277
1572
|
case "bottom":
|
|
1278
1573
|
case "left": {
|
|
1279
|
-
const length = value === "auto" ? "auto" : computedLength(value, style
|
|
1574
|
+
const length = value === "auto" ? "auto" : computedLength(value, style, true);
|
|
1280
1575
|
if (length === "auto" || Number.isFinite(length) || length?.unit) style.inset = style.inset.with(BOX_SIDES.indexOf(name), length);
|
|
1281
1576
|
break;
|
|
1282
1577
|
}
|
|
1283
1578
|
case "inset": {
|
|
1284
1579
|
const parts = splitTopLevel(value, " ").filter(Boolean)
|
|
1285
|
-
.map((part) => part === "auto" ? "auto" : computedLength(part, style
|
|
1580
|
+
.map((part) => part === "auto" ? "auto" : computedLength(part, style, true));
|
|
1286
1581
|
if (!parts.length || parts.length > 4 || parts.some((part) => part !== "auto" && !Number.isFinite(part) && !part?.unit)) break;
|
|
1287
1582
|
const [top, right = top, bottom = top, left = right] = parts;
|
|
1288
1583
|
style.inset = [top, right, bottom, left];
|
|
@@ -1294,10 +1589,19 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1294
1589
|
break;
|
|
1295
1590
|
}
|
|
1296
1591
|
case "transform": {
|
|
1297
|
-
const transform = value === "none" ? null : parseTransform(value, style
|
|
1592
|
+
const transform = value === "none" ? null : parseTransform(value, style);
|
|
1298
1593
|
if (transform !== undefined) style.transform = transform;
|
|
1299
1594
|
break;
|
|
1300
1595
|
}
|
|
1596
|
+
case "translate": {
|
|
1597
|
+
if (value === "none") { style.translate = null; break; }
|
|
1598
|
+
const parts = splitTopLevel(value, " ").filter(Boolean);
|
|
1599
|
+
if (parts.length < 1 || parts.length > 2) break;
|
|
1600
|
+
const lengths = parts.map(part => computedLength(part, style, true));
|
|
1601
|
+
if (lengths.some(part => !Number.isFinite(part) && !part?.unit)) break;
|
|
1602
|
+
style.translate = Object.freeze({x:lengths[0],y:lengths[1] ?? 0});
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1301
1605
|
case "transform-origin":
|
|
1302
1606
|
style.transformOrigin = value.toLowerCase().replace(/\s+/g, " ");
|
|
1303
1607
|
break;
|
|
@@ -1325,9 +1629,16 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1325
1629
|
if (Number.isFinite(weight) && weight >= 1 && weight <= 1000) style.fontWeight = weight;
|
|
1326
1630
|
break;
|
|
1327
1631
|
}
|
|
1632
|
+
case "font-style": if (["normal", "italic", "oblique"].includes(value)) style.fontStyle = value; break;
|
|
1633
|
+
case "font-stretch":
|
|
1634
|
+
case "font-width": {
|
|
1635
|
+
const stretch = fontStretch(value);
|
|
1636
|
+
if (Number.isFinite(stretch)) style.fontStretch = stretch;
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1328
1639
|
case "white-space": if (WHITE_SPACES.has(value)) style.whiteSpace = value; break;
|
|
1329
1640
|
case "border-spacing": {
|
|
1330
|
-
const parts = value.split(/\s+/).map((part) => computedLength(part, style
|
|
1641
|
+
const parts = value.split(/\s+/).map((part) => computedLength(part, style));
|
|
1331
1642
|
if (parts.length <= 2 && parts.every((part) => Number.isFinite(part) && part >= 0)) {
|
|
1332
1643
|
style.borderSpacing = Object.freeze([parts[0], parts[1] ?? parts[0]]);
|
|
1333
1644
|
}
|
|
@@ -1371,7 +1682,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1371
1682
|
// Non-negative, so 0 is valid (icon buttons set line-height: 0); a
|
|
1372
1683
|
// percentage computes to a length against this element's font size.
|
|
1373
1684
|
const percentage = /^(\d+\.?\d*|\.\d+)%$/.exec(value);
|
|
1374
|
-
const length = percentage ? Number(percentage[1]) / 100 * style.fontSize : computedLength(value, style
|
|
1685
|
+
const length = percentage ? Number(percentage[1]) / 100 * style.fontSize : computedLength(value, style);
|
|
1375
1686
|
if (length >= 0) {
|
|
1376
1687
|
style.lineHeight = length;
|
|
1377
1688
|
style.lineHeightFactor = null;
|
|
@@ -1380,12 +1691,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1380
1691
|
break;
|
|
1381
1692
|
}
|
|
1382
1693
|
case "margin": {
|
|
1383
|
-
const box = boxLengths(value, true, style
|
|
1694
|
+
const box = boxLengths(value, true, style, true);
|
|
1384
1695
|
if (box) style.margin = box;
|
|
1385
1696
|
break;
|
|
1386
1697
|
}
|
|
1387
1698
|
case "padding": {
|
|
1388
|
-
const box = boxLengths(value, false, style
|
|
1699
|
+
const box = boxLengths(value, false, style, false, true);
|
|
1389
1700
|
if (box) style.padding = box;
|
|
1390
1701
|
break;
|
|
1391
1702
|
}
|
|
@@ -1399,7 +1710,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1399
1710
|
case "padding-left": {
|
|
1400
1711
|
const [box, side] = name.split("-");
|
|
1401
1712
|
const isMargin = box === "margin";
|
|
1402
|
-
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style
|
|
1713
|
+
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style, !isMargin);
|
|
1403
1714
|
if (length === "auto" || (!isMargin && isLength(length) && typeof length === "object"
|
|
1404
1715
|
&& (!isPercentage(length) || length.value >= 0)) || (Number.isFinite(length) && (isMargin || length >= 0))) {
|
|
1405
1716
|
style[box] = style[box].with(BOX_SIDES.indexOf(side), length);
|
|
@@ -1408,7 +1719,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1408
1719
|
}
|
|
1409
1720
|
case "width":
|
|
1410
1721
|
case "height": {
|
|
1411
|
-
const length = value === "auto" ? "auto" : computedLength(value, style
|
|
1722
|
+
const length = value === "auto" ? "auto" : computedLength(value, style, true);
|
|
1412
1723
|
if (length === "auto" || isLength(length)) style[name] = length;
|
|
1413
1724
|
break;
|
|
1414
1725
|
}
|
|
@@ -1417,7 +1728,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1417
1728
|
case "min-height":
|
|
1418
1729
|
case "max-height": {
|
|
1419
1730
|
const none = name.startsWith("max-") && value === "none";
|
|
1420
|
-
const length = none ? "none" : computedLength(value, style
|
|
1731
|
+
const length = none ? "none" : computedLength(value, style, true);
|
|
1421
1732
|
if (none || isLength(length)) {
|
|
1422
1733
|
style[{ "min-width": "minWidth", "max-width": "maxWidth", "min-height": "minHeight", "max-height": "maxHeight" }[name]] = length;
|
|
1423
1734
|
}
|
|
@@ -1429,7 +1740,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1429
1740
|
case "border-bottom":
|
|
1430
1741
|
case "border-left": {
|
|
1431
1742
|
// Shorthands reset width (medium), style (none) and color (currentColor).
|
|
1432
|
-
const parsed = parseBorderShorthand(value, style
|
|
1743
|
+
const parsed = parseBorderShorthand(value, style);
|
|
1433
1744
|
if (!parsed) break;
|
|
1434
1745
|
const sides = name === "border" ? [0, 1, 2, 3] : [BOX_SIDES.indexOf(name.slice(7))];
|
|
1435
1746
|
for (const side of sides) {
|
|
@@ -1443,7 +1754,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1443
1754
|
case "border-style":
|
|
1444
1755
|
case "border-color": {
|
|
1445
1756
|
const parts = splitTopLevel(value, " ").filter(Boolean).map((part) => name === "border-width"
|
|
1446
|
-
? borderWidth(part, style
|
|
1757
|
+
? borderWidth(part, style)
|
|
1447
1758
|
: name === "border-style" ? (BORDER_STYLES.has(part) ? part : null) : borderColor(part));
|
|
1448
1759
|
if (!parts.length || parts.length > 4 || parts.some((part) => part === undefined || (name !== "border-color" && part == null))) break;
|
|
1449
1760
|
const [top, right = top, bottom = top, left = right] = parts;
|
|
@@ -1459,7 +1770,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1459
1770
|
const [, side, part] = name.split("-");
|
|
1460
1771
|
const index = BOX_SIDES.indexOf(side);
|
|
1461
1772
|
if (part === "width") {
|
|
1462
|
-
const width = borderWidth(value, style
|
|
1773
|
+
const width = borderWidth(value, style);
|
|
1463
1774
|
if (width != null) style.borderSpecifiedWidths = style.borderSpecifiedWidths.with(index, width);
|
|
1464
1775
|
} else if (part === "style") {
|
|
1465
1776
|
if (BORDER_STYLES.has(value)) style.borderStyles = style.borderStyles.with(index, value);
|
|
@@ -1470,12 +1781,40 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1470
1781
|
break;
|
|
1471
1782
|
}
|
|
1472
1783
|
case "border-radius": {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1784
|
+
const axes = splitTopLevel(value, "/");
|
|
1785
|
+
if (axes.length > 2) break;
|
|
1786
|
+
const horizontal = radiusLengths(axes[0], style, 4);
|
|
1787
|
+
const vertical = axes.length === 2 ? radiusLengths(axes[1], style, 4) : horizontal;
|
|
1788
|
+
if (!horizontal || !vertical) break;
|
|
1789
|
+
const expand = values => [values[0], values[1] ?? values[0], values[2] ?? values[0], values[3] ?? values[1] ?? values[0]];
|
|
1790
|
+
const x = expand(horizontal), y = expand(vertical);
|
|
1791
|
+
for (let i = 0; i < 4; i++) style[CORNER_FIELDS[i]] = Object.freeze([x[i], y[i]]);
|
|
1792
|
+
style.borderRadius = x[0];
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
case "border-top-left-radius": case "border-top-right-radius":
|
|
1796
|
+
case "border-bottom-right-radius": case "border-bottom-left-radius": {
|
|
1797
|
+
const lengths = radiusLengths(value, style, 2);
|
|
1798
|
+
if (lengths) style[name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())]
|
|
1799
|
+
= Object.freeze([lengths[0], lengths[1] ?? lengths[0]]);
|
|
1476
1800
|
break;
|
|
1477
1801
|
}
|
|
1478
1802
|
case "box-sizing": if (["content-box", "border-box"].includes(value)) style.boxSizing = value; break;
|
|
1803
|
+
case "flex-flow": {
|
|
1804
|
+
const parts = value.split(/\s+/);
|
|
1805
|
+
let direction = null;
|
|
1806
|
+
let wrap = null;
|
|
1807
|
+
for (const part of parts) {
|
|
1808
|
+
if (["row", "column"].includes(part) && direction == null) direction = part;
|
|
1809
|
+
else if (["nowrap", "wrap"].includes(part) && wrap == null) wrap = part;
|
|
1810
|
+
else return;
|
|
1811
|
+
}
|
|
1812
|
+
if (parts.length) {
|
|
1813
|
+
style.flexDirection = direction ?? "row";
|
|
1814
|
+
style.flexWrap = wrap ?? "nowrap";
|
|
1815
|
+
}
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1479
1818
|
case "flex-direction": if (["row", "column"].includes(value)) style.flexDirection = value; break;
|
|
1480
1819
|
case "flex-wrap": if (["nowrap", "wrap"].includes(value)) style.flexWrap = value; break;
|
|
1481
1820
|
case "justify-content": {
|
|
@@ -1507,12 +1846,16 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1507
1846
|
break;
|
|
1508
1847
|
}
|
|
1509
1848
|
case "gap": {
|
|
1510
|
-
const
|
|
1511
|
-
|
|
1849
|
+
const parts = splitTopLevel(value, " ").filter(Boolean);
|
|
1850
|
+
if (parts.length < 1 || parts.length > 2) break;
|
|
1851
|
+
const [row, column = row] = parts
|
|
1852
|
+
.map((part) => part === "normal" ? 0 : computedLength(part, style));
|
|
1512
1853
|
if (row >= 0 && column >= 0) {
|
|
1513
1854
|
style.gap = row;
|
|
1514
1855
|
style.rowGap = row;
|
|
1515
1856
|
style.columnGap = column;
|
|
1857
|
+
style.rowGapNormal = parts[0] === "normal";
|
|
1858
|
+
style.columnGapNormal = (parts[1] ?? parts[0]) === "normal";
|
|
1516
1859
|
}
|
|
1517
1860
|
break;
|
|
1518
1861
|
}
|
|
@@ -1521,13 +1864,50 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1521
1864
|
case "grid-column-gap": return setProperty(style, "column-gap", value, parentStyle);
|
|
1522
1865
|
case "grid-template-columns":
|
|
1523
1866
|
case "grid-template-rows": {
|
|
1524
|
-
const tracks = value === "none" ? null : parseTrackList(value, style
|
|
1867
|
+
const tracks = value === "none" ? null : parseTrackList(value, style);
|
|
1525
1868
|
if (value === "none" || tracks) style[name === "grid-template-columns" ? "gridTemplateColumns" : "gridTemplateRows"] = tracks;
|
|
1526
1869
|
break;
|
|
1527
1870
|
}
|
|
1871
|
+
case "grid-template-areas": {
|
|
1872
|
+
const areas = value === "none" ? null : parseGridAreas(value);
|
|
1873
|
+
if (value === "none" || areas) style.gridTemplateAreas = areas;
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
case "grid-template": {
|
|
1877
|
+
if (value === "none") {
|
|
1878
|
+
style.gridTemplateRows = style.gridTemplateColumns = style.gridTemplateAreas = null;
|
|
1879
|
+
break;
|
|
1880
|
+
}
|
|
1881
|
+
const parts = splitTopLevel(value, "/");
|
|
1882
|
+
if (parts.length > 2) break;
|
|
1883
|
+
const columns = parts[1] == null || parts[1] === "none" ? null : parseTrackList(parts[1], style);
|
|
1884
|
+
if (parts[1] != null && parts[1] !== "none" && !columns) break;
|
|
1885
|
+
// A string token needs no surrounding whitespace in minified CSS.
|
|
1886
|
+
const rowText = parts[0].replace(/\[[^\]]*\]/g, "").replace(/"[^"\\]*"|'[^'\\]*'/g, " $& ");
|
|
1887
|
+
const tokens = splitTopLevel(rowText, " ").filter(Boolean);
|
|
1888
|
+
if (tokens.some((token) => /^["']/.test(token))) {
|
|
1889
|
+
const rows = [], strings = [];
|
|
1890
|
+
let valid = true;
|
|
1891
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
1892
|
+
const token = tokens[index];
|
|
1893
|
+
if (!/^(?:"[^"\\]*"|'[^'\\]*')$/.test(token)) { valid = false; break; }
|
|
1894
|
+
strings.push(token);
|
|
1895
|
+
const size = tokens[index + 1] && !/^["']/.test(tokens[index + 1]) ? tokens[++index] : "auto";
|
|
1896
|
+
const track = parseTrackSize(size, style);
|
|
1897
|
+
if (!track) { valid = false; break; }
|
|
1898
|
+
rows.push(track);
|
|
1899
|
+
}
|
|
1900
|
+
const areas = valid && parseGridAreas(strings.join(" "));
|
|
1901
|
+
if (areas) Object.assign(style, { gridTemplateRows: Object.freeze(rows), gridTemplateColumns: columns, gridTemplateAreas: areas });
|
|
1902
|
+
} else if (parts.length === 2) {
|
|
1903
|
+
const rows = parts[0] === "none" ? null : parseTrackList(parts[0], style);
|
|
1904
|
+
if (parts[0] === "none" || rows) Object.assign(style, { gridTemplateRows: rows, gridTemplateColumns: columns, gridTemplateAreas: null });
|
|
1905
|
+
}
|
|
1906
|
+
break;
|
|
1907
|
+
}
|
|
1528
1908
|
case "grid-auto-columns":
|
|
1529
1909
|
case "grid-auto-rows": {
|
|
1530
|
-
const tracks = parseTrackList(value, style
|
|
1910
|
+
const tracks = parseTrackList(value, style);
|
|
1531
1911
|
if (tracks?.length && tracks.every((entry) => !entry.repeat)) {
|
|
1532
1912
|
style[name === "grid-auto-columns" ? "gridAutoColumns" : "gridAutoRows"] = tracks;
|
|
1533
1913
|
}
|
|
@@ -1553,12 +1933,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1553
1933
|
const [start, end] = value.split("/").map((part) => parseGridLine(part.trim()));
|
|
1554
1934
|
const axis = name === "grid-column" ? "Column" : "Row";
|
|
1555
1935
|
style[`grid${axis}Start`] = start;
|
|
1556
|
-
// A
|
|
1557
|
-
style[`grid${axis}End`] = end ?? "auto";
|
|
1936
|
+
// A lone area name denotes both edges; a lone integer spans one track.
|
|
1937
|
+
style[`grid${axis}End`] = end ?? (start.name ? start : "auto");
|
|
1558
1938
|
break;
|
|
1559
1939
|
}
|
|
1560
1940
|
case "grid-area": {
|
|
1561
|
-
const [rowStart, columnStart =
|
|
1941
|
+
const [rowStart, columnStart = rowStart.name ? rowStart : "auto",
|
|
1942
|
+
rowEnd = rowStart.name ? rowStart : "auto", columnEnd = columnStart.name ? columnStart : "auto"] = value.split("/")
|
|
1562
1943
|
.map((part) => parseGridLine(part.trim()));
|
|
1563
1944
|
Object.assign(style, { gridRowStart: rowStart, gridColumnStart: columnStart, gridRowEnd: rowEnd, gridColumnEnd: columnEnd });
|
|
1564
1945
|
break;
|
|
@@ -1572,8 +1953,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1572
1953
|
}
|
|
1573
1954
|
case "row-gap":
|
|
1574
1955
|
case "column-gap": {
|
|
1575
|
-
const length = value === "normal" ? 0 : computedLength(value, style
|
|
1576
|
-
if (length >= 0)
|
|
1956
|
+
const length = value === "normal" ? 0 : computedLength(value, style);
|
|
1957
|
+
if (length >= 0) {
|
|
1958
|
+
const field = name === "row-gap" ? "rowGap" : "columnGap";
|
|
1959
|
+
style[field] = length;
|
|
1960
|
+
style[`${field}Normal`] = value === "normal";
|
|
1961
|
+
}
|
|
1577
1962
|
break;
|
|
1578
1963
|
}
|
|
1579
1964
|
case "align-self": {
|
|
@@ -1588,12 +1973,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1588
1973
|
break;
|
|
1589
1974
|
}
|
|
1590
1975
|
case "flex-basis": {
|
|
1591
|
-
const basis = flexBasis(value, style
|
|
1976
|
+
const basis = flexBasis(value, style);
|
|
1592
1977
|
if (basis != null) style.flexBasis = basis;
|
|
1593
1978
|
break;
|
|
1594
1979
|
}
|
|
1595
1980
|
case "flex": {
|
|
1596
|
-
const flex = flexShorthand(value, style
|
|
1981
|
+
const flex = flexShorthand(value, style);
|
|
1597
1982
|
if (flex) Object.assign(style, flex);
|
|
1598
1983
|
break;
|
|
1599
1984
|
}
|
|
@@ -1807,7 +2192,7 @@ function cascadeDeclarations(element, rules, pseudo = null, keys = elementKeys(e
|
|
|
1807
2192
|
for (const declaration of rule.declarationList) {
|
|
1808
2193
|
const current = result.get(declaration.name);
|
|
1809
2194
|
if (current && compareCascadeParts(declaration.important, layer, specificity, order, current) < 0) continue;
|
|
1810
|
-
result.set(declaration.name, {
|
|
2195
|
+
result.set(declaration.name, { ...declaration, specificity, order, layer });
|
|
1811
2196
|
}
|
|
1812
2197
|
}
|
|
1813
2198
|
return result;
|
|
@@ -1842,7 +2227,10 @@ function compareDeclarations(left, right) {
|
|
|
1842
2227
|
const leftSpecificity = "specificity" in left ? left.specificity : 0;
|
|
1843
2228
|
const rightSpecificity = "specificity" in right ? right.specificity : 0;
|
|
1844
2229
|
if (leftSpecificity !== rightSpecificity) return leftSpecificity - rightSpecificity;
|
|
1845
|
-
|
|
2230
|
+
const leftOrder = left.order ?? 0;
|
|
2231
|
+
const rightOrder = right.order ?? 0;
|
|
2232
|
+
if (leftOrder !== rightOrder) return leftOrder - rightOrder;
|
|
2233
|
+
return (left.declarationIndex ?? 0) - (right.declarationIndex ?? 0);
|
|
1846
2234
|
}
|
|
1847
2235
|
|
|
1848
2236
|
const PSEUDO_ELEMENT = /(?:::?(before|after)|::([\w-]+))$/i;
|
|
@@ -1945,53 +2333,75 @@ function pseudoElement(pseudo) {
|
|
|
1945
2333
|
return { tagName: `::${pseudo}`, getAttribute: () => null, hasAttribute: () => false };
|
|
1946
2334
|
}
|
|
1947
2335
|
|
|
1948
|
-
/**
|
|
1949
|
-
* Keeps the translation part of a transform list as { x, y } lengths (px or
|
|
1950
|
-
* percentages of the element's own border box). Other functions are ignored
|
|
1951
|
-
* until transforms are painted with matrices.
|
|
1952
|
-
*/
|
|
1953
2336
|
/**
|
|
1954
2337
|
* The translation (layout moves the box) and scale (paint scales it about
|
|
1955
|
-
* transform-origin) of a transform list
|
|
2338
|
+
* transform-origin) of a transform list. Compose in list order: a translation
|
|
2339
|
+
* after a scale uses that scaled coordinate system. Percentages remain lengths
|
|
2340
|
+
* until layout knows the element's own border box. Other functions are ignored.
|
|
1956
2341
|
*/
|
|
1957
2342
|
function parseTransform(value, fontSize) {
|
|
1958
|
-
|
|
1959
|
-
|
|
2343
|
+
let x = 0;
|
|
2344
|
+
let y = 0;
|
|
1960
2345
|
let scaleX = 1;
|
|
1961
2346
|
let scaleY = 1;
|
|
1962
2347
|
let found = false;
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
2348
|
+
const functions = /\b([a-z][a-z0-9]*)\(/gi;
|
|
2349
|
+
let match;
|
|
2350
|
+
while ((match = functions.exec(value))) {
|
|
2351
|
+
let end = functions.lastIndex;
|
|
2352
|
+
let depth = 1;
|
|
2353
|
+
while (end < value.length && depth) {
|
|
2354
|
+
if (value[end] === "(") depth++;
|
|
2355
|
+
else if (value[end] === ")") depth--;
|
|
2356
|
+
end++;
|
|
2357
|
+
}
|
|
2358
|
+
if (depth) return undefined;
|
|
1966
2359
|
const kind = match[1].toLowerCase();
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2360
|
+
const parts = splitTopLevel(value.slice(functions.lastIndex, end - 1), ",");
|
|
2361
|
+
functions.lastIndex = end;
|
|
2362
|
+
if (["translate", "translatex", "translatey", "translate3d"].includes(kind)) {
|
|
2363
|
+
const args = parts.map(part => computedLength(part, fontSize, true));
|
|
2364
|
+
const max = kind === "translate3d" ? 3 : kind === "translate" ? 2 : 1;
|
|
2365
|
+
if (!args.length || args.length > max || args.some(part => !Number.isFinite(part) && !part?.unit)) return undefined;
|
|
2366
|
+
if (kind === "translatey") y = addTransformLength(y, scaleTransformLength(args[0], scaleY));
|
|
2367
|
+
else {
|
|
2368
|
+
x = addTransformLength(x, scaleTransformLength(args[0], scaleX));
|
|
2369
|
+
if (kind !== "translatex") y = addTransformLength(y, scaleTransformLength(args[1] ?? 0, scaleY));
|
|
2370
|
+
}
|
|
2371
|
+
} else if (["scale", "scalex", "scaley", "scale3d"].includes(kind)) {
|
|
2372
|
+
const args = parts.map(part => part.endsWith("%") ? Number(part.slice(0, -1)) / 100 : Number(part));
|
|
2373
|
+
const max = kind === "scale3d" ? 3 : kind === "scale" ? 2 : 1;
|
|
2374
|
+
if (!args.length || args.length > max || args.some(part => !Number.isFinite(part))) return undefined;
|
|
2375
|
+
if (kind === "scalex") scaleX *= args[0];
|
|
2376
|
+
else if (kind === "scaley") scaleY *= args[0];
|
|
2377
|
+
else { scaleX *= args[0]; scaleY *= args[1] ?? args[0]; }
|
|
2378
|
+
} else {
|
|
2379
|
+
continue;
|
|
1972
2380
|
}
|
|
1973
2381
|
found = true;
|
|
1974
2382
|
}
|
|
1975
|
-
if (!found
|
|
1976
|
-
return Object.freeze(
|
|
2383
|
+
if (!found) return null;
|
|
2384
|
+
return Object.freeze(scaleX === 1 && scaleY === 1 ? { x, y } : { x, y, scaleX, scaleY });
|
|
1977
2385
|
}
|
|
1978
2386
|
|
|
1979
|
-
function
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
}
|
|
1994
|
-
|
|
2387
|
+
function scaleTransformLength(value, factor) {
|
|
2388
|
+
if (factor === 1) return value;
|
|
2389
|
+
if (typeof value === "number") return value * factor;
|
|
2390
|
+
if (value.unit === "%") return Object.freeze({ unit: "%", value: value.value * factor });
|
|
2391
|
+
if (value.kind === "calc") return Object.freeze({ ...value, px: value.px * factor, percent: value.percent * factor });
|
|
2392
|
+
return Object.freeze({ unit: "math", kind: "scale", value, factor });
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
function addTransformLength(a, b) {
|
|
2396
|
+
if (a === 0) return b;
|
|
2397
|
+
if (b === 0) return a;
|
|
2398
|
+
const linear = value => typeof value === "number" ? { px: value, percent: 0 }
|
|
2399
|
+
: value.unit === "%" ? { px: 0, percent: value.value } : value.kind === "calc" ? value : null;
|
|
2400
|
+
const left = linear(a), right = linear(b);
|
|
2401
|
+
if (!left || !right) return Object.freeze({ unit: "math", kind: "sum", values: Object.freeze([a, b]) });
|
|
2402
|
+
const px = left.px + right.px, percent = left.percent + right.percent;
|
|
2403
|
+
return percent === 0 ? px : px === 0 ? Object.freeze({ unit: "%", value: percent })
|
|
2404
|
+
: Object.freeze({ unit: "math", kind: "calc", px, percent });
|
|
1995
2405
|
}
|
|
1996
2406
|
|
|
1997
2407
|
/** Parses `content` into its string, null for none/normal, undefined if unsupported. */
|
|
@@ -2073,12 +2483,22 @@ function copyProperty(style, name, source) {
|
|
|
2073
2483
|
style[side[1]] = style[side[1]].with(index, source[side[1]][index]);
|
|
2074
2484
|
return;
|
|
2075
2485
|
}
|
|
2486
|
+
const insetSide = BOX_SIDES.indexOf(name);
|
|
2487
|
+
if (insetSide >= 0) {
|
|
2488
|
+
style.inset = style.inset.with(insetSide, source.inset[insetSide]);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2076
2491
|
// border, border-<side>, border-<width|style|color>, border-<side>-<...>;
|
|
2077
2492
|
// not border-radius, border-spacing or border-collapse.
|
|
2078
|
-
const
|
|
2079
|
-
|
|
2080
|
-
?
|
|
2081
|
-
|
|
2493
|
+
const border = /^border(?:-(top|right|bottom|left))?(?:-(width|style|color))?$/.exec(name);
|
|
2494
|
+
if (border) {
|
|
2495
|
+
const fields = border[2] === "width" ? ["borderSpecifiedWidths"]
|
|
2496
|
+
: border[2] === "style" ? ["borderStyles"] : border[2] === "color" ? ["borderColors"] : BORDER_FIELDS;
|
|
2497
|
+
const side = border[1] == null ? -1 : BOX_SIDES.indexOf(border[1]);
|
|
2498
|
+
for (const field of fields) style[field] = side < 0 ? source[field] : style[field].with(side, source[field][side]);
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
const fields = PROPERTY_FIELDS[name] ?? [name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())];
|
|
2082
2502
|
for (const field of fields) {
|
|
2083
2503
|
if (Object.hasOwn(INITIAL, field)) style[field] = source[field];
|
|
2084
2504
|
}
|
|
@@ -2090,6 +2510,9 @@ function cssValue(style, name) {
|
|
|
2090
2510
|
"font-family": style.fontFamily.map((family) => JSON.stringify(family)).join(", "),
|
|
2091
2511
|
"font-size": `${style.fontSize}px`,
|
|
2092
2512
|
"font-weight": String(style.fontWeight),
|
|
2513
|
+
"font-style": style.fontStyle,
|
|
2514
|
+
"font-stretch": `${style.fontStretch}%`,
|
|
2515
|
+
"font-width": `${style.fontStretch}%`,
|
|
2093
2516
|
"line-height": style.lineHeightFactor != null ? String(style.lineHeightFactor)
|
|
2094
2517
|
: style.lineHeight == null ? "normal" : `${style.lineHeight}px`,
|
|
2095
2518
|
"list-style-type": style.listStyleType,
|
|
@@ -2105,7 +2528,22 @@ function pixelLength(value) {
|
|
|
2105
2528
|
return match ? Number(match[1]) : NaN;
|
|
2106
2529
|
}
|
|
2107
2530
|
|
|
2108
|
-
function
|
|
2531
|
+
function radiusLengths(value, style, maximum) {
|
|
2532
|
+
const parts = splitTopLevel(value, " ").filter(Boolean);
|
|
2533
|
+
if (!parts.length || parts.length > maximum) return null;
|
|
2534
|
+
const lengths = parts.map(part => computedLength(part, style, true));
|
|
2535
|
+
return lengths.every((length, index) => Number.isFinite(length)
|
|
2536
|
+
? length >= 0 || /^(?:calc|min|max|clamp)\(/i.test(parts[index])
|
|
2537
|
+
: length?.unit === "%" ? length.value >= 0 : length?.unit === "math") ? lengths : null;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
function fontStretch(value) {
|
|
2541
|
+
if (Object.hasOwn(FONT_STRETCH_KEYWORDS, value)) return FONT_STRETCH_KEYWORDS[value];
|
|
2542
|
+
const percent = /^(?:\d+\.?\d*|\.\d+)%$/.test(value) ? Number.parseFloat(value) : NaN;
|
|
2543
|
+
return percent >= 0 ? percent : NaN;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function computedLength(value, font, allowPercentage = false) {
|
|
2109
2547
|
const text = String(value).trim();
|
|
2110
2548
|
if (allowPercentage) {
|
|
2111
2549
|
const percentage = /^([+-]?(?:\d+\.?\d*|\.\d+))%$/.exec(text);
|
|
@@ -2114,14 +2552,14 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2114
2552
|
const absolute = pixelLength(text);
|
|
2115
2553
|
if (Number.isFinite(absolute)) return absolute;
|
|
2116
2554
|
if (/^calc\(/i.test(text)) {
|
|
2117
|
-
const sum = calcLength(text,
|
|
2555
|
+
const sum = calcLength(text, font);
|
|
2118
2556
|
if (!sum || !(sum.percent === 0 || allowPercentage)) return NaN;
|
|
2119
2557
|
return sum.percent === 0 ? sum.px : Object.freeze({ unit: "math", kind: "calc", px: sum.px, percent: sum.percent });
|
|
2120
2558
|
}
|
|
2121
2559
|
const math = /^(clamp|min|max)\((.*)\)$/i.exec(text);
|
|
2122
2560
|
if (math) {
|
|
2123
2561
|
const kind = math[1].toLowerCase();
|
|
2124
|
-
const values = splitTopLevel(math[2], ",").map((part) => computedLength(part,
|
|
2562
|
+
const values = splitTopLevel(math[2], ",").map((part) => computedLength(part, font, allowPercentage));
|
|
2125
2563
|
if (!values.length || values.some((part) => !Number.isFinite(part) && !(part?.unit))) return NaN;
|
|
2126
2564
|
// Percentages inside math functions resolve at layout time.
|
|
2127
2565
|
if (values.some((part) => part?.unit)) {
|
|
@@ -2133,14 +2571,38 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2133
2571
|
if (kind === "max") return Math.max(...values);
|
|
2134
2572
|
return values.length === 3 ? Math.max(values[0], Math.min(values[1], values[2])) : NaN;
|
|
2135
2573
|
}
|
|
2136
|
-
const relative = /^([+-]?(?:\d+\.?\d*|\.\d+))(rem|em|
|
|
2574
|
+
const relative = /^([+-]?(?:\d+\.?\d*|\.\d+))(rem|em|ch|[sld]?v(?:w|h|i|b|min|max)|cq(?:w|h|i|b|min|max)|pt)$/i.exec(text);
|
|
2137
2575
|
if (!relative) return NaN;
|
|
2138
2576
|
const unit = relative[2].toLowerCase();
|
|
2577
|
+
if (/^(?:[sld]?v|cq)/.test(unit)) {
|
|
2578
|
+
// A headless viewport has no retractable browser UI: small, large and
|
|
2579
|
+
// dynamic viewport sizes coincide. Inline/block follow horizontal writing.
|
|
2580
|
+
// Without an eligible query container, cq* uses the small viewport
|
|
2581
|
+
// (CSS Containment 3 §6). Container-type sizing is not implemented yet.
|
|
2582
|
+
const axis = unit.replace(/^(?:[sld]?v|cq)/, "");
|
|
2583
|
+
const {viewportWidth:width,viewportHeight:height} = environment;
|
|
2584
|
+
const size = axis === "w" || axis === "i" ? width : axis === "h" || axis === "b" ? height
|
|
2585
|
+
: axis === "min" ? Math.min(width,height) : Math.max(width,height);
|
|
2586
|
+
return Number(relative[1]) * size / 100;
|
|
2587
|
+
}
|
|
2588
|
+
const emSize = typeof font === "object" ? font.fontSize : font;
|
|
2589
|
+
// CSS Values 4 §6.1.1: ch is the advance of U+0030 in the used font.
|
|
2590
|
+
// The spec's 0.5em fallback applies when no font measurement is available.
|
|
2591
|
+
let characterWidth = emSize / 2;
|
|
2592
|
+
if (unit === "ch" && environment.measureText) {
|
|
2593
|
+
// The style is still being built; do not give a mutable style identity
|
|
2594
|
+
// to the renderer's immutable-style font-key cache.
|
|
2595
|
+
const measured = environment.measureText("0", {
|
|
2596
|
+
fontSize: emSize, fontFamily: font?.fontFamily ?? INITIAL.fontFamily,
|
|
2597
|
+
fontWeight: font?.fontWeight ?? INITIAL.fontWeight,
|
|
2598
|
+
fontStyle: font?.fontStyle ?? INITIAL.fontStyle,
|
|
2599
|
+
fontStretch: font?.fontStretch ?? INITIAL.fontStretch,
|
|
2600
|
+
});
|
|
2601
|
+
if (Number.isFinite(measured) && measured >= 0) characterWidth = measured;
|
|
2602
|
+
}
|
|
2139
2603
|
const scale = unit === "rem" ? environment.rootFontSize
|
|
2140
2604
|
: unit === "em" ? emSize
|
|
2141
|
-
: unit === "
|
|
2142
|
-
: unit === "vh" ? environment.viewportHeight / 100
|
|
2143
|
-
: 4 / 3;
|
|
2605
|
+
: unit === "ch" ? characterWidth : 4 / 3;
|
|
2144
2606
|
return Number(relative[1]) * scale;
|
|
2145
2607
|
}
|
|
2146
2608
|
|
|
@@ -2151,7 +2613,7 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2151
2613
|
* for anything else (such as min() of a percentage inside calc()).
|
|
2152
2614
|
*/
|
|
2153
2615
|
function calcLength(text, emSize) {
|
|
2154
|
-
const tokens = String(text).match(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(?:%|[a-z]+)?|[a-z-]+\(|[()
|
|
2616
|
+
const tokens = String(text).match(/[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?(?:%|[a-z]+)?|[a-z-]+\(|[(),*/+-]/gi);
|
|
2155
2617
|
if (!tokens || tokens.join("").replace(/\s+/g, "") !== String(text).replace(/\s+/g, "")) return null;
|
|
2156
2618
|
let index = 0;
|
|
2157
2619
|
// Values are { px, percent } (a length) or { number }.
|
|
@@ -2345,10 +2807,47 @@ function parseGridLine(token) {
|
|
|
2345
2807
|
if (span) return Object.freeze({ span: Math.max(1, Number(span[1] ?? span[2])) });
|
|
2346
2808
|
const line = Number(token);
|
|
2347
2809
|
if (Number.isInteger(line) && line !== 0) return Object.freeze({ line });
|
|
2348
|
-
|
|
2810
|
+
if (/^-?(?:[a-zA-Z_\u0080-\uFFFF]|--)[\w\u0080-\uFFFF-]*$/.test(token) && token !== "span") {
|
|
2811
|
+
return Object.freeze({ name: token });
|
|
2812
|
+
}
|
|
2349
2813
|
return "auto";
|
|
2350
2814
|
}
|
|
2351
2815
|
|
|
2816
|
+
/** CSS Grid §7.3: equal-width strings whose named cells form rectangles. */
|
|
2817
|
+
function parseGridAreas(value) {
|
|
2818
|
+
const rows = [];
|
|
2819
|
+
const strings = /\s*(["'])(.*?)\1\s*/gy;
|
|
2820
|
+
while (strings.lastIndex < value.length) {
|
|
2821
|
+
const match = strings.exec(value);
|
|
2822
|
+
if (!match) return null;
|
|
2823
|
+
const cells = match[2].match(/[\w\u0080-\uFFFF-]+|\.+|[^\s]/g) ?? [];
|
|
2824
|
+
if (!cells.length || cells.some((cell) => !/^(?:[\w\u0080-\uFFFF-]+|\.+)$/.test(cell))) return null;
|
|
2825
|
+
if (rows.length && cells.length !== rows[0].length) return null;
|
|
2826
|
+
rows.push(cells.map((cell) => cell[0] === "." ? null : cell));
|
|
2827
|
+
}
|
|
2828
|
+
if (!rows.length) return null;
|
|
2829
|
+
const areas = Object.create(null);
|
|
2830
|
+
for (let row = 0; row < rows.length; row++) {
|
|
2831
|
+
for (let column = 0; column < rows[row].length; column++) {
|
|
2832
|
+
const name = rows[row][column];
|
|
2833
|
+
if (name == null) continue;
|
|
2834
|
+
const area = areas[name] ??= { row, column, rowEnd: row + 1, columnEnd: column + 1 };
|
|
2835
|
+
area.rowEnd = row + 1;
|
|
2836
|
+
area.column = Math.min(area.column, column);
|
|
2837
|
+
area.columnEnd = Math.max(area.columnEnd, column + 1);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
for (const [name, area] of Object.entries(areas)) {
|
|
2841
|
+
for (let row = area.row; row < area.rowEnd; row++) {
|
|
2842
|
+
for (let column = area.column; column < area.columnEnd; column++) {
|
|
2843
|
+
if (rows[row][column] !== name) return null;
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
Object.freeze(area);
|
|
2847
|
+
}
|
|
2848
|
+
return Object.freeze({ rows: rows.length, columns: rows[0].length, areas: Object.freeze(areas) });
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2352
2851
|
const BORDER_STYLES = new Set(["none", "hidden", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"]);
|
|
2353
2852
|
const BORDER_WIDTH_KEYWORDS = Object.freeze({ thin: 1, medium: 3, thick: 5 });
|
|
2354
2853
|
|
|
@@ -2386,12 +2885,12 @@ function flexBasis(value, fontSize) {
|
|
|
2386
2885
|
function flexShorthand(value, fontSize) {
|
|
2387
2886
|
if (value === "none") return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" };
|
|
2388
2887
|
if (value === "auto") return { flexGrow: 1, flexShrink: 1, flexBasis: "auto" };
|
|
2389
|
-
const parts = value.trim().
|
|
2888
|
+
const parts = splitTopLevel(value.trim(), " ").filter(Boolean);
|
|
2390
2889
|
const numbers = [];
|
|
2391
2890
|
let basis = null;
|
|
2392
2891
|
for (const part of parts) {
|
|
2393
2892
|
const number = Number(part);
|
|
2394
|
-
if (part !== "" && Number.isFinite(number) && numbers.length < 2 && basis == null) numbers.push(number);
|
|
2893
|
+
if (part !== "" && Number.isFinite(number) && number >= 0 && numbers.length < 2 && basis == null) numbers.push(number);
|
|
2395
2894
|
else if (basis == null && (basis = flexBasis(part, fontSize)) != null) continue;
|
|
2396
2895
|
else return null;
|
|
2397
2896
|
}
|
|
@@ -2400,7 +2899,9 @@ function flexShorthand(value, fontSize) {
|
|
|
2400
2899
|
return {
|
|
2401
2900
|
flexGrow: numbers[0] ?? 1,
|
|
2402
2901
|
flexShrink: numbers[1] ?? 1,
|
|
2403
|
-
|
|
2902
|
+
// The omitted basis is a percentage, not a definite zero length. In an
|
|
2903
|
+
// auto-height column, 0% is indefinite and uses the item's content size.
|
|
2904
|
+
flexBasis: basis ?? (numbers.length ? Object.freeze({unit:"%",value:0}) : "auto"),
|
|
2404
2905
|
};
|
|
2405
2906
|
}
|
|
2406
2907
|
|
|
@@ -2476,10 +2977,16 @@ export function absolutizeStyleSheetUrls(text, base) {
|
|
|
2476
2977
|
function splitTopLevel(source, separator) {
|
|
2477
2978
|
const parts = [];
|
|
2478
2979
|
let depth = 0;
|
|
2980
|
+
let quote = "";
|
|
2479
2981
|
let start = 0;
|
|
2480
2982
|
for (let index = 0; index < source.length; index++) {
|
|
2481
2983
|
const character = source[index];
|
|
2482
|
-
if (
|
|
2984
|
+
if (quote) {
|
|
2985
|
+
if (character === "\\") index++;
|
|
2986
|
+
else if (character === quote) quote = "";
|
|
2987
|
+
} else if (character === "\\") index++;
|
|
2988
|
+
else if (character === '"' || character === "'") quote = character;
|
|
2989
|
+
else if (character === "(") depth++;
|
|
2483
2990
|
else if (character === ")") depth--;
|
|
2484
2991
|
else if ((character === separator || separator === " " && /\s/.test(character)) && depth === 0) {
|
|
2485
2992
|
parts.push(source.slice(start, index).trim());
|