Add concept of Liquid::Environment (#1815)

* Add concept of `Liquid::World`

* Rename `World` to `Environment`
This commit is contained in:
Ian Ker-Seymer
2024-08-07 15:00:44 -04:00
committed by GitHub
parent a0411e0927
commit fb6634f454
38 changed files with 463 additions and 226 deletions
+45 -3
View File
@@ -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" @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 ### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. 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: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Environment.default.error_mode = :strict
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. 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`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
+11 -10
View File
@@ -50,7 +50,18 @@ module Liquid
end end
require "liquid/version" 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/parse_tree_visitor'
require 'liquid/interrupts'
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
@@ -64,20 +75,14 @@ require 'liquid/strainer_template'
require 'liquid/strainer_factory' require 'liquid/strainer_factory'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits' require 'liquid/resource_limits'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
@@ -86,7 +91,3 @@ require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/registers' require 'liquid/registers'
require 'liquid/template_factory' require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+2 -6
View File
@@ -52,7 +52,7 @@ module Liquid
next parse_liquid_tag(markup, parse_context) next parse_liquid_tag(markup, parse_context)
end 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 # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -147,7 +147,7 @@ module Liquid
next next
end 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 # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -262,9 +262,5 @@ module Liquid
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags
Template.tags
end
end end
end end
+8
View File
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
+8 -7
View File
@@ -15,14 +15,15 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments 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 # 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) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
end 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 = [environments]
@environments.flatten! @environments.flatten!
@@ -32,7 +33,7 @@ module Liquid
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = 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 @base_scope_depth = 0
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@@ -40,10 +41,10 @@ module Liquid
@disabled_tags = {} @disabled_tags = {}
@registers.static[:cached_partials] ||= {} @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 @registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = environment.exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
@@ -60,7 +61,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= StrainerFactory.create(self, @filters) @strainer ||= @environment.create_strainer(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
+22
View File
@@ -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
+159
View File
@@ -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 -3
View File
@@ -3,9 +3,10 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth 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 : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@@ -35,7 +36,7 @@ module Liquid
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || @environment.error_mode
end end
def partial_options def partial_options
-2
View File
@@ -1001,6 +1001,4 @@ module Liquid
end end
end end
end end
Template.register_filter(StandardFilters)
end end
+9 -27
View File
@@ -5,37 +5,19 @@ module Liquid
module StrainerFactory module StrainerFactory
extend self extend self
def add_global_filter(filter) def add_global_filter(filter, environment = Environment.default)
strainer_class_cache.clear Deprecations.warn("StrainerFactory.add_global_filter", "Environment#register_filter")
GlobalCache.add_filter(filter) environment.register_filter(filter)
end end
def create(context, filters = []) def create(context, filters = Const::EMPTY_ARRAY, environment = Environment.default)
strainer_from_cache(filters).new(context) Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer")
environment.create_strainer(context, filters)
end end
def global_filter_names def global_filter_names(environment = Environment.default)
GlobalCache.filter_method_names Deprecations.warn("StrainerFactory.global_filter_names", "Environment#filter_method_names")
end Environment.strainer_template.filter_method_names
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 ||= {}
end end
end end
end end
+3
View File
@@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
+47
View File
@@ -0,0 +1,47 @@
# 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"
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,
}.freeze
end
end
-2
View File
@@ -72,6 +72,4 @@ module Liquid
end end
end end
end end
Template.register_tag('assign', Assign)
end end
-2
View File
@@ -26,6 +26,4 @@ module Liquid
output output
end end
end end
Template.register_tag('break', Break)
end end
-2
View File
@@ -39,6 +39,4 @@ module Liquid
true true
end end
end end
Template.register_tag('capture', Capture)
end end
-2
View File
@@ -123,6 +123,4 @@ module Liquid
end end
end end
end end
Template.register_tag('case', Case)
end end
-2
View File
@@ -85,6 +85,4 @@ module Liquid
raise_tag_never_closed("raw") raise_tag_never_closed("raw")
end end
end end
Template.register_tag('comment', Comment)
end end
-2
View File
@@ -17,6 +17,4 @@ module Liquid
output output
end end
end end
Template.register_tag('continue', Continue)
end end
-2
View File
@@ -78,6 +78,4 @@ module Liquid
end end
end end
end end
Template.register_tag('cycle', Cycle)
end end
-2
View File
@@ -35,6 +35,4 @@ module Liquid
output output
end end
end end
Template.register_tag('decrement', Decrement)
end end
-2
View File
@@ -36,6 +36,4 @@ module Liquid
end end
end end
end end
Template.register_tag('echo', Echo)
end end
-2
View File
@@ -201,6 +201,4 @@ module Liquid
end end
end end
end end
Template.register_tag('for', For)
end end
-2
View File
@@ -135,6 +135,4 @@ module Liquid
end end
end end
end end
Template.register_tag('if', If)
end end
-2
View File
@@ -14,6 +14,4 @@ module Liquid
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged)
end end
-2
View File
@@ -110,6 +110,4 @@ module Liquid
end end
end end
end end
Template.register_tag('include', Include)
end end
-2
View File
@@ -35,6 +35,4 @@ module Liquid
output output
end end
end end
Template.register_tag('increment', Increment)
end end
-2
View File
@@ -25,6 +25,4 @@ module Liquid
true true
end end
end end
Template.register_tag('#', InlineComment)
end end
-2
View File
@@ -56,6 +56,4 @@ module Liquid
end end
end end
end end
Template.register_tag('raw', Raw)
end end
-2
View File
@@ -108,6 +108,4 @@ module Liquid
end end
end end
end end
Template.register_tag('render', Render)
end end
-2
View File
@@ -91,6 +91,4 @@ module Liquid
raise Liquid::ArgumentError, "invalid integer" raise Liquid::ArgumentError, "invalid integer"
end end
end end
Template.register_tag('tablerow', TableRow)
end end
-2
View File
@@ -44,6 +44,4 @@ module Liquid
output output
end end
end end
Template.register_tag('unless', Unless)
end end
+55 -60
View File
@@ -18,42 +18,6 @@ module Liquid
attr_accessor :root, :name attr_accessor :root, :name
attr_reader :resource_limits, :warnings 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 attr_reader :profiler
class << self class << self
@@ -61,46 +25,71 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :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. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_accessor :error_mode def error_mode=(mode)
Template.error_mode = :lax Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end end
attr_accessor :file_system def error_mode
Template.file_system = BlankFileSystem.new Environment.default.error_mode
end
attr_accessor :tags def default_exception_renderer=(renderer)
Template.tags = TagRegistry.new Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
private :tags= 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) 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 end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
StrainerFactory.add_global_filter(mod) Deprecations.warn("Template.register_filter", "Environment#register_filter")
Environment.default.register_filter(mod)
end end
attr_accessor :default_resource_limits private def default_resource_limits=(limits)
Template.default_resource_limits = {} Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
private :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 # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
new.parse(source, options) environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
end end
end end
def initialize def initialize(environment: Environment.default)
@environment = environment
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits) @resource_limits = ResourceLimits.new(environment.default_resource_limits)
end end
# Parse source code. # Parse source code.
@@ -162,11 +151,11 @@ module Liquid
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift 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 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 when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
else else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
@@ -226,8 +215,14 @@ module Liquid
@options = options @options = options
@profiling = profiling @profiling = profiling
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) parse_context = if options.is_a?(ParseContext)
@warnings = parse_context.warnings options
else
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
ParseContext.new(opts)
end
@warnings = parse_context.warnings
parse_context parse_context
end end
+44
View File
@@ -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
+18 -9
View File
@@ -219,6 +219,21 @@ class ErrorHandlingTest < Minitest::Test
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end 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 def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
@@ -242,16 +257,10 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_included_template_name_with_line_numbers 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("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", template.errors.first.template_name)
end end
+2 -9
View File
@@ -49,14 +49,6 @@ end
class IncludeTagTest < Minitest::Test class IncludeTagTest < Minitest::Test
include Liquid 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 def test_include_tag_looks_for_file_system_in_registers_first
assert_equal( assert_equal(
'from OtherFileSystem', 'from OtherFileSystem',
@@ -214,9 +206,10 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
environment = Liquid::Environment.build(file_system: file_system)
assert_equal( assert_equal(
'from CountingFileSystemfrom CountingFileSystem', '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) assert_equal(1, file_system.count)
end end
+13 -29
View File
@@ -42,10 +42,11 @@ module Minitest
message: nil, partials: nil, error_mode: nil, render_errors: false, message: nil, partials: nil, error_mode: nil, render_errors: false,
template_factory: nil template_factory: nil
) )
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials || {}) 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) 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) output = template.render(context)
assert_equal(expected, output, message) assert_equal(expected, output, message)
end end
@@ -78,22 +79,12 @@ module Minitest
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}") assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end end
def with_global_filter(*globals) def with_global_filter(*globals, &blk)
original_global_cache = Liquid::StrainerFactory::GlobalCache environment = Liquid::Environment.build do |w|
Liquid::StrainerFactory.send(:remove_const, :GlobalCache) w.register_filters(globals)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate)) end
globals.each do |global| Environment.dangerously_override(environment, &blk)
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
end end
def with_error_mode(mode) def with_error_mode(mode)
@@ -104,18 +95,11 @@ module Minitest
Liquid::Template.error_mode = old_mode Liquid::Template.error_mode = old_mode
end end
def with_custom_tag(tag_name, tag_class) def with_custom_tag(tag_name, tag_class, &block)
old_tag = Liquid::Template.tags[tag_name] environment = Liquid::Environment.default.dup
begin environment.register_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class)
yield Environment.dangerously_override(environment, &block)
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end end
end end
end end
+3 -2
View File
@@ -58,8 +58,9 @@ class StrainerTemplateUnitTest < Minitest::Test
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
with_global_filter do with_global_filter do
strainer = Context.new.strainer context = Context.new
strainer.class.add_filter(PublicMethodOverrideFilter) context.add_filters([PublicMethodOverrideFilter])
strainer = context.strainer
assert(strainer.class.send(:filter_methods).include?('public_filter')) assert(strainer.class.send(:filter_methods).include?('public_filter'))
end end
end end
+10 -21
View File
@@ -36,7 +36,6 @@ class TemplateUnitTest < Minitest::Test
assert(Template.tags['custom'].equal?(original_klass)) assert(Template.tags['custom'].equal?(original_klass))
ensure ensure
Object.send(:remove_const, :CustomTag) Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting Liquid.cache_classes = original_cache_setting
end end
@@ -46,36 +45,26 @@ class TemplateUnitTest < Minitest::Test
original_klass = Class.new original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass) 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 assert(Template.tags['custom'].equal?(new_klass))
Object.send(:const_set, :CustomTag, new_klass) end
assert(Template.tags['custom'].equal?(new_klass))
ensure ensure
Object.send(:remove_const, :CustomTag) Object.send(:remove_const, :CustomTag)
Template.tags.delete('custom')
Liquid.cache_classes = original_cache_setting Liquid.cache_classes = original_cache_setting
end end
class FakeTag; 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 def test_tags_can_be_looped_over
Template.register_tag('fake', FakeTag) with_custom_tag('fake', FakeTag) do
result = Template.tags.map { |name, klass| [name, klass] } result = Template.tags.map { |name, klass| [name, klass] }
assert(result.include?(["fake", "TemplateUnitTest::FakeTag"])) assert(result.include?(["fake", "TemplateUnitTest::FakeTag"]))
ensure end
Template.tags.delete('fake')
end end
class TemplateSubclass < Liquid::Template class TemplateSubclass < Liquid::Template