module Zafu def self.parser_with_rules(*modules) parser = Class.new(Parser) modules.flatten.each do |mod| parser.send(:include, mod) end parser end class Parser # If you wonder what the difference is between 'after_wrap' and 'after_process' here it is: # 'after_wrap' is called by the 'wrap' method from within the method handler, 'after_process' is called # at the very end. Example: # # <% if var = Node.all %> | <--- #
  • ...
  • <--- content for after_wrap | <--- content for after_process # <% end %> | <--- # TEXT_CALLBACKS = %w{before_parse after_parse before_wrap wrap after_wrap after_process} PROCESS_CALLBACKS = %w{before_process expander process_unknown} CALLBACKS = TEXT_CALLBACKS + PROCESS_CALLBACKS @@callbacks = {} attr_accessor :text, :name, :method, :pass, :options, :blocks, :ids, :defined_ids, :parent, :errors # Method parameters "" (params contains {'attr' => 'name'}). attr_accessor :params class << self def new_with_url(path, opts={}) helper = opts[:helper] || Zafu::MockHelper.new text, fullpath, base_path = self.get_template_text(path, helper) return parser_error("template '#{path}' not found", 'include') unless text self.new(text, :helper => helper, :base_path => base_path, :included_history => [fullpath], :root => path) end # Retrieve the template text in the current folder or as an absolute path. # This method is used when 'including' text def get_template_text(path, helper, base_path=nil) res = helper.send(:get_template_text, path, base_path) return [parser_error("template '#{path}' not found", 'include'), nil, nil] unless res res end def parser_error(message, method) "#{erb_safe method} #{erb_safe message}" end def erb_safe(text) text.gsub('<%', '<%').gsub('%>', '%>') end CALLBACKS.each do |clbk| eval %Q{ attr_accessor :#{clbk}_callbacks def #{clbk}_callbacks @#{clbk}_callbacks ||= superclass.respond_to?(:#{clbk}_callbacks) ? superclass.#{clbk}_callbacks : [] end def #{clbk}(*args) self.#{clbk}_callbacks += args end } end end # class << self PROCESS_CALLBACKS.each do |clbk| eval %Q{ def #{clbk} self.class.#{clbk}_callbacks.each do |callback| send(callback) end end } end def expander self.class.expander_callbacks.reverse_each do |callback| if res = send(callback) if res.kind_of?(String) @result << res end return @result end end nil end TEXT_CALLBACKS.each do |clbk| eval %Q{ def #{clbk}(text) self.class.#{clbk}_callbacks.each do |callback| text = send(callback, text) end text end } end alias wrap_callbacks wrap def wrap(text) after_wrap( wrap_callbacks( before_wrap(text) + @out_post ) # @text contains unparsed data (white space) ) + @text end # This method is called at the very beginning of the processing chain and is # used to store state to make 'process' reintrant... def save_state { :@context => @context, # <== we need this when rendering twice the same part :@result => @result, :@out_post => @out_post, :@params => @params.dup, :@method => @method, :@var => @var, } end # Restore state from a hash def restore_state(saved) saved.each do |key, value| instance_variable_set(key, value) end end def parser_error(message, method = @method, halt = true) if halt self.class.parser_error(message, method) else @errors << self.class.parser_error(message, method) nil end end def parser_continue(message, method = @method) parser_error(message, method, false) end def process_unknown self.class.process_unknown_callbacks.each do |callback| if res = send(callback) return res end end @errors.empty? ? default_unknown : show_errors end def show_errors @errors.join(' ') end def initialize(text, opts={}) @stack = [] @ok = true @blocks = [] @errors = [] @options = {:mode=>:void, :method=>'void'}.merge(opts) @params = @options.delete(:params) || {} @method = @options.delete(:method) @ids = @options[:ids] ||= {} original_ids = @ids.dup @defined_ids = {} # ids defined in this node or this node's sub blocks mode = @options.delete(:mode) @parent = @options.delete(:parent) if opts[:sub] @text = text else @text = before_parse(text) end start(mode) # set name @name ||= extract_name @options[:ids][@name] = self if @name unless opts[:sub] @text = after_parse(@text) end @ids.keys.each do |k| if original_ids[k] != @ids[k] @defined_ids[k] = @ids[k] end end @ok end def extract_name @options[:name] || @params[:id] end def to_erb(context) context[:helper] ||= @options[:helper] process(context) end def start(mode) enter(mode) end # Pass some contextual information to siblings def pass(elems = nil) return @pass unless elems (@pass ||= {}).merge!(elems) @pass end # Hook called when replacing part of an included template with '...' # This replaces the current object 'self' which is in the original included template, with the custom version 'obj'. def replace_with(obj) # keep @method (obj's method is always 'with') @blocks = obj.blocks.empty? ? @blocks : obj.blocks obj.params.delete(:part) @params.merge!(obj.params) end # Hook called when including a part "" def include_part(obj) [obj] end def empty? @blocks == [] && (@params == {} || @params == {:part => @params[:part]}) end def process(context={}) return '' if @method == 'ignore' || @method.blank? saved = save_state if @name # we pass the name as 'context' in the children tags @context = context.merge(:name => @name) else @context = context end # FIXME: replace with array and join (faster) @result = "" @out_post = "" @pass = nil before_process res = wrap(expander || default_expander) res = after_process(res) # restore state restore_state(saved) res end # Default processing def default_expander if respond_to?("r_#{@method}".to_sym) do_method("r_#{@method}".to_sym) else do_method(:process_unknown) end end def do_method(sym) res = self.send(sym) if res.kind_of?(String) @result << res elsif @result.blank? @result << (@errors.empty? ? '' : show_errors) end @result end def r_void expand_with end alias to_s r_void def r_inspect expand_with(:preflight=>true) @blocks = [] self.inspect end # basic rule to display errors def default_unknown sp = "" @params.each do |k,v| sp += " #{k}=#{v.inspect.gsub("'","TMPQUOTE").gsub('"',"'").gsub("TMPQUOTE",'"')}" end res = "<r:#{@method}#{sp}" inner = expand_with if inner != '' res + ">#{inner}<r:/#{@method}>" else res + "/>" end end # Set context with variables (unsafe) from template. def r_expand_with hash = {} @params.each do |k,v| hash["exp_#{k}"] = v.inspect end expand_with(hash) end def r_ignore '' end def include_template return parser_error("missing 'template' attribute") unless @params[:template] if @options[:part] && @options[:part] == @params[:part] # fetching only a part, do not open this element (same as original caller) as it is useless and will make us loop the loop. @method = 'ignore' enter(:void) return end @method = 'void' # fetch text @options[:included_history] ||= [] included_text, absolute_url, base_path = self.class.get_template_text(@params[:template], @options[:helper], @options[:base_path]) if absolute_url absolute_url += "::#{@params[:part].gsub('/','_')}" if @params[:part] absolute_url += "??#{@options[:part].gsub('/','_')}" if @options[:part] if @options[:included_history].include?(absolute_url) included_text = parser_error("infinity loop: #{(@options[:included_history] + [absolute_url]).join(' --> ')}", 'include') else included_history = @options[:included_history] + [absolute_url] end else # Error: included_text contains the error meessage @blocks = [included_text] return end res = self.class.new(included_text, :helper => @options[:helper], :base_path => base_path, :included_history => included_history, :part => @params[:part], :parent => self) # we set :part to avoid loop failure when doing self inclusion if @params[:part] if iblock = res.ids[@params[:part]] included_blocks = include_part(iblock) # get all ids from inside the included part: @ids.merge! iblock.defined_ids else included_blocks = [parser_error("'#{@params[:part]}' not found in template '#{@params[:template]}'", 'include')] end else included_blocks = res.blocks @ids.merge! res.ids end enter(:void) # normal scan on content # replace 'with' @blocks.each do |b| next if b.kind_of?(String) || b.method != 'with' if target = res.ids[b.params[:part]] if target.kind_of?(String) # error elsif b.empty? target.method = 'ignore' else target.replace_with(b) end else # part not found parser_error("'#{b.params[:part]}' not found in template '#{@params[:template]}'", 'with') end end @blocks = included_blocks end # Return a hash of all descendants. Find a specific descendant with descendant['form'] for example. def all_descendants @all_descendants ||= begin d = {} @blocks.each do |b| next if b.kind_of?(String) b.public_descendants.each do |k,v| d[k] ||= [] d[k] += v end # latest is used first: use direct children before grandchildren. d[b.method] ||= [] d[b.method] << b end d end end # Find a direct child with +child[method]+. def child Hash[*@blocks.map do |b| b.kind_of?(String) ? nil : [b.method, b] end.compact.flatten] end def dynamic_blocks? @blocks.detect { |b| !b.kind_of?(String) } end def descendants(key) all_descendants[key] || [] end def ancestors @ancestors ||= begin if parent parent.ancestors + [parent] else [] end end end alias public_descendants all_descendants # Return the last defined parent for the given keys. def ancestor(keys) keys = Array(keys) ancestors.reverse_each do |a| if keys.include?(a.method) return a end end nil end # Return the last defined descendant for the given key. def descendant(key) descendants(key).last end # Return the root block (the one opened first). def root @root ||= parent ? parent.root : self end def success? return @ok end def flush(str=@text) return if str == '' if @blocks.last.kind_of?(String) @blocks[-1] << str else @blocks << str end @text = @text[str.length..-1] end # Build blocks def store(obj) if obj.kind_of?(String) && @blocks.last.kind_of?(String) @blocks[-1] << obj elsif obj != '' @blocks << obj end end # Output ERB code during ast processing. def out(str) @result << str # Avoid double entry when this is the last call in a render method. true end # Output ERB code that will be inserted after @result. def out_post(str) @out_post << str # Avoid double entry when this is the last call in a render method. true end # Advance parser. def eat(arg) if arg.kind_of?(String) len = arg.length elsif arg.kind_of?(Fixnum) len = arg else raise end @text = @text[len..-1] end def enter(mode) @stack << mode # puts "ENTER(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}" if mode == :void sym = :scan else sym = "scan_#{mode}".to_sym end while (@text != '' && @stack[-1] == mode) # puts "CONTINUE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}" self.send(sym) end # puts "LEAVE(#{@method},:#{mode}) [#{@text}] #{@zafu_tag_count.inspect}" end def make(mode, opts={}) if opts[:text] custom_text = opts.delete(:text) end text = custom_text || @text opts = @options.merge(opts).merge(:sub => true, :mode => mode, :parent => self) new_obj = self.class.new(text, opts) if new_obj.success? @text = new_obj.text unless custom_text new_obj.text = "" store new_obj else flush @text[0..(new_obj.text.length - @text.length)] unless custom_text end # puts "MADE #{new_obj.inspect}" # puts "TEXT #{@text.inspect}" new_obj end def leave(mode=nil) if mode.nil? @stack = [] return end pop = true while @stack != [] && pop pop = @stack.pop break if pop == mode end end def fail @ok = false @stack = [] end def check_params(*args) missing = [] if args[0].kind_of?(Array) # or groups ok = false args.each_index do |i| unless args[i].kind_of?(Array) missing[i] = [args[i]] next end missing[i] = [] args[i].each do |arg| missing[i] << arg.to_s unless @params[arg] end if missing[i] == [] ok = true break end end if ok return true else out "[#{@method} parameter(s) missing:#{missing[0].sort.join(', ')}]" return false end else args.each do |arg| missing << arg.to_s unless @params[arg] end end if missing != [] out "[#{@method} parameter(s) missing:#{missing.sort.join(', ')}]" return false end true end def expand_block(block, new_context={}) block.process(@context.merge(new_context)) end def expand_with(acontext={}) blocks = acontext.delete(:blocks) || @blocks res = "" only = acontext[:only] new_context = @context.merge(acontext) if acontext[:ignore] new_context[:ignore] = (@context[:ignore] || []) + (acontext[:ignore] || []).uniq end if acontext[:no_ignore] new_context[:ignore] = (new_context[:ignore] || []) - acontext[:no_ignore] end ignore = new_context[:ignore] blocks.each do |b| if b.kind_of?(String) if (!only || (only.kind_of?(Array) && only.include?(:string))) && (!ignore || !ignore.include?(:string)) res << b end elsif (!only || (only.kind_of?(Array) && only.include?(b.method)) || only =~ b.method) && (!ignore || !ignore.include?(b.method)) res << b.process(new_context.dup) if pass = b.pass new_context.merge!(pass) end end end res end def inspect attributes = [] params = [] (@params || {}).each do |k,v| unless v.nil? params << "#{k.inspect.gsub('"', "'")}=>'#{v}'" end end attributes << " {= #{params.sort.join(', ')}}" unless params == [] context = [] (@context || {}).each do |k,v| unless v.nil? context << "#{k.inspect.gsub('"', "'")}=>'#{v}'" end end attributes << " {> #{context.sort.join(', ')}}" unless context == [] res = [] @blocks.each do |b| if b.kind_of?(String) res << b else res << b.inspect end end result = "[#{@method}#{attributes.join('')}" if res != [] result += "]#{res}[/#{@method}]" else result += "/]" end result + @text end end # Parser end # Zafu