module Quarto # This abstract base class is a substitute for XSLT. Its # transform method takes a single # REXML::Element and applies rules defined in # the subclass. # # To define those rules, you subclass Transformer and # write methods to handle each element type. For example: # # class MyTranformer < Quarto::Transformer # # This method will handle all elements # def transform_book(book_element, raise_on_unrecognized_element) # # Return whatever string you like # # raise_on_unrecognized_element is provided # # so that you can pass it to recursive_transform # # if necessary. # end # end class Transformer # Recursively applies the transformation rules # you've defined to +element+ and its children, # returning the results as a string. Depending # on the rules you've set up, the result may # be XML or something else altogether. # # +element+ must be a REXML::Element # or a subclass thereof. If +element+ is a # REXML::Document, the document root and all # its children will be transformed. If +element+ # is a REXML::Element, only the element's # descendents will be tranformed; +element+ itself # will not be used. # # By default, unrecognized elements (and all their # descendants) will be ommited from the result tree. # # However, you can cause these unrecognized elements to # raise an exception by setting +raise_on_unrecognized_element+ # to true. def transform(element, raise_on_unrecognized_element = false) raise ArgumentError, "Expected REXML::Element but got #{element.inspect}" unless element.is_a?(REXML::Element) if element.is_a?(REXML::Document) recursive_transform(element.root, raise_on_unrecognized_element) else element.children.to_a.inject('') do |result, child| result + recursive_transform(child, raise_on_unrecognized_element) end end end protected # Creates an XML tag with the specified name, returning a string. # This method is meant to be called by subclasses of # Transformer. # # Example: # # content_tag('img', 'src' => 'http://example.com/image.jpg') # # If you need to use an absolute path for something, e.g. an image, # you should include Quarto::UrlHelper in your # Transformer subclass and call abs_path. def content_tag(tag_name, *args) if args.last.is_a?(Hash) attributes = args.pop else attributes = {} end if args.empty? if block_given? contents = yield else contents = nil end else contents = args[0] end output = "<#{tag_name}" attributes.each do |attr, value| output << " #{attr}=\"#{value}\"" end if contents.nil? or contents.empty? output << '/>' else output << ">#{contents}" end end # This method is meant to be overriden in subclasses. The # default implementation always returns false. # # When tranform is called, each descendant element # is passed to literal?. If it returns true, # the descdant is added to the result tree with the same # tag. If not, the Transformer will look for a # custom tranform method for that element. If none is found, # what happens next depends on the value of # raise_on_unrecognized_element in transform. # If it's true, an exception will be raised. Otherwise, the # element and its descendant will be ommitted from the result # tree. # # If literal? returns true for an element, # its descendant elements will not be added to the tree # verbatim, but will instead be subjected to the same # transformation process as everything else. # # See HtmlTransformer for an example implementation. def literal?(element) false end # Macro to define the literal? method. Accepts # one or more element names. # # Example: # # class MyTransformer < Quarto::Transformer # literals 'div', 'p', 'a' # end # # The above is equivalent to: # # class MyTransformer < Quarto::Transformer # def literal?(element) # ['div', 'p', 'a'].include?(element.name) # end # end def self.literals(*args) class_eval(%Q( def literal?(element) [#{args.collect { |e| "'#{e}'" }.join(',')}].include?(element.name) end )) end # Recursively transform the +element+ and all its children, # returning a string. Custom transform methods often call # this method. def recursive_transform(element, raise_on_unrecognized_element) if element.is_a?(REXML::Element) if respond_to?("transform_#{element.name}") send("transform_#{element.name}", element, raise_on_unrecognized_element) elsif literal?(element) contents = element.children.inject('') do |result, child| result + recursive_transform(child, raise_on_unrecognized_element) end content_tag(element.name, contents, element.attributes) elsif raise_on_unrecognized_element raise UnrecognizedElementError, "Unrecognized element: #{element.name}" else '' end elsif element.is_a?(REXML::Comment) '' else element.to_s end end # Replaces +element+ with +replace_with+, adding any # +attributes+. This is a convenience method for use # inside a custom transform method. It's not infinitely # flexible, but it simplifies a common task. Calls # recursive_transform on +element+'s children. def replace_element(element, replace_with, raise_on_unrecognized_element, attributes = {}) raise ArgumentError, "Expected REXML::Element but got #{element.inspect}" unless element.is_a?(REXML::Element) raise ArgumentError, "Expected String but got #{replace_with.inspect}" unless replace_with.is_a?(String) raise ArgumentError, "Expected Hash but got #{attributes.inspect}" unless attributes.is_a?(Hash) contents = element.children.inject('') do |result, child| result + recursive_transform(child, raise_on_unrecognized_element) end content_tag(replace_with, contents, attributes) end end class UnrecognizedElementError < RuntimeError; end end