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 lfs = require "lfs"
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 -------------------------------------------------------------------------------
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
107 local function parse_summary (description)
108 -- summary is never nil...
109 description = description or ""
111 -- append an " " at the end to make the pattern work in all cases
112 description = description.." "
114 -- read until the first period followed by a space or tab
115 local summary = string.match(description, "(.-%.)[%s\t]")
117 -- if pattern did not find the first sentence, summary is the whole description
118 summary = summary or description
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
131 local function parse_code (f, line, modulename)
134 if string.find(line, "^[\t ]*%-%-%-") then
135 -- reached another luadoc block, end this parsing
136 return line, code, modulename
138 -- look for a module definition
139 modulename = check_module(line, modulename)
141 table.insert(code, line)
145 -- reached end of file
146 return line, code, modulename
149 -------------------------------------------------------------------------------
150 -- Parses the information inside a block comment
151 -- @param block block with comment field
152 -- @return block parameter
154 local function parse_comment (block, first_line)
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
171 -- parse first line of code
173 local func_info = check_function(code)
174 local module_name = check_module(code)
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
188 -- TODO: comment without any code. Does this means we are dealing
189 -- with a file comment?
193 local currenttag = "description"
196 table.foreachi(block.comment, function (_, line)
197 line = util.trim_comment(line)
199 local r, _, tag, text = string.find(line, "@([_%w%.]+)%s+(.*)")
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)
208 currenttext = util.concat(currenttext, line)
209 assert(string.sub(currenttext, 1, 1) ~= " ", string.format("`%s', `%s'", currenttext, line))
212 tags.handle(currenttag, block, currenttext)
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))
221 -------------------------------------------------------------------------------
222 -- Parses a block of comment, started with ---. Read until the next block of
224 -- @param f file handle
225 -- @param line being parsed
226 -- @param modulename module already found, if any
228 -- @return block parsed
229 -- @return modulename if found
231 local function parse_block (f, line, modulename, first)
238 if string.find(line, "^[\t ]*%-%-") == nil then
239 -- reached end of comment, read the code below it
240 -- TODO: allow empty lines
241 line, block.code, modulename = parse_code(f, line, modulename)
243 -- parse information in block comment
244 block = parse_comment(block, first)
246 return line, block, modulename
248 table.insert(block.comment, line)
252 -- reached end of file
254 -- parse information in block comment
255 block = parse_comment(block, first)
257 return line, block, modulename
260 -------------------------------------------------------------------------------
261 -- Parses a file documented following luadoc format.
262 -- @param filepath full path of file to parse
263 -- @param doc table with documentation
264 -- @return table with documentation
266 function parse_file (filepath, doc)
268 local modulename = nil
271 local f = io.open(filepath, "r")
273 local line = f:read()
276 if string.find(line, "^[\t ]*%-%-%-") then
277 -- reached a luadoc block
279 line, block, modulename = parse_block(f, line, modulename, first)
280 table.insert(blocks, block)
282 -- look for a module definition
283 modulename = check_module(line, modulename)
285 -- TODO: keep beginning of file somewhere
293 -- store blocks in file hierarchy
294 assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
295 table.insert(doc.files, filepath)
296 doc.files[filepath] = {
300 -- functions = class_iterator(blocks, "function"),
301 -- tables = class_iterator(blocks, "table"),
304 local first = doc.files[filepath].doc[1]
305 if first and modulename then
306 doc.files[filepath].author = first.author
307 doc.files[filepath].copyright = first.copyright
308 doc.files[filepath].description = first.description
309 doc.files[filepath].release = first.release
310 doc.files[filepath].summary = first.summary
313 -- if module definition is found, store in module hierarchy
314 if modulename ~= nil then
315 if modulename == "..." then
316 modulename = string.gsub (filepath, "%.lua$", "")
317 modulename = string.gsub (modulename, "/", ".")
319 if doc.modules[modulename] ~= nil then
320 -- module is already defined, just add the blocks
321 table.foreachi(blocks, function (_, v)
322 table.insert(doc.modules[modulename].doc, v)
325 -- TODO: put this in a different module
326 table.insert(doc.modules, modulename)
327 doc.modules[modulename] = {
331 -- functions = class_iterator(blocks, "function"),
332 -- tables = class_iterator(blocks, "table"),
333 author = first and first.author,
334 copyright = first and first.copyright,
336 release = first and first.release,
340 -- find module description
341 for m in class_iterator(blocks, "module")() do
342 doc.modules[modulename].description = util.concat(
343 doc.modules[modulename].description,
345 doc.modules[modulename].summary = util.concat(
346 doc.modules[modulename].summary,
349 doc.modules[modulename].author = m.author
352 doc.modules[modulename].copyright = m.copyright
355 doc.modules[modulename].release = m.release
358 doc.modules[modulename].name = m.name
361 doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
362 doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
365 -- make functions table
366 doc.modules[modulename].functions = {}
367 for f in class_iterator(blocks, "function")() do
369 table.insert(doc.modules[modulename].functions, f.name)
370 doc.modules[modulename].functions[f.name] = f
375 doc.modules[modulename].tables = {}
376 for t in class_iterator(blocks, "table")() do
378 table.insert(doc.modules[modulename].tables, t.name)
379 doc.modules[modulename].tables[t.name] = t
384 -- make functions table
385 doc.files[filepath].functions = {}
386 for f in class_iterator(blocks, "function")() do
388 table.insert(doc.files[filepath].functions, f.name)
389 doc.files[filepath].functions[f.name] = f
394 doc.files[filepath].tables = {}
395 for t in class_iterator(blocks, "table")() do
397 table.insert(doc.files[filepath].tables, t.name)
398 doc.files[filepath].tables[t.name] = t
405 -------------------------------------------------------------------------------
406 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the
407 -- function that does the actual parsing
408 -- @param filepath full path of the file to parse
409 -- @param doc table with documentation
410 -- @return table with documentation
413 function file (filepath, doc)
414 local patterns = { "%.lua$", "%.luadoc$" }
415 local valid = table.foreachi(patterns, function (_, pattern)
416 if string.find(filepath, pattern) ~= nil then
422 logger:info(string.format("processing file `%s'", filepath))
423 doc = parse_file(filepath, doc)
429 -------------------------------------------------------------------------------
430 -- Recursively iterates through a directory, parsing each file
431 -- @param path directory to search
432 -- @param doc table with documentation
433 -- @return table with documentation
435 function directory (path, doc)
436 for f in lfs.dir(path) do
437 local fullpath = path .. "/" .. f
438 local attr = lfs.attributes(fullpath)
439 assert(attr, string.format("error stating file `%s'", fullpath))
441 if attr.mode == "file" then
442 doc = file(fullpath, doc)
443 elseif attr.mode == "directory" and f ~= "." and f ~= ".." then
444 doc = directory(fullpath, doc)
450 -- Recursively sorts the documentation table
451 local function recsort (tab)
453 -- sort list of functions by name alphabetically
454 for f, doc in pairs(tab) do
455 if doc.functions then
456 table.sort(doc.functions)
459 table.sort(doc.tables)
464 -------------------------------------------------------------------------------
466 function start (files, doc)
467 assert(files, "file list not specified")
469 -- Create an empty document, or use the given one
474 assert(doc.files, "undefined `files' field")
475 assert(doc.modules, "undefined `modules' field")
477 table.foreachi(files, function (_, path)
478 local attr = lfs.attributes(path)
479 assert(attr, string.format("error stating path `%s'", path))
481 if attr.mode == "file" then
482 doc = file(path, doc)
483 elseif attr.mode == "directory" then
484 doc = directory(path, doc)
488 -- order arrays alphabetically