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
@@ -1,18 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Ifchanged < Block
3
- def render(context)
4
- context.stack do
5
- output = super
5
+ def render_to_output_buffer(context, output)
6
+ block_output = +''
7
+ super(context, block_output)
6
8
 
7
- if output != context.registers[:ifchanged]
8
- context.registers[:ifchanged] = output
9
- output
10
- else
11
- ''.freeze
12
- end
9
+ if block_output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = block_output
11
+ output << block_output
13
12
  end
13
+
14
+ output
14
15
  end
15
16
  end
16
17
 
17
- Template.register_tag('ifchanged'.freeze, Ifchanged)
18
+ Template.register_tag('ifchanged', Ifchanged)
18
19
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # Include allows templates to relate with other templates
3
5
  #
@@ -14,100 +16,96 @@ module Liquid
14
16
  # {% include 'product' for products %}
15
17
  #
16
18
  class Include < Tag
17
- Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
19
+ prepend Tag::Disableable
20
+
21
+ SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
22
+ Syntax = SYNTAX
23
+
24
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
18
25
 
19
26
  def initialize(tag_name, markup, options)
20
27
  super
21
28
 
22
- if markup =~ Syntax
29
+ if markup =~ SYNTAX
23
30
 
24
- template_name = $1
25
- variable_name = $3
31
+ template_name = Regexp.last_match(1)
32
+ variable_name = Regexp.last_match(3)
26
33
 
27
- @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
28
- @template_name_expr = Expression.parse(template_name)
29
- @attributes = {}
34
+ @alias_name = Regexp.last_match(5)
35
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
36
+ @template_name_expr = parse_expression(template_name)
37
+ @attributes = {}
30
38
 
31
39
  markup.scan(TagAttributes) do |key, value|
32
- @attributes[key] = Expression.parse(value)
40
+ @attributes[key] = parse_expression(value)
33
41
  end
34
42
 
35
43
  else
36
- raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
44
+ raise SyntaxError, options[:locale].t("errors.syntax.include")
37
45
  end
38
46
  end
39
47
 
40
48
  def parse(_tokens)
41
49
  end
42
50
 
43
- def render(context)
51
+ def render_to_output_buffer(context, output)
44
52
  template_name = context.evaluate(@template_name_expr)
45
- raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
53
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
54
+
55
+ partial = PartialCache.load(
56
+ template_name,
57
+ context: context,
58
+ parse_context: parse_context
59
+ )
46
60
 
47
- partial = load_cached_partial(template_name, context)
48
- context_variable_name = template_name.split('/'.freeze).last
61
+ context_variable_name = @alias_name || template_name.split('/').last
49
62
 
50
63
  variable = if @variable_name_expr
51
64
  context.evaluate(@variable_name_expr)
52
65
  else
53
- context.find_variable(template_name)
66
+ context.find_variable(template_name, raise_on_not_found: false)
54
67
  end
55
68
 
56
69
  old_template_name = context.template_name
57
- old_partial = context.partial
70
+ old_partial = context.partial
58
71
  begin
59
72
  context.template_name = template_name
60
- context.partial = true
73
+ context.partial = true
61
74
  context.stack do
62
75
  @attributes.each do |key, value|
63
76
  context[key] = context.evaluate(value)
64
77
  end
65
78
 
66
79
  if variable.is_a?(Array)
67
- variable.collect do |var|
80
+ variable.each do |var|
68
81
  context[context_variable_name] = var
69
- partial.render(context)
82
+ partial.render_to_output_buffer(context, output)
70
83
  end
71
84
  else
72
85
  context[context_variable_name] = variable
73
- partial.render(context)
86
+ partial.render_to_output_buffer(context, output)
74
87
  end
75
88
  end
76
89
  ensure
77
90
  context.template_name = old_template_name
78
- context.partial = old_partial
91
+ context.partial = old_partial
79
92
  end
80
- end
81
93
 
82
- private
94
+ output
95
+ end
83
96
 
84
97
  alias_method :parse_context, :options
85
98
  private :parse_context
86
99
 
87
- def load_cached_partial(template_name, context)
88
- cached_partials = context.registers[:cached_partials] || {}
89
-
90
- if cached = cached_partials[template_name]
91
- return cached
100
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
101
+ def children
102
+ [
103
+ @node.template_name_expr,
104
+ @node.variable_name_expr,
105
+ ] + @node.attributes.values
92
106
  end
93
- source = read_template_from_file_system(context)
94
- begin
95
- parse_context.partial = true
96
- partial = Liquid::Template.parse(source, parse_context)
97
- ensure
98
- parse_context.partial = false
99
- end
100
- cached_partials[template_name] = partial
101
- context.registers[:cached_partials] = cached_partials
102
- partial
103
- end
104
-
105
- def read_template_from_file_system(context)
106
- file_system = context.registers[:file_system] || Liquid::Template.file_system
107
-
108
- file_system.read_template_file(context.evaluate(@template_name_expr))
109
107
  end
110
108
  end
111
109
 
112
- Template.register_tag('include'.freeze, Include)
110
+ Template.register_tag('include', Include)
113
111
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # increment is used in a place where one needs to insert a counter
3
5
  # into a template, and needs the counter to survive across
@@ -20,12 +22,14 @@ module Liquid
20
22
  @variable = markup.strip
21
23
  end
22
24
 
23
- def render(context)
25
+ def render_to_output_buffer(context, output)
24
26
  value = context.environments.first[@variable] ||= 0
25
27
  context.environments.first[@variable] = value + 1
26
- value.to_s
28
+
29
+ output << value.to_s
30
+ output
27
31
  end
28
32
  end
29
33
 
30
- Template.register_tag('increment'.freeze, Increment)
34
+ Template.register_tag('increment', Increment)
31
35
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Raw < Block
3
5
  Syntax = /\A\s*\z/
@@ -10,20 +12,21 @@ module Liquid
10
12
  end
11
13
 
12
14
  def parse(tokens)
13
- @body = ''
14
- while token = tokens.shift
15
- if token =~ FullTokenPossiblyInvalid
16
- @body << $1 if $1 != "".freeze
17
- return if block_delimiter == $2
15
+ @body = +''
16
+ while (token = tokens.shift)
17
+ if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
18
+ @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
19
+ return
18
20
  end
19
21
  @body << token unless token.empty?
20
22
  end
21
23
 
22
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
24
+ raise_tag_never_closed(block_name)
23
25
  end
24
26
 
25
- def render(_context)
26
- @body
27
+ def render_to_output_buffer(_context, output)
28
+ output << @body
29
+ output
27
30
  end
28
31
 
29
32
  def nodelist
@@ -37,11 +40,11 @@ module Liquid
37
40
  protected
38
41
 
39
42
  def ensure_valid_markup(tag_name, markup, parse_context)
40
- unless markup =~ Syntax
41
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
43
+ unless Syntax.match?(markup)
44
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
42
45
  end
43
46
  end
44
47
  end
45
48
 
46
- Template.register_tag('raw'.freeze, Raw)
49
+ Template.register_tag('raw', Raw)
47
50
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class Render < Tag
5
+ FOR = 'for'
6
+ SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
7
+
8
+ disable_tags "include"
9
+
10
+ attr_reader :template_name_expr, :attributes
11
+
12
+ def initialize(tag_name, markup, options)
13
+ super
14
+
15
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
16
+
17
+ template_name = Regexp.last_match(1)
18
+ with_or_for = Regexp.last_match(3)
19
+ variable_name = Regexp.last_match(4)
20
+
21
+ @alias_name = Regexp.last_match(6)
22
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
23
+ @template_name_expr = parse_expression(template_name)
24
+ @for = (with_or_for == FOR)
25
+
26
+ @attributes = {}
27
+ markup.scan(TagAttributes) do |key, value|
28
+ @attributes[key] = parse_expression(value)
29
+ end
30
+ end
31
+
32
+ def render_to_output_buffer(context, output)
33
+ render_tag(context, output)
34
+ end
35
+
36
+ def render_tag(context, output)
37
+ # Though we evaluate this here we will only ever parse it as a string literal.
38
+ template_name = context.evaluate(@template_name_expr)
39
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
40
+
41
+ partial = PartialCache.load(
42
+ template_name,
43
+ context: context,
44
+ parse_context: parse_context
45
+ )
46
+
47
+ context_variable_name = @alias_name || template_name.split('/').last
48
+
49
+ render_partial_func = ->(var, forloop) {
50
+ inner_context = context.new_isolated_subcontext
51
+ inner_context.template_name = template_name
52
+ inner_context.partial = true
53
+ inner_context['forloop'] = forloop if forloop
54
+
55
+ @attributes.each do |key, value|
56
+ inner_context[key] = context.evaluate(value)
57
+ end
58
+ inner_context[context_variable_name] = var unless var.nil?
59
+ partial.render_to_output_buffer(inner_context, output)
60
+ forloop&.send(:increment!)
61
+ }
62
+
63
+ variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
64
+ if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
65
+ forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
66
+ variable.each { |var| render_partial_func.call(var, forloop) }
67
+ else
68
+ render_partial_func.call(variable, nil)
69
+ end
70
+
71
+ output
72
+ end
73
+
74
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
75
+ def children
76
+ [
77
+ @node.template_name_expr,
78
+ ] + @node.attributes.values
79
+ end
80
+ end
81
+ end
82
+
83
+ Template.register_tag('render', Render)
84
+ end
@@ -1,54 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class TableRow < Block
3
5
  Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
6
 
7
+ attr_reader :variable_name, :collection_name, :attributes
8
+
5
9
  def initialize(tag_name, markup, options)
6
10
  super
7
11
  if markup =~ Syntax
8
- @variable_name = $1
9
- @collection_name = Expression.parse($2)
10
- @attributes = {}
12
+ @variable_name = Regexp.last_match(1)
13
+ @collection_name = parse_expression(Regexp.last_match(2))
14
+ @attributes = {}
11
15
  markup.scan(TagAttributes) do |key, value|
12
- @attributes[key] = Expression.parse(value)
16
+ @attributes[key] = parse_expression(value)
13
17
  end
14
18
  else
15
- raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
19
+ raise SyntaxError, options[:locale].t("errors.syntax.table_row")
16
20
  end
17
21
  end
18
22
 
19
- def render(context)
20
- collection = context.evaluate(@collection_name) or return ''.freeze
23
+ def render_to_output_buffer(context, output)
24
+ (collection = context.evaluate(@collection_name)) || (return '')
21
25
 
22
- from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
23
- to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
26
+ from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
27
+ to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
24
28
 
25
29
  collection = Utils.slice_collection(collection, from, to)
30
+ length = collection.length
26
31
 
27
- length = collection.length
32
+ cols = context.evaluate(@attributes['cols']).to_i
28
33
 
29
- cols = context.evaluate(@attributes['cols'.freeze]).to_i
30
-
31
- result = "<tr class=\"row1\">\n"
34
+ output << "<tr class=\"row1\">\n"
32
35
  context.stack do
33
36
  tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
34
- context['tablerowloop'.freeze] = tablerowloop
37
+ context['tablerowloop'] = tablerowloop
35
38
 
36
- collection.each_with_index do |item, index|
39
+ collection.each do |item|
37
40
  context[@variable_name] = item
38
41
 
39
- result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
42
+ output << "<td class=\"col#{tablerowloop.col}\">"
43
+ super
44
+ output << '</td>'
40
45
 
41
46
  if tablerowloop.col_last && !tablerowloop.last
42
- result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
47
+ output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
43
48
  end
44
49
 
45
50
  tablerowloop.send(:increment!)
46
51
  end
47
52
  end
48
- result << "</tr>\n"
49
- result
53
+
54
+ output << "</tr>\n"
55
+ output
56
+ end
57
+
58
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
59
+ def children
60
+ super + @node.attributes.values + [@node.collection_name]
61
+ end
50
62
  end
51
63
  end
52
64
 
53
- Template.register_tag('tablerow'.freeze, TableRow)
65
+ Template.register_tag('tablerow', TableRow)
54
66
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'if'
2
4
 
3
5
  module Liquid
@@ -6,25 +8,23 @@ module Liquid
6
8
  # {% unless x < 0 %} x is greater than zero {% endunless %}
7
9
  #
8
10
  class Unless < If
9
- def render(context)
10
- context.stack do
11
- # First condition is interpreted backwards ( if not )
12
- first_block = @blocks.first
13
- unless first_block.evaluate(context)
14
- return first_block.attachment.render(context)
15
- end
11
+ def render_to_output_buffer(context, output)
12
+ # First condition is interpreted backwards ( if not )
13
+ first_block = @blocks.first
14
+ unless first_block.evaluate(context)
15
+ return first_block.attachment.render_to_output_buffer(context, output)
16
+ end
16
17
 
17
- # After the first condition unless works just like if
18
- @blocks[1..-1].each do |block|
19
- if block.evaluate(context)
20
- return block.attachment.render(context)
21
- end
18
+ # After the first condition unless works just like if
19
+ @blocks[1..-1].each do |block|
20
+ if block.evaluate(context)
21
+ return block.attachment.render_to_output_buffer(context, output)
22
22
  end
23
-
24
- ''.freeze
25
23
  end
24
+
25
+ output
26
26
  end
27
27
  end
28
28
 
29
- Template.register_tag('unless'.freeze, Unless)
29
+ Template.register_tag('unless', Unless)
30
30
  end