liquid 4.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +101 -2
  3. data/README.md +8 -0
  4. data/lib/liquid.rb +18 -5
  5. data/lib/liquid/block.rb +47 -20
  6. data/lib/liquid/block_body.rb +192 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +110 -53
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  12. data/lib/liquid/expression.rb +30 -31
  13. data/lib/liquid/extensions.rb +8 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +35 -26
  19. data/lib/liquid/locales/en.yml +4 -2
  20. data/lib/liquid/parse_context.rb +21 -4
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +170 -63
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +32 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +41 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +35 -16
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +34 -0
  47. data/lib/liquid/tags/for.rb +79 -47
  48. data/lib/liquid/tags/if.rb +53 -30
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +42 -44
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +32 -20
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +53 -72
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +6 -4
  61. data/lib/liquid/variable.rb +55 -38
  62. data/lib/liquid/variable_lookup.rb +14 -6
  63. data/lib/liquid/version.rb +3 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +58 -0
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +609 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +73 -61
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +24 -8
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +41 -18
  79. data/test/integration/standard_filter_test.rb +513 -210
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +109 -53
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +83 -52
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +123 -120
  97. data/test/integration/trim_mode_test.rb +82 -44
  98. data/test/integration/variable_test.rb +46 -31
  99. data/test/test_helper.rb +75 -23
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +82 -72
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +12 -10
  105. data/test/unit/parse_tree_visitor_test.rb +254 -0
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +26 -19
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +79 -46
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -148
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # StrainerFactory is the factory for the filters system.
5
+ module StrainerFactory
6
+ extend self
7
+
8
+ def add_global_filter(filter)
9
+ strainer_class_cache.clear
10
+ global_filters << filter
11
+ end
12
+
13
+ def create(context, filters = [])
14
+ strainer_from_cache(filters).new(context)
15
+ end
16
+
17
+ private
18
+
19
+ def global_filters
20
+ @global_filters ||= []
21
+ end
22
+
23
+ def strainer_from_cache(filters)
24
+ strainer_class_cache[filters] ||= begin
25
+ klass = Class.new(StrainerTemplate)
26
+ global_filters.each { |f| klass.add_filter(f) }
27
+ filters.each { |f| klass.add_filter(f) }
28
+ klass
29
+ end
30
+ end
31
+
32
+ def strainer_class_cache
33
+ @strainer_class_cache ||= {}
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module Liquid
6
+ # StrainerTemplate is the computed class for the filters system.
7
+ # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
8
+ #
9
+ # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
10
+ # Context#add_filters or Template.register_filter
11
+ class StrainerTemplate
12
+ def initialize(context)
13
+ @context = context
14
+ end
15
+
16
+ class << self
17
+ def add_filter(filter)
18
+ return if include?(filter)
19
+
20
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
21
+ if invokable_non_public_methods.any?
22
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
23
+ end
24
+
25
+ include(filter)
26
+
27
+ filter_methods.merge(filter.public_instance_methods.map(&:to_s))
28
+ end
29
+
30
+ def invokable?(method)
31
+ filter_methods.include?(method.to_s)
32
+ end
33
+
34
+ private
35
+
36
+ def filter_methods
37
+ @filter_methods ||= Set.new
38
+ end
39
+ end
40
+
41
+ def invoke(method, *args)
42
+ if self.class.invokable?(method)
43
+ send(method, *args)
44
+ elsif @context.strict_filters
45
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
46
+ else
47
+ args.first
48
+ end
49
+ rescue ::ArgumentError => e
50
+ raise Liquid::ArgumentError, e.message, e.backtrace
51
+ end
52
+ end
53
+ end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class TablerowloopDrop < Drop
3
5
  def initialize(length, cols)
4
6
  @length = length
5
- @row = 1
6
- @col = 1
7
- @cols = cols
8
- @index = 0
7
+ @row = 1
8
+ @col = 1
9
+ @cols = cols
10
+ @index = 0
9
11
  end
10
12
 
11
13
  attr_reader :length, :col, :row
data/lib/liquid/tag.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Tag
3
5
  attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -5,20 +7,26 @@ module Liquid
5
7
  include ParserSwitching
6
8
 
7
9
  class << self
8
- def parse(tag_name, markup, tokenizer, options)
9
- tag = new(tag_name, markup, options)
10
+ def parse(tag_name, markup, tokenizer, parse_context)
11
+ tag = new(tag_name, markup, parse_context)
10
12
  tag.parse(tokenizer)
11
13
  tag
12
14
  end
13
15
 
16
+ def disable_tags(*tag_names)
17
+ @disabled_tags ||= []
18
+ @disabled_tags.concat(tag_names)
19
+ prepend(Disabler)
20
+ end
21
+
14
22
  private :new
15
23
  end
16
24
 
17
25
  def initialize(tag_name, markup, parse_context)
18
- @tag_name = tag_name
19
- @markup = markup
26
+ @tag_name = tag_name
27
+ @markup = markup
20
28
  @parse_context = parse_context
21
- @line_number = parse_context.line_number
29
+ @line_number = parse_context.line_number
22
30
  end
23
31
 
24
32
  def parse(_tokens)
@@ -33,11 +41,25 @@ module Liquid
33
41
  end
34
42
 
35
43
  def render(_context)
36
- ''.freeze
44
+ ''
45
+ end
46
+
47
+ # For backwards compatibility with custom tags. In a future release, the semantics
48
+ # of the `render_to_output_buffer` method will become the default and the `render`
49
+ # method will be removed.
50
+ def render_to_output_buffer(context, output)
51
+ output << render(context)
52
+ output
37
53
  end
38
54
 
39
55
  def blank?
40
56
  false
41
57
  end
58
+
59
+ private
60
+
61
+ def parse_expression(markup)
62
+ parse_context.parse_expression(markup)
63
+ end
42
64
  end
43
65
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ module Disableable
6
+ def render_to_output_buffer(context, output)
7
+ if context.tag_disabled?(tag_name)
8
+ output << disabled_error(context)
9
+ return
10
+ end
11
+ super
12
+ end
13
+
14
+ def disabled_error(context)
15
+ # raise then rescue the exception so that the Context#exception_renderer can re-raise it
16
+ raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
17
+ rescue DisabledError => exc
18
+ context.handle_error(exc, line_number)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Tag
5
+ module Disabler
6
+ module ClassMethods
7
+ attr_reader :disabled_tags
8
+ end
9
+
10
+ def self.prepended(base)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ def render_to_output_buffer(context, output)
15
+ context.with_disabled_tags(self.class.disabled_tags) do
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Assign sets a variable in your template.
3
5
  #
@@ -10,21 +12,28 @@ module Liquid
10
12
  class Assign < Tag
11
13
  Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
12
14
 
13
- def initialize(tag_name, markup, options)
15
+ # @api private
16
+ def self.raise_syntax_error(parse_context)
17
+ raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
18
+ end
19
+
20
+ attr_reader :to, :from
21
+
22
+ def initialize(tag_name, markup, parse_context)
14
23
  super
15
24
  if markup =~ Syntax
16
- @to = $1
17
- @from = Variable.new($2, options)
25
+ @to = Regexp.last_match(1)
26
+ @from = Variable.new(Regexp.last_match(2), parse_context)
18
27
  else
19
- raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
28
+ self.class.raise_syntax_error(parse_context)
20
29
  end
21
30
  end
22
31
 
23
- def render(context)
32
+ def render_to_output_buffer(context, output)
24
33
  val = @from.render(context)
25
34
  context.scopes.last[@to] = val
26
- context.resource_limits.assign_score += assign_score_of(val)
27
- ''.freeze
35
+ context.resource_limits.increment_assign_score(assign_score_of(val))
36
+ output
28
37
  end
29
38
 
30
39
  def blank?
@@ -35,17 +44,30 @@ module Liquid
35
44
 
36
45
  def assign_score_of(val)
37
46
  if val.instance_of?(String)
38
- val.length
39
- elsif val.instance_of?(Array) || val.instance_of?(Hash)
47
+ val.bytesize
48
+ elsif val.instance_of?(Array)
40
49
  sum = 1
41
50
  # Uses #each to avoid extra allocations.
42
51
  val.each { |child| sum += assign_score_of(child) }
43
52
  sum
53
+ elsif val.instance_of?(Hash)
54
+ sum = 1
55
+ val.each do |key, entry_value|
56
+ sum += assign_score_of(key)
57
+ sum += assign_score_of(entry_value)
58
+ end
59
+ sum
44
60
  else
45
61
  1
46
62
  end
47
63
  end
64
+
65
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
66
+ def children
67
+ [@node.from]
68
+ end
69
+ end
48
70
  end
49
71
 
50
- Template.register_tag('assign'.freeze, Assign)
72
+ Template.register_tag('assign', Assign)
51
73
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Break tag to be used to break out of a for loop.
3
5
  #
@@ -9,10 +11,13 @@ module Liquid
9
11
  # {% endfor %}
10
12
  #
11
13
  class Break < Tag
12
- def interrupt
13
- BreakInterrupt.new
14
+ INTERRUPT = BreakInterrupt.new.freeze
15
+
16
+ def render_to_output_buffer(context, output)
17
+ context.push_interrupt(INTERRUPT)
18
+ output
14
19
  end
15
20
  end
16
21
 
17
- Template.register_tag('break'.freeze, Break)
22
+ Template.register_tag('break', Break)
18
23
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Capture stores the result of a block into a variable without rendering it inplace.
3
5
  #
@@ -16,17 +18,18 @@ module Liquid
16
18
  def initialize(tag_name, markup, options)
17
19
  super
18
20
  if markup =~ Syntax
19
- @to = $1
21
+ @to = Regexp.last_match(1)
20
22
  else
21
- raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
23
+ raise SyntaxError, options[:locale].t("errors.syntax.capture")
22
24
  end
23
25
  end
24
26
 
25
- def render(context)
26
- output = super
27
- context.scopes.last[@to] = output
28
- context.resource_limits.assign_score += output.length
29
- ''.freeze
27
+ def render_to_output_buffer(context, output)
28
+ context.resource_limits.with_capture do
29
+ capture_output = render(context)
30
+ context.scopes.last[@to] = capture_output
31
+ end
32
+ output
30
33
  end
31
34
 
32
35
  def blank?
@@ -34,5 +37,5 @@ module Liquid
34
37
  end
35
38
  end
36
39
 
37
- Template.register_tag('capture'.freeze, Capture)
40
+ Template.register_tag('capture', Capture)
38
41
  end
@@ -1,24 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Case < Block
3
5
  Syntax = /(#{QuotedFragment})/o
4
6
  WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
5
7
 
8
+ attr_reader :blocks, :left
9
+
6
10
  def initialize(tag_name, markup, options)
7
11
  super
8
12
  @blocks = []
9
13
 
10
14
  if markup =~ Syntax
11
- @left = Expression.parse($1)
15
+ @left = parse_expression(Regexp.last_match(1))
12
16
  else
13
- raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
17
+ raise SyntaxError, options[:locale].t("errors.syntax.case")
14
18
  end
15
19
  end
16
20
 
17
21
  def parse(tokens)
18
- body = BlockBody.new
19
- while parse_body(body, tokens)
20
- body = @blocks.last.attachment
22
+ body = case_body = new_body
23
+ body = @blocks.last.attachment while parse_body(body, tokens)
24
+ @blocks.reverse_each do |condition|
25
+ body = condition.attachment
26
+ unless body.frozen?
27
+ body.remove_blank_strings if blank?
28
+ body.freeze
29
+ end
21
30
  end
31
+ case_body.freeze
22
32
  end
23
33
 
24
34
  def nodelist
@@ -27,45 +37,43 @@ module Liquid
27
37
 
28
38
  def unknown_tag(tag, markup, tokens)
29
39
  case tag
30
- when 'when'.freeze
40
+ when 'when'
31
41
  record_when_condition(markup)
32
- when 'else'.freeze
42
+ when 'else'
33
43
  record_else_condition(markup)
34
44
  else
35
45
  super
36
46
  end
37
47
  end
38
48
 
39
- def render(context)
40
- context.stack do
41
- execute_else_block = true
42
-
43
- output = ''
44
- @blocks.each do |block|
45
- if block.else?
46
- return block.attachment.render(context) if execute_else_block
47
- elsif block.evaluate(context)
48
- execute_else_block = false
49
- output << block.attachment.render(context)
50
- end
49
+ def render_to_output_buffer(context, output)
50
+ execute_else_block = true
51
+
52
+ @blocks.each do |block|
53
+ if block.else?
54
+ block.attachment.render_to_output_buffer(context, output) if execute_else_block
55
+ elsif block.evaluate(context)
56
+ execute_else_block = false
57
+ block.attachment.render_to_output_buffer(context, output)
51
58
  end
52
- output
53
59
  end
60
+
61
+ output
54
62
  end
55
63
 
56
64
  private
57
65
 
58
66
  def record_when_condition(markup)
59
- body = BlockBody.new
67
+ body = new_body
60
68
 
61
69
  while markup
62
70
  unless markup =~ WhenSyntax
63
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
71
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
64
72
  end
65
73
 
66
- markup = $2
74
+ markup = Regexp.last_match(2)
67
75
 
68
- block = Condition.new(@left, '=='.freeze, Expression.parse($1))
76
+ block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
69
77
  block.attach(body)
70
78
  @blocks << block
71
79
  end
@@ -73,14 +81,20 @@ module Liquid
73
81
 
74
82
  def record_else_condition(markup)
75
83
  unless markup.strip.empty?
76
- raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
84
+ raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
77
85
  end
78
86
 
79
87
  block = ElseCondition.new
80
- block.attach(BlockBody.new)
88
+ block.attach(new_body)
81
89
  @blocks << block
82
90
  end
91
+
92
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
93
+ def children
94
+ [@node.left] + @node.blocks
95
+ end
96
+ end
83
97
  end
84
98
 
85
- Template.register_tag('case'.freeze, Case)
99
+ Template.register_tag('case', Case)
86
100
  end