zenweb 2.18.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.
Files changed (57) hide show
  1. data.tar.gz.sig +0 -0
  2. data/History.txt +426 -0
  3. data/Manifest.txt +54 -0
  4. data/README.txt +63 -0
  5. data/Rakefile +22 -0
  6. data/bin/zenweb +27 -0
  7. data/bin/zenwebpage +66 -0
  8. data/bin/zenwebsite +39 -0
  9. data/design/REQUIREMENTS.txt +52 -0
  10. data/design/ZENWEB_2.txt +69 -0
  11. data/design/heirarchy.png +0 -0
  12. data/design/heirarchy.tgif +311 -0
  13. data/docs/Customizing +76 -0
  14. data/docs/FAQ +12 -0
  15. data/docs/Features +128 -0
  16. data/docs/Presentation +88 -0
  17. data/docs/QuickStart +32 -0
  18. data/docs/Renderers +85 -0
  19. data/docs/SiteMap +13 -0
  20. data/docs/YourOwnWebsite +32 -0
  21. data/docs/index +14 -0
  22. data/docs/metadata.txt +10 -0
  23. data/lib/ZenWeb.rb +850 -0
  24. data/lib/ZenWeb/CalendarRenderer.rb +162 -0
  25. data/lib/ZenWeb/CompactRenderer.rb +45 -0
  26. data/lib/ZenWeb/CompositeRenderer.rb +63 -0
  27. data/lib/ZenWeb/FileAttachmentRenderer.rb +57 -0
  28. data/lib/ZenWeb/FooterRenderer.rb +38 -0
  29. data/lib/ZenWeb/GenericRenderer.rb +143 -0
  30. data/lib/ZenWeb/HeaderRenderer.rb +52 -0
  31. data/lib/ZenWeb/HtmlRenderer.rb +81 -0
  32. data/lib/ZenWeb/HtmlTableRenderer.rb +94 -0
  33. data/lib/ZenWeb/HtmlTemplateRenderer.rb +173 -0
  34. data/lib/ZenWeb/MetadataRenderer.rb +83 -0
  35. data/lib/ZenWeb/RelativeRenderer.rb +98 -0
  36. data/lib/ZenWeb/RubyCodeRenderer.rb +56 -0
  37. data/lib/ZenWeb/SitemapRenderer.rb +56 -0
  38. data/lib/ZenWeb/StandardRenderer.rb +40 -0
  39. data/lib/ZenWeb/StupidRenderer.rb +88 -0
  40. data/lib/ZenWeb/SubpageRenderer.rb +45 -0
  41. data/lib/ZenWeb/TextToHtmlRenderer.rb +219 -0
  42. data/lib/ZenWeb/TocRenderer.rb +61 -0
  43. data/lib/ZenWeb/XXXRenderer.rb +32 -0
  44. data/test/SiteMap +14 -0
  45. data/test/Something +4 -0
  46. data/test/include.txt +3 -0
  47. data/test/index +8 -0
  48. data/test/metadata.txt +10 -0
  49. data/test/ryand/SiteMap +10 -0
  50. data/test/ryand/blah +4 -0
  51. data/test/ryand/blah-blah +4 -0
  52. data/test/ryand/index +52 -0
  53. data/test/ryand/metadata.txt +2 -0
  54. data/test/ryand/stuff/index +4 -0
  55. data/test/test_zenweb.rb +1624 -0
  56. metadata +161 -0
  57. metadata.gz.sig +0 -0
@@ -0,0 +1,14 @@
1
+ # "title" = "ZenWeb"
2
+ # "subtitle" = "Demo and Instructions"
3
+ # "description" = "Demo of ZenWeb"
4
+ # "keywords" = "ZenWeb, Stuff"
5
+
6
+ Welcome to ZenWeb. If you are viewing the plain-text version of this
7
+ document, I urge you to run 'make' and view the HTML version
8
+ instead. It at the very least helps demonstrate some of the
9
+ capabilities of ZenWeb. After that, you might want to start with
10
+ #{QuickStart}.
11
+
12
+ Eventually, visit all of the subpages below to learn how to use
13
+ ZenWeb. If I am missing anything or you'd like to make suggestions,
14
+ email #{support}.
@@ -0,0 +1,10 @@
1
+
2
+ 'renderers' = ['TocRenderer', 'HtmlTableRenderer', 'StandardRenderer', 'RelativeRenderer' ]
3
+
4
+ 'QuickStart' = '<A HREF="QuickStart.html">QuickStart</A>'
5
+ 'customizing' = '<A HREF="Customizing.html">customizing</A>'
6
+ 'Features' = '<A HREF="Features.html">features</A>'
7
+ 'TextToHTML' = '<A HREF="Features.html">Text-To-HTML</A>'
8
+ 'Your Own Website' = '<A HREF="YourOwnWebsite.html">Your Own Website</A>'
9
+ 'raa' = '<A HREF="http://www.ruby-lang.org/en/raa.html">Ruby Application Archive</A>'
10
+ 'support' = '<A HREF="mailto:support-zenweb@ZenSpider.com">support</A>'
@@ -0,0 +1,850 @@
1
+ #!/usr/local/bin/ruby -w
2
+
3
+ require 'ftools' # for File::* below
4
+
5
+ $TESTING = FALSE unless defined? $TESTING
6
+
7
+ # this is due to a stupid bug across 1.6.4, 1.6.7, and 1.7.2.
8
+ $PARAGRAPH_RE = Regexp.new( $/ * 2 + "+")
9
+ $PARAGRAPH_END_RE = Regexp.new( "^" + $/ + "+")
10
+
11
+ =begin
12
+ = ZenWeb
13
+
14
+ A set of classes for organizing and formating a collection of related
15
+ documents.
16
+
17
+ = SYNOPSIS
18
+
19
+ ZenWeb.rb directory
20
+
21
+ = DESCRIPTION
22
+
23
+ A ZenWebsite is a collection of documents in one or more directories,
24
+ organized by a sitemap. The sitemap references every document in the
25
+ collection and maintains their order and hierarchy.
26
+
27
+ Each directory may contain a metadata file of key/value pairs that can
28
+ be used by ZenWeb and by the documents themselves. Each metadata file
29
+ can override values from the metadata file in the parent
30
+ directory. Each document can also define metadata, which will also
31
+ override any values from the metadata files.
32
+
33
+ ZenWeb processes the sitemap and in turn all related documents. ZenWeb
34
+ uses a series of renderers (determined by metadata) to process the
35
+ documents and writes the end result to disk.
36
+
37
+ There are 5 major classes:
38
+
39
+ * ((<Class ZenWebsite>))
40
+ * ((<Class ZenDocument>))
41
+ * ((<Class ZenSitemap>))
42
+ * ((<Class Metadata>))
43
+ * ((<Class GenericRenderer>))
44
+
45
+ And many renderer classes, now located separately in the ZenWeb
46
+ sub-directory. For example:
47
+
48
+ * ((<Class SitemapRenderer>))
49
+ * ((<Class HtmlRenderer>))
50
+ * ((<Class HtmlTemplateRenderer>))
51
+ * ((<Class TextToHtmlRenderer>))
52
+ * ((<Class HeaderRenderer>))
53
+ * ((<Class FooterRenderer>))
54
+
55
+ =end
56
+
57
+ =begin
58
+
59
+ = Class ZenWebsite
60
+
61
+ ZenWebsite is the top level class. It is responsible for driving the
62
+ process.
63
+
64
+ === Methods
65
+
66
+ =end
67
+
68
+ class ZenWebsite
69
+
70
+ VERSION = '2.18.0'
71
+
72
+ attr_reader :datadir, :htmldir, :sitemap
73
+ attr_reader :documents if $TESTING
74
+ attr_reader :doc_order if $TESTING
75
+
76
+ =begin
77
+
78
+ --- ZenWebsite.new(sitemapURL, datadir, htmldir)
79
+
80
+ Creates a new ZenWebsite instance and preprocesses the sitemap and
81
+ all referenced documents.
82
+
83
+ =end
84
+
85
+ def initialize(sitemapUrl, datadir, htmldir)
86
+
87
+ unless (test(?d, datadir)) then
88
+ raise ArgumentError, "datadir must be a valid directory"
89
+ end
90
+
91
+ @datadir = datadir
92
+ @htmldir = htmldir
93
+ @sitemap = ZenSitemap.new(sitemapUrl, self)
94
+ @documents = @sitemap.documents
95
+ @doc_order = @sitemap.doc_order
96
+
97
+ # Tell each document to notify it's parent about itself.
98
+ @doc_order.each { | url |
99
+ doc = self[url]
100
+ parentURL = doc.parentURL
101
+ parentDoc = self[parentURL]
102
+ if (parentDoc and parentURL != url) then
103
+ parentDoc.addSubpage(doc.url)
104
+ end
105
+ }
106
+
107
+ end
108
+
109
+ =begin
110
+
111
+ --- ZenWebsite#renderSite
112
+
113
+ Iterates over all of the documents and asks them to
114
+ ((<render|ZenDocument#render>)).
115
+
116
+ =end
117
+
118
+ def renderSite()
119
+
120
+ puts "Generating website..." unless $TESTING
121
+ force = false
122
+ unless (test(?d, self.htmldir)) then
123
+ File::makedirs(self.htmldir)
124
+ else
125
+ # NOTE: It would be better to know what was changed and only
126
+ # rerender them and their previous and current immediate
127
+ # relatives.
128
+
129
+ # HACK: found a bug at the last minute. Looks minor, but I'm
130
+ # disabling this in case it's too annoying.
131
+ # force = self.sitemap.newerThanTarget
132
+ end
133
+
134
+ if force then
135
+ puts "Sitemap modified, regenerating entire website." unless $TESTING
136
+ end
137
+
138
+ @doc_order.each { | url |
139
+ doc = @documents[url]
140
+
141
+ doc.render(force)
142
+ }
143
+
144
+ self
145
+ end
146
+
147
+ ############################################################
148
+ # Accessors:
149
+
150
+ =begin
151
+
152
+ --- ZenWebsite#[](url)
153
+
154
+ Accesses a document by url.
155
+
156
+ =end
157
+
158
+ def [](url)
159
+ return @documents[url] || nil
160
+ end
161
+
162
+ =begin
163
+
164
+ --- ZenWebsite.banner()
165
+
166
+ Returns a string containing the ZenWeb banner including the version.
167
+
168
+ =end
169
+
170
+ def ZenWebsite.banner()
171
+ return "ZenWeb v. #{ZenWebsite::VERSION} http://www.zenspider.com/ZSS/Products/ZenWeb/"
172
+ end
173
+
174
+ def top
175
+ self[@doc_order.first]
176
+ end
177
+
178
+ end
179
+
180
+ =begin
181
+
182
+ = Class ZenDocument
183
+ A ZenDocument is an object representing a unit of input data,
184
+ typically a file. It may correspond to multiple output data (one
185
+ document could create several HTML pages).
186
+ === Methods
187
+
188
+ =end
189
+
190
+ class ZenDocument
191
+
192
+ # 1.8 has a bug in it that causes MASSIVE slowdown with cyclic
193
+ # object graphs. The fix has been submitted, but won't be released
194
+ # until 1.8.2 or above. This is a hacky workaround that makes
195
+ # running tolerable. I should come up with a better solution to deal
196
+ # with debugging, but I haven't actually needed to debug in a while.
197
+ # Basically, avoid ever showing the website or sitemap in an inspect.
198
+
199
+ if false and VERSION =~ /^1\.8/ then
200
+ def inspect
201
+ return "<#{self.class}\@#{self.object_id}: #{self.url}>"
202
+ end
203
+ end
204
+
205
+ # These are done manually:
206
+ # attr_reader :datapath, :htmlpath, :metadata
207
+ attr_reader :url, :subpages, :website, :content
208
+ attr_writer :content if $TESTING
209
+
210
+ =begin
211
+
212
+ --- ZenDocument.new(url, website)
213
+
214
+ Creates a new ZenDocument instance and preprocesses the metadata.
215
+
216
+ =end
217
+
218
+ def initialize(url, website)
219
+
220
+ raise ArgumentError, "url was nil" if url.nil?
221
+ raise ArgumentError, "web was nil" if website.nil?
222
+
223
+ @url = url
224
+ @website = website
225
+ @datapath = nil
226
+ @htmlpath = nil
227
+ @subpages = []
228
+ @content = ""
229
+
230
+ unless (test(?f, self.datapath)) then
231
+ raise ArgumentError, "url #{url} doesn't exist in #{self.datadir} (#{self.datapath})"
232
+ end
233
+
234
+ @metadata = nil
235
+
236
+ end
237
+
238
+ =begin
239
+
240
+ --- ZenDocument#parseMetadata
241
+
242
+ Opens the datafile and preparses the content for metadata. In a
243
+ document, metadata has the basic form of "# key = val" where key
244
+ and val are both proper ruby representations of the values in
245
+ question. Eval is used to convert them from textual representation
246
+ to an actual ruby object.
247
+
248
+ =end
249
+
250
+ def parseMetadata
251
+ # 1) Open file
252
+ # 2) Parse w/ generic parser for metadata, stripping it out.
253
+ count = 0
254
+
255
+ page = []
256
+
257
+ IO.foreach(self.datapath) { | line |
258
+ count += 1
259
+ # REFACTOR: class Metadata also has this.
260
+ if (line =~ /^\#\s*(\"(?:\\.|[^\"]+)\"|[^=]+)\s*=\s*(.*?)\s*$/) then
261
+ begin
262
+ key = $1
263
+ val = $2
264
+
265
+ key = eval(key)
266
+ val = eval(val)
267
+ rescue Exception
268
+ $stderr.puts "#{self.datapath}:#{count}: eval failed: #{line}"
269
+ else
270
+ self[key] = val
271
+ end
272
+ else
273
+ page.push(line)
274
+ end
275
+ }
276
+
277
+ @content = page.join('')
278
+ end
279
+
280
+ =begin
281
+
282
+ --- ZenDocument#renderContent
283
+
284
+ Renders the content of the document by passing the content to a
285
+ series of renderers. The renderers are specified by metadata as an
286
+ array of strings and each one must implement the GenericRenderer
287
+ interface.
288
+
289
+ =end
290
+
291
+ def renderContent()
292
+
293
+ # FIX this is mainly here to force the rendering of the metadata,
294
+ # which also forces the population of @content.
295
+ title = self['title']
296
+
297
+ # contents already preparsed for metadata
298
+ result = self.content
299
+
300
+ # 3) Use metadata to determine the rest of the renderers.
301
+ renderers = self['renderers'] || [ 'GenericRenderer' ]
302
+
303
+ # 4) For each renderer in list:
304
+
305
+ renderers.each { | rendererName |
306
+
307
+ # 4.1) Invoke a renderer by that name
308
+
309
+ renderer = nil
310
+ begin
311
+
312
+ # try to find ZenWeb/blah.rb first, then just blah.rb.
313
+ begin
314
+ require "ZenWeb/#{rendererName}"
315
+ rescue LoadError => loaderr
316
+ require "#{rendererName}" # FIX: ruby requires the quotes?!?!
317
+ end
318
+
319
+ theClass = Module.const_get(rendererName)
320
+ renderer = theClass.send("new", self)
321
+ rescue LoadError, NameError => err
322
+ raise NotImplementedError, "Renderer #{rendererName} is not implemented or loaded (#{err})"
323
+ end
324
+
325
+ # 4.2) Pass entire file contents to renderer and replace w/ result.
326
+ newresult = renderer.render(result)
327
+ result = newresult
328
+ }
329
+
330
+ return result
331
+ end
332
+
333
+ =begin
334
+
335
+ --- ZenDocument#render(force)
336
+
337
+ Gets the rendered content from ((<ZenDocument#renderContent>)) and
338
+ writes it to disk if it decides to or is told to force the
339
+ rendering. Returns true if it rendered the document.
340
+
341
+ =end
342
+
343
+ def render(force=false)
344
+ if force or self['force'] or self.newerThanTarget then
345
+
346
+ puts url unless $TESTING
347
+
348
+ path = self.htmlpath
349
+ dir = File.dirname(path)
350
+
351
+ unless (test(?d, dir)) then
352
+ File::makedirs(dir)
353
+ end
354
+
355
+ content = self.renderContent
356
+ out = File.new(self.htmlpath, "w")
357
+ out.print(content)
358
+ out.close
359
+ return true
360
+ else
361
+ return false
362
+ end
363
+ end
364
+
365
+ =begin
366
+
367
+ --- ZenDocument#newerThanTarget
368
+
369
+ Returns true if the sourcefile is newer than the targetfile.
370
+
371
+ =end
372
+
373
+ def newerThanTarget()
374
+ data = self.datapath
375
+ html = self.htmlpath
376
+
377
+ if test(?f, html) then
378
+ return test(?>, data, html)
379
+ else
380
+ return true
381
+ end
382
+ end
383
+
384
+ =begin
385
+
386
+ --- ZenDocument#parentURL
387
+
388
+ Returns the parent url of this document. That is either the
389
+ index.html document of the current directory, or the parent
390
+ directory.
391
+
392
+ =end
393
+
394
+ def parentURL()
395
+ self.url.sub(/\/[^\/]+\/index.html$/, "/index.html").sub(/\/[^\/]+$/, "/index.html")
396
+ end
397
+
398
+ =begin
399
+
400
+ --- ZenDocument#addSubpage
401
+
402
+ Adds a url to the list of subpages of this document.
403
+
404
+ =end
405
+
406
+ def addSubpage(url)
407
+ raise ArgumentError, "url must be a string" unless url.instance_of? String
408
+ if (url != self.url) then
409
+ self.subpages.push(url)
410
+ end
411
+ end
412
+
413
+ ############################################################
414
+ # Accessors:
415
+
416
+ =begin
417
+
418
+ --- ZenDocument#parent
419
+
420
+ Returns the document object corresponding to the parentURL or
421
+ itself if it IS the top.
422
+
423
+ =end
424
+
425
+ def parent
426
+ parentURL = self.parentURL
427
+ parent = (parentURL != self.url ? self.website[parentURL] : self)
428
+ parent = self if parent.nil?
429
+
430
+ return parent
431
+ end
432
+
433
+ =begin
434
+
435
+ --- ZenDocument#dir
436
+
437
+ Returns the path of the directory for this url.
438
+
439
+ =end
440
+
441
+ def dir()
442
+ return File.dirname(self.datapath)
443
+ end
444
+
445
+ =begin
446
+
447
+ --- ZenDocument#datapath
448
+
449
+ Returns the full path to the data document.
450
+
451
+ =end
452
+
453
+ def datapath()
454
+
455
+ if (@datapath.nil?) then
456
+ datapath = "#{self.datadir}#{@url}"
457
+ datapath.sub!(/\.html$/, "")
458
+ datapath.sub!(/~/, "")
459
+ @datapath = datapath
460
+ end
461
+
462
+ return @datapath
463
+ end
464
+
465
+ =begin
466
+
467
+ --- ZenDocument#htmlpath
468
+
469
+ Returns the full path to the rendered document.
470
+
471
+ =end
472
+
473
+ def htmlpath()
474
+
475
+ if (@htmlpath.nil?) then
476
+ htmlpath = "#{self.htmldir}#{@url}"
477
+ htmlpath.sub!(/~/, "")
478
+ @htmlpath = htmlpath
479
+ end
480
+
481
+ return @htmlpath
482
+ end
483
+
484
+ =begin
485
+
486
+ --- ZenDocument#fulltitle
487
+
488
+ Returns the concatination of the title and subtitle, if any.
489
+
490
+ =end
491
+
492
+ def fulltitle
493
+ title = self.title
494
+ subtitle = self['subtitle'] || nil
495
+
496
+ return title + (subtitle ? ": " + subtitle : '')
497
+ end
498
+
499
+ def title
500
+ self['title'] || "Unknown"
501
+ end
502
+
503
+ =begin
504
+
505
+ --- ZenDocument#[](key)
506
+
507
+ Returns the metadata corresponding to ((|key|)), or nil.
508
+
509
+ =end
510
+
511
+ def [](key)
512
+ return self.metadata[key]
513
+ end
514
+
515
+ =begin
516
+
517
+ --- ZenDocument#[]=(key, val)
518
+
519
+ Sets the metadata value at ((|key|)) to ((|val|)).
520
+
521
+ =end
522
+
523
+ def []=(key, val)
524
+ self.metadata[key] = val
525
+ end
526
+
527
+ =begin
528
+
529
+ --- ZenDocument#metadata
530
+
531
+ DOC
532
+
533
+ =end
534
+
535
+ def metadata
536
+ if @metadata.nil? then
537
+ @metadata = Metadata.new(self.dir, self.datadir)
538
+ self.parseMetadata
539
+ end
540
+
541
+ return @metadata
542
+ end
543
+
544
+ =begin
545
+
546
+ --- ZenDocument#datadir
547
+
548
+ Returns the directory that all documents are read from.
549
+
550
+ =end
551
+
552
+ def datadir
553
+ return self.website.datadir
554
+ end
555
+
556
+ =begin
557
+
558
+ --- ZenDocument#htmldir
559
+
560
+ Returns the directory that all rendered documents are written to.
561
+
562
+ =end
563
+
564
+ def htmldir
565
+ return self.website.htmldir
566
+ end
567
+
568
+ end
569
+
570
+ =begin
571
+
572
+ = Class ZenSitemap
573
+
574
+ A ZenSitemap is a type of ZenDocument represents a file that consists
575
+ of lines of urls. Each of those urls will correspond to a file in the
576
+ ((<datadir|ZenWebsite#datadir>)).
577
+
578
+ A ZenSitemap is a ZenDocument that knows about the order and hierarchy
579
+ of all of the other pages in the website.
580
+
581
+ === Methods
582
+
583
+ =end
584
+
585
+ class ZenSitemap < ZenDocument
586
+
587
+ attr_reader :documents, :doc_order
588
+
589
+ =begin
590
+
591
+ --- ZenSitemap.new(url, website)
592
+
593
+ Creates a new ZenSitemap instance and processes the sitemap
594
+ content instantiating a ZenDocument for every referenced document
595
+ in the sitemap.
596
+
597
+ =end
598
+
599
+ def initialize(url, website)
600
+ super(url, website)
601
+
602
+ @documents = {}
603
+ @doc_order = []
604
+
605
+ self['title'] ||= "SiteMap"
606
+ self['description'] ||= "This page links to every page in the website."
607
+ self['keywords'] ||= "sitemap, website"
608
+
609
+ count = 0
610
+
611
+ IO.foreach(self.datapath) { |f|
612
+ count += 1
613
+ f.chomp!
614
+
615
+ f.gsub!(/\s*\#.*/, '')
616
+ f.strip!
617
+
618
+ next if f == ""
619
+
620
+ if f =~ /^\s*([\/-_~\.\w]+)$/
621
+ url = $1
622
+
623
+ if (url == self.url) then
624
+ doc = self
625
+ else
626
+ doc = ZenDocument.new(url, @website)
627
+ end
628
+
629
+ self.documents[url] = doc
630
+ self.doc_order.push(url)
631
+ else
632
+ $stderr.puts "WARNING on line #{count}: syntax error: '#{f}'"
633
+ end
634
+ }
635
+
636
+ end # initialize
637
+
638
+ end
639
+
640
+ =begin
641
+
642
+ = Class Metadata
643
+
644
+ Metadata provides a hash whose content comes from a file whose name is
645
+ fixed. Metadata will also be provided by metadata files in parent
646
+ directories, up to a specified directory, or "/" by default.
647
+
648
+ === Methods
649
+
650
+ =end
651
+
652
+ class Metadata < Hash
653
+
654
+ RESERVED_WORDS=Regexp.new("\`|" + %w(^img ^link author banner bgcolor charset copyright description dtd email footer force head_extra header icbm(_title)? include keywords naked_page rating skipsubpages style stylesheet subtitle title).join("|"))
655
+
656
+ @@metadata = {}
657
+ @@count = {}
658
+ @@count.default = 0
659
+
660
+ =begin
661
+
662
+ --- Metadata#displayBadMetadata
663
+
664
+ Reports both unused metadata (only really good if you render the
665
+ entire site) and metadata accessed but not defined (sometimes gets
666
+ confused by legit ruby code).
667
+
668
+ =end
669
+
670
+ def self.displayBadMetadata
671
+
672
+ good_key = {}
673
+
674
+ puts
675
+ puts "Unused metadata entries:"
676
+ puts
677
+ @@metadata.each do |file, metadata|
678
+ puts "File = #{file}"
679
+ metadata.each_key do |key|
680
+ count = @@count[key]
681
+ good_key[key] = true
682
+ puts " #{key}" unless count > 0
683
+ end
684
+ end
685
+
686
+ puts
687
+ puts "Bad accesses:"
688
+ puts
689
+ @@count.each do |key, count|
690
+ puts " #{key}: #{count}" unless good_key[key] or key =~ RESERVED_WORDS
691
+ end
692
+ end
693
+
694
+ def [](key)
695
+ @@count[key] += 1
696
+ $stderr.puts " WARNING: metadata '#{key}' does not exist" unless $TESTING or key?(key) or key =~ RESERVED_WORDS
697
+ super
698
+ end
699
+
700
+ @@path = {}
701
+
702
+ =begin
703
+
704
+ --- Metadata.new(directory, toplevel = "/")
705
+
706
+ Instantiates a new metadata object and loads the data from
707
+ ((|directory|)) up to the ((|toplevel|)) directory.
708
+
709
+ =end
710
+
711
+ def initialize(directory, toplevel = "/")
712
+ super()
713
+
714
+ self.default = nil
715
+
716
+ unless (test(?e, directory)) then
717
+ raise ArgumentError, "directory #{directory} does not exist"
718
+ end
719
+
720
+ unless (test(?d, toplevel)) then
721
+ raise ArgumentError, "toplevel directory #{toplevel} does not exist"
722
+ end
723
+
724
+ # Check that toplevel is ABOVE directory, not below. Can be equal.
725
+ unless @@path.include? directory then
726
+ abs_dir = File.expand_path(directory)
727
+ @@path[directory] = abs_dir
728
+ else
729
+ abs_dir = @@path[directory]
730
+ end
731
+
732
+ unless @@path.include? toplevel then
733
+ abs_top = File.expand_path(toplevel)
734
+ @@path[toplevel] = abs_top
735
+ else
736
+ abs_top = @@path[toplevel]
737
+ end
738
+
739
+ if (abs_top.length > abs_dir.length || abs_dir.index(abs_top) != 0) then
740
+ raise ArgumentError, "toplevel is not a parent dir to directory"
741
+ end
742
+
743
+ if (test(?f, directory)) then
744
+ directory = File.dirname(directory)
745
+ end
746
+
747
+ self.loadFromDirectory(directory, toplevel)
748
+ end
749
+
750
+ =begin
751
+
752
+ --- Metadata#loadFromDirectory(directory, toplevel, count=1)
753
+
754
+ Loads a series of metadata files from the directory ((|toplevel|))
755
+ down to ((|directory|)). Each load in turn may override previous
756
+ values.
757
+
758
+ =end
759
+
760
+ def loadFromDirectory(directory, toplevel, count = 1)
761
+
762
+ raise "too many recursions" if (count > 20)
763
+
764
+ if (directory != toplevel && directory != "/" && directory != ".") then
765
+ # Recurse to parent directory. Increment count for basic loop protection.
766
+ self.loadFromDirectory(File.dirname(directory), toplevel, count + 1)
767
+ end
768
+
769
+ file = directory + "/" + "metadata.txt"
770
+ if (test(?f, file)) then
771
+ self.load(file)
772
+ end
773
+
774
+ end
775
+
776
+ =begin
777
+
778
+ --- Metadata#load(file)
779
+
780
+ Loads a specific file ((|file|)). If any keys already exist that
781
+ are specifed in the file, then they are overridden.
782
+
783
+ =end
784
+
785
+ def load(file)
786
+
787
+ count = 0
788
+
789
+ unless (@@metadata[file]) then
790
+ hash = {}
791
+
792
+ IO.foreach(file) { | line |
793
+ count += 1
794
+ if (line =~ /^\s*(\"(?:\\.|[^\"]+)\"|[^=]+)\s*=\s*(.*?)\s*$/) then
795
+
796
+ # REFACTEE: this is duplicated from above
797
+ begin
798
+ key = $1
799
+ val = $2
800
+
801
+ key = eval(key)
802
+ val = eval(val)
803
+ rescue Exception
804
+ $stderr.puts "WARNING on line #{count}: eval failed: #{line}: #{$!}"
805
+ else
806
+ hash[key] = val
807
+ end
808
+ elsif (line =~ /^\s*$/) then
809
+ # ignore
810
+ elsif (line =~ /^\#.*$/) then
811
+ # ignore
812
+ else
813
+ $stderr.puts "WARNING on line #{count}: cannot parse: #{line}"
814
+ end
815
+ }
816
+ @@metadata[file] = hash
817
+ end
818
+
819
+ self.update(@@metadata[file])
820
+
821
+ end
822
+
823
+ end
824
+
825
+ ############################################################
826
+ # Object methods - shortcuts for users
827
+
828
+ =begin
829
+
830
+ --- link(url, title)
831
+
832
+ Returns a string with an anchor with the appropriate data.
833
+
834
+ =end
835
+
836
+ def link(url, title)
837
+ return "<A HREF=\"#{url}\">#{title}</A>"
838
+ end
839
+
840
+ =begin
841
+
842
+ --- img(url, alt, height=0, width=0, border=0)
843
+
844
+ Returns a string with an image tag with the appropriate data.
845
+
846
+ =end
847
+
848
+ def img(url, alt, height=nil, width=nil, border=0)
849
+ return "<IMG SRC=\"#{url}\" ALT=\"#{alt}\""+(height ? " HEIGHT=#{height}" : '')+(width ? " WIDTH=#{width}" : '')+">"
850
+ end