1-- Copyright 2007-2021 Mitchell. See LICENSE. 2 3-- Textadept autocompletions and API documentation doclet for LuaDoc. 4-- This module is used by LuaDoc to create Lua autocompletion and API 5-- documentation files that Textadept can read. 6-- To preserve formatting, the included *luadoc.patch* file must be applied to 7-- your instance of LuaDoc. It will not affect the look of HTML web pages, only 8-- the look of plain-text output. 9-- Also requires LuaFileSystem (lfs) to be installed. 10-- @usage luadoc -d [output_path] -doclet path/to/tadoc [file(s)] 11local M = {} 12 13local CTAG = '%s\t%s\t/^%s$/;"\t%s\t%s' 14local string_format = string.format 15local lfs = require('lfs') 16 17-- As a special case for Textadept API tags, do not store the local path, but 18-- use a `_HOME` prefix that will be filled in by consumers. Do this by making 19-- use of a custom command line switch: --ta-home="path/to/ta/home". 20local _HOME 21for i = 1, #arg do 22 _HOME = arg[i]:match('^%-%-ta%-home=(.+)$') 23 if _HOME then _HOME = _HOME:gsub('%p', '%%%0') break end 24end 25 26-- Writes a ctag. 27-- @param file The file to write to. 28-- @param name The name of the tag. 29-- @param filename The filename the tag occurs in. 30-- @param code The line of code the tag occurs on. 31-- @param k The kind of ctag. The Lua module recognizes 4 kinds: m Module, f 32-- Function, t Table, and F Field. 33-- @param ext_fields The ext_fields for the ctag. 34local function write_tag(file, name, filename, code, k, ext_fields) 35 if _HOME then filename = filename:gsub(_HOME, '_HOME') end 36 if ext_fields == 'class:_G' then ext_fields = '' end 37 file[#file + 1] = string_format(CTAG, name, filename, code, k, ext_fields) 38end 39 40-- Sanitizes Markdown from the given documentation string by stripping links and 41-- replacing HTML entities. 42-- @param s String to sanitize Markdown from. 43-- @return string 44local function sanitize_markdown(s) 45 return s:gsub('%[([^%]\r\n]+)%]%b[]', '%1') -- [foo][] 46 :gsub('%[([^%]\r\n]+)%]%b()', '%1') -- [foo](bar) 47 :gsub('\r?\n\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar 48 :gsub('\r?\n%[([^%]\r\n]+)%]:[^\r\n]+', '') -- [foo]: bar 49 :gsub('&([%a]+);', {quot = '"', apos = "'"}) 50end 51 52-- Writes a function or field apidoc. 53-- @param file The file to write to. 54-- @param m The LuaDoc module object. 55-- @param b The LuaDoc block object. 56local function write_apidoc(file, m, b) 57 -- Function or field name. 58 local name = b.name 59 if not name:find('[%.:]') then name = string.format('%s.%s', m.name, name) end 60 -- Block documentation for the function or field. 61 local doc = {} 62 -- Function arguments or field type. 63 local class = b.class 64 local header = name 65 if class == 'function' then 66 header = header .. 67 (b.param and string.format('(%s)', table.concat(b.param, ', ')) or '') 68 elseif class == 'field' and b.description:find('^%s*%b()') then 69 header = string.format('%s %s', header, b.description:match('^%s*(%b())')) 70 elseif class == 'module' or class == 'table' then 71 header = string.format('%s (%s)', header, class) 72 end 73 doc[#doc + 1] = header 74 -- Function or field description. 75 local description = b.description 76 if class == 'module' then 77 -- Modules usually have additional Markdown documentation so just grab the 78 -- documentation before a Markdown header. 79 description = description:match('^(.-)[\r\n]+#') or description 80 elseif class == 'field' then 81 -- Type information is already in the header; discard it in the description. 82 description = description:match('^%s*%b()[\t ]*[\r\n]*(.+)$') or description 83 -- Strip consistent leading whitespace. 84 local indent 85 indent, description = description:match('^(%s*)(.*)$') 86 if indent ~= '' then 87 description = description:gsub('\n' .. indent, '\n') 88 end 89 end 90 doc[#doc + 1] = sanitize_markdown(description) 91 -- Function parameters (@param). 92 if class == 'function' and b.param then 93 for _, p in ipairs(b.param) do 94 if b.param[p] and #b.param[p] > 0 then 95 doc[#doc + 1] = string.format( 96 '@param %s %s', p, sanitize_markdown(b.param[p])) 97 end 98 end 99 end 100 -- Function usage (@usage). 101 if class == 'function' and b.usage then 102 if type(b.usage) == 'string' then 103 doc[#doc + 1] = '@usage ' .. b.usage 104 else 105 for _, u in ipairs(b.usage) do doc[#doc + 1] = '@usage ' .. u end 106 end 107 end 108 -- Function returns (@return). 109 if class == 'function' and b.ret then 110 if type(b.ret) == 'string' then 111 doc[#doc + 1] = '@return ' .. b.ret 112 else 113 for _, u in ipairs(b.ret) do doc[#doc + 1] = '@return ' .. u end 114 end 115 end 116 -- See also (@see). 117 if b.see then 118 if type(b.see) == 'string' then 119 doc[#doc + 1] = '@see ' .. b.see 120 else 121 for _, s in ipairs(b.see) do doc[#doc + 1] = '@see ' .. s end 122 end 123 end 124 -- Format the block documentation. 125 doc = table.concat(doc, '\n'):gsub('\\n', '\\\\n'):gsub('\n', '\\n') 126 file[#file + 1] = string.format('%s %s', name:match('[^%.:]+$'), doc) 127end 128 129-- Returns the absolute path of the given relative path. 130-- @param string path String relative path. 131-- @return absolute path 132local function abspath(path) 133 path = string.format('%s/%s', lfs.currentdir(), path) 134 path = path:gsub('%f[^/]%./', '') -- clean up './' 135 while path:find('[^/]+/%.%./') do 136 path = path:gsub('[^/]+/%.%./', '', 1) -- clean up '../' 137 end 138 return path 139end 140 141-- Called by LuaDoc to process a doc object. 142-- @param doc The LuaDoc doc object. 143function M.start(doc) 144-- require('luarocks.require') 145-- local profiler = require('profiler') 146-- profiler.start() 147 148 local modules, files = doc.modules, doc.files 149 150 -- Map doc objects to file names so a module can be mapped to its filename. 151 for _, filename in ipairs(files) do 152 local doc = files[filename].doc 153 files[doc] = abspath(filename) 154 end 155 156 -- Add a module's fields to its LuaDoc. 157 for _, filename in ipairs(files) do 158 local module_doc = files[filename].doc[1] 159 if module_doc and module_doc.class == 'module' and 160 modules[module_doc.name] then 161 modules[module_doc.name].fields = module_doc.field 162 elseif module_doc then 163 print(string.format('[WARN] %s has no module declaration', filename)) 164 end 165 end 166 167 -- Convert module functions in the Lua luadoc into LuaDoc modules. 168 local lua_luadoc = files['../modules/lua/lua.luadoc'] 169 if lua_luadoc and #files == 1 then 170 for _, function_name in ipairs(lua_luadoc.functions) do 171 local func = lua_luadoc.functions[function_name] 172 local module_name = func.name:match('^([^%.:]+)[%.:]') or '_G' 173 if not modules[module_name] then 174 modules[#modules + 1] = module_name 175 modules[module_name] = { 176 name = module_name, functions = {}, doc = {{code = func.code}} 177 } 178 files[modules[module_name].doc] = abspath(files[1]) 179 -- For functions like file:read(), 'file' is not a module; fake it. 180 if func.name:find(':') then modules[module_name].fake = true end 181 end 182 local module = modules[module_name] 183 module.description = string.format('Lua %s module.', module.name) 184 module.functions[#module.functions + 1] = func.name 185 module.functions[func.name] = func 186 end 187 for _, table_name in ipairs(lua_luadoc.tables) do 188 local table = lua_luadoc.tables[table_name] 189 local module = modules[table.name or '_G'] 190 if not module.fields then module.fields = {} end 191 local fields = module.fields 192 for k, v in pairs(table.field) do 193 if not tonumber(k) then fields[#fields + 1], fields[k] = k, v end 194 end 195 end 196 end 197 198 -- Process LuaDoc and write the tags and api files. 199 local ctags, apidoc = {}, {} 200 for _, module_name in ipairs(modules) do 201 local m = modules[module_name] 202 local filename = files[m.doc] 203 if not m.fake then 204 -- Tag and document the module. 205 write_tag(ctags, m.name, filename, m.doc[1].code[1], 'm', '') 206 if m.name:find('%.') then 207 -- Tag the last part of the module as a table of the first part. 208 local parent, child = m.name:match('^(.-)%.([^%.]+)$') 209 write_tag( 210 ctags, child, filename, m.doc[1].code[1], 'm', 'class:' .. parent) 211 end 212 m.class = 'module' 213 write_apidoc(apidoc, {name = '_G'}, m) 214 end 215 -- Tag and document the functions. 216 for _, function_name in ipairs(m.functions) do 217 local module_name, name = function_name:match('^(.-)[%.:]?([^.:]+)$') 218 if module_name == '' then module_name = m.name end 219 local func = m.functions[function_name] 220 write_tag( 221 ctags, name, filename, func.code[1], 'f', 'class:' .. module_name) 222 write_apidoc(apidoc, m, func) 223 end 224 if m.tables then 225 -- Document the tables. 226 for _, table_name in ipairs(m.tables) do 227 local table = m.tables[table_name] 228 local module_name = m.name 229 if table_name:find('^_G%.') then 230 module_name, table_name = table_name:match('^_G%.(.-)%.?([^%.]+)$') 231 if not module_name then 232 print('[ERROR] Cannot determine module name for ' .. table.name) 233 elseif module_name == '' then 234 module_name = '_G' -- _G.keys or _G.snippets 235 end 236 end 237 write_tag( 238 ctags, table_name, filename, table.code[1], 't', 239 'class:' .. module_name) 240 write_apidoc(apidoc, m, table) 241 if table.field then 242 -- Tag and document the table's fields. 243 table_name = string.format('%s.%s', module_name, table_name) 244 for _, field_name in ipairs(table.field) do 245 write_tag( 246 ctags, field_name, filename, table.code[1], 'F', 247 'class:' .. table_name) 248 write_apidoc(apidoc, {name = table_name}, { 249 name = field_name, 250 description = table.field[field_name], 251 class = 'table' 252 }) 253 end 254 end 255 end 256 end 257 if m.fields then 258 -- Tag and document the fields. 259 for _, field_name in ipairs(m.fields) do 260 local field = m.fields[field_name] 261 local module_name = m.name 262 if field_name:find('^_G%.') then 263 module_name, field_name = field_name:match('^_G%.(.-)%.?([^%.]+)$') 264 if not module_name then 265 print('[ERROR] Cannot determine module name for ' .. field.name) 266 end 267 end 268 write_tag( 269 ctags, field_name, filename, m.doc[1].code[1], 'F', 270 'class:' .. module_name) 271 write_apidoc(apidoc, {name = field_name}, { 272 name = string.format('%s.%s', module_name, field_name), 273 description = field, 274 class = 'field' 275 }) 276 end 277 end 278 end 279 table.sort(ctags) 280 table.sort(apidoc) 281 local f = io.open(M.options.output_dir .. '/tags', 'wb') 282 f:write(table.concat(ctags, '\n')) 283 f:close() 284 f = io.open(M.options.output_dir .. '/api', 'wb') 285 f:write(table.concat(apidoc, '\n')) 286 f:close() 287 288-- profiler.stop() 289end 290 291return M 292