Skip to content
Merged
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
57 changes: 57 additions & 0 deletions src/ExCSS.Tests/SelectorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <number>.
[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<ChildSelector>(((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<ChildSelector>(((StyleRule)sheet.Rules[0]).Selector);
Assert.Equal(step, child.Step);
Assert.Equal(offset, child.Offset);
}

[Theory]
// The production requires a <signless-integer> 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)
Expand Down
30 changes: 30 additions & 0 deletions src/ExCSS/Parser/SelectorConstructor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down Expand Up @@ -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 "<n-dimension> ['+' | '-'] <signless-integer>"
// production of <a-n-plus-b> (CSS Syntax 3 6.2). The compact form "10n+1" instead
// lexes as one signed <number> 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 <signless-integer> here, defined as "a <number-token>
// 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;
Expand Down Expand Up @@ -918,6 +947,7 @@ private enum ParseState : byte
Initial,
AfterInitialSign,
Offset,
AfterOffsetSign,
BeforeOf,
AfterOf
}
Expand Down