# # xoxo.rb -- a Ruby XOXO parser and generator # # Copyright (C) 2006 Christian Neukirchen # # This work is licensed under the same terms as Ruby itself. # require 'rexml/parsers/pullparser' # XOXO provides a Ruby API similar to Marshal and YAML (though more # specific) to load and dump XOXO[http://microformats.org/wiki/xoxo], # an simple, open outline format written in standard XHTML and # suitable for embedding in (X)HTML, Atom, RSS, and arbitrary XML. # module XOXO # xoxo.rb version number VERSION = "0.1" # Load and return a XOXO structure from the String, IO or StringIO or _xoxo_. # def self.load(xoxo) structs = Parser.new(xoxo).parse.structs while structs.kind_of?(Array) && structs.size == 1 structs = structs.first end structs end # Return a XOXO string corresponding to the Ruby object +struct+, # translated to the following rules: # # * Arrays become ordered lists
    . # * Hashes become definition lists
    , keys are # stringified with +to_s+. # * Everything else becomes stringified with +to_s+ and wrapped in # appropriate list elements (
  1. or
    /
    ). # # Additionally, you can pass these options on the _options_ hash: # :html_wrap => +true+:: Wrap the XOXO with a basic XHTML 1.0 # Transitional header. # :css => _css_:: Reference _css_ as stylesheet for the # wrapped XOXO document. # def self.dump(struct, options={}) struct = [struct] unless struct.kind_of? Array if options[:html_wrap] result = < EOF if options[:css] result << %Q[] end result << "" << make_xoxo(struct, 'xoxo') << "" else result = make_xoxo(struct, 'xoxo') end result end private def self.make_xoxo(struct, class_name=nil) s = '' case struct when Array if class_name s << %Q[
      ] else s << "
        " end struct.each { |item| s << "
      1. " << make_xoxo(item) << "
      2. " } s << "
      " when Hash d = struct.dup if d.has_key? 'url' s << '' << make_xoxo(text) << '' d.delete 'text' d.delete 'url' end unless d.empty? s << "
      " d.each { |key, value| s << "
      " << key.to_s << "
      " << make_xoxo(value) << "
      " } s << "
      " end when String s << struct else s << struct.to_s # too gentle? end s end end class XOXO::Parser # :nodoc: CONTAINER_TAGS = %w{dl ol ul} attr_reader :structs def initialize(xoxo) @parser = REXML::Parsers::PullParser.new(xoxo) @textstack = [''] @xostack = [] @structs = [] @tags = [] end def parse while @parser.has_next? res = @parser.pull if res.start_element? @tags << res[0] case res[0] when "a" attrs = normalize_attrs res[1] attrs['url'] = attrs['href'] attrs.delete 'href' push attrs @textstack << '' when "dl" push({}) when "ol", "ul" push [] when "li", "dt", "dd" @textstack << '' end elsif res.end_element? @tags.pop case res[0] when "a" val = @textstack.pop unless val.empty? val = '' if @xostack.last['title'] == val val = '' if @xostack.last['url'] == val @xostack.last['text'] = val unless val.empty? end @xostack.pop when "dl", "ol", "ul" @xostack.pop when "li" val = @textstack.pop while @structs.last != @xostack.last val = @structs.pop @xostack.last << val end @xostack.last << val if val.kind_of? String when "dt" # skip when "dd" val = @textstack.pop key = @textstack.pop val = @structs.pop if @structs.last != @xostack.last @xostack.last[key] = val end elsif res.text? unless @tags.empty? || CONTAINER_TAGS.include?(@tags.last) @textstack.last << res[0] end end end self end private def normalize_attrs(attrs) attrs.keys.find_all { |k, v| k != k.downcase }.each { |k, v| v = v.downcase if k == "rel" || k == "type" attrs.delete k attrs[k.downcase] = v } attrs end def push(struct) if struct == {} && @structs.last.kind_of?(Hash) && @structs.last.has_key?('url') && @structs.last != @xostack.last # put back the -made one for extra def's @xostack << @structs.last else @structs << struct @xostack << @structs.last end end end