Drop support for luaposix and bitlib (obsoleted by nixio)
[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