zenweb 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
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