liquid 2.6.1 → 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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +194 -29
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +60 -2
  5. data/lib/liquid.rb +25 -14
  6. data/lib/liquid/block.rb +47 -96
  7. data/lib/liquid/block_body.rb +143 -0
  8. data/lib/liquid/condition.rb +70 -39
  9. data/lib/liquid/context.rb +116 -157
  10. data/lib/liquid/document.rb +19 -9
  11. data/lib/liquid/drop.rb +31 -14
  12. data/lib/liquid/errors.rb +54 -10
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +19 -7
  15. data/lib/liquid/file_system.rb +25 -14
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +2 -3
  19. data/lib/liquid/lexer.rb +55 -0
  20. data/lib/liquid/locales/en.yml +26 -0
  21. data/lib/liquid/parse_context.rb +38 -0
  22. data/lib/liquid/parse_tree_visitor.rb +42 -0
  23. data/lib/liquid/parser.rb +90 -0
  24. data/lib/liquid/parser_switching.rb +31 -0
  25. data/lib/liquid/profiler.rb +158 -0
  26. data/lib/liquid/profiler/hooks.rb +23 -0
  27. data/lib/liquid/range_lookup.rb +37 -0
  28. data/lib/liquid/resource_limits.rb +23 -0
  29. data/lib/liquid/standardfilters.rb +311 -77
  30. data/lib/liquid/strainer.rb +39 -26
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +28 -11
  33. data/lib/liquid/tags/assign.rb +34 -10
  34. data/lib/liquid/tags/break.rb +1 -4
  35. data/lib/liquid/tags/capture.rb +11 -9
  36. data/lib/liquid/tags/case.rb +37 -22
  37. data/lib/liquid/tags/comment.rb +10 -3
  38. data/lib/liquid/tags/continue.rb +1 -4
  39. data/lib/liquid/tags/cycle.rb +20 -14
  40. data/lib/liquid/tags/decrement.rb +4 -8
  41. data/lib/liquid/tags/for.rb +121 -60
  42. data/lib/liquid/tags/if.rb +73 -30
  43. data/lib/liquid/tags/ifchanged.rb +3 -5
  44. data/lib/liquid/tags/include.rb +77 -46
  45. data/lib/liquid/tags/increment.rb +4 -8
  46. data/lib/liquid/tags/raw.rb +35 -10
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +6 -9
  49. data/lib/liquid/template.rb +130 -32
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/truffle.rb +5 -0
  52. data/lib/liquid/utils.rb +57 -4
  53. data/lib/liquid/variable.rb +121 -30
  54. data/lib/liquid/variable_lookup.rb +88 -0
  55. data/lib/liquid/version.rb +2 -1
  56. data/test/fixtures/en_locale.yml +9 -0
  57. data/test/integration/assign_test.rb +48 -0
  58. data/test/integration/blank_test.rb +106 -0
  59. data/test/integration/block_test.rb +12 -0
  60. data/test/{liquid → integration}/capture_test.rb +13 -3
  61. data/test/integration/context_test.rb +32 -0
  62. data/test/integration/document_test.rb +19 -0
  63. data/test/integration/drop_test.rb +273 -0
  64. data/test/integration/error_handling_test.rb +260 -0
  65. data/test/integration/filter_test.rb +178 -0
  66. data/test/integration/hash_ordering_test.rb +23 -0
  67. data/test/integration/output_test.rb +123 -0
  68. data/test/integration/parse_tree_visitor_test.rb +247 -0
  69. data/test/integration/parsing_quirks_test.rb +122 -0
  70. data/test/integration/render_profiling_test.rb +154 -0
  71. data/test/integration/security_test.rb +80 -0
  72. data/test/integration/standard_filter_test.rb +776 -0
  73. data/test/{liquid → integration}/tags/break_tag_test.rb +2 -3
  74. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -2
  75. data/test/integration/tags/for_tag_test.rb +410 -0
  76. data/test/integration/tags/if_else_tag_test.rb +188 -0
  77. data/test/integration/tags/include_tag_test.rb +253 -0
  78. data/test/integration/tags/increment_tag_test.rb +23 -0
  79. data/test/{liquid → integration}/tags/raw_tag_test.rb +9 -2
  80. data/test/integration/tags/standard_tag_test.rb +296 -0
  81. data/test/integration/tags/statements_test.rb +111 -0
  82. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +25 -24
  83. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  84. data/test/integration/template_test.rb +332 -0
  85. data/test/integration/trim_mode_test.rb +529 -0
  86. data/test/integration/variable_test.rb +96 -0
  87. data/test/test_helper.rb +106 -19
  88. data/test/truffle/truffle_test.rb +9 -0
  89. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +9 -9
  90. data/test/unit/condition_unit_test.rb +166 -0
  91. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +85 -74
  92. data/test/unit/file_system_unit_test.rb +35 -0
  93. data/test/unit/i18n_unit_test.rb +37 -0
  94. data/test/unit/lexer_unit_test.rb +51 -0
  95. data/test/unit/parser_unit_test.rb +82 -0
  96. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +4 -4
  97. data/test/unit/strainer_unit_test.rb +164 -0
  98. data/test/unit/tag_unit_test.rb +21 -0
  99. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  100. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  101. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  102. data/test/unit/template_unit_test.rb +78 -0
  103. data/test/unit/tokenizer_unit_test.rb +55 -0
  104. data/test/unit/variable_unit_test.rb +162 -0
  105. metadata +157 -77
  106. data/lib/extras/liquid_view.rb +0 -51
  107. data/lib/liquid/htmltags.rb +0 -74
  108. data/lib/liquid/module_ex.rb +0 -62
  109. data/test/liquid/assign_test.rb +0 -21
  110. data/test/liquid/condition_test.rb +0 -127
  111. data/test/liquid/drop_test.rb +0 -180
  112. data/test/liquid/error_handling_test.rb +0 -81
  113. data/test/liquid/file_system_test.rb +0 -29
  114. data/test/liquid/filter_test.rb +0 -125
  115. data/test/liquid/hash_ordering_test.rb +0 -25
  116. data/test/liquid/module_ex_test.rb +0 -87
  117. data/test/liquid/output_test.rb +0 -116
  118. data/test/liquid/parsing_quirks_test.rb +0 -52
  119. data/test/liquid/security_test.rb +0 -64
  120. data/test/liquid/standard_filter_test.rb +0 -251
  121. data/test/liquid/strainer_test.rb +0 -52
  122. data/test/liquid/tags/for_tag_test.rb +0 -297
  123. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  124. data/test/liquid/tags/include_tag_test.rb +0 -166
  125. data/test/liquid/tags/increment_tag_test.rb +0 -24
  126. data/test/liquid/tags/standard_tag_test.rb +0 -295
  127. data/test/liquid/tags/statements_test.rb +0 -134
  128. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  129. data/test/liquid/template_test.rb +0 -146
  130. data/test/liquid/variable_test.rb +0 -186
@@ -1,53 +1,66 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Strainer is the parent class for the filters system.
6
5
  # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
7
6
  #
8
7
  # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
9
8
  # Context#add_filters or Template.register_filter
10
9
  class Strainer #:nodoc:
11
- @@filters = []
12
- @@known_filters = Set.new
13
- @@known_methods = Set.new
10
+ @@global_strainer = Class.new(Strainer) do
11
+ @filter_methods = Set.new
12
+ end
13
+ @@strainer_class_cache = Hash.new do |hash, filters|
14
+ hash[filters] = Class.new(@@global_strainer) do
15
+ @filter_methods = @@global_strainer.filter_methods.dup
16
+ filters.each { |f| add_filter(f) }
17
+ end
18
+ end
14
19
 
15
20
  def initialize(context)
16
21
  @context = context
17
22
  end
18
23
 
19
- def self.global_filter(filter)
20
- raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
21
- add_known_filter(filter)
22
- @@filters << filter unless @@filters.include?(filter)
23
- end
24
-
25
- def self.add_known_filter(filter)
26
- unless @@known_filters.include?(filter)
27
- @@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
28
- new_methods = filter.instance_methods.map(&:to_s)
29
- new_methods.reject!{ |m| @@method_blacklist.include?(m) }
30
- @@known_methods.merge(new_methods)
31
- @@known_filters.add(filter)
24
+ class << self
25
+ attr_reader :filter_methods
26
+ end
27
+
28
+ def self.add_filter(filter)
29
+ raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
30
+ unless self.include?(filter)
31
+ invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
32
+ if invokable_non_public_methods.any?
33
+ raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
34
+ else
35
+ send(:include, filter)
36
+ @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
37
+ end
32
38
  end
33
39
  end
34
40
 
35
- def self.create(context)
36
- strainer = Strainer.new(context)
37
- @@filters.each { |m| strainer.extend(m) }
38
- strainer
41
+ def self.global_filter(filter)
42
+ @@strainer_class_cache.clear
43
+ @@global_strainer.add_filter(filter)
44
+ end
45
+
46
+ def self.invokable?(method)
47
+ @filter_methods.include?(method.to_s)
48
+ end
49
+
50
+ def self.create(context, filters = [])
51
+ @@strainer_class_cache[filters].new(context)
39
52
  end
40
53
 
41
54
  def invoke(method, *args)
42
- if invokable?(method)
55
+ if self.class.invokable?(method)
43
56
  send(method, *args)
57
+ elsif @context && @context.strict_filters
58
+ raise Liquid::UndefinedFilter, "undefined filter #{method}"
44
59
  else
45
60
  args.first
46
61
  end
47
- end
48
-
49
- def invokable?(method)
50
- @@known_methods.include?(method.to_s) && respond_to?(method)
62
+ rescue ::ArgumentError => e
63
+ raise Liquid::ArgumentError, e.message, e.backtrace
51
64
  end
52
65
  end
53
66
  end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ class TablerowloopDrop < Drop
3
+ def initialize(length, cols)
4
+ @length = length
5
+ @row = 1
6
+ @col = 1
7
+ @cols = cols
8
+ @index = 0
9
+ end
10
+
11
+ attr_reader :length, :col, :row
12
+
13
+ def index
14
+ @index + 1
15
+ end
16
+
17
+ def index0
18
+ @index
19
+ end
20
+
21
+ def col0
22
+ @col - 1
23
+ end
24
+
25
+ def rindex
26
+ @length - @index
27
+ end
28
+
29
+ def rindex0
30
+ @length - @index - 1
31
+ end
32
+
33
+ def first
34
+ @index == 0
35
+ end
36
+
37
+ def last
38
+ @index == @length - 1
39
+ end
40
+
41
+ def col_first
42
+ @col == 1
43
+ end
44
+
45
+ def col_last
46
+ @col == @cols
47
+ end
48
+
49
+ protected
50
+
51
+ def increment!
52
+ @index += 1
53
+
54
+ if @col == @cols
55
+ @col = 1
56
+ @row += 1
57
+ else
58
+ @col += 1
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,26 +1,43 @@
1
1
  module Liquid
2
-
3
2
  class Tag
3
+ attr_reader :nodelist, :tag_name, :line_number, :parse_context
4
+ alias_method :options, :parse_context
5
+ include ParserSwitching
6
+
7
+ class << self
8
+ def parse(tag_name, markup, tokenizer, options)
9
+ tag = new(tag_name, markup, options)
10
+ tag.parse(tokenizer)
11
+ tag
12
+ end
13
+
14
+ private :new
15
+ end
4
16
 
5
- attr_accessor :nodelist
6
-
7
- def initialize(tag_name, markup, tokens)
17
+ def initialize(tag_name, markup, parse_context)
8
18
  @tag_name = tag_name
9
19
  @markup = markup
10
- parse(tokens)
20
+ @parse_context = parse_context
21
+ @line_number = parse_context.line_number
11
22
  end
12
23
 
13
- def parse(tokens)
24
+ def parse(_tokens)
25
+ end
26
+
27
+ def raw
28
+ "#{@tag_name} #{@markup}"
14
29
  end
15
30
 
16
31
  def name
17
32
  self.class.name.downcase
18
33
  end
19
34
 
20
- def render(context)
21
- ''
35
+ def render(_context)
36
+ ''.freeze
22
37
  end
23
38
 
24
- end # Tag
25
-
26
- end # Liquid
39
+ def blank?
40
+ false
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Assign sets a variable in your template.
4
3
  #
5
4
  # {% assign foo = 'monkey' %}
@@ -9,27 +8,52 @@ module Liquid
9
8
  # {{ foo }}
10
9
  #
11
10
  class Assign < Tag
12
- Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
11
+ Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
12
+
13
+ attr_reader :to, :from
13
14
 
14
- def initialize(tag_name, markup, tokens)
15
+ def initialize(tag_name, markup, options)
16
+ super
15
17
  if markup =~ Syntax
16
18
  @to = $1
17
- @from = Variable.new($2)
19
+ @from = Variable.new($2, options)
18
20
  else
19
- raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
21
+ raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
20
22
  end
21
-
22
- super
23
23
  end
24
24
 
25
25
  def render(context)
26
26
  val = @from.render(context)
27
27
  context.scopes.last[@to] = val
28
- context.resource_limits[:assign_score_current] += (val.respond_to?(:length) ? val.length : 1)
29
- ''
28
+ context.resource_limits.assign_score += assign_score_of(val)
29
+ ''.freeze
30
+ end
31
+
32
+ def blank?
33
+ true
30
34
  end
31
35
 
36
+ private
37
+
38
+ def assign_score_of(val)
39
+ if val.instance_of?(String)
40
+ val.length
41
+ elsif val.instance_of?(Array) || val.instance_of?(Hash)
42
+ sum = 1
43
+ # Uses #each to avoid extra allocations.
44
+ val.each { |child| sum += assign_score_of(child) }
45
+ sum
46
+ else
47
+ 1
48
+ end
49
+ end
50
+
51
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
52
+ def children
53
+ [@node.from]
54
+ end
55
+ end
32
56
  end
33
57
 
34
- Template.register_tag('assign', Assign)
58
+ Template.register_tag('assign'.freeze, Assign)
35
59
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Break tag to be used to break out of a for loop.
4
3
  #
5
4
  # == Basic Usage:
@@ -10,12 +9,10 @@ module Liquid
10
9
  # {% endfor %}
11
10
  #
12
11
  class Break < Tag
13
-
14
12
  def interrupt
15
13
  BreakInterrupt.new
16
14
  end
17
-
18
15
  end
19
16
 
20
- Template.register_tag('break', Break)
17
+ Template.register_tag('break'.freeze, Break)
21
18
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Capture stores the result of a block into a variable without rendering it inplace.
4
3
  #
5
4
  # {% capture heading %}
@@ -12,25 +11,28 @@ module Liquid
12
11
  # in a sidebar or footer.
13
12
  #
14
13
  class Capture < Block
15
- Syntax = /(\w+)/
14
+ Syntax = /(#{VariableSignature}+)/o
16
15
 
17
- def initialize(tag_name, markup, tokens)
16
+ def initialize(tag_name, markup, options)
17
+ super
18
18
  if markup =~ Syntax
19
19
  @to = $1
20
20
  else
21
- raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
21
+ raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
22
22
  end
23
-
24
- super
25
23
  end
26
24
 
27
25
  def render(context)
28
26
  output = super
29
27
  context.scopes.last[@to] = output
30
- context.resource_limits[:assign_score_current] += (output.respond_to?(:length) ? output.length : 1)
31
- ''
28
+ context.resource_limits.assign_score += output.length
29
+ ''.freeze
30
+ end
31
+
32
+ def blank?
33
+ true
32
34
  end
33
35
  end
34
36
 
35
- Template.register_tag('capture', Capture)
37
+ Template.register_tag('capture'.freeze, Capture)
36
38
  end
@@ -1,26 +1,37 @@
1
1
  module Liquid
2
2
  class Case < Block
3
3
  Syntax = /(#{QuotedFragment})/o
4
- WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
4
+ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
5
5
 
6
- def initialize(tag_name, markup, tokens)
6
+ attr_reader :blocks, :left
7
+
8
+ def initialize(tag_name, markup, options)
9
+ super
7
10
  @blocks = []
8
11
 
9
12
  if markup =~ Syntax
10
- @left = $1
13
+ @left = Expression.parse($1)
11
14
  else
12
- raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
15
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
13
16
  end
17
+ end
14
18
 
15
- super
19
+ def parse(tokens)
20
+ body = BlockBody.new
21
+ while parse_body(body, tokens)
22
+ body = @blocks.last.attachment
23
+ end
24
+ end
25
+
26
+ def nodelist
27
+ @blocks.map(&:attachment)
16
28
  end
17
29
 
18
30
  def unknown_tag(tag, markup, tokens)
19
- @nodelist = []
20
31
  case tag
21
- when 'when'
32
+ when 'when'.freeze
22
33
  record_when_condition(markup)
23
- when 'else'
34
+ when 'else'.freeze
24
35
  record_else_condition(markup)
25
36
  else
26
37
  super
@@ -34,10 +45,10 @@ module Liquid
34
45
  output = ''
35
46
  @blocks.each do |block|
36
47
  if block.else?
37
- return render_all(block.attachment, context) if execute_else_block
48
+ return block.attachment.render(context) if execute_else_block
38
49
  elsif block.evaluate(context)
39
50
  execute_else_block = false
40
- output << render_all(block.attachment, context)
51
+ output << block.attachment.render(context)
41
52
  end
42
53
  end
43
54
  output
@@ -47,33 +58,37 @@ module Liquid
47
58
  private
48
59
 
49
60
  def record_when_condition(markup)
61
+ body = BlockBody.new
62
+
50
63
  while markup
51
- # Create a new nodelist and assign it to the new block
52
- if not markup =~ WhenSyntax
53
- raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
64
+ unless markup =~ WhenSyntax
65
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
54
66
  end
55
67
 
56
68
  markup = $2
57
69
 
58
- block = Condition.new(@left, '==', $1)
59
- block.attach(@nodelist)
60
- @blocks.push(block)
70
+ block = Condition.new(@left, '=='.freeze, Expression.parse($1))
71
+ block.attach(body)
72
+ @blocks << block
61
73
  end
62
74
  end
63
75
 
64
76
  def record_else_condition(markup)
65
-
66
- if not markup.strip.empty?
67
- raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
77
+ unless markup.strip.empty?
78
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
68
79
  end
69
80
 
70
81
  block = ElseCondition.new
71
- block.attach(@nodelist)
82
+ block.attach(BlockBody.new)
72
83
  @blocks << block
73
84
  end
74
85
 
75
-
86
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
87
+ def children
88
+ [@node.left] + @node.blocks
89
+ end
90
+ end
76
91
  end
77
92
 
78
- Template.register_tag('case', Case)
93
+ Template.register_tag('case'.freeze, Case)
79
94
  end