From 0c36c1f7517749ee86dd6bbc7351947749dc2cb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Sat, 17 Jan 2026 18:00:56 +0100 Subject: [PATCH 1/3] fix lexing of float literals with sign --- common/jinja/lexer.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/jinja/lexer.cpp b/common/jinja/lexer.cpp index 85eaa1a76b7..a7876a42f8c 100644 --- a/common/jinja/lexer.cpp +++ b/common/jinja/lexer.cpp @@ -259,6 +259,11 @@ lexer_result lexer::tokenize(const std::string & source) { // Check for numbers following the unary operator std::string num = consume_while(is_integer); + if (pos < src.size() && src[pos] == '.' && pos + 1 < src.size() && is_integer(src[pos + 1])) { + ++pos; // Consume '.' + std::string frac = consume_while(is_integer); + num += "." + frac; + } std::string value = std::string(1, ch) + num; token::type t = num.empty() ? token::unary_operator : token::numeric_literal; // JJ_DEBUG("consumed unary operator or numeric literal: '%s'", value.c_str()); From 56ea9e31bcb1bb92a9509ef6e41a5f3aca39acff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Sat, 17 Jan 2026 18:05:20 +0100 Subject: [PATCH 2/3] add test --- tests/test-jinja.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test-jinja.cpp b/tests/test-jinja.cpp index 7adb302ffbb..0f49bdc84f8 100644 --- a/tests/test-jinja.cpp +++ b/tests/test-jinja.cpp @@ -247,6 +247,12 @@ static void test_expressions(testing & t) { "Bob" ); + test_template(t, "negative float (not dot notation)", + "{{ -1.0 }}", + json::object(), + "-1.0" + ); + test_template(t, "bracket notation", "{{ user['name'] }}", {{"user", {{"name", "Bob"}}}}, From 2c602c2837e131eeafb5cff9ea95839ef42ef8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigbj=C3=B8rn=20Skj=C3=A6ret?= Date: Sat, 17 Jan 2026 22:33:31 +0100 Subject: [PATCH 3/3] consume_numeric --- common/jinja/lexer.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/common/jinja/lexer.cpp b/common/jinja/lexer.cpp index a7876a42f8c..598982c2fe4 100644 --- a/common/jinja/lexer.cpp +++ b/common/jinja/lexer.cpp @@ -91,6 +91,16 @@ lexer_result lexer::tokenize(const std::string & source) { return str; }; + auto consume_numeric = [&]() -> std::string { + std::string num = consume_while(is_integer); + if (pos < src.size() && src[pos] == '.' && pos + 1 < src.size() && is_integer(src[pos + 1])) { + ++pos; // Consume '.' + std::string frac = consume_while(is_integer); + num += "." + frac; + } + return num; + }; + auto next_pos_is = [&](std::initializer_list chars, size_t n = 1) -> bool { if (pos + n >= src.size()) return false; for (char c : chars) { @@ -258,12 +268,7 @@ lexer_result lexer::tokenize(const std::string & source) { ++pos; // Consume the operator // Check for numbers following the unary operator - std::string num = consume_while(is_integer); - if (pos < src.size() && src[pos] == '.' && pos + 1 < src.size() && is_integer(src[pos + 1])) { - ++pos; // Consume '.' - std::string frac = consume_while(is_integer); - num += "." + frac; - } + std::string num = consume_numeric(); std::string value = std::string(1, ch) + num; token::type t = num.empty() ? token::unary_operator : token::numeric_literal; // JJ_DEBUG("consumed unary operator or numeric literal: '%s'", value.c_str()); @@ -312,12 +317,7 @@ lexer_result lexer::tokenize(const std::string & source) { // Numbers if (is_integer(ch)) { start_pos = pos; - std::string num = consume_while(is_integer); - if (pos < src.size() && src[pos] == '.' && pos + 1 < src.size() && is_integer(src[pos + 1])) { - ++pos; // Consume '.' - std::string frac = consume_while(is_integer); - num += "." + frac; - } + std::string num = consume_numeric(); // JJ_DEBUG("consumed numeric literal: '%s'", num.c_str()); tokens.push_back({token::numeric_literal, num, start_pos}); continue;