diff --git a/src/ExCSS.Tests/SelectorsTests.cs b/src/ExCSS.Tests/SelectorsTests.cs index 9c171f09..71013a56 100644 --- a/src/ExCSS.Tests/SelectorsTests.cs +++ b/src/ExCSS.Tests/SelectorsTests.cs @@ -89,6 +89,63 @@ public async Task FindAllStandardPseudoElementSelectors(bool allowInvalidSelecto Assert.Equal(expectedCount, list.Count()); } + [Theory] + // "Whitespace is permitted on either side of the + or - that separates the An and B parts when both + // are present" (CSS Syntax 3 6.1), which lists "3n + 1" among its valid examples. The sign then + // arrives as a standalone delim token rather than as part of a signed . + [InlineData(":nth-child(10n + 1)", 10, 1)] + [InlineData(":nth-child(10n - 1)", 10, -1)] + [InlineData(":nth-child(2n + 0)", 2, 0)] + [InlineData(":nth-child(-2n + 3)", -2, 3)] + [InlineData(":nth-child( 10n + 1 )", 10, 1)] + // The compact form has always worked and must keep working. + [InlineData(":nth-child(10n+1)", 10, 1)] + [InlineData(":nth-child(10n-1)", 10, -1)] + [InlineData(":nth-child(odd)", 2, 1)] + [InlineData(":nth-child(even)", 2, 0)] + public void NthChildAcceptsAnBWithAndWithoutSpaces(string selector, int step, int offset) + { + var sheet = new StylesheetParser().Parse(selector + " { color: red }"); + + Assert.Equal(1, sheet.Rules.Length); + var child = Assert.IsAssignableFrom(((StyleRule)sheet.Rules[0]).Selector); + Assert.Equal(step, child.Step); + Assert.Equal(offset, child.Offset); + } + + [Theory] + [InlineData(":nth-last-child(3n + 2)", 3, 2)] + [InlineData(":nth-of-type(3n + 2)", 3, 2)] + [InlineData(":nth-last-of-type(3n + 2)", 3, 2)] + public void NthVariantsAcceptSpacedSign(string selector, int step, int offset) + { + var sheet = new StylesheetParser().Parse(selector + " { color: red }"); + + Assert.Equal(1, sheet.Rules.Length); + var child = Assert.IsAssignableFrom(((StyleRule)sheet.Rules[0]).Selector); + Assert.Equal(step, child.Step); + Assert.Equal(offset, child.Offset); + } + + [Theory] + // The production requires a after a standalone sign, so a second sign is invalid. + // CSS Syntax 3 6.1 lists "3n + -6" among its invalid examples. + [InlineData(":nth-child(3n + -6)")] + [InlineData(":nth-child(10n + -1)")] + [InlineData(":nth-child(10n + +1)")] + [InlineData(":nth-child(10n + + 1)")] + [InlineData(":nth-child(10n + n)")] + // 6.1's invalid whitespace examples: the An part and the leading sign may not be split. + [InlineData(":nth-child(3 n)")] + [InlineData(":nth-child(+ 2n)")] + [InlineData(":nth-child(+ 2)")] + public void NthChildRejectsMalformedSpacedOffset(string selector) + { + var sheet = new StylesheetParser().Parse(selector + " { color: red }"); + + Assert.Equal(0, sheet.Rules.Length); + } + private static bool HasStandardPseudoElementSelector(ISelector selector, bool negate = false) { if (selector is PseudoElementSelector pes) diff --git a/src/ExCSS/Parser/SelectorConstructor.cs b/src/ExCSS/Parser/SelectorConstructor.cs index c29678bf..bfcc3621 100644 --- a/src/ExCSS/Parser/SelectorConstructor.cs +++ b/src/ExCSS/Parser/SelectorConstructor.cs @@ -803,6 +803,7 @@ protected override bool OnToken(Token token) ParseState.Initial => OnInitial(token), ParseState.AfterInitialSign => OnAfterInitialSign(token), ParseState.Offset => OnOffset(token), + ParseState.AfterOffsetSign => OnAfterOffsetSign(token), ParseState.BeforeOf => OnBeforeOf(token), _ => OnAfter(token) }; @@ -879,11 +880,39 @@ private bool OnOffset(Token token) _offset *= _sign; _state = ParseState.BeforeOf; return false; + case TokenType.Delim when token.Data.IsOneOf("+", "-"): + // When whitespace separates the offset's sign from its digits the sign arrives as a + // standalone delim token - the " ['+' | '-'] " + // production of (CSS Syntax 3 6.2). The compact form "10n+1" instead + // lexes as one signed and is handled by the Number case above. + _sign = token.Data == "-" ? -1 : +1; + _state = ParseState.AfterOffsetSign; + return false; default: return OnBeforeOf(token); } } + private bool OnAfterOffsetSign(Token token) + { + switch (token.Type) + { + case TokenType.Whitespace: + return false; + case TokenType.Number when !token.Data.StartsWith("+") && !token.Data.StartsWith("-"): + // The production requires a here, defined as "a + // with its type flag set to integer, and no sign character" (CSS Syntax 3 6.2), so + // "10n + -1" and "10n + +1" are invalid - 6.1 lists "3n + -6" as an invalid example. + _valid = _valid && ((NumberToken) token).IsInteger && int.TryParse(token.Data, out _offset); + _offset *= _sign; + _state = ParseState.BeforeOf; + return false; + default: + _valid = false; + return token.Type == TokenType.RoundBracketClose; + } + } + private bool OnInitial(Token token) { if (token.Type == TokenType.Whitespace) return false; @@ -918,6 +947,7 @@ private enum ParseState : byte Initial, AfterInitialSign, Offset, + AfterOffsetSign, BeforeOf, AfterOf }