mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 08:50:45 -07:00
Dead code removed:
cursor.rb: parse_for_markup + attr_reader :for_var/collection/reversed
(zero callers in lib/; method also incomplete -- no limit/offset)
for.rb: Syntax regex, REVERSED_BYTES constant (both unreferenced by
lax_parse and strict_parse)
block_body.rb: BLANK_STRING_REGEX (exact duplicate of WhitespaceOrNothing)
Idioms:
expression.rb: parse_number returns nil on failure, not false
(Ruby convention for 'no result'; callers use if (num = parse_number))
block_body.rb: freeze (idempotent by spec); whitespace_handler marked private;
render_node gets rationale comment; redundant comments collapsed
variable_lookup.rb: COMMAND_METHODS -> %w[]; initialize loop uses each_with_index;
removes redundant &. on second clause of &&
for.rb: strict2_parse -> alias_method; nil-guard if/else -> ternary;
render_else -> ternary; include? checks unconsumed rest only
condition.rb: loop/break chain -> while condition.child_relation
utils.rb: tightens slice_collection_using_each loop
Comments:
variable.rb: backward pipe-walk algorithm; SPACE-only whitespace asymmetry
cursor.rb: COMPARISON_OPS identity-map exists for frozen string interning
if.rb: include? pre-check is both correctness guard and perf gate
for.rb: cursor->regex fallback for limit:/offset: attributes
strainer_template.rb: __LINE__+1 limitation in module_eval loop
156 lines
4.4 KiB
Ruby
156 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Liquid
|
|
class Expression
|
|
LITERALS = {
|
|
nil => nil,
|
|
'nil' => nil,
|
|
'null' => nil,
|
|
'' => nil,
|
|
'true' => true,
|
|
'false' => false,
|
|
'blank' => '',
|
|
'empty' => '',
|
|
# in lax mode, minus sign can be a VariableLookup
|
|
# For simplicity and performace, we treat it like a literal
|
|
'-' => VariableLookup.parse("-", nil).freeze,
|
|
}.freeze
|
|
|
|
# Use an atomic group (?>...) to avoid pathological backtracing from
|
|
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
|
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
|
|
|
class << self
|
|
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
|
|
parse(parser.expression, ss, cache)
|
|
end
|
|
|
|
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
|
return unless markup
|
|
|
|
# Only strip if there's leading/trailing whitespace (avoids allocation)
|
|
first_byte = markup.getbyte(0)
|
|
if first_byte && ByteTables::WHITESPACE[first_byte]
|
|
markup = markup.strip
|
|
elsif first_byte
|
|
markup = markup.strip if ByteTables::WHITESPACE[markup.getbyte(markup.bytesize - 1)]
|
|
end
|
|
|
|
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
|
(markup.start_with?("'") && markup.end_with?("'"))
|
|
return markup.byteslice(1, markup.bytesize - 2)
|
|
elsif LITERALS.key?(markup)
|
|
return LITERALS[markup]
|
|
end
|
|
|
|
# Cache only exists during parsing
|
|
if cache
|
|
return cache[markup] if cache.key?(markup)
|
|
|
|
cache[markup] = inner_parse(markup, ss, cache).freeze
|
|
else
|
|
inner_parse(markup, ss, nil).freeze
|
|
end
|
|
end
|
|
|
|
def inner_parse(markup, ss, cache)
|
|
if markup.start_with?("(") && markup.end_with?(")") && markup =~ RANGES_REGEX
|
|
return RangeLookup.parse(
|
|
Regexp.last_match(1),
|
|
Regexp.last_match(2),
|
|
ss,
|
|
cache,
|
|
)
|
|
end
|
|
|
|
if (num = parse_number(markup, ss))
|
|
num
|
|
else
|
|
VariableLookup.parse(markup, ss, cache)
|
|
end
|
|
end
|
|
|
|
def parse_number(markup, _ss = nil)
|
|
len = markup.bytesize
|
|
return if len == 0
|
|
|
|
# Quick reject: first byte must be digit or dash
|
|
pos = 0
|
|
first = markup.getbyte(pos)
|
|
if first == Cursor::DASH
|
|
pos += 1
|
|
return if pos >= len
|
|
|
|
b = markup.getbyte(pos)
|
|
return unless ByteTables::DIGIT[b]
|
|
|
|
pos += 1
|
|
elsif ByteTables::DIGIT[first]
|
|
pos += 1
|
|
else
|
|
return
|
|
end
|
|
|
|
# Scan digits
|
|
while pos < len
|
|
b = markup.getbyte(pos)
|
|
break unless ByteTables::DIGIT[b]
|
|
|
|
pos += 1
|
|
end
|
|
|
|
# If we consumed everything, it's a simple integer
|
|
if pos == len
|
|
return Integer(markup, 10)
|
|
end
|
|
|
|
# Check for dot (float)
|
|
if markup.getbyte(pos) == Cursor::DOT
|
|
dot_pos = pos
|
|
pos += 1
|
|
# Must have at least one digit after dot
|
|
digit_after_dot = pos
|
|
while pos < len
|
|
b = markup.getbyte(pos)
|
|
break unless ByteTables::DIGIT[b]
|
|
|
|
pos += 1
|
|
end
|
|
|
|
if pos > digit_after_dot && pos == len
|
|
# Simple float like "123.456"
|
|
return markup.to_f
|
|
elsif pos > digit_after_dot
|
|
# Float followed by more content: "1.2.3.4" — scan to find where the
|
|
# numeric portion ends (stop at next dot or non-digit).
|
|
return scan_float_with_trailing(markup, pos, len)
|
|
else
|
|
# dot at end: "123."
|
|
return markup.byteslice(0, dot_pos).to_f
|
|
end
|
|
end
|
|
|
|
# Not a number (has non-digit, non-dot characters)
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
# Scans forward from `pos` through digits, returning the float up to the
|
|
# next dot or the end of string. Returns nil when a non-digit, non-dot
|
|
# byte is found (not a valid number). Used by parse_number for inputs
|
|
# like "1.2.3.4" where the float literal ends at the second dot.
|
|
def scan_float_with_trailing(markup, pos, len)
|
|
while pos < len
|
|
b = markup.getbyte(pos)
|
|
return markup.byteslice(0, pos).to_f if b == Cursor::DOT
|
|
return unless ByteTables::DIGIT[b]
|
|
|
|
pos += 1
|
|
end
|
|
markup.byteslice(0, pos).to_f
|
|
end
|
|
end
|
|
end
|
|
end
|