zafu 0.5.0

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.
@@ -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