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.
- data.tar.gz.sig +0 -0
- data/History.txt +426 -0
- data/Manifest.txt +54 -0
- data/README.txt +63 -0
- data/Rakefile +22 -0
- data/bin/zenweb +27 -0
- data/bin/zenwebpage +66 -0
- data/bin/zenwebsite +39 -0
- data/design/REQUIREMENTS.txt +52 -0
- data/design/ZENWEB_2.txt +69 -0
- data/design/heirarchy.png +0 -0
- data/design/heirarchy.tgif +311 -0
- data/docs/Customizing +76 -0
- data/docs/FAQ +12 -0
- data/docs/Features +128 -0
- data/docs/Presentation +88 -0
- data/docs/QuickStart +32 -0
- data/docs/Renderers +85 -0
- data/docs/SiteMap +13 -0
- data/docs/YourOwnWebsite +32 -0
- data/docs/index +14 -0
- data/docs/metadata.txt +10 -0
- data/lib/ZenWeb.rb +850 -0
- data/lib/ZenWeb/CalendarRenderer.rb +162 -0
- data/lib/ZenWeb/CompactRenderer.rb +45 -0
- data/lib/ZenWeb/CompositeRenderer.rb +63 -0
- data/lib/ZenWeb/FileAttachmentRenderer.rb +57 -0
- data/lib/ZenWeb/FooterRenderer.rb +38 -0
- data/lib/ZenWeb/GenericRenderer.rb +143 -0
- data/lib/ZenWeb/HeaderRenderer.rb +52 -0
- data/lib/ZenWeb/HtmlRenderer.rb +81 -0
- data/lib/ZenWeb/HtmlTableRenderer.rb +94 -0
- data/lib/ZenWeb/HtmlTemplateRenderer.rb +173 -0
- data/lib/ZenWeb/MetadataRenderer.rb +83 -0
- data/lib/ZenWeb/RelativeRenderer.rb +98 -0
- data/lib/ZenWeb/RubyCodeRenderer.rb +56 -0
- data/lib/ZenWeb/SitemapRenderer.rb +56 -0
- data/lib/ZenWeb/StandardRenderer.rb +40 -0
- data/lib/ZenWeb/StupidRenderer.rb +88 -0
- data/lib/ZenWeb/SubpageRenderer.rb +45 -0
- data/lib/ZenWeb/TextToHtmlRenderer.rb +219 -0
- data/lib/ZenWeb/TocRenderer.rb +61 -0
- data/lib/ZenWeb/XXXRenderer.rb +32 -0
- data/test/SiteMap +14 -0
- data/test/Something +4 -0
- data/test/include.txt +3 -0
- data/test/index +8 -0
- data/test/metadata.txt +10 -0
- data/test/ryand/SiteMap +10 -0
- data/test/ryand/blah +4 -0
- data/test/ryand/blah-blah +4 -0
- data/test/ryand/index +52 -0
- data/test/ryand/metadata.txt +2 -0
- data/test/ryand/stuff/index +4 -0
- data/test/test_zenweb.rb +1624 -0
- metadata +161 -0
- metadata.gz.sig +0 -0
data/docs/index
ADDED
|
@@ -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}.
|
data/docs/metadata.txt
ADDED
|
@@ -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>'
|
data/lib/ZenWeb.rb
ADDED
|
@@ -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
|