luci-app-ocserv: uclibc's crypt() doesn't support sha2crypt
[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 "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, 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 block = {
274                 comment = {},
275                 code = {},
276         }
277
278         while line ~= nil do
279                 if string.find(line, "^[\t ]*%-%-") == nil then
280                         -- reached end of comment, read the code below it
281                         -- TODO: allow empty lines
282                         line, block.code, modulename = parse_code(f, line, modulename)
283
284                         -- parse information in block comment
285                         block, modulename = parse_comment(block, first, modulename)
286
287                         return line, block, modulename
288                 else
289                         table.insert(block.comment, line)
290                         line = f:read()
291                 end
292         end
293         -- reached end of file
294
295         -- parse information in block comment
296         block, modulename = parse_comment(block, first, modulename)
297
298         return line, block, modulename
299 end
300
301 -------------------------------------------------------------------------------
302 -- Parses a file documented following luadoc format.
303 -- @param filepath full path of file to parse
304 -- @param doc table with documentation
305 -- @return table with documentation
306
307 function parse_file (filepath, doc, handle, prev_line, prev_block, prev_modname)
308         local blocks = { prev_block }
309         local modulename = prev_modname
310
311         -- read each line
312         local f = handle or io.open(filepath, "r")
313         local i = 1
314         local line = prev_line or f:read()
315         local first = true
316         while line ~= nil do
317
318                 if string.find(line, "^[\t ]*%-%-%-") then
319                         -- reached a luadoc block
320                         local block, newmodname
321                         line, block, newmodname = parse_block(f, line, modulename, first)
322
323                         if modulename and newmodname and newmodname ~= modulename then
324                                 doc = parse_file( nil, doc, f, line, block, newmodname )
325                         else
326                                 table.insert(blocks, block)
327                                 modulename = newmodname
328                         end
329                 else
330                         -- look for a module definition
331                         local newmodname = check_module(line, modulename)
332
333                         if modulename and newmodname and newmodname ~= modulename then
334                                 parse_file( nil, doc, f )
335                         else
336                                 modulename = newmodname
337                         end
338
339                         -- TODO: keep beginning of file somewhere
340
341                         line = f:read()
342                 end
343                 first = false
344                 i = i + 1
345         end
346
347         if not handle then
348                 f:close()
349         end
350
351         if filepath then
352                 -- store blocks in file hierarchy
353                 assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
354                 table.insert(doc.files, filepath)
355                 doc.files[filepath] = {
356                         type = "file",
357                         name = filepath,
358                         doc = blocks,
359         --              functions = class_iterator(blocks, "function"),
360         --              tables = class_iterator(blocks, "table"),
361                 }
362         --
363                 local first = doc.files[filepath].doc[1]
364                 if first and modulename then
365                         doc.files[filepath].author = first.author
366                         doc.files[filepath].copyright = first.copyright
367                         doc.files[filepath].description = first.description
368                         doc.files[filepath].release = first.release
369                         doc.files[filepath].summary = first.summary
370                 end
371         end
372
373         -- if module definition is found, store in module hierarchy
374         if modulename ~= nil then
375                 if modulename == "..." then
376                         assert( filepath, "Can't determine name for virtual module from filepatch" )
377                         modulename = string.gsub (filepath, "%.lua$", "")
378                         modulename = string.gsub (modulename, "/", ".")
379                 end
380                 if doc.modules[modulename] ~= nil then
381                         -- module is already defined, just add the blocks
382                         table.foreachi(blocks, function (_, v)
383                                 table.insert(doc.modules[modulename].doc, v)
384                         end)
385                 else
386                         -- TODO: put this in a different module
387                         table.insert(doc.modules, modulename)
388                         doc.modules[modulename] = {
389                                 type = "module",
390                                 name = modulename,
391                                 doc = blocks,
392 --                              functions = class_iterator(blocks, "function"),
393 --                              tables = class_iterator(blocks, "table"),
394                                 author = first and first.author,
395                                 copyright = first and first.copyright,
396                                 description = "",
397                                 release = first and first.release,
398                                 summary = "",
399                         }
400
401                         -- find module description
402                         for m in class_iterator(blocks, "module")() do
403                                 doc.modules[modulename].description = util.concat(
404                                         doc.modules[modulename].description,
405                                         m.description)
406                                 doc.modules[modulename].summary = util.concat(
407                                         doc.modules[modulename].summary,
408                                         m.summary)
409                                 if m.author then
410                                         doc.modules[modulename].author = m.author
411                                 end
412                                 if m.copyright then
413                                         doc.modules[modulename].copyright = m.copyright
414                                 end
415                                 if m.release then
416                                         doc.modules[modulename].release = m.release
417                                 end
418                                 if m.name then
419                                         doc.modules[modulename].name = m.name
420                                 end
421                         end
422                         doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
423                         doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
424                 end
425
426                 -- make functions table
427                 doc.modules[modulename].functions = {}
428                 for f in class_iterator(blocks, "function")() do
429                         if f and f.name then
430                                 table.insert(doc.modules[modulename].functions, f.name)
431                                 doc.modules[modulename].functions[f.name] = f
432                         end
433                 end
434
435                 -- make tables table
436                 doc.modules[modulename].tables = {}
437                 for t in class_iterator(blocks, "table")() do
438                         if t and t.name then
439                                 table.insert(doc.modules[modulename].tables, t.name)
440                                 doc.modules[modulename].tables[t.name] = t
441                         end
442                 end
443
444                 -- make constants table
445                 doc.modules[modulename].constants = {}
446                 for c in class_iterator(blocks, "constant")() do
447                         if c and c.name then
448                                 table.insert(doc.modules[modulename].constants, c.name)
449                                 doc.modules[modulename].constants[c.name] = c
450                         end
451                 end
452         end
453
454         if filepath then
455                 -- make functions table
456                 doc.files[filepath].functions = {}
457                 for f in class_iterator(blocks, "function")() do
458                         if f and f.name then
459                                 table.insert(doc.files[filepath].functions, f.name)
460                                 doc.files[filepath].functions[f.name] = f
461                         end
462                 end
463
464                 -- make tables table
465                 doc.files[filepath].tables = {}
466                 for t in class_iterator(blocks, "table")() do
467                         if t and t.name then
468                                 table.insert(doc.files[filepath].tables, t.name)
469                                 doc.files[filepath].tables[t.name] = t
470                         end
471                 end
472         end
473
474         return doc
475 end
476
477 -------------------------------------------------------------------------------
478 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the
479 -- function that does the actual parsing
480 -- @param filepath full path of the file to parse
481 -- @param doc table with documentation
482 -- @return table with documentation
483 -- @see parse_file
484
485 function file (filepath, doc)
486         local patterns = { "%.lua$", "%.luadoc$" }
487         local valid = table.foreachi(patterns, function (_, pattern)
488                 if string.find(filepath, pattern) ~= nil then
489                         return true
490                 end
491         end)
492
493         if valid then
494                 logger:info(string.format("processing file `%s'", filepath))
495                 doc = parse_file(filepath, doc)
496         end
497
498         return doc
499 end
500
501 -------------------------------------------------------------------------------
502 -- Recursively iterates through a directory, parsing each file
503 -- @param path directory to search
504 -- @param doc table with documentation
505 -- @return table with documentation
506
507 function directory (path, doc)
508         for f in posix.dir(path) do
509                 local fullpath = path .. "/" .. f
510                 local attr = posix.stat(fullpath)
511                 assert(attr, string.format("error stating file `%s'", fullpath))
512
513                 if attr.type == "reg" then
514                         doc = file(fullpath, doc)
515                 elseif attr.type == "dir" and f ~= "." and f ~= ".." then
516                         doc = directory(fullpath, doc)
517                 end
518         end
519         return doc
520 end
521
522 -- Recursively sorts the documentation table
523 local function recsort (tab)
524         table.sort (tab)
525         -- sort list of functions by name alphabetically
526         for f, doc in pairs(tab) do
527                 if doc.functions then
528                         table.sort(doc.functions)
529                 end
530                 if doc.tables then
531                         table.sort(doc.tables)
532                 end
533         end
534 end
535
536 -------------------------------------------------------------------------------
537
538 function start (files, doc)
539         assert(files, "file list not specified")
540
541         -- Create an empty document, or use the given one
542         doc = doc or {
543                 files = {},
544                 modules = {},
545         }
546         assert(doc.files, "undefined `files' field")
547         assert(doc.modules, "undefined `modules' field")
548
549         table.foreachi(files, function (_, path)
550                 local attr = posix.stat(path)
551                 assert(attr, string.format("error stating path `%s'", path))
552
553                 if attr.type == "reg" then
554                         doc = file(path, doc)
555                 elseif attr.type == "dir" then
556                         doc = directory(path, doc)
557                 end
558         end)
559
560         -- order arrays alphabetically
561         recsort(doc.files)
562         recsort(doc.modules)
563
564         return doc
565 end