diff --git a/README.md b/README.md index 76cfa3c0..e1df18ba 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/lib/liquid.rb b/lib/liquid.rb index eba84e14..6f071a8a 100644 --- a/lib/liquid.rb +++ b/lib/liquid.rb @@ -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 } diff --git a/lib/liquid/block_body.rb b/lib/liquid/block_body.rb index 61096de8..ec40e212 100644 --- a/lib/liquid/block_body.rb +++ b/lib/liquid/block_body.rb @@ -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 diff --git a/lib/liquid/const.rb b/lib/liquid/const.rb new file mode 100644 index 00000000..607f51a5 --- /dev/null +++ b/lib/liquid/const.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Liquid + module Const + EMPTY_HASH = {}.freeze + EMPTY_ARRAY = [].freeze + end +end diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index e712b4cc..01123564 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -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. diff --git a/lib/liquid/deprecations.rb b/lib/liquid/deprecations.rb new file mode 100644 index 00000000..e1ec628f --- /dev/null +++ b/lib/liquid/deprecations.rb @@ -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 diff --git a/lib/liquid/environment.rb b/lib/liquid/environment.rb new file mode 100644 index 00000000..fdd404fc --- /dev/null +++ b/lib/liquid/environment.rb @@ -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] 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] 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] 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 diff --git a/lib/liquid/parse_context.rb b/lib/liquid/parse_context.rb index 87570ad5..7bd5418d 100644 --- a/lib/liquid/parse_context.rb +++ b/lib/liquid/parse_context.rb @@ -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 diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index 3b0ecf15..6f1afaca 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -1001,6 +1001,4 @@ module Liquid end end end - - Template.register_filter(StandardFilters) end diff --git a/lib/liquid/strainer_factory.rb b/lib/liquid/strainer_factory.rb index bb51ef06..869b09ad 100644 --- a/lib/liquid/strainer_factory.rb +++ b/lib/liquid/strainer_factory.rb @@ -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 diff --git a/lib/liquid/tag.rb b/lib/liquid/tag.rb index 8303a723..9ca97d0f 100644 --- a/lib/liquid/tag.rb +++ b/lib/liquid/tag.rb @@ -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 diff --git a/lib/liquid/tags.rb b/lib/liquid/tags.rb new file mode 100644 index 00000000..916a63bd --- /dev/null +++ b/lib/liquid/tags.rb @@ -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 diff --git a/lib/liquid/tags/assign.rb b/lib/liquid/tags/assign.rb index 9eff9796..70283178 100644 --- a/lib/liquid/tags/assign.rb +++ b/lib/liquid/tags/assign.rb @@ -72,6 +72,4 @@ module Liquid end end end - - Template.register_tag('assign', Assign) end diff --git a/lib/liquid/tags/break.rb b/lib/liquid/tags/break.rb index c2293161..cec64007 100644 --- a/lib/liquid/tags/break.rb +++ b/lib/liquid/tags/break.rb @@ -26,6 +26,4 @@ module Liquid output end end - - Template.register_tag('break', Break) end diff --git a/lib/liquid/tags/capture.rb b/lib/liquid/tags/capture.rb index 1b9f2819..59aa62e7 100644 --- a/lib/liquid/tags/capture.rb +++ b/lib/liquid/tags/capture.rb @@ -39,6 +39,4 @@ module Liquid true end end - - Template.register_tag('capture', Capture) end diff --git a/lib/liquid/tags/case.rb b/lib/liquid/tags/case.rb index 4fe4cd98..6b67601f 100644 --- a/lib/liquid/tags/case.rb +++ b/lib/liquid/tags/case.rb @@ -123,6 +123,4 @@ module Liquid end end end - - Template.register_tag('case', Case) end diff --git a/lib/liquid/tags/comment.rb b/lib/liquid/tags/comment.rb index 798355d5..659108e3 100644 --- a/lib/liquid/tags/comment.rb +++ b/lib/liquid/tags/comment.rb @@ -85,6 +85,4 @@ module Liquid raise_tag_never_closed("raw") end end - - Template.register_tag('comment', Comment) end diff --git a/lib/liquid/tags/continue.rb b/lib/liquid/tags/continue.rb index 87550838..48e23518 100644 --- a/lib/liquid/tags/continue.rb +++ b/lib/liquid/tags/continue.rb @@ -17,6 +17,4 @@ module Liquid output end end - - Template.register_tag('continue', Continue) end diff --git a/lib/liquid/tags/cycle.rb b/lib/liquid/tags/cycle.rb index 2514bf3b..06d788de 100644 --- a/lib/liquid/tags/cycle.rb +++ b/lib/liquid/tags/cycle.rb @@ -78,6 +78,4 @@ module Liquid end end end - - Template.register_tag('cycle', Cycle) end diff --git a/lib/liquid/tags/decrement.rb b/lib/liquid/tags/decrement.rb index 8a99eb94..67716662 100644 --- a/lib/liquid/tags/decrement.rb +++ b/lib/liquid/tags/decrement.rb @@ -35,6 +35,4 @@ module Liquid output end end - - Template.register_tag('decrement', Decrement) end diff --git a/lib/liquid/tags/echo.rb b/lib/liquid/tags/echo.rb index 8abb6c2b..2c45c9cb 100644 --- a/lib/liquid/tags/echo.rb +++ b/lib/liquid/tags/echo.rb @@ -36,6 +36,4 @@ module Liquid end end end - - Template.register_tag('echo', Echo) end diff --git a/lib/liquid/tags/for.rb b/lib/liquid/tags/for.rb index 9205fa42..0dda3081 100644 --- a/lib/liquid/tags/for.rb +++ b/lib/liquid/tags/for.rb @@ -201,6 +201,4 @@ module Liquid end end end - - Template.register_tag('for', For) end diff --git a/lib/liquid/tags/if.rb b/lib/liquid/tags/if.rb index 92ed8aa3..bcf250a1 100644 --- a/lib/liquid/tags/if.rb +++ b/lib/liquid/tags/if.rb @@ -135,6 +135,4 @@ module Liquid end end end - - Template.register_tag('if', If) end diff --git a/lib/liquid/tags/ifchanged.rb b/lib/liquid/tags/ifchanged.rb index dd3be532..50397794 100644 --- a/lib/liquid/tags/ifchanged.rb +++ b/lib/liquid/tags/ifchanged.rb @@ -14,6 +14,4 @@ module Liquid output end end - - Template.register_tag('ifchanged', Ifchanged) end diff --git a/lib/liquid/tags/include.rb b/lib/liquid/tags/include.rb index 7b9685ee..b4f1be13 100644 --- a/lib/liquid/tags/include.rb +++ b/lib/liquid/tags/include.rb @@ -110,6 +110,4 @@ module Liquid end end end - - Template.register_tag('include', Include) end diff --git a/lib/liquid/tags/increment.rb b/lib/liquid/tags/increment.rb index 5f7e4f6f..c650e5ac 100644 --- a/lib/liquid/tags/increment.rb +++ b/lib/liquid/tags/increment.rb @@ -35,6 +35,4 @@ module Liquid output end end - - Template.register_tag('increment', Increment) end diff --git a/lib/liquid/tags/inline_comment.rb b/lib/liquid/tags/inline_comment.rb index 493cfddc..3aa7c50f 100644 --- a/lib/liquid/tags/inline_comment.rb +++ b/lib/liquid/tags/inline_comment.rb @@ -25,6 +25,4 @@ module Liquid true end end - - Template.register_tag('#', InlineComment) end diff --git a/lib/liquid/tags/raw.rb b/lib/liquid/tags/raw.rb index 02ee2b3c..2721c4a2 100644 --- a/lib/liquid/tags/raw.rb +++ b/lib/liquid/tags/raw.rb @@ -56,6 +56,4 @@ module Liquid end end end - - Template.register_tag('raw', Raw) end diff --git a/lib/liquid/tags/render.rb b/lib/liquid/tags/render.rb index b9ae58ea..3615b1b3 100644 --- a/lib/liquid/tags/render.rb +++ b/lib/liquid/tags/render.rb @@ -108,6 +108,4 @@ module Liquid end end end - - Template.register_tag('render', Render) end diff --git a/lib/liquid/tags/table_row.rb b/lib/liquid/tags/table_row.rb index be9a64c4..f7e8cd72 100644 --- a/lib/liquid/tags/table_row.rb +++ b/lib/liquid/tags/table_row.rb @@ -91,6 +91,4 @@ module Liquid raise Liquid::ArgumentError, "invalid integer" end end - - Template.register_tag('tablerow', TableRow) end diff --git a/lib/liquid/tags/unless.rb b/lib/liquid/tags/unless.rb index 2fc79db5..a6faa6f9 100644 --- a/lib/liquid/tags/unless.rb +++ b/lib/liquid/tags/unless.rb @@ -44,6 +44,4 @@ module Liquid output end end - - Template.register_tag('unless', Unless) end diff --git a/lib/liquid/template.rb b/lib/liquid/template.rb index f6369d1a..9fb4089a 100644 --- a/lib/liquid/template.rb +++ b/lib/liquid/template.rb @@ -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 Template object from liquid source code # To enable profiling, pass in profile: true 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 diff --git a/lib/liquid/template/tag_registry.rb b/lib/liquid/template/tag_registry.rb new file mode 100644 index 00000000..d055ad2e --- /dev/null +++ b/lib/liquid/template/tag_registry.rb @@ -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 diff --git a/test/integration/error_handling_test.rb b/test/integration/error_handling_test.rb index 69ce8297..c19e7524 100644 --- a/test/integration/error_handling_test.rb +++ b/test/integration/error_handling_test.rb @@ -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 diff --git a/test/integration/tags/include_tag_test.rb b/test/integration/tags/include_tag_test.rb index b86d40ee..ad801f69 100644 --- a/test/integration/tags/include_tag_test.rb +++ b/test/integration/tags/include_tag_test.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb index 34d85aeb..e5c88fb2 100755 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -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 diff --git a/test/unit/strainer_template_unit_test.rb b/test/unit/strainer_template_unit_test.rb index fd6a5a3c..18b9f499 100644 --- a/test/unit/strainer_template_unit_test.rb +++ b/test/unit/strainer_template_unit_test.rb @@ -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 diff --git a/test/unit/template_unit_test.rb b/test/unit/template_unit_test.rb index b7566947..f44cd153 100644 --- a/test/unit/template_unit_test.rb +++ b/test/unit/template_unit_test.rb @@ -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