Liquid::Traversal

This enables traversal over whole document tree.
This commit is contained in:
Stephen Paul Weber
2018-10-15 10:11:58 -04:00
parent 53b8babf52
commit d789ec4175
10 changed files with 371 additions and 8 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ module Liquid
@@operators
end
attr_reader :attachment
attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@@ -83,7 +83,7 @@ module Liquid
protected
attr_reader :child_relation, :child_condition
attr_reader :child_relation
private
+2
View File
@@ -10,6 +10,8 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
attr_reader :to, :from
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
+2
View File
@@ -3,6 +3,8 @@ module Liquid
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
+2
View File
@@ -15,6 +15,8 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options)
super
case markup
+1 -2
View File
@@ -46,8 +46,7 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name
attr_reader :variable_name
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
+6 -4
View File
@@ -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
BOOLEAN_OPERATORS = %w(and or)
attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if'.freeze, markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
end
def nodelist
@blocks.map(&:attachment)
end
def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup)
+2
View File
@@ -16,6 +16,8 @@ module Liquid
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
+2
View File
@@ -2,6 +2,8 @@ module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
+118
View File
@@ -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
+234
View File
@@ -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