Merge branch 'master' into translations
[QuestHelper.git] / Development / docgen
blobbfc03b644ffc104d500bbc3692462ff4fbda1565
1 #!/usr/bin/env lua
3 -- Reads comments from lua files and uses them to generate documentation. Why? Because having
4 -- the documentation in the same location as stuff that's being documented makes life easier.
6 -- You can probably read this file for examples on how to document stuff.
8 loadfile("dump.lua")()
9 loadfile("fileutil.lua")()
11 local default_files =
12   {
13    "../Generic/table.lua",
14    "../Generic/cron.lua",
15    "../Generic/sortedlist.lua",
16    "../Generic/graph.lua"
17   }
19 local empty_table = {}
20 local items = {}
21 local next_anchor = 1
23 -- object = getObject(name, list)
24 -- .name (string) The name of the documented item.
25 -- .list (table) The table to search for the item in.
26 -- .object (docobj) The object pointed to by name.
27 local function getObject(name, list, parent, rel)
28   if not list then
29     list = items
30     assert(rel == nil)
31     rel = "."
32   end
33   
34   print("Name: "..name)
35   
36   assert(rel ~= nil)
37   
38   local _, _, real_name, new_rel, remainder = string.find(name, "(.-)%s*([%.:])%s*(.+)")
39   
40   if real_name then
41     name = real_name
42   end
43   
44   local item = list[name]
45   
46   if not item then
47     item = {}
48     list[name] = item
49     
50     item.name = name
51     item.fname = (parent and parent.fname and parent.fname .. "." .. name) or name
52     item.type = "<unknown>"
53     item.file = "<unknown>"
54     item.line = -1
55     item.anchor = "anchor_"..next_anchor
56     next_anchor = next_anchor + 1
57     
58     item.children = {}
59     item.notes = {}
60   end
61   
62   if remainder then
63     if rel ~= "." then
64       print("Name: "..name)
65       print("Remainder: "..remainder)
66       print("Misplaced ':' character?")
67     end
68     
69     return getObject(remainder, item.children, item, new_rel)
70   else
71     return item, rel
72   end
73 end
75 -- valid = isVariableString(var)
76 -- .var (string) The name of a variable.
77 -- .valid (boolean) True if var is a valid variable name.
78 -- Checks if a string would make a valid variable name.
79 local function isVariableString(var)
80   return string.len(var) > 0 and string.find(var, "^[%a_][%a%d_]*$") or var == "..."
81 end
83 -- array = readList(list)
84 -- .list (string) A comma seperated list of items.
85 -- .array (array) The list, broken up into tokens.
86 -- Breaks string containing a comma seperated list of items into tokens.
87 -- Returns nil if the list couldn't be parsed.
88 local function readVariableList(list)
89   local result = {}
90   
91   for arg in string.gmatch(list, "%s*([^,%s]+)%s*,?") do
92     if isVariableString(arg) then
93       table.insert(result, arg == "..." and "!" or arg)
94     else
95       return nil
96     end
97   end
98   
99   return result
102 -- name, arguments, returns = readFunctionLine(line)
103 -- .line (string) The line to read.
104 -- .name(string) The name of the function.
105 -- .arguments (array) An array of arguments to the function.
106 -- .returns (array) An array of return values from the function.
107 -- Returns nothing if it couldn't parse the line.
108 local function readFunctionLine(line)
109   local function_chunk, argument_chunk = select(3, string.find(line, "^(.-)%((.*)%)%s*$"))
110   if not function_chunk then return end
111   
112   local return_chunk, function_name_chunk = select(3, string.find(function_chunk, "^(.*)=%s*([^%s]+)%s*$"))
113   
114   if not function_name_chunk then
115     return_chunk, function_name_chunk = "", select(3, string.find(function_chunk, "^%s*([^%s]+)%s*$"))
116   end
117   
118   if function_name_chunk then
119     local arguments, returns = readVariableList(argument_chunk), readVariableList(return_chunk)
120     
121     if arguments and returns then
122       return function_name_chunk, arguments, returns
123     end
124   end
127 -- processComment(file, line, comment)
128 -- .file (string) The name of the file the comment came from.
129 -- .line (number) The line number of the file the comment came from.
130 -- .comment (table) An array of strings, the lines making up the comment.
131 -- Parses comments to extract information from them.
132 local function processComment(file, line, comment)
133   local obj, rel = nil, nil
134   
135   
136   local func, arg, ret = readFunctionLine(comment[1])
137   
138   if func then
139     obj, rel = getObject(func)
140     obj.type = "function"
141     obj.arg = arg
142     obj.ret = ret
143   else
144     local var, typename, desc = select(3, string.find(line, "^%s*%.(.-)%s*%((%a+)%)%s*(.-)%s*$"))
145     if var then
146       obj, rel = getObject(func)
147       obj.type = typename
148     end
149   end
150   
151   if obj then
152     obj.file = file
153     obj.line = line
154     
155     if obj.arg and rel == ":" then
156       table.insert(obj.arg, 1, "self")
157     end
158     
159     print((next(ret) and table.concat(ret, ", ") or "<nil>").." = "..func.."("..table.concat(arg, ", ")..")")
160     
161     for i = 2,#comment do
162       local line = comment[i]
163       local item, typename, desc = select(3, string.find(line, "^%s*%.(.-)%s*%((%a+)%)%s*(.-)%s*$"))
164       
165       if item then
166         local cobj = getObject(obj.fname.."."..(item == "..." and "!" or item))
167         cobj.file = file
168         cobj.line = line
169         cobj.type = typename
170         table.insert(cobj.notes, desc)
171       else
172         table.insert(obj.notes, line)
173       end
174     end
175   end
176   
177   -- TODO: Parse comments.
178   
179   --for i, line in ipairs(comment) do
180   --  print(line)
181   --end
184 -- clearTable(tbl)
185 -- .tbl (table) The table to clear.
186 -- Goes through a table and deletes all its keys.
187 local function clearTable(tbl)
188   for key in pairs(tbl) do
189     tbl[key] = nil
190   end
193 -- readLuaFile(file)
194 -- .file (string) The name of the file to read.
195 -- Reads the comments from a file and passes the comments to [processComment]
196 local function readLuaFile(file)
197   local stream = io.open(file, "r")
198   
199   if not stream then
200     print("Unable to open file: "..file)
201     return
202   end
203   
204   local comment = {}, comment_line, comment_type
205   
206   local line_number = 0
207   local line_remainder
208   
209   while true do
210     local line
211     if line_remainder and line_remainder ~= "" then
212       line = line_remainder
213       line_remainder = nil
214     else
215       line_number = line_number + 1
216       line = stream:read()
217       if not line then break end
218     end
219     
220     if next(comment) and comment_type == 2 then
221       local comment_text
222       comment_text, line_remainder = select(3, string.find(line, "^%s*(.-)%s*%]%](.*)"))
223       
224       
225       if comment_text then
226         table.insert(comment, comment_text)
227         processComment(file, comment_line, comment)
228         clearTable(comment)
229       else
230         table.insert(comment, line)
231       end
232     else
233       local comment_text = select(3, string.find(line, "^%s*%-%-%[%[%s*(.-)%s*$"))
234       
235       if comment_text then
236         if next(comment) then
237           processComment(file, comment_line, comment)
238           clearTable(comment)
239         end
240         
241         local short_text
242         short_text, line_remainder = select(3, string.find(comment_text, "^(.-)%s*%]%](.*)"))
243         
244         if short_text then
245           table.insert(comment, short_text)
246           processComment(file, line, comment)
247           clearTable(comment)
248         else
249           table.insert(comment, comment_text)
250           comment_type = 2
251           comment_line = line_number
252         end
253       else
254         comment_text = select(3, string.find(line, "^%s*%-%-%s*(.-)%s*$"))
255         if comment_text then
256           if next(comment) then
257             table.insert(comment, comment_text)
258           else
259             table.insert(comment, comment_text)
260             comment_type = 1
261             comment_line = line_number
262           end
263         elseif next(comment) then
264           processComment(file, comment_line, comment)
265           clearTable(comment)
266         end
267       end
268     end
269   end
270   
271   if next(comment) then
272     processComment(file, comment_line, comment)
273   end
274   
275   io.close(stream)
278 for i, file in ipairs(#arg > 0 and arg or default_files) do
279   readLuaFile(file)
282 local function HTMLText(text)
283   return string.gsub(text, ".", function (c)
284     if c == "<" then return "&lt;" end
285     if c == ">" then return "&gt;" end
286     if c == " " then return "&nbsp;" end
287     if c == "&" then return "&amp;" end
288     if c == "\"" then return "&quot;" end
289     if c == "\n" then return "<br/>" end
290     return c
291     end)
294 local function ParseParagraph(text)
295   -- TODO: Do this right.
296   return "<p>"..HTMLText(text).."</p>"
299 local function DescriptionText(lines)
300   local result = ""
301   local paragraph = ""
302   
303   for i, line in ipairs(lines) do
304     -- TODO: Actually parse the lines.
305     line = select(3, string.find(line, "^%s*(.-)%s*$"))
306     
307     if line == "" then
308       result = result .. ParseParagraph(paragraph)
309       paragraph = ""
310     else
311       paragraph = paragraph .. " " .. line
312     end
313   end
314   
315   if paragraph ~= "" then
316     result = result .. ParseParagraph(paragraph)
317   end
318   
319   return result
322 local WriteDocObject
324 local function WriteDocObjectList(list, prefix, buffer)
325   local array = {}
326   for key in pairs(list) do
327     table.insert(array, key)
328   end
329   
330   table.sort(array)
331   
332   for i, key in ipairs(array) do
333     buffer:add("<div class=\"item\">")
334     WriteDocObject(list[key], prefix, buffer)
335     buffer:add("</div>")
336   end
339 local function WriteDocObjectDescription(obj, prefix, buffer)
340   if not obj then
341     buffer:add("<p>No information available.</p>")
342   elseif obj.type == "function" then
343     if #obj.ret > 0 then
344       for i, name in ipairs(obj.ret) do
345         buffer:add("<span class=\"argument\">")
346         buffer:add(name == "!" and "..." or name)
347         buffer:add("<div class=\"description\">")
348         WriteDocObjectDescription(obj.children[name], nil, buffer)
349         buffer:add("</div>")
350         buffer:add("</span>")
351         if i ~= #obj.ret then buffer:add(", ") end
352       end
353       buffer:add(" = ")
354     end
355     
356     local first_arg = 1
357     if obj.arg[1] == "self" then
358       first_arg = 2
359       buffer:add((prefix or "???")..":")
360     else
361       buffer:add((prefix and (prefix .. ".")) or "")
362     end
363     
364     buffer:add("<span class=\"argument\">")
365     buffer:add(obj.name == "!" and "..." or obj.name)
366     buffer:add("</span>(")
367     
368     for arg = first_arg, #obj.arg do
369       local name = obj.arg[arg]
370       buffer:add("<span class=\"argument\">")
371       buffer:add(name == "!" and "..." or name)
372       buffer:add("<div class=\"description\">")
373       WriteDocObjectDescription(obj.children[name], nil, buffer)
374       buffer:add("</div>")
375       buffer:add("</span>")
376       if arg ~= #obj.arg then buffer:add(", ") end
377     end
378     
379     buffer:add(") <span class=\"typename\">function</span>")
380     buffer:add(DescriptionText(obj.notes))
381   else
382     buffer:add("<span class=\"argument\">"..HTMLText(obj.name or "unknown").."</span> <span class=\"typename\">"..HTMLText(obj.type or "unknown").."</span>")
383     WriteDocObjectList(obj.children, prefix and (prefix.."."..obj.name) or obj.name, buffer)
384     buffer:add(DescriptionText(obj.notes))
385   end
388 WriteDocObject = function(obj, prefix, buffer)
389   if not obj then
390     buffer:add("No information available.")
391   else
392     buffer:add("<a name=\""..obj.anchor.."\">")
393     WriteDocObjectDescription(obj, prefix, buffer)
394     buffer:add("</a>")
395   end
398 local function WritePage(filename, title, prefix, list)
399   local buffer = CreateBuffer()
400   buffer:add(string.format(
402 <?xml version="1.0" encoding="UTF-8"?>
403 <?xml-stylesheet href="style.css" type="text/css"?>
404 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
405 <html xmlns="http://www.w3.org/1999/xhtml"><head>
406   <title>%s</title>
407 </head><body><h1>%s</h1>
408 ]], title, title))
409   
410   WriteDocObjectList(list, prefix, buffer)
411   buffer:add("</body></html>")
412   
413   local stream = io.open(filename, "w")
414   if stream then
415     stream:write(buffer:dump())
416     io.close(stream)
417   else
418     print("Unable to write file: "..filename)
419   end
422 FileUtil.createDirectory("API")
423 FileUtil.copyFile("Data/style.css", "API/style.css")
424 WritePage("API/api.xhtml", "QuestHelper API Documentation", nil, items)