Skip to content

Commit 002e3b0

Browse files
authored
Added support for style selector parsing (#619)
1 parent fd076a4 commit 002e3b0

25 files changed

+2793
-7
lines changed

.changeset/tricky-melons-complain.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte-eslint-parser": minor
3+
---
4+
5+
feat: added support for style selector parsing

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"eslint-visitor-keys": "^4.0.0",
6060
"espree": "^10.0.0",
6161
"postcss": "^8.4.49",
62-
"postcss-scss": "^4.0.9"
62+
"postcss-scss": "^4.0.9",
63+
"postcss-selector-parser": "^7.0.0"
6364
},
6465
"devDependencies": {
6566
"@changesets/changelog-github": "^0.5.0",

src/parser/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { KEYS } from "../visitor-keys.js";
22
import { Context } from "../context/index.js";
33
import type {
44
Comment,
5+
SourceLocation,
56
SvelteProgram,
67
SvelteScriptElement,
78
SvelteStyleElement,
@@ -10,6 +11,11 @@ import type {
1011
import type { Program } from "estree";
1112
import type { ScopeManager } from "eslint-scope";
1213
import { Variable } from "eslint-scope";
14+
import type { Rule, Node } from "postcss";
15+
import type {
16+
Node as SelectorNode,
17+
Root as SelectorRoot,
18+
} from "postcss-selector-parser";
1319
import { parseScript, parseScriptInSvelte } from "./script.js";
1420
import type * as SvAST from "./svelte-ast-types.js";
1521
import type * as Compiler from "./svelte-ast-types-for-v5.js";
@@ -29,13 +35,15 @@ import {
2935
import { addReference } from "../scope/index.js";
3036
import {
3137
parseStyleContext,
38+
parseSelector,
3239
type StyleContext,
3340
type StyleContextNoStyleElement,
3441
type StyleContextParseError,
3542
type StyleContextSuccess,
3643
type StyleContextUnknownLang,
3744
styleNodeLoc,
3845
styleNodeRange,
46+
styleSelectorNodeLoc,
3947
} from "./style-context.js";
4048
import { getGlobalsForSvelte, getGlobalsForSvelteScript } from "./globals.js";
4149
import type { NormalizedParserOptions } from "./parser-options.js";
@@ -84,6 +92,12 @@ type ParseResult = {
8492
isSvelteScript: false;
8593
getSvelteHtmlAst: () => SvAST.Fragment | Compiler.Fragment;
8694
getStyleContext: () => StyleContext;
95+
getStyleSelectorAST: (rule: Rule) => SelectorRoot;
96+
styleNodeLoc: (node: Node) => Partial<SourceLocation>;
97+
styleNodeRange: (
98+
node: Node,
99+
) => [number | undefined, number | undefined];
100+
styleSelectorNodeLoc: (node: SelectorNode) => Partial<SourceLocation>;
87101
svelteParseContext: SvelteParseContext;
88102
}
89103
| {
@@ -221,6 +235,7 @@ function parseAsSvelte(
221235
(b): b is SvelteStyleElement => b.type === "SvelteStyleElement",
222236
);
223237
let styleContext: StyleContext | null = null;
238+
const selectorASTs: Map<Rule, SelectorRoot> = new Map();
224239

225240
resultScript.ast = ast as any;
226241
resultScript.services = Object.assign(resultScript.services || {}, {
@@ -235,8 +250,18 @@ function parseAsSvelte(
235250
}
236251
return styleContext;
237252
},
253+
getStyleSelectorAST(rule: Rule) {
254+
const cached = selectorASTs.get(rule);
255+
if (cached !== undefined) {
256+
return cached;
257+
}
258+
const ast = parseSelector(rule);
259+
selectorASTs.set(rule, ast);
260+
return ast;
261+
},
238262
styleNodeLoc,
239263
styleNodeRange,
264+
styleSelectorNodeLoc,
240265
svelteParseContext,
241266
});
242267
resultScript.visitorKeys = Object.assign({}, KEYS, resultScript.visitorKeys);

src/parser/style-context.ts

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { Node, Parser, Root } from "postcss";
1+
import type { Node, Parser, Root, Rule } from "postcss";
22
import postcss from "postcss";
33
import { parse as SCSSparse } from "postcss-scss";
4+
import {
5+
default as selectorParser,
6+
type Node as SelectorNode,
7+
type Root as SelectorRoot,
8+
} from "postcss-selector-parser";
49

510
import type { Context } from "../context/index.js";
611
import type { SourceLocation, SvelteStyleElement } from "../ast/index.js";
@@ -77,10 +82,25 @@ export function parseStyleContext(
7782
return { status: "parse-error", sourceLang, error };
7883
}
7984
fixPostCSSNodeLocation(sourceAst, styleElement);
80-
sourceAst.walk((node) => fixPostCSSNodeLocation(node, styleElement));
85+
sourceAst.walk((node) => {
86+
fixPostCSSNodeLocation(node, styleElement);
87+
});
8188
return { status: "success", sourceLang, sourceAst };
8289
}
8390

91+
/**
92+
* Parses a PostCSS Rule node's selector and returns its AST.
93+
*/
94+
export function parseSelector(rule: Rule): SelectorRoot {
95+
const processor = selectorParser();
96+
const root = processor.astSync(rule.selector);
97+
fixSelectorNodeLocation(root, rule);
98+
root.walk((node) => {
99+
fixSelectorNodeLocation(node, rule);
100+
});
101+
return root;
102+
}
103+
84104
/**
85105
* Extracts a node location (like that of any ESLint node) from a parsed svelte style node.
86106
*/
@@ -121,6 +141,24 @@ export function styleNodeRange(
121141
];
122142
}
123143

144+
/**
145+
* Extracts a node location (like that of any ESLint node) from a parsed svelte selector node.
146+
*/
147+
export function styleSelectorNodeLoc(
148+
node: SelectorNode,
149+
): Partial<SourceLocation> {
150+
return {
151+
start:
152+
node.source?.start !== undefined
153+
? {
154+
line: node.source.start.line,
155+
column: node.source.start.column - 1,
156+
}
157+
: undefined,
158+
end: node.source?.end,
159+
};
160+
}
161+
124162
/**
125163
* Fixes PostCSS AST locations to be relative to the whole file instead of relative to the <style> element.
126164
*/
@@ -144,3 +182,31 @@ function fixPostCSSNodeLocation(node: Node, styleElement: SvelteStyleElement) {
144182
node.source.end.column += styleElement.startTag.loc.end.column;
145183
}
146184
}
185+
186+
/**
187+
* Fixes selector AST locations to be relative to the whole file instead of relative to their parent rule.
188+
*/
189+
function fixSelectorNodeLocation(node: SelectorNode, rule: Rule) {
190+
if (node.source === undefined) {
191+
return;
192+
}
193+
const ruleLoc = styleNodeLoc(rule);
194+
195+
if (node.source.start !== undefined && ruleLoc.start !== undefined) {
196+
if (node.source.start.line === 1) {
197+
node.source.start.column += ruleLoc.start.column;
198+
}
199+
node.source.start.line += ruleLoc.start.line - 1;
200+
} else {
201+
node.source.start = undefined;
202+
}
203+
204+
if (node.source.end !== undefined && ruleLoc.start !== undefined) {
205+
if (node.source.end.line === 1) {
206+
node.source.end.column += ruleLoc.start.column;
207+
}
208+
node.source.end.line += ruleLoc.start.line - 1;
209+
} else {
210+
node.source.end = undefined;
211+
}
212+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script>
2+
let a = 10
3+
</script>
4+
5+
<span class="myClass">Hello!</span>
6+
7+
<b>{a}</b>
8+
9+
<style>
10+
.myClass {
11+
color: red;
12+
}
13+
14+
b {
15+
font-size: xx-large;
16+
}
17+
18+
a:active,
19+
a::before,
20+
b + a,
21+
b + .myClass,
22+
a[data-key="value"] {
23+
color: blue;
24+
}
25+
</style>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy