twig_ruby 0.0.1 → 0.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 (168) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -0
  3. data/lib/tasks/twig_parity.rake +278 -0
  4. data/lib/twig/auto_hash.rb +7 -1
  5. data/lib/twig/callable.rb +28 -1
  6. data/lib/twig/compiler.rb +35 -3
  7. data/lib/twig/environment.rb +198 -41
  8. data/lib/twig/error/base.rb +81 -16
  9. data/lib/twig/error/loader.rb +8 -0
  10. data/lib/twig/error/logic.rb +8 -0
  11. data/lib/twig/error/runtime.rb +8 -0
  12. data/lib/twig/expression_parser/base.rb +30 -0
  13. data/lib/twig/expression_parser/expression_parsers.rb +57 -0
  14. data/lib/twig/expression_parser/infix/arrow.rb +31 -0
  15. data/lib/twig/expression_parser/infix/binary.rb +34 -0
  16. data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
  17. data/lib/twig/expression_parser/infix/dot.rb +72 -0
  18. data/lib/twig/expression_parser/infix/filter.rb +43 -0
  19. data/lib/twig/expression_parser/infix/function.rb +67 -0
  20. data/lib/twig/expression_parser/infix/is.rb +53 -0
  21. data/lib/twig/expression_parser/infix/is_not.rb +19 -0
  22. data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
  23. data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
  24. data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
  25. data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
  26. data/lib/twig/expression_parser/prefix/literal.rb +244 -0
  27. data/lib/twig/expression_parser/prefix/unary.rb +29 -0
  28. data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
  29. data/lib/twig/extension/base.rb +26 -4
  30. data/lib/twig/extension/core.rb +1076 -48
  31. data/lib/twig/extension/debug.rb +25 -0
  32. data/lib/twig/extension/escaper.rb +73 -0
  33. data/lib/twig/extension/rails.rb +10 -57
  34. data/lib/twig/extension/string_loader.rb +19 -0
  35. data/lib/twig/extension_set.rb +117 -20
  36. data/lib/twig/file_extension_escaping_strategy.rb +35 -0
  37. data/lib/twig/lexer.rb +225 -81
  38. data/lib/twig/loader/array.rb +25 -8
  39. data/lib/twig/loader/chain.rb +93 -0
  40. data/lib/twig/loader/filesystem.rb +106 -7
  41. data/lib/twig/node/auto_escape.rb +18 -0
  42. data/lib/twig/node/base.rb +58 -2
  43. data/lib/twig/node/block.rb +2 -0
  44. data/lib/twig/node/block_reference.rb +5 -1
  45. data/lib/twig/node/body.rb +7 -0
  46. data/lib/twig/node/cache.rb +50 -0
  47. data/lib/twig/node/capture.rb +22 -0
  48. data/lib/twig/node/deprecated.rb +53 -0
  49. data/lib/twig/node/do.rb +19 -0
  50. data/lib/twig/node/embed.rb +43 -0
  51. data/lib/twig/node/expression/array.rb +29 -20
  52. data/lib/twig/node/expression/arrow_function.rb +55 -0
  53. data/lib/twig/node/expression/assign_name.rb +1 -1
  54. data/lib/twig/node/expression/binary/and.rb +17 -0
  55. data/lib/twig/node/expression/binary/base.rb +6 -4
  56. data/lib/twig/node/expression/binary/boolean.rb +24 -0
  57. data/lib/twig/node/expression/binary/concat.rb +20 -0
  58. data/lib/twig/node/expression/binary/elvis.rb +35 -0
  59. data/lib/twig/node/expression/binary/ends_with.rb +24 -0
  60. data/lib/twig/node/expression/binary/floor_div.rb +21 -0
  61. data/lib/twig/node/expression/binary/has_every.rb +20 -0
  62. data/lib/twig/node/expression/binary/has_some.rb +20 -0
  63. data/lib/twig/node/expression/binary/in.rb +20 -0
  64. data/lib/twig/node/expression/binary/matches.rb +24 -0
  65. data/lib/twig/node/expression/binary/not_in.rb +20 -0
  66. data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
  67. data/lib/twig/node/expression/binary/or.rb +15 -0
  68. data/lib/twig/node/expression/binary/starts_with.rb +24 -0
  69. data/lib/twig/node/expression/binary/xor.rb +17 -0
  70. data/lib/twig/node/expression/block_reference.rb +62 -0
  71. data/lib/twig/node/expression/call.rb +126 -6
  72. data/lib/twig/node/expression/constant.rb +3 -1
  73. data/lib/twig/node/expression/filter/default.rb +37 -0
  74. data/lib/twig/node/expression/filter/raw.rb +31 -0
  75. data/lib/twig/node/expression/filter.rb +2 -2
  76. data/lib/twig/node/expression/function.rb +37 -0
  77. data/lib/twig/node/expression/get_attribute.rb +51 -7
  78. data/lib/twig/node/expression/hash.rb +75 -0
  79. data/lib/twig/node/expression/helper_method.rb +6 -18
  80. data/lib/twig/node/expression/macro_reference.rb +43 -0
  81. data/lib/twig/node/expression/name.rb +42 -8
  82. data/lib/twig/node/expression/operator_escape.rb +13 -0
  83. data/lib/twig/node/expression/parent.rb +28 -0
  84. data/lib/twig/node/expression/support_defined_test.rb +23 -0
  85. data/lib/twig/node/expression/ternary.rb +7 -1
  86. data/lib/twig/node/expression/test/base.rb +26 -0
  87. data/lib/twig/node/expression/test/constant.rb +35 -0
  88. data/lib/twig/node/expression/test/defined.rb +33 -0
  89. data/lib/twig/node/expression/test/divisible_by.rb +23 -0
  90. data/lib/twig/node/expression/test/even.rb +21 -0
  91. data/lib/twig/node/expression/test/iterable.rb +21 -0
  92. data/lib/twig/node/expression/test/mapping.rb +21 -0
  93. data/lib/twig/node/expression/test/null.rb +21 -0
  94. data/lib/twig/node/expression/test/odd.rb +21 -0
  95. data/lib/twig/node/expression/test/same_as.rb +23 -0
  96. data/lib/twig/node/expression/test/sequence.rb +21 -0
  97. data/lib/twig/node/expression/unary/base.rb +3 -1
  98. data/lib/twig/node/expression/unary/not.rb +18 -0
  99. data/lib/twig/node/expression/unary/spread.rb +18 -0
  100. data/lib/twig/node/expression/unary/string_cast.rb +18 -0
  101. data/lib/twig/node/expression/variable/assign_template.rb +35 -0
  102. data/lib/twig/node/expression/variable/local.rb +35 -0
  103. data/lib/twig/node/expression/variable/template.rb +54 -0
  104. data/lib/twig/node/for.rb +38 -8
  105. data/lib/twig/node/for_loop.rb +0 -22
  106. data/lib/twig/node/if.rb +4 -1
  107. data/lib/twig/node/import.rb +32 -0
  108. data/lib/twig/node/include.rb +38 -8
  109. data/lib/twig/node/macro.rb +79 -0
  110. data/lib/twig/node/module.rb +278 -23
  111. data/lib/twig/node/output.rb +7 -0
  112. data/lib/twig/node/print.rb +4 -1
  113. data/lib/twig/node/set.rb +72 -0
  114. data/lib/twig/node/text.rb +4 -1
  115. data/lib/twig/node/with.rb +50 -0
  116. data/lib/twig/node/yield.rb +6 -1
  117. data/lib/twig/node_traverser.rb +50 -0
  118. data/lib/twig/node_visitor/base.rb +30 -0
  119. data/lib/twig/node_visitor/escaper.rb +165 -0
  120. data/lib/twig/node_visitor/safe_analysis.rb +127 -0
  121. data/lib/twig/node_visitor/spreader.rb +39 -0
  122. data/lib/twig/output_buffer.rb +14 -12
  123. data/lib/twig/parser.rb +281 -8
  124. data/lib/twig/rails/config.rb +33 -0
  125. data/lib/twig/rails/engine.rb +44 -0
  126. data/lib/twig/rails/renderer.rb +41 -0
  127. data/lib/twig/runtime/argument_spreader.rb +46 -0
  128. data/lib/twig/runtime/context.rb +154 -0
  129. data/lib/twig/runtime/enumerable_hash.rb +51 -0
  130. data/lib/twig/runtime/escaper.rb +155 -0
  131. data/lib/twig/runtime/loop_context.rb +81 -0
  132. data/lib/twig/runtime/loop_iterator.rb +60 -0
  133. data/lib/twig/runtime/spread.rb +21 -0
  134. data/lib/twig/runtime_loader/base.rb +12 -0
  135. data/lib/twig/runtime_loader/factory.rb +23 -0
  136. data/lib/twig/template.rb +267 -14
  137. data/lib/twig/template_wrapper.rb +42 -0
  138. data/lib/twig/token.rb +28 -2
  139. data/lib/twig/token_parser/apply.rb +48 -0
  140. data/lib/twig/token_parser/auto_escape.rb +45 -0
  141. data/lib/twig/token_parser/base.rb +26 -0
  142. data/lib/twig/token_parser/block.rb +4 -4
  143. data/lib/twig/token_parser/cache.rb +31 -0
  144. data/lib/twig/token_parser/deprecated.rb +40 -0
  145. data/lib/twig/token_parser/do.rb +19 -0
  146. data/lib/twig/token_parser/embed.rb +62 -0
  147. data/lib/twig/token_parser/extends.rb +4 -3
  148. data/lib/twig/token_parser/for.rb +14 -9
  149. data/lib/twig/token_parser/from.rb +57 -0
  150. data/lib/twig/token_parser/guard.rb +65 -0
  151. data/lib/twig/token_parser/if.rb +9 -9
  152. data/lib/twig/token_parser/import.rb +29 -0
  153. data/lib/twig/token_parser/include.rb +2 -2
  154. data/lib/twig/token_parser/macro.rb +109 -0
  155. data/lib/twig/token_parser/set.rb +76 -0
  156. data/lib/twig/token_parser/use.rb +54 -0
  157. data/lib/twig/token_parser/with.rb +36 -0
  158. data/lib/twig/token_parser/yield.rb +7 -7
  159. data/lib/twig/token_stream.rb +23 -3
  160. data/lib/twig/twig_filter.rb +20 -0
  161. data/lib/twig/twig_function.rb +37 -0
  162. data/lib/twig/twig_test.rb +31 -0
  163. data/lib/twig/util/callable_arguments_extractor.rb +227 -0
  164. data/lib/twig_ruby.rb +21 -2
  165. metadata +148 -6
  166. data/lib/twig/context.rb +0 -64
  167. data/lib/twig/expression_parser.rb +0 -517
  168. data/lib/twig/railtie.rb +0 -60
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module NodeVisitor
5
+ class Escaper < Base
6
+ attr_reader :escaping_strategy
7
+
8
+ def initialize
9
+ super
10
+
11
+ @default_strategy = false
12
+ @status_stack = []
13
+ @blocks = {}
14
+ @safe_vars = []
15
+ @safe_analysis = SafeAnalysis.new
16
+ end
17
+
18
+ def enter_node(node, env)
19
+ case node
20
+ when Node::Module
21
+ if env.extension?(Extension::Escaper)
22
+ default_strategy = env.extension(Extension::Escaper).default_strategy(node.template_name)
23
+ @default_strategy = default_strategy if default_strategy
24
+ end
25
+
26
+ @blocks = {}
27
+ @safe_vars = []
28
+ when Node::AutoEscape
29
+ @status_stack << node.attributes[:value].then { |v| v.is_a?(String) ? v.to_sym : v }
30
+ when Node::Block
31
+ @status_stack << blocks.fetch(node.attributes[:name], need_escaping)
32
+ when Node::Import
33
+ @safe_vars << node.nodes[:var].nodes[:var].attributes[:name]
34
+ end
35
+
36
+ node
37
+ end
38
+
39
+ def leave_node(node, env)
40
+ if node.is_a?(Node::Module)
41
+ @default_strategy = false
42
+ @safe_vars = []
43
+ @blocks = {}
44
+ elsif node.is_a?(Node::Expression::Filter)
45
+ return pre_escape_filter_node(node, env)
46
+ elsif node.is_a?(Node::Print) && (type = need_escaping) != false
47
+ expression = node.nodes[:expr]
48
+
49
+ if expression.is_a?(Node::Expression::OperatorEscape)
50
+ escape_conditional(expression, env, type)
51
+ else
52
+ node.nodes[:expr] = escape_expression(expression, env, type)
53
+ end
54
+
55
+ return node
56
+ end
57
+
58
+ if node.is_a?(Node::AutoEscape) || node.is_a?(Node::Block)
59
+ @status_stack.pop
60
+ elsif node.is_a?(Node::BlockReference)
61
+ @blocks[node.attributes[:name]] = need_escaping
62
+ end
63
+
64
+ node
65
+ end
66
+
67
+ private
68
+
69
+ # @param [Node::Expression::Base, Node::Expression::OperatorEscape] expression
70
+ # @param [Environment] env
71
+ # @param [Symbol] type
72
+ def escape_conditional(expression, env, type)
73
+ expression.operand_names_to_escape.each do |name|
74
+ operand = expression.nodes[name]
75
+
76
+ if operand.is_a?(Node::Expression::OperatorEscape)
77
+ escape_conditional(operand, env, type)
78
+ else
79
+ expression.nodes[name] = escape_expression(operand, env, type)
80
+ end
81
+ end
82
+ end
83
+
84
+ # @return [SafeAnalysis]
85
+ attr_reader :safe_analysis
86
+
87
+ # @return [String, Boolean]
88
+ attr_reader :default_strategy
89
+
90
+ # @return [Hash{String => String, Boolean}]
91
+ attr_reader :blocks
92
+
93
+ # @return [Array]
94
+ attr_reader :status_stack
95
+
96
+ # @return [String, Boolean]
97
+ def need_escaping
98
+ unless status_stack.empty?
99
+ return status_stack.last
100
+ end
101
+
102
+ default_strategy || false
103
+ end
104
+
105
+ # @param [Node::Expression::Base] expression
106
+ # @param [Environment] env
107
+ # @param [Symbol] type
108
+ def escape_expression(expression, env, type)
109
+ safe_for?(type, expression, env) ? expression : get_escaper_filter(env, type, expression)
110
+ end
111
+
112
+ # @param [Node::Exression::Filter] filter
113
+ # @param [Environment] env
114
+ # @return [Node::Expression::Filter]
115
+ def pre_escape_filter_node(filter, env)
116
+ if (type = filter.attributes[:twig_callable].pre_escape).nil?
117
+ return filter
118
+ end
119
+
120
+ node = filter.nodes[:node]
121
+
122
+ if safe_for?(type, node, env)
123
+ return filter
124
+ end
125
+
126
+ filter.nodes[:node] = get_escaper_filter(env, type, node)
127
+
128
+ filter
129
+ end
130
+
131
+ # @param [Environment] env
132
+ # @param [Symbol] type
133
+ # @param [Node::Expression::Base] node
134
+ # @return [Node::Expression::Filter]
135
+ def get_escaper_filter(env, type, node)
136
+ line = node.lineno
137
+ filter = env.filter('escape')
138
+ args = Node::Nodes.new(
139
+ AutoHash.new.add(
140
+ Node::Expression::Constant.new(type, line),
141
+ Node::Expression::Constant.new(nil, line),
142
+ Node::Expression::Constant.new(true, line)
143
+ )
144
+ )
145
+
146
+ Node::Expression::Filter.new(node, filter, args, line)
147
+ end
148
+
149
+ def safe_for?(type, expression, env)
150
+ safe = safe_analysis.safe(expression)
151
+
152
+ if safe.empty?
153
+ @traverser ||= NodeTraverser.new(env, [safe_analysis])
154
+
155
+ safe_analysis.safe_vars = @safe_vars
156
+ @traverser.traverse(expression)
157
+
158
+ safe = safe_analysis.safe(expression)
159
+ end
160
+
161
+ safe.include?(type) || safe.include?(:all)
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module NodeVisitor
5
+ class SafeAnalysis < Base
6
+ SAFE_ALL = [
7
+ Node::Expression::Constant,
8
+ Node::Expression::BlockReference,
9
+ Node::Expression::Parent,
10
+ Node::Expression::MacroReference,
11
+ ].freeze
12
+
13
+ def initialize
14
+ super
15
+
16
+ @data = {}
17
+ @safe_vars = []
18
+ end
19
+
20
+ def enter_node(node, env)
21
+ node
22
+ end
23
+
24
+ def leave_node(node, env)
25
+ if SAFE_ALL.any? { |klass| node.is_a?(klass) }
26
+ set_safe(node, [:all])
27
+ elsif node.is_a?(Node::Expression::OperatorEscape)
28
+ operands = node.operand_names_to_escape
29
+
30
+ if operands.length > 2
31
+ raise ArgumentError, "Operators with more than 2 operands are not supported yet, got #{operands.length}."
32
+ elsif operands.length == 2
33
+ safe = intersect_safe(safe(node.nodes[operands[0]]), safe(node.nodes[operands[1]]))
34
+ set_safe(node, safe)
35
+ end
36
+ elsif node.is_a?(Node::Expression::Filter)
37
+ # Filter expression is safe when the filter is safe
38
+ if node.attributes.key?(:twig_callable) && (filter = node.attributes[:twig_callable])
39
+ if (safe = filter.safe(node.nodes[:arguments])).empty?
40
+ safe = intersect_safe(safe(node.nodes[:node]), filter.preserves_safety)
41
+ end
42
+
43
+ set_safe(node, safe)
44
+ end
45
+ elsif node.is_a?(Node::Expression::Function)
46
+ # Function expression is safe when the function is safe
47
+ if node.attributes.key?(:twig_callable) && (function = node.attributes[:twig_callable])
48
+ set_safe(node, function.safe(node.nodes[:arguments]))
49
+ else
50
+ set_safe(node, [])
51
+ end
52
+ elsif node.is_a?(Node::Expression::GetAttribute) && node.nodes[:node].is_a?(Node::Expression::Variable::Context)
53
+ name = node.nodes[:node].attributes[:name]
54
+
55
+ if safe_vars.include?(name)
56
+ set_safe(node, [:all])
57
+ end
58
+ end
59
+
60
+ node
61
+ end
62
+
63
+ # @param [Node::Base] node
64
+ def safe(node)
65
+ hash = node.object_id
66
+
67
+ unless data.key?(hash)
68
+ return []
69
+ end
70
+
71
+ data[hash].each do |bucket|
72
+ next unless bucket[:key] == node
73
+
74
+ if bucket[:value].include?(:html_attr)
75
+ bucket[:value] << :html
76
+ end
77
+
78
+ return bucket[:value]
79
+ end
80
+
81
+ []
82
+ end
83
+
84
+ # @param [Array<String>] safe_vars
85
+ def safe_vars=(safe_vars)
86
+ @safe_vars = safe_vars.dup
87
+ end
88
+
89
+ private
90
+
91
+ # @return [Hash{Integer => Array}]
92
+ attr_reader :data
93
+
94
+ # @return [Array<String>]
95
+ attr_reader :safe_vars
96
+
97
+ # @param [Node::Base] node
98
+ # @param [Array] safe
99
+ def set_safe(node, safe)
100
+ hash = node.object_id
101
+ found = data.fetch(hash, []).detect { |bucket| bucket[:key] == node }
102
+
103
+ if found
104
+ found[:value] = safe
105
+
106
+ return
107
+ end
108
+
109
+ @data[hash] ||= []
110
+ @data[hash] << {
111
+ key: node,
112
+ value: safe,
113
+ }
114
+ end
115
+
116
+ # @param [Array<String>] a
117
+ # @param [Array<String>] b
118
+ def intersect_safe(a, b)
119
+ return [] if a.empty? || b.empty?
120
+ return b if a.include?(:all)
121
+ return a if b.include?(:all)
122
+
123
+ a & b
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module NodeVisitor
5
+ # This visitor checks the contents of the Spread operator to see if it's a hash or array.
6
+ # If it is, it converts it to a HashSpread or ArraySpread node.
7
+ #
8
+ # The cases where this is not, would be some kind of variable expression which we cannot determine
9
+ # at compile time, and still need to be sent through the ArgumentSpreader so that they can be spread
10
+ # properly into the destination callable.
11
+ class Spreader < Base
12
+ def enter_node(node, env)
13
+ node
14
+ end
15
+
16
+ def leave_node(node, env)
17
+ unless node.is_a?(Node::Expression::Unary::Spread)
18
+ return node
19
+ end
20
+
21
+ if node.nodes[:node].instance_of?(Node::Expression::Hash)
22
+ return Node::Expression::Unary::HashSpread.new(
23
+ node.nodes[:node],
24
+ node.lineno
25
+ )
26
+ end
27
+
28
+ if node.nodes[:node].instance_of?(Node::Expression::Array)
29
+ return Node::Expression::Unary::ArraySpread.new(
30
+ node.nodes[:node],
31
+ node.lineno
32
+ )
33
+ end
34
+
35
+ node
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,29 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Twig
4
+ # Anything passed through the Twig OutputBuffer is already
5
+ # being checked for escaping issues, so we can mark everything
6
+ # html_safe here. When being used with Rails, we just pass everything
7
+ # through this buffer into the Rails safe buffer so we don't need a
8
+ # bunch of html_safe calls everywhere.
4
9
  class OutputBuffer
5
- def initialize
10
+ def initialize(decorated = nil)
11
+ @decorated = decorated
6
12
  @buffer = +''
7
13
  end
8
14
 
9
15
  def append=(string)
10
- unless string.nil?
11
- string = string.to_s
12
-
13
- @buffer << if string.html_safe?
14
- string
15
- else
16
- CGI.escapeHTML(string)
17
- end
18
- end
16
+ self.safe_append = string
19
17
  end
20
18
 
21
19
  def safe_append=(string)
22
- @buffer << string.html_safe
20
+ if @decorated
21
+ @decorated.safe_append = string
22
+ else
23
+ @buffer << string.to_s.html_safe
24
+ end
23
25
  end
24
26
 
25
27
  def to_s
26
- @buffer
28
+ @decorated || @buffer
27
29
  end
28
30
  end
29
31
  end