5
Generate static HTML pages for simple websites.
6
Copyright (C) 2007, 2009 Wolfgang Oertl <wolfgang.oertl@gmail.com>
10
- recursively reads all files, processes the .html files, copies .css,
13
- uses a template file with header and document layout.
15
- parses the input HTML files and processes following patterns:
18
#label anchor, can be referenced. No whitespace in label.
19
* hide this entry (no text output)
20
=label reference to that anchor. If not text is given,
21
use the text of the referenced element.
22
noindex don't add to the index
23
Any text text content of this directive; must be the last
26
- Can generate a sorted index of keywords
28
- Generates a short horizontal and detailed vertical menu linking to all
29
the pages using the menu definition in the file "menu.lua" in the
32
- detects .html files which are not mentioned in the menu, and complains
35
- can use .html.in files in the _output_ directory instead of the
36
equivalent .html file in the input directory. This enables another
37
program to generate input files which will then get the document
38
structure and appear in the menu.
40
menu_entry structure: [1]=basename, [2]=title, [3]=submenu, [seen]=true
13
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
14
"http://www.w3.org/TR/html4/strict.dtd">
18
<meta name="description" content="The Lua-Gtk Homepage #TITLE#">
19
<meta name="keywords" content="Lua, Gtk">
20
<title>Lua-Gtk #TITLE#</title>
21
<link rel="stylesheet" href="lua-gtk.css" type="text/css">
27
<img width=128 height=128 border=0 alt="Lua-Gtk Logo" src="img/lua-gtk-logo.png"/>
28
<p>Binding to Gtk 2 for Lua</p>
30
<a href="index.html">Home</a> ·
31
<a href="examples1.html">Examples 1</a> ·
32
<a href="examples2.html">Examples 2</a> ·
33
<a href="reference.html">Reference</a>
46
output_dir = "../build/html/"
51
extensions = { png=true, gif=true, jpg=true, css=true }
53
-- Handling of {{...}} directives
54
items = {} -- array of items
55
items_byname = {} -- key=anchor name, value=item
56
files = {} -- array of input HTML files
57
curr_file = nil -- file currently being read, see _process_html
59
-- Handling of the index generation
60
index_string = nil -- computed index, used by generate_index()
62
-- expand tabs; taken from "Programming in Lua" by Roberto Ierusalimschy
63
function expand_tabs(s, tab)
66
s = string.gsub(s, "()\t", function(p)
67
local sp = tab - (p - 1 + corr) % tab
69
return string.rep(" ", sp)
76
"and", "break", "do", "else", "elseif", "end", "false", "for",
77
"function", "if", "in", "local", "nil", "not", "or", "repeat",
78
"return", "then", "true", "until", "while",
83
"assert", "collectgarbage", "dofile", "error", "getfenv",
84
"getmetatable", "ipairs", "load", "loadfile", "loadstring", "module",
85
"next", "pairs", "pcall", "print" "rawequal", "rawget", "rawset",
86
"require", "select", "setfenv", "setmetatable", "tonumber", "tostring",
87
"type", "unpack", "xpcall",
91
lua_gnome = { "gnome", "gtk", "glib", "gdk", "pango", "cairo", "gtkhtml",
98
col_res[#col_res + 1] = s
105
-- looking for start of word
107
if c == " " or c == "\n" then
115
if c == '"' or c == "'" then
120
if c >= '0' and c <= '9' then
129
if string.match(c, "^[a-zA-Z_-]$") then
138
-- number after "-": a negative constant.
139
if c >= "0" and c <= "9" and word == "-" then
143
local cl = lua_keyindex[word]
145
put(string.format("<b class=\"%s\">%s</b>", cl, word))
160
put("<b class=\"co\">" .. word .. "</b>\n")
172
put("<b class=\"st\">" .. word .. "</b>")
180
if c >= '0' and c <= '9' then
184
put("<b class=\"st\">" .. word .. "</b>")
192
-- Given some Lua code in "s" (may be one line or multiple lines), return
193
-- HTML code for a colorized (syntax highlighted) representation.
196
if not lua_keyindex then
198
for _, k in ipairs(lua_keywords) do lua_keyindex[k] = "kw" end
199
for _, k in ipairs(lua_gnome) do lua_keyindex[k] = "gn" end
200
for prefix, ar in pairs { [""]=_G, ["string."]=string,
201
["math."]=math, ["io."]=io, ["package."]=package,
202
["os."]=os, ["debug."]=debug, ["table."]=table,
203
["coroutine."]=coroutine } do
204
for k, v in pairs(ar) do
205
if type(v) == "function" then
206
lua_keyindex[prefix .. k] = "lb"
215
for c in string.gmatch(s, ".") do
216
state = states[state](c)
219
while col_res[#col_res] == "\n" do
220
table.remove(col_res)
222
return table.concat(col_res, "")
227
-- The environment available to the functions in the template. Note that
228
-- all global variables (including functions) are available too. This
229
-- should probably change.
53
html_header = function(title)
54
-- avoid the second return value (number of substitutions) to be
56
local s = string.gsub(html_header, "#TITLE#", title)
60
html_trailer = function()
64
233
-- extract a function from a Lua source file
65
234
copy_function = function(file, name)
69
local exists, _ = lfs.attributes("../" .. file)
70
if not exists then return "" end
238
local exists, _ = lfs.attributes(file)
239
if not exists then return "not found: " .. file end
72
for line in io.lines("../" .. file) do
241
for line in io.lines(file) do
73
242
if state == 0 then
74
243
if string.match(line, "function " .. name) then
333
-- Add some entries to the menu: _parent in each item, further a basename
334
-- index in config.menu_index.
336
function _prepare_menu(top, parent, ar)
338
for i, item in ipairs(ar) do
339
config.menu_index[item[1]] = item
340
item._parent = parent
342
_prepare_menu(top, item, item[3]) -- recurse
347
-- recursively look for the given basename.
348
-- ar_in: the part of the menu to look in
349
-- ar_out: path to the item if found; [1]=most specific, [2]=parent etc.
350
function _find_in_menu(basename, ar_in, ar_out)
352
for i, item in ipairs(ar_in) do
353
if item[1] == basename then
354
ar_out[#ar_out+1] = item
358
if item[3] and _find_in_menu(basename, item[3], ar_out) then
359
ar_out[#ar_out+1] = item
368
-- Build the side menu for the given menu_entry. It consists of all siblings
370
-- @param menu A menu structure
371
-- @param current The current menu; in order to descend there and display it
374
function make_side_menu(current)
377
-- determine the path to the current menu entry
387
_make_side_menu(tbl, config.menu, path)
389
if #tbl == 0 then return "" end
390
return table.concat(tbl, "\n")
393
function _make_side_menu(tbl, menu, path)
394
if #menu == 0 then return end
396
tbl[#tbl + 1] = "<ul>"
398
for i, item in ipairs(menu) do
399
if path[item] == 2 then
400
tbl[#tbl + 1] = string.format("<li><b>%s</b></li>", item[2])
402
tbl[#tbl + 1] = string.format("<li><a href=\"%s.html\">%s</a></li>",
405
if path[item] and item[3] then
406
tbl[#tbl + 1] = '<li>'
407
_make_side_menu(tbl, item[3], path)
408
tbl[#tbl + 1] = '</li>'
412
tbl[#tbl + 1] = "</ul>"
415
-- Helper function for _make_side_menu.
416
function _add_menu_items(tbl, ar)
417
for i, item in ipairs(ar) do
418
tbl[#tbl + 1] = string.format("<li><a href=\"%s.html\">%s</a></li>",
425
-- Fill the template using the current menu entry and the given input file,
426
-- and write the resulting HTML file to ofile.
428
-- @param basename Name of the output file without the output base path.
429
-- @param ar Array with variables available to the page for substitution
431
function _process_html(ifname, basename, menu_entry, do_index)
434
ifile = assert(io.open(ifname, "rb"))
441
menu_entry = menu_entry,
445
ar.SIDEMENU = make_side_menu(menu_entry)
446
ar.TITLE = menu_entry[2]
447
ar.MAINMENU = main_menu
448
ar.CONTENTCLASS = (ar.SIDEMENU == "") and "center" or "right"
451
for line in ifile:lines() do
452
buf[#buf + 1] = string.gsub(line, "{{(.-)}}", _html_pass1)
455
ar.CONTENT = table.concat(buf, "\n")
456
-- ar.CONTENT = string.gsub(ifile:read"*a", "{{(.-)}}", _html_pass1)
458
files[#files + 1] = curr_file
463
-- Second pass over HTML files and output.
465
function output_html()
466
local ifile, page, old_page, ofile, ofname, skip
468
if not page_template then
469
ifile = assert(io.open(input_dir .. "/template.html", "rb"))
470
page_template = ifile:read "*a"
474
for _, file in ipairs(files) do
475
_evaluate_html_pass2(file)
476
page = string.gsub(page_template, "#([A-Z]+)#", file.variables)
478
ofname = output_dir .. "/" .. file.basename
480
-- Check for changes. This avoids a newer date on unchanged
483
if lfs.attributes(ofname, "mode") == "file" then
484
ifile = assert(io.open(ofname, "rb"))
485
old_page = ifile:read"*a"
487
if page == old_page then
490
print("CHANGES IN", ofname)
495
ofile = assert(io.open(ofname, "wb"))
503
-- Split a string using a delimiter, which can be a search pattern. Make sure
504
-- that the delimiter doesn't match the empty string.
506
function split(s, delim, is_plain)
507
local ar, pos = {}, 1
510
local first, last = s:find(delim, pos, is_plain)
512
table.insert(ar, s:sub(pos, first-1))
515
table.insert(ar, s:sub(pos))
524
-- Handler for {{...}} matches during the first pass over the HTML content.
525
-- These strings are replaced by {{{%d}}}, the data being stored elsewhere.
527
-- Globals: curr_file is the file being read.
529
function _html_pass1(str)
532
-- Split the string into elements and fill "item" with data.
533
item = { file=curr_file }
534
for _, s in ipairs(split(str, " +")) do
535
c = string.sub(s, 1, 1)
537
item.is_anchor = true
538
item.anchor_name = string.sub(s, 2)
540
item.is_hidden = true
542
item.is_reference = true
543
item.ref_name = string.sub(s, 2)
544
elseif s == "noindex" then
545
item.omit_index = true
546
elseif item.text then
547
item.text = item.text .. " " .. s
553
-- if this item has no anchor name, generate the next available
554
if not item.is_anchor then
555
curr_file.index_count = curr_file.index_count + 1
556
item.anchor_name = string.format("idx%d", curr_file.index_count)
559
if item.is_hidden then
560
item.full_anchor = curr_file.basename
562
item.full_anchor = string.format("%s#%s", curr_file.basename,
566
-- assign the next number and store. If an anchor is defined, store
569
items[#items + 1] = item
570
if item.is_anchor then
571
assert(items_byname[item.anchor_name] == nil, "Duplicate anchor")
572
items_byname[item.anchor_name] = item
575
return string.format("{{{%d}}}", item.nr)
579
-- Replace the {{{%d}}} strings with their proper content.
581
function _html_pass2(nr)
584
item = assert(items[tonumber(nr)], "Item " .. tostring(nr) .. " not found")
586
-- nothing is output for hidden items.
587
if item.is_hidden then
588
-- assert(not item.is_anchor)
589
assert(not item.is_reference)
593
-- a reference is replaced with a link to the referenced anchor
594
if item.is_reference then
595
target = items_byname[item.ref_name]
597
error(string.format("%s(%d): Missing target %s",
603
assert(target.is_anchor)
604
assert(item.text or target.text)
605
return string.format('<a href="%s">%s</a>', target.full_anchor,
606
item.text or target.text)
609
-- named anchors are set
610
if item.is_anchor then
611
return string.format('<a name="%s">%s</a>', item.anchor_name,
615
-- unnamed anchor - for the index
617
return string.format('<a name="%s">%s</a>', item.anchor_name, item.text)
622
-- Perform the second pass over the HTML files. First, {{{%d}}} items left
623
-- by the first pass are replaced with their final value, and then inline
624
-- Lua code is executed.
626
function _evaluate_html_pass2(file)
627
local v = file.variables
629
v.CONTENT = string.gsub(v.CONTENT, "{{{(%d+)}}}", _html_pass2)
631
-- curr_menu = file.menu_entry
632
v.CONTENT = string.gsub(v.CONTENT, "<%%=(.-)%%>", function(fn)
633
local chunk = assert(loadstring("return " .. fn))
141
640
-- Process a file. If it is a HTML file, run the luadoc template routines on
142
641
-- it, otherwise (if it has a known extension) copy it to the destination.
144
643
function _read_file(path)
145
if string.match(path, "%.html$") then
146
print("Processing " .. path)
147
_mkdir(output_dir .. path)
148
local f = io.open(output_dir .. path, "w")
151
luadoc.lp.include(path, env)
644
local path1, path_in, basename, menu_entry
646
-- basename of the file to process
647
path1 = string.sub(path, #input_dir + 2)
648
_mkdir(output_dir .. "/" .. path1)
650
basename = string.match(path, "([a-z0-9_-]+)%.html$")
652
if basename == "template" then return end
653
menu_entry = assert(config.menu_index[basename],
654
"Missing menu entry for input file " .. basename)
655
-- if a .in file exists in the output directory, process that instead.
656
-- it might exist if the doc file has been preprocessed.
657
path_in = output_dir .. "/" .. path1 .. ".in"
658
if lfs.attributes(path_in, "mode") ~= "file" then
661
print("Processing " .. path1)
662
menu_entry.seen = true
663
_process_html(path_in, path1, menu_entry)
156
if string.match(path, "%.png$") or string.match(path, "%.css$") then
667
local ext = string.match(path, "([^.]+)$")
669
if extensions[ext] then
157
670
print("Copying " .. path)
158
_file_copy(path, output_dir .. path)
671
_file_copy(path, output_dir .. "/" .. path1)
164
678
-- Process a file or directory. Files are handled by _read_file, while
165
679
-- directories are recursed into.
699
-- Read the configuration file for the documentation, which currently only
700
-- defines the menu structure, including the title for each entry.
702
function _read_config(ifname)
703
local ifile = assert(io.open(ifname))
704
local s = ifile:read "*a"
706
local closure = assert(loadstring(s))
708
setfenv(closure, config)
711
-- build the main menu
713
for _, entry in ipairs(config.menu) do
714
tbl[#tbl + 1] = string.format("<a href=\"%s.html\">%s</a>",
717
main_menu = table.concat(tbl, " ·\n")
719
config.menu_index = {}
720
_prepare_menu(config.menu)
726
-- Walk the menu tree and find entries that no file was generated for.
727
-- Either find a ".in" file in the build directory, or complain.
729
function _check_menu()
732
for basename, item in pairs(config.menu_index) do
733
if not item.seen then
734
ifname = string.format("%s/%s.html.in", output_dir, basename)
735
ofbase = string.format("%s.html", basename)
737
if lfs.attributes(ifname, "mode") == "file" then
738
print("Processing " .. ifname)
740
_process_html(ifname, ofbase, item)
742
print("Missing input file for", basename)
750
-- Create a HTML snippet with the alphabetically sorted index. All the
751
-- HTML files have already been read.
753
function generate_index()
754
local keys, buf, item
756
-- collect all the strings to be placed in the index, sort.
758
for _, item in ipairs(items) do
759
if not item.omit_index and item.text then
760
keys[#keys + 1] = { string.upper(item.text), item }
763
table.sort(keys, function(a, b) return a[1] < b[1] end)
765
-- combine index entries with the same string
766
for i, item in ipairs(keys) do
767
while keys[i + 1] and keys[i + 1][1] == item[1] do
768
item[#item + 1] = keys[i + 1][2]
769
table.remove(keys, i + 1)
773
-- build the index string
775
for i, tmp in ipairs(keys) do
776
buf[#buf + 1] = tmp[2].text .. ": "
778
buf[#buf + 1] = string.format('%s<a href="%s">%d</a>',
779
i > 2 and ", " or "",
780
tmp[i].full_anchor, i - 1)
783
buf[#buf + 1] = "<br/>\n"
786
index_string = table.concat(buf)
791
print(string.format("Usage: %s [input directory] [output directory]",
798
_read_config(arg[1] .. "/menu.lua")
799
_read_file_dir(arg[1])