bee 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,464 @@
1
+ # Copyright 2006 Michel Casabianca <casa@sweetohm.net>
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ # Script to generate documentation.
16
+
17
+ $:.unshift(File.join(File.dirname(__FILE__)))
18
+ require 'bee'
19
+ require 'erb'
20
+ require 'fileutils'
21
+ require 'yaml'
22
+
23
+ module Bee
24
+
25
+ # Module for Bee documentation generation.
26
+ module Doc
27
+
28
+ # Copyright notice.
29
+ COPYRIGHT = 'BeeDoc version 0.1.0 (C) Michel Casabianca - 2006'
30
+ # Console help.
31
+ HELP = 'beedoc [-h] [-o dir] menu
32
+ -h Print help about usage and exit.
33
+ -o Directory where to generate documentation.
34
+ menu Menu file to process.'
35
+ # Exit value on error parsing command line
36
+ EXIT_PARSING_CMDLINE = 1
37
+ # Exit value on doc error
38
+ EXIT_DOC_ERROR = 2
39
+ # Exit value on unknown error
40
+ EXIT_UNKNOWN_ERROR = 3
41
+
42
+ #########################################################################
43
+ # CLASS DOCERROR #
44
+ #########################################################################
45
+
46
+ class DocError < RuntimeError; end
47
+
48
+ module DocErrorMixin
49
+
50
+ # Convenient method to raise a DocError.
51
+ # - message: error message.
52
+ def error(message)
53
+ raise DocError.new(message)
54
+ end
55
+
56
+ end
57
+
58
+ #########################################################################
59
+ # PARSE COMMAND LINE #
60
+ #########################################################################
61
+
62
+ # Parse command line and return arguments.
63
+ def self.parse_command_line
64
+ help = false
65
+ output = nil
66
+ require 'getoptlong'
67
+ begin
68
+ opts = GetoptLong.new(['--help', '-h', GetoptLong::NO_ARGUMENT ],
69
+ ['--output', '-o', GetoptLong::REQUIRED_ARGUMENT])
70
+ opts.each do |opt, arg|
71
+ case opt
72
+ when '--help'
73
+ help = true
74
+ when '--output'
75
+ output = arg
76
+ end
77
+ end
78
+ help = true if ARGV.length > 1
79
+ menu = ARGV[0]
80
+ return [help, output, menu]
81
+ end
82
+ end
83
+
84
+ # Run BeeDoc from command line.
85
+ def self.start_command_line
86
+ begin
87
+ help, output, menu = parse_command_line
88
+ raise "No menu file" if not menu
89
+ rescue
90
+ puts "ERROR: parsing command line (type 'beedoc -h' for help): #{$!}"
91
+ exit(EXIT_PARSING_CMDLINE)
92
+ end
93
+ begin
94
+ if help
95
+ puts COPYRIGHT
96
+ puts HELP
97
+ else
98
+ Bee::Doc::Menu.new(menu).generate(output)
99
+ puts "OK"
100
+ end
101
+ rescue DocError
102
+ puts "ERROR: #{$!}"
103
+ exit(EXIT_DOC_ERROR)
104
+ rescue Exception => e
105
+ puts "ERROR: #{$!}"
106
+ puts e.backtrace.join("\n")
107
+ exit(EXIT_UNKNOWN_ERROR)
108
+ end
109
+ end
110
+
111
+ #########################################################################
112
+ # CLASS MENU #
113
+ #########################################################################
114
+
115
+ class Menu
116
+
117
+ include DocErrorMixin
118
+
119
+ # Title for documentation.
120
+ attr_reader :title
121
+ # Generation date.
122
+ attr_reader :date
123
+ # List of menu entries.
124
+ attr_reader :entries
125
+ # Base directory.
126
+ attr_reader :base
127
+ # Contents for page being generated.
128
+ attr_reader :contents
129
+
130
+ # Constructor.
131
+ # - file: menu file to load.
132
+ def initialize(file)
133
+ begin
134
+ @base = File.dirname(file)
135
+ source = File.read(file)
136
+ menu = YAML::load(source)
137
+ @entries = []
138
+ error "Menu must be a list" unless menu.kind_of?(Array)
139
+ error "Menu description must be first entry" unless
140
+ menu[0].kind_of?(Hash) and menu[0]['menu']
141
+ @title = menu[0]['menu']
142
+ @date = Time.now
143
+ for entry in menu[1..-1]
144
+ @entries << Entry.new(entry, self)
145
+ end
146
+ rescue
147
+ error "Error loading menu file: #{$!}"
148
+ end
149
+ end
150
+
151
+ # Generate HTML documents.
152
+ # - directory: where to generate documentation.
153
+ def generate(directory)
154
+ error "Destination directory '#{directory}' already exists" if
155
+ File.exists?(directory)
156
+ FileUtils.makedirs(directory)
157
+ write_style_sheet = false
158
+ for entry in @entries
159
+ entry.generate(directory, @date)
160
+ write_style_sheet = true if not entry.template
161
+ end
162
+ if write_style_sheet
163
+ File.open(File.join(directory, 'stylesheet.css'), 'w') do |file|
164
+ file.write(Entry::DEFAULT_STYLESHEET)
165
+ end
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ #########################################################################
172
+ # CLASS ENTRY #
173
+ #########################################################################
174
+
175
+ # A menu entry.
176
+ class Entry
177
+
178
+ include BuildErrorMixin
179
+
180
+ # Parent menu.
181
+ attr_reader :menu
182
+ # Title.
183
+ attr_reader :title
184
+ # File.
185
+ attr_reader :file
186
+ # Destination file.
187
+ attr_reader :dest
188
+ # Directory.
189
+ attr_reader :dir
190
+ # File type.
191
+ attr_reader :type
192
+ # Table of contents.
193
+ attr_reader :toc
194
+ # Header depth for toc.
195
+ attr_reader :header
196
+ # Template for generation.
197
+ attr_reader :template
198
+ # Generation date.
199
+ attr_reader :date
200
+
201
+ # Constructor.
202
+ # - entry: menu entry as a Hash.
203
+ # - menu: the root menu for the entry.
204
+ def initialize(entry, menu)
205
+ # check keys in entry
206
+ error "Unknown entry type '#{entry['type']}'" unless
207
+ ['html', 'text', 'dir', 'link'].member?(entry['type'])
208
+ error "A menu entry must be a Hash" unless entry.kind_of?(Hash)
209
+ error "'title' key is mandatory in menu entries" unless entry['title']
210
+ error "One of 'file' or 'dir' keys must be in a menu entry" unless
211
+ (entry['file'] or entry['dir']) or entry['type'] == 'link'
212
+ error "'file' and 'dir' keys can't be in a menu entry at the " +
213
+ "same time" if entry['file'] and entry['dir']
214
+ # save keys into fields
215
+ @menu = menu
216
+ @title = entry['title']
217
+ @file = entry['file']
218
+ @dest = entry['dest']
219
+ @dir = entry['dir']
220
+ @type = entry['type']
221
+ @toc = entry['toc']
222
+ @header = entry['header']
223
+ @template = entry['template']
224
+ end
225
+
226
+ # Generate a menu entry.
227
+ # - directory: destination directory.
228
+ # - date: generation date.
229
+ def generate(directory, date)
230
+ return if type == 'link'
231
+ @date = date
232
+ # load template
233
+ if @template
234
+ template = ERB.new(File.read(@template))
235
+ else
236
+ template = ERB.new(DEFAULT_TEMPLATE)
237
+ end
238
+ if @dir
239
+ # entry is a directory: copy it to destination directory
240
+ puts "Copying directory '#{@dir}'..."
241
+ FileUtils.cp_r(File.join(@menu.base, @dir),
242
+ File.join(directory, dest))
243
+ else
244
+ # entry is a regular file: generate documentation with template
245
+ puts "Generating '#{@dest}'..."
246
+ @contents = File.read(File.join(@menu.base, @file))
247
+ @contents = '<pre>' + escape_special_characters(@contents) +
248
+ '</pre>' if text?
249
+ result = template.result(get_binding)
250
+ File.open(File.join(directory, @dest), 'w') do |file|
251
+ file.write(result)
252
+ end
253
+ end
254
+ end
255
+
256
+ private
257
+
258
+ # Tells if entry is a text one.
259
+ def text?
260
+ return type == 'text'
261
+ end
262
+
263
+ # Get a binding for ERB generation.
264
+ def get_binding
265
+ return binding
266
+ end
267
+
268
+ # Escape HTML special characters (such as & " ' < and >).
269
+ # - string: string to process.
270
+ # Return: processed string.
271
+ def escape_special_characters(string)
272
+ return string.gsub(/&/, '&amp;').gsub(/"/, '&quot;').
273
+ gsub(/'/, '&apos;').gsub(/</, '&lt;').gsub(/>/, '&gt;')
274
+ end
275
+
276
+ # Default ERB template to generate HTML.
277
+ DEFAULT_TEMPLATE = '<html>
278
+ <head>
279
+ <title><%= @menu.title %></title>
280
+ <link rel="stylesheet" type="text/css" href="stylesheet.css">
281
+ </head>
282
+ <body marginwidth="0" marginheight="0">
283
+ <table class="frame" width="100%" height="100%">
284
+ <tr>
285
+ <td class="frame" colspan="2"
286
+ align="right" valign="middle"
287
+ height="70">
288
+ <h1 class="title"><b><%= @menu.title %>&#xA0;&#xA0;</b></h1>
289
+ </td>
290
+ </tr>
291
+ <tr>
292
+ <td class="ruller" colspan="2"
293
+ align="left" valign="middle"
294
+ height="10">
295
+ <font size="-1">Generated on <%= @date.strftime("%Y-%m-%d %H:%M:%S") %></font>
296
+ </td>
297
+ </tr>
298
+ <tr>
299
+ <td class="menu"
300
+ align="left" valign="top"
301
+ width="150">
302
+ <%
303
+ for entry in @menu.entries
304
+ link = entry.dest
305
+ link += \'/index.html\' if entry.dir
306
+ %>
307
+ <a class="menu" href="<%= link %>"><%= entry.title %></a><br>
308
+ <%
309
+ end
310
+ %>
311
+ </td>
312
+ <td class="text"
313
+ align="left" valign="top">
314
+ <center><h1 class="title"><b><%= @title %></b></h1></center>
315
+ <%= @contents %>
316
+ </td>
317
+ </tr>
318
+ </table>
319
+ </body>
320
+ </html>'
321
+
322
+ public
323
+
324
+ # Default style sheet.
325
+ DEFAULT_STYLESHEET = '
326
+ h1,h2,h3 {
327
+ font-family: Verdana, Helvetica, Arial, sans-serif;
328
+ color: #FF9000;
329
+ }
330
+
331
+ h1.title {
332
+ font-family: Verdana, Helvetica, Arial, sans-serif;
333
+ font-size: 30pt;
334
+ color: #000000
335
+ }
336
+
337
+ table.frame {
338
+ border-width: 0px;
339
+ border-spacing: 0px;
340
+ }
341
+
342
+ td.frame {
343
+ padding: 0px;
344
+ text-align: right;
345
+ border: thin solid #OOO000;
346
+ }
347
+
348
+ td.text {
349
+ padding: 10px;
350
+ font-family: Verdana, Helvetica, Arial, sans-serif;
351
+ font-size: 10pt;
352
+ }
353
+
354
+ td.ruller {
355
+ padding: 3px;
356
+ background-color: #000000;
357
+ color: #FFFFFF;
358
+ font-family: Verdana, Helvetica, Arial, sans-serif;
359
+ font-size: 10pt;
360
+ }
361
+
362
+ td.menu {
363
+ padding: 10px;
364
+ background-color: #FF9000;
365
+ font-family: Verdana, Helvetica, Arial, sans-serif;
366
+ font-size: 12pt;
367
+ }
368
+
369
+ table.toc {
370
+ text-align: left;
371
+ border-width: 0pt;
372
+ background-color: #F9F9F9;
373
+ font-family: Verdana, Helvetica, Arial, sans-serif;
374
+ font-size: 10pt;
375
+ }
376
+
377
+ p {
378
+ font-family: Verdana, Helvetica, Arial, sans-serif;
379
+ font-size: 10pt;
380
+ }
381
+
382
+ table {
383
+ font-family: Verdana, Helvetica, Arial, sans-serif;
384
+ font-size: 10pt;
385
+ border: medium solid #000000;
386
+ border-collapse: collapse;
387
+ }
388
+
389
+ th {
390
+ padding: 5px;
391
+ background-color: #FF9000;
392
+ border: thin solid #OOO000;
393
+ text-align: left;
394
+ }
395
+
396
+ td {
397
+ padding: 5px;
398
+ border: thin solid #OOO000;
399
+ text-align: left;
400
+ }
401
+
402
+ li {
403
+ font-family: Verdana, Helvetica, Arial, sans-serif;
404
+ font-size: 10pt;
405
+ list-style: square;
406
+ }
407
+
408
+ a:link {
409
+ text-decoration: none;
410
+ color: #FF9000;
411
+ }
412
+
413
+ a:visited {
414
+ text-decoration: none;
415
+ color: #000000;
416
+ }
417
+
418
+ a:active {
419
+ text-decoration: none;
420
+ color: #FFFFFF;
421
+ background-color: #000000;
422
+ }
423
+
424
+ a:hover {
425
+ text-decoration: none;
426
+ color: #FFFFFF;
427
+ background-color: #000000;
428
+ }
429
+
430
+ a.menu:link {
431
+ text-decoration: none;
432
+ color: #000000;
433
+ }
434
+
435
+ a.menu:visited {
436
+ text-decoration: none;
437
+ color: #000000;
438
+ }
439
+
440
+ a.menu:active {
441
+ text-decoration: none;
442
+ color: #000000;
443
+ }
444
+
445
+ a.menu:hover {
446
+ text-decoration: none;
447
+ color: #FFFFFF;
448
+ background-color: #000000;
449
+ }
450
+
451
+ pre.code {
452
+ clear: both;
453
+ overflow: auto;
454
+ background-color: #EFEFEF;
455
+ padding: 0;
456
+ padding: 1.0em;
457
+ }
458
+ '
459
+
460
+ end
461
+
462
+ end
463
+
464
+ end