diff --git a/Gemfile b/Gemfile
index a404c0d4..fc327c92 100644
--- a/Gemfile
+++ b/Gemfile
@@ -29,3 +29,7 @@ group :test do
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false
end
+
+group :development do
+ gem "webrick"
+end
diff --git a/Gemfile.lock b/Gemfile.lock
new file mode 100644
index 00000000..eff33ff5
--- /dev/null
+++ b/Gemfile.lock
@@ -0,0 +1,77 @@
+GIT
+ remote: https://github.com/Shopify/liquid-c.git
+ revision: 5a786af7284df55e013ea20551c4b688d02e8326
+ ref: main
+ specs:
+ liquid-c (4.2.0)
+ liquid (>= 5.0.1)
+
+PATH
+ remote: .
+ specs:
+ liquid (5.6.0.alpha)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.2)
+ base64 (0.2.0)
+ benchmark-ips (2.13.0)
+ json (2.7.2)
+ language_server-protocol (3.17.0.3)
+ memory_profiler (1.0.1)
+ minitest (5.22.3)
+ parallel (1.24.0)
+ parser (3.3.0.5)
+ ast (~> 2.4.1)
+ racc
+ racc (1.7.3)
+ rainbow (3.1.1)
+ rake (13.2.1)
+ regexp_parser (2.9.0)
+ rexml (3.2.6)
+ rubocop (1.61.0)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.3.0.2)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.30.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.31.2)
+ parser (>= 3.3.0.4)
+ rubocop-performance (1.19.1)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-ast (>= 0.4.0)
+ rubocop-shopify (2.12.0)
+ rubocop (~> 1.44)
+ ruby-progressbar (1.13.0)
+ stackprof (0.2.26)
+ terminal-table (3.0.2)
+ unicode-display_width (>= 1.1.1, < 3)
+ unicode-display_width (2.5.0)
+ webrick (1.8.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ base64
+ benchmark-ips
+ liquid!
+ liquid-c!
+ memory_profiler
+ minitest
+ rake (~> 13.0)
+ rubocop (~> 1.61.0)
+ rubocop-performance
+ rubocop-shopify (~> 2.12.0)
+ stackprof
+ terminal-table
+ webrick
+
+BUNDLED WITH
+ 2.5.7
diff --git a/example/server/parser_attempt.rb b/example/server/parser_attempt.rb
new file mode 100644
index 00000000..68e214da
--- /dev/null
+++ b/example/server/parser_attempt.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'bundler/inline'
+
+gemfile(true) do
+ source "https://rubygems.org"
+ gem 'liquid'
+end
+
+require 'liquid'
+
+class Parser
+ def initialize(template)
+ @template = template
+ end
+
+ def parse
+ @parsed_template = Liquid::Template.parse(@template)
+ end
+
+ def test_parse
+ document = @parsed_template.root
+
+ variables = []
+
+ if document.is_a?(Liquid::Document)
+ body = document.body
+
+ if body.is_a?(Liquid::BlockBody)
+ body.nodelist.each do |node|
+ next unless node.is_a?(Liquid::Variable)
+
+ puts node.inspect
+ variable_name = node.name.name
+ variables << variable_name
+ end
+ end
+ end
+ puts "Variables: #{variables}"
+ end
+
+ def render
+ @parsed_template.render
+ end
+end
+
+starter_template = "{{ foo }}"
+starter_template_2 = "{{foo}}, {{bar}}"
+starter_template_2_1 = "{{ foo }} and {{ bar }}"
+starter_template_3 = "{% assign foo = 'bar' %}{{ foo }}"
+# Let's start small here
+template = <<~LIQUID
+ {% assign foo = 'bar' %}
+ {{ foo }}
+LIQUID
+
+parser = Parser.new(starter_template)
+parser.parse
+parser.test_parse
diff --git a/example/server/templates/index.liquid b/example/server/templates/index.liquid
index 4872aa84..52905508 100644
--- a/example/server/templates/index.liquid
+++ b/example/server/templates/index.liquid
@@ -1,6 +1,71 @@
-
Hello world!
+
+
-It is {{date}}
+
+
+
+ Simple Code Editor
+
+
+
+
+
+
+
+
+
+ {% snippet "main" %}
-
Check out the Products screen
+ {% # Snippet input %}
+ {% snippet "input" |type, name| %}
+
+
+
+
+ {% endsnippet %}
+
+ {% snippet "league" %}
+
Welcome to the league of super evil
+ {% endsnippet %}
+
+ {% render "league" %}
+ {% render "input", type: "text" %}
+ {% render "input", type: "password" %}
+
+ {% endsnippet %}
+ {% render 'main' %}
+
+
+
+
+
+
+
diff --git a/lib/liquid/locales/en.yml b/lib/liquid/locales/en.yml
index b99d490c..b2196686 100644
--- a/lib/liquid/locales/en.yml
+++ b/lib/liquid/locales/en.yml
@@ -5,6 +5,7 @@
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
+ snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [quoted string]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
diff --git a/lib/liquid/tags.rb b/lib/liquid/tags.rb
index dff7553f..3ec5e7cf 100644
--- a/lib/liquid/tags.rb
+++ b/lib/liquid/tags.rb
@@ -20,6 +20,7 @@ require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
require_relative "tags/doc"
+require_relative "tags/snippet"
module Liquid
module Tags
@@ -44,6 +45,7 @@ module Liquid
'echo' => Echo,
'tablerow' => TableRow,
'doc' => Doc,
+ 'snippet' => Snippet,
}.freeze
end
end
diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb
index 4d29e420..5bff6073 100644
--- a/lib/liquid/tags/render.rb
+++ b/lib/liquid/tags/render.rb
@@ -51,6 +51,24 @@ module Liquid
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
+ # Inline snippets take precedence over external snippets
+ if (inline_snippet = context.registers[:inline_snippet][template_name])
+ inner_context = context.new_isolated_subcontext
+
+ snippet_body = inline_snippet[:body]
+ snippet_args = inline_snippet[:args]
+ # Validate and set the arguments in the inner context
+ @attributes.each do |key, value|
+ unless snippet_args.include?(key)
+ raise Liquid::ArgumentError, "Invalid argument `#{key}` for snippet `#{template_name}`"
+ end
+
+ inner_context[key] = context.evaluate(value)
+ end
+
+ return output << snippet_body.render(inner_context)
+ end
+
partial = PartialCache.load(
template_name,
context: context,
diff --git a/lib/liquid/tags/snippet.rb b/lib/liquid/tags/snippet.rb
new file mode 100644
index 00000000..72c6c282
--- /dev/null
+++ b/lib/liquid/tags/snippet.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Liquid
+ # @liquid_public_docs
+ # @liquid_type tag
+ # @liquid_category theme
+ # @liquid_name snippet
+ # @liquid_summary
+ # Creates a new inline snippet using a string value as the identifier.
+ # @liquid_description
+ # You can create inline snippets to make your Liquid code more modular.
+ # @liquid_syntax
+ # {% snippet "input" %}
+ # value
+ # {% endsnippet %}
+ class Snippet < Block
+ SYNTAX = /(#{QuotedString})(?:\s*\|\s*([\w\s,]+)\s*\|)?/o
+ def initialize(tag_name, markup, options)
+ super
+
+ if markup =~ SYNTAX
+ @to = Regexp.last_match(1)
+ args = Regexp.last_match(2)
+
+ @args = args ? args.split(/\s*,\s*/) : []
+ else
+ raise SyntaxError, options[:locale].t("errors.syntax.snippet")
+ end
+ end
+
+ def render(context)
+ context.registers[:inline_snippet] ||= {}
+ context.registers[:inline_snippet][snippet_id] = {
+ body: snippet_body,
+ args: @args,
+ }
+ ''
+ end
+
+ private
+
+ def snippet_id
+ @to[1, @to.size - 2]
+ end
+
+ def snippet_body
+ body = @body
+ body
+ end
+ end
+end
diff --git a/test/integration/tags/snippet_test.rb b/test/integration/tags/snippet_test.rb
new file mode 100644
index 00000000..fd1cbcdb
--- /dev/null
+++ b/test/integration/tags/snippet_test.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+require 'test_helper'
+
+class SnippetTest < Minitest::Test
+ include Liquid
+
+ def test_valid_inline_snippet
+ template = <<~LIQUID.strip
+ {% snippet "input" %}
+ Hey
+ {% endsnippet %}
+ LIQUID
+ expected = ''
+
+ assert_template_result(expected, template)
+ end
+
+ def test_invalid_inline_snippet
+ template = <<~LIQUID.strip
+ {% snippet input %}
+ Hey
+ {% endsnippet %}
+ LIQUID
+ expected = "Syntax Error in 'snippet' - Valid syntax: snippet [quoted string]"
+
+ assert_match_syntax_error(expected, template)
+ end
+
+ def test_render_inline_snippet
+ template = <<~LIQUID.strip
+ {% snippet "hey" %}
+ Hey
+ {% endsnippet %}
+
+ {%- render "hey" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+ Hey
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_multiple_inline_snippets
+ template = <<~LIQUID.strip
+ {% snippet "input" %}
+
+ {% endsnippet %}
+
+ {% snippet "banner" %}
+
+ {% endsnippet %}
+
+ {%- render "input" -%}
+ {%- render "banner" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+
+
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_inline_snippet_with_argument
+ template = <<~LIQUID.strip
+ {% snippet "input" |type| %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_inline_snippet_with_multiple_arguments
+ template = <<~LIQUID.strip
+ {% snippet "input" |type, value| %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text", value: "Hello" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_inline_snippets_using_same_argument_name
+ template = <<~LIQUID.strip
+ {% snippet "input" |type| %}
+
+ {% endsnippet %}
+
+ {% snippet "inputs" |type, value| %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text" -%}
+ {%- render "inputs", type: "password", value: "pass" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+
+
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_inline_snippet_empty_string_when_missing_argument
+ template = <<~LIQUID.strip
+ {% snippet "input" |type| %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_inline_snippet_shouldnt_leak_context
+ template = <<~LIQUID.strip
+ {% snippet "input" |type, value| %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text", value: "Hello" -%}
+
+ {{ type }}
+ {{ value }}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+
+ def test_render_multiple_inline_snippets_without_leaking_context
+ template = <<~LIQUID.strip
+ {% snippet "input" |type| %}
+
+ {% endsnippet %}
+ {% snippet "no_leak" %}
+
+ {% endsnippet %}
+
+ {%- render "input", type: "text" -%}
+ {%- render "no_leak" -%}
+ LIQUID
+ expected = <<~OUTPUT
+
+
+
+
+
+ OUTPUT
+
+ assert_template_result(expected, template)
+ end
+end