zafu 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,564 @@
1
+ module Zafu
2
+
3
+ def self.parser_with_rules(*modules)
4
+ parser = Class.new(Parser)
5
+ modules.flatten.each do |mod|
6
+ parser.send(:include, mod)
7
+ end
8
+ parser
9
+ end
10
+
11
+ class Parser
12
+ @@callbacks = {}
13
+ attr_accessor :text, :method, :pass, :options, :blocks, :ids, :defined_ids, :parent
14
+ # Method parameters "<r:show attr='name'/>" (params contains {'attr' => 'name'}).
15
+ attr_accessor :params
16
+
17
+ class << self
18
+ def new_with_url(path, opts={})
19
+ helper = opts[:helper] || Zafu::MockHelper.new
20
+ text, fullpath, base_path = self.get_template_text(path, helper)
21
+ self.new(text, :helper => helper, :base_path => base_path, :included_history => [fullpath])
22
+ end
23
+
24
+ # Retrieve the template text in the current folder or as an absolute path.
25
+ # This method is used when 'including' text
26
+ def get_template_text(path, helper, base_path=nil)
27
+ res = helper.send(:get_template_text, path, base_path)
28
+ return [parser_error("template '#{path}' not found", 'include'), nil, nil] unless res
29
+ text, fullpath, base_path = *res
30
+ return res
31
+ end
32
+
33
+ def parser_error(message, method)
34
+ "<span class='parser_error'><span class='method'>#{method}</span> #{message}</span>"
35
+ end
36
+
37
+ attr_accessor :before_process_callbacks
38
+
39
+ def before_process_callbacks
40
+ @before_process_callbacks ||= []
41
+ end
42
+
43
+ def before_process(*args)
44
+ self.before_process_callbacks += args
45
+ end
46
+ end
47
+
48
+ def initialize(text, opts={})
49
+ @stack = []
50
+ @ok = true
51
+ @blocks = []
52
+
53
+ @options = {:mode=>:void, :method=>'void'}.merge(opts)
54
+ @params = @options.delete(:params) || {}
55
+ @method = @options.delete(:method)
56
+ @ids = @options[:ids] ||= {}
57
+ original_ids = @ids.dup
58
+ @defined_ids = {} # ids defined in this node or this node's sub blocks
59
+ mode = @options.delete(:mode)
60
+ @parent = @options.delete(:parent)
61
+
62
+ if opts[:sub]
63
+ @text = text
64
+ else
65
+ @text = before_parse(text)
66
+ end
67
+
68
+
69
+ start(mode)
70
+
71
+ # set name
72
+ @name ||= @options[:name] || @params[:id]
73
+ @options[:ids][@name] = self if @name
74
+
75
+ unless opts[:sub]
76
+ @text = after_parse(@text)
77
+ end
78
+ @ids.keys.each do |k|
79
+ if original_ids[k] != @ids[k]
80
+ @defined_ids[k] = @ids[k]
81
+ end
82
+ end
83
+ @ok
84
+ end
85
+
86
+ def to_erb(context)
87
+ context[:helper] ||= @options[:helper]
88
+ process(context)
89
+ end
90
+
91
+ def start(mode)
92
+ enter(mode)
93
+ end
94
+
95
+ # Hook called when replacing part of an included template with '<r:with part='main'>...</r:with>'
96
+ # This replaces the current object 'self' which is in the original included template, with the custom version 'obj'.
97
+ def replace_with(obj)
98
+ # keep @method (obj's method is always 'with')
99
+ @blocks = obj.blocks.empty? ? @blocks : obj.blocks
100
+ @params.merge!(obj.params)
101
+ end
102
+
103
+ # Hook called when including a part "<r:include template='layout' part='title'/>"
104
+ def include_part(obj)
105
+ [obj]
106
+ end
107
+
108
+ def empty?
109
+ @blocks == [] && (@params == {} || @params == {:part => @params[:part]})
110
+ end
111
+
112
+ def process(context={})
113
+ if @name
114
+ # we pass the name as 'context' in the children tags
115
+ @context = context.merge(:name => @name)
116
+ else
117
+ @context = context
118
+ end
119
+ @result = ""
120
+ @out_post = ""
121
+
122
+ before_process
123
+
124
+ @pass = {} # used to pass information to the parent
125
+ res = nil
126
+
127
+ if respond_to?("r_#{@method}".to_sym)
128
+ res = do_method("r_#{@method}".to_sym)
129
+ else
130
+ res = do_method(:r_unknown)
131
+ end
132
+
133
+ # @text contains unparsed data (empty space)
134
+ after_process(res + @text)
135
+ end
136
+
137
+ def do_method(sym)
138
+ res = self.send(sym)
139
+ if res.kind_of?(String)
140
+ @result << res
141
+ elsif @result.blank?
142
+ @result << @method
143
+ end
144
+ @result + @out_post
145
+ end
146
+
147
+ def r_void
148
+ expand_with
149
+ end
150
+
151
+ def r_ignore
152
+ end
153
+
154
+ alias to_s r_void
155
+
156
+ def r_inspect
157
+ expand_with(:preflight=>true)
158
+ @blocks = []
159
+ @pass.merge!(@parts||{})
160
+ self.inspect
161
+ end
162
+
163
+ # basic rule to display errors
164
+ def r_unknown
165
+ sp = ""
166
+ @params.each do |k,v|
167
+ sp += " #{k}=#{v.inspect.gsub("'","TMPQUOTE").gsub('"',"'").gsub("TMPQUOTE",'"')}"
168
+ end
169
+
170
+ res = "<span class='parser_unknown'>&lt;r:#{@method}#{sp}"
171
+ inner = expand_with
172
+ if inner != ''
173
+ res + "&gt;</span>#{inner}<span class='parser_unknown'>&lt;r:/#{@method}&gt;</span>"
174
+ else
175
+ res + "/&gt;</span>"
176
+ end
177
+ end
178
+
179
+ # Set context with variables (unsafe) from template.
180
+ def r_expand_with
181
+ hash = {}
182
+ @params.each do |k,v|
183
+ hash["exp_#{k}"] = v.inspect
184
+ end
185
+ expand_with(hash)
186
+ end
187
+
188
+ def before_process
189
+ self.class.before_process_callbacks.each do |callback|
190
+ self.send(callback)
191
+ end
192
+ end
193
+
194
+ def after_process(text)
195
+ text
196
+ end
197
+
198
+ def before_parse(text)
199
+ text
200
+ end
201
+
202
+ def after_parse(text)
203
+ text
204
+ end
205
+
206
+ def include_template
207
+ return parser_error("missing 'template' attribute", 'include') unless @params[:template]
208
+ if @options[:part] && @options[:part] == @params[:part]
209
+ # fetching only a part, do not open this element (same as original caller) as it is useless and will make us loop the loop.
210
+ @method = 'ignore'
211
+ enter(:void)
212
+ return
213
+ end
214
+ @method = 'void'
215
+
216
+ # fetch text
217
+ @options[:included_history] ||= []
218
+
219
+ included_text, absolute_url, base_path = self.class.get_template_text(@params[:template], @options[:helper], @options[:base_path])
220
+
221
+ if absolute_url
222
+ absolute_url += "::#{@params[:part].gsub('/','_')}" if @params[:part]
223
+ absolute_url += "??#{@options[:part].gsub('/','_')}" if @options[:part]
224
+ if @options[:included_history].include?(absolute_url)
225
+ included_text = parser_error("infinity loop: #{(@options[:included_history] + [absolute_url]).join(' --&gt; ')}", 'include')
226
+ else
227
+ included_history = @options[:included_history] + [absolute_url]
228
+ end
229
+ end
230
+ res = self.class.new(included_text, :helper => @options[:helper], :base_path => base_path, :included_history => included_history, :part => @params[:part]) # we set :part to avoid loop failure when doing self inclusion
231
+
232
+ if @params[:part]
233
+ if iblock = res.ids[@params[:part]]
234
+ included_blocks = include_part(iblock)
235
+ # get all ids from inside the included part:
236
+ @ids.merge! iblock.defined_ids
237
+ else
238
+ included_blocks = [parser_error("'#{@params[:part]}' not found in template '#{@params[:template]}'", 'include')]
239
+ end
240
+ else
241
+ included_blocks = res.blocks
242
+ @ids.merge! res.ids
243
+ end
244
+
245
+ enter(:void) # normal scan on content
246
+ # replace 'with'
247
+
248
+ not_found = []
249
+ @blocks.each do |b|
250
+ next if b.kind_of?(String) || b.method != 'with'
251
+ if target = res.ids[b.params[:part]]
252
+ if target.kind_of?(String)
253
+ # error
254
+ elsif b.empty?
255
+ target.method = 'ignore'
256
+ else
257
+ target.replace_with(b)
258
+ end
259
+ else
260
+ # part not found
261
+ not_found << parser_error("'#{b.params[:part]}' not found in template '#{@params[:template]}'", 'with')
262
+ end
263
+ end
264
+ @blocks = included_blocks + not_found
265
+ end
266
+
267
+ # Return a hash of all descendants. Find a specific descendant with descendant['form'] for example.
268
+ def all_descendants
269
+ @all_descendants ||= begin
270
+ d = {}
271
+ @blocks.each do |b|
272
+ next if b.kind_of?(String)
273
+ b.public_descendants.each do |k,v|
274
+ d[k] ||= []
275
+ d[k] += v
276
+ end
277
+ # latest is used first: use direct children before grandchildren.
278
+ d[b.method] ||= []
279
+ d[b.method] << b
280
+ end
281
+ d
282
+ end
283
+ end
284
+
285
+ # Find a direct child with +child[method]+.
286
+ def child
287
+ Hash[*@blocks.map do |b|
288
+ b.kind_of?(String) ? nil : [b.method, b]
289
+ end.compact.flatten]
290
+ end
291
+
292
+ def descendants(key)
293
+ all_descendants[key] || []
294
+ end
295
+
296
+ def ancestors
297
+ @ancestors ||= begin
298
+ if parent
299
+ parent.ancestors + [parent]
300
+ else
301
+ []
302
+ end
303
+ end
304
+ end
305
+
306
+ alias public_descendants all_descendants
307
+
308
+ # Return the last defined parent for the given key.
309
+ def ancestor(key)
310
+ res = nil
311
+ ancestors.reverse_each do |a|
312
+ if key == a.method
313
+ res = a
314
+ break
315
+ end
316
+ end
317
+ res
318
+ end
319
+
320
+ # Return the last defined descendant for the given key.
321
+ def descendant(key)
322
+ descendants(key).last
323
+ end
324
+
325
+ # Return the root block (the one opened first).
326
+ def root
327
+ @root ||= parent ? parent.root : self
328
+ end
329
+
330
+ def success?
331
+ return @ok
332
+ end
333
+
334
+ def flush(str=@text)
335
+ return if str == ''
336
+ if @blocks.last.kind_of?(String)
337
+ @blocks[-1] << str
338
+ else
339
+ @blocks << str
340
+ end
341
+ @text = @text[str.length..-1]
342
+ end
343
+
344
+ # Build blocks
345
+ def store(obj)
346
+ if obj.kind_of?(String) && @blocks.last.kind_of?(String)
347
+ @blocks[-1] << obj
348
+ elsif obj != ''
349
+ @blocks << obj
350
+ end
351
+ end
352
+
353
+ # Output ERB code during ast processing.
354
+ def out(str)
355
+ @result << str
356
+ # Avoid double entry when this is the last call in a render method.
357
+ nil
358
+ end
359
+
360
+ # Output ERB code that will be inserted after @result.
361
+ def out_post(str)
362
+ @out_post << str
363
+ # Avoid double entry when this is the last call in a render method.
364
+ nil
365
+ end
366
+
367
+ # Advance parser.
368
+ def eat(arg)
369
+ if arg.kind_of?(String)
370
+ len = arg.length
371
+ elsif arg.kind_of?(Fixnum)
372
+ len = arg
373
+ else
374
+ raise
375
+ end
376
+ @text = @text[len..-1]
377
+ end
378
+
379
+ def enter(mode)
380
+ @stack << mode
381
+ # puts "ENTER(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
382
+ if mode == :void
383
+ sym = :scan
384
+ else
385
+ sym = "scan_#{mode}".to_sym
386
+ end
387
+ while (@text != '' && @stack[-1] == mode)
388
+ # puts "CONTINUE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
389
+ self.send(sym)
390
+ end
391
+ # puts "LEAVE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}"
392
+ end
393
+
394
+ def make(mode, opts={})
395
+ if opts[:text]
396
+ custom_text = opts.delete(:text)
397
+ end
398
+ text = custom_text || @text
399
+ opts = @options.merge(opts).merge(:sub=>true, :mode=>mode, :parent => self)
400
+ new_obj = self.class.new(text,opts)
401
+ if new_obj.success?
402
+ @text = new_obj.text unless custom_text
403
+ new_obj.text = ""
404
+ store new_obj
405
+ else
406
+ flush @text[0..(new_obj.text.length - @text.length)] unless custom_text
407
+ end
408
+ # puts "MADE #{new_obj.inspect}"
409
+ # puts "TEXT #{@text.inspect}"
410
+ new_obj
411
+ end
412
+
413
+ def leave(mode=nil)
414
+ if mode.nil?
415
+ @stack = []
416
+ return
417
+ end
418
+ pop = true
419
+ while @stack != [] && pop
420
+ pop = @stack.pop
421
+ break if pop == mode
422
+ end
423
+ end
424
+
425
+ def fail
426
+ @ok = false
427
+ @stack = []
428
+ end
429
+
430
+ def check_params(*args)
431
+ missing = []
432
+ if args[0].kind_of?(Array)
433
+ # or groups
434
+ ok = false
435
+ args.each_index do |i|
436
+ unless args[i].kind_of?(Array)
437
+ missing[i] = [args[i]]
438
+ next
439
+ end
440
+ missing[i] = []
441
+ args[i].each do |arg|
442
+ missing[i] << arg.to_s unless @params[arg]
443
+ end
444
+ if missing[i] == []
445
+ ok = true
446
+ break
447
+ end
448
+ end
449
+ if ok
450
+ return true
451
+ else
452
+ out "[#{@method} parameter(s) missing:#{missing[0].sort.join(', ')}]"
453
+ return false
454
+ end
455
+ else
456
+ args.each do |arg|
457
+ missing << arg.to_s unless @params[arg]
458
+ end
459
+ end
460
+ if missing != []
461
+ out "[#{@method} parameter(s) missing:#{missing.sort.join(', ')}]"
462
+ return false
463
+ end
464
+ true
465
+ end
466
+
467
+ def expand_block(block, new_context={})
468
+ block.process(@context.merge(new_context))
469
+ end
470
+
471
+ def expand_with(acontext={})
472
+ blocks = acontext.delete(:blocks) || @blocks
473
+ res = ""
474
+
475
+ # FIXME: I think we can delete @pass and @parts stuff now (test first).
476
+
477
+ @pass = {} # current object sees some information from it's direct descendants
478
+ @parts = {}
479
+ only = acontext[:only]
480
+ new_context = @context.merge(acontext)
481
+
482
+ if acontext[:ignore]
483
+ new_context[:ignore] = (@context[:ignore] || []) + (acontext[:ignore] || []).uniq
484
+ end
485
+
486
+ if acontext[:no_ignore]
487
+ new_context[:ignore] = (new_context[:ignore] || []) - acontext[:no_ignore]
488
+ end
489
+
490
+ ignore = new_context[:ignore]
491
+
492
+ blocks.each do |b|
493
+ if b.kind_of?(String)
494
+ if (!only || only.include?(:string)) && (!ignore || !ignore.include?(:string))
495
+ res << b
496
+ end
497
+ elsif (!only || only.include?(b.method)) && (!ignore || !ignore.include?(b.method))
498
+ res << b.process(new_context.dup)
499
+ if pass = b.pass
500
+ if pass[:part]
501
+ @parts.merge!(pass[:part])
502
+ pass.delete(:part)
503
+ end
504
+ @pass.merge!(pass)
505
+ end
506
+ end
507
+ end
508
+ res
509
+ end
510
+
511
+ def inspect
512
+ attributes = []
513
+ params = []
514
+ (@params || {}).each do |k,v|
515
+ unless v.nil?
516
+ params << "#{k.inspect.gsub('"', "'")}=>'#{v}'"
517
+ end
518
+ end
519
+ attributes << " {= #{params.sort.join(', ')}}" unless params == []
520
+
521
+ context = []
522
+ (@context || {}).each do |k,v|
523
+ unless v.nil?
524
+ context << "#{k.inspect.gsub('"', "'")}=>'#{v}'"
525
+ end
526
+ end
527
+ attributes << " {> #{context.sort.join(', ')}}" unless context == []
528
+
529
+ pass = []
530
+ (@pass || {}).each do |k,v|
531
+ unless v.nil?
532
+ if v.kind_of?(Array)
533
+ pass << "#{k.inspect.gsub('"', "'")}=>#{v.inspect.gsub('"', "'")}"
534
+ elsif v.kind_of?(Parser)
535
+ pass << "#{k.inspect.gsub('"', "'")}=>['#{v}']"
536
+ else
537
+ pass << "#{k.inspect.gsub('"', "'")}=>#{v.inspect.gsub('"', "'")}"
538
+ end
539
+ end
540
+ end
541
+ attributes << " {< #{pass.sort.join(', ')}}" unless pass == []
542
+
543
+ res = []
544
+ @blocks.each do |b|
545
+ if b.kind_of?(String)
546
+ res << b
547
+ else
548
+ res << b.inspect
549
+ end
550
+ end
551
+ result = "[#{@method}#{attributes.join('')}"
552
+ if res != []
553
+ result += "]#{res}[/#{@method}]"
554
+ else
555
+ result += "/]"
556
+ end
557
+ result + @text
558
+ end
559
+
560
+ def parser_error(message, method = @method)
561
+ self.class.parser_error(message, method)
562
+ end
563
+ end # Parser
564
+ end # Zafu