diff --git a/fixtures/ast/declarationList/nesting.json b/fixtures/ast/declarationList/nesting.json index a712d3c4..499ce209 100644 --- a/fixtures/ast/declarationList/nesting.json +++ b/fixtures/ast/declarationList/nesting.json @@ -249,24 +249,153 @@ ] } }, - "don't parse nested rule when it not started with &": { + "nested rules with various selectors": { "source": ".bar & { color: green; }; div:hover { color: red; }", - "generate": ".bar & { color: green; };div:hover { color: red; }", + "generate": ".bar &{color:green}div:hover{color:red}", "ast": { - "type": "DeclarationList", "children": [ { - "type": "Raw", - "value": ".bar & { color: green; };" + "type": "Rule", + "prelude": { + "type": "SelectorList", + "children": [ + { + "type": "Selector", + "children": [ + { + "type": "ClassSelector", + "name": "bar" + }, + { + "type": "Combinator", + "name": " " + }, + { + "type": "NestingSelector" + } + ] + } + ] + }, + "block": { + "type": "Block", + "children": [ + { + "type": "Declaration", + "important": false, + "property": "color", + "value": { + "type": "Value", + "children": [ + { + "type": "Identifier", + "name": "green" + } + ] + } + } + ] + } }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "children": [ + { + "type": "Selector", + "children": [ + { + "type": "TypeSelector", + "name": "div" + }, + { + "type": "PseudoClassSelector", + "name": "hover", + "children": null + } + ] + } + ] + }, + "block": { + "type": "Block", + "children": [ + { + "type": "Declaration", + "important": false, + "property": "color", + "value": { + "type": "Value", + "children": [ + { + "type": "Identifier", + "name": "red" + } + ] + } + } + ] + } + } + ] + } + }, + "nested element selector": { + "source": "color: blue; p { margin: 0; }", + "generate": "color:blue;p{margin:0}", + "ast": { + "type": "DeclarationList", + "children": [ { "type": "Declaration", "important": false, - "property": "div", + "property": "color", "value": { - "type": "Raw", - "value": "hover { color: red; }" + "type": "Value", + "children": [ + { + "type": "Identifier", + "name": "blue" + } + ] + } + }, + { + "type": "Rule", + "prelude": { + "type": "SelectorList", + "children": [ + { + "type": "Selector", + "children": [ + { + "type": "TypeSelector", + "name": "p" + } + ] + } + ] + }, + "block": { + "type": "Block", + "children": [ + { + "type": "Declaration", + "important": false, + "property": "margin", + "value": { + "type": "Value", + "children": [ + { + "type": "Number", + "value": "0" + } + ] + } + } + ] } } ] diff --git a/lib/syntax/node/DeclarationList.js b/lib/syntax/node/DeclarationList.js index 2b40c994..8c0e1b07 100644 --- a/lib/syntax/node/DeclarationList.js +++ b/lib/syntax/node/DeclarationList.js @@ -2,15 +2,71 @@ import { WhiteSpace, Comment, Semicolon, - AtKeyword + AtKeyword, + Delim, + Hash, + LeftSquareBracket, + Colon, + Ident, + RightCurlyBracket } from '../../tokenizer/index.js'; const AMPERSAND = 0x0026; // U+0026 AMPERSAND (&) +const DOT = 0x002E; // U+002E FULL STOP (.) +const STAR = 0x002A; // U+002A ASTERISK (*); +const PLUSSIGN = 0x002B; // U+002B PLUS SIGN (+) +const GREATERTHANSIGN = 0x003E; // U+003E GREATER-THAN SIGN (>) +const TILDE = 0x007E; // U+007E TILDE (~) + +const selectorStarts = new Set([ + AMPERSAND, + DOT, + STAR, + PLUSSIGN, + GREATERTHANSIGN, + TILDE +]); function consumeRaw() { return this.Raw(this.consumeUntilSemicolonIncluded, true); } +function isElementSelectorStart() { + if (this.tokenType !== Ident) { + return false; + } + + const nextTokenType = this.lookupTypeNonSC(1); + + // Simple case: if next token is not colon, semicolon, or closing brace, it's likely a selector + if (nextTokenType !== Colon && nextTokenType !== Semicolon && nextTokenType !== RightCurlyBracket) { + return true; + } + + // Special handling for colon case - could be pseudo-class/pseudo-element + if (nextTokenType === Colon) { + // Look ahead further to see what follows the colon + const afterColonType = this.lookupTypeNonSC(2); + + // If after colon there's an identifier (pseudo-class/pseudo-element name), + // check what comes after that + if (afterColonType === Ident) { + const afterPseudoType = this.lookupTypeNonSC(3); + // If it's followed by { or other selector tokens, it's a selector + // If it's followed by ; or } or EOF, it's not a selector (property) + return afterPseudoType !== Semicolon && afterPseudoType !== RightCurlyBracket && afterPseudoType !== 0; // 0 is EOF + } + } + + return false; +} + +function isSelectorStart() { + return this.tokenType === Delim && selectorStarts.has(this.source.charCodeAt(this.tokenStart)) || + this.tokenType === Hash || this.tokenType === LeftSquareBracket || + this.tokenType === Colon || isElementSelectorStart.call(this); +} + export const name = 'DeclarationList'; export const structure = { children: [[ @@ -37,7 +93,7 @@ export function parse() { break; default: - if (this.isDelim(AMPERSAND)) { + if (isSelectorStart.call(this)) { children.push(this.parseWithFallback(this.Rule, consumeRaw)); } else { children.push(this.parseWithFallback(this.Declaration, consumeRaw));