mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3270183a7 | ||
|
|
3399981b89 | ||
|
|
404d71613c | ||
|
|
98a69c80ef | ||
|
|
30ea917a38 | ||
|
|
d7045f9d64 | ||
|
|
b397513f8b | ||
|
|
ffe48869be | ||
|
|
b233b3d081 | ||
|
|
ac91d31268 | ||
|
|
9067e5167a | ||
|
|
fb6634f454 | ||
|
|
a0411e0927 | ||
|
|
ed421202e2 | ||
|
|
d6ca569e8a | ||
|
|
d36937d17f |
@@ -7,6 +7,8 @@ end
|
||||
|
||||
gemspec
|
||||
|
||||
gem "base64"
|
||||
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
@@ -26,3 +28,7 @@ group :test do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
||||
end
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem "webrick"
|
||||
end
|
||||
|
||||
+5
-1
@@ -9,12 +9,13 @@ GIT
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
liquid (5.5.0)
|
||||
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)
|
||||
@@ -52,11 +53,13 @@ GEM
|
||||
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!
|
||||
@@ -68,6 +71,7 @@ DEPENDENCIES
|
||||
rubocop-shopify (~> 2.12.0)
|
||||
stackprof
|
||||
terminal-table
|
||||
webrick
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.7
|
||||
|
||||
@@ -52,6 +52,47 @@ For standard use you can just pass it the content of a file and call render with
|
||||
@template.render('name' => 'tobi') # => "hi tobi"
|
||||
```
|
||||
|
||||
### Concept of Environments
|
||||
|
||||
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
|
||||
|
||||
By using environments, you can:
|
||||
|
||||
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
|
||||
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
|
||||
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
|
||||
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
|
||||
|
||||
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
|
||||
|
||||
Here's an example of how you can define and use Environments in Liquid:
|
||||
|
||||
```ruby
|
||||
user_environment = Liquid::Environment.build do |environment|
|
||||
environment.register_tag("renderobj", RenderObjTag)
|
||||
end
|
||||
|
||||
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
|
||||
{% renderobj src: "path/to/model.obj" %}
|
||||
LIQUID
|
||||
```
|
||||
|
||||
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
|
||||
|
||||
Similarly, you can define another environment for a different context, such as email templates:
|
||||
|
||||
```ruby
|
||||
email_environment = Liquid::Environment.build do |environment|
|
||||
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
|
||||
end
|
||||
|
||||
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
|
||||
{% unsubscribe_footer %}
|
||||
LIQUID
|
||||
```
|
||||
|
||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
|
||||
|
||||
### Error Modes
|
||||
|
||||
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
|
||||
@@ -62,9 +103,10 @@ Liquid also comes with a stricter parser that can be used when editing templates
|
||||
when templates are invalid. You can enable this new parser like this:
|
||||
|
||||
```ruby
|
||||
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
|
||||
Liquid::Environment.default.error_mode = :strict
|
||||
Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
|
||||
Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
|
||||
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
|
||||
```
|
||||
|
||||
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
|
||||
|
||||
@@ -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>
|
||||
|
||||
+11
-10
@@ -50,7 +50,18 @@ module Liquid
|
||||
end
|
||||
|
||||
require "liquid/version"
|
||||
require "liquid/deprecations"
|
||||
require "liquid/const"
|
||||
require "liquid/template/tag_registry"
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/file_system'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block'
|
||||
require 'liquid/parse_tree_visitor'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/tags'
|
||||
require "liquid/environment"
|
||||
require 'liquid/lexer'
|
||||
require 'liquid/parser'
|
||||
require 'liquid/i18n'
|
||||
@@ -64,20 +75,14 @@ require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
require 'liquid/variable'
|
||||
require 'liquid/variable_lookup'
|
||||
require 'liquid/range_lookup'
|
||||
require 'liquid/file_system'
|
||||
require 'liquid/resource_limits'
|
||||
require 'liquid/template'
|
||||
require 'liquid/standardfilters'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
@@ -86,7 +91,3 @@ require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
||||
|
||||
@@ -52,7 +52,7 @@ module Liquid
|
||||
next parse_liquid_tag(markup, parse_context)
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -147,7 +147,7 @@ module Liquid
|
||||
next
|
||||
end
|
||||
|
||||
unless (tag = registered_tags[tag_name])
|
||||
unless (tag = parse_context.environment.tag_for_name(tag_name))
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -262,9 +262,5 @@ module Liquid
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
Template.tags
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Const
|
||||
EMPTY_HASH = {}.freeze
|
||||
EMPTY_ARRAY = [].freeze
|
||||
end
|
||||
end
|
||||
@@ -15,14 +15,15 @@ module Liquid
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
def self.build(environment: Environment.default, environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
|
||||
@environment = environment
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@@ -32,7 +33,7 @@ module Liquid
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@@ -40,10 +41,10 @@ module Liquid
|
||||
@disabled_tags = {}
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= Liquid::Template.file_system
|
||||
@registers.static[:file_system] ||= environment.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
self.exception_renderer = environment.exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
|
||||
end
|
||||
@@ -60,7 +61,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
@strainer ||= @environment.create_strainer(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "set"
|
||||
|
||||
module Liquid
|
||||
class Deprecations
|
||||
class << self
|
||||
attr_accessor :warned
|
||||
|
||||
Deprecations.warned = Set.new
|
||||
|
||||
def warn(name, alternative)
|
||||
return if warned.include?(name)
|
||||
|
||||
warned << name
|
||||
|
||||
caller_location = caller_locations(2, 1).first
|
||||
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,159 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# The Environment is the container for all configuration options of Liquid, such as
|
||||
# the registered tags, filters, and the default error mode.
|
||||
class Environment
|
||||
# The default error mode for all templates. This can be overridden on a
|
||||
# per-template basis.
|
||||
attr_accessor :error_mode
|
||||
|
||||
# The tags that are available to use in the template.
|
||||
attr_accessor :tags
|
||||
|
||||
# The strainer template which is used to store filters that are available to
|
||||
# use in templates.
|
||||
attr_accessor :strainer_template
|
||||
|
||||
# The exception renderer that is used to render exceptions that are raised
|
||||
# when rendering a template
|
||||
attr_accessor :exception_renderer
|
||||
|
||||
# The default file system that is used to load templates from.
|
||||
attr_accessor :file_system
|
||||
|
||||
# The default resource limits that are used to limit the resources that a
|
||||
# template can consume.
|
||||
attr_accessor :default_resource_limits
|
||||
|
||||
class << self
|
||||
# Creates a new environment instance.
|
||||
#
|
||||
# @param tags [Hash] The tags that are available to use in
|
||||
# the template.
|
||||
# @param file_system The default file system that is used
|
||||
# to load templates from.
|
||||
# @param error_mode [Symbol] The default error mode for all templates
|
||||
# (either :strict, :warn, or :lax).
|
||||
# @param exception_renderer [Proc] The exception renderer that is used to
|
||||
# render exceptions.
|
||||
# @yieldparam environment [Environment] The environment instance that is being built.
|
||||
# @return [Environment] The new environment instance.
|
||||
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
|
||||
ret = new
|
||||
ret.tags = Template::TagRegistry.new(tags) if tags
|
||||
ret.file_system = file_system if file_system
|
||||
ret.error_mode = error_mode if error_mode
|
||||
ret.exception_renderer = exception_renderer if exception_renderer
|
||||
yield ret if block_given?
|
||||
ret.freeze
|
||||
end
|
||||
|
||||
# Returns the default environment instance.
|
||||
#
|
||||
# @return [Environment] The default environment instance.
|
||||
def default
|
||||
@default ||= new
|
||||
end
|
||||
|
||||
# Sets the default environment instance for the duration of the block
|
||||
#
|
||||
# @param environment [Environment] The environment instance to use as the default for the
|
||||
# duration of the block.
|
||||
# @yield
|
||||
# @return [Object] The return value of the block.
|
||||
def dangerously_override(environment)
|
||||
original_default = @default
|
||||
@default = environment
|
||||
yield
|
||||
ensure
|
||||
@default = original_default
|
||||
end
|
||||
end
|
||||
|
||||
# Initializes a new environment instance.
|
||||
# @api private
|
||||
def initialize
|
||||
@tags = Template::TagRegistry.new(Tags::STANDARD_TAGS)
|
||||
@error_mode = :lax
|
||||
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
|
||||
klass.add_filter(StandardFilters)
|
||||
end
|
||||
@exception_renderer = ->(exception) { exception }
|
||||
@file_system = BlankFileSystem.new
|
||||
@default_resource_limits = Const::EMPTY_HASH
|
||||
@strainer_template_class_cache = {}
|
||||
end
|
||||
|
||||
# Registers a new tag with the environment.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @param klass [Liquid::Tag] The class that implements the tag.
|
||||
# @return [void]
|
||||
def register_tag(name, klass)
|
||||
@tags[name] = klass
|
||||
end
|
||||
|
||||
# Registers a new filter with the environment.
|
||||
#
|
||||
# @param filter [Module] The module that contains the filter methods.
|
||||
# @return [void]
|
||||
def register_filter(filter)
|
||||
@strainer_template_class_cache.clear
|
||||
@strainer_template.add_filter(filter)
|
||||
end
|
||||
|
||||
# Registers multiple filters with this environment.
|
||||
#
|
||||
# @param filters [Array<Module>] The modules that contain the filter methods.
|
||||
# @return [self]
|
||||
def register_filters(filters)
|
||||
@strainer_template_class_cache.clear
|
||||
filters.each { |f| @strainer_template.add_filter(f) }
|
||||
self
|
||||
end
|
||||
|
||||
# Creates a new strainer instance with the given filters, caching the result
|
||||
# for faster lookup.
|
||||
#
|
||||
# @param context [Liquid::Context] The context that the strainer will be
|
||||
# used in.
|
||||
# @param filters [Array<Module>] The filters that the strainer will have
|
||||
# access to.
|
||||
# @return [Liquid::Strainer] The new strainer instance.
|
||||
def create_strainer(context, filters = Const::EMPTY_ARRAY)
|
||||
return @strainer_template.new(context) if filters.empty?
|
||||
|
||||
strainer_template = @strainer_template_class_cache[filters] ||= begin
|
||||
klass = Class.new(@strainer_template)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
|
||||
strainer_template.new(context)
|
||||
end
|
||||
|
||||
# Returns the names of all the filter methods that are available to use in
|
||||
# the strainer template.
|
||||
#
|
||||
# @return [Array<String>] The names of all the filter methods.
|
||||
def filter_method_names
|
||||
@strainer_template.filter_method_names
|
||||
end
|
||||
|
||||
# Returns the tag class for the given tag name.
|
||||
#
|
||||
# @param name [String] The name of the tag.
|
||||
# @return [Liquid::Tag] The tag class.
|
||||
def tag_for_name(name)
|
||||
@tags[name]
|
||||
end
|
||||
|
||||
def freeze
|
||||
@tags.freeze
|
||||
# TODO: freeze the tags, currently this is not possible because of liquid-c
|
||||
# @strainer_template.freeze
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,6 +4,7 @@
|
||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{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) "
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
attr_reader :partial, :warnings, :error_mode
|
||||
attr_reader :partial, :warnings, :error_mode, :environment
|
||||
|
||||
def initialize(options = {})
|
||||
def initialize(options = Const::EMPTY_HASH)
|
||||
@environment = options.fetch(:environment, Environment.default)
|
||||
@template_options = options ? options.dup : {}
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@@ -35,7 +36,7 @@ module Liquid
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
@error_mode = @options[:error_mode] || @environment.error_mode
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
||||
@@ -1001,6 +1001,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_filter(StandardFilters)
|
||||
end
|
||||
|
||||
@@ -5,37 +5,19 @@ module Liquid
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter)
|
||||
strainer_class_cache.clear
|
||||
GlobalCache.add_filter(filter)
|
||||
def add_global_filter(filter, environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.add_global_filter", "Environment#register_filter")
|
||||
environment.register_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = [])
|
||||
strainer_from_cache(filters).new(context)
|
||||
def create(context, filters = Const::EMPTY_ARRAY, environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer")
|
||||
environment.create_strainer(context, filters)
|
||||
end
|
||||
|
||||
def global_filter_names
|
||||
GlobalCache.filter_method_names
|
||||
end
|
||||
|
||||
GlobalCache = Class.new(StrainerTemplate)
|
||||
|
||||
private
|
||||
|
||||
def strainer_from_cache(filters)
|
||||
if filters.empty?
|
||||
GlobalCache
|
||||
else
|
||||
strainer_class_cache[filters] ||= begin
|
||||
klass = Class.new(GlobalCache)
|
||||
filters.each { |f| klass.add_filter(f) }
|
||||
klass
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def strainer_class_cache
|
||||
@strainer_class_cache ||= {}
|
||||
def global_filter_names(environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.global_filter_names", "Environment#filter_method_names")
|
||||
Environment.strainer_template.filter_method_names
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "tags/table_row"
|
||||
require_relative "tags/echo"
|
||||
require_relative "tags/if"
|
||||
require_relative "tags/break"
|
||||
require_relative "tags/inline_comment"
|
||||
require_relative "tags/for"
|
||||
require_relative "tags/assign"
|
||||
require_relative "tags/ifchanged"
|
||||
require_relative "tags/case"
|
||||
require_relative "tags/include"
|
||||
require_relative "tags/continue"
|
||||
require_relative "tags/capture"
|
||||
require_relative "tags/decrement"
|
||||
require_relative "tags/unless"
|
||||
require_relative "tags/increment"
|
||||
require_relative "tags/comment"
|
||||
require_relative "tags/raw"
|
||||
require_relative "tags/render"
|
||||
require_relative "tags/cycle"
|
||||
require_relative "tags/snippet"
|
||||
|
||||
module Liquid
|
||||
module Tags
|
||||
STANDARD_TAGS = {
|
||||
'cycle' => Cycle,
|
||||
'render' => Render,
|
||||
'raw' => Raw,
|
||||
'comment' => Comment,
|
||||
'increment' => Increment,
|
||||
'unless' => Unless,
|
||||
'decrement' => Decrement,
|
||||
'capture' => Capture,
|
||||
'continue' => Continue,
|
||||
'include' => Include,
|
||||
'case' => Case,
|
||||
'ifchanged' => Ifchanged,
|
||||
'assign' => Assign,
|
||||
'for' => For,
|
||||
'#' => InlineComment,
|
||||
'break' => Break,
|
||||
'if' => If,
|
||||
'echo' => Echo,
|
||||
'tablerow' => TableRow,
|
||||
'snippet' => Snippet,
|
||||
}.freeze
|
||||
end
|
||||
end
|
||||
@@ -72,6 +72,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('assign', Assign)
|
||||
end
|
||||
|
||||
@@ -26,6 +26,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
|
||||
@@ -39,6 +39,4 @@ module Liquid
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('capture', Capture)
|
||||
end
|
||||
|
||||
@@ -123,6 +123,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('case', Case)
|
||||
end
|
||||
|
||||
@@ -85,6 +85,4 @@ module Liquid
|
||||
raise_tag_never_closed("raw")
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment', Comment)
|
||||
end
|
||||
|
||||
@@ -17,6 +17,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
|
||||
@@ -26,14 +26,20 @@ module Liquid
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = parse_expression(Regexp.last_match(1))
|
||||
@is_named = true
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@is_named = !@name.match?(/\w+:0x\h{8}/)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def named?
|
||||
@is_named
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
@@ -72,6 +78,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('cycle', Cycle)
|
||||
end
|
||||
|
||||
@@ -35,6 +35,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('decrement', Decrement)
|
||||
end
|
||||
|
||||
@@ -36,6 +36,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo', Echo)
|
||||
end
|
||||
|
||||
@@ -201,6 +201,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for', For)
|
||||
end
|
||||
|
||||
@@ -135,6 +135,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if', If)
|
||||
end
|
||||
|
||||
@@ -14,6 +14,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('ifchanged', Ifchanged)
|
||||
end
|
||||
|
||||
@@ -110,6 +110,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
|
||||
@@ -35,6 +35,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('increment', Increment)
|
||||
end
|
||||
|
||||
@@ -25,6 +25,4 @@ module Liquid
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('#', InlineComment)
|
||||
end
|
||||
|
||||
@@ -56,6 +56,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('raw', Raw)
|
||||
end
|
||||
|
||||
@@ -66,6 +66,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,
|
||||
@@ -108,6 +126,4 @@ module Liquid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render', Render)
|
||||
end
|
||||
|
||||
@@ -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
|
||||
@@ -65,6 +65,12 @@ module Liquid
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
break if interrupt.is_a?(BreakInterrupt)
|
||||
end
|
||||
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
@@ -91,6 +97,4 @@ module Liquid
|
||||
raise Liquid::ArgumentError, "invalid integer"
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
end
|
||||
|
||||
@@ -44,6 +44,4 @@ module Liquid
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('unless', Unless)
|
||||
end
|
||||
|
||||
+55
-60
@@ -18,42 +18,6 @@ module Liquid
|
||||
attr_accessor :root, :name
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
def [](tag_name)
|
||||
return nil unless @tags.key?(tag_name)
|
||||
return @cache[tag_name] if Liquid.cache_classes
|
||||
|
||||
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
||||
end
|
||||
|
||||
def []=(tag_name, klass)
|
||||
@tags[tag_name] = klass.name
|
||||
@cache[tag_name] = klass
|
||||
end
|
||||
|
||||
def delete(tag_name)
|
||||
@tags.delete(tag_name)
|
||||
@cache.delete(tag_name)
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@tags.each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :profiler
|
||||
|
||||
class << self
|
||||
@@ -61,46 +25,71 @@ module Liquid
|
||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_accessor :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
def error_mode=(mode)
|
||||
Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
|
||||
Environment.default.error_mode = mode
|
||||
end
|
||||
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
def error_mode
|
||||
Environment.default.error_mode
|
||||
end
|
||||
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
def default_exception_renderer=(renderer)
|
||||
Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
|
||||
Environment.default.exception_renderer = renderer
|
||||
end
|
||||
|
||||
def default_exception_renderer
|
||||
Environment.default.exception_renderer
|
||||
end
|
||||
|
||||
def file_system=(file_system)
|
||||
Deprecations.warn("Template.file_system=", "Environment#file_system=")
|
||||
Environment.default.file_system = file_system
|
||||
end
|
||||
|
||||
def file_system
|
||||
Environment.default.file_system
|
||||
end
|
||||
|
||||
def tags
|
||||
Environment.default.tags
|
||||
end
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
Deprecations.warn("Template.register_tag", "Environment#register_tag")
|
||||
Environment.default.register_tag(name, klass)
|
||||
end
|
||||
|
||||
# Pass a module with filter methods which should be available
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
Deprecations.warn("Template.register_filter", "Environment#register_filter")
|
||||
Environment.default.register_filter(mod)
|
||||
end
|
||||
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
private def default_resource_limits=(limits)
|
||||
Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
|
||||
Environment.default.default_resource_limits = limits
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
Environment.default.default_resource_limits
|
||||
end
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
new.parse(source, options)
|
||||
environment = options[:environment] || Environment.default
|
||||
new(environment: environment).parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
def initialize(environment: Environment.default)
|
||||
@environment = environment
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
@resource_limits = ResourceLimits.new(environment.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
@@ -162,11 +151,11 @@ module Liquid
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
when nil
|
||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
|
||||
else
|
||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||
end
|
||||
@@ -226,8 +215,14 @@ module Liquid
|
||||
@options = options
|
||||
@profiling = profiling
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@warnings = parse_context.warnings
|
||||
parse_context = if options.is_a?(ParseContext)
|
||||
options
|
||||
else
|
||||
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
|
||||
ParseContext.new(opts)
|
||||
end
|
||||
|
||||
@warnings = parse_context.warnings
|
||||
parse_context
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Template
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize(tags = nil)
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
tags.each { |tag_name, klass| self[tag_name] = klass }
|
||||
Deprecations.warn("Template::TagRegistry", "Use a Environment instance with zeitwerk")
|
||||
end
|
||||
|
||||
def [](tag_name)
|
||||
return nil unless @tags.key?(tag_name)
|
||||
return @cache[tag_name] if Liquid.cache_classes
|
||||
|
||||
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
||||
end
|
||||
|
||||
def delete(tag_name)
|
||||
Deprecations.warn("Template::TagRegistry#delete", "Use a Environment instance with immutable tags")
|
||||
@tags.delete(tag_name)
|
||||
@cache.delete(tag_name)
|
||||
end
|
||||
|
||||
def []=(tag_name, klass)
|
||||
@tags[tag_name] = klass.name
|
||||
@cache[tag_name] = klass
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@tags.each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.5.0"
|
||||
VERSION = "5.6.0.alpha"
|
||||
end
|
||||
|
||||
@@ -219,6 +219,21 @@ class ErrorHandlingTest < Minitest::Test
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
def test_setting_exception_renderer_on_environment
|
||||
exceptions = []
|
||||
exception_renderer = ->(e) do
|
||||
exceptions << e
|
||||
''
|
||||
end
|
||||
|
||||
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
end
|
||||
|
||||
def test_exception_renderer_exposing_non_liquid_error
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
|
||||
exceptions = []
|
||||
@@ -242,16 +257,10 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_included_template_name_with_line_numbers
|
||||
old_file_system = Liquid::Template.file_system
|
||||
environment = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
|
||||
page = template.render('errors' => ErrorDrop.new)
|
||||
ensure
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
end
|
||||
|
||||
@@ -49,14 +49,6 @@ end
|
||||
class IncludeTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@default_file_system = Liquid::Template.file_system
|
||||
end
|
||||
|
||||
def teardown
|
||||
Liquid::Template.file_system = @default_file_system
|
||||
end
|
||||
|
||||
def test_include_tag_looks_for_file_system_in_registers_first
|
||||
assert_equal(
|
||||
'from OtherFileSystem',
|
||||
@@ -214,9 +206,10 @@ class IncludeTagTest < Minitest::Test
|
||||
|
||||
def test_include_tag_caches_second_read_of_same_partial
|
||||
file_system = CountingFileSystem.new
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
assert_equal(
|
||||
'from CountingFileSystemfrom CountingFileSystem',
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
|
||||
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
|
||||
)
|
||||
assert_equal(1, file_system.count)
|
||||
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
|
||||
@@ -207,4 +207,52 @@ class TableRowTest < Minitest::Test
|
||||
render_errors: true,
|
||||
)
|
||||
end
|
||||
|
||||
def test_table_row_handles_interrupts
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td></tr>\n",
|
||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% break %} {{n}} {% endtablerow %}',
|
||||
)
|
||||
|
||||
assert_template_result(
|
||||
"<tr class=\"row1\">\n<td class=\"col1\"> 1 </td><td class=\"col2\"> 2 </td></tr>\n<tr class=\"row2\"><td class=\"col1\"> 3 </td></tr>\n",
|
||||
'{% tablerow n in (1...3) cols:2 %} {{n}} {% continue %} {{n}} {% endtablerow %}',
|
||||
)
|
||||
end
|
||||
|
||||
def test_table_row_does_not_leak_interrupts
|
||||
template = <<~LIQUID
|
||||
{% for i in (1..2) -%}
|
||||
{% for j in (1..2) -%}
|
||||
{% tablerow k in (1..3) %}{% break %}{% endtablerow -%}
|
||||
loop j={{ j }}
|
||||
{% endfor -%}
|
||||
loop i={{ i }}
|
||||
{% endfor -%}
|
||||
after loop
|
||||
LIQUID
|
||||
|
||||
expected = <<~STR
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=2
|
||||
loop i=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=1
|
||||
<tr class="row1">
|
||||
<td class="col1"></td></tr>
|
||||
loop j=2
|
||||
loop i=2
|
||||
after loop
|
||||
STR
|
||||
|
||||
assert_template_result(
|
||||
expected,
|
||||
template,
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
+13
-29
@@ -42,10 +42,11 @@ module Minitest
|
||||
message: nil, partials: nil, error_mode: nil, render_errors: false,
|
||||
template_factory: nil
|
||||
)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
|
||||
file_system = StubFileSystem.new(partials || {})
|
||||
environment = Liquid::Environment.build(file_system: file_system)
|
||||
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym, environment: environment)
|
||||
registers = Liquid::Registers.new(file_system: file_system, template_factory: template_factory)
|
||||
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
|
||||
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers, environment: environment)
|
||||
output = template.render(context)
|
||||
assert_equal(expected, output, message)
|
||||
end
|
||||
@@ -78,22 +79,12 @@ module Minitest
|
||||
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
|
||||
end
|
||||
|
||||
def with_global_filter(*globals)
|
||||
original_global_cache = Liquid::StrainerFactory::GlobalCache
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
|
||||
def with_global_filter(*globals, &blk)
|
||||
environment = Liquid::Environment.build do |w|
|
||||
w.register_filters(globals)
|
||||
end
|
||||
|
||||
globals.each do |global|
|
||||
Liquid::Template.register_filter(global)
|
||||
end
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
|
||||
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
|
||||
Liquid::StrainerFactory.send(:strainer_class_cache).clear
|
||||
end
|
||||
Environment.dangerously_override(environment, &blk)
|
||||
end
|
||||
|
||||
def with_error_mode(mode)
|
||||
@@ -104,18 +95,11 @@ module Minitest
|
||||
Liquid::Template.error_mode = old_mode
|
||||
end
|
||||
|
||||
def with_custom_tag(tag_name, tag_class)
|
||||
old_tag = Liquid::Template.tags[tag_name]
|
||||
begin
|
||||
Liquid::Template.register_tag(tag_name, tag_class)
|
||||
yield
|
||||
ensure
|
||||
if old_tag
|
||||
Liquid::Template.tags[tag_name] = old_tag
|
||||
else
|
||||
Liquid::Template.tags.delete(tag_name)
|
||||
end
|
||||
end
|
||||
def with_custom_tag(tag_name, tag_class, &block)
|
||||
environment = Liquid::Environment.default.dup
|
||||
environment.register_tag(tag_name, tag_class)
|
||||
|
||||
Environment.dangerously_override(environment, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,8 +58,9 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
|
||||
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
|
||||
with_global_filter do
|
||||
strainer = Context.new.strainer
|
||||
strainer.class.add_filter(PublicMethodOverrideFilter)
|
||||
context = Context.new
|
||||
context.add_filters([PublicMethodOverrideFilter])
|
||||
strainer = context.strainer
|
||||
assert(strainer.class.send(:filter_methods).include?('public_filter'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,7 +36,6 @@ class TemplateUnitTest < Minitest::Test
|
||||
assert(Template.tags['custom'].equal?(original_klass))
|
||||
ensure
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
Template.tags.delete('custom')
|
||||
Liquid.cache_classes = original_cache_setting
|
||||
end
|
||||
|
||||
@@ -46,36 +45,26 @@ class TemplateUnitTest < Minitest::Test
|
||||
|
||||
original_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, original_klass)
|
||||
Template.register_tag('custom', CustomTag)
|
||||
with_custom_tag('custom', CustomTag) do
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
new_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, new_klass)
|
||||
|
||||
new_klass = Class.new
|
||||
Object.send(:const_set, :CustomTag, new_klass)
|
||||
|
||||
assert(Template.tags['custom'].equal?(new_klass))
|
||||
assert(Template.tags['custom'].equal?(new_klass))
|
||||
end
|
||||
ensure
|
||||
Object.send(:remove_const, :CustomTag)
|
||||
Template.tags.delete('custom')
|
||||
Liquid.cache_classes = original_cache_setting
|
||||
end
|
||||
|
||||
class FakeTag; end
|
||||
|
||||
def test_tags_delete
|
||||
Template.register_tag('fake', FakeTag)
|
||||
assert_equal(FakeTag, Template.tags['fake'])
|
||||
|
||||
Template.tags.delete('fake')
|
||||
assert_nil(Template.tags['fake'])
|
||||
end
|
||||
|
||||
def test_tags_can_be_looped_over
|
||||
Template.register_tag('fake', FakeTag)
|
||||
result = Template.tags.map { |name, klass| [name, klass] }
|
||||
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
|
||||
ensure
|
||||
Template.tags.delete('fake')
|
||||
with_custom_tag('fake', FakeTag) do
|
||||
result = Template.tags.map { |name, klass| [name, klass] }
|
||||
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
|
||||
end
|
||||
end
|
||||
|
||||
class TemplateSubclass < Liquid::Template
|
||||
|
||||
Reference in New Issue
Block a user