Skip to content

Commit

Permalink
Add concept of Liquid::World
Browse files Browse the repository at this point in the history
  • Loading branch information
ianks committed Jul 17, 2024
1 parent ed42120 commit 0f691d0
Show file tree
Hide file tree
Showing 38 changed files with 459 additions and 222 deletions.
48 changes: 45 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 Worlds

In Liquid, a "World" 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 Worlds, 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 Worlds 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 Worlds in Liquid:

```ruby
user_world = Liquid::World.build do |world|
world.register_tag("renderobj", RenderObjTag)
end

Liquid::Template.parse(<<~LIQUID, world: user_world)
{% renderobj src: "path/to/model.obj" %}
LIQUID
```

In this example, `RenderObjTag` is a custom tag that is only available within the `user_world`.

Similarly, you can define another world for a different context, such as email templates:

```ruby
email_world = Liquid::World.build do |world|
world.register_tag("unsubscribe_footer", UnsubscribeFooter)
end

Liquid::Template.parse(<<~LIQUID, world: email_world)
{% unsubscribe_footer %}
LIQUID
```

By using Worlds, 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.
Expand All @@ -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::World.default.error_mode = :strict
Liquid::World.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::World.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::World.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`:
Expand Down
21 changes: 11 additions & 10 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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/world"
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
Expand All @@ -64,20 +75,14 @@ module Liquid
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'
Expand All @@ -86,7 +91,3 @@ module Liquid
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 }
8 changes: 2 additions & 6 deletions lib/liquid/block_body.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def freeze
next parse_liquid_tag(markup, parse_context)
end

unless (tag = registered_tags[tag_name])
unless (tag = parse_context.world.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
Expand Down Expand Up @@ -147,7 +147,7 @@ def self.rescue_render_node(context, output, line_number, exc, blank_tag)
next
end

unless (tag = registered_tags[tag_name])
unless (tag = parse_context.world.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
Expand Down Expand Up @@ -262,9 +262,5 @@ def raise_missing_tag_terminator(token, parse_context)
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
8 changes: 8 additions & 0 deletions lib/liquid/const.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
15 changes: 8 additions & 7 deletions lib/liquid/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, :world

# rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
def self.build(world: World.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 = {}, world = World.default)
@world = world
@environments = [environments]
@environments.flatten!

Expand All @@ -32,18 +33,18 @@ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_erro
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@resource_limits = resource_limits || ResourceLimits.new(world.default_resource_limits)
@base_scope_depth = 0
@interrupts = []
@filters = []
@global_filter = nil
@disabled_tags = {}

@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= Liquid::Template.file_system
@registers.static[:file_system] ||= world.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new

self.exception_renderer = Template.default_exception_renderer
self.exception_renderer = world.exception_renderer
if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
Expand All @@ -60,7 +61,7 @@ def warnings
end

def strainer
@strainer ||= StrainerFactory.create(self, @filters)
@strainer ||= @world.create_strainer(self, @filters)
end

# Adds filters to this context.
Expand Down
22 changes: 22 additions & 0 deletions lib/liquid/deprecations.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 4 additions & 3 deletions lib/liquid/parse_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, :world

def initialize(options = {})
def initialize(options = Const::EMPTY_HASH)
@world = options.fetch(:world, World.default)
@template_options = options ? options.dup : {}

@locale = @template_options[:locale] ||= I18n.new
Expand Down Expand Up @@ -35,7 +36,7 @@ def partial=(value)
@partial = value
@options = value ? partial_options : @template_options

@error_mode = @options[:error_mode] || Template.error_mode
@error_mode = @options[:error_mode] || @world.error_mode
end

def partial_options
Expand Down
2 changes: 0 additions & 2 deletions lib/liquid/standardfilters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1001,6 +1001,4 @@ def each
end
end
end

Template.register_filter(StandardFilters)
end
36 changes: 9 additions & 27 deletions lib/liquid/strainer_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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, world = World.default)
Deprecations.warn("StrainerFactory.add_global_filter", "World#register_filter")
world.register_filter(filter)
end

def create(context, filters = [])
strainer_from_cache(filters).new(context)
def create(context, filters = Const::EMPTY_ARRAY, world = World.default)
Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer")
world.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(world = World.default)
Deprecations.warn("StrainerFactory.global_filter_names", "World#filter_method_names")
World.strainer_template.filter_method_names
end
end
end
3 changes: 3 additions & 0 deletions lib/liquid/tag.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
47 changes: 47 additions & 0 deletions lib/liquid/tags.rb
Original file line number Diff line number Diff line change
@@ -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 changes: 0 additions & 2 deletions lib/liquid/tags/assign.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,4 @@ def children
end
end
end

Template.register_tag('assign', Assign)
end
2 changes: 0 additions & 2 deletions lib/liquid/tags/break.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,4 @@ def render_to_output_buffer(context, output)
output
end
end

Template.register_tag('break', Break)
end
2 changes: 0 additions & 2 deletions lib/liquid/tags/capture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,4 @@ def blank?
true
end
end

Template.register_tag('capture', Capture)
end
2 changes: 0 additions & 2 deletions lib/liquid/tags/case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,4 @@ def children
end
end
end

Template.register_tag('case', Case)
end
2 changes: 0 additions & 2 deletions lib/liquid/tags/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,4 @@ def parse_raw_tag_body(tokenizer)
raise_tag_never_closed("raw")
end
end

Template.register_tag('comment', Comment)
end
2 changes: 0 additions & 2 deletions lib/liquid/tags/continue.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,4 @@ def render_to_output_buffer(context, output)
output
end
end

Template.register_tag('continue', Continue)
end
Loading

0 comments on commit 0f691d0

Please sign in to comment.