liquid 3.0.6 → 4.0.3

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 (103) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +154 -58
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +33 -0
  5. data/lib/liquid/block.rb +42 -125
  6. data/lib/liquid/block_body.rb +99 -79
  7. data/lib/liquid/condition.rb +52 -32
  8. data/lib/liquid/context.rb +57 -51
  9. data/lib/liquid/document.rb +19 -9
  10. data/lib/liquid/drop.rb +17 -16
  11. data/lib/liquid/errors.rb +20 -24
  12. data/lib/liquid/expression.rb +26 -10
  13. data/lib/liquid/extensions.rb +19 -7
  14. data/lib/liquid/file_system.rb +11 -11
  15. data/lib/liquid/forloop_drop.rb +42 -0
  16. data/lib/liquid/i18n.rb +6 -6
  17. data/lib/liquid/interrupts.rb +1 -2
  18. data/lib/liquid/lexer.rb +12 -8
  19. data/lib/liquid/locales/en.yml +6 -2
  20. data/lib/liquid/parse_context.rb +38 -0
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser_switching.rb +4 -4
  23. data/lib/liquid/profiler/hooks.rb +7 -7
  24. data/lib/liquid/profiler.rb +18 -19
  25. data/lib/liquid/range_lookup.rb +16 -1
  26. data/lib/liquid/resource_limits.rb +23 -0
  27. data/lib/liquid/standardfilters.rb +207 -61
  28. data/lib/liquid/strainer.rb +15 -8
  29. data/lib/liquid/tablerowloop_drop.rb +62 -0
  30. data/lib/liquid/tag.rb +9 -8
  31. data/lib/liquid/tags/assign.rb +25 -4
  32. data/lib/liquid/tags/break.rb +0 -3
  33. data/lib/liquid/tags/capture.rb +1 -1
  34. data/lib/liquid/tags/case.rb +27 -12
  35. data/lib/liquid/tags/comment.rb +2 -2
  36. data/lib/liquid/tags/cycle.rb +16 -8
  37. data/lib/liquid/tags/decrement.rb +1 -4
  38. data/lib/liquid/tags/for.rb +103 -75
  39. data/lib/liquid/tags/if.rb +60 -44
  40. data/lib/liquid/tags/ifchanged.rb +0 -2
  41. data/lib/liquid/tags/include.rb +71 -51
  42. data/lib/liquid/tags/raw.rb +32 -4
  43. data/lib/liquid/tags/table_row.rb +21 -31
  44. data/lib/liquid/tags/unless.rb +3 -4
  45. data/lib/liquid/template.rb +42 -54
  46. data/lib/liquid/tokenizer.rb +31 -0
  47. data/lib/liquid/truffle.rb +5 -0
  48. data/lib/liquid/utils.rb +52 -8
  49. data/lib/liquid/variable.rb +59 -46
  50. data/lib/liquid/variable_lookup.rb +14 -6
  51. data/lib/liquid/version.rb +2 -1
  52. data/lib/liquid.rb +10 -7
  53. data/test/integration/assign_test.rb +8 -8
  54. data/test/integration/blank_test.rb +14 -14
  55. data/test/integration/block_test.rb +12 -0
  56. data/test/integration/context_test.rb +2 -2
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +42 -40
  59. data/test/integration/error_handling_test.rb +96 -43
  60. data/test/integration/filter_test.rb +60 -20
  61. data/test/integration/hash_ordering_test.rb +9 -9
  62. data/test/integration/output_test.rb +26 -27
  63. data/test/integration/parse_tree_visitor_test.rb +247 -0
  64. data/test/integration/parsing_quirks_test.rb +19 -13
  65. data/test/integration/render_profiling_test.rb +20 -20
  66. data/test/integration/security_test.rb +23 -7
  67. data/test/integration/standard_filter_test.rb +426 -46
  68. data/test/integration/tags/break_tag_test.rb +1 -2
  69. data/test/integration/tags/continue_tag_test.rb +0 -1
  70. data/test/integration/tags/for_tag_test.rb +135 -100
  71. data/test/integration/tags/if_else_tag_test.rb +75 -77
  72. data/test/integration/tags/include_tag_test.rb +50 -31
  73. data/test/integration/tags/increment_tag_test.rb +10 -11
  74. data/test/integration/tags/raw_tag_test.rb +7 -1
  75. data/test/integration/tags/standard_tag_test.rb +121 -122
  76. data/test/integration/tags/statements_test.rb +3 -5
  77. data/test/integration/tags/table_row_test.rb +20 -19
  78. data/test/integration/tags/unless_else_tag_test.rb +6 -6
  79. data/test/integration/template_test.rb +199 -49
  80. data/test/integration/trim_mode_test.rb +529 -0
  81. data/test/integration/variable_test.rb +27 -13
  82. data/test/test_helper.rb +33 -6
  83. data/test/truffle/truffle_test.rb +9 -0
  84. data/test/unit/block_unit_test.rb +8 -5
  85. data/test/unit/condition_unit_test.rb +94 -77
  86. data/test/unit/context_unit_test.rb +69 -72
  87. data/test/unit/file_system_unit_test.rb +3 -3
  88. data/test/unit/i18n_unit_test.rb +2 -2
  89. data/test/unit/lexer_unit_test.rb +12 -9
  90. data/test/unit/parser_unit_test.rb +2 -2
  91. data/test/unit/regexp_unit_test.rb +1 -1
  92. data/test/unit/strainer_unit_test.rb +96 -1
  93. data/test/unit/tag_unit_test.rb +7 -2
  94. data/test/unit/tags/case_tag_unit_test.rb +1 -1
  95. data/test/unit/tags/for_tag_unit_test.rb +2 -2
  96. data/test/unit/tags/if_tag_unit_test.rb +1 -1
  97. data/test/unit/template_unit_test.rb +14 -5
  98. data/test/unit/tokenizer_unit_test.rb +24 -7
  99. data/test/unit/variable_unit_test.rb +60 -43
  100. metadata +62 -50
  101. data/lib/liquid/module_ex.rb +0 -62
  102. data/lib/liquid/token.rb +0 -18
  103. data/test/unit/module_ex_unit_test.rb +0 -87
@@ -12,7 +12,9 @@ module Liquid
12
12
  class If < Block
13
13
  Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
14
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
- BOOLEAN_OPERATORS = %w(and or)
15
+ BOOLEAN_OPERATORS = %w(and or).freeze
16
+
17
+ attr_reader :blocks
16
18
 
17
19
  def initialize(tag_name, markup, options)
18
20
  super
@@ -21,7 +23,12 @@ module Liquid
21
23
  end
22
24
 
23
25
  def nodelist
24
- @blocks.flat_map(&:attachment)
26
+ @blocks.map(&:attachment)
27
+ end
28
+
29
+ def parse(tokens)
30
+ while parse_body(@blocks.last.attachment, tokens)
31
+ end
25
32
  end
26
33
 
27
34
  def unknown_tag(tag, markup, tokens)
@@ -36,7 +43,7 @@ module Liquid
36
43
  context.stack do
37
44
  @blocks.each do |block|
38
45
  if block.evaluate(context)
39
- return render_all(block.attachment, context)
46
+ return block.attachment.render(context)
40
47
  end
41
48
  end
42
49
  ''.freeze
@@ -45,61 +52,70 @@ module Liquid
45
52
 
46
53
  private
47
54
 
48
- def push_block(tag, markup)
49
- block = if tag == 'else'.freeze
50
- ElseCondition.new
51
- else
52
- parse_with_selected_parser(markup)
53
- end
54
-
55
- @blocks.push(block)
56
- @nodelist = block.attach(Array.new)
55
+ def push_block(tag, markup)
56
+ block = if tag == 'else'.freeze
57
+ ElseCondition.new
58
+ else
59
+ parse_with_selected_parser(markup)
57
60
  end
58
61
 
59
- def lax_parse(markup)
60
- expressions = markup.scan(ExpressionsAndOperators)
61
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
62
+ @blocks.push(block)
63
+ block.attach(BlockBody.new)
64
+ end
62
65
 
63
- condition = Condition.new($1, $2, $3)
66
+ def lax_parse(markup)
67
+ expressions = markup.scan(ExpressionsAndOperators)
68
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
64
69
 
65
- while not expressions.empty?
66
- operator = expressions.pop.to_s.strip
70
+ condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
67
71
 
68
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
72
+ until expressions.empty?
73
+ operator = expressions.pop.to_s.strip
69
74
 
70
- new_condition = Condition.new($1, $2, $3)
71
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
72
- new_condition.send(operator, condition)
73
- condition = new_condition
74
- end
75
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
75
76
 
76
- condition
77
+ new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
78
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
79
+ new_condition.send(operator, condition)
80
+ condition = new_condition
77
81
  end
78
82
 
79
- def strict_parse(markup)
80
- p = Parser.new(markup)
81
- condition = parse_binary_comparison(p)
82
- p.consume(:end_of_string)
83
- condition
83
+ condition
84
+ end
85
+
86
+ def strict_parse(markup)
87
+ p = Parser.new(markup)
88
+ condition = parse_binary_comparisons(p)
89
+ p.consume(:end_of_string)
90
+ condition
91
+ end
92
+
93
+ def parse_binary_comparisons(p)
94
+ condition = parse_comparison(p)
95
+ first_condition = condition
96
+ while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
97
+ child_condition = parse_comparison(p)
98
+ condition.send(op, child_condition)
99
+ condition = child_condition
84
100
  end
101
+ first_condition
102
+ end
85
103
 
86
- def parse_binary_comparison(p)
87
- condition = parse_comparison(p)
88
- if op = (p.id?('and'.freeze) || p.id?('or'.freeze))
89
- condition.send(op, parse_binary_comparison(p))
90
- end
91
- condition
104
+ def parse_comparison(p)
105
+ a = Expression.parse(p.expression)
106
+ if op = p.consume?(:comparison)
107
+ b = Expression.parse(p.expression)
108
+ Condition.new(a, op, b)
109
+ else
110
+ Condition.new(a)
92
111
  end
112
+ end
93
113
 
94
- def parse_comparison(p)
95
- a = p.expression
96
- if op = p.consume?(:comparison)
97
- b = p.expression
98
- Condition.new(a, op, b)
99
- else
100
- Condition.new(a)
101
- end
114
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
115
+ def children
116
+ @node.blocks
102
117
  end
118
+ end
103
119
  end
104
120
 
105
121
  Template.register_tag('if'.freeze, If)
@@ -1,9 +1,7 @@
1
1
  module Liquid
2
2
  class Ifchanged < Block
3
-
4
3
  def render(context)
5
4
  context.stack do
6
-
7
5
  output = super
8
6
 
9
7
  if output != context.registers[:ifchanged]
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Include allows templates to relate with other templates
4
3
  #
5
4
  # Simply include another template:
@@ -17,17 +16,22 @@ module Liquid
17
16
  class Include < Tag
18
17
  Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
19
18
 
19
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
20
+
20
21
  def initialize(tag_name, markup, options)
21
22
  super
22
23
 
23
24
  if markup =~ Syntax
24
25
 
25
- @template_name = $1
26
- @variable_name = $3
27
- @attributes = {}
26
+ template_name = $1
27
+ variable_name = $3
28
+
29
+ @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
30
+ @template_name_expr = Expression.parse(template_name)
31
+ @attributes = {}
28
32
 
29
33
  markup.scan(TagAttributes) do |key, value|
30
- @attributes[key] = value
34
+ @attributes[key] = Expression.parse(value)
31
35
  end
32
36
 
33
37
  else
@@ -35,69 +39,85 @@ module Liquid
35
39
  end
36
40
  end
37
41
 
38
- def parse(tokens)
42
+ def parse(_tokens)
39
43
  end
40
44
 
41
45
  def render(context)
42
- partial = load_cached_partial(context)
43
- variable = context[@variable_name || @template_name[1..-2]]
46
+ template_name = context.evaluate(@template_name_expr)
47
+ raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
44
48
 
45
- context.stack do
46
- @attributes.each do |key, value|
47
- context[key] = context[value]
48
- end
49
+ partial = load_cached_partial(template_name, context)
50
+ context_variable_name = template_name.split('/'.freeze).last
51
+
52
+ variable = if @variable_name_expr
53
+ context.evaluate(@variable_name_expr)
54
+ else
55
+ context.find_variable(template_name, raise_on_not_found: false)
56
+ end
49
57
 
50
- context_variable_name = @template_name[1..-2].split('/'.freeze).last
51
- if variable.is_a?(Array)
52
- variable.collect do |var|
53
- context[context_variable_name] = var
58
+ old_template_name = context.template_name
59
+ old_partial = context.partial
60
+ begin
61
+ context.template_name = template_name
62
+ context.partial = true
63
+ context.stack do
64
+ @attributes.each do |key, value|
65
+ context[key] = context.evaluate(value)
66
+ end
67
+
68
+ if variable.is_a?(Array)
69
+ variable.collect do |var|
70
+ context[context_variable_name] = var
71
+ partial.render(context)
72
+ end
73
+ else
74
+ context[context_variable_name] = variable
54
75
  partial.render(context)
55
76
  end
56
- else
57
- context[context_variable_name] = variable
58
- partial.render(context)
59
77
  end
78
+ ensure
79
+ context.template_name = old_template_name
80
+ context.partial = old_partial
60
81
  end
61
82
  end
62
83
 
63
84
  private
64
- def load_cached_partial(context)
65
- cached_partials = context.registers[:cached_partials] || {}
66
- template_name = context[@template_name]
67
85
 
68
- if cached = cached_partials[template_name]
69
- return cached
70
- end
71
- source = read_template_from_file_system(context)
72
- partial = Liquid::Template.parse(source, pass_options)
73
- cached_partials[template_name] = partial
74
- context.registers[:cached_partials] = cached_partials
75
- partial
76
- end
86
+ alias_method :parse_context, :options
87
+ private :parse_context
77
88
 
78
- def read_template_from_file_system(context)
79
- file_system = context.registers[:file_system] || Liquid::Template.file_system
80
-
81
- # make read_template_file call backwards-compatible.
82
- case file_system.method(:read_template_file).arity
83
- when 1
84
- file_system.read_template_file(context[@template_name])
85
- when 2
86
- file_system.read_template_file(context[@template_name], context)
87
- else
88
- raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
89
- end
89
+ def load_cached_partial(template_name, context)
90
+ cached_partials = context.registers[:cached_partials] || {}
91
+
92
+ if cached = cached_partials[template_name]
93
+ return cached
90
94
  end
95
+ source = read_template_from_file_system(context)
96
+ begin
97
+ parse_context.partial = true
98
+ partial = Liquid::Template.parse(source, parse_context)
99
+ ensure
100
+ parse_context.partial = false
101
+ end
102
+ cached_partials[template_name] = partial
103
+ context.registers[:cached_partials] = cached_partials
104
+ partial
105
+ end
91
106
 
92
- def pass_options
93
- dont_pass = @options[:include_options_blacklist]
94
- return {locale: @options[:locale]} if dont_pass == true
95
- opts = @options.merge(included: true, include_options_blacklist: false)
96
- if dont_pass.is_a?(Array)
97
- dont_pass.each {|o| opts.delete(o)}
98
- end
99
- opts
107
+ def read_template_from_file_system(context)
108
+ file_system = context.registers[:file_system] || Liquid::Template.file_system
109
+
110
+ file_system.read_template_file(context.evaluate(@template_name_expr))
111
+ end
112
+
113
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
114
+ def children
115
+ [
116
+ @node.template_name_expr,
117
+ @node.variable_name_expr
118
+ ] + @node.attributes.values
100
119
  end
120
+ end
101
121
  end
102
122
 
103
123
  Template.register_tag('include'.freeze, Include)
@@ -1,16 +1,44 @@
1
1
  module Liquid
2
2
  class Raw < Block
3
+ Syntax = /\A\s*\z/
3
4
  FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
4
5
 
6
+ def initialize(tag_name, markup, parse_context)
7
+ super
8
+
9
+ ensure_valid_markup(tag_name, markup, parse_context)
10
+ end
11
+
5
12
  def parse(tokens)
6
- @nodelist ||= []
7
- @nodelist.clear
13
+ @body = ''
8
14
  while token = tokens.shift
9
15
  if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != "".freeze
16
+ @body << $1 if $1 != "".freeze
11
17
  return if block_delimiter == $2
12
18
  end
13
- @nodelist << token if not token.empty?
19
+ @body << token unless token.empty?
20
+ end
21
+
22
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
23
+ end
24
+
25
+ def render(_context)
26
+ @body
27
+ end
28
+
29
+ def nodelist
30
+ [@body]
31
+ end
32
+
33
+ def blank?
34
+ @body.empty?
35
+ end
36
+
37
+ protected
38
+
39
+ 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))
14
42
  end
15
43
  end
16
44
  end
@@ -2,14 +2,16 @@ module Liquid
2
2
  class TableRow < Block
3
3
  Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
4
 
5
+ attr_reader :variable_name, :collection_name, :attributes
6
+
5
7
  def initialize(tag_name, markup, options)
6
8
  super
7
9
  if markup =~ Syntax
8
10
  @variable_name = $1
9
- @collection_name = $2
11
+ @collection_name = Expression.parse($2)
10
12
  @attributes = {}
11
13
  markup.scan(TagAttributes) do |key, value|
12
- @attributes[key] = value
14
+ @attributes[key] = Expression.parse(value)
13
15
  end
14
16
  else
15
17
  raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
@@ -17,55 +19,43 @@ module Liquid
17
19
  end
18
20
 
19
21
  def render(context)
20
- collection = context[@collection_name] or return ''.freeze
22
+ collection = context.evaluate(@collection_name) or return ''.freeze
21
23
 
22
- from = @attributes['offset'.freeze] ? context[@attributes['offset'.freeze]].to_i : 0
23
- to = @attributes['limit'.freeze] ? from + context[@attributes['limit'.freeze]].to_i : nil
24
+ from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
25
+ to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
24
26
 
25
27
  collection = Utils.slice_collection(collection, from, to)
26
28
 
27
29
  length = collection.length
28
30
 
29
- cols = context[@attributes['cols'.freeze]].to_i
30
-
31
- row = 1
32
- col = 0
31
+ cols = context.evaluate(@attributes['cols'.freeze]).to_i
33
32
 
34
33
  result = "<tr class=\"row1\">\n"
35
34
  context.stack do
35
+ tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
36
+ context['tablerowloop'.freeze] = tablerowloop
36
37
 
37
- collection.each_with_index do |item, index|
38
+ collection.each do |item|
38
39
  context[@variable_name] = item
39
- context['tablerowloop'.freeze] = {
40
- 'length'.freeze => length,
41
- 'index'.freeze => index + 1,
42
- 'index0'.freeze => index,
43
- 'col'.freeze => col + 1,
44
- 'col0'.freeze => col,
45
- 'rindex'.freeze => length - index,
46
- 'rindex0'.freeze => length - index - 1,
47
- 'first'.freeze => (index == 0),
48
- 'last'.freeze => (index == length - 1),
49
- 'col_first'.freeze => (col == 0),
50
- 'col_last'.freeze => (col == cols - 1)
51
- }
52
-
53
-
54
- col += 1
55
40
 
56
- result << "<td class=\"col#{col}\">" << super << '</td>'
41
+ result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
57
42
 
58
- if col == cols and (index != length - 1)
59
- col = 0
60
- row += 1
61
- result << "</tr>\n<tr class=\"row#{row}\">"
43
+ if tablerowloop.col_last && !tablerowloop.last
44
+ result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
62
45
  end
63
46
 
47
+ tablerowloop.send(:increment!)
64
48
  end
65
49
  end
66
50
  result << "</tr>\n"
67
51
  result
68
52
  end
53
+
54
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
55
+ def children
56
+ super + @node.attributes.values + [@node.collection_name]
57
+ end
58
+ end
69
59
  end
70
60
 
71
61
  Template.register_tag('tablerow'.freeze, TableRow)
@@ -1,4 +1,4 @@
1
- require File.dirname(__FILE__) + '/if'
1
+ require_relative 'if'
2
2
 
3
3
  module Liquid
4
4
  # Unless is a conditional just like 'if' but works on the inverse logic.
@@ -8,17 +8,16 @@ module Liquid
8
8
  class Unless < If
9
9
  def render(context)
10
10
  context.stack do
11
-
12
11
  # First condition is interpreted backwards ( if not )
13
12
  first_block = @blocks.first
14
13
  unless first_block.evaluate(context)
15
- return render_all(first_block.attachment, context)
14
+ return first_block.attachment.render(context)
16
15
  end
17
16
 
18
17
  # After the first condition unless works just like if
19
18
  @blocks[1..-1].each do |block|
20
19
  if block.evaluate(context)
21
- return render_all(block.attachment, context)
20
+ return block.attachment.render(context)
22
21
  end
23
22
  end
24
23
 
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Templates are central to liquid.
4
3
  # Interpretating templates is a two step process. First you compile the
5
4
  # source code you got. During compile time some extensive error checking is performed.
@@ -14,21 +13,21 @@ module Liquid
14
13
  # template.render('user_name' => 'bob')
15
14
  #
16
15
  class Template
17
- DEFAULT_OPTIONS = {
18
- :locale => I18n.new
19
- }
16
+ attr_accessor :root
17
+ attr_reader :resource_limits, :warnings
20
18
 
21
- attr_accessor :root, :resource_limits
22
19
  @@file_system = BlankFileSystem.new
23
20
 
24
21
  class TagRegistry
22
+ include Enumerable
23
+
25
24
  def initialize
26
- @tags = {}
25
+ @tags = {}
27
26
  @cache = {}
28
27
  end
29
28
 
30
29
  def [](tag_name)
31
- return nil unless @tags.has_key?(tag_name)
30
+ return nil unless @tags.key?(tag_name)
32
31
  return @cache[tag_name] if Liquid.cache_classes
33
32
 
34
33
  lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
@@ -44,6 +43,10 @@ module Liquid
44
43
  @cache.delete(tag_name)
45
44
  end
46
45
 
46
+ def each(&block)
47
+ @tags.each(&block)
48
+ end
49
+
47
50
  private
48
51
 
49
52
  def lookup_class(name)
@@ -66,6 +69,11 @@ module Liquid
66
69
  # :error raises an error when tainted output is used
67
70
  attr_writer :taint_mode
68
71
 
72
+ attr_accessor :default_exception_renderer
73
+ Template.default_exception_renderer = lambda do |exception|
74
+ exception
75
+ end
76
+
69
77
  def file_system
70
78
  @@file_system
71
79
  end
@@ -83,11 +91,11 @@ module Liquid
83
91
  end
84
92
 
85
93
  def error_mode
86
- @error_mode || :lax
94
+ @error_mode ||= :lax
87
95
  end
88
96
 
89
97
  def taint_mode
90
- @taint_mode || :lax
98
+ @taint_mode ||= :lax
91
99
  end
92
100
 
93
101
  # Pass a module with filter methods which should be available
@@ -110,7 +118,8 @@ module Liquid
110
118
  end
111
119
 
112
120
  def initialize
113
- @resource_limits = self.class.default_resource_limits.dup
121
+ @rethrow_errors = false
122
+ @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
114
123
  end
115
124
 
116
125
  # Parse source code.
@@ -119,16 +128,12 @@ module Liquid
119
128
  @options = options
120
129
  @profiling = options[:profile]
121
130
  @line_numbers = options[:line_numbers] || @profiling
122
- @root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
123
- @warnings = nil
131
+ parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
132
+ @root = Document.parse(tokenize(source), parse_context)
133
+ @warnings = parse_context.warnings
124
134
  self
125
135
  end
126
136
 
127
- def warnings
128
- return [] unless @root
129
- @warnings ||= @root.warnings
130
- end
131
-
132
137
  def registers
133
138
  @registers ||= {}
134
139
  end
@@ -167,7 +172,7 @@ module Liquid
167
172
  c = args.shift
168
173
 
169
174
  if @rethrow_errors
170
- c.exception_handler = ->(e) { true }
175
+ c.exception_renderer = ->(e) { raise }
171
176
  end
172
177
 
173
178
  c
@@ -186,27 +191,20 @@ module Liquid
186
191
  when Hash
187
192
  options = args.pop
188
193
 
189
- if options[:registers].is_a?(Hash)
190
- self.registers.merge!(options[:registers])
191
- end
192
-
193
- if options[:filters]
194
- context.add_filters(options[:filters])
195
- end
194
+ registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
196
195
 
197
- if options[:exception_handler]
198
- context.exception_handler = options[:exception_handler]
199
- end
200
- when Module
201
- context.add_filters(args.pop)
202
- when Array
196
+ apply_options_to_context(context, options)
197
+ when Module, Array
203
198
  context.add_filters(args.pop)
204
199
  end
205
200
 
201
+ # Retrying a render resets resource usage
202
+ context.resource_limits.reset
203
+
206
204
  begin
207
205
  # render the nodelist.
208
206
  # for performance reasons we get an array back here. join will make a string out of it.
209
- result = with_profiling do
207
+ result = with_profiling(context) do
210
208
  @root.render(context)
211
209
  end
212
210
  result.respond_to?(:join) ? result.join : result
@@ -224,32 +222,14 @@ module Liquid
224
222
 
225
223
  private
226
224
 
227
- # Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
228
225
  def tokenize(source)
229
- source = source.source if source.respond_to?(:source)
230
- return [] if source.to_s.empty?
231
-
232
- tokens = calculate_line_numbers(source.split(TemplateParser))
233
-
234
- # removes the rogue empty element at the beginning of the array
235
- tokens.shift if tokens[0] and tokens[0].empty?
236
-
237
- tokens
226
+ Tokenizer.new(source, @line_numbers)
238
227
  end
239
228
 
240
- def calculate_line_numbers(raw_tokens)
241
- return raw_tokens unless @line_numbers
229
+ def with_profiling(context)
230
+ if @profiling && !context.partial
231
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
242
232
 
243
- current_line = 1
244
- raw_tokens.map do |token|
245
- Token.new(token, current_line).tap do
246
- current_line += token.count("\n")
247
- end
248
- end
249
- end
250
-
251
- def with_profiling
252
- if @profiling && !@options[:included]
253
233
  @profiler = Profiler.new
254
234
  @profiler.start
255
235
 
@@ -262,5 +242,13 @@ module Liquid
262
242
  yield
263
243
  end
264
244
  end
245
+
246
+ def apply_options_to_context(context, options)
247
+ context.add_filters(options[:filters]) if options[:filters]
248
+ context.global_filter = options[:global_filter] if options[:global_filter]
249
+ context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
250
+ context.strict_variables = options[:strict_variables] if options[:strict_variables]
251
+ context.strict_filters = options[:strict_filters] if options[:strict_filters]
252
+ end
265
253
  end
266
254
  end