flavour_saver 0.3.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +81 -0
  6. data/Guardfile +12 -0
  7. data/LICENSE +22 -0
  8. data/README.md +339 -0
  9. data/Rakefile +2 -0
  10. data/flavour_saver.gemspec +28 -0
  11. data/lib/flavour_saver/helpers.rb +118 -0
  12. data/lib/flavour_saver/lexer.rb +127 -0
  13. data/lib/flavour_saver/nodes.rb +177 -0
  14. data/lib/flavour_saver/parser.rb +183 -0
  15. data/lib/flavour_saver/partial.rb +29 -0
  16. data/lib/flavour_saver/rails_partial.rb +10 -0
  17. data/lib/flavour_saver/runtime.rb +269 -0
  18. data/lib/flavour_saver/template.rb +19 -0
  19. data/lib/flavour_saver/version.rb +3 -0
  20. data/lib/flavour_saver.rb +78 -0
  21. data/spec/acceptance/backtrack_spec.rb +14 -0
  22. data/spec/acceptance/comment_spec.rb +12 -0
  23. data/spec/acceptance/custom_block_helper_spec.rb +35 -0
  24. data/spec/acceptance/custom_helper_spec.rb +15 -0
  25. data/spec/acceptance/ensure_no_rce_spec.rb +26 -0
  26. data/spec/acceptance/handlebars_qunit_spec.rb +911 -0
  27. data/spec/acceptance/if_else_spec.rb +17 -0
  28. data/spec/acceptance/multi_level_with_spec.rb +15 -0
  29. data/spec/acceptance/one_character_identifier_spec.rb +13 -0
  30. data/spec/acceptance/runtime_run_spec.rb +27 -0
  31. data/spec/acceptance/sections_spec.rb +25 -0
  32. data/spec/acceptance/segment_literals_spec.rb +26 -0
  33. data/spec/acceptance/simple_expression_spec.rb +13 -0
  34. data/spec/fixtures/backtrack.hbs +4 -0
  35. data/spec/fixtures/comment.hbs +1 -0
  36. data/spec/fixtures/custom_block_helper.hbs +3 -0
  37. data/spec/fixtures/custom_helper.hbs +1 -0
  38. data/spec/fixtures/if_else.hbs +5 -0
  39. data/spec/fixtures/multi_level_if.hbs +12 -0
  40. data/spec/fixtures/multi_level_with.hbs +11 -0
  41. data/spec/fixtures/one_character_identifier.hbs +1 -0
  42. data/spec/fixtures/sections.hbs +9 -0
  43. data/spec/fixtures/simple_expression.hbs +1 -0
  44. data/spec/lib/flavour_saver/lexer_spec.rb +187 -0
  45. data/spec/lib/flavour_saver/parser_spec.rb +277 -0
  46. data/spec/lib/flavour_saver/runtime_spec.rb +190 -0
  47. data/spec/lib/flavour_saver/template_spec.rb +5 -0
  48. metadata +243 -0
@@ -0,0 +1,183 @@
1
+ require 'rltk'
2
+ require 'rltk/ast'
3
+ require 'flavour_saver/nodes'
4
+
5
+ module FlavourSaver
6
+ class Parser < RLTK::Parser
7
+
8
+ class UnbalancedBlockError < StandardError; end
9
+
10
+ class Environment < RLTK::Parser::Environment
11
+ def push_block block
12
+ blocks.push(block.name)
13
+ block
14
+ end
15
+
16
+ def pop_block block
17
+ b = blocks.pop
18
+ raise UnbalancedBlockError, "Unable to find matching opening for {{/#{block.name}}}" if b != block.name
19
+ block
20
+ end
21
+
22
+ def blocks
23
+ @blocks ||= []
24
+ end
25
+ end
26
+
27
+ left :DOT
28
+ right :EQ
29
+
30
+ production(:template) do
31
+ clause('template_items') { |i| TemplateNode.new(i) }
32
+ clause('') { TemplateNode.new([]) }
33
+ end
34
+
35
+ # empty_list(:template_items, [:output, :expression], 'WHITE?')
36
+ production(:template_items) do
37
+ clause('template_item') { |i| [i] }
38
+ clause('template_items template_item') { |i0,i1| i0 << i1 }
39
+ end
40
+
41
+ production(:template_item) do
42
+ clause('output') { |e| e }
43
+ clause('expression') { |e| e }
44
+ end
45
+
46
+ production(:output) do
47
+ clause('OUT') { |o| OutputNode.new(o) }
48
+ end
49
+
50
+ production(:expression) do
51
+ clause('block_expression') { |e| e }
52
+ clause('expr') { |e| ExpressionNode.new(e) }
53
+ clause('expr_comment') { |e| CommentNode.new(e) }
54
+ clause('expr_safe') { |e| SafeExpressionNode.new(e) }
55
+ clause('partial') { |e| e }
56
+ end
57
+
58
+ production(:partial) do
59
+ clause('EXPRST WHITE? GT WHITE? IDENT WHITE? EXPRE') { |_,_,_,_,e,_,_| PartialNode.new(e,[]) }
60
+ clause('EXPRST WHITE? GT WHITE? IDENT WHITE? call WHITE? EXPRE') { |_,_,_,_,e0,_,e1,_,_| PartialNode.new(e0,e1,nil) }
61
+ clause('EXPRST WHITE? GT WHITE? IDENT WHITE? lit WHITE? EXPRE') { |_,_,_,_,e0,_,e1,_,_| PartialNode.new(e0,[],e1) }
62
+ clause('EXPRST WHITE? GT WHITE? LITERAL WHITE? EXPRE') { |_,_,_,_,e,_,_| PartialNode.new(e,[]) }
63
+ clause('EXPRST WHITE? GT WHITE? LITERAL WHITE? call WHITE? EXPRE') { |_,_,_,_,e0,_,e1,_,_| PartialNode.new(e0,e1,nil) }
64
+ clause('EXPRST WHITE? GT WHITE? LITERAL WHITE? lit WHITE? EXPRE') { |_,_,_,_,e0,_,e1,_,_| PartialNode.new(e0,[],e1) }
65
+ end
66
+
67
+ production(:block_expression) do
68
+ clause('expr_bl_start template expr_else template expr_bl_end') { |e0,e1,_,e3,e2| BlockExpressionNodeWithElse.new([e0], e1,e2,e3) }
69
+ clause('expr_bl_start template expr_bl_end') { |e0,e1,e2| BlockExpressionNode.new([e0],e1,e2) }
70
+ clause('expr_bl_inv_start template expr_else template expr_bl_end') { |e0,e1,_,e3,e2| BlockExpressionNodeWithElse.new([e0], e2,e2,e1) }
71
+ clause('expr_bl_inv_start template expr_bl_end') { |e0,e1,e2| BlockExpressionNodeWithElse.new([e0],TemplateNode.new([]),e2,e1) }
72
+ end
73
+
74
+ production(:expr_else) do
75
+ clause('EXPRST WHITE? ELSE WHITE? EXPRE') { |_,_,_,_,_| }
76
+ clause('EXPRST WHITE? HAT WHITE? EXPRE') { |_,_,_,_,_| }
77
+ end
78
+
79
+ production(:expr) do
80
+ clause('EXPRST expression_contents EXPRE') { |_,e,_| e }
81
+ end
82
+
83
+ production(:expr_comment) do
84
+ clause('EXPRST BANG COMMENT EXPRE') { |_,_,e,_| e }
85
+ end
86
+
87
+ production(:expr_safe) do
88
+ clause('TEXPRST expression_contents TEXPRE') { |_,e,_| e }
89
+ clause('EXPRST AMP expression_contents EXPRE') { |_,_,e,_| e }
90
+ end
91
+
92
+ production(:expr_bl_start) do
93
+ clause('EXPRST HASH WHITE? IDENT WHITE? EXPRE') { |_,_,_,e,_,_| push_block CallNode.new(e,[]) }
94
+ clause('EXPRST HASH WHITE? IDENT WHITE arguments EXPRE') { |_,_,_,e,_,a,_| push_block CallNode.new(e,a) }
95
+ end
96
+
97
+ production(:expr_bl_inv_start) do
98
+ clause('EXPRST HAT WHITE? IDENT WHITE? EXPRE') { |_,_,_,e,_,_| push_block CallNode.new(e,[]) }
99
+ clause('EXPRST HAT WHITE? IDENT WHITE arguments EXPRE') { |_,_,_,e,_,a,_| push_block CallNode.new(e,a) }
100
+ end
101
+
102
+ production(:expr_bl_end) do
103
+ clause('EXPRST FWSL WHITE? IDENT WHITE? EXPRE') { |_,_,_,e,_,_| pop_block CallNode.new(e,[]) }
104
+ end
105
+
106
+ production(:expression_contents) do
107
+ clause('WHITE? call WHITE?') { |_,e,_| e }
108
+ clause('WHITE? local WHITE?') { |_,e,_| [e] }
109
+ end
110
+
111
+ production(:call) do
112
+ clause('object_path') { |e| e }
113
+ clause('object_path WHITE arguments') { |e0,_,e1| e0.last.arguments = e1; e0 }
114
+ clause('DOT') { |_| [CallNode.new('this', [])] }
115
+ end
116
+
117
+ production(:local) do
118
+ clause('AT IDENT') { |_,e| LocalVarNode.new(e) }
119
+ end
120
+
121
+ production('arguments') do
122
+ clause('argument_list') { |e| e }
123
+ clause('argument_list hash') { |e0,e1| e0 + [e1] }
124
+ clause('hash') { |e| [e] }
125
+ end
126
+
127
+ nonempty_list(:argument_list, [:object_path,:lit], :WHITE)
128
+
129
+ production(:lit) do
130
+ clause('string') { |e| e }
131
+ clause('number') { |e| e }
132
+ clause('boolean') { |e| e }
133
+ end
134
+
135
+ production(:string) do
136
+ clause('STRING') { |e| StringNode.new(e) }
137
+ end
138
+
139
+ production(:number) do
140
+ clause('NUMBER') { |n| NumberNode.new(n) }
141
+ end
142
+
143
+ production(:boolean) do
144
+ clause('BOOL') { |b| b ? TrueNode.new(true) : FalseNode.new(false) }
145
+ end
146
+
147
+ production(:hash) do
148
+ clause('hash_item') { |e| e }
149
+ clause('hash WHITE hash_item') { |e0,_,e1| e0.merge(e1) }
150
+ end
151
+
152
+ production(:hash_item) do
153
+ clause('IDENT EQ string') { |e0,_,e1| { e0.to_sym => e1 } }
154
+ clause('IDENT EQ object_path') { |e0,_,e1| { e0.to_sym => e1 } }
155
+ end
156
+
157
+ production(:object_sep) do
158
+ clause('DOT') { |_| }
159
+ clause('FWSL') { |_| }
160
+ end
161
+
162
+ nonempty_list(:object_path, :object, :object_sep)
163
+
164
+ production(:object) do
165
+ clause('IDENT') { |e| CallNode.new(e, []) }
166
+ clause('LITERAL') { |e| LiteralCallNode.new(e, []) }
167
+ clause('parent_call') { |e| e }
168
+ end
169
+
170
+ production(:parent_call) do
171
+ clause('backtrack IDENT') { |i,e| ParentCallNode.new(e,[],i) }
172
+ clause('backtrack LITERAL') { |i,e| ParentCallNode.new(e,[],i) }
173
+ end
174
+
175
+ production(:backtrack) do
176
+ clause('DOT DOT FWSL') { |_,_,_| 1 }
177
+ clause('backtrack DOT DOT FWSL') { |i,_,_,_| i += 1 }
178
+ end
179
+
180
+ finalize
181
+
182
+ end
183
+ end
@@ -0,0 +1,29 @@
1
+ module FlavourSaver
2
+ UnknownPartialException = Class.new(StandardError)
3
+
4
+ class Partial
5
+
6
+ def self.register_partial(name, content=nil, &block)
7
+ if block.respond_to? :call
8
+ partials[name.to_s] = block
9
+ else
10
+ partials[name.to_s] = Parser.parse(Lexer.lex(content))
11
+ end
12
+ end
13
+
14
+ def self.reset_partials
15
+ @partials = {}
16
+ end
17
+
18
+ def self.partials
19
+ @partials ||= {}
20
+ end
21
+
22
+ def self.fetch(name)
23
+ p = partials[name.to_s]
24
+ raise UnknownPartialException, "I can't find the partial named #{name.inspect}" unless p
25
+ p
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ module FlavourSaver
2
+ class RailsPartial
3
+ def self.register_partial(*args)
4
+ raise RuntimeError, "No need to register partials inside Rails."
5
+ end
6
+ def self.reset_partials; end
7
+ def self.partials; end
8
+ def self.fetch; end
9
+ end
10
+ end
@@ -0,0 +1,269 @@
1
+ require 'cgi'
2
+
3
+ module FlavourSaver
4
+ UnknownNodeTypeException = Class.new(StandardError)
5
+ UnknownContextException = Class.new(StandardError)
6
+ InappropriateUseOfElseException = Class.new(StandardError)
7
+ UndefinedPrivateVariableException = Class.new(StandardError)
8
+ UnknownHelperException = Class.new(RuntimeError)
9
+ class Runtime
10
+
11
+ attr_accessor :context, :parent, :ast
12
+
13
+ def self.run(ast, context, locals={}, helpers=[])
14
+ self.new(ast, context, locals, helpers).to_s
15
+ end
16
+
17
+ def initialize(ast, context=nil, locals={}, helpers=[],parent=nil)
18
+ @ast = ast
19
+ @locals = locals
20
+ @helpers = helpers
21
+ @context = context
22
+ @parent = parent
23
+ @privates = {}
24
+ end
25
+
26
+ def to_s(tmp_context = nil,privates={})
27
+ result = nil
28
+ if tmp_context
29
+ old_context = @context
30
+ @context = tmp_context
31
+ old_privates = @privates
32
+ @privates = @privates.dup.merge(privates) if privates.any?
33
+ result = evaluate_node(@ast)
34
+ @privates = old_privates
35
+ @context = old_context
36
+ else
37
+ result = evaluate_node(@ast)
38
+ end
39
+ result
40
+ end
41
+
42
+ def private_variable_set(name,value)
43
+ @privates[name.to_s] = value
44
+ end
45
+
46
+ def private_variable_get(name)
47
+ begin
48
+ @privates.fetch(name)
49
+ rescue KeyError => e
50
+ raise UndefinedPrivateVariableException, "private variable not found @#{name}"
51
+ end
52
+ end
53
+
54
+ def strip(tmp_context = nil)
55
+ self.to_s(tmp_context).gsub(/[\s\r\n]+/,' ').strip
56
+ end
57
+
58
+ def evaluate_node(node)
59
+ case node
60
+ when TemplateNode
61
+ node.items.map { |n| evaluate_node(n) }.join('')
62
+ when BlockExpressionNode
63
+ evaluate_block(node).to_s
64
+ when OutputNode
65
+ node.value
66
+ when NumberNode
67
+ if node.value =~ /\./
68
+ node.value.to_f
69
+ else
70
+ node.value.to_i
71
+ end
72
+ when ValueNode
73
+ node.value
74
+ when SafeExpressionNode
75
+ evaluate_expression(node).to_s
76
+ when ExpressionNode
77
+ escape(evaluate_expression(node).to_s)
78
+ when CallNode
79
+ evaluate_call(node)
80
+ when Hash
81
+ node.each do |key,value|
82
+ node[key] = evaluate_argument(value)
83
+ end
84
+ node
85
+ when CommentNode
86
+ ''
87
+ when PartialNode
88
+ evaluate_partial(node)
89
+ else
90
+ raise UnknownNodeTypeException, "Don't know how to deal with a node of type #{node.class.to_s.inspect}."
91
+ end
92
+ end
93
+
94
+ def parent
95
+ raise UnknownContextException, "No parent context in which to evaluate the parentiness of the context" unless @parent
96
+ @parent
97
+ end
98
+
99
+ def parent?
100
+ !!@parent
101
+ end
102
+
103
+ def evaluate_partial(node)
104
+ _context = context
105
+ _context = evaluate_argument(node.context) if node.context
106
+ if defined?(::Rails)
107
+ context.send(:render, :partial => node.name, :object => _context)
108
+ else
109
+ partial = Partial.fetch(node.name)
110
+ if partial.respond_to? :call
111
+ partial.call(_context)
112
+ else
113
+ create_child_runtime(partial).to_s(_context)
114
+ end
115
+ end
116
+ end
117
+
118
+ def evaluate_call(call, context=context, &block)
119
+ context = Helpers.decorate_with(context,@helpers,@locals) unless context.is_a? Helpers::Decorator
120
+ case call
121
+ when ParentCallNode
122
+ depth = call.depth
123
+ (2..depth).inject(parent) { |p| p.parent }.evaluate_call(call.to_callnode,&block)
124
+ when LiteralCallNode
125
+ result = context.send(:[], call.name)
126
+ result = result.call(*call.arguments.map { |a| evaluate_argument(a) },&block) if result.respond_to? :call
127
+ result
128
+ when LocalVarNode
129
+ result = private_variable_get(call.name)
130
+ else
131
+ raise UnknownHelperException, "Template context doesn't respond to method #{call.name.inspect}." unless context.respond_to? call.name
132
+ context.public_send(call.name, *call.arguments.map { |a| evaluate_argument(a) }, &block)
133
+ end
134
+ end
135
+
136
+ def evaluate_argument(arg)
137
+ if arg.is_a? Array
138
+ evaluate_object_path(arg)
139
+ else
140
+ evaluate_node(arg)
141
+ end
142
+ end
143
+
144
+ def evaluate_object_path(path, &block)
145
+ path.inject(context) do |context,call|
146
+ context = evaluate_call(call, context, &block)
147
+ end
148
+ end
149
+
150
+ def evaluate_expression(node, &block)
151
+ evaluate_object_path(node.method)
152
+ end
153
+
154
+ def evaluate_block(node,block_context=@context)
155
+ call = node.method.first
156
+ content_runtime = create_child_runtime(node.contents)
157
+ alternate_runtime = create_child_runtime(node.alternate) if node.respond_to? :alternate
158
+ block_runtime = BlockRuntime.new(block_context,content_runtime,alternate_runtime)
159
+
160
+ result = evaluate_call(call, block_context) { block_runtime }
161
+
162
+ # If the helper fails to call it's provided block then all
163
+ # sorts of wacky default behaviour kicks in. I don't like it,
164
+ # but that's the spec.
165
+ if !block_runtime.rendered?
166
+
167
+ # If the result is collectiony then act as an implicit
168
+ # "each"
169
+ if result && result.respond_to?(:each)
170
+ if result.respond_to?(:size) && (result.size > 0)
171
+ r = []
172
+ # Not using #each_with_index because the object might
173
+ # not actually be an Enumerable
174
+ count = 0
175
+ result.each do |e|
176
+ r << block_runtime.contents(e, {'index' => count})
177
+ count += 1
178
+ end
179
+ result = r.join('')
180
+ else
181
+ result = block_runtime.inverse
182
+ end
183
+
184
+ # Otherwise it behaves as an implicit "if"
185
+ elsif result
186
+ result = block_runtime.contents result
187
+ else
188
+ if block_runtime.has_inverse?
189
+ result = block_runtime.inverse
190
+ else
191
+ result = ''
192
+ end
193
+ end
194
+ end
195
+ result
196
+ end
197
+
198
+ def create_child_runtime(body=[])
199
+ node = body.is_a?(TemplateNode) ? body : TemplateNode.new(body)
200
+ Runtime.new(node,nil,@locals,@helpers,self)
201
+ end
202
+
203
+ def inspect
204
+ "#<FlavourSaver::Runtime contents=#{@ast.inspect}>"
205
+ end
206
+
207
+ class BlockRuntime
208
+ def initialize(block_context,content_runtime,alternate_runtime=nil)
209
+ @block_context = block_context
210
+ @content_runtime = content_runtime
211
+ @alternate_runtime = alternate_runtime
212
+ @render_count = 0
213
+ end
214
+
215
+ def contents(context=@block_context,locals={})
216
+ @render_count += 1
217
+ @content_runtime.to_s(context,locals) if @content_runtime
218
+ end
219
+
220
+ def inverse(context=@block_context)
221
+ @render_count += 1
222
+ @alternate_runtime.to_s(context) if @alternate_runtime
223
+ end
224
+
225
+ def has_inverse?
226
+ !!@alternate_runtime
227
+ end
228
+
229
+ def rendered?
230
+ @render_count > 0 ? @render_count : false
231
+ end
232
+
233
+ def rendered!
234
+ @render_count += 1
235
+ end
236
+ end
237
+
238
+ private
239
+
240
+ def escape(output)
241
+ if output.respond_to?(:html_safe) && output.html_safe?
242
+ # If the string is already marked as html_safe then don't
243
+ # escape it any further.
244
+ output
245
+
246
+ else
247
+ output = CGI.escapeHTML(output)
248
+
249
+ # We can't just use CGI.escapeHTML because Handlebars does extra
250
+ # escaping for its JavaScript environment. Thems the breaks.
251
+ output = output.gsub(/(['"`])/) do |match|
252
+ case match
253
+ when "'"
254
+ "&#x27;"
255
+ when '"'
256
+ "&quot;"
257
+ when '`'
258
+ "&#x60;"
259
+ end
260
+ end
261
+
262
+ # Mark it as already escaped if we're in Rails
263
+ output.html_safe if output.respond_to? :html_safe
264
+
265
+ output
266
+ end
267
+ end
268
+ end
269
+ end