# frozen_string_literal: true require 'test_helper' # Ruby 3.3 compatibility for peek_byte and scan_byte require 'strscan' unless StringScanner.method_defined?(:peek_byte) class StringScanner def peek_byte return nil if eos? string.getbyte(pos) end end end unless StringScanner.method_defined?(:scan_byte) class StringScanner def scan_byte return nil if eos? byte = string.getbyte(pos) self.pos += 1 byte end end end class CompileTest < Minitest::Test include Liquid def test_compile_simple_string template = Template.parse("Hello, World!") compiled = template.compile_to_ruby assert_equal "Hello, World!", compiled.call({}) end def test_compile_variable template = Template.parse("Hello, {{ name }}!") compiled = template.compile_to_ruby assert_equal "Hello, World!", compiled.call({ "name" => "World" }) end def test_compile_variable_with_filter template = Template.parse("{{ name | upcase }}") compiled = template.compile_to_ruby assert_equal "WORLD", compiled.call({ "name" => "world" }) end def test_compile_variable_with_multiple_filters template = Template.parse("{{ name | downcase | capitalize }}") compiled = template.compile_to_ruby assert_equal "Hello", compiled.call({ "name" => "HELLO" }) end def test_compile_if_true template = Template.parse("{% if show %}visible{% endif %}") compiled = template.compile_to_ruby assert_equal "visible", compiled.call({ "show" => true }) assert_equal "", compiled.call({ "show" => false }) end def test_compile_if_else template = Template.parse("{% if show %}yes{% else %}no{% endif %}") compiled = template.compile_to_ruby assert_equal "yes", compiled.call({ "show" => true }) assert_equal "no", compiled.call({ "show" => false }) end def test_compile_if_elsif template = Template.parse("{% if x == 1 %}one{% elsif x == 2 %}two{% else %}other{% endif %}") compiled = template.compile_to_ruby assert_equal "one", compiled.call({ "x" => 1 }) assert_equal "two", compiled.call({ "x" => 2 }) assert_equal "other", compiled.call({ "x" => 3 }) end def test_compile_unless template = Template.parse("{% unless hidden %}visible{% endunless %}") compiled = template.compile_to_ruby assert_equal "visible", compiled.call({ "hidden" => false }) assert_equal "", compiled.call({ "hidden" => true }) end def test_compile_for_loop template = Template.parse("{% for item in items %}{{ item }} {% endfor %}") compiled = template.compile_to_ruby assert_equal "a b c ", compiled.call({ "items" => ["a", "b", "c"] }) end def test_compile_for_loop_with_forloop template = Template.parse("{% for item in items %}{{ forloop.index }}:{{ item }} {% endfor %}") compiled = template.compile_to_ruby assert_equal "1:a 2:b 3:c ", compiled.call({ "items" => ["a", "b", "c"] }) end def test_compile_for_loop_else template = Template.parse("{% for item in items %}{{ item }}{% else %}empty{% endfor %}") compiled = template.compile_to_ruby assert_equal "abc", compiled.call({ "items" => ["a", "b", "c"] }) assert_equal "empty", compiled.call({ "items" => [] }) end def test_compile_for_with_limit template = Template.parse("{% for item in items limit:2 %}{{ item }}{% endfor %}") compiled = template.compile_to_ruby assert_equal "ab", compiled.call({ "items" => ["a", "b", "c", "d"] }) end def test_compile_assign template = Template.parse("{% assign x = 'hello' %}{{ x }}") compiled = template.compile_to_ruby assert_equal "hello", compiled.call({}) end def test_compile_assign_with_filter template = Template.parse("{% assign x = name | upcase %}{{ x }}") compiled = template.compile_to_ruby assert_equal "WORLD", compiled.call({ "name" => "world" }) end def test_compile_capture template = Template.parse("{% capture greeting %}Hello, {{ name }}!{% endcapture %}{{ greeting }}") compiled = template.compile_to_ruby assert_equal "Hello, World!", compiled.call({ "name" => "World" }) end def test_compile_case template = Template.parse("{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}") compiled = template.compile_to_ruby assert_equal "one", compiled.call({ "x" => 1 }) assert_equal "two", compiled.call({ "x" => 2 }) assert_equal "other", compiled.call({ "x" => 3 }) end def test_compile_raw template = Template.parse("{% raw %}{{ not_a_variable }}{% endraw %}") compiled = template.compile_to_ruby assert_equal "{{ not_a_variable }}", compiled.call({}) end def test_compile_comment template = Template.parse("before{% comment %}hidden{% endcomment %}after") compiled = template.compile_to_ruby assert_equal "beforeafter", compiled.call({}) end def test_compile_increment template = Template.parse("{% increment x %}{% increment x %}{% increment x %}") compiled = template.compile_to_ruby assert_equal "012", compiled.call({}) end def test_compile_decrement template = Template.parse("{% decrement x %}{% decrement x %}{% decrement x %}") compiled = template.compile_to_ruby assert_equal "-1-2-3", compiled.call({}) end def test_compile_cycle template = Template.parse("{% for i in (1..3) %}{% cycle 'a', 'b' %}{% endfor %}") compiled = template.compile_to_ruby assert_equal "aba", compiled.call({}) end def test_compile_nested_property_access template = Template.parse("{{ user.profile.name }}") compiled = template.compile_to_ruby data = { "user" => { "profile" => { "name" => "Alice" } } } assert_equal "Alice", compiled.call(data) end def test_compile_array_access template = Template.parse("{{ items[1] }}") compiled = template.compile_to_ruby assert_equal "b", compiled.call({ "items" => ["a", "b", "c"] }) end def test_compile_size_filter template = Template.parse("{{ items | size }}") compiled = template.compile_to_ruby assert_equal "3", compiled.call({ "items" => [1, 2, 3] }) end def test_compile_join_filter template = Template.parse("{{ items | join: ', ' }}") compiled = template.compile_to_ruby assert_equal "a, b, c", compiled.call({ "items" => ["a", "b", "c"] }) end def test_compile_split_filter template = Template.parse("{% assign arr = str | split: ',' %}{{ arr | size }}") compiled = template.compile_to_ruby assert_equal "3", compiled.call({ "str" => "a,b,c" }) end def test_compile_math_filters template = Template.parse("{{ x | plus: 5 | minus: 2 | times: 3 }}") compiled = template.compile_to_ruby assert_equal "24", compiled.call({ "x" => 5 }) end def test_compile_default_filter template = Template.parse("{{ x | default: 'nothing' }}") compiled = template.compile_to_ruby assert_equal "hello", compiled.call({ "x" => "hello" }) assert_equal "nothing", compiled.call({ "x" => nil }) assert_equal "nothing", compiled.call({}) end def test_compile_first_last_filters template = Template.parse("{{ items | first }}-{{ items | last }}") compiled = template.compile_to_ruby assert_equal "a-c", compiled.call({ "items" => ["a", "b", "c"] }) end def test_compile_escape_filter template = Template.parse("{{ html | escape }}") compiled = template.compile_to_ruby assert_equal "<p>hello</p>", compiled.call({ "html" => "
hello
" }) end def test_compile_replace_filter template = Template.parse("{{ str | replace: 'foo', 'bar' }}") compiled = template.compile_to_ruby assert_equal "bar baz bar", compiled.call({ "str" => "foo baz foo" }) end def test_compile_append_prepend_filters template = Template.parse("{{ name | prepend: 'Hello, ' | append: '!' }}") compiled = template.compile_to_ruby assert_equal "Hello, World!", compiled.call({ "name" => "World" }) end def test_compile_for_break template = Template.parse("{% for i in (1..5) %}{% if i == 3 %}{% break %}{% endif %}{{ i }}{% endfor %}") compiled = template.compile_to_ruby assert_equal "12", compiled.call({}) end def test_compile_for_continue template = Template.parse("{% for i in (1..5) %}{% if i == 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}") compiled = template.compile_to_ruby assert_equal "1245", compiled.call({}) end def test_compile_comparison_operators template = Template.parse("{% if x > 5 %}big{% elsif x == 5 %}five{% else %}small{% endif %}") compiled = template.compile_to_ruby assert_equal "big", compiled.call({ "x" => 10 }) assert_equal "five", compiled.call({ "x" => 5 }) assert_equal "small", compiled.call({ "x" => 2 }) end def test_compile_and_or_operators template = Template.parse("{% if a and b %}both{% elsif a or b %}one{% else %}none{% endif %}") compiled = template.compile_to_ruby assert_equal "both", compiled.call({ "a" => true, "b" => true }) assert_equal "one", compiled.call({ "a" => true, "b" => false }) assert_equal "none", compiled.call({ "a" => false, "b" => false }) end def test_compile_contains template = Template.parse("{% if str contains 'hello' %}found{% else %}not found{% endif %}") compiled = template.compile_to_ruby assert_equal "found", compiled.call({ "str" => "say hello world" }) assert_equal "not found", compiled.call({ "str" => "goodbye" }) end def test_compile_produces_valid_ruby template = Template.parse("{% for item in items %}{{ item | upcase }}{% endfor %}") compiled = template.compile_to_ruby # Should produce valid Ruby syntax - if eval succeeds without error, it's valid proc = eval(compiled.code) assert_kind_of Proc, proc end def test_compile_vs_render_equivalence templates = [ "Hello, {{ name }}!", "{% if show %}visible{% else %}hidden{% endif %}", "{% for i in (1..3) %}{{ i }}{% endfor %}", "{{ str | upcase | split: '' | join: '-' }}", "{% assign x = 5 %}{% assign y = x | plus: 3 %}{{ y }}", ] assigns_list = [ { "name" => "World", "show" => true, "str" => "hello" }, { "name" => "Ruby", "show" => false, "str" => "test" }, ] templates.each do |source| template = Template.parse(source) compiled = template.compile_to_ruby assigns_list.each do |assigns| expected = template.render(assigns) actual = compiled.call(assigns.dup) assert_equal expected, actual, "Mismatch for template '#{source}' with assigns #{assigns}" end end end def test_compiled_template_class template = Template.parse("Hello, {{ name }}!") compiled = template.compile_to_ruby assert_instance_of Liquid::Compile::CompiledTemplate, compiled assert_respond_to compiled, :call assert_respond_to compiled, :code assert_respond_to compiled, :to_s assert_respond_to compiled, :to_proc end def test_compiled_template_code template = Template.parse("Hello!") compiled = template.compile_to_ruby assert_kind_of String, compiled.code assert_includes compiled.code, "__output__" assert_includes compiled.code, "Hello!" end def test_external_tags_indicator # A simple template should have no external tags template = Template.parse("Hello!") compiled = template.compile_to_ruby assert_equal false, compiled.has_external_tags? assert_empty compiled.external_tags end def test_external_filters_indicator # A simple template should have no external filters template = Template.parse("{{ name | upcase }}") compiled = template.compile_to_ruby assert_equal false, compiled.has_external_filters? end def test_comprehensive_output_equivalence # Comprehensive test comparing compiled vs interpreted output test_cases = [ # Basic variables { source: "{{ x }}", assigns: { "x" => "hello" } }, { source: "{{ x }}", assigns: { "x" => 123 } }, { source: "{{ x }}", assigns: { "x" => nil } }, # Nested access { source: "{{ a.b.c }}", assigns: { "a" => { "b" => { "c" => "deep" } } } }, { source: "{{ items[0] }}", assigns: { "items" => ["first", "second"] } }, # Filters { source: "{{ x | upcase }}", assigns: { "x" => "hello" } }, { source: "{{ x | size }}", assigns: { "x" => [1, 2, 3] } }, { source: "{{ x | default: 'fallback' }}", assigns: { "x" => nil } }, { source: "{{ x | plus: 10 }}", assigns: { "x" => 5 } }, { source: "{{ x | split: ',' | first }}", assigns: { "x" => "a,b,c" } }, # Conditionals { source: "{% if x %}yes{% endif %}", assigns: { "x" => true } }, { source: "{% if x %}yes{% endif %}", assigns: { "x" => false } }, { source: "{% if x > 5 %}big{% else %}small{% endif %}", assigns: { "x" => 10 } }, # Loops { source: "{% for i in items %}{{ i }}{% endfor %}", assigns: { "items" => [1, 2, 3] } }, { source: "{% for i in (1..3) %}{{ i }}{% endfor %}", assigns: {} }, { source: "{% for i in items %}{{ forloop.index }}{% endfor %}", assigns: { "items" => %w[a b] } }, { source: "{% for i in items %}{% if forloop.first %}first{% endif %}{% endfor %}", assigns: { "items" => [1, 2] } }, # Assignments { source: "{% assign y = x | upcase %}{{ y }}", assigns: { "x" => "hello" } }, { source: "{% capture c %}Hello {{ x }}{% endcapture %}{{ c }}", assigns: { "x" => "World" } }, # Case { source: "{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}", assigns: { "x" => 1 } }, { source: "{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}", assigns: { "x" => 3 } }, # Increment/Decrement { source: "{% increment x %}{% increment x %}", assigns: {} }, { source: "{% decrement x %}{% decrement x %}", assigns: {} }, # Cycle { source: "{% for i in (1..4) %}{% cycle 'a', 'b' %}{% endfor %}", assigns: {} }, # Break/Continue { source: "{% for i in (1..5) %}{% if i == 3 %}{% break %}{% endif %}{{ i }}{% endfor %}", assigns: {} }, { source: "{% for i in (1..5) %}{% if i == 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}", assigns: {} }, # Raw { source: "{% raw %}{{ not a var }}{% endraw %}", assigns: {} }, # Comment { source: "before{% comment %}hidden{% endcomment %}after", assigns: {} }, # Tablerow { source: "{% tablerow i in items cols:2 %}{{ i }}{% endtablerow %}", assigns: { "items" => [1, 2, 3] } }, ] test_cases.each do |tc| source = tc[:source] assigns = tc[:assigns] template = Template.parse(source) compiled = template.compile_to_ruby expected = template.render(assigns.dup) actual = compiled.call(assigns.dup) assert_equal expected, actual, "Output mismatch for: #{source.inspect}\nAssigns: #{assigns.inspect}" end end def test_debug_mode_adds_comments template = Template.parse("{{ name }}") compiled = template.compile_to_ruby(debug: true) assert_includes compiled.code, "# LIQUID" assert_includes compiled.code, "# Compiled from Liquid template" end def test_filter_handler_can_be_set # Create a custom filter module filter_mod = Module.new do def custom_filter(input) "custom:#{input}" end end class_with_filter = Class.new do include filter_mod end template = Template.parse("{{ x | custom_filter }}") compiled = template.compile_to_ruby compiled.filter_handler = class_with_filter.new # The template should use the custom filter result = compiled.call({ "x" => "test" }) assert_equal "custom:test", result end # Test Drop support def test_compile_with_drop # Create a simple Drop class product_drop = Class.new(Liquid::Drop) do def initialize(name, price) super() @name = name @price = price end def name @name end def price @price end def discounted_price @price * 0.9 end end template = Template.parse("Product: {{ product.name }} costs ${{ product.price }}") compiled = template.compile_to_ruby drop = product_drop.new("Widget", 100) result = compiled.call({ "product" => drop }) assert_equal "Product: Widget costs $100", result end def test_compile_with_drop_context_access # Create a Drop that uses context context_aware_drop = Class.new(Liquid::Drop) do def initialize(multiplier) super() @multiplier = multiplier end def computed_value # Access another variable via context base = @context["base_value"] || 0 base * @multiplier end end template = Template.parse("Result: {{ calc.computed_value }}") compiled = template.compile_to_ruby drop = context_aware_drop.new(3) result = compiled.call({ "calc" => drop, "base_value" => 10 }) assert_equal "Result: 30", result end def test_compile_with_nested_drops inner_drop = Class.new(Liquid::Drop) do def initialize(value) super() @value = value end def value @value end end outer_drop = Class.new(Liquid::Drop) do def initialize(inner) super() @inner = inner end def inner @inner end end template = Template.parse("{{ outer.inner.value }}") compiled = template.compile_to_ruby inner = inner_drop.new("nested!") outer = outer_drop.new(inner) result = compiled.call({ "outer" => outer }) assert_equal "nested!", result end def test_compile_with_forloop_drop # ForloopDrop is a built-in Drop - ensure it works template = Template.parse("{% for item in items %}{{ forloop.index }}:{{ item }} {% endfor %}") compiled = template.compile_to_ruby # Note: Compiled code uses a hash for forloop, not the actual ForloopDrop # This test verifies the hash-based forloop still works result = compiled.call({ "items" => ["a", "b", "c"] }) assert_equal "1:a 2:b 3:c ", result end def test_compile_with_registers template = Template.parse("{{ product.name }}") compiled = template.compile_to_ruby # Create a Drop that checks registers product_drop = Class.new(Liquid::Drop) do def name # Access registers through context store = @context.registers[:store] || "Unknown Store" "Product from #{store}" end end drop = product_drop.new result = compiled.call({ "product" => drop }, registers: { store: "Acme Corp" }) assert_equal "Product from Acme Corp", result end end