diff options
| author | Alexander Pickering <alex@cogarr.net> | 2020-07-05 12:22:36 -0400 |
|---|---|---|
| committer | Alexander Pickering <alex@cogarr.net> | 2020-07-05 12:22:36 -0400 |
| commit | d2ba262c5307aa14c325ef53d8e4e56a5ece0376 (patch) | |
| tree | ad06e57708cd45b457dbea4804812b1e5ddfcb36 /init.lua | |
| download | mdoc-d2ba262c5307aa14c325ef53d8e4e56a5ece0376.tar.gz mdoc-d2ba262c5307aa14c325ef53d8e4e56a5ece0376.tar.bz2 mdoc-d2ba262c5307aa14c325ef53d8e4e56a5ece0376.zip | |
Initial Commit
Diffstat (limited to 'init.lua')
| -rw-r--r-- | init.lua | 572 |
1 files changed, 572 insertions, 0 deletions
diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..a3515ed --- /dev/null +++ b/init.lua @@ -0,0 +1,572 @@ +--[[
+Mdoc creats documentation from comments in source files.
+Mdoc works in several steps:
+ 1. Create "chunks" from input source files
+ 2. Create a dependnecy graph, so we only update the things that
+ need updating
+ 3. Output html documentation
+]]
+local lpeg = require("lpeg")
+local lfs = require("lfs")
+local et = require("etlua")
+require("ext")
+local opt = require("opt_parser")
+
+local args = {...}
+local options = opt.parse_options(args)
+if options.help then
+ print(opt.help())
+ return
+end
+print("options:",options)
+
+local state = {
+ title = "",
+ sections = {},
+ files = {},
+ documents = {},
+ known_names = {},
+ index = {},
+}
+
+local function log(...)
+ if options.verbose then
+ print(...)
+ end
+end
+local function logf(...)
+ if options.verbose then
+ printf(...)
+ end
+end
+
+local toskip = {["."] = true,[".."] = true}
+local function scan(path,callback)
+ assert(path,"Cannot scan without a path to scan (was nil)")
+ assert(callback,"Cannot scan without a callback to call (was nil)")
+ for item in lfs.dir(path) do
+ log("looking at:",item)
+ if toskip[item] then
+ goto nextitem
+ end
+ local fullpath = path .. "/" .. item
+ local attributes = lfs.attributes(fullpath)
+ if attributes.mode == "directory" or attributes.mode == "link" then
+ log("found directory:",directory)
+ scan(fullpath,callback)
+ elseif attributes.mode == "file" then
+ log("found item:",item)
+ callback(fullpath)
+ end
+ ::nextitem::
+ end
+end
+for _,path in pairs(options.paths) do
+ scan(path,function(p)
+ local attributes = lfs.attributes(p)
+ table.insert(state.files,{
+ path = p,
+ relpath = p:match(string.format("%s/(.*)",path)),
+ lastmod = attributes.modification,
+ })
+ end)
+end
+logf("Found %d files.",#state.files)
+
+for _,path in pairs(options.document_paths) do
+ scan(path,function(p)
+ local attributes = lfs.attributes(p)
+ table.insert(state.documents,{
+ path = p,
+ lastmod = attributes.modification,
+ })
+ end)
+end
+logf("Found %d documents.",#state.documents)
+
+
+local function parse(text,filename)
+ assert(options.parser,"Failed to find a parser option")
+ assert(text,"Failed to get text")
+ text = text:gsub("@{(.+)}",function(data)
+ return string.format("[%s](%s.html)",data,data)
+ end)
+ log("Using parser:",options.parser)
+ local tmp = options.output .. "/" .. os.tmpname()
+ log("using temp name:",tmp)
+ local pd = io.popen(options.parser .. " > " .. tmp,"w")
+ pd:write(text)
+ pd:close()
+ local id = assert(io.open(tmp,"r"))
+ local ret = id:read("*a")
+ id:close()
+ assert(os.remove(tmp))
+ return ret
+end
+
+local function table_to_string(tbl)
+ --Collect all of our tables first,
+ --so that we don't break when we have recursive tables.
+ local tables = {}
+ local table_order = {}
+ local function tables_helper(t)
+ tables[t] = #table_order + 1
+ table_order[#table_order + 1] = t
+ for k,v in pairs(t) do
+ if type(k) == "table" and not tables[k] then
+ tables_helper(k)
+ end
+ if type(v) == "table" and not tables[v] then
+ tables_helper(v)
+ end
+ end
+ end
+ tables_helper(tbl)
+
+ --Get the string representation of an element
+ local errfun = function(e) error("Cannot format a " .. type(e)) end
+ local rep_map = {
+ --things we can format
+ number = function(e)
+ if e % 1 == 0 then return string.format("%d",e)
+ else return string.format("%f",e) end
+ end,
+ string = function(e) return string.format("%q",e) end,
+ boolean = function(e) return e and "true" or "false" end,
+ table = function(e)
+ assertf(tables[e] ~= nil,"Could not find dependency table %s",tostring(e))
+ return string.format("table_%d",tables[e])
+ end,
+ --things we can't format
+ ["function"] = errfun,
+ coroutine = errfun,
+ file = errfun,
+ userdata = errfun,
+ --nil can never happen
+ }
+ local sb = {}
+ --Create all the variables first, so that recursive tables will work
+ for n,_ in pairs(table_order) do
+ sb[#sb + 1] = string.format("local table_%d = {}",n)
+ end
+ --Go backwards through tables, since that should be the
+ --"dependency" order
+ for i = #table_order, 1, -1 do -- -1 is needed in case #table_order == 0
+ local tstr = {}
+ local this_table = table_order[i]
+ for k,v in pairs(this_table) do
+ tstr[#tstr + 1] = string.format("table_%d[%s] = %s",i,rep_map[type(k)](k), rep_map[type(v)](v))
+ end
+ sb[#sb + 1] = table.concat(tstr,"\n")
+ end
+ sb[#sb + 1] = "return table_1"
+ return table.concat(sb,"\n\n");
+end
+
+--io.open automatically creates parent directories
+local oldopen = io.open
+function io.open(path,mode)
+ if mode == "w" or mode == "w+" then
+ local path_so_far = ""
+ for folder in path:gmatch("([^/]+)/") do
+ path_so_far = path_so_far .. folder .. "/"
+ if lfs.attributes(path_so_far) == nil then
+ lfs.mkdir(path_so_far)
+ end
+ end
+ end
+ return oldopen(path,mode)
+end
+
+--Create a cache directory
+if not lfs.attributes(options.output .. "/cache") then
+ assert(lfs.mkdir(options.output .. "/cache"))
+ assert(lfs.mkdir(options.output .. "/cache/documents"))
+ assert(lfs.mkdir(options.output .. "/cache/files"))
+ assert(lfs.mkdir(options.output .. "/cache/chunks"))
+end
+
+--Get the data for the reference documents
+for dn, data in pairs(state.documents) do
+ local file_name = data.path:match(".*/([^.]+)%.[^.]+$")
+ local cachefile = string.format("%s/cache/documents/%s",options.output,file_name)
+ local out_attrs = lfs.attributes(cachefile)
+ if not out_attrs or out_attrs.modification < data.lastmod then
+ --we're out of date, update
+ local fd = assert(io.open(data.path,"r"))
+ local file_text = fd:read("*a")
+ fd:close()
+ local parsed_text = parse(file_text, data.path)
+ local od = assert(io.open(cachefile,"w"))
+ od:write(parsed_text)
+ od:close()
+ state.sections[file_name] = {
+ type = "reference",
+ data_file = cachefile
+ }
+ end
+end
+
+--Openers and closers
+local parse_between = {
+ ["/***"] = "*/",
+ ["--[[**"] = "]]"
+}
+for _,file in pairs(state.files) do
+ local cachefilename = string.format("%s/cache/files/%s",options.output,file.relpath)
+ local cacheattrs = lfs.attributes(cachefilename)
+ if (not cacheattrs) or cacheattrs.modification < file.lastmod then
+ local chunks = {}
+ local in_chunk = false
+ local line_num = 0
+ local closer
+ local fd = assert(io.open(file.path,"r"))
+ for line in fd:lines() do
+ line_num = line_num + 1
+ if parse_between[line] then
+ closer = parse_between[line]
+ table.insert(chunks,{})
+ in_chunk = true
+ end
+ if in_chunk then
+ table.insert(chunks[#chunks],line)
+ end
+ if in_chunk and line == closer then
+ log("found end, chunks:",chunks)
+ chunks[#chunks] = {
+ text = chunks[#chunks],
+ file = file.path,
+ line = line_num,
+ relpath = file.relpath,
+ }
+ in_chunk = false
+ end
+ end
+ file.chunks = chunks
+ local cachefile = assert(io.open(cachefilename,"w"))
+ cachefile:write(table_to_string(chunks))
+ cachefile:close()
+ else
+ file.chunks = loadfile(cachefilename)()
+ end
+end
+
+for _,file in pairs(state.files) do
+ local function process_chunk(chunk)
+ assert(chunk.file and chunk.line, "Chunk without file or line num:")
+ local section = nil
+ local partname = nil
+ local sectiontype = nil
+ local short_desc
+ local desc = {}
+ --Parameters are {
+ -- name = "string",
+ -- type = "string",
+ -- optional = bool
+ -- optchain = bool
+ -- default = nil | "string"
+ --}
+ local ret = {}
+ for num,line in pairs(chunk.text) do
+ if num == 2 then
+ if line:match("^@.*") then
+ short_desc = "no short description provided"
+ else
+ short_desc = line
+ end
+ end
+ if num > 2 and type(desc) == "table" and not line:match("^@.*") then
+ table.insert(desc,line)
+ end
+ if line:match("^@") then
+ local command, rest = line:match("^@([^ ]+) (.*)")
+ if command == "function" then
+ sectiontype = "function"
+ if rest:find(":") then
+ local classname, functionname = rest:match("^([^:]+):([^%(]+)")
+ ret = {
+ --state.sections[classname] = state.sections[classname] or {type = "class"}
+ --state.sections[classname][functionname] = state.sections[classname][functionname] or {
+ type="method",
+ name = functionname,
+ path = {classname,functionname},
+ short_desc = parse(short_desc),
+ line = chunk.line,
+ desc = parse(table.concat(desc,"\n")),
+ params = {},
+ returns = {},
+ file = chunk.relpath
+ }
+ section = classname
+ partname = functionname
+ else
+ local namespace, functionname
+ local c = rest:match("^([^.%(]+)%(")
+ if c then
+ namespace = "_G"
+ functionname = c
+ else
+ namespace, functionname = rest:match("([^.]+)%.([^%(]+)")
+ end
+ log("namespace:",namespace,"functionname:",functionname)
+ ret = {
+ --state.sections[namespace] = state.sections[namespace] or {type = "namespace"}
+ --state.sections[namespace][functionname] = state.sections[namespace][functionname] or {
+ type="function",
+ name = functionname,
+ short_desc = parse(short_desc),
+ path = {namespace,functionname},
+ line = chunk.line,
+ desc = parse(table.concat(desc,"\n")),
+ params = {},
+ returns = {},
+ references = {},
+ file = chunk.relpath
+ }
+ section = namespace
+ partname = functionname
+ end
+ elseif command == "tparam" or command == "tparam?" then
+ local parts = {}
+ local vartype, name, description
+ for word in rest:gmatch("([^ ]+)") do table.insert(parts,word) end
+ assertf(#parts >= 2,"@tparam at %s:%d requires at least a type and a variable name",chunk.file,chunk.line_num)
+ if parts[1] then
+ vartype = parts[1]
+ table.remove(parts,1)
+ end
+ if parts[1] then
+ name = parts[1]
+ table.remove(parts,1)
+ end
+ description = table.concat(parts,"\n")
+
+ assertf(section and partname and sectiontype == "function","Tried to specify a tparam for something that wasn't a function at %s:%d",chunk.file,chunk.line)
+ local param = {
+ type = vartype,
+ name = name,
+ description = parse(description),
+ optional = command == "tparam?"
+ }
+ table.insert(ret.params,param)
+ --table.insert(state.sections[section][partname].params,param)
+ elseif command == "treturn" then
+ local vartype, description = rest:match("([^ ]+) (.*)")
+ assertf(section and partname and sectiontype == "function","Tried to specify a treturn for something that wasn't a function at %s:%d",chunk.file,chunk.line)
+ local tret = {
+ type = vartype,
+ description = description and parse(description)
+ }
+ table.insert(ret.returns,tret)
+ --table.insert(state.sections[section][partname].returns,{
+ --type = vartype,
+ --description = description and parse(description)
+ --})
+ elseif command == "field" then
+ local namespace, field, value
+ local c,v = rest:match("^([^ ]+) ([^ ]+)$")
+ if c then
+ namespace = "_G"
+ field = c
+ value = v
+ else
+ namespace, field, value = rest:match("([^.]+).([^ ]+) ?(.*)")
+ end
+ assertf(namespace and field and value, "Improperly defined @field at %s:%d, should be '@field module.field Description here'",chunk.file,chunk.line)
+ section = namespace
+ partname = field
+ state.sections[namespace] = state.sections[namespace] or {type="namespace"}
+ assertf(not state.sections[namespace][field],"2 or more definitions for %s.%s",namespace,field)
+ ret = {
+ --state.sections[namespace][field] = {
+ type = "field",
+ name = field,
+ path = {namespace,field},
+ short_desc = parse(short_desc),
+ desc = desc and parse(table.concat(desc,"\n")),
+ line = chunk.line,
+ file = chunk.relpath
+ }
+ elseif command == "class" then
+ local classname = rest
+ section = classname
+ ret = {
+ --state.sections[classname] = state.sections[classname] or {
+ type = "class",
+ name = classname,
+ short_desc = parse(short_desc),
+ desc = parse(table.concat(desc,"\n")),
+ path = {classname}
+ }
+ partname = nil
+ elseif command == "inherits" then
+ assertf(ret and ret.type == "class","Don't know what is using @inherits at %s:%d, add a @class definition above it",chunk.file,chunk.line)
+ ret.inherits = ret.inherits or {}
+ table.insert(ret.inherits,rest)
+ --state.sections[section].inherits = state.sections[section].inherits or {}
+ --table.insert(state.sections[section].inherits,rest)
+ else
+ assertf(ret and ret.path[1] ,"Don't know where to put @%s, add a @function, @field or @table marker before it.",command)
+ if partname then
+ assertf(ret.path[2] ,"Don't know where to put @%s, add a @function, @field, or @table marker before it")
+ ret[command] = rest
+ --state.sections[section][partname][command] = rest
+ else
+ ret[command] = rest
+ --state.sections[section][command] = rest
+ end
+ end
+ end
+ end
+ return ret
+ end
+ local cachefilename = string.format("%s/cache/chunks/%s",options.output,file.relpath)
+ local cachefileattrs = lfs.attributes(cachefilename)
+ if (not cachefileattrs) or cachefileattrs.modification < file.lastmod then
+ for k,v in pairs(file.chunks) do
+ file.chunks[k] = process_chunk(v)
+ end
+ local cachefile = assert(io.open(cachefilename,"w"))
+ cachefile:write(table_to_string(file.chunks))
+ cachefile:close()
+ else
+ file.chunks = loadfile(cachefilename)()
+ end
+end
+
+for _,file in pairs(state.files) do
+ for _, chunk in pairs(file.chunks) do
+ assertf(chunk.path, "Chunk at %s:%d didn't have a path.", file.name, chunk.line)
+ local cursor = state.sections
+ for _, path_part in pairs(chunk.path) do
+ if not cursor[path_part] then
+ cursor[path_part] = {}
+ end
+ cursor = cursor[path_part]
+ end
+ for k,v in pairs(chunk) do
+ cursor[k] = v
+ end
+ end
+end
+
+--Get the data for the reference documents
+if options.index ~= nil then
+ local file_mod = lfs.attributes(options.index).modification
+ local file_name = options.index:match(".*/([^.]+)%.[^.]+$")
+ local cachefile = string.format("%s/cache/documents/%s",options.output,file_name)
+ local out_attrs = lfs.attributes(cachefile)
+ if not out_attrs or out_attrs.modification < file_mod then
+ --we're out of date, update
+ local fd = assert(io.open(options.index,"r"))
+ local file_text = fd:read("*a")
+ fd:close()
+ local parsed_text = parse(file_text, options.index)
+ local od = assert(io.open(cachefile,"w"))
+ od:write(parsed_text)
+ od:close()
+ end
+ state.sections[file_name] = {
+ type = "reference",
+ data_file = cachefile
+ }
+ state.index = state.sections[file_name]
+end
+
+log("After copying everything in, state.sections is:")
+log(state.sections)
+
+local headers = {}
+for name,section in pairs(state.sections) do
+ log("section:",name,"type:",section.type,"data:")
+ local ust = "namespace"
+ for _,v in pairs(section) do
+ if v.type == "method" then
+ ust = "class"
+ end
+ end
+ section.type = section.type or ust
+ headers[section.type] = headers[section.type] or {}
+ section.name = name
+ table.insert(headers[section.type],section)
+end
+for _,group in pairs(headers) do
+ table.sort(group,function(a,b)
+ --print("sorting:",a.type,"against",b.type)
+ return a.type < b.type
+ end)
+end
+--print("headers:",headers)
+
+local nvfd = assert(io.open("navbar.etlua","r"))
+local navbar = assert(et.compile(nvfd:read("*a")))
+nvfd:close()
+local navbarhtml = navbar{headers = headers}
+local pagefd = assert(io.open("page.etlua","r"))
+local page = assert(et.compile(pagefd:read("*a")))
+pagefd:close()
+local funcsignaturefd = assert(io.open("funcsignature.etlua","r"))
+local funcsignature = assert(et.compile(funcsignaturefd:read("*a")))
+funcsignaturefd:close()
+local sorted_headers = {}
+for name,_ in pairs(headers) do
+ table.insert(sorted_headers,name)
+end
+table.sort(sorted_headers)
+for _,name in pairs(sorted_headers) do
+ local header = headers[name]
+ for _,section in pairs(header) do
+ log("About to render section:",section.name)
+ log(section)
+ local pagehtml = assert(page{
+ header = section,
+ navbar = navbarhtml,
+ options = options,
+ et = et,
+ funcsig = funcsignature,
+ })
+ log("Done rendering pagehtml")
+ log("section name:",section.name)
+ log("section:",section)
+ local ofd = assert(io.open(options.output .. "/" .. section.name .. ".html","w"))
+ ofd:write(pagehtml)
+ ofd:close()
+ end
+end
+
+if options.index ~= nil then
+ local indexfd = assert(io.open("index.etlua","r"))
+ local index = assert(et.compile(indexfd:read("*a")))
+ indexfd:close()
+ log("state.index:",state.index)
+ local indextextfd = assert(io.open(state.index.data_file,"r"))
+ local indextext = indextextfd:read("*a")
+ indextextfd:close()
+ local indexhtml = index{
+ navbar = navbarhtml,
+ options = options,
+ et = et,
+ text = indextext,
+ }
+ local ofd = assert(io.open(options.output .. "/index.html", "w"))
+ ofd:write(indexhtml)
+ ofd:close()
+end
+
+--Copy style css
+local css = assert(io.open("style.css","r"))
+local css_out = assert(io.open(options.output .. "/style.css","w"))
+for line in css:lines() do
+ css_out:write(line)
+end
+css:close()
+css_out:close()
+
+--Generate html
+--for header, sections in pairs(headers) do
+
+ ----local ofd = assert(io.open(options.output .. "/" .. name,"w"))
+ --print("want to output section:",name,section.type)
+--end
+
+--print(state)
|
