buninu-browser 0.2.2 → 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/audio/playback-buffer.js +27 -0
- package/lib/audio/pulse-client.js +18 -8
- package/lib/cdp-server.js +53 -14
- package/lib/happy-dom/document-base.js +33 -0
- package/lib/happy-dom/parser.js +20 -6
- package/lib/happy-dom/selector-matcher.js +2 -2
- package/lib/happy-dom/selector-pseudo-adapter.js +33 -0
- package/lib/happy-dom/svg-attribute-adapter.js +50 -0
- package/lib/headless-shell.js +12 -6
- package/lib/input/hit-test.js +67 -19
- package/lib/layout/text-layout.js +289 -131
- 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 +42 -0
- package/lib/media/webm-stream.js +106 -0
- package/lib/network/network-service.js +268 -120
- package/lib/paint/display-list.js +22 -27
- 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 +40 -6
- package/lib/render-tree/svg.js +15 -7
- package/lib/renderer/bindings.js +244 -82
- 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 +27 -10
- package/lib/renderer/custom-element-reactions.js +42 -0
- package/lib/renderer/font-loading.js +227 -0
- package/lib/renderer/history.js +42 -0
- package/lib/renderer/host.js +36 -5
- package/lib/renderer/image-loading.js +27 -0
- package/lib/renderer/image-resources.js +65 -0
- package/lib/renderer/interfaces.js +28 -0
- package/lib/renderer/keyboard-input.js +12 -0
- package/lib/renderer/layout-observers.js +236 -0
- package/lib/renderer/main.js +33 -5
- package/lib/renderer/media-playback-events.js +13 -0
- package/lib/renderer/media-source.js +126 -55
- package/lib/renderer/page-realm.js +278 -147
- package/lib/renderer/page-renderer.js +840 -301
- package/lib/renderer/piano-input.js +22 -0
- package/lib/renderer/pointer-move.js +29 -0
- 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 +145 -0
- package/lib/renderer/workers.js +2 -2
- package/lib/style/computed-style.js +632 -108
- 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/compositor.test.js +38 -0
- package/test/computed-style-binding.test.js +151 -0
- package/test/computed-style.test.js +279 -1
- package/test/display-list.test.js +26 -0
- package/test/document-base.test.js +87 -0
- package/test/dom-platform.test.js +461 -0
- package/test/download.test.js +8 -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/headless-shell.test.js +22 -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-playback-events.test.js +22 -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 +160 -2
- package/test/paint-frame.test.js +103 -0
- package/test/paint-paragraph-cache.test.js +87 -0
- package/test/piano-input.test.js +44 -0
- package/test/playback-buffer.test.js +31 -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 +27 -5
- 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 +168 -0
- package/test/video-keyframe.test.js +61 -0
- package/test/video-presentation.test.js +136 -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
|
@@ -15,24 +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",
|
|
43
|
+
"none", "contents", "block", "flex", "grid", "inline", "inline-block", "inline-flex", "inline-grid", "list-item",
|
|
36
44
|
"table", "inline-table", "table-row-group", "table-header-group", "table-footer-group", "table-row",
|
|
37
45
|
"table-cell", "table-caption", "table-column", "table-column-group",
|
|
38
46
|
]);
|
|
@@ -40,6 +48,8 @@ const DISPLAYS = new Set([
|
|
|
40
48
|
const FONT_SIZE_KEYWORDS = Object.freeze({
|
|
41
49
|
"xx-small": 9, "x-small": 10, small: 13, medium: 16, large: 18, "x-large": 24, "xx-large": 32, "xxx-large": 48,
|
|
42
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 });
|
|
43
53
|
/** HTML §15.3.8 table elements and their display types. */
|
|
44
54
|
const TABLE_DISPLAYS = new Map(Object.entries({
|
|
45
55
|
TABLE: "table", CAPTION: "table-caption", COLGROUP: "table-column-group", COL: "table-column",
|
|
@@ -56,29 +66,40 @@ const BACKGROUND_REPEATS = new Set(["repeat", "repeat-x", "repeat-y", "no-repeat
|
|
|
56
66
|
const BACKGROUND_BOXES = new Set(["border-box", "padding-box", "content-box"]);
|
|
57
67
|
const BACKGROUND_ATTACHMENTS = new Set(["scroll", "fixed", "local"]);
|
|
58
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]);
|
|
59
71
|
const WHITE_SPACES = new Set(["normal", "nowrap", "pre", "pre-wrap"]);
|
|
60
72
|
// CSS Cascade 4 §7.3 CSS-wide keywords; revert is treated as unset (no user/UA origin split).
|
|
61
73
|
const CSS_WIDE_KEYWORDS = new Set(["inherit", "initial", "unset", "revert", "revert-layer"]);
|
|
62
74
|
// Computed-style fields a shorthand (or a property not named after its field) covers.
|
|
63
75
|
const PROPERTY_FIELDS = Object.freeze({
|
|
76
|
+
"-webkit-line-clamp": ["webkitLineClamp"],
|
|
77
|
+
"-webkit-box-orient": ["webkitBoxOrient"],
|
|
64
78
|
overflow: ["overflowX", "overflowY", "overflowScrollX", "overflowScrollY"],
|
|
65
79
|
"overflow-x": ["overflowX", "overflowScrollX"],
|
|
66
80
|
"overflow-y": ["overflowY", "overflowScrollY"],
|
|
67
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"],
|
|
68
85
|
flex: ["flexGrow", "flexShrink", "flexBasis"],
|
|
69
86
|
"flex-flow": ["flexDirection", "flexWrap"],
|
|
70
|
-
gap: ["rowGap", "columnGap", "gap"],
|
|
71
|
-
"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"],
|
|
72
93
|
"grid-area": ["gridRowStart", "gridColumnStart", "gridRowEnd", "gridColumnEnd"],
|
|
73
94
|
"grid-row": ["gridRowStart", "gridRowEnd"],
|
|
74
95
|
"grid-column": ["gridColumnStart", "gridColumnEnd"],
|
|
75
|
-
"grid-template": ["gridTemplateColumns", "gridTemplateRows"],
|
|
96
|
+
"grid-template": ["gridTemplateColumns", "gridTemplateRows", "gridTemplateAreas"],
|
|
76
97
|
"place-content": ["alignContent", "justifyContent"],
|
|
77
98
|
"place-items": ["alignItems", "justifyItems"],
|
|
78
99
|
"place-self": ["alignSelf", "justifySelf"],
|
|
79
100
|
"line-height": ["lineHeight", "lineHeightFactor"],
|
|
80
101
|
"list-style": ["listStyleType"],
|
|
81
|
-
"border-radius": ["borderRadius"],
|
|
102
|
+
"border-radius": ["borderRadius", ...CORNER_FIELDS],
|
|
82
103
|
"text-decoration": ["textDecorationLine", "textDecorationColor"],
|
|
83
104
|
"text-decoration-line": ["textDecorationLine"],
|
|
84
105
|
"text-decoration-color": ["textDecorationColor"],
|
|
@@ -88,12 +109,19 @@ const BORDER_FIELDS = Object.freeze(["borderSpecifiedWidths", "borderStyles", "b
|
|
|
88
109
|
|
|
89
110
|
const INITIAL = Object.freeze({
|
|
90
111
|
display: "inline",
|
|
112
|
+
direction: "ltr",
|
|
91
113
|
// text-decoration: this element's own lines ("none" or an array) and color
|
|
92
114
|
// (null = currentColor); textDecorations adds what ancestors propagate.
|
|
93
115
|
textDecorationLine: "none",
|
|
94
116
|
textDecorationColor: null,
|
|
95
117
|
textDecorations: null,
|
|
96
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",
|
|
97
125
|
colorScheme: "light",
|
|
98
126
|
backgroundColor: "rgba(0, 0, 0, 0)",
|
|
99
127
|
backgroundImage: null,
|
|
@@ -106,11 +134,15 @@ const INITIAL = Object.freeze({
|
|
|
106
134
|
fontFamily: Object.freeze(["sans-serif"]),
|
|
107
135
|
fontSize: 16,
|
|
108
136
|
fontWeight: 400,
|
|
137
|
+
fontStyle: "normal",
|
|
138
|
+
fontStretch: 100,
|
|
109
139
|
// null means `line-height: normal`, which layout resolves from font metrics.
|
|
110
140
|
lineHeight: null,
|
|
111
141
|
lineHeightFactor: null,
|
|
112
142
|
textAlign: "start",
|
|
113
143
|
whiteSpace: "normal",
|
|
144
|
+
webkitLineClamp: null,
|
|
145
|
+
webkitBoxOrient: "horizontal",
|
|
114
146
|
listStyleType: "disc",
|
|
115
147
|
margin: Object.freeze([0, 0, 0, 0]),
|
|
116
148
|
padding: Object.freeze([0, 0, 0, 0]),
|
|
@@ -127,6 +159,10 @@ const INITIAL = Object.freeze({
|
|
|
127
159
|
borderStyles: Object.freeze(["none", "none", "none", "none"]),
|
|
128
160
|
borderSpecifiedWidths: Object.freeze([3, 3, 3, 3]),
|
|
129
161
|
borderRadius: 0,
|
|
162
|
+
borderTopLeftRadius: ZERO_RADIUS,
|
|
163
|
+
borderTopRightRadius: ZERO_RADIUS,
|
|
164
|
+
borderBottomRightRadius: ZERO_RADIUS,
|
|
165
|
+
borderBottomLeftRadius: ZERO_RADIUS,
|
|
130
166
|
flexDirection: "row",
|
|
131
167
|
flexWrap: "nowrap",
|
|
132
168
|
justifyContent: "normal",
|
|
@@ -136,6 +172,10 @@ const INITIAL = Object.freeze({
|
|
|
136
172
|
gap: 0,
|
|
137
173
|
rowGap: 0,
|
|
138
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,
|
|
139
179
|
flexGrow: 0,
|
|
140
180
|
flexShrink: 1,
|
|
141
181
|
flexBasis: "auto",
|
|
@@ -145,12 +185,14 @@ const INITIAL = Object.freeze({
|
|
|
145
185
|
inset: Object.freeze(["auto", "auto", "auto", "auto"]),
|
|
146
186
|
zIndex: "auto",
|
|
147
187
|
transform: null,
|
|
188
|
+
translate: null,
|
|
148
189
|
// Raw text (default "50% 50%"); paint resolves it against the border box.
|
|
149
190
|
transformOrigin: null,
|
|
150
191
|
justifyItems: "normal",
|
|
151
192
|
justifySelf: "auto",
|
|
152
193
|
gridTemplateColumns: null,
|
|
153
194
|
gridTemplateRows: null,
|
|
195
|
+
gridTemplateAreas: null,
|
|
154
196
|
gridAutoColumns: Object.freeze([AUTO_TRACK]),
|
|
155
197
|
gridAutoRows: Object.freeze([AUTO_TRACK]),
|
|
156
198
|
gridAutoFlow: Object.freeze({ column: false, dense: false }),
|
|
@@ -184,6 +226,18 @@ const INITIAL = Object.freeze({
|
|
|
184
226
|
clipPath: null,
|
|
185
227
|
});
|
|
186
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
|
+
|
|
187
241
|
/**
|
|
188
242
|
* User-agent defaults from the HTML Standard's suggested rendering rules
|
|
189
243
|
* (WHATWG HTML §15.3, CC BY 4.0) for the elements the renderer lays out.
|
|
@@ -238,6 +292,7 @@ const UA_CONTROL_DECLARATIONS = Object.freeze({
|
|
|
238
292
|
select: parseDeclarations(`${FIELD}; border-radius: 2px; padding: 0 4px; white-space: pre`),
|
|
239
293
|
});
|
|
240
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"];
|
|
241
296
|
|
|
242
297
|
/** Which UA control defaults an element gets, or null. */
|
|
243
298
|
export function controlKind(element) {
|
|
@@ -266,6 +321,7 @@ const DEFAULT_ENVIRONMENT = Object.freeze({ viewportWidth: 800, viewportHeight:
|
|
|
266
321
|
let environment = DEFAULT_ENVIRONMENT;
|
|
267
322
|
|
|
268
323
|
export class StyleEngine {
|
|
324
|
+
#measureText;
|
|
269
325
|
#styles = new WeakMap();
|
|
270
326
|
#customProperties = new WeakMap();
|
|
271
327
|
#pseudoStyles = new WeakMap();
|
|
@@ -281,6 +337,10 @@ export class StyleEngine {
|
|
|
281
337
|
#parsedSheets = new Map();
|
|
282
338
|
#parsedSheetsKey = "";
|
|
283
339
|
|
|
340
|
+
constructor({ measureText = null } = {}) {
|
|
341
|
+
this.#measureText = measureText;
|
|
342
|
+
}
|
|
343
|
+
|
|
284
344
|
/**
|
|
285
345
|
* Computes the document's styles. With `dirty` (elements whose subtree's
|
|
286
346
|
* styles may have changed), only those subtrees are restyled and every
|
|
@@ -295,6 +355,7 @@ export class StyleEngine {
|
|
|
295
355
|
viewportHeight: viewport.height ?? DEFAULT_ENVIRONMENT.viewportHeight,
|
|
296
356
|
rootFontSize: DEFAULT_ENVIRONMENT.rootFontSize,
|
|
297
357
|
mobile: viewport.mobile === true,
|
|
358
|
+
measureText: this.#measureText,
|
|
298
359
|
};
|
|
299
360
|
this.restyled = 0;
|
|
300
361
|
try {
|
|
@@ -638,9 +699,37 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
638
699
|
else if (BLOCK_TAGS.has(element.tagName)) values.display = element.tagName === "LI" ? "list-item" : "block";
|
|
639
700
|
else if (["BUTTON", "INPUT", "SELECT", "TEXTAREA"].includes(element.tagName)) values.display = "inline-block";
|
|
640
701
|
if (["B", "STRONG"].includes(element.tagName)) values.fontWeight = 700;
|
|
702
|
+
if (["I", "EM", "CITE", "DFN", "VAR", "ADDRESS"].includes(element.tagName)) values.fontStyle = "italic";
|
|
641
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;
|
|
642
713
|
|
|
643
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
|
+
}
|
|
644
733
|
const isLink = (element.tagName === "A" || element.tagName === "AREA") && element.hasAttribute("href");
|
|
645
734
|
const control = controlKind(element);
|
|
646
735
|
if (control === "hidden") values.display = "none";
|
|
@@ -658,23 +747,58 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
658
747
|
declarations.set(declaration.name, { ...declaration, specificity: 1_000_000, order: Infinity });
|
|
659
748
|
}
|
|
660
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
|
+
}
|
|
661
759
|
// The font size comes first (em lengths use it), from whichever of
|
|
662
760
|
// font-size and the font shorthand wins the cascade.
|
|
663
761
|
const fontSize = declarations.get("font-size");
|
|
664
762
|
const font = declarations.get("font");
|
|
665
763
|
if (font && (!fontSize || compareDeclarations(font, fontSize) > 0)) {
|
|
666
|
-
const
|
|
667
|
-
|
|
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);
|
|
668
768
|
} else if (fontSize) {
|
|
669
769
|
applyDeclaration(values, fontSize, customProperties, parentStyle);
|
|
670
770
|
}
|
|
671
771
|
// light-dark() depends on the used scheme regardless of declaration order.
|
|
672
772
|
const colorScheme = declarations.get("color-scheme");
|
|
673
773
|
if (colorScheme) applyDeclaration(values, colorScheme, customProperties, parentStyle);
|
|
774
|
+
const directionDeclaration = declarations.get("direction");
|
|
775
|
+
if (directionDeclaration) applyDeclaration(values, directionDeclaration, customProperties, parentStyle);
|
|
674
776
|
// Apply in cascade order so a later shorthand overrides earlier longhands and vice versa.
|
|
675
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
|
+
}
|
|
676
800
|
for (const declaration of ordered) {
|
|
677
|
-
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;
|
|
678
802
|
// The shorthand's size was settled above; its other parts apply in order.
|
|
679
803
|
applyDeclaration(values, declaration.name === "font" ? { ...declaration, name: "font-parts" } : declaration, customProperties, parentStyle);
|
|
680
804
|
}
|
|
@@ -684,7 +808,7 @@ export function computeElementStyle(element, parentStyle = null, authorDeclarati
|
|
|
684
808
|
const parentDisplay = parentStyle?.display ?? "block";
|
|
685
809
|
const flexOrGridItem = ["flex", "inline-flex", "grid", "inline-grid"].includes(parentDisplay);
|
|
686
810
|
if (values.float !== "none" && (["absolute", "fixed"].includes(values.position) || flexOrGridItem)) values.float = "none";
|
|
687
|
-
if (values.float !== "none" && values.display
|
|
811
|
+
if (values.float !== "none" && !["none", "contents"].includes(values.display)) {
|
|
688
812
|
values.display = { flex: "inline-flex", grid: "inline-grid", "inline-flex": "inline-flex", "inline-grid": "inline-grid" }[values.display]
|
|
689
813
|
?? "inline-block";
|
|
690
814
|
}
|
|
@@ -861,6 +985,110 @@ export function parseStyleSheet(source, viewport = environment, counter = { orde
|
|
|
861
985
|
return rules;
|
|
862
986
|
}
|
|
863
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
|
+
|
|
864
1092
|
function newLayer(parent) {
|
|
865
1093
|
return { parent, children: new Map(), rank: Infinity };
|
|
866
1094
|
}
|
|
@@ -916,13 +1144,17 @@ function rankLayerTree(root) {
|
|
|
916
1144
|
}
|
|
917
1145
|
|
|
918
1146
|
/** Splits a `font` shorthand value into size, line height, weight and family. */
|
|
919
|
-
function parseFontShorthand(value) {
|
|
1147
|
+
export function parseFontShorthand(value) {
|
|
920
1148
|
const tokens = splitTopLevel(value, " ").filter(Boolean);
|
|
921
1149
|
let weight = null;
|
|
1150
|
+
let style = "normal";
|
|
1151
|
+
let stretch = "normal";
|
|
922
1152
|
for (let index = 0; index < tokens.length; index++) {
|
|
923
1153
|
const token = tokens[index];
|
|
924
1154
|
const lower = token.toLowerCase();
|
|
925
|
-
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;
|
|
926
1158
|
if (["bold", "bolder", "lighter"].includes(lower) || /^[1-9]00$/.test(lower)) {
|
|
927
1159
|
weight = lower === "bolder" ? "700" : lower === "lighter" ? "300" : lower;
|
|
928
1160
|
continue;
|
|
@@ -938,7 +1170,7 @@ function parseFontShorthand(value) {
|
|
|
938
1170
|
} else if (lineHeight === undefined && tokens[rest]?.startsWith("/")) lineHeight = tokens[rest++].slice(1); // "14px /1.5"
|
|
939
1171
|
const family = tokens.slice(rest).join(" ").trim();
|
|
940
1172
|
if (!family || !(FONT_SIZE_KEYWORDS[size] || /^[\d.]/.test(size) || /^(?:calc|clamp|min|max)\(/.test(size))) return null;
|
|
941
|
-
return { size, lineHeight: lineHeight || null, weight, family };
|
|
1173
|
+
return { size, lineHeight: lineHeight || null, weight, style, stretch, family };
|
|
942
1174
|
}
|
|
943
1175
|
return null;
|
|
944
1176
|
}
|
|
@@ -955,7 +1187,8 @@ function findTopLevel(source, character, start, end) {
|
|
|
955
1187
|
if (quote) {
|
|
956
1188
|
if (current === BACKSLASH) index++;
|
|
957
1189
|
else if (current === quote) quote = 0;
|
|
958
|
-
} else if (current ===
|
|
1190
|
+
} else if (current === BACKSLASH) index++;
|
|
1191
|
+
else if (current === QUOTE || current === APOSTROPHE) quote = current;
|
|
959
1192
|
else if (current === OPEN_PAREN) depth++;
|
|
960
1193
|
else if (current === CLOSE_PAREN) depth = Math.max(0, depth - 1);
|
|
961
1194
|
else if (current === target && depth === 0) return index;
|
|
@@ -972,7 +1205,8 @@ function matchingBrace(source, open, end) {
|
|
|
972
1205
|
if (quote) {
|
|
973
1206
|
if (current === BACKSLASH) index++;
|
|
974
1207
|
else if (current === quote) quote = 0;
|
|
975
|
-
} else if (current ===
|
|
1208
|
+
} else if (current === BACKSLASH) index++;
|
|
1209
|
+
else if (current === QUOTE || current === APOSTROPHE) quote = current;
|
|
976
1210
|
else if (current === OPEN_BRACE) depth++;
|
|
977
1211
|
else if (current === CLOSE_BRACE && --depth === 0) return index;
|
|
978
1212
|
}
|
|
@@ -1041,6 +1275,7 @@ function mediaLength(value) {
|
|
|
1041
1275
|
|
|
1042
1276
|
export function parseDeclarations(source) {
|
|
1043
1277
|
const declarations = new Map();
|
|
1278
|
+
let declarationIndex = 0;
|
|
1044
1279
|
for (const part of splitDeclarations(String(source))) {
|
|
1045
1280
|
const colon = part.indexOf(":");
|
|
1046
1281
|
if (colon < 1) continue;
|
|
@@ -1051,7 +1286,7 @@ export function parseDeclarations(source) {
|
|
|
1051
1286
|
const important = /\s*!important\s*$/i.test(value);
|
|
1052
1287
|
value = value.replace(/\s*!important\s*$/i, "").trim();
|
|
1053
1288
|
if (!name || !value || (declarations.get(name)?.important && !important)) continue;
|
|
1054
|
-
declarations.set(name, { name, value, important });
|
|
1289
|
+
declarations.set(name, { name, value, important, declarationIndex: declarationIndex++ });
|
|
1055
1290
|
}
|
|
1056
1291
|
return declarations;
|
|
1057
1292
|
}
|
|
@@ -1074,7 +1309,46 @@ function applyDeclaration(style, declaration, customProperties, parentStyle) {
|
|
|
1074
1309
|
}
|
|
1075
1310
|
}
|
|
1076
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
|
+
|
|
1077
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
|
+
}
|
|
1078
1352
|
if (CSS_WIDE_KEYWORDS.has(value)) {
|
|
1079
1353
|
const inherits = value === "inherit" || (value !== "initial" && INHERITED.has(name));
|
|
1080
1354
|
copyProperty(style, name, inherits && parentStyle ? parentStyle : INITIAL);
|
|
@@ -1082,6 +1356,9 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1082
1356
|
}
|
|
1083
1357
|
if (typeof value !== "string") return;
|
|
1084
1358
|
switch (name) {
|
|
1359
|
+
case "direction":
|
|
1360
|
+
if (value === "ltr" || value === "rtl") style.direction = value;
|
|
1361
|
+
break;
|
|
1085
1362
|
case "color-scheme": {
|
|
1086
1363
|
const schemes = value.toLowerCase().split(/\s+/).filter((part) => part !== "only");
|
|
1087
1364
|
if (schemes.every((part) => ["normal", "light", "dark"].includes(part))) {
|
|
@@ -1090,6 +1367,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1090
1367
|
break;
|
|
1091
1368
|
}
|
|
1092
1369
|
case "display": if (DISPLAYS.has(value)) style.display = value; break;
|
|
1370
|
+
case "-webkit-line-clamp":
|
|
1371
|
+
if (value === "none") style.webkitLineClamp = null;
|
|
1372
|
+
else if (/^[1-9]\d*$/.test(value)) style.webkitLineClamp = Number(value);
|
|
1373
|
+
break;
|
|
1374
|
+
case "-webkit-box-orient":
|
|
1375
|
+
if (["vertical", "horizontal"].includes(value)) style.webkitBoxOrient = value;
|
|
1376
|
+
break;
|
|
1093
1377
|
case "text-decoration-line": {
|
|
1094
1378
|
const lines = parseDecorationLines(value);
|
|
1095
1379
|
if (lines) style.textDecorationLine = lines;
|
|
@@ -1111,7 +1395,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1111
1395
|
if (!parsed) return;
|
|
1112
1396
|
lines = parsed;
|
|
1113
1397
|
} else if (DECORATION_STYLES.has(lower) || lower === "auto" || lower === "from-font"
|
|
1114
|
-
|| Number.isFinite(computedLength(token, style
|
|
1398
|
+
|| Number.isFinite(computedLength(token, style, true)) || /%$/.test(token)) {
|
|
1115
1399
|
continue;
|
|
1116
1400
|
} else if (lower === "currentcolor") {
|
|
1117
1401
|
color = null;
|
|
@@ -1166,7 +1450,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1166
1450
|
if (value === "auto") style.clip = null;
|
|
1167
1451
|
const rect = /^rect\(\s*(.*)\s*\)$/i.exec(value);
|
|
1168
1452
|
if (rect) {
|
|
1169
|
-
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)));
|
|
1170
1454
|
if (edges.length === 4 && edges.every((edge) => edge === "auto" || Number.isFinite(edge))) style.clip = Object.freeze(edges);
|
|
1171
1455
|
}
|
|
1172
1456
|
break;
|
|
@@ -1176,7 +1460,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1176
1460
|
if (value === "none") style.clipPath = null;
|
|
1177
1461
|
const inset = /^inset\(\s*([^)]*?)\s*(?:round\s[^)]*)?\)$/i.exec(value);
|
|
1178
1462
|
if (inset) {
|
|
1179
|
-
const box = boxLengths(inset[1], false, style
|
|
1463
|
+
const box = boxLengths(inset[1], false, style, false, true);
|
|
1180
1464
|
if (box) style.clipPath = Object.freeze({ inset: box });
|
|
1181
1465
|
} else if (/^rect\(\s*0(?:px)?\s+0(?:px)?\s+0(?:px)?\s+0(?:px)?\s*\)$/i.test(value)) {
|
|
1182
1466
|
style.clipPath = Object.freeze({ empty: true });
|
|
@@ -1188,6 +1472,25 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1188
1472
|
if (color) style.color = color;
|
|
1189
1473
|
break;
|
|
1190
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;
|
|
1191
1494
|
case "background-color": {
|
|
1192
1495
|
const color = resolvedColor(value, style.colorScheme);
|
|
1193
1496
|
if (color) style.backgroundColor = color;
|
|
@@ -1226,7 +1529,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1226
1529
|
break;
|
|
1227
1530
|
}
|
|
1228
1531
|
case "box-shadow": {
|
|
1229
|
-
const shadows = value === "none" ? null : parseBoxShadows(value, style
|
|
1532
|
+
const shadows = value === "none" ? null : parseBoxShadows(value, style);
|
|
1230
1533
|
if (value === "none" || shadows) style.boxShadow = shadows;
|
|
1231
1534
|
break;
|
|
1232
1535
|
}
|
|
@@ -1236,8 +1539,11 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1236
1539
|
break;
|
|
1237
1540
|
case "font-size": {
|
|
1238
1541
|
const parentSize = parentStyle?.fontSize ?? INITIAL.fontSize;
|
|
1542
|
+
const specified = computedLength(value, parentStyle ?? INITIAL, true);
|
|
1543
|
+
const relativeSize = specified?.unit === "%" ? parentSize * specified.value / 100
|
|
1544
|
+
: specified?.unit === "math" && specified.kind === "calc" ? specified.px + parentSize * specified.percent / 100 : specified;
|
|
1239
1545
|
const length = FONT_SIZE_KEYWORDS[value] ?? (value === "larger" ? parentSize * 1.2 : value === "smaller" ? parentSize / 1.2
|
|
1240
|
-
:
|
|
1546
|
+
: relativeSize);
|
|
1241
1547
|
if (length > 0) style.fontSize = length;
|
|
1242
1548
|
break;
|
|
1243
1549
|
}
|
|
@@ -1250,8 +1556,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1250
1556
|
if (!parsed) break;
|
|
1251
1557
|
if (name === "font") setProperty(style, "font-size", parsed.size, parentStyle);
|
|
1252
1558
|
setProperty(style, "line-height", parsed.lineHeight ?? "normal", parentStyle);
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
+
}
|
|
1255
1565
|
break;
|
|
1256
1566
|
}
|
|
1257
1567
|
case "position":
|
|
@@ -1261,13 +1571,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1261
1571
|
case "right":
|
|
1262
1572
|
case "bottom":
|
|
1263
1573
|
case "left": {
|
|
1264
|
-
const length = value === "auto" ? "auto" : computedLength(value, style
|
|
1574
|
+
const length = value === "auto" ? "auto" : computedLength(value, style, true);
|
|
1265
1575
|
if (length === "auto" || Number.isFinite(length) || length?.unit) style.inset = style.inset.with(BOX_SIDES.indexOf(name), length);
|
|
1266
1576
|
break;
|
|
1267
1577
|
}
|
|
1268
1578
|
case "inset": {
|
|
1269
1579
|
const parts = splitTopLevel(value, " ").filter(Boolean)
|
|
1270
|
-
.map((part) => part === "auto" ? "auto" : computedLength(part, style
|
|
1580
|
+
.map((part) => part === "auto" ? "auto" : computedLength(part, style, true));
|
|
1271
1581
|
if (!parts.length || parts.length > 4 || parts.some((part) => part !== "auto" && !Number.isFinite(part) && !part?.unit)) break;
|
|
1272
1582
|
const [top, right = top, bottom = top, left = right] = parts;
|
|
1273
1583
|
style.inset = [top, right, bottom, left];
|
|
@@ -1279,10 +1589,19 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1279
1589
|
break;
|
|
1280
1590
|
}
|
|
1281
1591
|
case "transform": {
|
|
1282
|
-
const transform = value === "none" ? null : parseTransform(value, style
|
|
1592
|
+
const transform = value === "none" ? null : parseTransform(value, style);
|
|
1283
1593
|
if (transform !== undefined) style.transform = transform;
|
|
1284
1594
|
break;
|
|
1285
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
|
+
}
|
|
1286
1605
|
case "transform-origin":
|
|
1287
1606
|
style.transformOrigin = value.toLowerCase().replace(/\s+/g, " ");
|
|
1288
1607
|
break;
|
|
@@ -1310,9 +1629,16 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1310
1629
|
if (Number.isFinite(weight) && weight >= 1 && weight <= 1000) style.fontWeight = weight;
|
|
1311
1630
|
break;
|
|
1312
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
|
+
}
|
|
1313
1639
|
case "white-space": if (WHITE_SPACES.has(value)) style.whiteSpace = value; break;
|
|
1314
1640
|
case "border-spacing": {
|
|
1315
|
-
const parts = value.split(/\s+/).map((part) => computedLength(part, style
|
|
1641
|
+
const parts = value.split(/\s+/).map((part) => computedLength(part, style));
|
|
1316
1642
|
if (parts.length <= 2 && parts.every((part) => Number.isFinite(part) && part >= 0)) {
|
|
1317
1643
|
style.borderSpacing = Object.freeze([parts[0], parts[1] ?? parts[0]]);
|
|
1318
1644
|
}
|
|
@@ -1356,7 +1682,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1356
1682
|
// Non-negative, so 0 is valid (icon buttons set line-height: 0); a
|
|
1357
1683
|
// percentage computes to a length against this element's font size.
|
|
1358
1684
|
const percentage = /^(\d+\.?\d*|\.\d+)%$/.exec(value);
|
|
1359
|
-
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);
|
|
1360
1686
|
if (length >= 0) {
|
|
1361
1687
|
style.lineHeight = length;
|
|
1362
1688
|
style.lineHeightFactor = null;
|
|
@@ -1365,12 +1691,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1365
1691
|
break;
|
|
1366
1692
|
}
|
|
1367
1693
|
case "margin": {
|
|
1368
|
-
const box = boxLengths(value, true, style
|
|
1694
|
+
const box = boxLengths(value, true, style, true);
|
|
1369
1695
|
if (box) style.margin = box;
|
|
1370
1696
|
break;
|
|
1371
1697
|
}
|
|
1372
1698
|
case "padding": {
|
|
1373
|
-
const box = boxLengths(value, false, style
|
|
1699
|
+
const box = boxLengths(value, false, style, false, true);
|
|
1374
1700
|
if (box) style.padding = box;
|
|
1375
1701
|
break;
|
|
1376
1702
|
}
|
|
@@ -1384,15 +1710,16 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1384
1710
|
case "padding-left": {
|
|
1385
1711
|
const [box, side] = name.split("-");
|
|
1386
1712
|
const isMargin = box === "margin";
|
|
1387
|
-
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style
|
|
1388
|
-
if (length === "auto" || (
|
|
1713
|
+
const length = isMargin && value === "auto" ? "auto" : computedLength(value, style, !isMargin);
|
|
1714
|
+
if (length === "auto" || (!isMargin && isLength(length) && typeof length === "object"
|
|
1715
|
+
&& (!isPercentage(length) || length.value >= 0)) || (Number.isFinite(length) && (isMargin || length >= 0))) {
|
|
1389
1716
|
style[box] = style[box].with(BOX_SIDES.indexOf(side), length);
|
|
1390
1717
|
}
|
|
1391
1718
|
break;
|
|
1392
1719
|
}
|
|
1393
1720
|
case "width":
|
|
1394
1721
|
case "height": {
|
|
1395
|
-
const length = value === "auto" ? "auto" : computedLength(value, style
|
|
1722
|
+
const length = value === "auto" ? "auto" : computedLength(value, style, true);
|
|
1396
1723
|
if (length === "auto" || isLength(length)) style[name] = length;
|
|
1397
1724
|
break;
|
|
1398
1725
|
}
|
|
@@ -1401,7 +1728,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1401
1728
|
case "min-height":
|
|
1402
1729
|
case "max-height": {
|
|
1403
1730
|
const none = name.startsWith("max-") && value === "none";
|
|
1404
|
-
const length = none ? "none" : computedLength(value, style
|
|
1731
|
+
const length = none ? "none" : computedLength(value, style, true);
|
|
1405
1732
|
if (none || isLength(length)) {
|
|
1406
1733
|
style[{ "min-width": "minWidth", "max-width": "maxWidth", "min-height": "minHeight", "max-height": "maxHeight" }[name]] = length;
|
|
1407
1734
|
}
|
|
@@ -1413,7 +1740,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1413
1740
|
case "border-bottom":
|
|
1414
1741
|
case "border-left": {
|
|
1415
1742
|
// Shorthands reset width (medium), style (none) and color (currentColor).
|
|
1416
|
-
const parsed = parseBorderShorthand(value, style
|
|
1743
|
+
const parsed = parseBorderShorthand(value, style);
|
|
1417
1744
|
if (!parsed) break;
|
|
1418
1745
|
const sides = name === "border" ? [0, 1, 2, 3] : [BOX_SIDES.indexOf(name.slice(7))];
|
|
1419
1746
|
for (const side of sides) {
|
|
@@ -1427,7 +1754,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1427
1754
|
case "border-style":
|
|
1428
1755
|
case "border-color": {
|
|
1429
1756
|
const parts = splitTopLevel(value, " ").filter(Boolean).map((part) => name === "border-width"
|
|
1430
|
-
? borderWidth(part, style
|
|
1757
|
+
? borderWidth(part, style)
|
|
1431
1758
|
: name === "border-style" ? (BORDER_STYLES.has(part) ? part : null) : borderColor(part));
|
|
1432
1759
|
if (!parts.length || parts.length > 4 || parts.some((part) => part === undefined || (name !== "border-color" && part == null))) break;
|
|
1433
1760
|
const [top, right = top, bottom = top, left = right] = parts;
|
|
@@ -1443,7 +1770,7 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1443
1770
|
const [, side, part] = name.split("-");
|
|
1444
1771
|
const index = BOX_SIDES.indexOf(side);
|
|
1445
1772
|
if (part === "width") {
|
|
1446
|
-
const width = borderWidth(value, style
|
|
1773
|
+
const width = borderWidth(value, style);
|
|
1447
1774
|
if (width != null) style.borderSpecifiedWidths = style.borderSpecifiedWidths.with(index, width);
|
|
1448
1775
|
} else if (part === "style") {
|
|
1449
1776
|
if (BORDER_STYLES.has(value)) style.borderStyles = style.borderStyles.with(index, value);
|
|
@@ -1454,12 +1781,40 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1454
1781
|
break;
|
|
1455
1782
|
}
|
|
1456
1783
|
case "border-radius": {
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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]]);
|
|
1460
1800
|
break;
|
|
1461
1801
|
}
|
|
1462
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
|
+
}
|
|
1463
1818
|
case "flex-direction": if (["row", "column"].includes(value)) style.flexDirection = value; break;
|
|
1464
1819
|
case "flex-wrap": if (["nowrap", "wrap"].includes(value)) style.flexWrap = value; break;
|
|
1465
1820
|
case "justify-content": {
|
|
@@ -1491,12 +1846,16 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1491
1846
|
break;
|
|
1492
1847
|
}
|
|
1493
1848
|
case "gap": {
|
|
1494
|
-
const
|
|
1495
|
-
|
|
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));
|
|
1496
1853
|
if (row >= 0 && column >= 0) {
|
|
1497
1854
|
style.gap = row;
|
|
1498
1855
|
style.rowGap = row;
|
|
1499
1856
|
style.columnGap = column;
|
|
1857
|
+
style.rowGapNormal = parts[0] === "normal";
|
|
1858
|
+
style.columnGapNormal = (parts[1] ?? parts[0]) === "normal";
|
|
1500
1859
|
}
|
|
1501
1860
|
break;
|
|
1502
1861
|
}
|
|
@@ -1505,13 +1864,50 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1505
1864
|
case "grid-column-gap": return setProperty(style, "column-gap", value, parentStyle);
|
|
1506
1865
|
case "grid-template-columns":
|
|
1507
1866
|
case "grid-template-rows": {
|
|
1508
|
-
const tracks = value === "none" ? null : parseTrackList(value, style
|
|
1867
|
+
const tracks = value === "none" ? null : parseTrackList(value, style);
|
|
1509
1868
|
if (value === "none" || tracks) style[name === "grid-template-columns" ? "gridTemplateColumns" : "gridTemplateRows"] = tracks;
|
|
1510
1869
|
break;
|
|
1511
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
|
+
}
|
|
1512
1908
|
case "grid-auto-columns":
|
|
1513
1909
|
case "grid-auto-rows": {
|
|
1514
|
-
const tracks = parseTrackList(value, style
|
|
1910
|
+
const tracks = parseTrackList(value, style);
|
|
1515
1911
|
if (tracks?.length && tracks.every((entry) => !entry.repeat)) {
|
|
1516
1912
|
style[name === "grid-auto-columns" ? "gridAutoColumns" : "gridAutoRows"] = tracks;
|
|
1517
1913
|
}
|
|
@@ -1537,12 +1933,13 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1537
1933
|
const [start, end] = value.split("/").map((part) => parseGridLine(part.trim()));
|
|
1538
1934
|
const axis = name === "grid-column" ? "Column" : "Row";
|
|
1539
1935
|
style[`grid${axis}Start`] = start;
|
|
1540
|
-
// A
|
|
1541
|
-
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");
|
|
1542
1938
|
break;
|
|
1543
1939
|
}
|
|
1544
1940
|
case "grid-area": {
|
|
1545
|
-
const [rowStart, columnStart =
|
|
1941
|
+
const [rowStart, columnStart = rowStart.name ? rowStart : "auto",
|
|
1942
|
+
rowEnd = rowStart.name ? rowStart : "auto", columnEnd = columnStart.name ? columnStart : "auto"] = value.split("/")
|
|
1546
1943
|
.map((part) => parseGridLine(part.trim()));
|
|
1547
1944
|
Object.assign(style, { gridRowStart: rowStart, gridColumnStart: columnStart, gridRowEnd: rowEnd, gridColumnEnd: columnEnd });
|
|
1548
1945
|
break;
|
|
@@ -1556,8 +1953,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1556
1953
|
}
|
|
1557
1954
|
case "row-gap":
|
|
1558
1955
|
case "column-gap": {
|
|
1559
|
-
const length = value === "normal" ? 0 : computedLength(value, style
|
|
1560
|
-
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
|
+
}
|
|
1561
1962
|
break;
|
|
1562
1963
|
}
|
|
1563
1964
|
case "align-self": {
|
|
@@ -1572,12 +1973,12 @@ function setProperty(style, name, value, parentStyle = null) {
|
|
|
1572
1973
|
break;
|
|
1573
1974
|
}
|
|
1574
1975
|
case "flex-basis": {
|
|
1575
|
-
const basis = flexBasis(value, style
|
|
1976
|
+
const basis = flexBasis(value, style);
|
|
1576
1977
|
if (basis != null) style.flexBasis = basis;
|
|
1577
1978
|
break;
|
|
1578
1979
|
}
|
|
1579
1980
|
case "flex": {
|
|
1580
|
-
const flex = flexShorthand(value, style
|
|
1981
|
+
const flex = flexShorthand(value, style);
|
|
1581
1982
|
if (flex) Object.assign(style, flex);
|
|
1582
1983
|
break;
|
|
1583
1984
|
}
|
|
@@ -1791,7 +2192,7 @@ function cascadeDeclarations(element, rules, pseudo = null, keys = elementKeys(e
|
|
|
1791
2192
|
for (const declaration of rule.declarationList) {
|
|
1792
2193
|
const current = result.get(declaration.name);
|
|
1793
2194
|
if (current && compareCascadeParts(declaration.important, layer, specificity, order, current) < 0) continue;
|
|
1794
|
-
result.set(declaration.name, {
|
|
2195
|
+
result.set(declaration.name, { ...declaration, specificity, order, layer });
|
|
1795
2196
|
}
|
|
1796
2197
|
}
|
|
1797
2198
|
return result;
|
|
@@ -1826,7 +2227,10 @@ function compareDeclarations(left, right) {
|
|
|
1826
2227
|
const leftSpecificity = "specificity" in left ? left.specificity : 0;
|
|
1827
2228
|
const rightSpecificity = "specificity" in right ? right.specificity : 0;
|
|
1828
2229
|
if (leftSpecificity !== rightSpecificity) return leftSpecificity - rightSpecificity;
|
|
1829
|
-
|
|
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);
|
|
1830
2234
|
}
|
|
1831
2235
|
|
|
1832
2236
|
const PSEUDO_ELEMENT = /(?:::?(before|after)|::([\w-]+))$/i;
|
|
@@ -1929,53 +2333,75 @@ function pseudoElement(pseudo) {
|
|
|
1929
2333
|
return { tagName: `::${pseudo}`, getAttribute: () => null, hasAttribute: () => false };
|
|
1930
2334
|
}
|
|
1931
2335
|
|
|
1932
|
-
/**
|
|
1933
|
-
* Keeps the translation part of a transform list as { x, y } lengths (px or
|
|
1934
|
-
* percentages of the element's own border box). Other functions are ignored
|
|
1935
|
-
* until transforms are painted with matrices.
|
|
1936
|
-
*/
|
|
1937
2336
|
/**
|
|
1938
2337
|
* The translation (layout moves the box) and scale (paint scales it about
|
|
1939
|
-
* 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.
|
|
1940
2341
|
*/
|
|
1941
2342
|
function parseTransform(value, fontSize) {
|
|
1942
|
-
|
|
1943
|
-
|
|
2343
|
+
let x = 0;
|
|
2344
|
+
let y = 0;
|
|
1944
2345
|
let scaleX = 1;
|
|
1945
2346
|
let scaleY = 1;
|
|
1946
2347
|
let found = false;
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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;
|
|
1950
2359
|
const kind = match[1].toLowerCase();
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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;
|
|
1956
2380
|
}
|
|
1957
2381
|
found = true;
|
|
1958
2382
|
}
|
|
1959
|
-
if (!found
|
|
1960
|
-
return Object.freeze(
|
|
2383
|
+
if (!found) return null;
|
|
2384
|
+
return Object.freeze(scaleX === 1 && scaleY === 1 ? { x, y } : { x, y, scaleX, scaleY });
|
|
1961
2385
|
}
|
|
1962
2386
|
|
|
1963
|
-
function
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
}
|
|
1978
|
-
|
|
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 });
|
|
1979
2405
|
}
|
|
1980
2406
|
|
|
1981
2407
|
/** Parses `content` into its string, null for none/normal, undefined if unsupported. */
|
|
@@ -2057,12 +2483,22 @@ function copyProperty(style, name, source) {
|
|
|
2057
2483
|
style[side[1]] = style[side[1]].with(index, source[side[1]][index]);
|
|
2058
2484
|
return;
|
|
2059
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
|
+
}
|
|
2060
2491
|
// border, border-<side>, border-<width|style|color>, border-<side>-<...>;
|
|
2061
2492
|
// not border-radius, border-spacing or border-collapse.
|
|
2062
|
-
const
|
|
2063
|
-
|
|
2064
|
-
?
|
|
2065
|
-
|
|
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())];
|
|
2066
2502
|
for (const field of fields) {
|
|
2067
2503
|
if (Object.hasOwn(INITIAL, field)) style[field] = source[field];
|
|
2068
2504
|
}
|
|
@@ -2074,6 +2510,9 @@ function cssValue(style, name) {
|
|
|
2074
2510
|
"font-family": style.fontFamily.map((family) => JSON.stringify(family)).join(", "),
|
|
2075
2511
|
"font-size": `${style.fontSize}px`,
|
|
2076
2512
|
"font-weight": String(style.fontWeight),
|
|
2513
|
+
"font-style": style.fontStyle,
|
|
2514
|
+
"font-stretch": `${style.fontStretch}%`,
|
|
2515
|
+
"font-width": `${style.fontStretch}%`,
|
|
2077
2516
|
"line-height": style.lineHeightFactor != null ? String(style.lineHeightFactor)
|
|
2078
2517
|
: style.lineHeight == null ? "normal" : `${style.lineHeight}px`,
|
|
2079
2518
|
"list-style-type": style.listStyleType,
|
|
@@ -2089,7 +2528,22 @@ function pixelLength(value) {
|
|
|
2089
2528
|
return match ? Number(match[1]) : NaN;
|
|
2090
2529
|
}
|
|
2091
2530
|
|
|
2092
|
-
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) {
|
|
2093
2547
|
const text = String(value).trim();
|
|
2094
2548
|
if (allowPercentage) {
|
|
2095
2549
|
const percentage = /^([+-]?(?:\d+\.?\d*|\.\d+))%$/.exec(text);
|
|
@@ -2098,14 +2552,14 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2098
2552
|
const absolute = pixelLength(text);
|
|
2099
2553
|
if (Number.isFinite(absolute)) return absolute;
|
|
2100
2554
|
if (/^calc\(/i.test(text)) {
|
|
2101
|
-
const sum = calcLength(text,
|
|
2555
|
+
const sum = calcLength(text, font);
|
|
2102
2556
|
if (!sum || !(sum.percent === 0 || allowPercentage)) return NaN;
|
|
2103
2557
|
return sum.percent === 0 ? sum.px : Object.freeze({ unit: "math", kind: "calc", px: sum.px, percent: sum.percent });
|
|
2104
2558
|
}
|
|
2105
2559
|
const math = /^(clamp|min|max)\((.*)\)$/i.exec(text);
|
|
2106
2560
|
if (math) {
|
|
2107
2561
|
const kind = math[1].toLowerCase();
|
|
2108
|
-
const values = splitTopLevel(math[2], ",").map((part) => computedLength(part,
|
|
2562
|
+
const values = splitTopLevel(math[2], ",").map((part) => computedLength(part, font, allowPercentage));
|
|
2109
2563
|
if (!values.length || values.some((part) => !Number.isFinite(part) && !(part?.unit))) return NaN;
|
|
2110
2564
|
// Percentages inside math functions resolve at layout time.
|
|
2111
2565
|
if (values.some((part) => part?.unit)) {
|
|
@@ -2117,14 +2571,38 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2117
2571
|
if (kind === "max") return Math.max(...values);
|
|
2118
2572
|
return values.length === 3 ? Math.max(values[0], Math.min(values[1], values[2])) : NaN;
|
|
2119
2573
|
}
|
|
2120
|
-
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);
|
|
2121
2575
|
if (!relative) return NaN;
|
|
2122
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
|
+
}
|
|
2123
2603
|
const scale = unit === "rem" ? environment.rootFontSize
|
|
2124
2604
|
: unit === "em" ? emSize
|
|
2125
|
-
: unit === "
|
|
2126
|
-
: unit === "vh" ? environment.viewportHeight / 100
|
|
2127
|
-
: 4 / 3;
|
|
2605
|
+
: unit === "ch" ? characterWidth : 4 / 3;
|
|
2128
2606
|
return Number(relative[1]) * scale;
|
|
2129
2607
|
}
|
|
2130
2608
|
|
|
@@ -2135,7 +2613,7 @@ function computedLength(value, emSize, allowPercentage = false) {
|
|
|
2135
2613
|
* for anything else (such as min() of a percentage inside calc()).
|
|
2136
2614
|
*/
|
|
2137
2615
|
function calcLength(text, emSize) {
|
|
2138
|
-
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);
|
|
2139
2617
|
if (!tokens || tokens.join("").replace(/\s+/g, "") !== String(text).replace(/\s+/g, "")) return null;
|
|
2140
2618
|
let index = 0;
|
|
2141
2619
|
// Values are { px, percent } (a length) or { number }.
|
|
@@ -2329,10 +2807,47 @@ function parseGridLine(token) {
|
|
|
2329
2807
|
if (span) return Object.freeze({ span: Math.max(1, Number(span[1] ?? span[2])) });
|
|
2330
2808
|
const line = Number(token);
|
|
2331
2809
|
if (Number.isInteger(line) && line !== 0) return Object.freeze({ line });
|
|
2332
|
-
|
|
2810
|
+
if (/^-?(?:[a-zA-Z_\u0080-\uFFFF]|--)[\w\u0080-\uFFFF-]*$/.test(token) && token !== "span") {
|
|
2811
|
+
return Object.freeze({ name: token });
|
|
2812
|
+
}
|
|
2333
2813
|
return "auto";
|
|
2334
2814
|
}
|
|
2335
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
|
+
|
|
2336
2851
|
const BORDER_STYLES = new Set(["none", "hidden", "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"]);
|
|
2337
2852
|
const BORDER_WIDTH_KEYWORDS = Object.freeze({ thin: 1, medium: 3, thick: 5 });
|
|
2338
2853
|
|
|
@@ -2370,12 +2885,12 @@ function flexBasis(value, fontSize) {
|
|
|
2370
2885
|
function flexShorthand(value, fontSize) {
|
|
2371
2886
|
if (value === "none") return { flexGrow: 0, flexShrink: 0, flexBasis: "auto" };
|
|
2372
2887
|
if (value === "auto") return { flexGrow: 1, flexShrink: 1, flexBasis: "auto" };
|
|
2373
|
-
const parts = value.trim().
|
|
2888
|
+
const parts = splitTopLevel(value.trim(), " ").filter(Boolean);
|
|
2374
2889
|
const numbers = [];
|
|
2375
2890
|
let basis = null;
|
|
2376
2891
|
for (const part of parts) {
|
|
2377
2892
|
const number = Number(part);
|
|
2378
|
-
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);
|
|
2379
2894
|
else if (basis == null && (basis = flexBasis(part, fontSize)) != null) continue;
|
|
2380
2895
|
else return null;
|
|
2381
2896
|
}
|
|
@@ -2384,7 +2899,9 @@ function flexShorthand(value, fontSize) {
|
|
|
2384
2899
|
return {
|
|
2385
2900
|
flexGrow: numbers[0] ?? 1,
|
|
2386
2901
|
flexShrink: numbers[1] ?? 1,
|
|
2387
|
-
|
|
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"),
|
|
2388
2905
|
};
|
|
2389
2906
|
}
|
|
2390
2907
|
|
|
@@ -2460,12 +2977,18 @@ export function absolutizeStyleSheetUrls(text, base) {
|
|
|
2460
2977
|
function splitTopLevel(source, separator) {
|
|
2461
2978
|
const parts = [];
|
|
2462
2979
|
let depth = 0;
|
|
2980
|
+
let quote = "";
|
|
2463
2981
|
let start = 0;
|
|
2464
2982
|
for (let index = 0; index < source.length; index++) {
|
|
2465
2983
|
const character = source[index];
|
|
2466
|
-
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++;
|
|
2467
2990
|
else if (character === ")") depth--;
|
|
2468
|
-
else if (character === separator && depth === 0) {
|
|
2991
|
+
else if ((character === separator || separator === " " && /\s/.test(character)) && depth === 0) {
|
|
2469
2992
|
parts.push(source.slice(start, index).trim());
|
|
2470
2993
|
start = index + 1;
|
|
2471
2994
|
}
|
|
@@ -2475,10 +2998,11 @@ function splitTopLevel(source, separator) {
|
|
|
2475
2998
|
}
|
|
2476
2999
|
|
|
2477
3000
|
function boxLengths(value, allowNegative, emSize, allowAuto, allowPercentage = false) {
|
|
2478
|
-
const parts = value.trim().
|
|
3001
|
+
const parts = splitTopLevel(value.trim(), " ").filter(Boolean).map((part) =>
|
|
2479
3002
|
allowAuto && part === "auto" ? "auto" : computedLength(part, emSize, allowPercentage));
|
|
2480
3003
|
if (parts.length < 1 || parts.length > 4 || parts.some((part) =>
|
|
2481
|
-
part !== "auto" && !
|
|
3004
|
+
part !== "auto" && !(allowPercentage && isLength(part) && typeof part === "object"
|
|
3005
|
+
&& (allowNegative || !isPercentage(part) || part.value >= 0)) && (!Number.isFinite(part) || (!allowNegative && part < 0)))) return null;
|
|
2482
3006
|
const [top, right = top, bottom = top, left = right] = parts.length === 3
|
|
2483
3007
|
? [parts[0], parts[1], parts[2], parts[1]]
|
|
2484
3008
|
: parts.length === 2
|