mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 00:40:40 -07:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3270183a7 | ||
|
|
3399981b89 | ||
|
|
404d71613c | ||
|
|
98a69c80ef | ||
|
|
30ea917a38 | ||
|
|
d7045f9d64 | ||
|
|
b397513f8b | ||
|
|
ffe48869be |
@@ -28,3 +28,7 @@ group :test do
|
|||||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem "webrick"
|
||||||
|
end
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ GEM
|
|||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
|
webrick (1.8.1)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
ruby
|
ruby
|
||||||
@@ -70,6 +71,7 @@ DEPENDENCIES
|
|||||||
rubocop-shopify (~> 2.12.0)
|
rubocop-shopify (~> 2.12.0)
|
||||||
stackprof
|
stackprof
|
||||||
terminal-table
|
terminal-table
|
||||||
|
webrick
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.7
|
2.5.7
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,6 +1,71 @@
|
|||||||
<p>Hello world!</p>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
<p>It is {{date}}</p>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Simple Code Editor</title>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/dracula.min.css">
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/xml/xml.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/htmlmixed/htmlmixed.min.js"></script>
|
||||||
|
<style>
|
||||||
|
.liquid, .CodeMirror {
|
||||||
|
position: fixed;
|
||||||
|
height: 100vh;
|
||||||
|
width: 50vw;
|
||||||
|
top: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.liquid {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.CodeMirror {
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.CodeMirror-hscrollbar {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="liquid">
|
||||||
|
{% snippet "main" %}
|
||||||
|
|
||||||
<p>Check out the <a href="/products">Products</a> screen </p>
|
{% # Snippet input %}
|
||||||
|
{% snippet "input" |type, name| %}
|
||||||
|
<div>
|
||||||
|
<label>{{ type | capitalize }}</label>
|
||||||
|
<input type={{ type }}>
|
||||||
|
</div>
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{% snippet "league" %}
|
||||||
|
<h1>Welcome to the league of super evil</h1>
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{% render "league" %}
|
||||||
|
{% render "input", type: "text" %}
|
||||||
|
{% render "input", type: "password" %}
|
||||||
|
|
||||||
|
{% endsnippet %}
|
||||||
|
{% render 'main' %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<textarea id="code">
|
||||||
|
{% capture html %}{% render 'main' %}{% endcapture %}
|
||||||
|
{{ html | escape }}
|
||||||
|
</textarea>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const editorElement = document.querySelector('#code')
|
||||||
|
const editor = CodeMirror.fromTextArea(editorElement, {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: "htmlmixed",
|
||||||
|
theme: "dracula"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
||||||
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
||||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
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: "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_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) "
|
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ require_relative "tags/comment"
|
|||||||
require_relative "tags/raw"
|
require_relative "tags/raw"
|
||||||
require_relative "tags/render"
|
require_relative "tags/render"
|
||||||
require_relative "tags/cycle"
|
require_relative "tags/cycle"
|
||||||
|
require_relative "tags/snippet"
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
module Tags
|
module Tags
|
||||||
@@ -42,6 +43,7 @@ module Liquid
|
|||||||
'if' => If,
|
'if' => If,
|
||||||
'echo' => Echo,
|
'echo' => Echo,
|
||||||
'tablerow' => TableRow,
|
'tablerow' => TableRow,
|
||||||
|
'snippet' => Snippet,
|
||||||
}.freeze
|
}.freeze
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -66,6 +66,24 @@ module Liquid
|
|||||||
template_name = @template_name_expr
|
template_name = @template_name_expr
|
||||||
raise ::ArgumentError unless template_name.is_a?(String)
|
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(
|
partial = PartialCache.load(
|
||||||
template_name,
|
template_name,
|
||||||
context: context,
|
context: context,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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" %}
|
||||||
|
<input />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{% snippet "banner" %}
|
||||||
|
<marquee direction="up" height="100px">
|
||||||
|
Welcome to my store!
|
||||||
|
</marquee>
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input" -%}
|
||||||
|
{%- render "banner" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input />
|
||||||
|
|
||||||
|
<marquee direction="up" height="100px">
|
||||||
|
Welcome to my store!
|
||||||
|
</marquee>
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_inline_snippet_with_argument
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type| %}
|
||||||
|
<input type="{{ type }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
<input type="text" />
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_inline_snippet_with_multiple_arguments
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type, value| %}
|
||||||
|
<input type="{{ type }}" value="{{ value }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text", value: "Hello" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
<input type="text" value="Hello" />
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_inline_snippets_using_same_argument_name
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type| %}
|
||||||
|
<input type="{{ type }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{% snippet "inputs" |type, value| %}
|
||||||
|
<input type="{{ type }}" value="{{ value }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text" -%}
|
||||||
|
{%- render "inputs", type: "password", value: "pass" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<input type="text" />
|
||||||
|
|
||||||
|
<input type="password" value="pass" />
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_inline_snippet_empty_string_when_missing_argument
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type| %}
|
||||||
|
<input type="{{ type }}" value="{{ value }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
<input type="text" value="" />
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_inline_snippet_shouldnt_leak_context
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type, value| %}
|
||||||
|
<input type="{{ type }}" value="{{ value }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text", value: "Hello" -%}
|
||||||
|
|
||||||
|
{{ type }}
|
||||||
|
{{ value }}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
<input type="text" value="Hello" />
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_render_multiple_inline_snippets_without_leaking_context
|
||||||
|
template = <<~LIQUID.strip
|
||||||
|
{% snippet "input" |type| %}
|
||||||
|
<input type="{{ type }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
{% snippet "no_leak" %}
|
||||||
|
<input type="{{ type }}" />
|
||||||
|
{% endsnippet %}
|
||||||
|
|
||||||
|
{%- render "input", type: "text" -%}
|
||||||
|
{%- render "no_leak" -%}
|
||||||
|
LIQUID
|
||||||
|
expected = <<~OUTPUT
|
||||||
|
|
||||||
|
|
||||||
|
<input type="text" />
|
||||||
|
|
||||||
|
<input type="" />
|
||||||
|
OUTPUT
|
||||||
|
|
||||||
|
assert_template_result(expected, template)
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user