ADDED cli.lua Index: cli.lua ================================================================== --- cli.lua +++ cli.lua @@ -0,0 +1,206 @@ +local ct = require 'cortav' +local ss = require 'sirsem' + +local default_mode = { + ['render:format'] = 'html'; + ['html:gen-styles'] = true; +} + +local function +kmap(fn, list) + local new = {} + for k, v in pairs(list) do + local nk,nv = fn(k,v) + new[nk or k] = nv or v + end + return new +end + +local function +kfilter(list, fn) + local new = {} + for k, v in pairs(list) do + if fn(k,v) then new[k] = v end + end + return new +end + +local function +main(input, output, log, mode, vars) + local doc = ct.parse(input.stream, input.src, mode) + input.stream:close() + if mode['parse:show-tree'] then + log:write(dump(doc)) + end + + if not mode['render:format'] then + error 'what output format should i translate the input to?' + end + if mode['render:format'] == 'none' then return 0 end + if not ct.render[mode['render:format']] then + ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw() + end + + local render_opts = kmap(function(k,v) + return k:sub(2+#mode['render:format']) + end, kfilter(mode, function(m) + return ss.str.begins(m, mode['render:format']..':') + end)) + + doc.vars = vars + + -- this is kind of gross but the context object belongs to the parser, + -- not the renderer, so that's not a suitable place for this information + doc.stage = { + kind = 'render'; + format = mode['render:format']; + mode = mode; + } + + output:write(ct.render[mode['render:format']](doc, render_opts)) + return 0 +end + +local inp,outp,log = io.stdin, io.stdout, io.stderr + +local function entry_cli() + local mode, vars, input = default_mode, {}, { + stream = inp; + src = { + file = '(stdin)'; + } + } + + local optnparams = function(o) + local param_opts = { + out = 1; + log = 1; + define = 2; -- key value + ['mode-set'] = 1; + ['mode-clear'] = 1; + mode = 2; + } + return param_opts[o] or 0 + end + + local optmap = { + o = 'out'; + l = 'log'; + d = 'define'; + V = 'version'; + h = 'help'; + y = 'mode-set', n = 'mode-clear'; + m = 'mode'; + } + + local checkmodekey = function(key) + if not key:match '[^:]+:.+' then + ct.exns.cli('invalid mode key %s', key):throw() + end + return key + end + local onswitch = { + out = function(file) + local nf = io.open(file,'wb') + if nf then outp:close() outp = nf else + ct.exns.io('could not open output file for writing', 'open',file):throw() + end + end; + log = function(file) + local nf = io.open(file,'wb') + if nf then log:close() log = nf else + ct.exns.io('could not open log file for writing', 'open',file):throw() + end + end; + define = function(key,value) + if ss.str.begins(key, 'cortav.') or ss.str.begins(key, 'env.') then + ct.exns.cli 'cannot define variable in restricted namespace':throw() + end + vars[key] = value + end; + mode = function(key,value) mode[checkmodekey(key)] = value end; + ['mode-set'] = function(key) mode[checkmodekey(key)] = true end; + ['mode-clear'] = function(key) mode[checkmodekey(key)] = false end; + } + + local args = {} + local keepParsing = true + do local i = 1 while i <= #arg do local v = arg[i] + local execLongOpt = function(longopt) + if not onswitch[longopt] then + ct.exns.cli('switch --%s unrecognized', longopt):throw() + end + local nargs = optnparams(longopt) + + if nargs > 1 then + if i + nargs > #arg then + ct.exns.cli('not enough arguments for switch --%s (%u expected)', longopt, nargs):throw() + end + local nt = {} + for j = i+1, i+nargs do + table.insert(nt, arg[j]) + end + onswitch[longopt](table.unpack(nt)) + elseif nargs == 1 then + onswitch[longopt](arg[i+1]) + end + i = i + nargs + end + if v == '--' then + keepParsing = false + else + local longopt = v:match '^%-%-(.+)$' + if keepParsing and longopt then + execLongOpt(longopt) + else + if keepParsing and v:sub(1,1) == '-' then + for c,p in ss.str.enc.utf8.each(v:sub(2)) do + if optmap[c] then + execLongOpt(optmap[c]) + else + ct.exns.cli('switch -%i unrecognized', c):throw() + end + end + else + table.insert(args, v) + end + end + + end + i = i + 1 end end + + if args[1] and args[1] ~= '' then + local file = io.open(arg[1], "rb") + if not file then error('unable to load file ' .. args[1]) end + input.stream = file + input.src.file = args[1] + end + + return main(input, outp, log, mode, vars) +end + +local ok, e = pcall(entry_cli) +-- local ok, e = true, entry_cli() +if not ok then + local str = 'translation failure' + if ss.exn.is(e) then + str = e.kind.desc + end + local color = false + if log:seek() == nil then + -- this is not a very reliable heuristic for detecting + -- attachment to a tty but it's better than nothing + if os.getenv('COLORTERM') then + color = true + else + local term = os.getenv('TERM') + if term:find 'color' then color = true end + end + end + if color then + str = string.format('\27[1;31m%s\27[m', str) + end + log:write(string.format('%s: %s\n', str, e)) + os.exit(1) +end +os.exit(e) Index: cortav.ct ================================================================== --- cortav.ct +++ cortav.ct @@ -224,12 +224,43 @@ ~~~ # reference implementation the cortav standard is implemented in [$cortav.lua], found in this repository. only the way [$cortav.lua] interprets the cortav language is defined as a reference implementation; other behaviors are simply how [$cortav.lua] implements the specification and may be copied, ignored, tweaked, violently assaulted, or used as inspiration by a compliant parser. -## invocation -[$cortav.lua] is operated from the command line, either with the command [$lua cortav.lua] or by first compiling it to bytecode; a makefile for producing a "bytecode binary" that can be executed like a normal executable is included in the repository. henceforth it will be assumed you are using the compiled form; if you are instead running [$cortav.lua] directly as an interpreted script, just replace [$$ cortav] with [$$ lua cortav.lua] in incantations. +the reference implementation can be used both as a lua library and from the command line. [$cortav.lua] contains the parser and renderers, [$ext/*] contain various extensions, [$sirsem.lua] contains utility functions, and [$cli.lua] contains the CLI driver. + +## lua library +there are various ways to use cortav from a lua script; the simplest however is probably to precompile your script with luac and link in the necessary components of the implementation. for instance, say we have the following program + +~~~ stdin2html.lua [lua] ~~~ +local ct = require 'cortav' +local mode = {} +local doc = ct.parse(io.stdin, {file = '(stdin)'}, mode) +doc.stage = { + kind = 'render'; + format = 'html'; + mode = mode; +} +output:write(ct.render.html(doc, {accent = '320'})) +~~~ + +and the only extension we need is the table-of-contents extension. our script can be translated into a self-contained lua bytecode blob with the following command + +~~~ +$ luac -s -o stdin2html.lc $cortav_repo/{sirsem,cortav,ext/toc}.lua stdin2html.lua +~~~ + +and can then be operated with the command [$lua stdin2html.lc], with no further need for the cortav repository files. note that the order of the [$luac] command is important! [$sirsem.lua] must come first, followed by [$cortav.lua], followed by any extensions. your driver script (i.e. the script with the entry point into the application) should always come last. + +## command line driver +the [$cortav.lua] command line driver can be run from the repository directory with the command [$lua ./cli.lua], or by first compiling it into a bytecode form that links in all its dependencies. this is the preferred method for installation, as it produces a self-contained executable which loads more quickly, but running the driver in script form may be desirable for development or debugging. + +the repository contains a GNU makefile to automate compilation of the reference implementation on unix-like OSes. simply run [$$ make cortav] or [$$ gmake cortav] from the repository root to produce a self-contained bytecode executable that can be installed anywhere on your filesystem, with no dependencies other than the lua interpreter. + +! note that the makefile strips debugging symbols to save space, so running [$cli.lua] directly as a script may be helpful if you encounter errors and need stacktraces or other debugging information. + +henceforth it will be assumed that you have produced the [$cortav] executable and placed it somewhere in your [$$PATH]; if you are instead running [$cortav.lua] directly as an interpreted script, you'll need to replace [$$ cortav] with [$$ lua ./cli.lua] in incantations. when run without commands, [$cortav.lua] will read input from standard input and write to standard output. alternately, a source file can be given as an argument. to write to a specific file instead of the standard output stream, use the [$-o [!file]] flag. ~~~ $ cortav readme.ct -o readme.html Index: cortav.lua ================================================================== --- cortav.lua +++ cortav.lua @@ -1,195 +1,34 @@ -- [ʞ] cortav.lua -- ~ lexi hale -- © AGPLv3 -- ? reference implementation of the cortav document language -local ct = { render = {} } - -local function hexdump(s) - local hexlines, charlines = {},{} - for i=1,#s do - local line = math.floor((i-1)/16) + 1 - hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i)) - charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' ' - end - local str = '' - for i=1,#hexlines do - str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n' - end - return str -end - -local function dump(o, state, path, depth) - state = state or {tbls = {}} - depth = depth or 0 - local pfx = string.rep(' ', depth) - if type(o) == "table" then - local str = '' - for k,p in pairs(o) do - local done = false - local exp - if type(p) == 'table' then - if state.tbls[p] then - exp = '<' .. state.tbls[p] ..'>' - done = true - else - state.tbls[p] = path and string.format('%s.%s', path, k) or k - end - end - if not done then - local function dodump() return dump( - p, state, - path and string.format("%s.%s", path, k) or k, - depth + 1 - ) end - -- boy this is ugly - if type(p) ~= 'table' or - getmetatable(p) == nil or - getmetatable(p).__tostring == nil then - exp = dodump() - end - if type(p) == 'table' then - exp = string.format('{\n%s%s}', exp, pfx) - local meta = getmetatable(p) - if meta then - if meta.__tostring then - exp = tostring(p) - end - if meta.__name then - exp = meta.__name .. ' ' .. exp - end - end - end - end - str = str .. pfx .. string.format("%s = %s\n", k, exp) - end - return str - elseif type(o) == "string" then - return string.format('“%s”', o) - else - return tostring(o) - end -end - -local function -lerp(t, a, b) - return (1-t)*a + (t*b) -end - -local function -startswith(str, pfx) - return string.sub(str, 1, #pfx) == pfx -end - -local function declare(c) - local cls = setmetatable({ - __name = c.ident; - }, { - __name = 'class'; - __tostring = function() return c.ident or '(class)' end; - }) - - cls.__call = c.call - cls.__index = function(self, k) - if c.default and c.default[k] then - return c.default[k] - end - if k == 'clone' then - return function(self) - local new = cls.mk() - for k,v in pairs(self) do - new[k] = v - end - if c.clonesetup then - c.clonesetup(new, self) - end - return new - end - elseif k == 'to' then - return function(self, to, ...) - if to == 'string' then return tostring(self) - elseif to == 'number' then return tonumber(self) - elseif to == 'int' then return math.floor(tonumber(self)) - elseif c.cast and c.cast[to] then - return c.cast[to](self, ...) - elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then - else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end - end - end - if c.fns then return c.fns[k] end - end - - if c.cast then - if c.cast.string then - cls.__tostring = c.cast.string - end - if c.cast.number then - cls.__tonumber = c.cast.number - end - end - - cls.mk = function(...) - local val = setmetatable(c.mk and c.mk(...) or {}, cls) - if c.init then - for k,v in pairs(c.init) do - val[k] = v - end - end - if c.construct then - c.construct(val, ...) - end - return val - end - getmetatable(cls).__call = function(_, ...) return cls.mk(...) end - cls.is = function(o) return getmetatable(o) == cls end - return cls -end -ct.exn = declare { - ident = 'exn'; - mk = function(kind, ...) - return { - vars = {...}; - kind = kind; - } - end; - cast = { - string = function(me) - return me.kind.report(table.unpack(me.vars)) - end; - }; - fns = { - throw = function(me) error(me) end; - } -} -ct.exnkind = declare { - ident = 'exn-kind'; - mk = function(desc, report) - return { - desc = desc; - report = report or function(msg,...) - return string.format(msg,...) - end; - } - end; - call = function(me, ...) - return ct.exn(me, ...) - end; -} +local ss = require 'sirsem' +-- aliases for commonly used sirsem funcs +local startswith = ss.str.begins +local eachcode = ss.str.enc.utf8.each +local dump = ss.dump +local declare = ss.declare + +-- make this module available to require() when linked into a lua bytecode program with luac +local ct = ss.namespace 'cortav' +ct.render = {} ct.exns = { - tx = ct.exnkind('translation error', function(msg,...) + tx = ss.exnkind('translation error', function(msg,...) return string.format("(%s:%u) "..msg, ...) end); - io = ct.exnkind('IO error', function(msg, ...) + io = ss.exnkind('IO error', function(msg, ...) return string.format("<%s %s> "..msg, ...) end); - cli = ct.exnkind 'command line parse error'; - mode = ct.exnkind('bad mode', function(msg, ...) + cli = ss.exnkind 'command line parse error'; + mode = ss.exnkind('bad mode', function(msg, ...) return string.format("mode “%s” "..msg, ...) end); - unimpl = ct.exnkind 'feature not implemented'; + unimpl = ss.exnkind 'feature not implemented'; + ext = ss.exnkind 'extension error'; } ct.ctx = declare { mk = function(src) return {src = src} end; ident = 'context'; @@ -308,29 +147,30 @@ meta = {}; vars = {}; } end; } -local function map(fn, lst) - local new = {} - for k,v in pairs(lst) do - table.insert(new, fn(v,k)) - end - return new -end -local function reduce(fn, acc, lst) - for i,v in ipairs(lst) do - acc = fn(acc, v, i) - end - return acc -end +-- FP helper functions + local function fmtfn(str) return function(...) return string.format(str, ...) end end +ct.ext = { loaded = {} } +function ct.ext.install(ext) + if not ext.id then + ct.exns.ext 'extension missing “id” field':throw() + end + if ct.ext.loaded[ext.id] then + ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw() + end + ct.ext.loaded[ext.id] = ext +end + +-- renderer engines function ct.render.html(doc, opts) local doctitle = opts['title'] local f = string.format local ids = {} local canonicalID = {} @@ -630,11 +470,11 @@ end return tag('table',nil,catenate(tb)) end; listing = function(b,s) stylesNeeded.block_code_listing = true - local nodes = map(function(l) + local nodes = ss.map(function(l) if #l > 0 then return tag('div',nil,sr.htmlSpan(l, b, s)) else return elt('hr') end @@ -659,12 +499,12 @@ return r end local elt = function(t,attrs) return f('<%s%s>', t, - attrs and reduce(function(a,b) return a..b end, '', - map(function(v,k) + attrs and ss.reduce(function(a,b) return a..b end, '', + ss.map(function(v,k) if v == true then return ' '..k elseif v then return f(' %s="%s"', k, v) end end, attrs)) or '') @@ -838,11 +678,11 @@ local l, sep, sat for i=1,3 do -- 🙄 l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$') if l then break end end - l = lerp(tonumber(l), tbg, tfg) + l = ss.math.lerp(tonumber(l), tbg, tfg) return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha)) end end css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace) css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end) @@ -889,34 +729,10 @@ else return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) end end -local function eachcode(str, ascode) - local pos = { - code = 1; - byte = 1; - } - return function() - if pos.byte > #str then return nil end - local thischar = utf8.codepoint(str, pos.byte) - local lastpos = { - code = pos.code; - byte = pos.byte; - next = pos; - } - if not ascode then - thischar = utf8.char(thischar) - pos.byte = pos.byte + #thischar - else - pos.byte = pos.byte + #utf8.char(thischar) - end - pos.code = pos.code + 1 - return thischar, lastpos - end -end - do -- define span control sequences local function formatter(sty) return function(s,c) return { kind = 'format'; @@ -948,13 +764,14 @@ origin = c:clone(); } end end ct.spanctls = { - {seq = '$', parse = formatter 'literal'}; {seq = '!', parse = formatter 'emph'}; {seq = '*', parse = formatter 'strong'}; + {seq = '~', parse = formatter 'strike'}; + {seq = '+', parse = formatter 'inser'}; {seq = '\\', parse = function(s, c) -- raw return s end}; {seq = '$\\', parse = function(s, c) -- raw return { @@ -962,10 +779,11 @@ style = 'literal'; spans = {s}; origin = c:clone(); } end}; + {seq = '$', parse = formatter 'literal'}; {seq = '&', parse = function(s, c) local r, t = s:match '^([^%s]+)%s*(.-)$' return { kind = 'term'; spans = (t and t ~= "") and ct.parse_span(t, c) or {}; @@ -990,29 +808,14 @@ } end function ct.parse_span(str,ctx) local function delimited(start, stop, s) - local depth = 0 - if not startswith(s, start) then return nil end - for c,p in eachcode(s) do - if c == '\\' then - p.next.byte = p.next.byte + #utf8.char(utf8.codepoint(s, p.next.byte)) - p.next.code = p.next.code + 1 - elseif c == start then - depth = depth + 1 - elseif c == stop then - depth = depth - 1 - if depth == 0 then - return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME - elseif depth < 0 then - ctx:fail('out of place %s', stop) - end - end - end - - ctx:fail('[%s] expected before end of line', stop) + -- local r = { pcall(ss.str.delimit, 'utf8', start, stop, s) } + -- if r[1] then return table.unpack(r, 2) end + -- ctx:fail(tostring(e)) + return ss.str.delimit(ss.str.enc.utf8, start, stop, s) end local buf = "" local spans = {} local function flush() if buf ~= "" then @@ -1079,11 +882,10 @@ end end flush() return spans end - local function blockwrap(fn) return function(l,c) local block = fn(l,c) @@ -1168,11 +970,14 @@ local function insert_table_row(l,c) local row = {} local buf local flush = function() - if buf then table.insert(row, buf) end + if buf then + buf.str = buf.str:gsub('%s+$','') + table.insert(row, buf) + end buf = { str = '' } end for c,p in eachcode(l) do if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then flush() @@ -1385,213 +1190,5 @@ end end return ctx.doc end - -local default_mode = { - ['render:format'] = 'html'; - ['html:gen-styles'] = true; -} - -local function filter(list, fn) - local new = {} - for i, v in ipairs(list) do - if fn(v,i) then table.insert(new, v) end - end - return new -end - -local function kmap(fn, list) - local new = {} - for k, v in pairs(list) do - local nk,nv = fn(k,v) - new[nk or k] = nv or v - end - return new -end -local function kfilter(list, fn) - local new = {} - for k, v in pairs(list) do - if fn(k,v) then new[k] = v end - end - return new -end - -local function main(input, output, log, mode, vars) - local doc = ct.parse(input.stream, input.src, mode) - input.stream:close() - if mode['parse:show-tree'] then - log:write(dump(doc)) - end - - if not mode['render:format'] then - error 'what output format should i translate the input to?' - end - if mode['render:format'] == 'none' then return 0 end - if not ct.render[mode['render:format']] then - ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw() - end - - local render_opts = kmap(function(k,v) - return k:sub(2+#mode['render:format']) - end, kfilter(mode, function(m) - return startswith(m, mode['render:format']..':') - end)) - - doc.vars = vars - - -- this is kind of gross but the context object belongs to the parser, - -- not the renderer, so that's not a suitable place for this information - doc.stage = { - kind = 'render'; - format = mode['render:format']; - mode = mode; - } - - output:write(ct.render[mode['render:format']](doc, render_opts)) - return 0 -end - -local inp,outp,log = io.stdin, io.stdout, io.stderr - -local function entry_cli() - local mode, vars, input = default_mode, {}, { - stream = inp; - src = { - file = '(stdin)'; - } - } - - local optnparams = function(o) - local param_opts = { - out = 1; - log = 1; - define = 2; -- key value - ['mode-set'] = 1; - ['mode-clear'] = 1; - mode = 2; - } - return param_opts[o] or 0 - end - - local optmap = { - o = 'out'; - l = 'log'; - d = 'define'; - V = 'version'; - h = 'help'; - y = 'mode-set', n = 'mode-clear'; - m = 'mode'; - } - - local checkmodekey = function(key) - if not key:match '[^:]+:.+' then - ct.exns.cli('invalid mode key %s', key):throw() - end - return key - end - local onswitch = { - out = function(file) - local nf = io.open(file,'wb') - if nf then outp:close() outp = nf else - ct.exns.io('could not open output file for writing', 'open',file):throw() - end - end; - log = function(file) - local nf = io.open(file,'wb') - if nf then log:close() log = nf else - ct.exns.io('could not open log file for writing', 'open',file):throw() - end - end; - define = function(key,value) - if startswith(key, 'cortav.') or startswith(key, 'env.') then - ct.exns.cli 'cannot define variable in restricted namespace':throw() - end - vars[key] = value - end; - mode = function(key,value) mode[checkmodekey(key)] = value end; - ['mode-set'] = function(key) mode[checkmodekey(key)] = true end; - ['mode-clear'] = function(key) mode[checkmodekey(key)] = false end; - } - - local args = {} - local keepParsing = true - do local i = 1 while i <= #arg do local v = arg[i] - local execLongOpt = function(longopt) - if not onswitch[longopt] then - ct.exns.cli('switch --%s unrecognized', longopt):throw() - end - local nargs = optnparams(longopt) - - if nargs > 1 then - if i + nargs > #arg then - ct.exns.cli('not enough arguments for switch --%s (%u expected)', longopt, nargs):throw() - end - local nt = {} - for j = i+1, i+nargs do - table.insert(nt, arg[j]) - end - onswitch[longopt](table.unpack(nt)) - elseif nargs == 1 then - onswitch[longopt](arg[i+1]) - end - i = i + nargs - end - if v == '--' then - keepParsing = false - else - local longopt = v:match '^%-%-(.+)$' - if keepParsing and longopt then - execLongOpt(longopt) - else - if keepParsing and v:sub(1,1) == '-' then - for c,p in eachcode(v:sub(2)) do - if optmap[c] then - execLongOpt(optmap[c]) - else - ct.exns.cli('switch -%i unrecognized', c):throw() - end - end - else - table.insert(args, v) - end - end - - end - i = i + 1 end end - - if args[1] and args[1] ~= '' then - local file = io.open(arg[1], "rb") - if not file then error('unable to load file ' .. args[1]) end - input.stream = file - input.src.file = args[1] - end - - return main(input, outp, log, mode, vars) -end - -local ok, e = pcall(entry_cli) --- local ok, e = true, entry_cli() -if not ok then - local str = 'translation failure' - if ct.exn.is(e) then - str = e.kind.desc - end - local color = false - if log:seek() == nil then - -- this is not a very reliable heuristic for detecting - -- attachment to a tty but it's better than nothing - if os.getenv('COLORTERM') then - color = true - else - local term = os.getenv('TERM') - if term:find 'color' then color = true end - end - end - if color then - str = string.format('\27[1;31m%s\27[m', str) - end - log:write(string.format('%s: %s\n', str, e)) - os.exit(1) -end -os.exit(e) ADDED ext/toc.lua Index: ext/toc.lua ================================================================== --- ext/toc.lua +++ ext/toc.lua @@ -0,0 +1,10 @@ +local ct = require 'cortav' +local ss = require 'sirsem' + +ct.ext.install { + id = 'toc'; + desc = 'provides a table of contents for HTML renderer plus generic fallback'; + directive = function(words) + + end; +} Index: makefile ================================================================== --- makefile +++ makefile @@ -1,7 +1,18 @@ lua != which lua luac != which luac -cortav: cortav.lua +extens ?= $(patsubst ext/%.lua,%,$(wildcard ext/*.lua)) +extens_srcs = $(patsubst %,ext/%.lua,$(extens)) + +cortav: sirsem.lua cortav.lua $(extens_srcs) cli.lua echo '#!$(lua)' > $@ - luac -s -o - $< >> $@ + luac -o - $^ >> $@ chmod +x $@ + +cortav.html: cortav.ct cortav + ./cortav $< -o $@ -m render:format html -y html:fossil-uv + +.PHONY: syncdoc +syncdoc: cortav.html + fossil uv add $< + fossil uv sync ADDED sirsem.lua Index: sirsem.lua ================================================================== --- sirsem.lua +++ sirsem.lua @@ -0,0 +1,274 @@ +-- [ʞ] sirsem.lua +-- ~ lexu hale +-- ? utility library with functionality common to +-- cortav.lua and its extensions +-- from Ranuir "software utility" +-- > local ss = require 'sirsem.lua' + +local ss +do -- pull ourselves up by our own bootstraps + local package = _G.package -- prevent namespace from being broken by env shenanigans + local function namespace(name, tbl) + local pkg = tbl or {} + if package then + package.loaded[name] = pkg + end + return pkg + end + ss = namespace 'sirsem' + ss.namespace = namespace +end + +function ss.map(fn, lst) + local new = {} + for k,v in pairs(lst) do + table.insert(new, fn(v,k)) + end + return new +end +function ss.reduce(fn, acc, lst) + for i,v in ipairs(lst) do + acc = fn(acc, v, i) + end + return acc +end +function ss.filter(list, fn) + local new = {} + for i, v in ipairs(list) do + if fn(v,i) then table.insert(new, v) end + end + return new +end + +ss.str = {} + +function ss.str.begins(str, pfx) + return string.sub(str, 1, #pfx) == pfx +end + +ss.str.enc = { + utf8 = { + char = utf8.char; + codepoint = utf8.codepoint; + }; + c6b = {}; + ascii = {}; +} + +function ss.str.enc.utf8.each(str, ascode) + local pos = { + code = 1; + byte = 1; + } + return function() + if pos.byte > #str then return nil end + local thischar = utf8.codepoint(str, pos.byte) + local lastpos = { + code = pos.code; + byte = pos.byte; + next = pos; + } + if not ascode then + thischar = utf8.char(thischar) + pos.byte = pos.byte + #thischar + else + pos.byte = pos.byte + #utf8.char(thischar) + end + pos.code = pos.code + 1 + return thischar, lastpos + end +end + +ss.math = {} + +function ss.math.lerp(t, a, b) + return (1-t)*a + (t*b) +end + +function ss.dump(o, state, path, depth) + state = state or {tbls = {}} + depth = depth or 0 + local pfx = string.rep(' ', depth) + if type(o) == "table" then + local str = '' + for k,p in pairs(o) do + local done = false + local exp + if type(p) == 'table' then + if state.tbls[p] then + exp = '<' .. state.tbls[p] ..'>' + done = true + else + state.tbls[p] = path and string.format('%s.%s', path, k) or k + end + end + if not done then + local function dodump() return dump( + p, state, + path and string.format("%s.%s", path, k) or k, + depth + 1 + ) end + -- boy this is ugly + if type(p) ~= 'table' or + getmetatable(p) == nil or + getmetatable(p).__tostring == nil then + exp = dodump() + end + if type(p) == 'table' then + exp = string.format('{\n%s%s}', exp, pfx) + local meta = getmetatable(p) + if meta then + if meta.__tostring then + exp = tostring(p) + end + if meta.__name then + exp = meta.__name .. ' ' .. exp + end + end + end + end + str = str .. pfx .. string.format("%s = %s\n", k, exp) + end + return str + elseif type(o) == "string" then + return string.format('“%s”', o) + else + return tostring(o) + end +end + +function ss.hexdump(s) + local hexlines, charlines = {},{} + for i=1,#s do + local line = math.floor((i-1)/16) + 1 + hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i)) + charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' ' + end + local str = '' + for i=1,#hexlines do + str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n' + end + return str +end + +function ss.declare(c) + local cls = setmetatable({ + __name = c.ident; + }, { + __name = 'class'; + __tostring = function() return c.ident or '(class)' end; + }) + + cls.__call = c.call + cls.__index = function(self, k) + if c.default and c.default[k] then + return c.default[k] + end + if k == 'clone' then + return function(self) + local new = cls.mk() + for k,v in pairs(self) do + new[k] = v + end + if c.clonesetup then + c.clonesetup(new, self) + end + return new + end + elseif k == 'to' then + return function(self, to, ...) + if to == 'string' then return tostring(self) + elseif to == 'number' then return tonumber(self) + elseif to == 'int' then return math.floor(tonumber(self)) + elseif c.cast and c.cast[to] then + return c.cast[to](self, ...) + elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then + else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end + end + end + if c.fns then return c.fns[k] end + end + + if c.cast then + if c.cast.string then + cls.__tostring = c.cast.string + end + if c.cast.number then + cls.__tonumber = c.cast.number + end + end + + cls.mk = function(...) + local val = setmetatable(c.mk and c.mk(...) or {}, cls) + if c.init then + for k,v in pairs(c.init) do + val[k] = v + end + end + if c.construct then + c.construct(val, ...) + end + return val + end + getmetatable(cls).__call = function(_, ...) return cls.mk(...) end + cls.is = function(o) return getmetatable(o) == cls end + return cls +end + +-- tidy exceptions + +ss.exn = ss.declare { + ident = 'exn'; + mk = function(kind, ...) + return { + vars = {...}; + kind = kind; + } + end; + cast = { + string = function(me) + return me.kind.report(table.unpack(me.vars)) + end; + }; + fns = { + throw = function(me) error(me) end; + } +} +ss.exnkind = ss.declare { + ident = 'exn-kind'; + mk = function(desc, report) + return { + desc = desc; + report = report or function(msg,...) + return string.format(msg,...) + end; + } + end; + call = function(me, ...) + return ss.exn(me, ...) + end; +} +ss.str.exn = ss.exnkind 'failure while string munging' + +function ss.str.delimit(encoding, start, stop, s) + local depth = 0 + encoding = encoding or ss.str.enc.utf8 + if not ss.str.begins(s, start) then return nil end + for c,p in encoding.each(s) do + if c == (encoding.escape or '\\') then + p.next.byte = p.next.byte + #encoding.char(encoding.codepoint(s, p.next.byte)) + p.next.code = p.next.code + 1 + elseif c == start then + depth = depth + 1 + elseif c == stop then + depth = depth - 1 + if depth == 0 then + return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME + elseif depth < 0 then + ss.str.exn('out of place %s', stop):throw() + end + end + end + + ss.str.exn('[%s] expected before end of line', stop):throw() +end