mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-29 15:15:19 -07:00
Liquid::Traversal
This enables traversal over whole document tree.
This commit is contained in:
@@ -29,7 +29,7 @@ module Liquid
|
|||||||
@@operators
|
@@operators
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :attachment
|
attr_reader :attachment, :child_condition
|
||||||
attr_accessor :left, :operator, :right
|
attr_accessor :left, :operator, :right
|
||||||
|
|
||||||
def initialize(left = nil, operator = nil, right = nil)
|
def initialize(left = nil, operator = nil, right = nil)
|
||||||
@@ -83,7 +83,7 @@ module Liquid
|
|||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
attr_reader :child_relation, :child_condition
|
attr_reader :child_relation
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ module Liquid
|
|||||||
class Assign < Tag
|
class Assign < Tag
|
||||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||||
|
|
||||||
|
attr_reader :to, :from
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ module Liquid
|
|||||||
Syntax = /(#{QuotedFragment})/o
|
Syntax = /(#{QuotedFragment})/o
|
||||||
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
||||||
|
|
||||||
|
attr_reader :blocks, :left
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@blocks = []
|
@blocks = []
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ module Liquid
|
|||||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||||
|
|
||||||
|
attr_reader :variables
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
case markup
|
case markup
|
||||||
|
|||||||
@@ -46,8 +46,7 @@ module Liquid
|
|||||||
class For < Block
|
class For < Block
|
||||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||||
|
|
||||||
attr_reader :collection_name
|
attr_reader :collection_name, :variable_name, :limit, :from
|
||||||
attr_reader :variable_name
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
|||||||
@@ -14,21 +14,23 @@ module Liquid
|
|||||||
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
|
||||||
BOOLEAN_OPERATORS = %w(and or)
|
BOOLEAN_OPERATORS = %w(and or)
|
||||||
|
|
||||||
|
attr_reader :blocks
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
@blocks = []
|
@blocks = []
|
||||||
push_block('if'.freeze, markup)
|
push_block('if'.freeze, markup)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def nodelist
|
||||||
|
@blocks.map(&:attachment)
|
||||||
|
end
|
||||||
|
|
||||||
def parse(tokens)
|
def parse(tokens)
|
||||||
while parse_body(@blocks.last.attachment, tokens)
|
while parse_body(@blocks.last.attachment, tokens)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
|
||||||
@blocks.map(&:attachment)
|
|
||||||
end
|
|
||||||
|
|
||||||
def unknown_tag(tag, markup, tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
||||||
push_block(tag, markup)
|
push_block(tag, markup)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ module Liquid
|
|||||||
class Include < Tag
|
class Include < Tag
|
||||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||||
|
|
||||||
|
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ module Liquid
|
|||||||
class TableRow < Block
|
class TableRow < Block
|
||||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||||
|
|
||||||
|
attr_reader :variable_name, :collection_name, :attributes
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, options)
|
||||||
super
|
super
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Liquid
|
||||||
|
class Traversal
|
||||||
|
def self.for(node, callbacks = Hash.new(proc {}))
|
||||||
|
kase = CASES.find { |(klass, _)| node.is_a?(klass) }&.last
|
||||||
|
(kase || self).new(node, callbacks)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(node, callbacks)
|
||||||
|
@node = node
|
||||||
|
@callbacks = callbacks
|
||||||
|
end
|
||||||
|
|
||||||
|
def callback_for(*classes, &block)
|
||||||
|
callback = block
|
||||||
|
callback = ->(node, _) { block.call(node) } if block.arity.abs == 1
|
||||||
|
callback = ->(_, _) { block.call } if block.arity.zero?
|
||||||
|
classes.each { |klass| @callbacks[klass] = callback }
|
||||||
|
self
|
||||||
|
end
|
||||||
|
|
||||||
|
def traverse(context = nil)
|
||||||
|
children.map do |node|
|
||||||
|
item, new_context = @callbacks[node.class].call(node, context)
|
||||||
|
[
|
||||||
|
item,
|
||||||
|
Traversal.for(node, @callbacks).traverse(new_context || context)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
def children
|
||||||
|
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
||||||
|
end
|
||||||
|
|
||||||
|
class Assign < Traversal
|
||||||
|
def children
|
||||||
|
[@node.from]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Case < Traversal
|
||||||
|
def children
|
||||||
|
[@node.left] + @node.blocks
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Condition < Traversal
|
||||||
|
def children
|
||||||
|
[
|
||||||
|
@node.left, @node.right,
|
||||||
|
@node.child_condition, @node.attachment
|
||||||
|
].compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Cycle < Traversal
|
||||||
|
def children
|
||||||
|
Array(@node.variables)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class For < Traversal
|
||||||
|
def children
|
||||||
|
(super + [@node.limit, @node.from, @node.collection_name]).compact
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class If < Traversal
|
||||||
|
def children
|
||||||
|
@node.blocks
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Include < Traversal
|
||||||
|
def children
|
||||||
|
[
|
||||||
|
@node.template_name_expr,
|
||||||
|
@node.variable_name_expr
|
||||||
|
] + @node.attributes.values
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class TableRow < Traversal
|
||||||
|
def children
|
||||||
|
super + @node.attributes.values + [@node.collection_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Variable < Traversal
|
||||||
|
def children
|
||||||
|
[@node.name] + @node.filters.flatten
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class VariableLookup < Traversal
|
||||||
|
def children
|
||||||
|
@node.lookups
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
CASES = {
|
||||||
|
Liquid::Assign => Assign,
|
||||||
|
Liquid::Case => Case,
|
||||||
|
Liquid::Condition => Condition,
|
||||||
|
Liquid::Cycle => Cycle,
|
||||||
|
Liquid::For => For,
|
||||||
|
Liquid::If => If,
|
||||||
|
Liquid::Include => Include,
|
||||||
|
Liquid::TableRow => TableRow,
|
||||||
|
Liquid::Variable => Variable,
|
||||||
|
Liquid::VariableLookup => VariableLookup
|
||||||
|
}.freeze
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'test_helper'
|
||||||
|
require 'liquid/traversal'
|
||||||
|
|
||||||
|
class TraversalTest < Minitest::Test
|
||||||
|
include Liquid
|
||||||
|
|
||||||
|
def test_variable
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({{ test }}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_varible_with_filter
|
||||||
|
assert_equal(
|
||||||
|
["test", "infilter"],
|
||||||
|
traversal(%({{ test | split: infilter }}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_dynamic_variable
|
||||||
|
assert_equal(
|
||||||
|
["test", "inlookup"],
|
||||||
|
traversal(%({{ test[inlookup] }}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_if_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if test %}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complex_if_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 and 2 == test %}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_if_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 %}{{ test }}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unless_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% unless test %}{% endunless %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complex_unless_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% unless 1 == 1 and 2 == test %}{% endunless %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unless_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% unless 1 == 1 %}{{ test }}{% endunless %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_elsif_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 %}{% elsif test %}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_complex_elsif_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_elsif_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_else_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_left
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% case test %}{% endcase %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_condition
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% case 1 %}{% when test %}{% endcase %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_when_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_case_else_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% case 1 %}{% else %}{{ test }}{% endcase %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_for_in
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% for x in test %}{% endfor %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_for_limit
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% for x in (1..5) limit: test %}{% endfor %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_for_offset
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% for x in (1..5) offset: test %}{% endfor %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_for_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% for x in (1..5) %}{{ test }}{% endfor %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_in
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% tablerow x in test %}{% endtablerow %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_limit
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% tablerow x in (1..5) limit: test %}{% endtablerow %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_offset
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% tablerow x in (1..5) offset: test %}{% endtablerow %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_tablerow_body
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_cycle
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% cycle test %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_assign
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% assign x = test %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_capture
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% capture x %}{{ test }}{% endcapture %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% include test %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include_with
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% include "hai" with test %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_include_for
|
||||||
|
assert_equal(
|
||||||
|
["test"],
|
||||||
|
traversal(%({% include "hai" for test %}))
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def traversal(template)
|
||||||
|
ParseTreeVisitor
|
||||||
|
.for(Template.parse(template).root)
|
||||||
|
.add_callback_for(VariableLookup, &:name)
|
||||||
|
.visit.flatten.compact
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user