From 5afc76e2fdaf033eaaf9d84b4d8b45dea8b3d354 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 01:15:48 +0100 Subject: [PATCH 1/6] support nested and attribute selectors --- packages/css-data/src/parse-css.test.ts | 37 ++++++++++- packages/css-data/src/parse-css.ts | 83 ++++++++++++++++--------- 2 files changed, 87 insertions(+), 33 deletions(-) diff --git a/packages/css-data/src/parse-css.test.ts b/packages/css-data/src/parse-css.test.ts index f18adb595fca..054bf14f49bb 100644 --- a/packages/css-data/src/parse-css.test.ts +++ b/packages/css-data/src/parse-css.test.ts @@ -194,7 +194,18 @@ describe("Parse CSS", () => { ]); }); + test("attribute selector", () => { + expect(parseCss(`[class^="a"] { color: #ff0000 }`)).toEqual([ + { + selector: '[class^="a"]', + property: "color", + value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, + }, + ]); + }); + test("parse first pseudo class as selector", () => { + // E.g. :root expect(parseCss(`:first-pseudo:my-state { color: #ff0000 }`)).toEqual([ { selector: ":first-pseudo", @@ -471,11 +482,33 @@ describe("Parse CSS", () => { }); test("parse child combinator", () => { - expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([]); + expect(parseCss(`a > b { color: #ff0000 }`)).toEqual([ + { + selector: "a > b", + property: "color", + value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, + }, + ]); }); test("parse space combinator", () => { - expect(parseCss(`a b { color: #ff0000 }`)).toEqual([]); + expect(parseCss(`.a b { color: #ff0000 }`)).toEqual([ + { + selector: "a b", + property: "color", + value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, + }, + ]); + }); + + test("parse nested selectors as one token", () => { + expect(parseCss(`a b c.d { color: #ff0000 }`)).toEqual([ + { + selector: "a b c.d", + property: "color", + value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, + }, + ]); }); }); diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index 9efb238c115a..fe30862b6d26 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -161,6 +161,7 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { if (node.type === "MediaQuery" && node.children.size > 1) { invalidBreakpoint = true; } + ``; }, }); const generated = csstree.generate(this.atrule.prelude); @@ -168,43 +169,63 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { breakpoint = generated; } } - if (invalidBreakpoint) { + if (invalidBreakpoint || this.rule.prelude.type !== "SelectorList") { return; } const selectors: Selector[] = []; - if (this.rule.prelude.type === "SelectorList") { - for (const selector of this.rule.prelude.children) { - if (selector.type !== "Selector" || selector.children.size > 2) { - continue; - } - const [nameNode, stateNode] = selector.children; - let name; - if ( - nameNode.type === "ClassSelector" || - nameNode.type === "TypeSelector" - ) { - name = nameNode.name; - } else if (nameNode.type === "PseudoClassSelector") { - name = `:${nameNode.name}`; - } else { - continue; + + for (const node of this.rule.prelude.children) { + if (node.type !== "Selector") { + continue; + } + let selector: Selector | undefined = undefined; + node.children.forEach((node) => { + let name: string = ""; + let state: string | undefined; + switch (node.type) { + case "TypeSelector": + name = node.name; + break; + case "ClassSelector": + // .a {} vs a.b {} + name = selector ? `.${node.name}` : node.name; + break; + case "AttributeSelector": + if (node.value) { + name = `[${csstree.generate(node.name)}${node.matcher}${csstree.generate(node.value)}]`; + } + break; + case "PseudoClassSelector": { + // First pseudo selector is not a state but an element selector, e.g. :root + if (selector) { + state = `:${node.name}`; + } else { + name = `:${node.name}`; + } + break; + } + case "Combinator": + // " " vs " > " + name = node.name === " " ? node.name : ` ${node.name} `; + break; + case "PseudoElementSelector": + state = `::${node.name}`; + break; } - if (stateNode?.type === "PseudoClassSelector") { - selectors.push({ - name, - state: `:${stateNode.name}`, - }); - } else if (stateNode?.type === "PseudoElementSelector") { - selectors.push({ - name, - state: `::${stateNode.name}`, - }); - } else { - selectors.push({ - name, - }); + + if (selector) { + selector.name += name; + if (state) { + selector.state = state; + } + return; } + selector = { name, state }; + }); + if (selector) { + selectors.push(selector); + selector = undefined; } } From 88ead9256b02e1cac38c77e1909693ae63be0757 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 10:40:11 +0100 Subject: [PATCH 2/6] use for loop --- packages/css-data/src/parse-css.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index fe30862b6d26..6a7a47fb9ff1 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -180,37 +180,38 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { continue; } let selector: Selector | undefined = undefined; - node.children.forEach((node) => { + for (const childNode of node.children) { let name: string = ""; let state: string | undefined; - switch (node.type) { + switch (childNode.type) { case "TypeSelector": - name = node.name; + name = childNode.name; break; case "ClassSelector": // .a {} vs a.b {} - name = selector ? `.${node.name}` : node.name; + name = selector ? `.${childNode.name}` : childNode.name; break; case "AttributeSelector": - if (node.value) { - name = `[${csstree.generate(node.name)}${node.matcher}${csstree.generate(node.value)}]`; + if (childNode.value) { + name = `[${csstree.generate(childNode.name)}${childNode.matcher}${csstree.generate(childNode.value)}]`; } break; case "PseudoClassSelector": { // First pseudo selector is not a state but an element selector, e.g. :root if (selector) { - state = `:${node.name}`; + state = `:${childNode.name}`; } else { - name = `:${node.name}`; + name = `:${childNode.name}`; } break; } case "Combinator": // " " vs " > " - name = node.name === " " ? node.name : ` ${node.name} `; + name = + childNode.name === " " ? childNode.name : ` ${childNode.name} `; break; case "PseudoElementSelector": - state = `::${node.name}`; + state = `::${childNode.name}`; break; } @@ -222,7 +223,7 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { return; } selector = { name, state }; - }); + } if (selector) { selectors.push(selector); selector = undefined; From 199f2c32e28509e6dacad3792d96acad51578276 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 10:41:10 +0100 Subject: [PATCH 3/6] better comment --- packages/css-data/src/parse-css.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index 6a7a47fb9ff1..d5e786a76328 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -188,7 +188,8 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { name = childNode.name; break; case "ClassSelector": - // .a {} vs a.b {} + // .a => a + // .a.b => a.b name = selector ? `.${childNode.name}` : childNode.name; break; case "AttributeSelector": From bfd112009599f7fc924eb3c1d3813d05da69e730 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 14:49:01 +0100 Subject: [PATCH 4/6] fix the logic --- packages/css-data/src/parse-css.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index d5e786a76328..b0e771bbcd97 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -221,9 +221,9 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { if (state) { selector.state = state; } - return; + } else { + selector = { name, state }; } - selector = { name, state }; } if (selector) { selectors.push(selector); From 15ec2ef10a5a794f55784d476ddc5865d2273cd8 Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 14:50:40 +0100 Subject: [PATCH 5/6] simplify --- packages/css-data/src/parse-css.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index b0e771bbcd97..6428fcbe92b0 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -193,9 +193,7 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { name = selector ? `.${childNode.name}` : childNode.name; break; case "AttributeSelector": - if (childNode.value) { - name = `[${csstree.generate(childNode.name)}${childNode.matcher}${csstree.generate(childNode.value)}]`; - } + name = csstree.generate(childNode); break; case "PseudoClassSelector": { // First pseudo selector is not a state but an element selector, e.g. :root From e5721cd2d7e7cbab697963952a20575b658ea0eb Mon Sep 17 00:00:00 2001 From: Oleg Isonen Date: Fri, 4 Oct 2024 15:55:40 +0100 Subject: [PATCH 6/6] make selector match the actual selector --- packages/css-data/src/parse-css.test.ts | 32 ++++++++++++------------- packages/css-data/src/parse-css.ts | 10 ++++---- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/css-data/src/parse-css.test.ts b/packages/css-data/src/parse-css.test.ts index 054bf14f49bb..6d6e662b25c2 100644 --- a/packages/css-data/src/parse-css.test.ts +++ b/packages/css-data/src/parse-css.test.ts @@ -5,7 +5,7 @@ describe("Parse CSS", () => { test("longhand property name with keyword value", () => { expect(parseCss(`.test { background-color: red }`)).toEqual([ { - selector: "test", + selector: ".test", property: "backgroundColor", value: { type: "keyword", value: "red" }, }, @@ -15,7 +15,7 @@ describe("Parse CSS", () => { test("one class selector rules", () => { expect(parseCss(`.test { color: #ff0000 }`)).toEqual([ { - selector: "test", + selector: ".test", property: "color", value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, }, @@ -30,7 +30,7 @@ describe("Parse CSS", () => { `; expect(parseCss(css)).toEqual([ { - selector: "test", + selector: ".test", property: "backgroundImage", value: { type: "layers", @@ -45,7 +45,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundPositionX", value: { type: "layers", @@ -56,7 +56,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundPositionY", value: { type: "layers", @@ -67,7 +67,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundSize", value: { type: "layers", @@ -90,7 +90,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundRepeat", value: { type: "layers", @@ -101,7 +101,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundAttachment", value: { type: "layers", @@ -112,7 +112,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundOrigin", value: { type: "layers", @@ -123,7 +123,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundClip", value: { type: "layers", @@ -134,7 +134,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundColor", value: { alpha: 1, b: 252, g: 255, r: 235, type: "rgb" }, }, @@ -149,7 +149,7 @@ describe("Parse CSS", () => { `; expect(parseCss(css)).toEqual([ { - selector: "test", + selector: ".test", property: "backgroundImage", value: { type: "layers", @@ -157,7 +157,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundPositionX", value: { type: "layers", @@ -165,7 +165,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundPositionY", value: { type: "layers", @@ -173,7 +173,7 @@ describe("Parse CSS", () => { }, }, { - selector: "test", + selector: ".test", property: "backgroundSize", value: { type: "layers", @@ -494,7 +494,7 @@ describe("Parse CSS", () => { test("parse space combinator", () => { expect(parseCss(`.a b { color: #ff0000 }`)).toEqual([ { - selector: "a b", + selector: ".a b", property: "color", value: { alpha: 1, b: 0, g: 0, r: 255, type: "rgb" }, }, diff --git a/packages/css-data/src/parse-css.ts b/packages/css-data/src/parse-css.ts index 6428fcbe92b0..1f4fc285ccef 100644 --- a/packages/css-data/src/parse-css.ts +++ b/packages/css-data/src/parse-css.ts @@ -188,9 +188,7 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { name = childNode.name; break; case "ClassSelector": - // .a => a - // .a.b => a.b - name = selector ? `.${childNode.name}` : childNode.name; + name = `.${childNode.name}`; break; case "AttributeSelector": name = csstree.generate(childNode); @@ -204,14 +202,14 @@ export const parseCss = (css: string, options: ParserOptions = {}) => { } break; } + case "PseudoElementSelector": + state = `::${childNode.name}`; + break; case "Combinator": // " " vs " > " name = childNode.name === " " ? childNode.name : ` ${childNode.name} `; break; - case "PseudoElementSelector": - state = `::${childNode.name}`; - break; } if (selector) {