diff options
Diffstat (limited to 'modules/lua/tadoc.lua')
-rw-r--r-- | modules/lua/tadoc.lua | 245 |
1 files changed, 245 insertions, 0 deletions
diff --git a/modules/lua/tadoc.lua b/modules/lua/tadoc.lua new file mode 100644 index 00000000..70c0133b --- /dev/null +++ b/modules/lua/tadoc.lua @@ -0,0 +1,245 @@ +-- Copyright 2007-2014 Mitchell mitchell.att.foicica.com. See LICENSE. + +-- Textadept autocompletions and API documentation doclet for LuaDoc. +-- This module is used by LuaDoc to create Lua autocompletion and API +-- documentation files that Textadept can read. +-- To preserve formatting, the included *luadoc.patch* file must be applied to +-- your instance of LuaDoc. It will not affect the look of HTML web pages, only +-- the look of plain-text output. +-- @usage luadoc -d [output_path] -doclet path/to/tadoc [file(s)] +local M = {} + +local CTAGS_FMT = '%s\t_\t0;"\t%s\t%s' +local string_format = string.format + +-- Writes a ctag. +-- @param file The file to write to. +-- @param name The name of the tag. +-- @param k The kind of ctag. The Lua module recognizes 4 kinds: m Module, f +-- Function, t Table, and F Field. +-- @param ext_fields The ext_fields for the ctag. +local function write_tag(file, name, k, ext_fields) + if ext_fields == 'class:_G' then ext_fields = '' end + file[#file + 1] = string_format(CTAGS_FMT, name, k, ext_fields) +end + +-- Sanitizes Markdown from the given documentation string by stripping links and +-- replacing HTML entities. +-- @param s String to sanitize Markdown from. +-- @return string +local function sanitize_markdown(s) + return s:gsub('%[([^%]\r\n]+)%]%b[]', '%1') -- [foo][] + :gsub('%[([^%]\r\n]+)%]%b()', '%1') -- [foo](bar) + :gsub('\r?\n\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar + :gsub('\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar + :gsub('&([%a]+);', {quot = '"', apos = "'"}) +end + +-- Writes a function or field apidoc. +-- @param file The file to write to. +-- @param m The LuaDoc module object. +-- @param b The LuaDoc block object. +local function write_apidoc(file, m, b) + -- Function or field name. + local name = b.name + if not name:find('[%.:]') then name = m.name..'.'..name end + -- Block documentation for the function or field. + local doc = {} + -- Function arguments or field type. + local class = b.class + local header = name + if class == 'function' then + header = header..(b.param and '('..table.concat(b.param, ', ')..')' or '') + elseif class == 'field' and b.description:find('^%s*%b()') then + header = header..' '..b.description:match('^%s*(%b())') + elseif class == 'module' or class == 'table' then + header = header..' ('..class..')' + end + doc[#doc + 1] = header + -- Function or field description. + local description = b.description + if class == 'module' then + -- Modules usually have additional Markdown documentation so just grab the + -- documentation before a Markdown header. + description = description:match('^(.-)[\r\n]+#') or description + elseif class == 'field' then + -- Type information is already in the header; discard it in the description. + description = description:match('^%s*%b()[\t ]*[\r\n]*(.+)$') or description + -- Strip consistent leading whitespace. + local indent + indent, description = description:match('^(%s*)(.*)$') + if indent ~= '' then description = description:gsub('\n'..indent, '\n') end + end + doc[#doc + 1] = sanitize_markdown(description) + -- Function parameters (@param). + if class == 'function' and b.param then + for _, p in ipairs(b.param) do + if b.param[p] and #b.param[p] > 0 then + doc[#doc + 1] = '@param '..p..' '..sanitize_markdown(b.param[p]) + end + end + end + -- Function usage (@usage). + if class == 'function' and b.usage then + if type(b.usage) == 'string' then + doc[#doc + 1] = '@usage '..b.usage + else + for _, u in ipairs(b.usage) do doc[#doc + 1] = '@usage '..u end + end + end + -- Function returns (@return). + if class == 'function' and b.ret then + if type(b.ret) == 'string' then + doc[#doc + 1] = '@return '..b.ret + else + for _, u in ipairs(b.ret) do doc[#doc + 1] = '@return '..u end + end + end + -- See also (@see). + if b.see then + if type(b.see) == 'string' then + doc[#doc + 1] = '@see '..b.see + else + for _, s in ipairs(b.see) do doc[#doc + 1] = '@see '..s end + end + end + -- Format the block documentation. + doc = table.concat(doc, '\n'):gsub('\n', '\\n') + file[#file + 1] = name:match('[^%.:]+$')..' '..doc +end + +-- Called by LuaDoc to process a doc object. +-- @param doc The LuaDoc doc object. +function M.start(doc) +-- require('luarocks.require') +-- local profiler = require('profiler') +-- profiler.start() + + local modules, files = doc.modules, doc.files + + -- Create a map of file names to doc objects so their module names can be + -- determined. + local filedocs = {} + for i = 1, #files do filedocs[files[i]] = files[files[i]].doc end + + -- Add a module's fields to its LuaDoc. + for i = 1, #files do + local module_doc = filedocs[files[i]][1] + if module_doc and module_doc.class == 'module' then + modules[module_doc.name].fields = module_doc.field + elseif module_doc then + print('[WARN] '..files[i]..' has no module declaration') + end + end + + -- Convert module functions in the Lua luadoc into LuaDoc modules. + local lua_luadoc = files['../modules/lua/lua.luadoc'] + if lua_luadoc then + for i = 1, #lua_luadoc.functions do + local f = lua_luadoc.functions[lua_luadoc.functions[i]] + local module_name = f.name:match('^([^%.:]+)[%.:]') or '_G' + if not modules[module_name] then + modules[#modules + 1] = module_name + modules[module_name] = {name = module_name, functions = {}} + -- For functions like file:read(), 'file' is not a module; fake it. + if f.name:find(':') then modules[module_name].fake = true end + end + local module = modules[module_name] + module.description = 'Lua '..module.name..' module.' + module.functions[#module.functions + 1] = f.name + module.functions[f.name] = f + end + for i = 1, #lua_luadoc.tables do + local t = lua_luadoc.tables[lua_luadoc.tables[i]] + local module = modules[t.name or '_G'] + if not module.fields then module.fields = {} end + local fields = module.fields + for k, v in pairs(t.field) do + if not tonumber(k) then fields[#fields + 1], fields[k] = k, v end + end + end + end + + -- Process LuaDoc and write the tags and api files. + local ctags, apidoc = {}, {} + for i = 1, #modules do + local m = modules[modules[i]] + if not m.fake then + -- Tag and document the module. + write_tag(ctags, m.name, 'm', '') + if m.name:find('%.') then + -- Tag the last part of the module as a table of the first part. + local parent, child = m.name:match('^(.-)%.([^%.]+)$') + write_tag(ctags, child, 'm', 'class:'..parent) + end + m.class = 'module' + write_apidoc(apidoc, {name = '_G'}, m) + end + -- Tag and document the functions. + for i = 1, #m.functions do + local module_name, name = m.functions[i]:match('^(.-)[%.:]?([^.:]+)$') + if module_name == '' then module_name = m.name end + write_tag(ctags, name, 'f', 'class:'..module_name) + write_apidoc(apidoc, m, m.functions[m.functions[i]]) + end + if m.tables then + -- Document the tables. + for i = 1, #m.tables do + local table_name, table = m.tables[i], m.tables[m.tables[i]] + local module_name = m.name + if table_name:find('^_G%.') then + module_name, table_name = table_name:match('^_G%.(.-)%.?([^%.]+)$') + if not module_name then + print('[ERROR] Cannot determine module name for '..table.name) + elseif module_name == '' then + module_name = '_G' -- _G.keys or _G.snippets + end + end + write_tag(ctags, table_name, 't', 'class:'..module_name) + write_apidoc(apidoc, m, table) + if table.field then + -- Tag and document the table's fields. + table_name = module_name..'.'..table_name + for j = 1, #table.field do + write_tag(ctags, table.field[j], 'F', 'class:'..table_name) + write_apidoc(apidoc, {name = table_name}, { + name = table.field[j], + description = table.field[table.field[j]], + class = 'table' + }) + end + end + end + end + if m.fields then + -- Tag and document the fields. + for i = 1, #m.fields do + local field_name, field = m.fields[i], m.fields[m.fields[i]] + local module_name = m.name + if field_name:find('^_G%.') then + module_name, field_name = field_name:match('^_G%.(.-)%.?([^%.]+)$') + if not module_name then + print('[ERROR] Cannot determine module name for '..field.name) + end + end + write_tag(ctags, field_name, 'F', 'class:'..module_name) + write_apidoc(apidoc, {name = field_name}, { + name = module_name..'.'..field_name, description = field, + class = 'field' + }) + end + end + end + table.sort(ctags) + table.sort(apidoc) + local f = io.open(M.options.output_dir..'/tags', 'wb') + f:write(table.concat(ctags, '\n')) + f:close() + f = io.open(M.options.output_dir..'api', 'wb') + f:write(table.concat(apidoc, '\n')) + f:close() + +-- profiler.stop() +end + +return M |