mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
VariableLookup: fast path for simple identifier chains
Skip the expensive recursive VariableParser regex for simple lookups like 'product.title' (~90% of real-world cases). SIMPLE_LOOKUP_RE validates the input is a plain a.b.c chain (no brackets, no quotes). On match, byte-walks on dots to split segments instead of invoking the regex engine. Falls through to the original VariableParser scan for complex inputs.
This commit is contained in:
@@ -4,6 +4,25 @@ module Liquid
|
|||||||
class VariableLookup
|
class VariableLookup
|
||||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||||
|
|
||||||
|
# Matches simple identifier chains: name(.name)* with no brackets/quotes
|
||||||
|
SIMPLE_LOOKUP_RE = /\A[\w-]+\??(?:\.[\w-]+\??)*\z/
|
||||||
|
|
||||||
|
# Returns true when markup is a simple dotted identifier chain that the
|
||||||
|
# fast path in initialize can handle. Accepts:
|
||||||
|
# - Single names: "product", "item"
|
||||||
|
# - Dotted chains: "product.title", "cart.items.first"
|
||||||
|
# - Question-mark suffixes: "product.available?"
|
||||||
|
# - Hyphens in names: "my-var.some-field"
|
||||||
|
# Rejects (falls through to VariableParser regex):
|
||||||
|
# - Bracket lookups: "product[0]", "hash['key']"
|
||||||
|
# - Quoted strings, empty input, leading/trailing dots
|
||||||
|
# Fallback: when this returns false, initialize uses the original
|
||||||
|
# markup.scan(VariableParser) path — behavior is identical to
|
||||||
|
# the pre-optimization code for any input the fast path rejects.
|
||||||
|
def self.simple_lookup?(markup)
|
||||||
|
markup.bytesize > 0 && markup.match?(SIMPLE_LOOKUP_RE)
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :name, :lookups
|
attr_reader :name, :lookups
|
||||||
|
|
||||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||||
@@ -11,6 +30,31 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||||
|
if self.class.simple_lookup?(markup)
|
||||||
|
dot_pos = markup.index('.')
|
||||||
|
if dot_pos.nil?
|
||||||
|
@name = markup
|
||||||
|
@lookups = Const::EMPTY_ARRAY
|
||||||
|
@command_flags = 0
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
@name = markup.byteslice(0, dot_pos)
|
||||||
|
@lookups = []
|
||||||
|
@command_flags = 0
|
||||||
|
pos = dot_pos + 1
|
||||||
|
len = markup.bytesize
|
||||||
|
while pos < len
|
||||||
|
seg_start = pos
|
||||||
|
pos += 1 while pos < len && markup.getbyte(pos) != ByteTables::DOT
|
||||||
|
seg = markup.byteslice(seg_start, pos - seg_start)
|
||||||
|
@command_flags |= 1 << @lookups.length if COMMAND_METHODS.include?(seg)
|
||||||
|
@lookups << seg
|
||||||
|
pos += 1 # skip dot
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
lookups = markup.scan(VariableParser)
|
lookups = markup.scan(VariableParser)
|
||||||
|
|
||||||
name = lookups.shift
|
name = lookups.shift
|
||||||
|
|||||||
Reference in New Issue
Block a user