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