diff --git a/scripts/docgen.lua b/scripts/docgen.lua
new file mode 100644
index 0000000..0282733
--- /dev/null
+++ b/scripts/docgen.lua
@@ -0,0 +1,455 @@
+local _tl_compat; if (tonumber((_VERSION or ''):match('[%d.]*$')) or 0) < 5.3 then local p, m = true, require('compat53.module'); if p then _tl_compat = m end end; local assert = _tl_compat and _tl_compat.assert or assert; local io = _tl_compat and _tl_compat.io or io; local ipairs = _tl_compat and _tl_compat.ipairs or ipairs; local math = _tl_compat and _tl_compat.math or math; local pcall = _tl_compat and _tl_compat.pcall or pcall; local string = _tl_compat and _tl_compat.string or string; local table = _tl_compat and _tl_compat.table or table
+local ansi = require("cyan.ansi")
+local cs = require("cyan.colorstring")
+local fs = require("cyan.fs")
+local log = require("cyan.log")
+local util = require("cyan.util")
+local keys = util.tab.keys
+
+local info = log.create_logger(
+io.stdout,
+"normal",
+cs.highlight({ ansi.color.bright.cyan }, "Docgen"),
+cs.highlight({ ansi.color.bright.cyan }, "..."))
+
+
+local has_ltreesitter, ts = pcall(require, "ltreesitter")
+if not has_ltreesitter then
+ log.warn("docgen requires the ltreesitter module, which lua was unable to find\n", ts)
+ return
+end
+
+local has_teal_parser, teal_parser = pcall(ts.require, "teal")
+if not has_teal_parser then
+ log.warn("docgen requires tree-sitter-teal, which ltreesitter could not find:\n", teal_parser)
+ return
+end
+
+local template_file = fs.path.new("doc-template.html")
+local template = assert(fs.read(template_file:to_real_path()))
+
+local docfile = fs.path.new("docs/index.html")
+
+
+local function assertf(val, fmt, ...)
+ assert(val, fmt:format(...))
+end
+
+
+local function html(tags)
+ local flattened = {}
+ for _, v in ipairs(tags) do
+ if type(v) == "string" then
+ if #v > 0 then
+ table.insert(flattened, v)
+ end
+ else
+ table.insert(flattened, html(v))
+ end
+ end
+ return table.concat(flattened)
+end
+local function tag_wrapper(name)
+ return function(content, attrs)
+ if type(content) == "string" then
+ content = { content }
+ end
+ local start = { "<", name }
+ if attrs then
+ local attr_keys = util.tab.from(keys(attrs))
+ table.sort(attr_keys)
+ for _, key in ipairs(attr_keys) do
+ table.insert(start, (" %s=%s"):format(key, attrs[key]))
+ end
+ end
+ table.insert(start, ">")
+ table.insert(content, 1, start)
+ table.insert(content, "" .. name .. ">\n")
+ return content
+ end
+end
+local _h1 = tag_wrapper("h1")
+local h2 = tag_wrapper("h2")
+local h3 = tag_wrapper("h3")
+local _h4 = tag_wrapper("h4")
+local pre = tag_wrapper("pre")
+local p = tag_wrapper("p")
+local a = tag_wrapper("a")
+local br = "
"
+local function doc_header(content, name)
+ return h3(a({ "", content, "
" }, { name = name }))
+end
+
+local emit = setmetatable({}, {
+ __newindex = function(self, name, emitter)
+
+ rawset(self, name, function(prefix, n, out)
+ assertf(prefix, "nil prefix for emitter %q", name)
+ assertf(n, "nil node for emitter %q", name)
+ assertf(out, "nil output for emitter %q", name)
+ assertf(n:type() == name, "Wrong node type (%q) for emitter %q", n:type(), name)
+
+ local obj_name = emitter(prefix, n, out)
+ assertf(obj_name, "Emitter %q did not return object name", name)
+ return obj_name
+ end)
+ end,
+ __index = function(_, name)
+ error(("No emitter for node %q"):format(name))
+ end,
+})
+
+local function escape(str)
+ return (str:gsub("\n\n", br):
+ gsub("`(.-)`", "%1
"):
+ gsub("([\"'])([^%1]-)%1", "%1%2%1
"))
+end
+
+local html_escape_map = {
+ ["<"] = "<",
+ [">"] = ">",
+ ["'"] = "'",
+ ['"'] = """,
+ ['&'] = "&",
+}
+
+local function escape_html_chars(str)
+ return str:gsub("[<>'\"&]", html_escape_map)
+end
+
+emit["function_statement"] = function(prefix, n, out)
+ local sig = n:child_by_field_name("signature")
+ local ret = sig:child_by_field_name("return_type")
+ local typeargs = sig:child_by_field_name("typeargs")
+ local name = n:child_by_field_name("name"):source()
+
+ table.insert(
+ out,
+ html({
+ doc_header({ name,
+typeargs and escape_html_chars(typeargs:source()) or "",
+sig:child_by_field_name("arguments"):source(),
+(ret and ": " .. ret:source() or ""), },
+ name),
+ p({ escape(table.concat(prefix)) }),
+ }))
+
+ return name
+end
+
+emit["enum_declaration"] = function(prefix, n, out)
+ local body = n:child_by_field_name("enum_body")
+ assertf(body, "enum_body is nil for %s", tostring(n));
+
+ local name = n:child_by_field_name("name"):source()
+ local entries = {}
+
+ for child in body:named_children() do
+ table.insert(entries, child:source())
+ end
+
+
+ table.insert(
+ out,
+ html({
+ doc_header({ "type ", name }, name),
+ pre({ "enum ", name, br, " ",
+table.concat(entries, br .. " "),
+br,
+"end ", }),
+ p({ escape(table.concat(prefix)) }),
+ }))
+
+
+ return name
+end
+
+emit["record_declaration"] = function(prefix, n, out)
+ local fields = {}
+ local meta = {}
+ local body = n:child_by_field_name("record_body")
+ assertf(body, "record_body is nil for %s", tostring(n))
+
+ local private_fields = {}
+
+ for c in body:named_children() do
+ if c:type() == "field" then
+ local key = c:child_by_field_name("key")
+ local is_string = false
+ if not key then
+ key = c:child_by_field_name("string_key")
+ is_string = true
+ end
+
+
+ local is_private = key:source():sub(1, 1) == "_" or is_string and key:source():sub(2, 2) == "_"
+ local t = c:child_by_field_name("type")
+ table.insert(
+ is_private and private_fields or fields,
+ (is_string and "[%s]" or "%s"):format(key:source()) ..
+ ": " .. t:source())
+
+ elseif c:type() == "metamethod" then
+ table.insert(
+ meta,
+ c:source())
+
+ elseif c:type() == "record_array_type" then
+ table.insert(
+ fields,
+ 1,
+ "{" .. c:child(0):source() .. "}")
+
+ end
+ end
+
+ local obj_name = n:child_by_field_name("name"):source()
+
+ local pre_contents = { "record ", obj_name }
+
+ if #fields > 0 then
+ table.insert(pre_contents, br .. " " .. table.concat(fields, br .. " "))
+ end
+
+ if #meta > 0 then
+ if #fields > 0 then
+ table.insert(pre_contents, br)
+ end
+ table.insert(pre_contents, br .. " " .. table.concat(meta, br .. " "))
+ end
+
+ if #private_fields > 0 then
+ if #fields > 0 or #meta > 0 then
+ table.insert(pre_contents, br)
+ end
+ table.insert(pre_contents, br .. " -- private fields
" .. br .. " ")
+ table.insert(pre_contents, table.concat(private_fields, br .. " "))
+ table.insert(pre_contents, "
") + elseif current_state == "code" then + ins("
") + end + current_state = nil + elseif current_state then + error("Attempt to use @@" .. sub .. " inside of @@" .. current_state) + else + current_state = sub + if current_state == "table" then + ins("
" .. col .. " | ") + end + table.insert(row, "
---|
" .. escape_html_chars(col) .. " | ") + end + table.insert(row, "") + rows = 0 + end + end +end + +table.insert(table_of_contents, " | ") +table.sort(output) + +local final_table_of_contents = table.concat(table_of_contents) +local final_output = table.concat(output) + +local final_blob = template:gsub( +"", +function(match) + if match == "TABLE OF CONTENTS" then + return final_table_of_contents + elseif match == "CONTENTS" then + return final_output + end + + return match +end) + + +local fh = assert(io.open(docfile:to_real_path(), "w")) +fh:write(final_blob) +fh:close() +info("Wrote ", cs.highlight(cs.colors.file, docfile:to_real_path())) diff --git a/tlconfig.lua b/tlconfig.lua index 44da0ed..cc8d1bb 100644 --- a/tlconfig.lua +++ b/tlconfig.lua @@ -11,7 +11,7 @@ return { build = { post = { "scripts/gen_rockspec.tl", - "scripts/docgen.tl", + "scripts/docgen.lua", }, }, },