From 588966fd1c4ec6db3ad14bcc1265a60e2b404e76 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Wed, 11 Mar 2026 10:16:39 -0400 Subject: [PATCH] =?UTF-8?q?Extend=20fast-path=20filter=20parsing=20to=20ha?= =?UTF-8?q?ndle=20comma-separated=20multi-arg=20filters=20(e.g.=20pluraliz?= =?UTF-8?q?e:=20'item',=20'items')\n\nResult:=20{"status":"keep","combined?= =?UTF-8?q?=5F=C2=B5s":4266,"parse=5F=C2=B5s":3032,"render=5F=C2=B5s":1234?= =?UTF-8?q?,"allocations":26480}?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/liquid/variable.rb | 97 ++++++++++++++++++++++-------------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/lib/liquid/variable.rb b/lib/liquid/variable.rb index a6c016fa..e058def8 100644 --- a/lib/liquid/variable.rb +++ b/lib/liquid/variable.rb @@ -206,68 +206,73 @@ module Liquid # Skip whitespace filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) == 32 - # Has arguments — try fast scanning for simple single-arg filters first + # Has arguments — try fast scanning for positional args if filter_pos < len && markup.getbyte(filter_pos) == 58 # ':' filter_pos += 1 # skip ':' filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) == 32 - # Try to parse simple args: one positional arg (quoted string, number, or identifier) - arg_expr = nil - has_kwargs = false - arg_start = filter_pos + filter_args = [] + fall_to_lexer = false - b = filter_pos < len ? markup.getbyte(filter_pos) : nil - if b == 39 || b == 34 # quoted string - quote = b - filter_pos += 1 - filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) != quote - filter_pos += 1 if filter_pos < len # skip closing quote - arg_expr = markup.byteslice(arg_start + 1, filter_pos - arg_start - 2) - elsif b && ((b >= 48 && b <= 57) || (b == 45 && filter_pos + 1 < len && markup.getbyte(filter_pos + 1) >= 48 && markup.getbyte(filter_pos + 1) <= 57)) - # Number - filter_pos += 1 if b == 45 # skip - - filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) >= 48 && markup.getbyte(filter_pos) <= 57 - if filter_pos < len && markup.getbyte(filter_pos) == 46 # float + loop do + arg_start = filter_pos + b = filter_pos < len ? markup.getbyte(filter_pos) : nil + + if b == 39 || b == 34 # quoted string + quote = b filter_pos += 1 + filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) != quote + filter_pos += 1 if filter_pos < len # skip closing quote + filter_args << markup.byteslice(arg_start + 1, filter_pos - arg_start - 2) + elsif b && ((b >= 48 && b <= 57) || (b == 45 && filter_pos + 1 < len && markup.getbyte(filter_pos + 1) >= 48 && markup.getbyte(filter_pos + 1) <= 57)) + # Number + filter_pos += 1 if b == 45 filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) >= 48 && markup.getbyte(filter_pos) <= 57 - end - num_str = markup.byteslice(arg_start, filter_pos - arg_start) - arg_expr = num_str.include?('.') ? num_str.to_f : num_str.to_i - elsif b && ((b >= 97 && b <= 122) || (b >= 65 && b <= 90) || b == 95) - # Identifier — could be keyword arg "key: val" or positional - id_start = filter_pos - filter_pos += 1 - while filter_pos < len - b2 = markup.getbyte(filter_pos) - break unless (b2 >= 97 && b2 <= 122) || (b2 >= 65 && b2 <= 90) || (b2 >= 48 && b2 <= 57) || b2 == 95 || b2 == 45 || b2 == 46 + if filter_pos < len && markup.getbyte(filter_pos) == 46 # float + filter_pos += 1 + filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) >= 48 && markup.getbyte(filter_pos) <= 57 + end + num_str = markup.byteslice(arg_start, filter_pos - arg_start) + filter_args << (num_str.include?('.') ? num_str.to_f : num_str.to_i) + elsif b && ((b >= 97 && b <= 122) || (b >= 65 && b <= 90) || b == 95) + # Identifier + id_start = filter_pos filter_pos += 1 - end - # Check for '?' suffix - filter_pos += 1 if filter_pos < len && markup.getbyte(filter_pos) == 63 + while filter_pos < len + b2 = markup.getbyte(filter_pos) + break unless (b2 >= 97 && b2 <= 122) || (b2 >= 65 && b2 <= 90) || (b2 >= 48 && b2 <= 57) || b2 == 95 || b2 == 45 || b2 == 46 + filter_pos += 1 + end + filter_pos += 1 if filter_pos < len && markup.getbyte(filter_pos) == 63 + + # Check if keyword arg (id followed by ':') + kw_check = filter_pos + kw_check += 1 while kw_check < len && markup.getbyte(kw_check) == 32 + if kw_check < len && markup.getbyte(kw_check) == 58 + fall_to_lexer = true + break + end - # Check if this is a keyword arg (id followed by ':') - kw_check = filter_pos - kw_check += 1 while kw_check < len && markup.getbyte(kw_check) == 32 - if kw_check < len && markup.getbyte(kw_check) == 58 - has_kwargs = true # fall through to Lexer - else id_markup = markup.byteslice(id_start, filter_pos - id_start) - cache = parse_context.expression_cache - arg_expr = Expression.parse(id_markup, parse_context.string_scanner, cache) + filter_args << Expression.parse(id_markup, parse_context.string_scanner, parse_context.expression_cache) + else + fall_to_lexer = true + break end - end - if arg_expr != nil && !has_kwargs - # Skip trailing whitespace after arg + # Skip whitespace after arg filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) == 32 - # Check if there's a comma (multiple args) — fall through to Lexer - if filter_pos < len && markup.getbyte(filter_pos) == 44 # ',' - has_kwargs = true + # Comma = more args; pipe/end = done + if filter_pos < len && markup.getbyte(filter_pos) == 44 + filter_pos += 1 + filter_pos += 1 while filter_pos < len && markup.getbyte(filter_pos) == 32 + else + break end end - unless arg_expr != nil && !has_kwargs + if fall_to_lexer # Complex filter — fall to Lexer for this and remaining filters rest_start = fname_start rest_start -= 1 while rest_start > pos && markup.getbyte(rest_start) != 124 @@ -283,7 +288,7 @@ module Liquid return true end - @filters << [filtername, [arg_expr]] + @filters << [filtername, filter_args] else # No args — add as simple filter @filters << [filtername, Const::EMPTY_ARRAY]