From e87b06ee0fe2a588b72a356bbb8378899365d626 Mon Sep 17 00:00:00 2001 From: Alexander Pickering Date: Sun, 5 Jul 2020 17:18:56 -0400 Subject: Add rockspec Add a rockspec and move the files around so that luarocks can install it correctly --- README.md | 21 ++ ext.lua | 38 --- func.etlua | 43 ---- funcsignature.etlua | 22 -- index.etlua | 19 -- init.lua | 572 ------------------------------------------ mdoc-dev-1.rockspec | 35 +++ navbar.etlua | 25 -- opt_parser.lua | 188 -------------- page.etlua | 97 ------- share/func.etlua.lua | 43 ++++ share/funcsignature.etlua.lua | 24 ++ share/index.etlua.lua | 19 ++ share/navbar.etlua.lua | 26 ++ share/page.etlua.lua | 91 +++++++ share/style.css.lua | 28 +++ src/ext.lua | 38 +++ src/init.lua | 569 +++++++++++++++++++++++++++++++++++++++++ src/opt_parser.lua | 191 ++++++++++++++ style.css | 27 -- 20 files changed, 1085 insertions(+), 1031 deletions(-) create mode 100644 README.md delete mode 100644 ext.lua delete mode 100644 func.etlua delete mode 100644 funcsignature.etlua delete mode 100644 index.etlua delete mode 100644 init.lua create mode 100644 mdoc-dev-1.rockspec delete mode 100644 navbar.etlua delete mode 100644 opt_parser.lua delete mode 100644 page.etlua create mode 100644 share/func.etlua.lua create mode 100644 share/funcsignature.etlua.lua create mode 100644 share/index.etlua.lua create mode 100644 share/navbar.etlua.lua create mode 100644 share/page.etlua.lua create mode 100644 share/style.css.lua create mode 100644 src/ext.lua create mode 100644 src/init.lua create mode 100644 src/opt_parser.lua delete mode 100644 style.css diff --git a/README.md b/README.md new file mode 100644 index 0000000..d9fb25e --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# MDoc + +## Overview + +A documentation engine for Lua + +MDoc was built after frustrations with +[LDoc](https://stevedonovan.github.io/ldoc/manual/doc.md.html), +the usual and most popular documentation engine for lua. Like LDoc, MDoc users +comments in the source code to generate html files for documentation. Unlike +LDoc, it makes no attempts to be backwards compatible with LuaDoc. This results +in a documentation engine that is free from constraints like "One class per +file" or "one module per file". MDoc is still in it's early stages, expect +frequent code churn in the near future. + +MDoc is used to document my homebrew VR platform, [Brok\[en\]gine](https://cogarr.net/source/cgit.cgi/brokengine/), +and you can see an example of mdoc in action in the [Brok\[en\]gine reference]. + +## Installation + +The easiest way to download MDoc is with [luarocks](https://github.com/luarocks/luarocks) diff --git a/ext.lua b/ext.lua deleted file mode 100644 index 1c05b9f..0000000 --- a/ext.lua +++ /dev/null @@ -1,38 +0,0 @@ ---[[ -Extensions that don't belong anywhere else -]] - --- Override tostring to display more info about the table -local old_tostring = tostring -local numtabs = 0 -local printed_tables = {} -function tostring(el) - if type(el) == "table" and printed_tables[el] == nil then - printed_tables[el] = true - numtabs = numtabs + 1 - local strbuilder = {"{"} - for k,v in pairs(el) do - strbuilder[#strbuilder + 1] = string.format("%s%s : %s", string.rep("\t",numtabs), tostring(k), tostring(v)) - end - printed_tables[el] = nil - strbuilder[#strbuilder + 1] = string.rep("\t",numtabs - 1) .. "}" - numtabs = numtabs - 1 - return table.concat(strbuilder,"\n") - end - return old_tostring(el) -end - ---functions to save my hand -function assertf(bool,msg,...) - if not bool then - error(string.format(msg,...),2) - end -end - -function errorf(fmt,...) - error(string.format(fmt,...),2) -end - -function printf(fmt,...) - print(string.format(fmt,...)) -end diff --git a/func.etlua b/func.etlua deleted file mode 100644 index f639603..0000000 --- a/func.etlua +++ /dev/null @@ -1,43 +0,0 @@ - -
-

- <%- name %>( - <% for n, param in pairs(data.params) do %> - <% if param.optional then %>[<% end %> - <% if n ~= 1 then %>,<% end %> - <%- param.type %> <%- param.name %> - <% if param.optional then %>]<% end %> - <% end %> - ) -

-

<%- data.short_desc %> -

<%- data.desc %> - <% if #data.params > 0 then %> -

Parameters
- - <% end %> - <% if #data.returns > 0 then %> -
Returns
- - <% end %> -

Defined at <%- data.file %>:<%- data.line %> -

- diff --git a/funcsignature.etlua b/funcsignature.etlua deleted file mode 100644 index 48d92ba..0000000 --- a/funcsignature.etlua +++ /dev/null @@ -1,22 +0,0 @@ -<% assert(func, "Requires function to render a function signature") %> -<% assert(funcname, "Requires a function name to render a function signature") %> -<%- funcname %>( - <% if func.params and #func.params > 0 then %> - <% local optchain = false %> - <% for paramid, param in pairs(func.params) do %> - <% if paramid == 1 and param.optional then %> - [ - <% elseif param.optional then %> - [, - <% elseif paramid > 1 then %> - , - <% end %> - <%- param.name %> - <% if paramid == 1 and param.optional then %> - ] - <% elseif param.optional then %> - ] - <% end %> - <% end %> - <% end %> -) diff --git a/index.etlua b/index.etlua deleted file mode 100644 index 708e2b5..0000000 --- a/index.etlua +++ /dev/null @@ -1,19 +0,0 @@ - - - - <%- options.title %> - - - - - - <%- navbar %> -
- <%- text %> -
- - - - - diff --git a/init.lua b/init.lua deleted file mode 100644 index a3515ed..0000000 --- a/init.lua +++ /dev/null @@ -1,572 +0,0 @@ ---[[ -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) diff --git a/mdoc-dev-1.rockspec b/mdoc-dev-1.rockspec new file mode 100644 index 0000000..467195e --- /dev/null +++ b/mdoc-dev-1.rockspec @@ -0,0 +1,35 @@ +package = "mdoc" +version = "dev-1" +source = { + url = "https://cogarr.net/source/cgit.cgi/mdoc", + tag = "v1.0" +} +description = { + summary = "A documentation tool for Lua", + detailed = "An alternative to LDoc, MDoc creates reference documentation from source code.", + homepage = "https://cogarr.net/source/cgit.cgi/mdoc/", + license = "BSD/2 Clause" +} +dependencies = { + "lua >= 5.1", + "etlua >= 1.3", + "luafilesystem >= 1.7" +} +build = { + type = "builtin", + modules = { + ["mdoc.opt"] = "src/opt_parser.lua", + ["mdoc.ext"] = "src/ext.lua", + ["mdoc.files.func"] = "share/func.etlua.lua", + ["mdoc.files.funcsignature"] = "share/funcsignature.etlua.lua", + ["mdoc.files.index"] = "share/index.etlua.lua", + ["mdoc.files.navbar"] = "share/navbar.etlua.lua", + ["mdoc.files.page"] = "share/page.etlua.lua", + ["mdoc.files.style"] = "share/style.css.lua" + }, + install = { + bin = { + mdoc = "src/init.lua" + }, + } +} diff --git a/navbar.etlua b/navbar.etlua deleted file mode 100644 index 368ea10..0000000 --- a/navbar.etlua +++ /dev/null @@ -1,25 +0,0 @@ - diff --git a/opt_parser.lua b/opt_parser.lua deleted file mode 100644 index d99c804..0000000 --- a/opt_parser.lua +++ /dev/null @@ -1,188 +0,0 @@ - -local lfs = require("lfs") -require("ext") -local ret = {} - -ret.options = { - paths = { - type = "folder", - multiple = true, - short = "-p", - long = "--path", - consumes = 1, - }, - output = { - type = "folder", - short = "-o", - long = "--output", - consumes = 1, - default = ".", - }, - title = { - type = "string", - short = "-t", - long = "--tile", - consumes = 1, - default = "Mdoc Generated Page", - }, - index = { - type = "file", - short = "-i", - long = "--index", - consumes = 1, - }, - document_paths = { - type = "folder", - multiple = true, - short = "-d", - long = "--document", - consumes = 1, - }, - parser = { - type = "executable", - short = "-m", - long = "--markup-parser", - consumes = 1, - default = "markdown", - }, - --append = { - --type = "file", - --short = "-a", - --long = "--append", - --consumes = 1, - --multiple = true, - --}, - verbose = { - type = "flag", - short = "-v", - long = "--verbose" - }, - help = { - type = "flag", - short = "-h", - long = "--help", - } -} - -local function check_file_type(path, type) - local attr, err = lfs.attributes(path) - if attr == nil then - return attr, err - end - if attr.mode == type then - return path - else - return false, string.format("%s was not a folder, it was a %s",path, attr.mode) - end -end - -ret.check_flag = function(...) - return true -end - -ret.check_folder = function(path) - if path:match("/$") then - path = path:sub(1,-2) - end - return check_file_type(path,"directory") -end - -ret.check_string = function(str) - if type(str) == "string" then - return str - else - return false, string.format("%s was not a string, it was a %s",tostring(str), type(str)) - end -end - -ret.check_file = function(path) - return check_file_type(path,"file") -end - -ret.check_executable = function(name) - local tmpname = "./" .. os.tmpname() - local pd = assert(io.popen(name .. " > " .. tmpname, "w")) - pd:write("Hello, world!") - pd:close() - local id = assert(io.open(tmpname,"r")) - local dat = id:read("*a") - id:close() - assert(os.remove(tmpname)) - if string.len(dat) > 0 then - return name - else - return false, string.format("Tried to execute %q, but it did not create any data with the input 'Hello, world!', are you sure it's an executale on your PATH?", name) - end -end - -local option_lookup = {} -for k,v in pairs(ret.options) do - if v.short then - option_lookup[v.short] = k - end - if v.long then - option_lookup[v.long] = k - end -end - -ret.parse_options = function(args) - print("parsing args:",args) - local parsed = {} - local i = 1 - while i <= #args do - local option_name = option_lookup[args[i]] - print("found option:",option_name) - if not option_name then - errorf("Unknown option #%d: %q",i, args[i]) - end - local option = ret.options[option_name] - assertf(#args > i + (option.consumes or 0) - 1, "Option #%d (%q) consumes %d arguments, but found end of arguments",i,args[i],option.consumes) - local args_for_option = {} - for j = i, i+(option.consumes or 0) do - table.insert(args_for_option,args[j+1]) - end - --special, if we only consume 1 option, just pass that. - if option.consumes == 1 then - args_for_option = args_for_option[1] - end - print("checking",option.type) - print("with",args_for_option) - local check = assert(ret["check_" .. option.type](args_for_option)) - if option.multiple then - parsed[option_name] = parsed[option_name] or {} - table.insert(parsed[option_name],check) - else - parsed[option_name] = check - end - i = i + 1 + (option.consumes or 0) - end - --Set defaults for things that don't have them yet - for option_name, option in pairs(ret.options) do - if parsed[option_name] == nil then - if option.multiple and not option.default then - parsed[option_name] = {} - else - parsed[option_name] = option.default - end - end - end - return parsed -end - -ret.help = function() - print([=[ -mdoc - lua documentation -mdoc -p [-p ...][ -o ][ -t "title"][ -i ][ -d [ -d ...]][ -m ][ -h] - - -p | --path : Path to search for source files - -o | --output = "." : Folder to output HTML files to (and a cache folder) - -t | --title "name" = "Mdoc Generated Page" : Title for the html files - -i | --index : File to use for the index file - -d | --document : Path to search for files to put inder the References section - -m | --markup-parser : Executable to use to parse the descriptions and refrence documents. - Executable should accept a file path as it's argument, and generate html as it's output. - -h | --help : print this help -]=]) -end - -return ret diff --git a/page.etlua b/page.etlua deleted file mode 100644 index 47d9ad0..0000000 --- a/page.etlua +++ /dev/null @@ -1,97 +0,0 @@ - - - - <%- options.title %> - - - - - - <%- navbar %> -
- <% if header.data then %> - <%- header.data %> - <% else %> -
-

<%- header.name %>

- <% if header.short_desc then %> -

<%- header.short_desc %> - <% header.short_desc = nil %> - <% end %> - <% if header.desc then %> -

<%- header.desc or "" %> - <% header.desc = nil %> - <% end %> - <% if header.inherits then %> -

Inherits from - <% for _,name in pairs(header.inherits) do %> - <%- name %> - <% end %> - <% end %> -

- <% local funcorder = {} %> - <% for funcname,_ in pairs(header) do %> - <% table.insert(funcorder,funcname) %> - <% end %> - <% table.sort(funcorder) %> -
-

Contents

- - - - - - <% for _, funcname in ipairs(funcorder) do %> - <% local data = header[funcname] %> - <% if data and data.short_desc then %> - - <% if data.type == "function" or data.type == "method" then %> - - - <% else %> - - - <% end %> - - <% end %> - <% end %> -
NameDescription
<%- funcsig({funcname = funcname, func = header[funcname]}) %><%- data.short_desc %><%- data.name %><%- data.short_desc %>
-
-
- <% local func_fd = assert(io.open("func.etlua","r")) %> - <% local func_sec = assert(et.compile(func_fd:read("*a"))) %> - <% func_fd:close() %> - <% for _, funcname in ipairs(funcorder) do %> - <% local data = header[funcname] %> - <% print("Data:", data) %> - <% if data.type == "method" or data.type == "function" then %> - <% print("about to render...") %> - <%- assert(func_sec{ - name = funcname, - data = data - }) %> - <% print("done rendering...") %> -
- <% elseif data.type == "field" then %> - <% print("about to do field ") %> -

<%- funcname %>

-

<%- data.desc or "" %> -


- <% else %> - <% print("About to do error...") %> - <% if funcname ~= "name" and funcname ~= "type" then %> -

Unknown field type: <%- data and data.type or "" %> for <%- funcname %> -


- <% end %> - <% end %> - <% print("Next chunk...") %> - <% end %> - <% print("done with all chunks...") %> - <% end %> -
- - - - - diff --git a/share/func.etlua.lua b/share/func.etlua.lua new file mode 100644 index 0000000..f635851 --- /dev/null +++ b/share/func.etlua.lua @@ -0,0 +1,43 @@ +return [[ +
+

+ <%- name %>( + <% for n, param in pairs(data.params) do %> + <% if param.optional then %>[<% end %> + <% if n ~= 1 then %>,<% end %> + <%- param.type %> <%- param.name %> + <% if param.optional then %>]<% end %> + <% end %> + ) +

+

<%- data.short_desc %> +

<%- data.desc %> + <% if #data.params > 0 then %> +

Parameters
+
    + <% for _, param in pairs(data.params) do %> +
  • +

    <%- param.name %> + <% if param.optional then %>(optional)<% end %> + <% if param.type then %> + (<%- param.type %>) + <% end %> +

    <%- param.description %> +

  • + <% end %> +
+ <% end %> + <% if #data.returns > 0 then %> +
Returns
+
    + <% for _, ret in pairs(data.returns) do %> +
  • +

    <%- ret.type %> +

    <%- ret.description %> +

  • + <% end %> +
+ <% end %> +

Defined at <%- data.file %>:<%- data.line %> +

+]] diff --git a/share/funcsignature.etlua.lua b/share/funcsignature.etlua.lua new file mode 100644 index 0000000..de34880 --- /dev/null +++ b/share/funcsignature.etlua.lua @@ -0,0 +1,24 @@ +return [[ +<% assert(func, "Requires function to render a function signature") %> +<% assert(funcname, "Requires a function name to render a function signature") %> +<%- funcname %>( + <% if func.params and #func.params > 0 then %> + <% local optchain = false %> + <% for paramid, param in pairs(func.params) do %> + <% if paramid == 1 and param.optional then %> + [ + <% elseif param.optional then %> + [, + <% elseif paramid > 1 then %> + , + <% end %> + <%- param.name %> + <% if paramid == 1 and param.optional then %> + ] + <% elseif param.optional then %> + ] + <% end %> + <% end %> + <% end %> +) +]] diff --git a/share/index.etlua.lua b/share/index.etlua.lua new file mode 100644 index 0000000..9f07fff --- /dev/null +++ b/share/index.etlua.lua @@ -0,0 +1,19 @@ +return [[ + + + + + <%- options.title %> + + + + + + <%- navbar %> +
+ <%- text %> +
+ + +]] diff --git a/share/navbar.etlua.lua b/share/navbar.etlua.lua new file mode 100644 index 0000000..21192f7 --- /dev/null +++ b/share/navbar.etlua.lua @@ -0,0 +1,26 @@ +return [[ + +]] diff --git a/share/page.etlua.lua b/share/page.etlua.lua new file mode 100644 index 0000000..6e359e6 --- /dev/null +++ b/share/page.etlua.lua @@ -0,0 +1,91 @@ +return [[ + + + + <%- options.title %> + + + + + + <%- navbar %> +
+ <% if header.data_file then %> + <% local reffd = io.open(header.data_file,"r") %> +

<%- reffd:read("*a") %> + <% reffd:close() %> + <% else %> +

+

<%- header.name %>

+ <% if header.short_desc then %> +

<%- header.short_desc %> + <% header.short_desc = nil %> + <% end %> + <% if header.desc then %> +

<%- header.desc or "" %> + <% header.desc = nil %> + <% end %> + <% if header.inherits then %> +

Inherits from + <% for _,name in pairs(header.inherits) do %> + <%- name %> + <% end %> + <% end %> +

+ <% local funcorder = {} %> + <% for funcname,_ in pairs(header) do %> + <% table.insert(funcorder,funcname) %> + <% end %> + <% table.sort(funcorder) %> +
+

Contents

+ + + + + + <% for _, funcname in ipairs(funcorder) do %> + <% local data = header[funcname] %> + <% if data and data.short_desc then %> + + <% if data.type == "function" or data.type == "method" then %> + + + <% else %> + + + <% end %> + + <% end %> + <% end %> +
NameDescription
<%- funcsig({funcname = funcname, func = header[funcname]}) %><%- data.short_desc %><%- data.name %><%- data.short_desc %>
+
+
+ <% local func_sec = assert(et.compile(require("mdoc.files.func"))) %> + <% for _, funcname in ipairs(funcorder) do %> + <% local data = header[funcname] %> + <% if data.type == "method" or data.type == "function" then %> + <%- assert(func_sec{ + name = funcname, + data = data + }) %> +
+ <% elseif data.type == "field" then %> +

<%- funcname %>

+

<%- data.desc or "" %> +


+ <% else %> + <% if funcname ~= "name" and funcname ~= "type" then %> +

Unknown field type: <%- data and data.type or "" %> for <%- funcname %> +

<%- tostring(data) %> +

<%- tostring(header) %> +


+ <% end %> + <% end %> + <% end %> + <% end %> +
+ + +]] diff --git a/share/style.css.lua b/share/style.css.lua new file mode 100644 index 0000000..88df99e --- /dev/null +++ b/share/style.css.lua @@ -0,0 +1,28 @@ +return [[ +nav{ + float:left; + padding:2em; + border-right:1px solid black; +} +article{ + margin: auto auto auto 20%; +} +a{ + text-decoration: none; +} +a:hover{ + text-decoration:underline; +} +nav>ol{ + list-style-type: none; +} +body{ + line-height:1.6; + font-size:18px; + color:#444; + padding:0 10px +} +h1,h2,h3{ + line-height:1.2 +} +]] diff --git a/src/ext.lua b/src/ext.lua new file mode 100644 index 0000000..1c05b9f --- /dev/null +++ b/src/ext.lua @@ -0,0 +1,38 @@ +--[[ +Extensions that don't belong anywhere else +]] + +-- Override tostring to display more info about the table +local old_tostring = tostring +local numtabs = 0 +local printed_tables = {} +function tostring(el) + if type(el) == "table" and printed_tables[el] == nil then + printed_tables[el] = true + numtabs = numtabs + 1 + local strbuilder = {"{"} + for k,v in pairs(el) do + strbuilder[#strbuilder + 1] = string.format("%s%s : %s", string.rep("\t",numtabs), tostring(k), tostring(v)) + end + printed_tables[el] = nil + strbuilder[#strbuilder + 1] = string.rep("\t",numtabs - 1) .. "}" + numtabs = numtabs - 1 + return table.concat(strbuilder,"\n") + end + return old_tostring(el) +end + +--functions to save my hand +function assertf(bool,msg,...) + if not bool then + error(string.format(msg,...),2) + end +end + +function errorf(fmt,...) + error(string.format(fmt,...),2) +end + +function printf(fmt,...) + print(string.format(fmt,...)) +end diff --git a/src/init.lua b/src/init.lua new file mode 100644 index 0000000..0d30148 --- /dev/null +++ b/src/init.lua @@ -0,0 +1,569 @@ +--[[ +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 lfs = require("lfs") +local et = require("etlua") +require("mdoc.ext") +local opt = require("mdoc.opt") + +local args = {...} +local options = opt.parse_options(args) +if options.help then + print(opt.help()) + return +end +if not options.paths or #options.paths == 0 then + print(opt.help()) + return +end + +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 + 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 or options.nocache 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 or options.nocache 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 or options.nocache 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 or options.nocache 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("../share/navbar.etlua","r")) +local navbar = assert(et.compile(require("mdoc.files.navbar"))) +local navbarhtml = navbar{headers = headers} +--local pagefd = assert(io.open("../share/page.etlua","r")) +local page = assert(et.compile(require("mdoc.files.page"))) +--local funcsignaturefd = assert(io.open("../share/funcsignature.etlua","r")) +local funcsignature = assert(et.compile(require("mdoc.files.funcsignature"))) +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("../share/index.etlua","r")) + local index = assert(et.compile(require("mdoc.files.index"))) + 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 = assert(require("mdoc.files.style")) +local css_out = assert(io.open(options.output .. "/style.css","w")) +css_out:write(css) +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) diff --git a/src/opt_parser.lua b/src/opt_parser.lua new file mode 100644 index 0000000..5c3b28d --- /dev/null +++ b/src/opt_parser.lua @@ -0,0 +1,191 @@ + +local lfs = require("lfs") +require("mdoc.ext") +local ret = {} + +ret.options = { + paths = { + type = "folder", + multiple = true, + short = "-p", + long = "--path", + consumes = 1, + }, + output = { + type = "folder", + short = "-o", + long = "--output", + consumes = 1, + default = ".", + }, + title = { + type = "string", + short = "-t", + long = "--tile", + consumes = 1, + default = "Mdoc Generated Page", + }, + index = { + type = "file", + short = "-i", + long = "--index", + consumes = 1, + }, + document_paths = { + type = "folder", + multiple = true, + short = "-d", + long = "--document", + consumes = 1, + }, + parser = { + type = "executable", + short = "-m", + long = "--markup-parser", + consumes = 1, + default = "markdown", + }, + nocache = { + type = "flag", + short = "-c", + long = "--no-cache", + }, + --append = { + --type = "file", + --short = "-a", + --long = "--append", + --consumes = 1, + --multiple = true, + --}, + verbose = { + type = "flag", + short = "-v", + long = "--verbose" + }, + help = { + type = "flag", + short = "-h", + long = "--help", + } +} + +local function check_file_type(path, type) + local attr, err = lfs.attributes(path) + if attr == nil then + return attr, err + end + if attr.mode == type then + return path + else + return false, string.format("%s was not a folder, it was a %s",path, attr.mode) + end +end + +ret.check_flag = function(...) + return true +end + +ret.check_folder = function(path) + if path:match("/$") then + path = path:sub(1,-2) + end + return check_file_type(path,"directory") +end + +ret.check_string = function(str) + if type(str) == "string" then + return str + else + return false, string.format("%s was not a string, it was a %s",tostring(str), type(str)) + end +end + +ret.check_file = function(path) + return check_file_type(path,"file") +end + +ret.check_executable = function(name) + local tmpname = "./" .. os.tmpname() + local pd = assert(io.popen(name .. " > " .. tmpname, "w")) + pd:write("Hello, world!") + pd:close() + local id = assert(io.open(tmpname,"r")) + local dat = id:read("*a") + id:close() + assert(os.remove(tmpname)) + if string.len(dat) > 0 then + return name + else + return false, string.format("Tried to execute %q, but it did not create any data with the input 'Hello, world!', are you sure it's an executale on your PATH?", name) + end +end + +local option_lookup = {} +for k,v in pairs(ret.options) do + if v.short then + option_lookup[v.short] = k + end + if v.long then + option_lookup[v.long] = k + end +end + +ret.parse_options = function(args) + local parsed = {} + local i = 1 + while i <= #args do + local option_name = option_lookup[args[i]] + if not option_name then + errorf("Unknown option #%d: %q",i, args[i]) + end + local option = ret.options[option_name] + assertf(#args > i + (option.consumes or 0) - 1, "Option #%d (%q) consumes %d arguments, but found end of arguments",i,args[i],option.consumes) + local args_for_option = {} + for j = i, i+(option.consumes or 0) do + table.insert(args_for_option,args[j+1]) + end + --special, if we only consume 1 option, just pass that. + if option.consumes == 1 then + args_for_option = args_for_option[1] + end + local check = assert(ret["check_" .. option.type](args_for_option)) + if option.multiple then + parsed[option_name] = parsed[option_name] or {} + table.insert(parsed[option_name],check) + else + parsed[option_name] = check + end + i = i + 1 + (option.consumes or 0) + end + --Set defaults for things that don't have them yet + for option_name, option in pairs(ret.options) do + if parsed[option_name] == nil then + if option.multiple and not option.default then + parsed[option_name] = {} + else + parsed[option_name] = option.default + end + end + end + return parsed +end + +ret.help = function() + print([=[ +mdoc - lua documentation +mdoc -p [-p ...][ -o ][ -t "title"][ -i ][ -d [ -d ...]][ -m ][ -h] + + -p | --path : Path to search for source files + -o | --output = "." : Folder to output HTML files to (and a cache folder) + -t | --title "name" = "Mdoc Generated Page" : Title for the html files + -i | --index : File to use for the index file + -d | --document : Path to search for files to put inder the References section + -m | --markup-parser : Executable to use to parse the descriptions and refrence documents. + Executable should accept a file path as it's argument, and generate html as it's output. + -h | --help : print this help + -v | --verbose : print extra information during run + -c | --no-cache : rebuild files, even if they're not out of date. +]=]) +end + +return ret diff --git a/style.css b/style.css deleted file mode 100644 index ef54cec..0000000 --- a/style.css +++ /dev/null @@ -1,27 +0,0 @@ -nav{ - float:left; - padding:2em; - border-right:1px solid black; -} -article{ - margin: auto auto auto 20%; -} -a{ - text-decoration: none; -} -a:hover{ - text-decoration:underline; -} -nav>ol{ - list-style-type: none; -} -body{ - line-height:1.6; - font-size:18px; - color:#444; - padding:0 10px -} -h1,h2,h3{ - line-height:1.2 -} - -- cgit v1.2.3-70-g09d2