From 265f51f304245346e9183ca296317b0dcc717f8a Mon Sep 17 00:00:00 2001 From: Romain Menke <11521496+romainmenke@users.noreply.github.com> Date: Wed, 20 Sep 2023 00:24:14 +0200 Subject: [PATCH] add support for several new selectors (#356) --- src/parser/cssParser.ts | 13 +++- src/services/selectorPrinting.ts | 101 +++++++++++++++++--------- src/test/css/parser.test.ts | 6 ++ src/test/css/selectorPrinting.test.ts | 31 ++++++++ 4 files changed, 117 insertions(+), 34 deletions(-) diff --git a/src/parser/cssParser.ts b/src/parser/cssParser.ts index bfc4b10b..a965a6fd 100644 --- a/src/parser/cssParser.ts +++ b/src/parser/cssParser.ts @@ -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); } diff --git a/src/services/selectorPrinting.ts b/src/services/selectorPrinting.ts index 989a71e5..6e49f66e 100644 --- a/src/services/selectorPrinting.ts +++ b/src/services/selectorPrinting.ts @@ -358,6 +358,42 @@ export class SelectorPrinting { } private selectorToSpecificityMarkedString(node: nodes.Node): MarkedString { + const calculateMostSpecificListItem = (childElements: Array): 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(); @@ -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; } @@ -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; diff --git a/src/test/css/parser.test.ts b/src/test/css/parser.test.ts index 34f45a67..e6f23ee4 100644 --- a/src/test/css/parser.test.ts +++ b/src/test/css/parser.test.ts @@ -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)); @@ -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 () { diff --git a/src/test/css/selectorPrinting.test.ts b/src/test/css/selectorPrinting.test.ts index 65411244..8b1cc56b 100644 --- a/src/test/css/selectorPrinting.test.ts +++ b/src/test/css/selectorPrinting.test.ts @@ -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: '' }, + '[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: '' }, + '[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: '' }, + '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 2, 0)' + ]); + + assertSelectorMarkdown(p, '#foo:host-context(foo)', '#foo', [ + { language: 'html', value: '' }, + '[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: '' }, + '[Selector Specificity](https://developer.mozilla.org/docs/Web/CSS/Specificity): (1, 0, 2)' + ]); + }); });