Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 137 additions & 8 deletions fixtures/ast/declarationList/nesting.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
}
]
}
}
]
Expand Down
60 changes: 58 additions & 2 deletions lib/syntax/node/DeclarationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: [[
Expand All @@ -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));
Expand Down