liquid 3.0.6 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
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