diff --git a/src/ExCSS.Tests/Tokenization.cs b/src/ExCSS.Tests/Tokenization.cs index c7b8756..e3e9a04 100644 --- a/src/ExCSS.Tests/Tokenization.cs +++ b/src/ExCSS.Tests/Tokenization.cs @@ -96,6 +96,59 @@ public void CssParserUnicodeRangeSelectedRangeIsExpandedOnDemand() Assert.Equal(new[] {"A", "B", "C"}, range.SelectedRange); } + [Fact] + public void CssParserFunctionWithNestedParentheses() + { + // A bare parenthesized group inside a function's arguments must not terminate the function: + // only the ')' matching the function's own '(' does. Previously "calc((1px + 2px) * 3)" ended + // at the inner ')', leaving "* 3)" behind as stray top-level tokens. + var teststring = "calc((1px + 2px) * 3)"; + var tokenizer = new Lexer(new TextSource(teststring)); + var token = tokenizer.Get(); + + Assert.Equal(TokenType.Function, token.Type); + Assert.Equal("calc", token.Data); + Assert.Equal(teststring, token.ToValue()); + Assert.Equal(TokenType.EndOfFile, tokenizer.Get().Type); + } + + [Fact] + public void CssParserFunctionWithDeeplyNestedParentheses() + { + var teststring = "calc(((1px + 2px) * (3 - 1)) / 2)"; + var tokenizer = new Lexer(new TextSource(teststring)); + var token = tokenizer.Get(); + + Assert.Equal(TokenType.Function, token.Type); + Assert.Equal(teststring, token.ToValue()); + Assert.Equal(TokenType.EndOfFile, tokenizer.Get().Type); + } + + [Fact] + public void CssParserFunctionWithNestedFunctionAndParentheses() + { + // A nested function's own parens are consumed by its own recursive call, so they never surface + // as bare bracket tokens here - this asserts the depth counter doesn't double-count them. + var teststring = "calc(min(10px, (2px + 3px)) * 2)"; + var tokenizer = new Lexer(new TextSource(teststring)); + var token = tokenizer.Get(); + + Assert.Equal(TokenType.Function, token.Type); + Assert.Equal(teststring, token.ToValue()); + Assert.Equal(TokenType.EndOfFile, tokenizer.Get().Type); + } + + [Fact] + public void CssParserFunctionWithUnbalancedParenthesesStopsAtEof() + { + var teststring = "calc((1px + 2px"; + var tokenizer = new Lexer(new TextSource(teststring)); + var token = tokenizer.Get(); + + Assert.Equal(TokenType.Function, token.Type); + Assert.Equal(TokenType.EndOfFile, tokenizer.Get().Type); + } + [Fact] public void ValueContextHash() { diff --git a/src/ExCSS/Parser/Lexer.cs b/src/ExCSS/Parser/Lexer.cs index 7a8df52..3a8a62b 100644 --- a/src/ExCSS/Parser/Lexer.cs +++ b/src/ExCSS/Parser/Lexer.cs @@ -1091,10 +1091,12 @@ private Token NewFunction(string value) { var function = new FunctionToken(value, _position); - // Track paren depth so a bare parenthesized group nested in the arguments - e.g. - // calc((100% - 20px) / 3) - isn't mistaken for the end of the function; only the ')' matching - // this function's own '(' terminates it. (Also submitted standalone as the nested-parentheses - // lexer fix; if that merges first, this rebases cleanly.) + // Tracks paren depth so a bare (non-function) parenthesized group nested inside the + // function's arguments - e.g. calc((1px + 2px) * 3) - isn't mistaken for the end of the + // function: only the closing paren matching this function's own opening paren (depth + // returning to 0) terminates it. A nested function call's own parens are already fully + // consumed by its own (recursive) call to this method before it's added as a single token + // here, so they never surface as bare RoundBracketOpen/Close tokens at this level. var depth = 1; var token = Get(); while (token.Type != TokenType.EndOfFile)