liquid-4-0-2 4.0.2

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 +7 -0
  2. data/History.md +235 -0
  3. data/LICENSE +20 -0
  4. data/README.md +108 -0
  5. data/lib/liquid.rb +80 -0
  6. data/lib/liquid/block.rb +77 -0
  7. data/lib/liquid/block_body.rb +142 -0
  8. data/lib/liquid/condition.rb +151 -0
  9. data/lib/liquid/context.rb +226 -0
  10. data/lib/liquid/document.rb +27 -0
  11. data/lib/liquid/drop.rb +78 -0
  12. data/lib/liquid/errors.rb +56 -0
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +74 -0
  15. data/lib/liquid/file_system.rb +73 -0
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +16 -0
  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 +485 -0
  30. data/lib/liquid/strainer.rb +66 -0
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +43 -0
  33. data/lib/liquid/tags/assign.rb +59 -0
  34. data/lib/liquid/tags/break.rb +18 -0
  35. data/lib/liquid/tags/capture.rb +38 -0
  36. data/lib/liquid/tags/case.rb +94 -0
  37. data/lib/liquid/tags/comment.rb +16 -0
  38. data/lib/liquid/tags/continue.rb +18 -0
  39. data/lib/liquid/tags/cycle.rb +65 -0
  40. data/lib/liquid/tags/decrement.rb +35 -0
  41. data/lib/liquid/tags/for.rb +203 -0
  42. data/lib/liquid/tags/if.rb +122 -0
  43. data/lib/liquid/tags/ifchanged.rb +18 -0
  44. data/lib/liquid/tags/include.rb +124 -0
  45. data/lib/liquid/tags/increment.rb +31 -0
  46. data/lib/liquid/tags/raw.rb +47 -0
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +30 -0
  49. data/lib/liquid/template.rb +254 -0
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/utils.rb +83 -0
  52. data/lib/liquid/variable.rb +148 -0
  53. data/lib/liquid/variable_lookup.rb +88 -0
  54. data/lib/liquid/version.rb +4 -0
  55. data/test/fixtures/en_locale.yml +9 -0
  56. data/test/integration/assign_test.rb +48 -0
  57. data/test/integration/blank_test.rb +106 -0
  58. data/test/integration/block_test.rb +12 -0
  59. data/test/integration/capture_test.rb +50 -0
  60. data/test/integration/context_test.rb +32 -0
  61. data/test/integration/document_test.rb +19 -0
  62. data/test/integration/drop_test.rb +273 -0
  63. data/test/integration/error_handling_test.rb +260 -0
  64. data/test/integration/filter_test.rb +178 -0
  65. data/test/integration/hash_ordering_test.rb +23 -0
  66. data/test/integration/output_test.rb +123 -0
  67. data/test/integration/parse_tree_visitor_test.rb +247 -0
  68. data/test/integration/parsing_quirks_test.rb +122 -0
  69. data/test/integration/render_profiling_test.rb +154 -0
  70. data/test/integration/security_test.rb +80 -0
  71. data/test/integration/standard_filter_test.rb +698 -0
  72. data/test/integration/tags/break_tag_test.rb +15 -0
  73. data/test/integration/tags/continue_tag_test.rb +15 -0
  74. data/test/integration/tags/for_tag_test.rb +410 -0
  75. data/test/integration/tags/if_else_tag_test.rb +188 -0
  76. data/test/integration/tags/include_tag_test.rb +245 -0
  77. data/test/integration/tags/increment_tag_test.rb +23 -0
  78. data/test/integration/tags/raw_tag_test.rb +31 -0
  79. data/test/integration/tags/standard_tag_test.rb +296 -0
  80. data/test/integration/tags/statements_test.rb +111 -0
  81. data/test/integration/tags/table_row_test.rb +64 -0
  82. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  83. data/test/integration/template_test.rb +332 -0
  84. data/test/integration/trim_mode_test.rb +529 -0
  85. data/test/integration/variable_test.rb +96 -0
  86. data/test/test_helper.rb +116 -0
  87. data/test/unit/block_unit_test.rb +58 -0
  88. data/test/unit/condition_unit_test.rb +166 -0
  89. data/test/unit/context_unit_test.rb +489 -0
  90. data/test/unit/file_system_unit_test.rb +35 -0
  91. data/test/unit/i18n_unit_test.rb +37 -0
  92. data/test/unit/lexer_unit_test.rb +51 -0
  93. data/test/unit/parser_unit_test.rb +82 -0
  94. data/test/unit/regexp_unit_test.rb +44 -0
  95. data/test/unit/strainer_unit_test.rb +164 -0
  96. data/test/unit/tag_unit_test.rb +21 -0
  97. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  98. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  99. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  100. data/test/unit/template_unit_test.rb +78 -0
  101. data/test/unit/tokenizer_unit_test.rb +55 -0
  102. data/test/unit/variable_unit_test.rb +162 -0
  103. metadata +224 -0
@@ -0,0 +1,16 @@
1
+ module Liquid
2
+ # An interrupt is any command that breaks processing of a block (ex: a for loop).
3
+ class Interrupt
4
+ attr_reader :message
5
+
6
+ def initialize(message = nil)
7
+ @message = message || "interrupt".freeze
8
+ end
9
+ end
10
+
11
+ # Interrupt that is thrown whenever a {% break %} is called.
12
+ class BreakInterrupt < Interrupt; end
13
+
14
+ # Interrupt that is thrown whenever a {% continue %} is called.
15
+ class ContinueInterrupt < Interrupt; end
16
+ end
@@ -0,0 +1,55 @@
1
+ require "strscan"
2
+ module Liquid
3
+ class Lexer
4
+ SPECIALS = {
5
+ '|'.freeze => :pipe,
6
+ '.'.freeze => :dot,
7
+ ':'.freeze => :colon,
8
+ ','.freeze => :comma,
9
+ '['.freeze => :open_square,
10
+ ']'.freeze => :close_square,
11
+ '('.freeze => :open_round,
12
+ ')'.freeze => :close_round,
13
+ '?'.freeze => :question,
14
+ '-'.freeze => :dash
15
+ }
16
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
+ SINGLE_STRING_LITERAL = /'[^\']*'/
18
+ DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
+ DOTDOT = /\.\./
21
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
22
+ WHITESPACE_OR_NOTHING = /\s*/
23
+
24
+ def initialize(input)
25
+ @ss = StringScanner.new(input)
26
+ end
27
+
28
+ def tokenize
29
+ @output = []
30
+
31
+ until @ss.eos?
32
+ @ss.skip(WHITESPACE_OR_NOTHING)
33
+ break if @ss.eos?
34
+ tok = case
35
+ when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
36
+ when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
37
+ when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
38
+ when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
39
+ when t = @ss.scan(IDENTIFIER) then [:id, t]
40
+ when t = @ss.scan(DOTDOT) then [:dotdot, t]
41
+ else
42
+ c = @ss.getch
43
+ if s = SPECIALS[c]
44
+ [s, c]
45
+ else
46
+ raise SyntaxError, "Unexpected character #{c}"
47
+ end
48
+ end
49
+ @output << tok
50
+ end
51
+
52
+ @output << [:end_of_string]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ ---
2
+ errors:
3
+ syntax:
4
+ tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
5
+ assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
6
+ capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
7
+ case: "Syntax Error in 'case' - Valid syntax: case [condition]"
8
+ case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
9
+ case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
10
+ cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
11
+ for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
12
+ for_invalid_in: "For loops require an 'in' clause"
13
+ for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
14
+ if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
15
+ include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
+ unknown_tag: "Unknown tag '%{tag}'"
17
+ invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
18
+ unexpected_else: "%{block_name} tag does not expect 'else' tag"
19
+ unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
20
+ tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
21
+ variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
22
+ tag_never_closed: "'%{block_name}' tag was never closed"
23
+ meta_syntax_error: "Liquid syntax error: #{e.message}"
24
+ table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
25
+ argument:
26
+ include: "Argument error in tag 'include' - Illegal template name"
@@ -0,0 +1,38 @@
1
+ module Liquid
2
+ class ParseContext
3
+ attr_accessor :locale, :line_number, :trim_whitespace, :depth
4
+ attr_reader :partial, :warnings, :error_mode
5
+
6
+ def initialize(options = {})
7
+ @template_options = options ? options.dup : {}
8
+ @locale = @template_options[:locale] ||= I18n.new
9
+ @warnings = []
10
+ self.depth = 0
11
+ self.partial = false
12
+ end
13
+
14
+ def [](option_key)
15
+ @options[option_key]
16
+ end
17
+
18
+ def partial=(value)
19
+ @partial = value
20
+ @options = value ? partial_options : @template_options
21
+ @error_mode = @options[:error_mode] || Template.error_mode
22
+ value
23
+ end
24
+
25
+ def partial_options
26
+ @partial_options ||= begin
27
+ dont_pass = @template_options[:include_options_blacklist]
28
+ if dont_pass == true
29
+ { locale: locale }
30
+ elsif dont_pass.is_a?(Array)
31
+ @template_options.reject { |k, v| dont_pass.include?(k) }
32
+ else
33
+ @template_options
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class ParseTreeVisitor
5
+ def self.for(node, callbacks = Hash.new(proc {}))
6
+ if defined?(node.class::ParseTreeVisitor)
7
+ node.class::ParseTreeVisitor
8
+ else
9
+ self
10
+ end.new(node, callbacks)
11
+ end
12
+
13
+ def initialize(node, callbacks)
14
+ @node = node
15
+ @callbacks = callbacks
16
+ end
17
+
18
+ def add_callback_for(*classes, &block)
19
+ callback = block
20
+ callback = ->(node, _) { yield node } if block.arity.abs == 1
21
+ callback = ->(_, _) { yield } if block.arity.zero?
22
+ classes.each { |klass| @callbacks[klass] = callback }
23
+ self
24
+ end
25
+
26
+ def visit(context = nil)
27
+ children.map do |node|
28
+ item, new_context = @callbacks[node.class].call(node, context)
29
+ [
30
+ item,
31
+ ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
32
+ ]
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def children
39
+ @node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ module Liquid
2
+ class Parser
3
+ def initialize(input)
4
+ l = Lexer.new(input)
5
+ @tokens = l.tokenize
6
+ @p = 0 # pointer to current location
7
+ end
8
+
9
+ def jump(point)
10
+ @p = point
11
+ end
12
+
13
+ def consume(type = nil)
14
+ token = @tokens[@p]
15
+ if type && token[0] != type
16
+ raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
17
+ end
18
+ @p += 1
19
+ token[1]
20
+ end
21
+
22
+ # Only consumes the token if it matches the type
23
+ # Returns the token's contents if it was consumed
24
+ # or false otherwise.
25
+ def consume?(type)
26
+ token = @tokens[@p]
27
+ return false unless token && token[0] == type
28
+ @p += 1
29
+ token[1]
30
+ end
31
+
32
+ # Like consume? Except for an :id token of a certain name
33
+ def id?(str)
34
+ token = @tokens[@p]
35
+ return false unless token && token[0] == :id
36
+ return false unless token[1] == str
37
+ @p += 1
38
+ token[1]
39
+ end
40
+
41
+ def look(type, ahead = 0)
42
+ tok = @tokens[@p + ahead]
43
+ return false unless tok
44
+ tok[0] == type
45
+ end
46
+
47
+ def expression
48
+ token = @tokens[@p]
49
+ if token[0] == :id
50
+ variable_signature
51
+ elsif [:string, :number].include? token[0]
52
+ consume
53
+ elsif token.first == :open_round
54
+ consume
55
+ first = expression
56
+ consume(:dotdot)
57
+ last = expression
58
+ consume(:close_round)
59
+ "(#{first}..#{last})"
60
+ else
61
+ raise SyntaxError, "#{token} is not a valid expression"
62
+ end
63
+ end
64
+
65
+ def argument
66
+ str = ""
67
+ # might be a keyword argument (identifier: expression)
68
+ if look(:id) && look(:colon, 1)
69
+ str << consume << consume << ' '.freeze
70
+ end
71
+
72
+ str << expression
73
+ str
74
+ end
75
+
76
+ def variable_signature
77
+ str = consume(:id)
78
+ while look(:open_square)
79
+ str << consume
80
+ str << expression
81
+ str << consume(:close_square)
82
+ end
83
+ if look(:dot)
84
+ str << consume
85
+ str << variable_signature
86
+ end
87
+ str
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ module ParserSwitching
3
+ def parse_with_selected_parser(markup)
4
+ case parse_context.error_mode
5
+ when :strict then strict_parse_with_error_context(markup)
6
+ when :lax then lax_parse(markup)
7
+ when :warn
8
+ begin
9
+ return strict_parse_with_error_context(markup)
10
+ rescue SyntaxError => e
11
+ parse_context.warnings << e
12
+ return lax_parse(markup)
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def strict_parse_with_error_context(markup)
20
+ strict_parse(markup)
21
+ rescue SyntaxError => e
22
+ e.line_number = line_number
23
+ e.markup_context = markup_context(markup)
24
+ raise e
25
+ end
26
+
27
+ def markup_context(markup)
28
+ "in \"#{markup.strip}\""
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,158 @@
1
+ require 'liquid/profiler/hooks'
2
+
3
+ module Liquid
4
+ # Profiler enables support for profiling template rendering to help track down performance issues.
5
+ #
6
+ # To enable profiling, first require 'liquid/profiler'.
7
+ # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
8
+ # After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
9
+ # class via the <tt>Liquid::Template#profiler</tt> method.
10
+ #
11
+ # template = Liquid::Template.parse(template_content, profile: true)
12
+ # output = template.render
13
+ # profile = template.profiler
14
+ #
15
+ # This object contains all profiling information, containing information on what tags were rendered,
16
+ # where in the templates these tags live, and how long each tag took to render.
17
+ #
18
+ # This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
19
+ # inside of <tt>{% include %}</tt> tags.
20
+ #
21
+ # profile.each do |node|
22
+ # # Access to the node itself
23
+ # node.code
24
+ #
25
+ # # Which template and line number of this node.
26
+ # # If top level, this will be "<root>".
27
+ # node.partial
28
+ # node.line_number
29
+ #
30
+ # # Render time in seconds of this node
31
+ # node.render_time
32
+ #
33
+ # # If the template used {% include %}, this node will also have children.
34
+ # node.children.each do |child2|
35
+ # # ...
36
+ # end
37
+ # end
38
+ #
39
+ # Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
40
+ #
41
+ # All render times are in seconds. There is a small performance hit when profiling is enabled.
42
+ #
43
+ class Profiler
44
+ include Enumerable
45
+
46
+ class Timing
47
+ attr_reader :code, :partial, :line_number, :children
48
+
49
+ def initialize(node, partial)
50
+ @code = node.respond_to?(:raw) ? node.raw : node
51
+ @partial = partial
52
+ @line_number = node.respond_to?(:line_number) ? node.line_number : nil
53
+ @children = []
54
+ end
55
+
56
+ def self.start(node, partial)
57
+ new(node, partial).tap(&:start)
58
+ end
59
+
60
+ def start
61
+ @start_time = Time.now
62
+ end
63
+
64
+ def finish
65
+ @end_time = Time.now
66
+ end
67
+
68
+ def render_time
69
+ @end_time - @start_time
70
+ end
71
+ end
72
+
73
+ def self.profile_node_render(node)
74
+ if Profiler.current_profile && node.respond_to?(:render)
75
+ Profiler.current_profile.start_node(node)
76
+ output = yield
77
+ Profiler.current_profile.end_node(node)
78
+ output
79
+ else
80
+ yield
81
+ end
82
+ end
83
+
84
+ def self.profile_children(template_name)
85
+ if Profiler.current_profile
86
+ Profiler.current_profile.push_partial(template_name)
87
+ output = yield
88
+ Profiler.current_profile.pop_partial
89
+ output
90
+ else
91
+ yield
92
+ end
93
+ end
94
+
95
+ def self.current_profile
96
+ Thread.current[:liquid_profiler]
97
+ end
98
+
99
+ def initialize
100
+ @partial_stack = ["<root>"]
101
+
102
+ @root_timing = Timing.new("", current_partial)
103
+ @timing_stack = [@root_timing]
104
+
105
+ @render_start_at = Time.now
106
+ @render_end_at = @render_start_at
107
+ end
108
+
109
+ def start
110
+ Thread.current[:liquid_profiler] = self
111
+ @render_start_at = Time.now
112
+ end
113
+
114
+ def stop
115
+ Thread.current[:liquid_profiler] = nil
116
+ @render_end_at = Time.now
117
+ end
118
+
119
+ def total_render_time
120
+ @render_end_at - @render_start_at
121
+ end
122
+
123
+ def each(&block)
124
+ @root_timing.children.each(&block)
125
+ end
126
+
127
+ def [](idx)
128
+ @root_timing.children[idx]
129
+ end
130
+
131
+ def length
132
+ @root_timing.children.length
133
+ end
134
+
135
+ def start_node(node)
136
+ @timing_stack.push(Timing.start(node, current_partial))
137
+ end
138
+
139
+ def end_node(_node)
140
+ timing = @timing_stack.pop
141
+ timing.finish
142
+
143
+ @timing_stack.last.children << timing
144
+ end
145
+
146
+ def current_partial
147
+ @partial_stack.last
148
+ end
149
+
150
+ def push_partial(partial_name)
151
+ @partial_stack.push(partial_name)
152
+ end
153
+
154
+ def pop_partial
155
+ @partial_stack.pop
156
+ end
157
+ end
158
+ end