From 97e6893c1a31dc00a31228d9f61099b83a3c5171 Mon Sep 17 00:00:00 2001 From: Tobi Lutke Date: Wed, 11 Mar 2026 07:14:14 -0400 Subject: [PATCH] replace VariableParser regex scan with manual byte parser in VariableLookup --- lib/liquid/variable_lookup.rb | 56 ++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/liquid/variable_lookup.rb b/lib/liquid/variable_lookup.rb index 4fba2a65..a61790a2 100644 --- a/lib/liquid/variable_lookup.rb +++ b/lib/liquid/variable_lookup.rb @@ -10,8 +10,62 @@ module Liquid new(markup, string_scanner, cache) end + # Fast manual scanner replacing markup.scan(VariableParser) + # VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|[\w-]+\??/ + # Splits "product.variants[0].title" into ["product", "variants", "[0]", "title"] + def self.scan_variable(markup) + result = [] + pos = 0 + len = markup.bytesize + + while pos < len + byte = markup.getbyte(pos) + + if byte == 91 # '[' + # Scan balanced brackets + depth = 1 + start = pos + pos += 1 + while pos < len && depth > 0 + b = markup.getbyte(pos) + if b == 91 + depth += 1 + elsif b == 93 + depth -= 1 + end + pos += 1 + end + if depth == 0 + result << markup.byteslice(start, pos - start) + else + # Unbalanced bracket - skip '[' and continue + pos = start + 1 + end + elsif byte == 46 # '.' + pos += 1 + elsif (byte >= 97 && byte <= 122) || (byte >= 65 && byte <= 90) || (byte >= 48 && byte <= 57) || byte == 95 || byte == 45 # \w or - + start = pos + pos += 1 + while pos < len + b = markup.getbyte(pos) + break unless (b >= 97 && b <= 122) || (b >= 65 && b <= 90) || (b >= 48 && b <= 57) || b == 95 || b == 45 + pos += 1 + end + # Check trailing '?' + if pos < len && markup.getbyte(pos) == 63 + pos += 1 + end + result << markup.byteslice(start, pos - start) + else + pos += 1 + end + end + + result + end + def initialize(markup, string_scanner = StringScanner.new(""), cache = nil) - lookups = markup.scan(VariableParser) + lookups = self.class.scan_variable(markup) name = lookups.shift if name&.start_with?('[') && name&.end_with?(']')