334f4a24dd00cd91a65c3ce8407447d1e2a19b07
[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)
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 return block
219 end
220
221 -------------------------------------------------------------------------------
222 -- Parses a block of comment, started with ---. Read until the next block of
223 -- comment.
224 -- @param f file handle
225 -- @param line being parsed
226 -- @param modulename module already found, if any
227 -- @return line
228 -- @return block parsed
229 -- @return modulename if found
230
231 local function parse_block (f, line, modulename, first)
232 local block = {
233 comment = {},
234 code = {},
235 }
236
237 while line ~= nil do
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)
242
243 -- parse information in block comment
244 block = parse_comment(block, first)
245
246 return line, block, modulename
247 else
248 table.insert(block.comment, line)
249 line = f:read()
250 end
251 end
252 -- reached end of file
253
254 -- parse information in block comment
255 block = parse_comment(block, first)
256
257 return line, block, modulename
258 end
259
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
265
266 function parse_file (filepath, doc)
267 local blocks = {}
268 local modulename = nil
269
270 -- read each line
271 local f = io.open(filepath, "r")
272 local i = 1
273 local line = f:read()
274 local first = true
275 while line ~= nil do
276 if string.find(line, "^[\t ]*%-%-%-") then
277 -- reached a luadoc block
278 local block
279 line, block, modulename = parse_block(f, line, modulename, first)
280 table.insert(blocks, block)
281 else
282 -- look for a module definition
283 modulename = check_module(line, modulename)
284
285 -- TODO: keep beginning of file somewhere
286
287 line = f:read()
288 end
289 first = false
290 i = i + 1
291 end
292 f:close()
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] = {
297 type = "file",
298 name = filepath,
299 doc = blocks,
300 -- functions = class_iterator(blocks, "function"),
301 -- tables = class_iterator(blocks, "table"),
302 }
303 --
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
311 end
312
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, "/", ".")
318 end
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)
323 end)
324 else
325 -- TODO: put this in a different module
326 table.insert(doc.modules, modulename)
327 doc.modules[modulename] = {
328 type = "module",
329 name = modulename,
330 doc = blocks,
331 -- functions = class_iterator(blocks, "function"),
332 -- tables = class_iterator(blocks, "table"),
333 author = first and first.author,
334 copyright = first and first.copyright,
335 description = "",
336 release = first and first.release,
337 summary = "",
338 }
339
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,
344 m.description)
345 doc.modules[modulename].summary = util.concat(
346 doc.modules[modulename].summary,
347 m.summary)
348 if m.author then
349 doc.modules[modulename].author = m.author
350 end
351 if m.copyright then
352 doc.modules[modulename].copyright = m.copyright
353 end
354 if m.release then
355 doc.modules[modulename].release = m.release
356 end
357 if m.name then
358 doc.modules[modulename].name = m.name
359 end
360 end
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 ""
363 end
364
365 -- make functions table
366 doc.modules[modulename].functions = {}
367 for f in class_iterator(blocks, "function")() do
368 if f and f.name then
369 table.insert(doc.modules[modulename].functions, f.name)
370 doc.modules[modulename].functions[f.name] = f
371 end
372 end
373
374 -- make tables table
375 doc.modules[modulename].tables = {}
376 for t in class_iterator(blocks, "table")() do
377 if t and t.name then
378 table.insert(doc.modules[modulename].tables, t.name)
379 doc.modules[modulename].tables[t.name] = t
380 end
381 end
382 end
383
384 -- make functions table
385 doc.files[filepath].functions = {}
386 for f in class_iterator(blocks, "function")() do
387 if f and f.name then
388 table.insert(doc.files[filepath].functions, f.name)
389 doc.files[filepath].functions[f.name] = f
390 end
391 end
392
393 -- make tables table
394 doc.files[filepath].tables = {}
395 for t in class_iterator(blocks, "table")() do
396 if t and t.name then
397 table.insert(doc.files[filepath].tables, t.name)
398 doc.files[filepath].tables[t.name] = t
399 end
400 end
401
402 return doc
403 end
404
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
411 -- @see parse_file
412
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
417 return true
418 end
419 end)
420
421 if valid then
422 logger:info(string.format("processing file `%s'", filepath))
423 doc = parse_file(filepath, doc)
424 end
425
426 return doc
427 end
428
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
434
435 function directory (path, doc)
436 for f in posix.files(path) do
437 local fullpath = path .. "/" .. f
438 local attr = posix.stat(fullpath)
439 assert(attr, string.format("error stating file `%s'", fullpath))
440
441 if attr.type == "regular" then
442 doc = file(fullpath, doc)
443 elseif attr.type == "directory" and f ~= "." and f ~= ".." then
444 doc = directory(fullpath, doc)
445 end
446 end
447 return doc
448 end
449
450 -- Recursively sorts the documentation table
451 local function recsort (tab)
452 table.sort (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)
457 end
458 if doc.tables then
459 table.sort(doc.tables)
460 end
461 end
462 end
463
464 -------------------------------------------------------------------------------
465
466 function start (files, doc)
467 assert(files, "file list not specified")
468
469 -- Create an empty document, or use the given one
470 doc = doc or {
471 files = {},
472 modules = {},
473 }
474 assert(doc.files, "undefined `files' field")
475 assert(doc.modules, "undefined `modules' field")
476
477 table.foreachi(files, function (_, path)
478 local attr = posix.stat(path)
479 assert(attr, string.format("error stating path `%s'", path))
480
481 if attr.type == "regular" then
482 doc = file(path, doc)
483 elseif attr.type == "directory" then
484 doc = directory(path, doc)
485 end
486 end)
487
488 -- order arrays alphabetically
489 recsort(doc.files)
490 recsort(doc.modules)
491
492 return doc
493 end