# = 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 ( 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 << "- " << make_xoxo(item) << "
"
}
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
- 2
- 3
', html
end
def test_nested_list
l = ['1', ['2', '3']]
assert_equal '- 1
- 2
- 3
', XOXO.dump(l)
end
def test_hash
h = {'test' => '1', 'name' => 'Kevin'}
# Changed since Ruby sorts the hash differently.
assert_equal '- name
- Kevin
- test
- 1
', XOXO.dump(h)
end
def test_single_item
l = 'test'
assert_equal '- 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 <
- test
EOF
end
def test_wrap_item_with_css
l = 'test'
html = XOXO.dump(l, :html_wrap => true, :css => 'reaptest.css')
assert_equal <
- 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 'badworse- good
- buy
now
'
assert_equal({'good' => 'buy'}, h)
end
def test_xoxo_junk_in_elements
l = XOXO.load '- bad
- good
- buy
worse - bag
- OK
fish
'
assert_equal([{'good' => 'buy'}, ['OK']], l)
end
def test_xoxo_with_spaces_and_newlines
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
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 = <
- 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 '- 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
- xmlurl
- description
- Boing Boing Blog
Financial Cryptography- alturls
- xmlurl
- description
- Financial Cryptography
HubLog- alturls
- xmlurl
- 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