* luci/contrib: fixed anchors in virtual module documentation, only render parameters...
[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 posix = require "posix"
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, modulename)
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         if block.name and block.class == "module" then
219                 modulename = block.name
220         end
221
222         return block, modulename
223 end
224
225 -------------------------------------------------------------------------------
226 -- Parses a block of comment, started with ---. Read until the next block of
227 -- comment.
228 -- @param f file handle
229 -- @param line being parsed
230 -- @param modulename module already found, if any
231 -- @return line
232 -- @return block parsed
233 -- @return modulename if found
234
235 local function parse_block (f, line, modulename, first)
236         local block = {
237                 comment = {},
238                 code = {},
239         }
240
241         while line ~= nil do
242                 if string.find(line, "^[\t ]*%-%-") == nil then
243                         -- reached end of comment, read the code below it
244                         -- TODO: allow empty lines
245                         line, block.code, modulename = parse_code(f, line, modulename)
246
247                         -- parse information in block comment
248                         block, modulename = parse_comment(block, first, modulename)
249
250                         return line, block, modulename
251                 else
252                         table.insert(block.comment, line)
253                         line = f:read()
254                 end
255         end
256         -- reached end of file
257
258         -- parse information in block comment
259         block, modulename = parse_comment(block, first, modulename)
260
261         return line, block, modulename
262 end
263
264 -------------------------------------------------------------------------------
265 -- Parses a file documented following luadoc format.
266 -- @param filepath full path of file to parse
267 -- @param doc table with documentation
268 -- @return table with documentation
269
270 function parse_file (filepath, doc, handle, prev_line, prev_block, prev_modname)
271         local blocks = { prev_block }
272         local modulename = prev_modname
273
274         -- read each line
275         local f = handle or io.open(filepath, "r")
276         local i = 1
277         local line = prev_line or f:read()
278         local first = true
279         while line ~= nil do
280
281                 if string.find(line, "^[\t ]*%-%-%-") then
282                         -- reached a luadoc block
283                         local block, newmodname
284                         line, block, newmodname = parse_block(f, line, modulename, first)
285
286                         if modulename and newmodname and newmodname ~= modulename then
287                                 doc = parse_file( nil, doc, f, line, block, newmodname )
288                         else
289                                 table.insert(blocks, block)
290                                 modulename = newmodname
291                         end
292                 else
293                         -- look for a module definition
294                         local newmodname = check_module(line, modulename)
295
296                         if modulename and newmodname and newmodname ~= modulename then
297                                 parse_file( nil, doc, f )
298                         else
299                                 modulename = newmodname
300                         end
301
302                         -- TODO: keep beginning of file somewhere
303
304                         line = f:read()
305                 end
306                 first = false
307                 i = i + 1
308         end
309
310         if not handle then
311                 f:close()
312         end
313
314         if filepath then
315                 -- store blocks in file hierarchy
316                 assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
317                 table.insert(doc.files, filepath)
318                 doc.files[filepath] = {
319                         type = "file",
320                         name = filepath,
321                         doc = blocks,
322         --              functions = class_iterator(blocks, "function"),
323         --              tables = class_iterator(blocks, "table"),
324                 }
325         --
326                 local first = doc.files[filepath].doc[1]
327                 if first and modulename then
328                         doc.files[filepath].author = first.author
329                         doc.files[filepath].copyright = first.copyright
330                         doc.files[filepath].description = first.description
331                         doc.files[filepath].release = first.release
332                         doc.files[filepath].summary = first.summary
333                 end
334         end
335
336         -- if module definition is found, store in module hierarchy
337         if modulename ~= nil then
338                 if modulename == "..." then
339                         assert( filepath, "Can't determine name for virtual module from filepatch" )
340                         modulename = string.gsub (filepath, "%.lua$", "")
341                         modulename = string.gsub (modulename, "/", ".")
342                 end
343                 if doc.modules[modulename] ~= nil then
344                         -- module is already defined, just add the blocks
345                         table.foreachi(blocks, function (_, v)
346                                 table.insert(doc.modules[modulename].doc, v)
347                         end)
348                 else
349                         -- TODO: put this in a different module
350                         table.insert(doc.modules, modulename)
351                         doc.modules[modulename] = {
352                                 type = "module",
353                                 name = modulename,
354                                 doc = blocks,
355 --                              functions = class_iterator(blocks, "function"),
356 --                              tables = class_iterator(blocks, "table"),
357                                 author = first and first.author,
358                                 copyright = first and first.copyright,
359                                 description = "",
360                                 release = first and first.release,
361                                 summary = "",
362                         }
363
364                         -- find module description
365                         for m in class_iterator(blocks, "module")() do
366                                 doc.modules[modulename].description = util.concat(
367                                         doc.modules[modulename].description,
368                                         m.description)
369                                 doc.modules[modulename].summary = util.concat(
370                                         doc.modules[modulename].summary,
371                                         m.summary)
372                                 if m.author then
373                                         doc.modules[modulename].author = m.author
374                                 end
375                                 if m.copyright then
376                                         doc.modules[modulename].copyright = m.copyright
377                                 end
378                                 if m.release then
379                                         doc.modules[modulename].release = m.release
380                                 end
381                                 if m.name then
382                                         doc.modules[modulename].name = m.name
383                                 end
384                         end
385                         doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
386                         doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
387                 end
388
389                 -- make functions table
390                 doc.modules[modulename].functions = {}
391                 for f in class_iterator(blocks, "function")() do
392                         if f and f.name then
393                                 table.insert(doc.modules[modulename].functions, f.name)
394                                 doc.modules[modulename].functions[f.name] = f
395                         end
396                 end
397
398                 -- make tables table
399                 doc.modules[modulename].tables = {}
400                 for t in class_iterator(blocks, "table")() do
401                         if t and t.name then
402                                 table.insert(doc.modules[modulename].tables, t.name)
403                                 doc.modules[modulename].tables[t.name] = t
404                         end
405                 end
406         end
407
408         if filepath then
409                 -- make functions table
410                 doc.files[filepath].functions = {}
411                 for f in class_iterator(blocks, "function")() do
412                         if f and f.name then
413                                 table.insert(doc.files[filepath].functions, f.name)
414                                 doc.files[filepath].functions[f.name] = f
415                         end
416                 end
417
418                 -- make tables table
419                 doc.files[filepath].tables = {}
420                 for t in class_iterator(blocks, "table")() do
421                         if t and t.name then
422                                 table.insert(doc.files[filepath].tables, t.name)
423                                 doc.files[filepath].tables[t.name] = t
424                         end
425                 end
426         end
427
428         return doc
429 end
430
431 -------------------------------------------------------------------------------
432 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the
433 -- function that does the actual parsing
434 -- @param filepath full path of the file to parse
435 -- @param doc table with documentation
436 -- @return table with documentation
437 -- @see parse_file
438
439 function file (filepath, doc)
440         local patterns = { "%.lua$", "%.luadoc$" }
441         local valid = table.foreachi(patterns, function (_, pattern)
442                 if string.find(filepath, pattern) ~= nil then
443                         return true
444                 end
445         end)
446
447         if valid then
448                 logger:info(string.format("processing file `%s'", filepath))
449                 doc = parse_file(filepath, doc)
450         end
451
452         return doc
453 end
454
455 -------------------------------------------------------------------------------
456 -- Recursively iterates through a directory, parsing each file
457 -- @param path directory to search
458 -- @param doc table with documentation
459 -- @return table with documentation
460
461 function directory (path, doc)
462         for f in posix.files(path) do
463                 local fullpath = path .. "/" .. f
464                 local attr = posix.stat(fullpath)
465                 assert(attr, string.format("error stating file `%s'", fullpath))
466
467                 if attr.type == "regular" then
468                         doc = file(fullpath, doc)
469                 elseif attr.type == "directory" and f ~= "." and f ~= ".." then
470                         doc = directory(fullpath, doc)
471                 end
472         end
473         return doc
474 end
475
476 -- Recursively sorts the documentation table
477 local function recsort (tab)
478         table.sort (tab)
479         -- sort list of functions by name alphabetically
480         for f, doc in pairs(tab) do
481                 if doc.functions then
482                         table.sort(doc.functions)
483                 end
484                 if doc.tables then
485                         table.sort(doc.tables)
486                 end
487         end
488 end
489
490 -------------------------------------------------------------------------------
491
492 function start (files, doc)
493         assert(files, "file list not specified")
494
495         -- Create an empty document, or use the given one
496         doc = doc or {
497                 files = {},
498                 modules = {},
499         }
500         assert(doc.files, "undefined `files' field")
501         assert(doc.modules, "undefined `modules' field")
502
503         table.foreachi(files, function (_, path)
504                 local attr = posix.stat(path)
505                 assert(attr, string.format("error stating path `%s'", path))
506
507                 if attr.type == "regular" then
508                         doc = file(path, doc)
509                 elseif attr.type == "directory" then
510                         doc = directory(path, doc)
511                 end
512         end)
513
514         -- order arrays alphabetically
515         recsort(doc.files)
516         recsort(doc.modules)
517
518         return doc
519 end