Skip to content

Commit da7e9cd

Browse files
authoredOct 4, 2024··
experimental: support nested and attribute selectors (#4213)
## Description Needed to support all wf presets for fixing this #4155 ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 5de6) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent f1aa37c commit da7e9cd

File tree

2 files changed

+99
-47
lines changed

2 files changed

+99
-47
lines changed
 

‎packages/css-data/src/parse-css.test.ts

+50-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe("Parse CSS", () => {
55
test("longhand property name with keyword value", () => {
66
expect(parseCss(`.test { background-color: red }`)).toEqual([
77
{
8-
selector: "test",
8+
selector: ".test",
99
property: "backgroundColor",
1010
value: { type: "keyword", value: "red" },
1111
},
@@ -15,7 +15,7 @@ describe("Parse CSS", () => {
1515
test("one class selector rules", () => {
1616
expect(parseCss(`.test { color: #ff0000 }`)).toEqual([
1717
{
18-
selector: "test",
18+
selector: ".test",
1919
property: "color",
2020
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
2121
},
@@ -30,7 +30,7 @@ describe("Parse CSS", () => {
3030
`;
3131
expect(parseCss(css)).toEqual([
3232
{
33-
selector: "test",
33+
selector: ".test",
3434
property: "backgroundImage",
3535
value: {
3636
type: "layers",
@@ -45,7 +45,7 @@ describe("Parse CSS", () => {
4545
},
4646
},
4747
{
48-
selector: "test",
48+
selector: ".test",
4949
property: "backgroundPositionX",
5050
value: {
5151
type: "layers",
@@ -56,7 +56,7 @@ describe("Parse CSS", () => {
5656
},
5757
},
5858
{
59-
selector: "test",
59+
selector: ".test",
6060
property: "backgroundPositionY",
6161
value: {
6262
type: "layers",
@@ -67,7 +67,7 @@ describe("Parse CSS", () => {
6767
},
6868
},
6969
{
70-
selector: "test",
70+
selector: ".test",
7171
property: "backgroundSize",
7272
value: {
7373
type: "layers",
@@ -90,7 +90,7 @@ describe("Parse CSS", () => {
9090
},
9191
},
9292
{
93-
selector: "test",
93+
selector: ".test",
9494
property: "backgroundRepeat",
9595
value: {
9696
type: "layers",
@@ -101,7 +101,7 @@ describe("Parse CSS", () => {
101101
},
102102
},
103103
{
104-
selector: "test",
104+
selector: ".test",
105105
property: "backgroundAttachment",
106106
value: {
107107
type: "layers",
@@ -112,7 +112,7 @@ describe("Parse CSS", () => {
112112
},
113113
},
114114
{
115-
selector: "test",
115+
selector: ".test",
116116
property: "backgroundOrigin",
117117
value: {
118118
type: "layers",
@@ -123,7 +123,7 @@ describe("Parse CSS", () => {
123123
},
124124
},
125125
{
126-
selector: "test",
126+
selector: ".test",
127127
property: "backgroundClip",
128128
value: {
129129
type: "layers",
@@ -134,7 +134,7 @@ describe("Parse CSS", () => {
134134
},
135135
},
136136
{
137-
selector: "test",
137+
selector: ".test",
138138
property: "backgroundColor",
139139
value: { alpha: 1, b: 252, g: 255, r: 235, type: "rgb" },
140140
},
@@ -149,31 +149,31 @@ describe("Parse CSS", () => {
149149
`;
150150
expect(parseCss(css)).toEqual([
151151
{
152-
selector: "test",
152+
selector: ".test",
153153
property: "backgroundImage",
154154
value: {
155155
type: "layers",
156156
value: [{ type: "keyword", value: "none" }],
157157
},
158158
},
159159
{
160-
selector: "test",
160+
selector: ".test",
161161
property: "backgroundPositionX",
162162
value: {
163163
type: "layers",
164164
value: [{ type: "unit", unit: "px", value: 0 }],
165165
},
166166
},
167167
{
168-
selector: "test",
168+
selector: ".test",
169169
property: "backgroundPositionY",
170170
value: {
171171
type: "layers",
172172
value: [{ type: "unit", unit: "px", value: 0 }],
173173
},
174174
},
175175
{
176-
selector: "test",
176+
selector: ".test",
177177
property: "backgroundSize",
178178
value: {
179179
type: "layers",
@@ -194,7 +194,18 @@ describe("Parse CSS", () => {
194194
]);
195195
});
196196

197+
test("attribute selector", () => {
198+
expect(parseCss(`[class^="a"] { color: #ff0000 }`)).toEqual([
199+
{
200+
selector: '[class^="a"]',
201+
property: "color",
202+
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
203+
},
204+
]);
205+
});
206+
197207
test("parse first pseudo class as selector", () => {
208+
// E.g. :root
198209
expect(parseCss(`:first-pseudo:my-state { color: #ff0000 }`)).toEqual([
199210
{
200211
selector: ":first-pseudo",
@@ -471,11 +482,33 @@ describe("Parse CSS", () => {
471482
});
472483

473484
test("parse child combinator", () => {
474-
expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([]);
485+
expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([
486+
{
487+
selector: "a > b",
488+
property: "color",
489+
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
490+
},
491+
]);
475492
});
476493

477494
test("parse space combinator", () => {
478-
expect(parseCss(`a b { color: #ff0000 }`)).toEqual([]);
495+
expect(parseCss(`.a b { color: #ff0000 }`)).toEqual([
496+
{
497+
selector: ".a b",
498+
property: "color",
499+
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
500+
},
501+
]);
502+
});
503+
504+
test("parse nested selectors as one token", () => {
505+
expect(parseCss(`a b c.d { color: #ff0000 }`)).toEqual([
506+
{
507+
selector: "a b c.d",
508+
property: "color",
509+
value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" },
510+
},
511+
]);
479512
});
480513
});
481514

‎packages/css-data/src/parse-css.ts

+49-30
Original file line numberDiff line numberDiff line change
@@ -161,51 +161,70 @@ export const parseCss = (css: string, options: ParserOptions = {}) => {
161161
if (node.type === "MediaQuery" && node.children.size > 1) {
162162
invalidBreakpoint = true;
163163
}
164+
``;
164165
},
165166
});
166167
const generated = csstree.generate(this.atrule.prelude);
167168
if (generated) {
168169
breakpoint = generated;
169170
}
170171
}
171-
if (invalidBreakpoint) {
172+
if (invalidBreakpoint || this.rule.prelude.type !== "SelectorList") {
172173
return;
173174
}
174175

175176
const selectors: Selector[] = [];
176-
if (this.rule.prelude.type === "SelectorList") {
177-
for (const selector of this.rule.prelude.children) {
178-
if (selector.type !== "Selector" || selector.children.size > 2) {
179-
continue;
180-
}
181-
const [nameNode, stateNode] = selector.children;
182-
let name;
183-
if (
184-
nameNode.type === "ClassSelector" ||
185-
nameNode.type === "TypeSelector"
186-
) {
187-
name = nameNode.name;
188-
} else if (nameNode.type === "PseudoClassSelector") {
189-
name = `:${nameNode.name}`;
190-
} else {
191-
continue;
177+
178+
for (const node of this.rule.prelude.children) {
179+
if (node.type !== "Selector") {
180+
continue;
181+
}
182+
let selector: Selector | undefined = undefined;
183+
for (const childNode of node.children) {
184+
let name: string = "";
185+
let state: string | undefined;
186+
switch (childNode.type) {
187+
case "TypeSelector":
188+
name = childNode.name;
189+
break;
190+
case "ClassSelector":
191+
name = `.${childNode.name}`;
192+
break;
193+
case "AttributeSelector":
194+
name = csstree.generate(childNode);
195+
break;
196+
case "PseudoClassSelector": {
197+
// First pseudo selector is not a state but an element selector, e.g. :root
198+
if (selector) {
199+
state = `:${childNode.name}`;
200+
} else {
201+
name = `:${childNode.name}`;
202+
}
203+
break;
204+
}
205+
case "PseudoElementSelector":
206+
state = `::${childNode.name}`;
207+
break;
208+
case "Combinator":
209+
// " " vs " > "
210+
name =
211+
childNode.name === " " ? childNode.name : ` ${childNode.name} `;
212+
break;
192213
}
193-
if (stateNode?.type === "PseudoClassSelector") {
194-
selectors.push({
195-
name,
196-
state: `:${stateNode.name}`,
197-
});
198-
} else if (stateNode?.type === "PseudoElementSelector") {
199-
selectors.push({
200-
name,
201-
state: `::${stateNode.name}`,
202-
});
214+
215+
if (selector) {
216+
selector.name += name;
217+
if (state) {
218+
selector.state = state;
219+
}
203220
} else {
204-
selectors.push({
205-
name,
206-
});
221+
selector = { name, state };
207222
}
208223
}
224+
if (selector) {
225+
selectors.push(selector);
226+
selector = undefined;
227+
}
209228
}
210229

211230
const stringValue = csstree.generate(node.value);

0 commit comments

Comments
 (0)
Please sign in to comment.