root/trunk/lib/parser/lib/parser.rb

Revision 1232, 15.1 kB (checked in by gaspard, 3 months ago)

commit 1f7bac6636a1b57299d51fd22e0afc860ace9f4b
Author: Gaspard Bucher <gaspard@teti.ch>

Better export (log_at, event_at dates included), fixed node list pseudo_id bug. #226.

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