Skip to content

Commit e70f53b

Browse files
committed
feat(doc): move to structured format
This change introduces the files `docs.lua` and `docgen.lua` which are used to generate `docs.md`. `docs.lua` use a bespoke format designed so it can emit the `docs.md` as close to the original version. `docgen.lua` takes `docs.lua` as an input an emits `docs.md`. To regenerate the `docs.md`, simply run `lua docgen.lua`. Closes #632
1 parent ec4185f commit e70f53b

File tree

4 files changed

+5410
-267
lines changed

4 files changed

+5410
-267
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
docs.md linguist-documentation
2+
docs.md linguist-generated

docgen.lua

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
local doctypes = require('docs')
2+
local doc = doctypes[1]
3+
local types = doctypes[2]
4+
5+
--- @param str string
6+
--- @return string
7+
local function dedent(str)
8+
local prefix = 99
9+
10+
str = str:gsub('^\n', ''):gsub('%s+$', '')
11+
12+
for line in str:gmatch('[^\n]+') do
13+
local s, e = line:find('^%s*')
14+
local amount = (e - s) + 1
15+
if amount < prefix then
16+
prefix = amount
17+
end
18+
end
19+
20+
local result = {} --- @type string[]
21+
for line in str:gmatch('([^\n]*)\n?') do
22+
result[#result + 1] = line:sub(prefix + 1)
23+
end
24+
25+
local ret = table.concat(result, '\n')
26+
ret = ret:gsub('\n+$', '')
27+
28+
return ret
29+
end
30+
31+
--- @param lvl integer
32+
--- @param str string
33+
--- @return string
34+
local function heading(lvl, str)
35+
return string.rep('#', lvl) .. ' ' .. str:gsub(' %- ', '')
36+
end
37+
38+
--- @param ty Doc.Type
39+
--- @return boolean
40+
local function isoptional(ty)
41+
if type(ty) == 'string' then
42+
return ty == 'nil'
43+
end
44+
if ty.kind == 'union' then
45+
for _, uty in ipairs(ty[1]) do
46+
if isoptional(uty) then
47+
return true
48+
end
49+
end
50+
end
51+
return false
52+
end
53+
54+
--- @param method Doc.Method
55+
--- @return string
56+
local function sig(method)
57+
local args = {} --- @type string[]
58+
for _, param in ipairs(method.params or {}) do
59+
local nm = param.name
60+
if isoptional(param.type) then
61+
nm = '[' .. nm .. ']'
62+
end
63+
args[#args + 1] = nm
64+
end
65+
return ('`uv.%s(%s)`'):format(method.name, table.concat(args, ', '))
66+
end
67+
68+
local function pad(lvl)
69+
return string.rep(' ', lvl * 2)
70+
end
71+
72+
local normty
73+
74+
--- @param ty Doc.Type.Fun
75+
--- @param lvl? integer
76+
--- @param desc? string
77+
--- @return string
78+
local function normtyfun(ty, lvl, desc)
79+
local r = {} --- @type string[]
80+
r[#r + 1] = '`callable`'
81+
if desc then
82+
r[#r] = r[#r] .. ' ' .. desc
83+
end
84+
for _, arg in ipairs(ty.args) do
85+
local arg_nm, arg_ty, arg_desc = arg[1], arg[2], arg[3]
86+
if arg_nm == 'err' then
87+
arg_ty = 'nil|string'
88+
end
89+
r[#r + 1] = ('%s- `%s`: %s'):format(pad(lvl), arg_nm, normty(arg_ty, lvl + 1))
90+
if arg_desc then
91+
r[#r] = r[#r] .. ' ' .. arg_desc
92+
end
93+
end
94+
95+
return table.concat(r, '\n')
96+
end
97+
98+
--- @param ty Doc.Type.Table
99+
--- @param lvl? integer
100+
--- @param desc? string
101+
--- @return string
102+
local function normtytbl(ty, lvl, desc)
103+
local r = {} --- @type string[]
104+
r[#r + 1] = '`table`'
105+
if desc then
106+
r[#r] = r[#r] .. ' ' .. desc
107+
end
108+
for _, field in ipairs(ty.fields) do
109+
local name, aty, default, adesc = field[1], field[2], field[3], field[4]
110+
r[#r + 1] = ('%s- `%s`: %s'):format(pad(lvl), name, normty(aty, lvl + 1, adesc))
111+
if default then
112+
r[#r] = ('%s (default: `%s`)'):format(r[#r], default)
113+
end
114+
end
115+
116+
return table.concat(r, '\n')
117+
end
118+
119+
--- @param ty string
120+
--- @param _lvl? integer
121+
--- @param desc? string
122+
--- @return string
123+
local function normtystr(ty, _lvl, desc)
124+
do -- TODO(lewis6991): remove
125+
if ty == 'uv_handle_t' or ty == 'uv_req_t' or ty == 'uv_stream_t' then
126+
return '`userdata` for sub-type of `' .. ty .. '`'
127+
end
128+
ty = ty:gsub('uv_[a-z_]+', '%0 userdata')
129+
ty = ty:gsub('%|', '` or `')
130+
end
131+
132+
local desc_str = desc and ' ' .. desc or ''
133+
return '`' .. ty .. '`' .. desc_str
134+
end
135+
136+
--- @param ty Doc.Type.Dict
137+
--- @param lvl? integer
138+
--- @param desc? string
139+
--- @return string
140+
local function normtydict(ty, lvl, desc)
141+
local r = {} --- @type string[]
142+
r[#r + 1] = '`table`'
143+
if desc then
144+
r[#r] = r[#r] .. ' ' .. desc
145+
end
146+
-- TODO(lewis6991): remove
147+
if ty.key == 'integer' then
148+
ty.key = '1, 2, 3, ..., n'
149+
end
150+
r[#r + 1] = ('%s- `[%s]`: %s'):format(pad(lvl), ty.key, normty(ty.value, lvl + 1))
151+
return table.concat(r, '\n')
152+
end
153+
154+
--- @param ty Doc.Type.Union
155+
--- @param lvl? integer
156+
--- @param desc? string
157+
--- @return string
158+
local function normtyunion(ty, lvl, desc)
159+
local tys = ty[1]
160+
161+
local r = {} --- @type string[]
162+
163+
local main_ty --- @type integer?
164+
for i, uty in ipairs(tys) do
165+
if normty(uty):match('\n') then
166+
main_ty = i
167+
end
168+
r[#r + 1] = normty(uty, lvl, i == #tys and desc or nil)
169+
end
170+
171+
if main_ty and #tys > 1 then
172+
local others = {} --- @type string[]
173+
for i, oty in ipairs(r) do
174+
if i ~= main_ty then
175+
others[#others + 1] = oty
176+
end
177+
end
178+
local other_ty_str = table.concat(others, ' or ')
179+
-- local other_ty_str = table.concat(others, '|')
180+
181+
return (r[main_ty]:gsub('\n', ' or ' .. other_ty_str .. '\n', 1))
182+
end
183+
184+
return table.concat(r, ' or ')
185+
-- return table.concat(r, '|')
186+
end
187+
188+
local types_noinline = {
189+
threadargs = true,
190+
buffer = true,
191+
}
192+
193+
--- @param ty Doc.Type
194+
--- @param lvl? integer
195+
--- @param desc? string
196+
--- @return string
197+
function normty(ty, lvl, desc)
198+
-- resolve type
199+
if types[ty] and not types_noinline[ty] and not types[ty].extends then
200+
ty = types[ty]
201+
end
202+
lvl = lvl or 0
203+
local f --- @type fun(ty: Doc.Type, lvl: integer, desc: string): string
204+
if type(ty) == 'string' then
205+
f = normtystr
206+
elseif ty.kind == 'function' then
207+
f = normtyfun
208+
elseif ty.kind == 'table' then
209+
f = normtytbl
210+
elseif ty.kind == 'dict' then
211+
f = normtydict
212+
elseif ty.kind == 'union' then
213+
f = normtyunion
214+
end
215+
return f(ty, lvl, desc)
216+
end
217+
218+
--- @param out file*
219+
--- @param param Doc.Method.Param
220+
local function write_param(out, param)
221+
out:write(('- `%s`:'):format(param.name))
222+
local ty = param.type
223+
if ty then
224+
out:write(' ', normty(ty, 1, param.desc))
225+
elseif param.desc then
226+
out:write(' ', param.desc)
227+
end
228+
if param.default then
229+
out:write((' (default: `%s`)'):format(param.default))
230+
end
231+
out:write('\n')
232+
end
233+
234+
--- @param ty Doc.Type
235+
local function remove_nil(ty)
236+
if type(ty) == 'table' and ty.kind == 'union' then
237+
for i, uty in ipairs(ty[1]) do
238+
if uty == 'nil' then
239+
table.remove(ty[1], i)
240+
break
241+
else
242+
remove_nil(uty)
243+
end
244+
end
245+
end
246+
end
247+
248+
--- @param out file*
249+
--- @param x string|Doc.Method.Return[]
250+
--- @param variant? string
251+
local function write_return(out, x, variant)
252+
local variant_str = variant and (' (%s version)'):format(variant) or ''
253+
if type(x) == 'string' then
254+
out:write(('**Returns%s:** %s\n'):format(variant_str, normty(x)))
255+
elseif type(x) == 'table' then
256+
if x[2] and x[2][2] == 'err' and x[3] and x[3][2] == 'err_name' then
257+
local sty = x[1][1]
258+
remove_nil(sty)
259+
local rty = normty(sty, nil, 'or `fail`')
260+
out:write(('**Returns%s:** %s\n\n'):format(variant_str, rty))
261+
return
262+
else
263+
local tys = {} --- @type string[]
264+
for _, ret in ipairs(x) do
265+
tys[#tys + 1] = normty(ret[1])
266+
end
267+
out:write(('**Returns%s:** %s\n'):format(variant_str, table.concat(tys, ', ')))
268+
end
269+
else
270+
out:write('**Returns:** Nothing.\n')
271+
end
272+
out:write('\n')
273+
end
274+
275+
--- @param out file*
276+
--- @param method Doc.Method
277+
--- @param lvl integer
278+
local function write_method(out, method, lvl)
279+
out:write(heading(lvl, sig(method)))
280+
out:write('\n\n')
281+
282+
if method.method_form then
283+
out:write(('> method form `%s`\n\n'):format(method.method_form))
284+
end
285+
286+
if method.deprecated then
287+
out:write('**Deprecated:** ', dedent(method.deprecated))
288+
out:write('\n\n')
289+
return
290+
end
291+
292+
if method.params then
293+
out:write('**Parameters:**\n')
294+
for _, param in ipairs(method.params) do
295+
write_param(out, param)
296+
end
297+
out:write('\n')
298+
end
299+
300+
if method.desc then
301+
out:write(dedent(method.desc))
302+
out:write('\n\n')
303+
end
304+
305+
if method.returns_doc then
306+
out:write('**Returns:**')
307+
local r = dedent(method.returns_doc)
308+
if r:sub(1, 1) == '-' then
309+
out:write('\n')
310+
else
311+
out:write(' ')
312+
end
313+
out:write(r)
314+
out:write('\n')
315+
out:write('\n')
316+
elseif method.returns_sync and method.returns_async then
317+
write_return(out, method.returns_sync, 'sync')
318+
write_return(out, method.returns_async, 'async')
319+
else
320+
write_return(out, method.returns)
321+
end
322+
323+
if method.example then
324+
out:write(dedent(method.example))
325+
out:write('\n\n')
326+
end
327+
328+
if method.see then
329+
out:write(('See [%s][].\n\n'):format(method.see))
330+
end
331+
332+
for _, note in ipairs(method.notes or {}) do
333+
local notes = dedent(note)
334+
out:write('**Note**:')
335+
if notes:sub(1, 3) == '1. ' then
336+
out:write('\n', notes, '\n\n')
337+
else
338+
out:write(' ', notes, '\n\n')
339+
end
340+
end
341+
342+
for _, warn in ipairs(method.warnings or {}) do
343+
out:write(('**Warning**: %s\n\n'):format(dedent(warn)))
344+
end
345+
346+
if method.since then
347+
out:write(('**Note**: New in libuv version %s.\n\n'):format(method.since))
348+
end
349+
end
350+
351+
--- @param out file*
352+
--- @param section Doc
353+
--- @param lvl integer
354+
local function write_section(out, section, lvl)
355+
local title = section.title
356+
if title then
357+
out:write(heading(lvl, title))
358+
out:write('\n\n')
359+
end
360+
local id = section.id
361+
if id then
362+
local tag = assert(title):match('^`[a-z_]+`') or title
363+
out:write(('[%s]: #%s\n\n'):format(tag, id))
364+
end
365+
366+
if section.desc then
367+
out:write(dedent(section.desc))
368+
out:write('\n\n')
369+
end
370+
371+
if section.constants then
372+
for _, constant in ipairs(section.constants) do
373+
out:write(('- `%s`: "%s"\n'):format(constant[1], constant[2]))
374+
end
375+
out:write('\n')
376+
end
377+
378+
for _, method in ipairs(section.methods or {}) do
379+
write_method(out, method, lvl + 1)
380+
end
381+
382+
for _, subsection in ipairs(section.sections or {}) do
383+
write_section(out, subsection, lvl + 1)
384+
end
385+
end
386+
387+
local out = assert(io.open('docs.md', 'w'))
388+
389+
for _, section in ipairs(doc) do
390+
write_section(out, section, 1)
391+
end

0 commit comments

Comments
 (0)