* luci: add modified luadoc source to contrib
[project/luci.git] / contrib / luadoc / lua / luadoc / taglet / standard.lua
1 -------------------------------------------------------------------------------
2 -- @release $Id: standard.lua,v 1.39 2007/12/21 17:50:48 tomas Exp $
3 -------------------------------------------------------------------------------
4
5 local assert, pairs, tostring, type = assert, pairs, tostring, type
6 local io = require "io"
7 local lfs = require "lfs"
8 local luadoc = require "luadoc"
9 local util = require "luadoc.util"
10 local tags = require "luadoc.taglet.standard.tags"
11 local string = require "string"
12 local table = require "table"
13
14 module 'luadoc.taglet.standard'
15
16 -------------------------------------------------------------------------------
17 -- Creates an iterator for an array base on a class type.
18 -- @param t array to iterate over
19 -- @param class name of the class to iterate over
20
21 function class_iterator (t, class)
22         return function ()
23                 local i = 1
24                 return function ()
25                         while t[i] and t[i].class ~= class do
26                                 i = i + 1
27                         end
28                         local v = t[i]
29                         i = i + 1
30                         return v
31                 end
32         end
33 end
34
35 -- Patterns for function recognition
36 local identifiers_list_pattern = "%s*(.-)%s*"
37 local identifier_pattern = "[^%(%s]+"
38 local function_patterns = {
39         "^()%s*function%s*("..identifier_pattern..")%s*%("..identifiers_list_pattern.."%)",
40         "^%s*(local%s)%s*function%s*("..identifier_pattern..")%s*%("..identifiers_list_pattern.."%)",
41         "^()%s*("..identifier_pattern..")%s*%=%s*function%s*%("..identifiers_list_pattern.."%)",
42 }
43
44 -------------------------------------------------------------------------------
45 -- Checks if the line contains a function definition
46 -- @param line string with line text
47 -- @return function information or nil if no function definition found
48
49 local function check_function (line)
50         line = util.trim(line)
51
52         local info = table.foreachi(function_patterns, function (_, pattern)
53                 local r, _, l, id, param = string.find(line, pattern)
54                 if r ~= nil then
55                         return {
56                                 name = id,
57                                 private = (l == "local"),
58                                 param = util.split("%s*,%s*", param),
59                         }
60                 end
61         end)
62
63         -- TODO: remove these assert's?
64         if info ~= nil then
65                 assert(info.name, "function name undefined")
66                 assert(info.param, string.format("undefined parameter list for function `%s'", info.name))
67         end
68
69         return info
70 end
71
72 -------------------------------------------------------------------------------
73 -- Checks if the line contains a module definition.
74 -- @param line string with line text
75 -- @param currentmodule module already found, if any
76 -- @return the name of the defined module, or nil if there is no module 
77 -- definition
78
79 local function check_module (line, currentmodule)
80         line = util.trim(line)
81         
82         -- module"x.y"
83         -- module'x.y'
84         -- module[[x.y]]
85         -- module("x.y")
86         -- module('x.y')
87         -- module([[x.y]])
88         -- module(...)
89
90         local r, _, modulename = string.find(line, "^module%s*[%s\"'(%[]+([^,\"')%]]+)")
91         if r then
92                 -- found module definition
93                 logger:debug(string.format("found module `%s'", modulename))
94                 return modulename
95         end
96         return currentmodule
97 end
98
99 -------------------------------------------------------------------------------
100 -- Extracts summary information from a description. The first sentence of each 
101 -- doc comment should be a summary sentence, containing a concise but complete 
102 -- description of the item. It is important to write crisp and informative 
103 -- initial sentences that can stand on their own
104 -- @param description text with item description
105 -- @return summary string or nil if description is nil
106
107 local function parse_summary (description)
108         -- summary is never nil...
109         description = description or ""
110         
111         -- append an " " at the end to make the pattern work in all cases
112         description = description.." "
113
114         -- read until the first period followed by a space or tab       
115         local summary = string.match(description, "(.-%.)[%s\t]")
116         
117         -- if pattern did not find the first sentence, summary is the whole description
118         summary = summary or description
119         
120         return summary
121 end
122
123 -------------------------------------------------------------------------------
124 -- @param f file handle
125 -- @param line current line being parsed
126 -- @param modulename module already found, if any
127 -- @return current line
128 -- @return code block
129 -- @return modulename if found
130
131 local function parse_code (f, line, modulename)
132         local code = {}
133         while line ~= nil do
134                 if string.find(line, "^[\t ]*%-%-%-") then
135                         -- reached another luadoc block, end this parsing
136                         return line, code, modulename
137                 else
138                         -- look for a module definition
139                         modulename = check_module(line, modulename)
140
141                         table.insert(code, line)
142                         line = f:read()
143                 end
144         end
145         -- reached end of file
146         return line, code, modulename
147 end
148
149 -------------------------------------------------------------------------------
150 -- Parses the information inside a block comment
151 -- @param block block with comment field
152 -- @return block parameter
153
154 local function parse_comment (block, first_line)
155
156         -- get the first non-empty line of code
157         local code = table.foreachi(block.code, function(_, line)
158                 if not util.line_empty(line) then
159                         -- `local' declarations are ignored in two cases:
160                         -- when the `nolocals' option is turned on; and
161                         -- when the first block of a file is parsed (this is
162                         --      necessary to avoid confusion between the top
163                         --      local declarations and the `module' definition.
164                         if (options.nolocals or first_line) and line:find"^%s*local" then
165                                 return
166                         end
167                         return line
168                 end
169         end)
170         
171         -- parse first line of code
172         if code ~= nil then
173                 local func_info = check_function(code)
174                 local module_name = check_module(code)
175                 if func_info then
176                         block.class = "function"
177                         block.name = func_info.name
178                         block.param = func_info.param
179                         block.private = func_info.private
180                 elseif module_name then
181                         block.class = "module"
182                         block.name = module_name
183                         block.param = {}
184                 else
185                         block.param = {}
186                 end
187         else
188                 -- TODO: comment without any code. Does this means we are dealing
189                 -- with a file comment?
190         end
191
192         -- parse @ tags
193         local currenttag = "description"
194         local currenttext
195         
196         table.foreachi(block.comment, function (_, line)
197                 line = util.trim_comment(line)
198                 
199                 local r, _, tag, text = string.find(line, "@([_%w%.]+)%s+(.*)")
200                 if r ~= nil then
201                         -- found new tag, add previous one, and start a new one
202                         -- TODO: what to do with invalid tags? issue an error? or log a warning?
203                         tags.handle(currenttag, block, currenttext)
204                         
205                         currenttag = tag
206                         currenttext = text
207                 else
208                         currenttext = util.concat(currenttext, line)
209                         assert(string.sub(currenttext, 1, 1) ~= " ", string.format("`%s', `%s'", currenttext, line))
210                 end
211         end)
212         tags.handle(currenttag, block, currenttext)
213
214         -- extracts summary information from the description
215         block.summary = parse_summary(block.description)
216         assert(string.sub(block.description, 1, 1) ~= " ", string.format("`%s'", block.description))
217         
218         return block
219 end
220
221 -------------------------------------------------------------------------------
222 -- Parses a block of comment, started with ---. Read until the next block of
223 -- comment.
224 -- @param f file handle
225 -- @param line being parsed
226 -- @param modulename module already found, if any
227 -- @return line
228 -- @return block parsed
229 -- @return modulename if found
230
231 local function parse_block (f, line, modulename, first)
232         local block = {
233                 comment = {},
234                 code = {},
235         }
236
237         while line ~= nil do
238                 if string.find(line, "^[\t ]*%-%-") == nil then
239                         -- reached end of comment, read the code below it
240                         -- TODO: allow empty lines
241                         line, block.code, modulename = parse_code(f, line, modulename)
242                         
243                         -- parse information in block comment
244                         block = parse_comment(block, first)
245
246                         return line, block, modulename
247                 else
248                         table.insert(block.comment, line)
249                         line = f:read()
250                 end
251         end
252         -- reached end of file
253         
254         -- parse information in block comment
255         block = parse_comment(block, first)
256         
257         return line, block, modulename
258 end
259
260 -------------------------------------------------------------------------------
261 -- Parses a file documented following luadoc format.
262 -- @param filepath full path of file to parse
263 -- @param doc table with documentation
264 -- @return table with documentation
265
266 function parse_file (filepath, doc)
267         local blocks = {}
268         local modulename = nil
269         
270         -- read each line
271         local f = io.open(filepath, "r")
272         local i = 1
273         local line = f:read()
274         local first = true
275         while line ~= nil do
276                 if string.find(line, "^[\t ]*%-%-%-") then
277                         -- reached a luadoc block
278                         local block
279                         line, block, modulename = parse_block(f, line, modulename, first)
280                         table.insert(blocks, block)
281                 else
282                         -- look for a module definition
283                         modulename = check_module(line, modulename)
284                         
285                         -- TODO: keep beginning of file somewhere
286                         
287                         line = f:read()
288                 end
289                 first = false
290                 i = i + 1
291         end
292         f:close()
293         -- store blocks in file hierarchy
294         assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
295         table.insert(doc.files, filepath)
296         doc.files[filepath] = {
297                 type = "file",
298                 name = filepath,
299                 doc = blocks,
300 --              functions = class_iterator(blocks, "function"),
301 --              tables = class_iterator(blocks, "table"),
302         }
303 --
304         local first = doc.files[filepath].doc[1]
305         if first and modulename then
306                 doc.files[filepath].author = first.author
307                 doc.files[filepath].copyright = first.copyright
308                 doc.files[filepath].description = first.description
309                 doc.files[filepath].release = first.release
310                 doc.files[filepath].summary = first.summary
311         end
312
313         -- if module definition is found, store in module hierarchy
314         if modulename ~= nil then
315                 if modulename == "..." then
316                                 modulename = string.gsub (filepath, "%.lua$", "")
317                                 modulename = string.gsub (modulename, "/", ".")
318                 end
319                 if doc.modules[modulename] ~= nil then
320                         -- module is already defined, just add the blocks
321                         table.foreachi(blocks, function (_, v)
322                                 table.insert(doc.modules[modulename].doc, v)
323                         end)
324                 else
325                         -- TODO: put this in a different module
326                         table.insert(doc.modules, modulename)
327                         doc.modules[modulename] = {
328                                 type = "module",
329                                 name = modulename,
330                                 doc = blocks,
331 --                              functions = class_iterator(blocks, "function"),
332 --                              tables = class_iterator(blocks, "table"),
333                                 author = first and first.author,
334                                 copyright = first and first.copyright,
335                                 description = "",
336                                 release = first and first.release,
337                                 summary = "",
338                         }
339                         
340                         -- find module description
341                         for m in class_iterator(blocks, "module")() do
342                                 doc.modules[modulename].description = util.concat(
343                                         doc.modules[modulename].description, 
344                                         m.description)
345                                 doc.modules[modulename].summary = util.concat(
346                                         doc.modules[modulename].summary, 
347                                         m.summary)
348                                 if m.author then
349                                         doc.modules[modulename].author = m.author
350                                 end
351                                 if m.copyright then
352                                         doc.modules[modulename].copyright = m.copyright
353                                 end
354                                 if m.release then
355                                         doc.modules[modulename].release = m.release
356                                 end
357                                 if m.name then
358                                         doc.modules[modulename].name = m.name
359                                 end
360                         end
361                         doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
362                         doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
363                 end
364                 
365                 -- make functions table
366                 doc.modules[modulename].functions = {}
367                 for f in class_iterator(blocks, "function")() do
368                         if f and f.name then
369                                 table.insert(doc.modules[modulename].functions, f.name)
370                                 doc.modules[modulename].functions[f.name] = f
371                         end
372                 end
373                 
374                 -- make tables table
375                 doc.modules[modulename].tables = {}
376                 for t in class_iterator(blocks, "table")() do
377                         if t and t.name then
378                                 table.insert(doc.modules[modulename].tables, t.name)
379                                 doc.modules[modulename].tables[t.name] = t
380                         end
381                 end
382         end
383         
384         -- make functions table
385         doc.files[filepath].functions = {}
386         for f in class_iterator(blocks, "function")() do
387                 if f and f.name then
388                         table.insert(doc.files[filepath].functions, f.name)
389                         doc.files[filepath].functions[f.name] = f
390                 end
391         end
392         
393         -- make tables table
394         doc.files[filepath].tables = {}
395         for t in class_iterator(blocks, "table")() do
396                 if t and t.name then
397                         table.insert(doc.files[filepath].tables, t.name)
398                         doc.files[filepath].tables[t.name] = t
399                 end
400         end
401         
402         return doc
403 end
404
405 -------------------------------------------------------------------------------
406 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the 
407 -- function that does the actual parsing
408 -- @param filepath full path of the file to parse
409 -- @param doc table with documentation
410 -- @return table with documentation
411 -- @see parse_file
412
413 function file (filepath, doc)
414         local patterns = { "%.lua$", "%.luadoc$" }
415         local valid = table.foreachi(patterns, function (_, pattern)
416                 if string.find(filepath, pattern) ~= nil then
417                         return true
418                 end
419         end)
420         
421         if valid then
422                 logger:info(string.format("processing file `%s'", filepath))
423                 doc = parse_file(filepath, doc)
424         end
425         
426         return doc
427 end
428
429 -------------------------------------------------------------------------------
430 -- Recursively iterates through a directory, parsing each file
431 -- @param path directory to search
432 -- @param doc table with documentation
433 -- @return table with documentation
434
435 function directory (path, doc)
436         for f in lfs.dir(path) do
437                 local fullpath = path .. "/" .. f
438                 local attr = lfs.attributes(fullpath)
439                 assert(attr, string.format("error stating file `%s'", fullpath))
440                 
441                 if attr.mode == "file" then
442                         doc = file(fullpath, doc)
443                 elseif attr.mode == "directory" and f ~= "." and f ~= ".." then
444                         doc = directory(fullpath, doc)
445                 end
446         end
447         return doc
448 end
449
450 -- Recursively sorts the documentation table
451 local function recsort (tab)
452         table.sort (tab)
453         -- sort list of functions by name alphabetically
454         for f, doc in pairs(tab) do
455                 if doc.functions then
456                         table.sort(doc.functions)
457                 end
458                 if doc.tables then
459                         table.sort(doc.tables)
460                 end
461         end
462 end
463
464 -------------------------------------------------------------------------------
465
466 function start (files, doc)
467         assert(files, "file list not specified")
468         
469         -- Create an empty document, or use the given one
470         doc = doc or {
471                 files = {},
472                 modules = {},
473         }
474         assert(doc.files, "undefined `files' field")
475         assert(doc.modules, "undefined `modules' field")
476         
477         table.foreachi(files, function (_, path)
478                 local attr = lfs.attributes(path)
479                 assert(attr, string.format("error stating path `%s'", path))
480                 
481                 if attr.mode == "file" then
482                         doc = file(path, doc)
483                 elseif attr.mode == "directory" then
484                         doc = directory(path, doc)
485                 end
486         end)
487         
488         -- order arrays alphabetically
489         recsort(doc.files)
490         recsort(doc.modules)
491
492         return doc
493 end