Skip to content

Commit

Permalink
add support for several new selectors (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
romainmenke authored Sep 19, 2023
1 parent 1ded897 commit 265f51f
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 34 deletions.
13 changes: 12 additions & 1 deletion src/parser/cssParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,18 @@ export class Parser {

return null;
};
node.addChild(this.try(tryAsSelector) || this._parseBinaryExpr());

let hasSelector = node.addChild(this.try(tryAsSelector));
if (!hasSelector) {
if (
node.addChild(this._parseBinaryExpr()) &&
this.acceptIdent('of') &&
!node.addChild(this.try(tryAsSelector))
) {
return this.finish(node, ParseError.SelectorExpected);
}
}

if (!this.accept(TokenType.ParenthesisR)) {
return this.finish(node, ParseError.RightParenthesisExpected);
}
Expand Down
101 changes: 68 additions & 33 deletions src/services/selectorPrinting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,42 @@ export class SelectorPrinting {
}

private selectorToSpecificityMarkedString(node: nodes.Node): MarkedString {
const calculateMostSpecificListItem = (childElements: Array<nodes.Node>): Specificity => {
const specificity = new Specificity();

let mostSpecificListItem = new Specificity();

for (const containerElement of childElements) {
for (const childElement of containerElement.getChildren()) {
const itemSpecificity = calculateScore(childElement);
if (itemSpecificity.id > mostSpecificListItem.id) {
mostSpecificListItem = itemSpecificity;
continue;
} else if (itemSpecificity.id < mostSpecificListItem.id) {
continue;
}

if (itemSpecificity.attr > mostSpecificListItem.attr) {
mostSpecificListItem = itemSpecificity;
continue;
} else if (itemSpecificity.attr < mostSpecificListItem.attr) {
continue;
}

if (itemSpecificity.tag > mostSpecificListItem.tag) {
mostSpecificListItem = itemSpecificity;
continue;
}
}
}

specificity.id += mostSpecificListItem.id;
specificity.attr += mostSpecificListItem.attr;
specificity.tag += mostSpecificListItem.tag;

return specificity;
};

//https://www.w3.org/TR/selectors-3/#specificity
const calculateScore = (node: nodes.Node): Specificity => {
const specificity = new Specificity();
Expand All @@ -384,7 +420,23 @@ export class SelectorPrinting {

case nodes.NodeType.PseudoSelector:
const text = element.getText();
const childElements = element.getChildren();

if (this.isPseudoElementIdentifier(text)) {
if (text.match(/^::slotted/i) && childElements.length > 0) {
// The specificity of ::slotted() is that of a pseudo-element, plus the specificity of its argument.
// ::slotted() does not allow a selector list as its argument, but this isn't the right place to give feedback on validity.
// Reporting the most specific child will be correct for correct CSS and will be forgiving in case of mistakes.
specificity.tag++;

let mostSpecificListItem = calculateMostSpecificListItem(childElements);

specificity.id += mostSpecificListItem.id;
specificity.attr += mostSpecificListItem.attr;
specificity.tag += mostSpecificListItem.tag;
continue elementLoop;
}

specificity.tag++; // pseudo element
continue elementLoop;
}
Expand All @@ -395,39 +447,22 @@ export class SelectorPrinting {
}

// the most specific child selector
if (text.match(/^:(not|has|is)/i) && element.getChildren().length > 0) {
let mostSpecificListItem = new Specificity();

for (const containerElement of element.getChildren()) {
let list;
if (containerElement.type === nodes.NodeType.Undefined) { // containerElement is a list of selectors
list = containerElement.getChildren();
} else { // containerElement is a selector
list = [containerElement];
}

for (const childElement of containerElement.getChildren()) {
const itemSpecificity = calculateScore(childElement);
if (itemSpecificity.id > mostSpecificListItem.id) {
mostSpecificListItem = itemSpecificity;
continue;
} else if (itemSpecificity.id < mostSpecificListItem.id) {
continue;
}

if (itemSpecificity.attr > mostSpecificListItem.attr) {
mostSpecificListItem = itemSpecificity;
continue;
} else if (itemSpecificity.attr < mostSpecificListItem.attr) {
continue;
}

if (itemSpecificity.tag > mostSpecificListItem.tag) {
mostSpecificListItem = itemSpecificity;
continue;
}
}
}
if (text.match(/^:(?:not|has|is)/i) && childElements.length > 0) {
let mostSpecificListItem = calculateMostSpecificListItem(childElements);

specificity.id += mostSpecificListItem.id;
specificity.attr += mostSpecificListItem.attr;
specificity.tag += mostSpecificListItem.tag;
continue elementLoop;
}

if (text.match(/^:(?:nth-child|nth-last-child|host|host-context)/i) && childElements.length > 0) {
// The specificity of :host() is that of a pseudo-class, plus the specificity of its argument.
// The specificity of :host-context() is that of a pseudo-class, plus the specificity of its argument.
// The specificity of an :nth-child() or :nth-last-child() selector is the specificity of the pseudo class itself (counting as one pseudo-class selector) plus the specificity of the most specific complex selector in its selector list argument.
specificity.attr++;

let mostSpecificListItem = calculateMostSpecificListItem(childElements);

specificity.id += mostSpecificListItem.id;
specificity.attr += mostSpecificListItem.attr;
Expand Down
6 changes: 6 additions & 0 deletions src/test/css/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ suite('CSS - Parser', () => {
assertNode(':some', parser, parser._parsePseudo.bind(parser));
assertNode(':some(thing)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(12)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(1n)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(-n+3)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(2n+1)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(2n+1 of .foo)', parser, parser._parsePseudo.bind(parser));
assertNode(':nth-child(2n+1 of .foo > bar, :not(*) ~ [other="value"])', parser, parser._parsePseudo.bind(parser));
assertNode(':lang(it)', parser, parser._parsePseudo.bind(parser));
assertNode(':not(.class)', parser, parser._parsePseudo.bind(parser));
assertNode(':not(:disabled)', parser, parser._parsePseudo.bind(parser));
Expand All @@ -461,6 +466,7 @@ suite('CSS - Parser', () => {
assertNode(':has(~ div .test)', parser, parser._parsePseudo.bind(parser)); // #250
assertError('::', parser, parser._parsePseudo.bind(parser), ParseError.IdentifierExpected);
assertError(':: foo', parser, parser._parsePseudo.bind(parser), ParseError.IdentifierExpected);
assertError(':nth-child(1n of)', parser, parser._parsePseudo.bind(parser), ParseError.SelectorExpected);
});

test('declaration', function () {
Expand Down
31 changes: 31 additions & 0 deletions src/test/css/selectorPrinting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,35 @@ suite('CSS - MarkedStringPrinter selectors specificities', () => {
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 2, 0)'
]);
});

test('nth-child, nth-last-child specificity', function () {
assertSelectorMarkdown(p, '#foo:nth-child(-n+3 of li.important)', '#foo', [
{ language: 'html', value: '<element id="foo" :nth-child>' },
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 2, 1)'
]);

assertSelectorMarkdown(p, '#foo:nth-last-child(-n+3 of li, .important)', '#foo', [
{ language: 'html', value: '<element id="foo" :nth-last-child>' },
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 2, 0)'
]);
});

test('host, host-context specificity', function () {
assertSelectorMarkdown(p, '#foo:host(.foo)', '#foo', [
{ language: 'html', value: '<element id="foo" :host>' },
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 2, 0)'
]);

assertSelectorMarkdown(p, '#foo:host-context(foo)', '#foo', [
{ language: 'html', value: '<element id="foo" :host-context>' },
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 1, 1)'
]);
});

test('slotted specificity', function () {
assertSelectorMarkdown(p, '#foo::slotted(foo)', '#foo', [
{ language: 'html', value: '<element id="foo" ::slotted>' },
'[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 0, 2)'
]);
});
});

0 comments on commit 265f51f

Please sign in to comment.