From d789ec4175176127ca50e0e39e77540c5d7222d6 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 10 Sep 2018 13:36:27 -0400 Subject: [PATCH] Liquid::Traversal This enables traversal over whole document tree. --- lib/liquid/condition.rb | 4 +- lib/liquid/tags/assign.rb | 2 + lib/liquid/tags/case.rb | 2 + lib/liquid/tags/cycle.rb | 2 + lib/liquid/tags/for.rb | 3 +- lib/liquid/tags/if.rb | 10 +- lib/liquid/tags/include.rb | 2 + lib/liquid/tags/table_row.rb | 2 + lib/liquid/traversal.rb | 118 +++++++++++++++ test/integration/traversal_test.rb | 234 +++++++++++++++++++++++++++++ 10 files changed, 371 insertions(+), 8 deletions(-) create mode 100644 lib/liquid/traversal.rb create mode 100644 test/integration/traversal_test.rb diff --git a/lib/liquid/condition.rb b/lib/liquid/condition.rb index 3e798498..72bd2ee7 100644 --- a/lib/liquid/condition.rb +++ b/lib/liquid/condition.rb @@ -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 diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index f6cd5fad..ee6fa76a 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -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 diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb index 453b4d67..f55aa613 100644 --- a/lib/liquid/tags/case.rb +++ b/lib/liquid/tags/case.rb @@ -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 = [] diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb index ad116a61..6cf77a22 100644 --- a/lib/liquid/tags/cycle.rb +++ b/lib/liquid/tags/cycle.rb @@ -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 diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 6c95624d..c529aaec 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -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 diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index 904369dd..2a917410 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -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) diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index a8007036..a334d83f 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -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 diff --git a/lib/liquid/tags/table_row.rb b/lib/liquid/tags/table_row.rb index cfdef33f..99d12ec7 100644 --- a/lib/liquid/tags/table_row.rb +++ b/lib/liquid/tags/table_row.rb @@ -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 diff --git a/lib/liquid/traversal.rb b/lib/liquid/traversal.rb new file mode 100644 index 00000000..339fc2c0 --- /dev/null +++ b/lib/liquid/traversal.rb @@ -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 diff --git a/test/integration/traversal_test.rb b/test/integration/traversal_test.rb new file mode 100644 index 00000000..6254cb9b --- /dev/null +++ b/test/integration/traversal_test.rb @@ -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