twig_ruby 0.0.1 → 0.0.2

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tasks/twig_parity.rake +278 -0
  3. data/lib/twig/auto_hash.rb +7 -1
  4. data/lib/twig/callable.rb +28 -1
  5. data/lib/twig/compiler.rb +35 -3
  6. data/lib/twig/environment.rb +198 -41
  7. data/lib/twig/error/base.rb +81 -16
  8. data/lib/twig/error/loader.rb +8 -0
  9. data/lib/twig/error/logic.rb +8 -0
  10. data/lib/twig/error/runtime.rb +8 -0
  11. data/lib/twig/expression_parser/base.rb +30 -0
  12. data/lib/twig/expression_parser/expression_parsers.rb +57 -0
  13. data/lib/twig/expression_parser/infix/arrow.rb +31 -0
  14. data/lib/twig/expression_parser/infix/binary.rb +34 -0
  15. data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
  16. data/lib/twig/expression_parser/infix/dot.rb +72 -0
  17. data/lib/twig/expression_parser/infix/filter.rb +43 -0
  18. data/lib/twig/expression_parser/infix/function.rb +67 -0
  19. data/lib/twig/expression_parser/infix/is.rb +53 -0
  20. data/lib/twig/expression_parser/infix/is_not.rb +19 -0
  21. data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
  22. data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
  23. data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
  24. data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
  25. data/lib/twig/expression_parser/prefix/literal.rb +244 -0
  26. data/lib/twig/expression_parser/prefix/unary.rb +29 -0
  27. data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
  28. data/lib/twig/extension/base.rb +26 -4
  29. data/lib/twig/extension/core.rb +1076 -48
  30. data/lib/twig/extension/debug.rb +25 -0
  31. data/lib/twig/extension/escaper.rb +73 -0
  32. data/lib/twig/extension/rails.rb +10 -57
  33. data/lib/twig/extension/string_loader.rb +19 -0
  34. data/lib/twig/extension_set.rb +117 -20
  35. data/lib/twig/file_extension_escaping_strategy.rb +35 -0
  36. data/lib/twig/lexer.rb +225 -81
  37. data/lib/twig/loader/array.rb +25 -8
  38. data/lib/twig/loader/chain.rb +93 -0
  39. data/lib/twig/loader/filesystem.rb +106 -7
  40. data/lib/twig/node/auto_escape.rb +18 -0
  41. data/lib/twig/node/base.rb +58 -2
  42. data/lib/twig/node/block.rb +2 -0
  43. data/lib/twig/node/block_reference.rb +5 -1
  44. data/lib/twig/node/body.rb +7 -0
  45. data/lib/twig/node/cache.rb +50 -0
  46. data/lib/twig/node/capture.rb +22 -0
  47. data/lib/twig/node/deprecated.rb +53 -0
  48. data/lib/twig/node/do.rb +19 -0
  49. data/lib/twig/node/embed.rb +43 -0
  50. data/lib/twig/node/expression/array.rb +29 -20
  51. data/lib/twig/node/expression/arrow_function.rb +55 -0
  52. data/lib/twig/node/expression/assign_name.rb +1 -1
  53. data/lib/twig/node/expression/binary/and.rb +17 -0
  54. data/lib/twig/node/expression/binary/base.rb +6 -4
  55. data/lib/twig/node/expression/binary/boolean.rb +24 -0
  56. data/lib/twig/node/expression/binary/concat.rb +20 -0
  57. data/lib/twig/node/expression/binary/elvis.rb +35 -0
  58. data/lib/twig/node/expression/binary/ends_with.rb +24 -0
  59. data/lib/twig/node/expression/binary/floor_div.rb +21 -0
  60. data/lib/twig/node/expression/binary/has_every.rb +20 -0
  61. data/lib/twig/node/expression/binary/has_some.rb +20 -0
  62. data/lib/twig/node/expression/binary/in.rb +20 -0
  63. data/lib/twig/node/expression/binary/matches.rb +24 -0
  64. data/lib/twig/node/expression/binary/not_in.rb +20 -0
  65. data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
  66. data/lib/twig/node/expression/binary/or.rb +15 -0
  67. data/lib/twig/node/expression/binary/starts_with.rb +24 -0
  68. data/lib/twig/node/expression/binary/xor.rb +17 -0
  69. data/lib/twig/node/expression/block_reference.rb +62 -0
  70. data/lib/twig/node/expression/call.rb +126 -6
  71. data/lib/twig/node/expression/constant.rb +3 -1
  72. data/lib/twig/node/expression/filter/default.rb +37 -0
  73. data/lib/twig/node/expression/filter/raw.rb +31 -0
  74. data/lib/twig/node/expression/filter.rb +2 -2
  75. data/lib/twig/node/expression/function.rb +37 -0
  76. data/lib/twig/node/expression/get_attribute.rb +51 -7
  77. data/lib/twig/node/expression/hash.rb +75 -0
  78. data/lib/twig/node/expression/helper_method.rb +6 -18
  79. data/lib/twig/node/expression/macro_reference.rb +43 -0
  80. data/lib/twig/node/expression/name.rb +42 -8
  81. data/lib/twig/node/expression/operator_escape.rb +13 -0
  82. data/lib/twig/node/expression/parent.rb +28 -0
  83. data/lib/twig/node/expression/support_defined_test.rb +23 -0
  84. data/lib/twig/node/expression/ternary.rb +7 -1
  85. data/lib/twig/node/expression/test/base.rb +26 -0
  86. data/lib/twig/node/expression/test/constant.rb +35 -0
  87. data/lib/twig/node/expression/test/defined.rb +33 -0
  88. data/lib/twig/node/expression/test/divisible_by.rb +23 -0
  89. data/lib/twig/node/expression/test/even.rb +21 -0
  90. data/lib/twig/node/expression/test/iterable.rb +21 -0
  91. data/lib/twig/node/expression/test/mapping.rb +21 -0
  92. data/lib/twig/node/expression/test/null.rb +21 -0
  93. data/lib/twig/node/expression/test/odd.rb +21 -0
  94. data/lib/twig/node/expression/test/same_as.rb +23 -0
  95. data/lib/twig/node/expression/test/sequence.rb +21 -0
  96. data/lib/twig/node/expression/unary/base.rb +3 -1
  97. data/lib/twig/node/expression/unary/not.rb +18 -0
  98. data/lib/twig/node/expression/unary/spread.rb +18 -0
  99. data/lib/twig/node/expression/unary/string_cast.rb +18 -0
  100. data/lib/twig/node/expression/variable/assign_template.rb +35 -0
  101. data/lib/twig/node/expression/variable/local.rb +35 -0
  102. data/lib/twig/node/expression/variable/template.rb +54 -0
  103. data/lib/twig/node/for.rb +38 -8
  104. data/lib/twig/node/for_loop.rb +0 -22
  105. data/lib/twig/node/if.rb +4 -1
  106. data/lib/twig/node/import.rb +32 -0
  107. data/lib/twig/node/include.rb +38 -8
  108. data/lib/twig/node/macro.rb +79 -0
  109. data/lib/twig/node/module.rb +278 -23
  110. data/lib/twig/node/output.rb +7 -0
  111. data/lib/twig/node/print.rb +4 -1
  112. data/lib/twig/node/set.rb +72 -0
  113. data/lib/twig/node/text.rb +4 -1
  114. data/lib/twig/node/with.rb +50 -0
  115. data/lib/twig/node/yield.rb +6 -1
  116. data/lib/twig/node_traverser.rb +50 -0
  117. data/lib/twig/node_visitor/base.rb +30 -0
  118. data/lib/twig/node_visitor/escaper.rb +165 -0
  119. data/lib/twig/node_visitor/safe_analysis.rb +127 -0
  120. data/lib/twig/node_visitor/spreader.rb +39 -0
  121. data/lib/twig/output_buffer.rb +14 -12
  122. data/lib/twig/parser.rb +281 -8
  123. data/lib/twig/rails/config.rb +33 -0
  124. data/lib/twig/rails/engine.rb +44 -0
  125. data/lib/twig/rails/renderer.rb +41 -0
  126. data/lib/twig/runtime/argument_spreader.rb +46 -0
  127. data/lib/twig/runtime/context.rb +154 -0
  128. data/lib/twig/runtime/enumerable_hash.rb +51 -0
  129. data/lib/twig/runtime/escaper.rb +155 -0
  130. data/lib/twig/runtime/loop_context.rb +81 -0
  131. data/lib/twig/runtime/loop_iterator.rb +60 -0
  132. data/lib/twig/runtime/spread.rb +21 -0
  133. data/lib/twig/runtime_loader/base.rb +12 -0
  134. data/lib/twig/runtime_loader/factory.rb +23 -0
  135. data/lib/twig/template.rb +267 -14
  136. data/lib/twig/template_wrapper.rb +42 -0
  137. data/lib/twig/token.rb +28 -2
  138. data/lib/twig/token_parser/apply.rb +48 -0
  139. data/lib/twig/token_parser/auto_escape.rb +45 -0
  140. data/lib/twig/token_parser/base.rb +26 -0
  141. data/lib/twig/token_parser/block.rb +4 -4
  142. data/lib/twig/token_parser/cache.rb +31 -0
  143. data/lib/twig/token_parser/deprecated.rb +40 -0
  144. data/lib/twig/token_parser/do.rb +19 -0
  145. data/lib/twig/token_parser/embed.rb +62 -0
  146. data/lib/twig/token_parser/extends.rb +4 -3
  147. data/lib/twig/token_parser/for.rb +14 -9
  148. data/lib/twig/token_parser/from.rb +57 -0
  149. data/lib/twig/token_parser/guard.rb +65 -0
  150. data/lib/twig/token_parser/if.rb +9 -9
  151. data/lib/twig/token_parser/import.rb +29 -0
  152. data/lib/twig/token_parser/include.rb +2 -2
  153. data/lib/twig/token_parser/macro.rb +109 -0
  154. data/lib/twig/token_parser/set.rb +76 -0
  155. data/lib/twig/token_parser/use.rb +54 -0
  156. data/lib/twig/token_parser/with.rb +36 -0
  157. data/lib/twig/token_parser/yield.rb +7 -7
  158. data/lib/twig/token_stream.rb +23 -3
  159. data/lib/twig/twig_filter.rb +20 -0
  160. data/lib/twig/twig_function.rb +37 -0
  161. data/lib/twig/twig_test.rb +31 -0
  162. data/lib/twig/util/callable_arguments_extractor.rb +227 -0
  163. data/lib/twig_ruby.rb +21 -2
  164. metadata +145 -6
  165. data/lib/twig/context.rb +0 -64
  166. data/lib/twig/expression_parser.rb +0 -517
  167. data/lib/twig/railtie.rb +0 -60
data/lib/twig/lexer.rb CHANGED
@@ -18,10 +18,16 @@ module Twig
18
18
  REGEX_EXPONENT = /[eE][+-]?#{REGEX_LNUM}/
19
19
  REGEX_DNUM = /#{REGEX_LNUM}(?:#{REGEX_FRAC})?/
20
20
 
21
- REGEX_NAME = /[a-zA-Z_][a-zA-Z0-9_]*/
21
+ REGEX_NAME = /[a-zA-Z_\u{007f}-\u{00ff}][a-zA-Z0-9_\u{007f}-\u{00ff}]*/u
22
22
  REGEX_SYMBOL = /:#{REGEX_NAME}/
23
- REGEX_STRING = /\A"([^#"\\]*(?:\\\\.[^#"\\]*)*)"|'([^'\\]*(?:\\\\.[^'\\]*)*)'/s
24
- REGEX_NUMBER = /\A(?:#{REGEX_DNUM}(?:#{REGEX_EXPONENT})?)/x
23
+ REGEX_CVAR = /@#{REGEX_NAME}/
24
+ REGEX_STRING = /\G"([^#"\\]*(?:\\.[^#"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/mu
25
+ REGEX_DQ_STRING_PART = /\G[^#"\\]*(?:(?:\.|#(?!\{))[^#"\\]*)*/mu
26
+ REGEX_INLINE_COMMENT = /#[^\n]*/
27
+ REGEX_DQ_STRING_DELIM = /\G"/
28
+ REGEX_INTERP_START = /\G#\{[[:space:]]*/
29
+ REGEX_INTERP_END = /\G[[:space:]]*\}/
30
+ REGEX_NUMBER = /\G(?:#{REGEX_DNUM}(?:#{REGEX_EXPONENT})?)/x
25
31
 
26
32
  STATE_DATA = 0
27
33
  STATE_BLOCK = 1
@@ -29,6 +35,14 @@ module Twig
29
35
  STATE_STRING = 3
30
36
  STATE_INTERPOLATION = 4
31
37
 
38
+ SPECIAL_CHARS = {
39
+ 'f' => "\f",
40
+ 'n' => "\n",
41
+ 'r' => "\r",
42
+ 't' => "\t",
43
+ 'v' => "\v",
44
+ }.freeze
45
+
32
46
  # @param [Environment] environment
33
47
  def initialize(environment)
34
48
  @environment = environment
@@ -56,6 +70,10 @@ module Twig
56
70
  lex_block
57
71
  when STATE_VAR
58
72
  lex_var
73
+ when STATE_STRING
74
+ lex_string
75
+ when STATE_INTERPOLATION
76
+ lex_interpolation
59
77
  else
60
78
  raise "Unknown state: #{@state}"
61
79
  end
@@ -91,17 +109,26 @@ module Twig
91
109
  # Push the template text first
92
110
  text = text_content = @code[@cursor, (position.begin(0) - @cursor)]
93
111
 
94
- # TODO: Trim
112
+ # trim?
113
+ char = @positions[@position][2]
114
+
115
+ if char == WHITESPACE_TRIM
116
+ text = text.rstrip
117
+ elsif char == WHITESPACE_LINE_TRIM
118
+ text = text.gsub(/[ \t\0\x0B]*\z/, '')
119
+ end
95
120
 
96
121
  push_token(Token::TEXT_TYPE, text)
97
122
  move_cursor(text_content + position.to_s)
98
123
 
99
124
  case @positions[@position][1]
125
+ when TAG_COMMENT[0]
126
+ lex_comment
100
127
  when TAG_BLOCK[0]
101
- if (match = @code[@cursor...].match(lex_block_raw_regex))
128
+ if (match = @code.match(lex_block_raw_regex, @cursor))
102
129
  move_cursor(match.to_s)
103
130
  lex_raw_data
104
- elsif (match = @code[@cursor...].match(lex_block_line_regex))
131
+ elsif (match = @code.match(lex_block_line_regex, @cursor))
105
132
  move_cursor(match[0].to_s)
106
133
  @lineno = match[1].to_i
107
134
  else
@@ -114,24 +141,24 @@ module Twig
114
141
  push_state(STATE_VAR)
115
142
  @current_var_block_line = @lineno
116
143
  else
117
- raise "Invalid start token #{@positions[@position]}"
144
+ raise Error::Syntax.new("Invalid start token #{@positions[@position]}", @lineno)
118
145
  end
119
146
  end
120
147
 
121
148
  def lex_raw_data
122
- unless (match = @code[@cursor...].match(lex_raw_data_regex))
123
- raise "Uexpected end of file. Unclosed 'verbatim' block"
149
+ unless (match = @code.match(lex_raw_data_regex, @cursor))
150
+ raise Error::Syntax.new("Uexpected end of file. Unclosed 'verbatim' block.", @lineno, @source)
124
151
  end
125
152
 
126
- text = @code[@cursor, match.begin(0)]
127
- move_cursor(@code[@cursor, (match.begin(0) + match.to_s.length)])
153
+ text = @code[@cursor, match.begin(0) - @cursor]
154
+ move_cursor(@code[@cursor, (text.length + match.to_s.length)])
128
155
 
129
156
  # trim
130
157
  if match[1]
131
158
  text = if match[1] == WHITESPACE_TRIM
132
- text.gsub(/ *$/, '') # space trim
133
- else
134
159
  text.rstrip # line trim
160
+ else
161
+ text.gsub(/[ \t\0\x0B]*\z/, '') # space trim
135
162
  end
136
163
  end
137
164
 
@@ -139,7 +166,7 @@ module Twig
139
166
  end
140
167
 
141
168
  def lex_block
142
- if @brackets.empty? && (match = @code[@cursor..].match(lex_block_regex))
169
+ if @brackets.empty? && (match = @code.match(lex_block_regex, @cursor))
143
170
  push_token(Token::BLOCK_END_TYPE)
144
171
  move_cursor(match.to_s)
145
172
  pop_state
@@ -149,7 +176,7 @@ module Twig
149
176
  end
150
177
 
151
178
  def lex_var
152
- match = @code[@cursor...].match(lex_var_regex)
179
+ match = @code.match(lex_var_regex, @cursor)
153
180
 
154
181
  if @brackets.empty? && match
155
182
  push_token(Token::VAR_END_TYPE)
@@ -161,75 +188,144 @@ module Twig
161
188
  end
162
189
 
163
190
  def lex_expression
164
- @code[@cursor..].match(/\A\s+/) do |match|
191
+ @code.match(/\G[[:space:]]+/, @cursor) do |match|
165
192
  move_cursor(match.to_s)
166
193
 
167
194
  if @cursor >= @end
168
- raise "Unclosed #{@state == STATE_BLOCK ? 'block' : 'variable'}"
195
+ raise Error::Syntax.new(
196
+ "Unclosed \"#{@state == STATE_BLOCK ? 'block' : 'variable'}\".",
197
+ @lineno,
198
+ @source
199
+ )
169
200
  end
170
201
  end
171
202
 
172
- # Spread operator
173
- if code_at?(0, '.') && (@cursor + 2 < @end) && code_at?(1, '.') && code_at?(2, '.')
174
- push_token(Token::SPREAD_TYPE)
175
- move_cursor('...')
176
- # Arrow function
177
- elsif code_at?(0, '=') && (@cursor + 1 < @end) && code_at?(1, '>')
178
- push_token(Token::ARROW_TYPE)
179
- move_cursor('=>')
180
- elsif (match = @code[@cursor..].match(operator_regex))
181
- push_token(Token::OPERATOR_TYPE, match.to_s.gsub('/\s+/', ' '))
203
+ if (match = @code.match(operator_regex, @cursor))
204
+ operator = match.to_s.gsub(/[[:space:]]+/, ' ')
205
+
206
+ if OPENING_BRACKET.include?(operator)
207
+ check_brackets(operator)
208
+ end
209
+
210
+ push_token(Token::OPERATOR_TYPE, operator)
182
211
  move_cursor(match.to_s)
183
- elsif (match = @code[@cursor..].match(/\A#{REGEX_NAME}/))
212
+ elsif (match = @code.match(/\G#{REGEX_NAME}(?:\?(?!\?))?/, @cursor)) # Optional ? but not ??
184
213
  push_token(Token::NAME_TYPE, match.to_s)
185
214
  move_cursor(match.to_s)
186
- elsif (match = @code[@cursor..].match(/\A#{REGEX_SYMBOL}/))
215
+ elsif (match = @code.match(/\G#{REGEX_SYMBOL}/, @cursor))
187
216
  push_token(Token::SYMBOL_TYPE, match.to_s[1..])
188
217
  move_cursor(match.to_s)
189
- elsif (match = @code[@cursor..].match(REGEX_NUMBER))
218
+ elsif (match = @code.match(/\G#{REGEX_CVAR}/, @cursor))
219
+ push_token(Token::CLASS_VAR_TYPE, match.to_s)
220
+ move_cursor(match.to_s)
221
+ elsif (match = @code.match(REGEX_NUMBER, @cursor))
190
222
  value = match.to_s.tr('_', '')
191
223
  value = value.to_i.to_s == value ? value.to_i : value.to_f
192
224
  push_token(Token::NUMBER_TYPE, value)
193
225
  move_cursor(match.to_s)
194
226
  elsif code_at?(0, PUNCTUATION)
195
- # opening bracket
196
- if code_at?(0, OPENING_BRACKET)
197
- @brackets << [code_at, @lineno]
198
- elsif code_at?(0, CLOSING_BRACKET)
199
- if @brackets.empty?
200
- raise Error::Syntax.new("Unexpected closing bracket: #{code_at}", @lineno, @source)
201
- end
227
+ check_brackets(code_at)
228
+ push_token(Token::PUNCTUATION_TYPE, code_at)
229
+ @cursor += 1
230
+ elsif (match = @code.match(REGEX_STRING, @cursor))
231
+ push_token(Token::STRING_TYPE, stripcslashes(match.to_s[1...-1], match.to_s[0]))
232
+ move_cursor(match.to_s)
233
+ elsif (match = @code.match(REGEX_DQ_STRING_DELIM, @cursor))
234
+ @brackets << ['"', @lineno]
235
+ push_state(STATE_STRING)
236
+ move_cursor(match.to_s)
237
+ elsif (match = @code.match(REGEX_INLINE_COMMENT, @cursor))
238
+ move_cursor(match.to_s)
239
+ else
240
+ raise Error::Syntax.new("Unexpected character '#{code_at}'", @lineno, @source)
241
+ end
242
+ end
202
243
 
203
- expect, lineno = @brackets.pop
244
+ def lex_comment
245
+ unless (match = @code.match(lex_comment_regex, @cursor))
246
+ raise Error::Syntax.new('Unclosed comment.', @lineno, @source)
247
+ end
204
248
 
205
- unless code_at?(0, expect.tr(OPENING_BRACKET.join, CLOSING_BRACKET.join))
206
- raise Error::Syntax.new("Unclosed bracket: #{code_at}", lineno, @source)
207
- end
249
+ move_cursor(@code[@cursor...match.offset(0)[1]])
250
+ end
251
+
252
+ def lex_string
253
+ if (match = @code.match(REGEX_INTERP_START, @cursor))
254
+ @brackets << ['#{', @lineno]
255
+ push_token(Token::INTERPOLATION_START_TYPE)
256
+ move_cursor(match.to_s)
257
+ push_state(STATE_INTERPOLATION)
258
+ elsif (match = @code.match(REGEX_DQ_STRING_PART, @cursor)) && match.to_s != ''
259
+ push_token(Token::STRING_TYPE, stripcslashes(match.to_s, '"'))
260
+ move_cursor(match.to_s)
261
+ elsif @code.match(REGEX_DQ_STRING_DELIM, @cursor)
262
+ expect, lineno = @brackets.pop
263
+
264
+ unless code_at?(0, '"')
265
+ raise Error::Syntax.new("Unclosed '#{expect}'.", lineno, @source)
208
266
  end
209
267
 
210
- push_token(Token::PUNCTUATION_TYPE, code_at)
268
+ pop_state
211
269
  @cursor += 1
212
- elsif (match = @code[@cursor..].match(REGEX_STRING))
213
- push_token(Token::STRING_TYPE, match.to_s[1...-1])
214
- move_cursor(match.to_s)
270
+ else
271
+ Error::Syntax.new("Unexpected character '#{code_at}'.", @lineno, @source)
272
+ end
273
+ end
274
+
275
+ # @return [String]
276
+ def stripcslashes(str, quote_type)
277
+ result = ''
278
+ length = str.length
279
+
280
+ i = 0
281
+ while i < length
282
+ pos = str.index('\\', i)
283
+
284
+ unless pos
285
+ result += str[i..]
286
+ break
287
+ end
288
+
289
+ result += str[i...pos]
290
+ i = pos + 1
291
+
292
+ if i >= length
293
+ result += '\\'
294
+ break
295
+ end
296
+
297
+ next_char = str[i]
298
+
299
+ if SPECIAL_CHARS.key?(next_char)
300
+ result += SPECIAL_CHARS[next_char]
301
+ elsif next_char == '\\' || next_char == quote_type
302
+ result += next_char
303
+ elsif next_char == '#' && i + 1 < length && str[i + 1] == '{'
304
+ result += '#{'
305
+ i += 1
306
+ elsif next_char == 'x' && i + 1 < length && str[i + 1].match?(/[0-9a-fA-F]/)
307
+ hex = str[i + 1]
308
+ i += 1
309
+ if i + 1 < length && str[i + 1].match?(/[0-9a-fA-F]/)
310
+ hex += str[i + 1]
311
+ i += 1
312
+ end
313
+ result += [hex.to_i(16)].pack('C')
314
+ elsif next_char.match?(/[0-7]/) # octal
315
+ octal = next_char
316
+ while i + 1 < length && str[i + 1].match?(/[0-7]/) && octal.length < 3
317
+ octal += str[i + 1]
318
+ i += 1
319
+ end
320
+ result += [octal.to_i(8)].pack('C')
321
+ else
322
+ result += "\\#{next_char}"
323
+ end
324
+
325
+ i += 1
215
326
  end
216
327
 
217
- <<-TEMP
218
- // opening double quoted string
219
- elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
220
- $this->brackets[] = ['"', $this->lineno];
221
- $this->pushState(self::STATE_STRING);
222
- $this->moveCursor($match[0]);
223
- }
224
- // inline comment
225
- elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
226
- $this->moveCursor($match[0]);
227
- }
228
- // unlexable
229
- else {
230
- throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
231
- }
232
- TEMP
328
+ result
233
329
  end
234
330
 
235
331
  def push_token(type, value = '')
@@ -238,6 +334,18 @@ module Twig
238
334
  @tokens << Token.new(type, value, @lineno)
239
335
  end
240
336
 
337
+ def lex_interpolation
338
+ bracket = @brackets.last
339
+ if bracket[0] == '#{' && (match = @code.match(REGEX_INTERP_END, @cursor))
340
+ @brackets.pop
341
+ push_token(Token::INTERPOLATION_END_TYPE)
342
+ move_cursor(match.to_s)
343
+ pop_state
344
+ else
345
+ lex_expression
346
+ end
347
+ end
348
+
241
349
  def push_state(state)
242
350
  @states << @state
243
351
  @state = state
@@ -287,36 +395,47 @@ module Twig
287
395
 
288
396
  def lex_var_regex
289
397
  @lex_var_regex ||=
290
- /\A\s*(?:
398
+ /\G[[:space:]]*(?:
291
399
  #{Regexp.union(
292
- "#{WHITESPACE_TRIM}#{TAG_VARIABLE[1]}\\s*",
293
- WHITESPACE_LINE_TRIM + TAG_VARIABLE[1] + "[#{WHITESPACE_LINE_CHARS}]*",
400
+ /#{WHITESPACE_TRIM}#{TAG_VARIABLE[1]}[[:space:]]*/,
401
+ /#{WHITESPACE_LINE_TRIM}#{TAG_VARIABLE[1]}[#{WHITESPACE_LINE_CHARS}]*/,
294
402
  TAG_VARIABLE[1]
295
403
  )}
296
404
  )/x
297
405
  end
298
406
 
407
+ def lex_comment_regex
408
+ @lex_comment_regex ||=
409
+ /
410
+ (?:#{Regexp.union(
411
+ /#{WHITESPACE_TRIM}#{TAG_COMMENT[1]}[[:space:]]*\n?/,
412
+ /#{WHITESPACE_LINE_TRIM}#{TAG_COMMENT[1]}[#{WHITESPACE_LINE_CHARS}]*/,
413
+ /#{TAG_COMMENT[1]}\n?/
414
+ )})
415
+ /mxu
416
+ end
417
+
299
418
  def lex_block_raw_regex
300
419
  @lex_block_raw_regex ||=
301
- /\A\s*verbatim\s*(?:
420
+ /\G[[:space:]]*verbatim[[:space:]]*(?:
302
421
  #{Regexp.union(
303
- "#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\\s*",
304
- WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
422
+ /#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}[[:space:]]*/,
423
+ /#{WHITESPACE_LINE_TRIM}#{TAG_BLOCK[1]}[#{WHITESPACE_LINE_CHARS}]*/,
305
424
  TAG_BLOCK[1]
306
425
  )}
307
- )/sx
426
+ )/mxu
308
427
  end
309
428
 
310
429
  def lex_block_line_regex
311
- @lex_block_line_regex ||= /\A\s*line\s+(\d+)\s*#{Regexp.escape(TAG_BLOCK[1])}/s
430
+ @lex_block_line_regex ||= /\G[[:space:]]*line[[:space:]]+(\d+)[[:space:]]*#{Regexp.escape(TAG_BLOCK[1])}/mu
312
431
  end
313
432
 
314
433
  def lex_block_regex
315
434
  @lex_block_regex ||=
316
- /\A\s*(?:
435
+ /\G[[:space:]]*(?:
317
436
  #{Regexp.union(
318
- /#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\s*\n?/,
319
- WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
437
+ /#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}[[:space:]]*\n?/,
438
+ /#{WHITESPACE_LINE_TRIM}#{TAG_BLOCK[1]}[#{WHITESPACE_LINE_CHARS}]*/,
320
439
  /#{TAG_BLOCK[1]}\n?/
321
440
  )}
322
441
  )/x
@@ -326,20 +445,25 @@ module Twig
326
445
  @lex_raw_data_regex ||=
327
446
  /
328
447
  #{TAG_BLOCK[0]}
329
- (#{Regexp.union(WHITESPACE_TRIM, WHITESPACE_LINE_TRIM)})?\s*endverbatim\s*
448
+ (#{Regexp.union(WHITESPACE_TRIM, WHITESPACE_LINE_TRIM)})?[[:space:]]*endverbatim[[:space:]]*
330
449
  (?:#{Regexp.union(
331
- "#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\\s*",
332
- WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
450
+ /#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}[[:space:]]*/,
451
+ /#{WHITESPACE_LINE_TRIM}#{TAG_BLOCK[1]}[#{WHITESPACE_LINE_CHARS}]*/,
333
452
  TAG_BLOCK[1]
334
453
  )})
335
- /sx
454
+ /xu
336
455
  end
337
456
 
338
457
  def operator_regex
339
458
  return @operator_regex if defined?(@operator_regex)
340
459
 
341
- unary, binary = @environment.operators
342
- operators = ([:'='] + unary.keys + binary.keys).
460
+ expression_parsers = ['=']
461
+
462
+ environment.expression_parsers.each do |expression_parser|
463
+ expression_parsers.concat([expression_parser.name], expression_parser.aliases)
464
+ end
465
+
466
+ operators = expression_parsers.
343
467
  to_h { |op| [op, op.length] }.
344
468
  sort_by { |_, length| -length }.
345
469
  to_h
@@ -352,7 +476,7 @@ module Twig
352
476
  # an operator that ends with a character must be followed by
353
477
  # a whitespace, a parenthesis, an opening map [ or sequence {
354
478
  if operator[-1].match(/\w/)
355
- regex << '(?=[\s()\[{])'
479
+ regex << '(?=[[[:space:]]()\[{])'
356
480
  end
357
481
 
358
482
  # an operator that begins with a character must not have a dot or pipe before
@@ -361,12 +485,32 @@ module Twig
361
485
  end
362
486
 
363
487
  # an operator with a space can be any amount of whitespaces
364
- regex.gsub!(/\s+/, '\s+')
488
+ regex.gsub!(/(\\\s)+/, '[[:space:]]+')
365
489
 
366
490
  chain << regex
367
491
  end
368
492
 
369
- @operator_regex = Regexp.new("\\A(?:#{chain.join('|')})")
493
+ @operator_regex = Regexp.new("\\G(?:#{chain.join('|')})")
370
494
  end
495
+
496
+ # @param [String]
497
+ def check_brackets(code)
498
+ if OPENING_BRACKET.include?(code)
499
+ @brackets << [code, @lineno]
500
+ elsif CLOSING_BRACKET.include?(code)
501
+ if @brackets.empty?
502
+ raise Error::Syntax.new("Unexpected \"#{code}\".", @lineno, @source)
503
+ end
504
+
505
+ expect, lineno = @brackets.pop
506
+
507
+ unless code_at?(0, expect.tr(OPENING_BRACKET.join, CLOSING_BRACKET.join))
508
+ raise Error::Syntax.new("Unclosed \"#{expect}\".", lineno, @source)
509
+ end
510
+ end
511
+ end
512
+
513
+ # @return [Environment]
514
+ attr_reader :environment
371
515
  end
372
516
  end
@@ -7,33 +7,50 @@ module Twig
7
7
  def initialize(templates)
8
8
  super()
9
9
 
10
- @templates = templates.transform_keys(&:to_sym)
10
+ @templates = templates.transform_keys { |name| normalize_name(name) }
11
11
  end
12
12
 
13
13
  def get_source_context(name)
14
- name = name.to_sym
15
- raise "LoaderError: Template #{name} is not defined" unless @templates[name]
14
+ name = normalize_name(name)
15
+
16
+ unless @templates[name]
17
+ raise Error::Loader, "Template \"#{name}\" is not defined."
18
+ end
16
19
 
17
20
  ::Twig::Source.new(@templates[name], name)
18
21
  end
19
22
 
20
23
  def exists?(name)
21
- @templates.key?(name.to_sym)
24
+ name = normalize_name(name)
25
+
26
+ @templates.key?(name)
22
27
  end
23
28
 
24
29
  def get_cache_key(name)
25
- name = name.to_sym
26
- raise "LoaderError: Template #{name} is not defined" unless @templates[name]
30
+ name = normalize_name(name)
31
+
32
+ unless @templates[name]
33
+ raise Error::Loader, "Template \"#{name}\" is not defined."
34
+ end
27
35
 
28
36
  "#{name}:#{@templates[name]}"
29
37
  end
30
38
 
31
39
  def fresh?(name, time)
32
- name = name.to_sym
33
- raise "LoaderError: Template #{name} is not defined" unless @templates[name]
40
+ name = normalize_name(name)
41
+
42
+ unless @templates[name]
43
+ raise Error::Loader, "Template \"#{name}\" is not defined."
44
+ end
34
45
 
35
46
  true
36
47
  end
48
+
49
+ private
50
+
51
+ def normalize_name(name)
52
+ name.to_s
53
+ end
37
54
  end
38
55
  end
39
56
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Loader
5
+ class Chain < Loader::Base
6
+ # @param [Array<Loader::Base>] loaders
7
+ def initialize(loaders)
8
+ super()
9
+
10
+ @loaders = loaders
11
+ end
12
+
13
+ def get_source_context(name)
14
+ exceptions = []
15
+
16
+ loaders.each do |loader|
17
+ unless loader.exists?(name)
18
+ next
19
+ end
20
+
21
+ begin
22
+ return loader.get_source_context(name)
23
+ rescue Twig::Error::Loader => e
24
+ exceptions << "#{loader.class.name}: #{e.message}"
25
+ end
26
+ end
27
+
28
+ exceptions = exceptions.any? ? "(#{exceptions.join(', ')})" : ''
29
+
30
+ raise Twig::Error::Loader, "Template \"#{name}\" is not defined#{exceptions}."
31
+ end
32
+
33
+ def exists?(name)
34
+ @has_source_cache ||= {}
35
+
36
+ return @has_source_cache[name] if @has_source_cache.key?(name)
37
+
38
+ loaders.each do |loader|
39
+ if loader.exists?(name)
40
+ return @has_source_cache[name] = true
41
+ end
42
+ end
43
+
44
+ @has_source_cache[name] = false
45
+ end
46
+
47
+ def get_cache_key(name)
48
+ exceptions = []
49
+
50
+ loaders.each do |loader|
51
+ unless loader.exists?(name)
52
+ next
53
+ end
54
+
55
+ begin
56
+ return loader.get_cache_key(name)
57
+ rescue Twig::Error::Loader => e
58
+ exceptions << "#{loader.class.name}: #{e.message}"
59
+ end
60
+ end
61
+
62
+ exceptions = exceptions.any? ? "(#{exceptions.join(', ')})" : ''
63
+
64
+ raise Twig::Error::Loader, "Template \"#{name}\" is not defined#{exceptions}."
65
+ end
66
+
67
+ def fresh?(name, time)
68
+ exceptions = []
69
+
70
+ loaders.each do |loader|
71
+ unless loader.exists?(name)
72
+ next
73
+ end
74
+
75
+ begin
76
+ return loader.fresh?(name, time)
77
+ rescue Twig::Error::Loader => e
78
+ exceptions << "#{loader.class.name}: #{e.message}"
79
+ end
80
+ end
81
+
82
+ exceptions = exceptions.any? ? "(#{exceptions.join(', ')})" : ''
83
+
84
+ raise Twig::Error::Loader, "Template \"#{name}\" is not defined#{exceptions}."
85
+ end
86
+
87
+ private
88
+
89
+ # @return [::Array<Loader::Base>]
90
+ attr_accessor :loaders
91
+ end
92
+ end
93
+ end