# = xoxo.rb # # == Copyright (C) 2006 Christian Neukirchen # # Ruby License # # This module is free software. You may use, modify, and/or redistribute this # software under the same terms as Ruby. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. # # == Special Thanks # # Special thanks go to Christian Neukirchen for xoxo.rb. # * http://chneukirchen.org/repos/xoxo-rb/. # # == Author(s) # # * Christian Neukirchen # Author:: Christian Neukirchen # Copyright:: Copyright (c) 2006 Christian Neukirchen # License:: Ruby License require 'rexml/parsers/pullparser' # = XOXO # # XOXO is a Ruby XOXO parser and generator. It 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 class Object # Dump object as XOXO. def to_xoxo(*args) XOXO.dump(self,*args) end end # _____ _ # |_ _|__ ___| |_ # | |/ _ \/ __| __| # | | __/\__ \ |_ # |_|\___||___/\__| # =begin test require 'test/unit' class TCXOXO < Test::Unit::TestCase def test_simple_list l = ['1', '2', '3'] html = XOXO.dump(l) assert_equal '
      1. 1
      2. 2
      3. 3
      ', html end def test_nested_list l = ['1', ['2', '3']] assert_equal '
      1. 1
        1. 2
        2. 3
      ', XOXO.dump(l) end def test_hash h = {'test' => '1', 'name' => 'Kevin'} # Changed since Ruby sorts the hash differently. assert_equal '
      1. name
        Kevin
        test
        1
      ', XOXO.dump(h) end def test_single_item l = 'test' assert_equal '
      1. test
      ', XOXO.dump(l) end def test_wrap_differs l = 'test' html = XOXO.dump(l) html_wrap = XOXO.dump(l, :html_wrap => true) assert_not_equal html, html_wrap end def test_wrap_single_item l = 'test' html = XOXO.dump(l, :html_wrap => true) assert_equal <
      1. test
      EOF end def test_wrap_item_with_css l = 'test' html = XOXO.dump(l, :html_wrap => true, :css => 'reaptest.css') assert_equal <
      1. test
      EOF end def test_hash_roundtrip h = {'test' => '1', 'name' => 'Kevin'} assert_equal h, XOXO.load(XOXO.dump(h)) end def test_hash_with_url_roundtrip h = {'url' => 'http://example.com', 'name' => 'Kevin'} assert_equal h, XOXO.load(XOXO.dump(h)) end def test_nested_hash_roundtrip h = {'test' => '1', 'inner' => {'name' => 'Kevin'}} assert_equal h, XOXO.load(XOXO.dump(h)) end def test_nested_hash_with_url_roundtrip h = {'url' => 'http://example.com', 'inner' => { 'url' => 'http://slashdot.org', 'name' => 'Kevin'}} assert_equal h, XOXO.load(XOXO.dump(h)) end def test_list_round_trip l = ['3', '2', '1'] assert_equal l, XOXO.load(XOXO.dump(l)) end def test_list_of_hashes_round_trip l = ['3', {'a' => '2'}, {'b' => '1', 'c' => '4'}] assert_equal l, XOXO.load(XOXO.dump(l)) end def test_list_of_lists_round_trip l = ['3', ['a', '2'], ['b', ['1', ['c', '4']]]] assert_equal l, XOXO.load(XOXO.dump(l)) end def test_hashes_of_lists_roundtrip h = { 'test' => ['1', '2'], 'name' => 'Kevin', 'nestlist' => ['a', ['b', 'c']], 'nestdict' => {'e' => '6', 'f' => '7'} } assert_equal h, XOXO.load(XOXO.dump(h)) end def test_xoxo_junk_in_containers h = XOXO.load '
        bad
      1. worse
        good
        buy
        now
      ' assert_equal({'good' => 'buy'}, h) end def test_xoxo_junk_in_elements l = XOXO.load '
      1. bad
        good
        buy
        worse
      2. bag
        1. OK
        fish
      ' assert_equal([{'good' => 'buy'}, ['OK']], l) end def test_xoxo_with_spaces_and_newlines xoxo_sample = <
    1. text
      item 1
      description
      This item represents the main point we're trying to make.
      url
      http://example.com/more.xoxo
      title
      title of item 1
      type
      text/xml
      rel
      help
    EOF h = XOXO.load xoxo_sample h2 = { 'text' => 'item 1', 'description' => " This item represents the main point we're trying to make.", 'url' => 'http://example.com/more.xoxo', 'title' => 'title of item 1', 'type' => 'text/xml', 'rel' => 'help' } assert_equal h2, XOXO.load(xoxo_sample) end def test_special_attribute_decoding xoxo_sample = <
  2. text
    item 1
    url
    http://example.com/more.xoxo
    title
    title of item 1
    type
    text/xml
    rel
    help
EOF smart_xoxo_sample = <
  • item 1
  • EOF assert_equal XOXO.load(xoxo_sample), XOXO.load(smart_xoxo_sample) end def test_special_attribute_and_dl_decoding xoxo_sample = <
  • text
    item 1
    description
    This item represents the main point we're trying to make.
    url
    http://example.com/more.xoxo
    title
    title of item 1
    type
    text/xml
    rel
    help
  • EOF smart_xoxo_sample = <
  • item 1
    description
    This item represents the main point we're trying to make.
  • EOF assert_equal XOXO.load(xoxo_sample), XOXO.load(smart_xoxo_sample) end def test_special_attribute_encode h = { 'url' => 'http://example.com/more.xoxo', 'title' => 'sample url', 'type' => "text/xml", 'rel' => 'help', 'text' => 'an example' } assert_equal '
    1. an example
    ', XOXO.dump(h) end def test_special_attribute_roundtrip_full h = { 'url' => 'http://example.com/more.xoxo', 'title' => 'sample url', 'type' => "text/xml", 'rel' => 'help', 'text' => 'an example' } assert_equal h, XOXO.load(XOXO.dump(h)) end def test_special_attribute_roundtrip_no_text h = { 'url' => 'http://example.com/more.xoxo', 'title' => 'sample url', 'type' => "text/xml", 'rel' => 'help' } assert_equal h, XOXO.load(XOXO.dump(h)) end def test_special_attribute_roundtrip_no_text_or_title h = {'url' => 'http://example.com/more.xoxo'} assert_equal h, XOXO.load(XOXO.dump(h)) end def test_attention_roundtrip kmattn = <
  • Boing Boing Blog
    alturls
    1. xmlurl
    description
    Boing Boing Blog
  • Financial Cryptography
    alturls
    1. xmlurl
    description
    Financial Cryptography
  • HubLog
    alturls
    1. xmlurl
    2. foafurl
    description
    HubLog
  • EOF assert_equal kmattn, XOXO.dump(XOXO.load(kmattn)) assert_equal XOXO.load(kmattn), XOXO.load(XOXO.dump(XOXO.load(kmattn))) assert_equal XOXO.dump(XOXO.load(kmattn)), XOXO.dump(XOXO.load(XOXO.dump(XOXO.load(kmattn)))) end def test_unicode_roundtrip unicode = "Tantek \xc3\x87elik and a snowman \xe2\x98\x83" assert_equal unicode, XOXO.load(XOXO.dump(unicode)) end # TBD: Implement proper encodings. # # def test_utf8_roundtrip # end # def test_windows1252_roundtrip # end end =end