Extend fast-path filter parsing to handle comma-separated multi-arg filters (e.g. pluralize: 'item', 'items')\n\nResult: {"status":"keep","combined_µs":4266,"parse_µs":3032,"render_µs":1234,"allocations":26480}

This commit is contained in:
Tobi Lutke
2026-04-04 17:42:33 -07:00
committed by Chris Pak
parent 0e5edcc9da
commit 588966fd1c
+29 -24
View File
@@ -206,35 +206,36 @@ 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
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
arg_expr = markup.byteslice(arg_start + 1, filter_pos - arg_start - 2)
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 # skip -
filter_pos += 1 if b == 45
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
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)
arg_expr = num_str.include?('.') ? num_str.to_f : num_str.to_i
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 — could be keyword arg "key: val" or positional
# Identifier
id_start = filter_pos
filter_pos += 1
while filter_pos < len
@@ -242,32 +243,36 @@ module Liquid
break unless (b2 >= 97 && b2 <= 122) || (b2 >= 65 && b2 <= 90) || (b2 >= 48 && b2 <= 57) || b2 == 95 || b2 == 45 || b2 == 46
filter_pos += 1
end
# Check for '?' suffix
filter_pos += 1 if filter_pos < len && markup.getbyte(filter_pos) == 63
# Check if this is a keyword arg (id followed by ':')
# 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
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)
end
fall_to_lexer = true
break
end
if arg_expr != nil && !has_kwargs
# Skip trailing whitespace after arg
id_markup = markup.byteslice(id_start, filter_pos - id_start)
filter_args << Expression.parse(id_markup, parse_context.string_scanner, parse_context.expression_cache)
else
fall_to_lexer = true
break
end
# 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]