require 'rexml/document' require 'rexml/streamlistener' require 'facet/dictionary' require 'facet/string/blank' module Nitro # :section: A collection of standard morphers. # The base morpher class. Morphers are triggered # by a special 'key' attribute in the xml stream and # transform the owner element. # # key = attribute 'key' # name = element name # attributes = alement attributes class Morpher def initialize(key, name, attributes, compiler = nil) @key = key @name = name @attributes = attributes @value = @attributes[@key] @compiler = compiler end def before_start(buffer); end def after_start(buffer); end def before_end(buffer); end def after_end(buffer); end end # A useful super class for morphers. class StandardMorpher < Morpher def after_end(buffer) # gmosx: leave the leading space. buffer << " <?r end ?>" end end # attribute: times # # <li times="3">...</li> # # becomes # # <?r 3.times do ?> # <li>...</li> # <?r end ?> class TimesMorpher < StandardMorpher def before_start(buffer) # gmosx: leave the trailing space. buffer << "<?r #@value.times do ?> " @attributes.delete(@key) end end # attribute: each, for # # <li each="item in array">my item is #{item}</li> # # becomes # # <?r for item in array ?> # <li>my item is #{item}</li> # <?r end ?> class EachMorpher < Morpher def before_start(buffer) if @value =~ / in / buffer << "<?r for #@value ?> " @attributes.delete(@key) end end def after_end(buffer) if @value =~ / in / buffer << " <?r end ?>" end end end # attribute: if, unless # # <div prop1="one" if="@mycond" prop2="two">@mycond is true</div> # # becomes # # <?r if @mycond ?> # <div prop1="one" prop2="two">@mycond is true</div> # <?r end ?> class IfMorpher < StandardMorpher def before_start(buffer) buffer << "<?r #@key #@value ?> " @attributes.delete(@key) end end # attribute: selected_if, checked_if, selected_unless, checked_unless # # <option value="1" selected_if="@cond">opt1</option> # # becomes # # <?r if @cond ?> # <option value="1" selected="selected">opt1</option> # <?r else ?> # <option value="1">opt1</option> # <?r end ?> class SelectedIfMorpher < StandardMorpher def before_start(buffer) @attr, @cond = @key.split('_') @attributes.delete(@key) @attributes[@attr] = @attr buffer << "<?r #@cond #@value ?> " end def after_start(buffer) @start_index = buffer.length end def before_end(buffer) @attributes.delete(@attr) @end_index = buffer.length buffer << Morphing.emit_end(@name) buffer << "<?r else ?>" buffer << Morphing.emit_start(@name, @attributes) buffer << buffer[@start_index...@end_index] end end # :section: The morphing system. # A compiler module that translates xml stream. Multiple # 'key' attributes are supported per element. class Morphing #-- # The listener used to parse the xml stream. # # TODO: add support for morphing comments, text, etc. #++ class Listener # :nodoc: all include REXML::StreamListener # The compiler that calls this compiling stage. Useful # for advanced morphing effects and inter-stage # communication. attr_accessor :compiler attr_accessor :buffer def initialize super @buffer = '' @stack = [] end def tag_start(name, attributes) morphers = [] #for key, val in attributes # if morpher_class = Morphing.morphers[key] # morphers << morpher_class.new(key, name, attributes) # end #end Morphing.morphers.each do |key, morpher_class| if attributes.has_key? key morphers << morpher_class.new(key, name, attributes, compiler) end end morphers.each { |h| h.before_start(@buffer) } @buffer << Morphing.emit_start(name, attributes) morphers.each { |h| h.after_start(@buffer) } @stack.push(morphers) end def tag_end(name) morphers = @stack.pop morphers.reverse.each { |h| h.before_end(@buffer) } @buffer << Morphing.emit_end(name) morphers.reverse.each { |h| h.after_end(@buffer) } end def text(str) @buffer << str end def instruction(name, attributes) @buffer << "<?#{name}#{attributes}?>" end def cdata(content) @buffer << "<![CDATA[#{content}]]>" end def comment(c) unless Nitro::Template.strip_xml_comments @buffer << "<!--#{c}-->" end end def doctype(name, pub_sys, long_name, uri) @buffer << "<!DOCTYPE #{name} #{pub_sys} #{long_name} #{uri}>\n" end end def self.emit_start(name, attributes) attrs = attributes.map{ |k, v| %|#{k}="#{v}"| }.join(' ') attrs.blank? ? "<#{name}>" : "<#{name} #{attrs}>" end def self.emit_end(name) "</#{name}>" end def self.transform(source, compiler = nil) listener = Listener.new listener.compiler = compiler REXML::Document.parse_stream(source, listener) return listener.buffer end # The morphers map. @morphers = Dictionary.new class << self def morphers @morphers end def morphers=(hash) @morphers = hash end def add_morpher(key, klass) @morphers[key.to_s] = klass end alias_method :add, :add_morpher def delete_morpher(key) @morphers.delete(key) end alias_method :delete, :delete_morpher end # Install the default morphers. add_morpher :times, TimesMorpher add_morpher :if, IfMorpher add_morpher :unless, IfMorpher add_morpher :each, EachMorpher add_morpher :for, EachMorpher add_morpher :selected_if, SelectedIfMorpher add_morpher :selected_unless, SelectedIfMorpher add_morpher :checked_if, SelectedIfMorpher add_morpher :checked_unless, SelectedIfMorpher end end