Merge pull request #304 from nmav/ocserv-crypt
[project/luci.git] / build / luadoc / 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 "nixio.fs"
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 -- Patterns for constant recognition
100 local constant_patterns = {
101         "^()%s*([A-Z][A-Z0-9_]*)%s*=",
102         "^%s*(local%s)%s*([A-Z][A-Z0-9_]*)%s*=",
103 }
104
105 -------------------------------------------------------------------------------
106 -- Checks if the line contains a constant definition
107 -- @param line string with line text
108 -- @return constant information or nil if no constant definition found
109
110 local function check_constant (line)
111         line = util.trim(line)
112
113         local info = table.foreachi(constant_patterns, function (_, pattern)
114                 local r, _, l, id = string.find(line, pattern)
115                 if r ~= nil then
116                         return {
117                                 name = id,
118                                 private = (l == "local"),
119                         }
120                 end
121         end)
122
123         -- TODO: remove these assert's?
124         if info ~= nil then
125                 assert(info.name, "constant name undefined")
126         end
127
128         return info
129 end
130
131 -------------------------------------------------------------------------------
132 -- Extracts summary information from a description. The first sentence of each
133 -- doc comment should be a summary sentence, containing a concise but complete
134 -- description of the item. It is important to write crisp and informative
135 -- initial sentences that can stand on their own
136 -- @param description text with item description
137 -- @return summary string or nil if description is nil
138
139 local function parse_summary (description)
140         -- summary is never nil...
141         description = description or ""
142
143         -- append an " " at the end to make the pattern work in all cases
144         description = description.." "
145
146         -- read until the first period followed by a space or tab
147         local summary = string.match(description, "(.-%.)[%s\t]")
148
149         -- if pattern did not find the first sentence, summary is the whole description
150         summary = summary or description
151
152         return summary
153 end
154
155 -------------------------------------------------------------------------------
156 -- @param f file handle
157 -- @param line current line being parsed
158 -- @param modulename module already found, if any
159 -- @return current line
160 -- @return code block
161 -- @return modulename if found
162
163 local function parse_code (f, line, modulename)
164         local code = {}
165         while line ~= nil do
166                 if string.find(line, "^[\t ]*%-%-%-") then
167                         -- reached another luadoc block, end this parsing
168                         return line, code, modulename
169                 else
170                         -- look for a module definition
171                         modulename = check_module(line, modulename)
172
173                         table.insert(code, line)
174                         line = f:read()
175                 end
176         end
177         -- reached end of file
178         return line, code, modulename
179 end
180
181 -------------------------------------------------------------------------------
182 -- Parses the information inside a block comment
183 -- @param block block with comment field
184 -- @return block parameter
185
186 local function parse_comment (block, first_line, modulename)
187
188         -- get the first non-empty line of code
189         local code = table.foreachi(block.code, function(_, line)
190                 if not util.line_empty(line) then
191                         -- `local' declarations are ignored in two cases:
192                         -- when the `nolocals' option is turned on; and
193                         -- when the first block of a file is parsed (this is
194                         --      necessary to avoid confusion between the top
195                         --      local declarations and the `module' definition.
196                         if (options.nolocals or first_line) and line:find"^%s*local" then
197                                 return
198                         end
199                         return line
200                 end
201         end)
202
203         -- parse first line of code
204         if code ~= nil then
205                 local func_info = check_function(code)
206                 local module_name = check_module(code)
207                 local const_info = check_constant(code)
208                 if func_info then
209                         block.class = "function"
210                         block.name = func_info.name
211                         block.param = func_info.param
212                         block.private = func_info.private
213                 elseif const_info then
214                         block.class = "constant"
215                         block.name = const_info.name
216                         block.private = const_info.private
217                 elseif module_name then
218                         block.class = "module"
219                         block.name = module_name
220                         block.param = {}
221                 else
222                         block.param = {}
223                 end
224         else
225                 -- TODO: comment without any code. Does this means we are dealing
226                 -- with a file comment?
227         end
228
229         -- parse @ tags
230         local currenttag = "description"
231         local currenttext
232
233         table.foreachi(block.comment, function (_, line)
234                 line = util.trim_comment(line)
235
236                 local r, _, tag, text = string.find(line, "@([_%w%.]+)%s+(.*)")
237                 if r ~= nil then
238                         -- found new tag, add previous one, and start a new one
239                         -- TODO: what to do with invalid tags? issue an error? or log a warning?
240                         tags.handle(currenttag, block, currenttext)
241
242                         currenttag = tag
243                         currenttext = text
244                 else
245                         currenttext = util.concat(currenttext, "\n" .. line)
246                         assert(string.sub(currenttext, 1, 1) ~= " ", string.format("`%s', `%s'", currenttext, line))
247                 end
248         end)
249         tags.handle(currenttag, block, currenttext)
250
251         -- extracts summary information from the description
252         block.summary = parse_summary(block.description)
253         assert(string.sub(block.description, 1, 1) ~= " ", string.format("`%s'", block.description))
254
255         if block.name and block.class == "module" then
256                 modulename = block.name
257         end
258
259         return block, modulename
260 end
261
262 -------------------------------------------------------------------------------
263 -- Parses a block of comment, started with ---. Read until the next block of
264 -- comment.
265 -- @param f file handle
266 -- @param line being parsed
267 -- @param modulename module already found, if any
268 -- @return line
269 -- @return block parsed
270 -- @return modulename if found
271
272 local function parse_block (f, line, modulename, first)
273         local multiline = not not (line and line:match("%[%["))
274         local block = {
275                 comment = {},
276                 code = {},
277         }
278
279         while line ~= nil do
280                 if (multiline == true and string.find(line, "%]%]") ~= nil) or
281                    (multiline == false and string.find(line, "^[\t ]*%-%-") == nil) then
282                         -- reached end of comment, read the code below it
283                         -- TODO: allow empty lines
284                         line, block.code, modulename = parse_code(f, line, modulename)
285
286                         -- parse information in block comment
287                         block, modulename = parse_comment(block, first, modulename)
288
289                         return line, block, modulename
290                 else
291                         table.insert(block.comment, line)
292                         line = f:read()
293                 end
294         end
295         -- reached end of file
296
297         -- parse information in block comment
298         block, modulename = parse_comment(block, first, modulename)
299
300         return line, block, modulename
301 end
302
303 -------------------------------------------------------------------------------
304 -- Parses a file documented following luadoc format.
305 -- @param filepath full path of file to parse
306 -- @param doc table with documentation
307 -- @return table with documentation
308
309 function parse_file (filepath, doc, handle, prev_line, prev_block, prev_modname)
310         local blocks = { prev_block }
311         local modulename = prev_modname
312
313         -- read each line
314         local f = handle or io.open(filepath, "r")
315         local i = 1
316         local line = prev_line or f:read()
317         local first = true
318         while line ~= nil do
319
320                 if string.find(line, "^[\t ]*%-%-%-") then
321                         -- reached a luadoc block
322                         local block, newmodname
323                         line, block, newmodname = parse_block(f, line, modulename, first)
324
325                         if modulename and newmodname and newmodname ~= modulename then
326                                 doc = parse_file( nil, doc, f, line, block, newmodname )
327                         else
328                                 table.insert(blocks, block)
329                                 modulename = newmodname
330                         end
331                 else
332                         -- look for a module definition
333                         local newmodname = check_module(line, modulename)
334
335                         if modulename and newmodname and newmodname ~= modulename then
336                                 parse_file( nil, doc, f )
337                         else
338                                 modulename = newmodname
339                         end
340
341                         -- TODO: keep beginning of file somewhere
342
343                         line = f:read()
344                 end
345                 first = false
346                 i = i + 1
347         end
348
349         if not handle then
350                 f:close()
351         end
352
353         if filepath then
354                 -- store blocks in file hierarchy
355                 assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
356                 table.insert(doc.files, filepath)
357                 doc.files[filepath] = {
358                         type = "file",
359                         name = filepath,
360                         doc = blocks,
361         --              functions = class_iterator(blocks, "function"),
362         --              tables = class_iterator(blocks, "table"),
363                 }
364         --
365                 local first = doc.files[filepath].doc[1]
366                 if first and modulename then
367                         doc.files[filepath].author = first.author
368                         doc.files[filepath].copyright = first.copyright
369                         doc.files[filepath].description = first.description
370                         doc.files[filepath].release = first.release
371                         doc.files[filepath].summary = first.summary
372                 end
373         end
374
375         -- if module definition is found, store in module hierarchy
376         if modulename ~= nil then
377                 if modulename == "..." then
378                         assert( filepath, "Can't determine name for virtual module from filepatch" )
379                         modulename = string.gsub (filepath, "%.lua$", "")
380                         modulename = string.gsub (modulename, "/", ".")
381                 end
382                 if doc.modules[modulename] ~= nil then
383                         -- module is already defined, just add the blocks
384                         table.foreachi(blocks, function (_, v)
385                                 table.insert(doc.modules[modulename].doc, v)
386                         end)
387                 else
388                         -- TODO: put this in a different module
389                         table.insert(doc.modules, modulename)
390                         doc.modules[modulename] = {
391                                 type = "module",
392                                 name = modulename,
393                                 doc = blocks,
394 --                              functions = class_iterator(blocks, "function"),
395 --                              tables = class_iterator(blocks, "table"),
396                                 author = first and first.author,
397                                 copyright = first and first.copyright,
398                                 description = "",
399                                 release = first and first.release,
400                                 summary = "",
401                         }
402
403                         -- find module description
404                         for m in class_iterator(blocks, "module")() do
405                                 doc.modules[modulename].description = util.concat(
406                                         doc.modules[modulename].description,
407                                         m.description)
408                                 doc.modules[modulename].summary = util.concat(
409                                         doc.modules[modulename].summary,
410                                         m.summary)
411                                 if m.author then
412                                         doc.modules[modulename].author = m.author
413                                 end
414                                 if m.copyright then
415                                         doc.modules[modulename].copyright = m.copyright
416                                 end
417                                 if m.release then
418                                         doc.modules[modulename].release = m.release
419                                 end
420                                 if m.name then
421                                         doc.modules[modulename].name = m.name
422                                 end
423                         end
424                         doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
425                         doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
426                 end
427
428                 -- make functions table
429                 doc.modules[modulename].functions = {}
430                 for f in class_iterator(blocks, "function")() do
431                         if f and f.name then
432                                 table.insert(doc.modules[modulename].functions, f.name)
433                                 doc.modules[modulename].functions[f.name] = f
434                         end
435                 end
436
437                 -- make tables table
438                 doc.modules[modulename].tables = {}
439                 for t in class_iterator(blocks, "table")() do
440                         if t and t.name then
441                                 table.insert(doc.modules[modulename].tables, t.name)
442                                 doc.modules[modulename].tables[t.name] = t
443                         end
444                 end
445
446                 -- make constants table
447                 doc.modules[modulename].constants = {}
448                 for c in class_iterator(blocks, "constant")() do
449                         if c and c.name then
450                                 table.insert(doc.modules[modulename].constants, c.name)
451                                 doc.modules[modulename].constants[c.name] = c
452                         end
453                 end
454         end
455
456         if filepath then
457                 -- make functions table
458                 doc.files[filepath].functions = {}
459                 for f in class_iterator(blocks, "function")() do
460                         if f and f.name then
461                                 table.insert(doc.files[filepath].functions, f.name)
462                                 doc.files[filepath].functions[f.name] = f
463                         end
464                 end
465
466                 -- make tables table
467                 doc.files[filepath].tables = {}
468                 for t in class_iterator(blocks, "table")() do
469                         if t and t.name then
470                                 table.insert(doc.files[filepath].tables, t.name)
471                                 doc.files[filepath].tables[t.name] = t
472                         end
473                 end
474         end
475
476         return doc
477 end
478
479 -------------------------------------------------------------------------------
480 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the
481 -- function that does the actual parsing
482 -- @param filepath full path of the file to parse
483 -- @param doc table with documentation
484 -- @return table with documentation
485 -- @see parse_file
486
487 function file (filepath, doc)
488         local patterns = { "%.lua$", "%.luadoc$" }
489         local valid = table.foreachi(patterns, function (_, pattern)
490                 if string.find(filepath, pattern) ~= nil then
491                         return true
492                 end
493         end)
494
495         if valid then
496                 logger:info(string.format("processing file `%s'", filepath))
497                 doc = parse_file(filepath, doc)
498         end
499
500         return doc
501 end
502
503 -------------------------------------------------------------------------------
504 -- Recursively iterates through a directory, parsing each file
505 -- @param path directory to search
506 -- @param doc table with documentation
507 -- @return table with documentation
508
509 function directory (path, doc)
510         for f in posix.dir(path) do
511                 local fullpath = path .. "/" .. f
512                 local attr = posix.stat(fullpath)
513                 assert(attr, string.format("error stating file `%s'", fullpath))
514
515                 if attr.type == "reg" then
516                         doc = file(fullpath, doc)
517                 elseif attr.type == "dir" and f ~= "." and f ~= ".." then
518                         doc = directory(fullpath, doc)
519                 end
520         end
521         return doc
522 end
523
524 -- Recursively sorts the documentation table
525 local function recsort (tab)
526         table.sort (tab)
527         -- sort list of functions by name alphabetically
528         for f, doc in pairs(tab) do
529                 if doc.functions then
530                         table.sort(doc.functions)
531                 end
532                 if doc.tables then
533                         table.sort(doc.tables)
534                 end
535         end
536 end
537
538 -------------------------------------------------------------------------------
539
540 function start (files, doc)
541         assert(files, "file list not specified")
542
543         -- Create an empty document, or use the given one
544         doc = doc or {
545                 files = {},
546                 modules = {},
547         }
548         assert(doc.files, "undefined `files' field")
549         assert(doc.modules, "undefined `modules' field")
550
551         table.foreachi(files, function (_, path)
552                 local attr = posix.stat(path)
553                 assert(attr, string.format("error stating path `%s'", path))
554
555                 if attr.type == "reg" then
556                         doc = file(path, doc)
557                 elseif attr.type == "dir" then
558                         doc = directory(path, doc)
559                 end
560         end)
561
562         -- order arrays alphabetically
563         recsort(doc.files)
564         recsort(doc.modules)
565
566         return doc
567 end