* luci/contrib: added support for multiple modules per file in luadoc
[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 "posix"
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 -------------------------------------------------------------------------------
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
106
107 local function parse_summary (description)
108 -- summary is never nil...
109 description = description or ""
110
111 -- append an " " at the end to make the pattern work in all cases
112 description = description.." "
113
114 -- read until the first period followed by a space or tab
115 local summary = string.match(description, "(.-%.)[%s\t]")
116
117 -- if pattern did not find the first sentence, summary is the whole description
118 summary = summary or description
119
120 return summary
121 end
122
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
130
131 local function parse_code (f, line, modulename)
132 local code = {}
133 while line ~= nil do
134 if string.find(line, "^[\t ]*%-%-%-") then
135 -- reached another luadoc block, end this parsing
136 return line, code, modulename
137 else
138 -- look for a module definition
139 modulename = check_module(line, modulename)
140
141 table.insert(code, line)
142 line = f:read()
143 end
144 end
145 -- reached end of file
146 return line, code, modulename
147 end
148
149 -------------------------------------------------------------------------------
150 -- Parses the information inside a block comment
151 -- @param block block with comment field
152 -- @return block parameter
153
154 local function parse_comment (block, first_line, modulename)
155
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
165 return
166 end
167 return line
168 end
169 end)
170
171 -- parse first line of code
172 if code ~= nil then
173 local func_info = check_function(code)
174 local module_name = check_module(code)
175 if func_info then
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
183 block.param = {}
184 else
185 block.param = {}
186 end
187 else
188 -- TODO: comment without any code. Does this means we are dealing
189 -- with a file comment?
190 end
191
192 -- parse @ tags
193 local currenttag = "description"
194 local currenttext
195
196 table.foreachi(block.comment, function (_, line)
197 line = util.trim_comment(line)
198
199 local r, _, tag, text = string.find(line, "@([_%w%.]+)%s+(.*)")
200 if r ~= nil then
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)
204
205 currenttag = tag
206 currenttext = text
207 else
208 currenttext = util.concat(currenttext, line)
209 assert(string.sub(currenttext, 1, 1) ~= " ", string.format("`%s', `%s'", currenttext, line))
210 end
211 end)
212 tags.handle(currenttag, block, currenttext)
213
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))
217
218 if block.name and block.class == "module" then
219 modulename = block.name
220 end
221
222 return block, modulename
223 end
224
225 -------------------------------------------------------------------------------
226 -- Parses a block of comment, started with ---. Read until the next block of
227 -- comment.
228 -- @param f file handle
229 -- @param line being parsed
230 -- @param modulename module already found, if any
231 -- @return line
232 -- @return block parsed
233 -- @return modulename if found
234
235 local function parse_block (f, line, modulename, first)
236 local block = {
237 comment = {},
238 code = {},
239 }
240
241 while line ~= nil do
242 if string.find(line, "^[\t ]*%-%-") == nil then
243 -- reached end of comment, read the code below it
244 -- TODO: allow empty lines
245 line, block.code, modulename = parse_code(f, line, modulename)
246
247 -- parse information in block comment
248 block, modulename = parse_comment(block, first, modulename)
249
250 return line, block, modulename
251 else
252 table.insert(block.comment, line)
253 line = f:read()
254 end
255 end
256 -- reached end of file
257
258 -- parse information in block comment
259 block, modulename = parse_comment(block, first, modulename)
260
261 return line, block, modulename
262 end
263
264 -------------------------------------------------------------------------------
265 -- Parses a file documented following luadoc format.
266 -- @param filepath full path of file to parse
267 -- @param doc table with documentation
268 -- @return table with documentation
269
270 function parse_file (filepath, doc, handle, prev_line, prev_block, prev_modname)
271 local blocks = { prev_block }
272 local modulename = prev_modname
273
274 -- read each line
275 local f = handle or io.open(filepath, "r")
276 local i = 1
277 local line = prev_line or f:read()
278 local first = true
279 while line ~= nil do
280
281 if string.find(line, "^[\t ]*%-%-%-") then
282 -- reached a luadoc block
283 local block, newmodname
284 line, block, newmodname = parse_block(f, line, modulename, first)
285
286 if modulename and newmodname and newmodname ~= modulename then
287 doc = parse_file( nil, doc, f, line, block, newmodname )
288 else
289 table.insert(blocks, block)
290 modulename = newmodname
291 end
292 else
293 -- look for a module definition
294 local newmodname = check_module(line, modulename)
295
296 if modulename and newmodname and newmodname ~= modulename then
297 parse_file( nil, doc, f )
298 else
299 modulename = newmodname
300 end
301
302 -- TODO: keep beginning of file somewhere
303
304 line = f:read()
305 end
306 first = false
307 i = i + 1
308 end
309
310 if not handle then
311 f:close()
312 end
313
314 if filepath then
315 -- store blocks in file hierarchy
316 assert(doc.files[filepath] == nil, string.format("doc for file `%s' already defined", filepath))
317 table.insert(doc.files, filepath)
318 doc.files[filepath] = {
319 type = "file",
320 name = filepath,
321 doc = blocks,
322 -- functions = class_iterator(blocks, "function"),
323 -- tables = class_iterator(blocks, "table"),
324 }
325 --
326 local first = doc.files[filepath].doc[1]
327 if first and modulename then
328 doc.files[filepath].author = first.author
329 doc.files[filepath].copyright = first.copyright
330 doc.files[filepath].description = first.description
331 doc.files[filepath].release = first.release
332 doc.files[filepath].summary = first.summary
333 end
334 end
335
336 -- if module definition is found, store in module hierarchy
337 if modulename ~= nil then
338 if modulename == "..." then
339 assert( filepath, "Can't determine name for virtual module from filepatch" )
340 modulename = string.gsub (filepath, "%.lua$", "")
341 modulename = string.gsub (modulename, "/", ".")
342 end
343 if doc.modules[modulename] ~= nil then
344 -- module is already defined, just add the blocks
345 table.foreachi(blocks, function (_, v)
346 table.insert(doc.modules[modulename].doc, v)
347 end)
348 else
349 -- TODO: put this in a different module
350 table.insert(doc.modules, modulename)
351 doc.modules[modulename] = {
352 type = "module",
353 name = modulename,
354 doc = blocks,
355 -- functions = class_iterator(blocks, "function"),
356 -- tables = class_iterator(blocks, "table"),
357 author = first and first.author,
358 copyright = first and first.copyright,
359 description = "",
360 release = first and first.release,
361 summary = "",
362 }
363
364 -- find module description
365 for m in class_iterator(blocks, "module")() do
366 doc.modules[modulename].description = util.concat(
367 doc.modules[modulename].description,
368 m.description)
369 doc.modules[modulename].summary = util.concat(
370 doc.modules[modulename].summary,
371 m.summary)
372 if m.author then
373 doc.modules[modulename].author = m.author
374 end
375 if m.copyright then
376 doc.modules[modulename].copyright = m.copyright
377 end
378 if m.release then
379 doc.modules[modulename].release = m.release
380 end
381 if m.name then
382 doc.modules[modulename].name = m.name
383 end
384 end
385 doc.modules[modulename].description = doc.modules[modulename].description or (first and first.description) or ""
386 doc.modules[modulename].summary = doc.modules[modulename].summary or (first and first.summary) or ""
387 end
388
389 -- make functions table
390 doc.modules[modulename].functions = {}
391 for f in class_iterator(blocks, "function")() do
392 if f and f.name then
393 table.insert(doc.modules[modulename].functions, f.name)
394 doc.modules[modulename].functions[f.name] = f
395 end
396 end
397
398 -- make tables table
399 doc.modules[modulename].tables = {}
400 for t in class_iterator(blocks, "table")() do
401 if t and t.name then
402 table.insert(doc.modules[modulename].tables, t.name)
403 doc.modules[modulename].tables[t.name] = t
404 end
405 end
406 end
407
408 if filepath then
409 -- make functions table
410 doc.files[filepath].functions = {}
411 for f in class_iterator(blocks, "function")() do
412 if f and f.name then
413 table.insert(doc.files[filepath].functions, f.name)
414 doc.files[filepath].functions[f.name] = f
415 end
416 end
417
418 -- make tables table
419 doc.files[filepath].tables = {}
420 for t in class_iterator(blocks, "table")() do
421 if t and t.name then
422 table.insert(doc.files[filepath].tables, t.name)
423 doc.files[filepath].tables[t.name] = t
424 end
425 end
426 end
427
428 return doc
429 end
430
431 -------------------------------------------------------------------------------
432 -- Checks if the file is terminated by ".lua" or ".luadoc" and calls the
433 -- function that does the actual parsing
434 -- @param filepath full path of the file to parse
435 -- @param doc table with documentation
436 -- @return table with documentation
437 -- @see parse_file
438
439 function file (filepath, doc)
440 local patterns = { "%.lua$", "%.luadoc$" }
441 local valid = table.foreachi(patterns, function (_, pattern)
442 if string.find(filepath, pattern) ~= nil then
443 return true
444 end
445 end)
446
447 if valid then
448 logger:info(string.format("processing file `%s'", filepath))
449 doc = parse_file(filepath, doc)
450 end
451
452 return doc
453 end
454
455 -------------------------------------------------------------------------------
456 -- Recursively iterates through a directory, parsing each file
457 -- @param path directory to search
458 -- @param doc table with documentation
459 -- @return table with documentation
460
461 function directory (path, doc)
462 for f in posix.files(path) do
463 local fullpath = path .. "/" .. f
464 local attr = posix.stat(fullpath)
465 assert(attr, string.format("error stating file `%s'", fullpath))
466
467 if attr.type == "regular" then
468 doc = file(fullpath, doc)
469 elseif attr.type == "directory" and f ~= "." and f ~= ".." then
470 doc = directory(fullpath, doc)
471 end
472 end
473 return doc
474 end
475
476 -- Recursively sorts the documentation table
477 local function recsort (tab)
478 table.sort (tab)
479 -- sort list of functions by name alphabetically
480 for f, doc in pairs(tab) do
481 if doc.functions then
482 table.sort(doc.functions)
483 end
484 if doc.tables then
485 table.sort(doc.tables)
486 end
487 end
488 end
489
490 -------------------------------------------------------------------------------
491
492 function start (files, doc)
493 assert(files, "file list not specified")
494
495 -- Create an empty document, or use the given one
496 doc = doc or {
497 files = {},
498 modules = {},
499 }
500 assert(doc.files, "undefined `files' field")
501 assert(doc.modules, "undefined `modules' field")
502
503 table.foreachi(files, function (_, path)
504 local attr = posix.stat(path)
505 assert(attr, string.format("error stating path `%s'", path))
506
507 if attr.type == "regular" then
508 doc = file(path, doc)
509 elseif attr.type == "directory" then
510 doc = directory(path, doc)
511 end
512 end)
513
514 -- order arrays alphabetically
515 recsort(doc.files)
516 recsort(doc.modules)
517
518 return doc
519 end