1 -------------------------------------------------------------------------------
2 -- @release $Id: standard.lua,v 1.39 2007/12/21 17:50:48 tomas Exp $
3 -------------------------------------------------------------------------------
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"
14 module 'luadoc.taglet.standard'
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
21 function class_iterator (t, class)
25 while t[i] and t[i].class ~= class do
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.."%)",
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
49 local function check_function (line)
50 line = util.trim(line)
52 local info = table.foreachi(function_patterns, function (_, pattern)
53 local r, _, l, id, param = string.find(line, pattern)
57 private = (l == "local"),
58 param = { } --util.split("%s*,%s*", param),
63 -- TODO: remove these assert's?
65 assert(info.name, "function name undefined")
66 assert(info.param, string.format("undefined parameter list for function `%s'", info.name))
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
79 local function check_module (line, currentmodule)
80 line = util.trim(line)
90 local r, _, modulename = string.find(line, "^module%s*[%s\"'(%[]+([^,\"')%]]+)")
92 -- found module definition
93 logger:debug(string.format("found module `%s'", modulename))
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*=",
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
110 local function check_constant (line)
111 line = util.trim(line)
113 local info = table.foreachi(constant_patterns, function (_, pattern)
114 local r, _, l, id = string.find(line, pattern)
118 private = (l == "local"),
123 -- TODO: remove these assert's?
125 assert(info.name, "constant name undefined")
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
139 local function parse_summary (description)
140 -- summary is never nil...
141 description = description or ""
143 -- append an " " at the end to make the pattern work in all cases
144 description = description.." "
146 -- read until the first period followed by a space or tab
147 local summary = string.match(description, "(.-%.)[%s\t]")
149 -- if pattern did not find the first sentence, summary is the whole description
150 summary = summary or description
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
163 local function parse_code (f, line, modulename)
166 if string.find(line, "^[\t ]*%-%-%-") then
167 -- reached another luadoc block, end this parsing
168 return line, code, modulename
170 -- look for a module definition
171 modulename = check_module(line, modulename)
173 table.insert(code, line)
177 -- reached end of file
178 return line, code, modulename
181 -------------------------------------------------------------------------------
182 -- Parses the information inside a block comment
183 -- @param block block with comment field
184 -- @return block parameter
186 local function parse_comment (block, first_line, modulename)
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
203 -- parse first line of code
205 local func_info = check_function(code)
206 local module_name = check_module(code)
207 local const_info = check_constant(code)
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
225 -- TODO: comment without any code. Does this means we are dealing
226 -- with a file comment?
230 local currenttag = "description"
233 table.foreachi(block.comment, function (_, line)
234 line = util.trim_comment(line)
236 local r, _, tag, text = string.find(line, "@([_%w%.]+)%s+(.*)")
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)
245 currenttext = util.concat(currenttext, "\n" .. line)
246 assert(string.sub(currenttext, 1, 1) ~= " ", string.format("`%s', `%s'", currenttext, line))
249 tags.handle(currenttag, block, currenttext)
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))
255 if block.name and block.class == "module" then
256 modulename = block.name
259 return block, modulename
262 -------------------------------------------------------------------------------
263 -- Parses a block of comment, started with ---. Read until the next block of
265 -- @param f file handle
266 -- @param line being parsed
267 -- @param modulename module already found, if any
269 -- @return block parsed
270 -- @return modulename if found
272 local function parse_block (f, line, modulename, first)
273 local multiline = not not (line and line:match("%[%["))
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)
286 -- parse information in block comment
287 block, modulename = parse_comment(block, first, modulename)
289 return line, block, modulename
291 table.insert(block.comment, line)
295 -- reached end of file
297 -- parse information in block comment
298 block, modulename = parse_comment(block, first, modulename)
300 return line, block, modulename
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
309 function parse_file (filepath, doc, handle, prev_line, prev_block, prev_modname)
310 local blocks = { prev_block }
311 local modulename = prev_modname
314 local f = handle or io.open(filepath, "r")
316 local line = prev_line or f:read()
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)
325 if modulename and newmodname and newmodname ~= modulename then
326 doc = parse_file( nil, doc, f, line, block, newmodname )
328 table.insert(blocks, block)
329 modulename = newmodname
332 -- look for a module definition
333 local newmodname = check_module(line, modulename)
335 if modulename and newmodname and newmodname ~= modulename then
336 parse_file( nil, doc, f )
338 modulename = newmodname
341 -- TODO: keep beginning of file somewhere
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] = {
361 -- functions = class_iterator(blocks, "function"),
362 -- tables = class_iterator(blocks, "table"),
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
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, "/", ".")
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)
388 -- TODO: put this in a different module
389 table.insert(doc.modules, modulename)
390 doc.modules[modulename] = {
394 -- functions = class_iterator(blocks, "function"),
395 -- tables = class_iterator(blocks, "table"),
396 author = first and first.author,
397 copyright = first and first.copyright,
399 release = first and first.release,
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,
408 doc.modules[modulename].summary = util.concat(
409 doc.modules[modulename].summary,
412 doc.modules[modulename].author = m.author
415 doc.modules[modulename].copyright = m.copyright
418 doc.modules[modulename].release = m.release
421 doc.modules[modulename].name = m.name
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 ""
428 -- make functions table
429 doc.modules[modulename].functions = {}
430 for f in class_iterator(blocks, "function")() do
432 table.insert(doc.modules[modulename].functions, f.name)
433 doc.modules[modulename].functions[f.name] = f
438 doc.modules[modulename].tables = {}
439 for t in class_iterator(blocks, "table")() do
441 table.insert(doc.modules[modulename].tables, t.name)
442 doc.modules[modulename].tables[t.name] = t
446 -- make constants table
447 doc.modules[modulename].constants = {}
448 for c in class_iterator(blocks, "constant")() do
450 table.insert(doc.modules[modulename].constants, c.name)
451 doc.modules[modulename].constants[c.name] = c
457 -- make functions table
458 doc.files[filepath].functions = {}
459 for f in class_iterator(blocks, "function")() do
461 table.insert(doc.files[filepath].functions, f.name)
462 doc.files[filepath].functions[f.name] = f
467 doc.files[filepath].tables = {}
468 for t in class_iterator(blocks, "table")() do
470 table.insert(doc.files[filepath].tables, t.name)
471 doc.files[filepath].tables[t.name] = t
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
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
496 logger:info(string.format("processing file `%s'", filepath))
497 doc = parse_file(filepath, doc)
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
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))
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)
524 -- Recursively sorts the documentation table
525 local function recsort (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)
533 table.sort(doc.tables)
538 -------------------------------------------------------------------------------
540 function start (files, doc)
541 assert(files, "file list not specified")
543 -- Create an empty document, or use the given one
548 assert(doc.files, "undefined `files' field")
549 assert(doc.modules, "undefined `modules' field")
551 table.foreachi(files, function (_, path)
552 local attr = posix.stat(path)
553 assert(attr, string.format("error stating path `%s'", path))
555 if attr.type == "reg" then
556 doc = file(path, doc)
557 elseif attr.type == "dir" then
558 doc = directory(path, doc)
562 -- order arrays alphabetically