microfiche 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,541 @@
1
+ # = xoxo.rb
2
+ #
3
+ # == Copyright (C) 2006 Christian Neukirchen
4
+ #
5
+ # Ruby License
6
+ #
7
+ # This module is free software. You may use, modify, and/or redistribute this
8
+ # software under the same terms as Ruby.
9
+ #
10
+ # This program is distributed in the hope that it will be useful, but WITHOUT
11
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12
+ # FOR A PARTICULAR PURPOSE.
13
+ #
14
+ # == Special Thanks
15
+ #
16
+ # Special thanks go to Christian Neukirchen for xoxo.rb.
17
+ # * http://chneukirchen.org/repos/xoxo-rb/.
18
+ #
19
+ # == Author(s)
20
+ #
21
+ # * Christian Neukirchen
22
+
23
+ # Author:: Christian Neukirchen
24
+ # Copyright:: Copyright (c) 2006 Christian Neukirchen
25
+ # License:: Ruby License
26
+
27
+ require 'rexml/parsers/pullparser'
28
+
29
+ # = XOXO
30
+ #
31
+ # XOXO is a Ruby XOXO parser and generator. It provides
32
+ # a Ruby API similar to Marshal and YAML (though more
33
+ # specific) to load and dump XOXO[http://microformats.org/wiki/xoxo],
34
+ # an simple, open outline format written in standard XHTML and
35
+ # suitable for embedding in (X)HTML, Atom, RSS, and arbitrary XML.
36
+
37
+ module XOXO
38
+
39
+ # xoxo.rb version number
40
+ #VERSION = "0.1"
41
+
42
+ # Load and return a XOXO structure from the String, IO or StringIO or _xoxo_.
43
+ #
44
+ def self.load(xoxo)
45
+ structs = Parser.new(xoxo).parse.structs
46
+ while structs.kind_of?(Array) && structs.size == 1
47
+ structs = structs.first
48
+ end
49
+ structs
50
+ end
51
+
52
+ # Return a XOXO string corresponding to the Ruby object +struct+,
53
+ # translated to the following rules:
54
+ #
55
+ # * Arrays become ordered lists <tt><ol></tt>.
56
+ # * Hashes become definition lists <tt><dl></tt>, keys are
57
+ # stringified with +to_s+.
58
+ # * Everything else becomes stringified with +to_s+ and wrapped in
59
+ # appropriate list elements (<tt><li></tt> or <tt><dt></tt>/<tt><dd></tt>).
60
+ #
61
+ # Additionally, you can pass these options on the _options_ hash:
62
+ # <tt>:html_wrap</tt> => +true+:: Wrap the XOXO with a basic XHTML 1.0
63
+ # Transitional header.
64
+ # <tt>:css</tt> => _css_:: Reference _css_ as stylesheet for the
65
+ # wrapped XOXO document.
66
+ #
67
+ def self.dump(struct, options={})
68
+ struct = [struct] unless struct.kind_of? Array
69
+
70
+ if options[:html_wrap]
71
+ result = <<EOF.strip
72
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
73
+ http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
74
+ <html xmlns="http://www.w3.org/1999/xhtml"><head profile=""><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
75
+ EOF
76
+ if options[:css]
77
+ result << %Q[<style type="text/css" >@import "#{options[:css]}";</style>]
78
+ end
79
+
80
+ result << "</head><body>" << make_xoxo(struct, 'xoxo') << "</body></html>"
81
+ else
82
+ result = make_xoxo(struct, 'xoxo')
83
+ end
84
+
85
+ result
86
+ end
87
+
88
+ private
89
+
90
+ def self.make_xoxo(struct, class_name=nil)
91
+ s = ''
92
+ case struct
93
+ when Array
94
+ if class_name
95
+ s << %Q[<ol class="#{class_name}">]
96
+ else
97
+ s << "<ol>"
98
+ end
99
+ struct.each { |item|
100
+ s << "<li>" << make_xoxo(item) << "</li>"
101
+ }
102
+ s << "</ol>"
103
+
104
+ when Hash
105
+ d = struct.dup
106
+ if d.has_key? 'url'
107
+ s << '<a href="' << d['url'].to_s << '" '
108
+ text = d.fetch('text') { d.fetch('title', d['url']) }
109
+ %w[title rel type].each { |tag|
110
+ if d.has_key? tag
111
+ s << tag.to_s << '="' << make_xoxo(d.delete(tag)) << '" '
112
+ end
113
+ }
114
+ s << '>' << make_xoxo(text) << '</a>'
115
+ d.delete 'text'
116
+ d.delete 'url'
117
+ end
118
+
119
+ unless d.empty?
120
+ s << "<dl>"
121
+ d.each { |key, value|
122
+ s << "<dt>" << key.to_s << "</dt><dd>" << make_xoxo(value) << "</dd>"
123
+ }
124
+ s << "</dl>"
125
+ end
126
+
127
+ when String
128
+ s << struct
129
+
130
+ else
131
+ s << struct.to_s # too gentle?
132
+ end
133
+
134
+ s
135
+ end
136
+ end
137
+
138
+ class XOXO::Parser # :nodoc:
139
+ CONTAINER_TAGS = %w{dl ol ul}
140
+
141
+ attr_reader :structs
142
+
143
+ def initialize(xoxo)
144
+ @parser = REXML::Parsers::PullParser.new(xoxo)
145
+
146
+ @textstack = ['']
147
+ @xostack = []
148
+ @structs = []
149
+ @tags = []
150
+ end
151
+
152
+ def parse
153
+ while @parser.has_next?
154
+ res = @parser.pull
155
+
156
+ if res.start_element?
157
+ @tags << res[0]
158
+
159
+ case res[0]
160
+ when "a"
161
+ attrs = normalize_attrs res[1]
162
+ attrs['url'] = attrs['href']
163
+ attrs.delete 'href'
164
+ push attrs
165
+ @textstack << ''
166
+
167
+ when "dl"
168
+ push({})
169
+
170
+ when "ol", "ul"
171
+ push []
172
+
173
+ when "li", "dt", "dd"
174
+ @textstack << ''
175
+
176
+ end
177
+ elsif res.end_element?
178
+ @tags.pop
179
+
180
+ case res[0]
181
+ when "a"
182
+ val = @textstack.pop
183
+ unless val.empty?
184
+ val = '' if @xostack.last['title'] == val
185
+ val = '' if @xostack.last['url'] == val
186
+ @xostack.last['text'] = val unless val.empty?
187
+ end
188
+ @xostack.pop
189
+
190
+ when "dl", "ol", "ul"
191
+ @xostack.pop
192
+
193
+ when "li"
194
+ val = @textstack.pop
195
+ while @structs.last != @xostack.last
196
+ val = @structs.pop
197
+ @xostack.last << val
198
+ end
199
+ @xostack.last << val if val.kind_of? String
200
+
201
+ when "dt"
202
+ # skip
203
+
204
+ when "dd"
205
+ val = @textstack.pop
206
+ key = @textstack.pop
207
+
208
+ val = @structs.pop if @structs.last != @xostack.last
209
+ @xostack.last[key] = val
210
+
211
+ end
212
+ elsif res.text?
213
+ unless @tags.empty? || CONTAINER_TAGS.include?(@tags.last)
214
+ @textstack.last << res[0]
215
+ end
216
+ end
217
+ end
218
+
219
+ self
220
+ end
221
+
222
+ private
223
+
224
+ def normalize_attrs(attrs)
225
+ attrs.keys.find_all { |k, v| k != k.downcase }.each { |k, v|
226
+ v = v.downcase if k == "rel" || k == "type"
227
+ attrs.delete k
228
+ attrs[k.downcase] = v
229
+ }
230
+ attrs
231
+ end
232
+
233
+ def push(struct)
234
+ if struct == {} && @structs.last.kind_of?(Hash) &&
235
+ @structs.last.has_key?('url') &&
236
+ @structs.last != @xostack.last
237
+ # put back the <a>-made one for extra def's
238
+ @xostack << @structs.last
239
+ else
240
+ @structs << struct
241
+ @xostack << @structs.last
242
+ end
243
+ end
244
+ end
245
+
246
+
247
+ class Object
248
+
249
+ # Dump object as XOXO.
250
+
251
+ def to_xoxo(*args)
252
+ XOXO.dump(self,*args)
253
+ end
254
+
255
+ end
256
+
257
+
258
+ # _____ _
259
+ # |_ _|__ ___| |_
260
+ # | |/ _ \/ __| __|
261
+ # | | __/\__ \ |_
262
+ # |_|\___||___/\__|
263
+ #
264
+
265
+ =begin test
266
+
267
+ require 'test/unit'
268
+
269
+ class TCXOXO < Test::Unit::TestCase
270
+
271
+ def test_simple_list
272
+ l = ['1', '2', '3']
273
+ html = XOXO.dump(l)
274
+ assert_equal '<ol class="xoxo"><li>1</li><li>2</li><li>3</li></ol>', html
275
+ end
276
+
277
+ def test_nested_list
278
+ l = ['1', ['2', '3']]
279
+ assert_equal '<ol class="xoxo"><li>1</li><li><ol><li>2</li><li>3</li></ol></li></ol>', XOXO.dump(l)
280
+ end
281
+
282
+ def test_hash
283
+ h = {'test' => '1', 'name' => 'Kevin'}
284
+ # Changed since Ruby sorts the hash differently.
285
+ assert_equal '<ol class="xoxo"><li><dl><dt>name</dt><dd>Kevin</dd><dt>test</dt><dd>1</dd></dl></li></ol>', XOXO.dump(h)
286
+ end
287
+
288
+ def test_single_item
289
+ l = 'test'
290
+ assert_equal '<ol class="xoxo"><li>test</li></ol>', XOXO.dump(l)
291
+ end
292
+
293
+ def test_wrap_differs
294
+ l = 'test'
295
+ html = XOXO.dump(l)
296
+ html_wrap = XOXO.dump(l, :html_wrap => true)
297
+ assert_not_equal html, html_wrap
298
+ end
299
+
300
+ def test_wrap_single_item
301
+ l = 'test'
302
+ html = XOXO.dump(l, :html_wrap => true)
303
+ assert_equal <<EOF.strip, html
304
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
305
+ http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
306
+ <html xmlns="http://www.w3.org/1999/xhtml"><head profile=""><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /></head><body><ol class="xoxo"><li>test</li></ol></body></html>
307
+ EOF
308
+ end
309
+
310
+ def test_wrap_item_with_css
311
+ l = 'test'
312
+ html = XOXO.dump(l, :html_wrap => true, :css => 'reaptest.css')
313
+ assert_equal <<EOF.strip, html
314
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN
315
+ http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
316
+ <html xmlns="http://www.w3.org/1999/xhtml"><head profile=""><meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><style type="text/css" >@import "reaptest.css";</style></head><body><ol class="xoxo"><li>test</li></ol></body></html>
317
+ EOF
318
+ end
319
+
320
+ def test_hash_roundtrip
321
+ h = {'test' => '1', 'name' => 'Kevin'}
322
+ assert_equal h, XOXO.load(XOXO.dump(h))
323
+ end
324
+
325
+ def test_hash_with_url_roundtrip
326
+ h = {'url' => 'http://example.com', 'name' => 'Kevin'}
327
+ assert_equal h, XOXO.load(XOXO.dump(h))
328
+ end
329
+
330
+ def test_nested_hash_roundtrip
331
+ h = {'test' => '1', 'inner' => {'name' => 'Kevin'}}
332
+ assert_equal h, XOXO.load(XOXO.dump(h))
333
+ end
334
+
335
+ def test_nested_hash_with_url_roundtrip
336
+ h = {'url' => 'http://example.com', 'inner' => {
337
+ 'url' => 'http://slashdot.org', 'name' => 'Kevin'}}
338
+ assert_equal h, XOXO.load(XOXO.dump(h))
339
+ end
340
+
341
+ def test_list_round_trip
342
+ l = ['3', '2', '1']
343
+ assert_equal l, XOXO.load(XOXO.dump(l))
344
+ end
345
+
346
+ def test_list_of_hashes_round_trip
347
+ l = ['3', {'a' => '2'}, {'b' => '1', 'c' => '4'}]
348
+ assert_equal l, XOXO.load(XOXO.dump(l))
349
+ end
350
+
351
+ def test_list_of_lists_round_trip
352
+ l = ['3', ['a', '2'], ['b', ['1', ['c', '4']]]]
353
+ assert_equal l, XOXO.load(XOXO.dump(l))
354
+ end
355
+
356
+ def test_hashes_of_lists_roundtrip
357
+ h = {
358
+ 'test' => ['1', '2'],
359
+ 'name' => 'Kevin',
360
+ 'nestlist' => ['a', ['b', 'c']],
361
+ 'nestdict' => {'e' => '6', 'f' => '7'}
362
+ }
363
+ assert_equal h, XOXO.load(XOXO.dump(h))
364
+ end
365
+
366
+ def test_xoxo_junk_in_containers
367
+ h = XOXO.load '<ol>bad<li><dl>worse<dt>good</dt><dd>buy</dd> now</dl></li></ol>'
368
+ assert_equal({'good' => 'buy'}, h)
369
+ end
370
+
371
+ def test_xoxo_junk_in_elements
372
+ l = XOXO.load '<ol><li>bad<dl><dt>good</dt><dd>buy</dd></dl>worse</li><li>bag<ol><li>OK</li></ol>fish</li></ol>'
373
+ assert_equal([{'good' => 'buy'}, ['OK']], l)
374
+ end
375
+
376
+ def test_xoxo_with_spaces_and_newlines
377
+ xoxo_sample = <<EOF.strip
378
+ <ol class='xoxo'>
379
+ <li>
380
+ <dl>
381
+ <dt>text</dt>
382
+ <dd>item 1</dd>
383
+ <dt>description</dt>
384
+ <dd> This item represents the main point we're trying to make.</dd>
385
+ <dt>url</dt>
386
+ <dd>http://example.com/more.xoxo</dd>
387
+ <dt>title</dt>
388
+ <dd>title of item 1</dd>
389
+ <dt>type</dt>
390
+ <dd>text/xml</dd>
391
+ <dt>rel</dt>
392
+ <dd>help</dd>
393
+ </dl>
394
+ </li>
395
+ </ol>
396
+ EOF
397
+ h = XOXO.load xoxo_sample
398
+ h2 = {
399
+ 'text' => 'item 1',
400
+ 'description' => " This item represents the main point we're trying to make.",
401
+ 'url' => 'http://example.com/more.xoxo',
402
+ 'title' => 'title of item 1',
403
+ 'type' => 'text/xml',
404
+ 'rel' => 'help'
405
+ }
406
+ assert_equal h2, XOXO.load(xoxo_sample)
407
+ end
408
+
409
+ def test_special_attribute_decoding
410
+ xoxo_sample = <<EOF.strip
411
+ <ol class='xoxo'>
412
+ <li>
413
+ <dl>
414
+ <dt>text</dt>
415
+ <dd>item 1</dd>
416
+ <dt>url</dt>
417
+ <dd>http://example.com/more.xoxo</dd>
418
+ <dt>title</dt>
419
+ <dd>title of item 1</dd>
420
+ <dt>type</dt>
421
+ <dd>text/xml</dd>
422
+ <dt>rel</dt>
423
+ <dd>help</dd>
424
+ </dl>
425
+ </li>
426
+ </ol>
427
+ EOF
428
+ smart_xoxo_sample = <<EOF.strip
429
+ <ol class='xoxo'>
430
+ <li><a href="http://example.com/more.xoxo"
431
+ title="title of item 1"
432
+ type="text/xml"
433
+ rel="help">item 1</a>
434
+ <!-- note how the "text" property is simply the contents of the <a> element -->
435
+ </li>
436
+ </ol>
437
+ EOF
438
+ assert_equal XOXO.load(xoxo_sample), XOXO.load(smart_xoxo_sample)
439
+ end
440
+
441
+ def test_special_attribute_and_dl_decoding
442
+ xoxo_sample = <<EOF.strip
443
+ <ol class="xoxo">
444
+ <li>
445
+ <dl>
446
+ <dt>text</dt>
447
+ <dd>item 1</dd>
448
+ <dt>description</dt>
449
+ <dd> This item represents the main point we're trying to make.</dd>
450
+ <dt>url</dt>
451
+ <dd>http://example.com/more.xoxo</dd>
452
+ <dt>title</dt>
453
+ <dd>title of item 1</dd>
454
+ <dt>type</dt>
455
+ <dd>text/xml</dd>
456
+ <dt>rel</dt>
457
+ <dd>help</dd>
458
+ </dl>
459
+ </li>
460
+ </ol>
461
+ EOF
462
+ smart_xoxo_sample = <<EOF.strip
463
+ <ol class="xoxo">
464
+ <li><a href="http://example.com/more.xoxo"
465
+ title="title of item 1"
466
+ type="text/xml"
467
+ rel="help">item 1</a>
468
+ <!-- note how the "text" property is simply the contents of the <a> element -->
469
+ <dl>
470
+ <dt>description</dt>
471
+ <dd> This item represents the main point we're trying to make.</dd>
472
+ </dl>
473
+ </li>
474
+ </ol>
475
+ EOF
476
+ assert_equal XOXO.load(xoxo_sample), XOXO.load(smart_xoxo_sample)
477
+ end
478
+
479
+ def test_special_attribute_encode
480
+ h = {
481
+ 'url' => 'http://example.com/more.xoxo',
482
+ 'title' => 'sample url',
483
+ 'type' => "text/xml",
484
+ 'rel' => 'help',
485
+ 'text' => 'an example'
486
+ }
487
+ assert_equal '<ol class="xoxo"><li><a href="http://example.com/more.xoxo" title="sample url" rel="help" type="text/xml" >an example</a></li></ol>', XOXO.dump(h)
488
+ end
489
+
490
+ def test_special_attribute_roundtrip_full
491
+ h = {
492
+ 'url' => 'http://example.com/more.xoxo',
493
+ 'title' => 'sample url',
494
+ 'type' => "text/xml",
495
+ 'rel' => 'help',
496
+ 'text' => 'an example'
497
+ }
498
+ assert_equal h, XOXO.load(XOXO.dump(h))
499
+ end
500
+
501
+ def test_special_attribute_roundtrip_no_text
502
+ h = {
503
+ 'url' => 'http://example.com/more.xoxo',
504
+ 'title' => 'sample url',
505
+ 'type' => "text/xml",
506
+ 'rel' => 'help'
507
+ }
508
+ assert_equal h, XOXO.load(XOXO.dump(h))
509
+ end
510
+
511
+ def test_special_attribute_roundtrip_no_text_or_title
512
+ h = {'url' => 'http://example.com/more.xoxo'}
513
+ assert_equal h, XOXO.load(XOXO.dump(h))
514
+ end
515
+
516
+ def test_attention_roundtrip
517
+ kmattn = <<EOF.strip
518
+ <ol class="xoxo"><li><a href="http://www.boingboing.net/" title="Boing Boing Blog" >Boing Boing Blog</a><dl><dt>alturls</dt><dd><ol><li><a href="http://boingboing.net/rss.xml" >xmlurl</a></li></ol></dd><dt>description</dt><dd>Boing Boing Blog</dd></dl></li><li><a href="http://www.financialcryptography.com/" title="Financial Cryptography" >Financial Cryptography</a><dl><dt>alturls</dt><dd><ol><li><a href="http://www.financialcryptography.com/mt/index.rdf" >xmlurl</a></li></ol></dd><dt>description</dt><dd>Financial Cryptography</dd></dl></li><li><a href="http://hublog.hubmed.org/" title="HubLog" >HubLog</a><dl><dt>alturls</dt><dd><ol><li><a href="http://hublog.hubmed.org/index.xml" >xmlurl</a></li><li><a href="http://hublog.hubmed.org/foaf.rdf" >foafurl</a></li></ol></dd><dt>description</dt><dd>HubLog</dd></dl></li></ol>
519
+ EOF
520
+ assert_equal kmattn, XOXO.dump(XOXO.load(kmattn))
521
+ assert_equal XOXO.load(kmattn), XOXO.load(XOXO.dump(XOXO.load(kmattn)))
522
+ assert_equal XOXO.dump(XOXO.load(kmattn)),
523
+ XOXO.dump(XOXO.load(XOXO.dump(XOXO.load(kmattn))))
524
+ end
525
+
526
+ def test_unicode_roundtrip
527
+ unicode = "Tantek \xc3\x87elik and a snowman \xe2\x98\x83"
528
+ assert_equal unicode, XOXO.load(XOXO.dump(unicode))
529
+ end
530
+
531
+ # TBD: Implement proper encodings.
532
+ #
533
+ # def test_utf8_roundtrip
534
+ # end
535
+ # def test_windows1252_roundtrip
536
+ # end
537
+ end
538
+
539
+ =end
540
+
541
+