cortav  Check-in [9c67b0312c]

Overview
Comment:split cortav into modules, enable use as library, create extension mechanism stub, fix up docs
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 9c67b0312cf48cba2342fd07fed44011495309452736859527d0473f98429640
User & Date: lexi on 2021-12-20 00:09:46
Other Links: manifest | tags
Context
2021-12-20
00:14
error improvements, sirsem bug fix check-in: 709518a06e user: lexi tags: trunk
00:09
split cortav into modules, enable use as library, create extension mechanism stub, fix up docs check-in: 9c67b0312c user: lexi tags: trunk
2021-12-19
18:12
add rudimentary syntax hiliting for kate/kwrite/kdepart check-in: 87fed4ec34 user: lexi tags: trunk
Changes

Added cli.lua version [23a0968fc6].

            1  +local ct = require 'cortav'
            2  +local ss = require 'sirsem'
            3  +
            4  +local default_mode = {
            5  +	['render:format'] = 'html';
            6  +	['html:gen-styles'] = true;
            7  +}
            8  +
            9  +local function
           10  +kmap(fn, list)
           11  +	local new = {}
           12  +	for k, v in pairs(list) do
           13  +		local nk,nv = fn(k,v)
           14  +		new[nk or k] = nv or v
           15  +	end
           16  +	return new
           17  +end
           18  +
           19  +local function
           20  +kfilter(list, fn)
           21  +	local new = {}
           22  +	for k, v in pairs(list) do
           23  +		if fn(k,v) then new[k] = v end
           24  +	end
           25  +	return new
           26  +end
           27  +
           28  +local function
           29  +main(input, output, log, mode, vars)
           30  +	local doc = ct.parse(input.stream, input.src, mode)
           31  +	input.stream:close()
           32  +	if mode['parse:show-tree'] then
           33  +		log:write(dump(doc))
           34  +	end
           35  +
           36  +	if not mode['render:format'] then
           37  +		error 'what output format should i translate the input to?'
           38  +	end
           39  +	if mode['render:format'] == 'none' then return 0 end
           40  +	if not ct.render[mode['render:format']] then
           41  +		ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw()
           42  +	end
           43  +	
           44  +	local render_opts = kmap(function(k,v)
           45  +		return k:sub(2+#mode['render:format'])
           46  +	end, kfilter(mode, function(m)
           47  +		return ss.str.begins(m, mode['render:format']..':')
           48  +	end))
           49  +
           50  +	doc.vars = vars
           51  +	
           52  +	-- this is kind of gross but the context object belongs to the parser,
           53  +	-- not the renderer, so that's not a suitable place for this information
           54  +	doc.stage = {
           55  +		kind = 'render';
           56  +		format = mode['render:format'];
           57  +		mode = mode;
           58  +	}
           59  +
           60  +	output:write(ct.render[mode['render:format']](doc, render_opts))
           61  +	return 0
           62  +end
           63  +
           64  +local inp,outp,log = io.stdin, io.stdout, io.stderr
           65  +
           66  +local function entry_cli()
           67  +	local mode, vars, input = default_mode, {}, {
           68  +		stream = inp;
           69  +		src = {
           70  +			file = '(stdin)';
           71  +		}
           72  +	}
           73  +
           74  +	local optnparams = function(o)
           75  +		local param_opts = {
           76  +			out = 1;
           77  +			log = 1;
           78  +			define = 2; -- key value
           79  +			['mode-set'] = 1;
           80  +			['mode-clear'] = 1;
           81  +			mode = 2;
           82  +		}
           83  +		return param_opts[o] or 0
           84  +	end
           85  +
           86  +	local optmap = {
           87  +		o = 'out';
           88  +		l = 'log';
           89  +		d = 'define';
           90  +		V = 'version';
           91  +		h = 'help';
           92  +		y = 'mode-set', n = 'mode-clear';
           93  +		m = 'mode';
           94  +	}
           95  +
           96  +	local checkmodekey = function(key)
           97  +		if not key:match '[^:]+:.+' then
           98  +			ct.exns.cli('invalid mode key %s', key):throw()
           99  +		end
          100  +		return key
          101  +	end
          102  +	local onswitch = {
          103  +		out = function(file)
          104  +			local nf = io.open(file,'wb')
          105  +			if nf then outp:close() outp = nf else
          106  +				ct.exns.io('could not open output file for writing', 'open',file):throw()
          107  +			end
          108  +		end;
          109  +		log = function(file)
          110  +			local nf = io.open(file,'wb')
          111  +			if nf then log:close() log = nf else
          112  +				ct.exns.io('could not open log file for writing', 'open',file):throw()
          113  +			end
          114  +		end;
          115  +		define = function(key,value)
          116  +			if ss.str.begins(key, 'cortav.') or ss.str.begins(key, 'env.') then
          117  +				ct.exns.cli 'cannot define variable in restricted namespace':throw()
          118  +			end
          119  +			vars[key] = value
          120  +		end;
          121  +		mode = function(key,value) mode[checkmodekey(key)] = value end;
          122  +		['mode-set'] = function(key) mode[checkmodekey(key)] = true end;
          123  +		['mode-clear'] = function(key) mode[checkmodekey(key)] = false end;
          124  +	}
          125  +
          126  +	local args = {}
          127  +	local keepParsing = true
          128  +	do local i = 1 while i <= #arg do local v = arg[i]
          129  +		local execLongOpt = function(longopt)
          130  +			if not onswitch[longopt] then
          131  +				ct.exns.cli('switch --%s unrecognized', longopt):throw()
          132  +			end
          133  +			local nargs = optnparams(longopt)
          134  +
          135  +			if nargs > 1 then
          136  +				if i + nargs > #arg then
          137  +					ct.exns.cli('not enough arguments for switch --%s (%u expected)', longopt, nargs):throw()
          138  +				end
          139  +				local nt = {}
          140  +				for j = i+1, i+nargs do
          141  +					table.insert(nt, arg[j])
          142  +				end
          143  +				onswitch[longopt](table.unpack(nt))
          144  +			elseif nargs == 1 then
          145  +				onswitch[longopt](arg[i+1])
          146  +			end
          147  +			i = i + nargs
          148  +		end
          149  +		if v == '--' then
          150  +			keepParsing = false
          151  +		else
          152  +			local longopt = v:match '^%-%-(.+)$'
          153  +			if keepParsing and longopt then
          154  +				execLongOpt(longopt)
          155  +			else
          156  +				if keepParsing and v:sub(1,1) == '-' then
          157  +					for c,p in ss.str.enc.utf8.each(v:sub(2)) do
          158  +						if optmap[c] then
          159  +							execLongOpt(optmap[c])
          160  +						else
          161  +							ct.exns.cli('switch -%i unrecognized', c):throw()
          162  +						end
          163  +					end
          164  +				else
          165  +					table.insert(args, v)
          166  +				end
          167  +			end
          168  +
          169  +		end
          170  +	i = i + 1 end end
          171  +
          172  +	if args[1] and args[1] ~= '' then
          173  +		local file = io.open(arg[1], "rb")
          174  +		if not file then error('unable to load file ' .. args[1]) end
          175  +		input.stream = file
          176  +		input.src.file = args[1]
          177  +	end
          178  +
          179  +	return main(input, outp, log, mode, vars)
          180  +end
          181  +
          182  +local ok, e = pcall(entry_cli)
          183  +-- local ok, e = true, entry_cli()
          184  +if not ok then
          185  +	local str = 'translation failure'
          186  +	if ss.exn.is(e) then
          187  +		str = e.kind.desc
          188  +	end
          189  +	local color = false
          190  +	if log:seek() == nil then
          191  +		-- this is not a very reliable heuristic for detecting
          192  +		-- attachment to a tty but it's better than nothing
          193  +		if os.getenv('COLORTERM') then
          194  +			color = true
          195  +		else
          196  +			local term = os.getenv('TERM')
          197  +			if term:find 'color' then color = true end
          198  +		end
          199  +	end
          200  +	if color then
          201  +		str = string.format('\27[1;31m%s\27[m', str)
          202  +	end
          203  +	log:write(string.format('%s: %s\n', str, e))
          204  +	os.exit(1)
          205  +end
          206  +os.exit(e)

Modified cortav.ct from [fcb217abd6] to [96194d0b88].

   222    222   <A> …oh, [!fuck].
   223    223   (signal lost)
   224    224   ~~~
   225    225   
   226    226   # reference implementation
   227    227   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.
   228    228   
   229         -## invocation
   230         -[$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.
          229  +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.
          230  +
          231  +## lua library
          232  +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
          233  +
          234  +~~~ stdin2html.lua [lua] ~~~
          235  +local ct = require 'cortav'
          236  +local mode = {}
          237  +local doc = ct.parse(io.stdin, {file = '(stdin)'}, mode)
          238  +doc.stage = {
          239  +	kind = 'render';
          240  +	format = 'html';
          241  +	mode = mode;
          242  +}
          243  +output:write(ct.render.html(doc, {accent = '320'}))
          244  +~~~
          245  +
          246  +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
          247  +
          248  +~~~
          249  +$ luac -s -o stdin2html.lc $cortav_repo/{sirsem,cortav,ext/toc}.lua stdin2html.lua
          250  +~~~
          251  +
          252  +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.
          253  +
          254  +## command line driver
          255  +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.
          256  +
          257  +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.
          258  +
          259  +! 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.
          260  +
          261  +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.
   231    262   
   232    263   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.
   233    264   
   234    265   ~~~
   235    266   $ cortav readme.ct -o readme.html
   236    267   	# reads from readme.ct, writes to readme.html
   237    268   $ cortav -o readme.html

Modified cortav.lua from [a950584594] to [2576638f1f].

     1      1   -- [ʞ] cortav.lua
     2      2   --  ~ lexi hale <lexi@hale.su>
     3      3   --  © AGPLv3
     4      4   --  ? reference implementation of the cortav document language
     5      5   
     6         -local ct = { render = {} }
     7         -
     8         -local function hexdump(s)
     9         -	local hexlines, charlines = {},{}
    10         -	for i=1,#s do
    11         -		local line = math.floor((i-1)/16) + 1
    12         -		hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
    13         -		charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
    14         -	end
    15         -	local str = ''
    16         -	for i=1,#hexlines do
    17         -		str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
    18         -	end
    19         -	return str
    20         -end
    21         -
    22         -local function dump(o, state, path, depth)
    23         -	state = state or {tbls = {}}
    24         -	depth = depth or 0
    25         -	local pfx = string.rep('   ', depth)
    26         -	if type(o) == "table" then
    27         -		local str = ''
    28         -		for k,p in pairs(o) do
    29         -			local done = false
    30         -			local exp
    31         -			if type(p) == 'table' then
    32         -				if state.tbls[p] then
    33         -					exp = '<' .. state.tbls[p] ..'>'
    34         -					done = true
    35         -				else
    36         -					state.tbls[p] = path and string.format('%s.%s', path, k) or k
    37         -				end
    38         -			end
    39         -			if not done then
    40         -				local function dodump() return dump(
    41         -					p, state,
    42         -					path and string.format("%s.%s", path, k) or k,
    43         -					depth + 1
    44         -				) end
    45         -				-- boy this is ugly
    46         -				if type(p) ~= 'table' or
    47         -					getmetatable(p) == nil or
    48         -					getmetatable(p).__tostring == nil then
    49         -					exp = dodump()
    50         -				end
    51         -				if type(p) == 'table' then
    52         -					exp = string.format('{\n%s%s}', exp, pfx)
    53         -					local meta = getmetatable(p)
    54         -					if meta then
    55         -						if meta.__tostring then
    56         -							exp = tostring(p)
    57         -						end
    58         -						if meta.__name then
    59         -							exp = meta.__name .. ' ' .. exp
    60         -						end
    61         -					end
    62         -				end
    63         -			end
    64         -			str = str .. pfx .. string.format("%s = %s\n", k, exp)
    65         -		end
    66         -		return str
    67         -	elseif type(o) == "string" then
    68         -		return string.format('“%s”', o)
    69         -	else
    70         -		return tostring(o)
    71         -	end
    72         -end
    73         -
    74         -local function
    75         -lerp(t, a, b)
    76         -	return (1-t)*a + (t*b)
    77         -end
    78         -
    79         -local function
    80         -startswith(str, pfx)
    81         -	return string.sub(str, 1, #pfx) == pfx
    82         -end
    83         -
    84         -local function declare(c)
    85         -	local cls = setmetatable({
    86         -		__name = c.ident;
    87         -	}, {
    88         -		__name = 'class';
    89         -		__tostring = function() return c.ident or '(class)' end;
    90         -	})
    91         -
    92         -	cls.__call = c.call
    93         -	cls.__index = function(self, k)
    94         -		if c.default and c.default[k] then
    95         -			return c.default[k]
    96         -		end
    97         -		if k == 'clone' then
    98         -			return function(self)
    99         -				local new = cls.mk()
   100         -				for k,v in pairs(self) do
   101         -					new[k] = v
   102         -				end
   103         -				if c.clonesetup then
   104         -					c.clonesetup(new, self)
   105         -				end
   106         -				return new
   107         -			end
   108         -		elseif k == 'to' then
   109         -			return function(self, to, ...)
   110         -				if to == 'string' then return tostring(self)
   111         -				elseif to == 'number' then return tonumber(self)
   112         -				elseif to == 'int' then return math.floor(tonumber(self))
   113         -				elseif c.cast and c.cast[to] then
   114         -					return c.cast[to](self, ...)
   115         -				elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
   116         -				else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
   117         -			end
   118         -		end
   119         -		if c.fns then return c.fns[k] end
   120         -	end
   121         -
   122         -	if c.cast then
   123         -		if c.cast.string then
   124         -			cls.__tostring = c.cast.string
   125         -		end
   126         -		if c.cast.number then
   127         -			cls.__tonumber = c.cast.number
   128         -		end
   129         -	end
   130         -
   131         -	cls.mk = function(...)
   132         -		local val = setmetatable(c.mk and c.mk(...) or {}, cls)
   133         -		if c.init then
   134         -			for k,v in pairs(c.init) do
   135         -				val[k] = v
   136         -			end
   137         -		end
   138         -		if c.construct then
   139         -			c.construct(val, ...)
   140         -		end
   141         -		return val
   142         -	end
   143         -	getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
   144         -	cls.is = function(o) return getmetatable(o) == cls end
   145         -	return cls
   146         -end
   147         -ct.exn = declare {
   148         -	ident = 'exn';
   149         -	mk = function(kind, ...)
   150         -		return {
   151         -			vars = {...};
   152         -			kind = kind;
   153         -		}
   154         -	end;
   155         -	cast = {
   156         -		string = function(me)
   157         -			return me.kind.report(table.unpack(me.vars))
   158         -		end;
   159         -	};
   160         -	fns = {
   161         -		throw = function(me) error(me) end;
   162         -	}
   163         -}
   164         -ct.exnkind = declare {
   165         -	ident = 'exn-kind';
   166         -	mk = function(desc, report)
   167         -		return {
   168         -			desc = desc;
   169         -			report = report or function(msg,...)
   170         -				return string.format(msg,...)
   171         -			end;
   172         -		}
   173         -	end;
   174         -	call = function(me, ...)
   175         -		return ct.exn(me, ...)
   176         -	end;
   177         -}
            6  +local ss = require 'sirsem'
            7  +-- aliases for commonly used sirsem funcs
            8  +local startswith = ss.str.begins
            9  +local eachcode = ss.str.enc.utf8.each
           10  +local dump = ss.dump
           11  +local declare = ss.declare
           12  +
           13  +-- make this module available to require() when linked into a lua bytecode program with luac
           14  +local ct = ss.namespace 'cortav'
           15  +ct.render = {}
   178     16   
   179     17   ct.exns = {
   180         -	tx = ct.exnkind('translation error', function(msg,...)
           18  +	tx = ss.exnkind('translation error', function(msg,...)
   181     19   		return string.format("(%s:%u) "..msg, ...)
   182     20   	end);
   183         -	io = ct.exnkind('IO error', function(msg, ...)
           21  +	io = ss.exnkind('IO error', function(msg, ...)
   184     22   		return string.format("<%s %s> "..msg, ...)
   185     23   	end);
   186         -	cli = ct.exnkind 'command line parse error';
   187         -	mode = ct.exnkind('bad mode', function(msg, ...)
           24  +	cli = ss.exnkind 'command line parse error';
           25  +	mode = ss.exnkind('bad mode', function(msg, ...)
   188     26   		return string.format("mode “%s” "..msg, ...)
   189     27   	end);
   190         -	unimpl = ct.exnkind 'feature not implemented';
           28  +	unimpl = ss.exnkind 'feature not implemented';
           29  +	ext = ss.exnkind 'extension error';
   191     30   }
   192     31   
   193     32   ct.ctx = declare {
   194     33   	mk = function(src) return {src = src} end;
   195     34   	ident = 'context';
   196     35   	cast = {
   197     36   		string = function(me)
................................................................................
   306    145   		secorder = {};
   307    146   		embed = {};
   308    147   		meta = {};
   309    148   		vars = {};
   310    149   	} end;
   311    150   }
   312    151   
   313         -local function map(fn, lst)
   314         -	local new = {}
   315         -	for k,v in pairs(lst) do
   316         -		table.insert(new, fn(v,k))
   317         -	end
   318         -	return new
   319         -end
   320         -local function reduce(fn, acc, lst)
   321         -	for i,v in ipairs(lst) do
   322         -		acc = fn(acc, v, i)
   323         -	end
   324         -	return acc
   325         -end
          152  +-- FP helper functions
          153  +
   326    154   local function fmtfn(str)
   327    155   	return function(...)
   328    156   		return string.format(str, ...)
   329    157   	end
   330    158   end
   331    159   
          160  +ct.ext = { loaded = {} }
          161  +function ct.ext.install(ext)
          162  +	if not ext.id then
          163  +		ct.exns.ext 'extension missing “id” field':throw()
          164  +	end
          165  +	if ct.ext.loaded[ext.id] then
          166  +		ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw()
          167  +	end
          168  +	ct.ext.loaded[ext.id] = ext
          169  +end
          170  +
          171  +-- renderer engines
   332    172   function ct.render.html(doc, opts)
   333    173   	local doctitle = opts['title']
   334    174   	local f = string.format
   335    175   	local ids = {}
   336    176   	local canonicalID = {}
   337    177   	local function getSafeID(obj)
   338    178   		if canonicalID[obj] then
................................................................................
   628    468   					end
   629    469   					table.insert(tb, tag('tr',nil,catenate(row)))
   630    470   				end
   631    471   				return tag('table',nil,catenate(tb))
   632    472   			end;
   633    473   			listing = function(b,s)
   634    474   				stylesNeeded.block_code_listing = true
   635         -				local nodes = map(function(l)
          475  +				local nodes = ss.map(function(l)
   636    476   					if #l > 0 then
   637    477   						return tag('div',nil,sr.htmlSpan(l, b, s))
   638    478   					else
   639    479   						return elt('hr')
   640    480   					end
   641    481   				end, b.lines)
   642    482   				if b.title then
................................................................................
   657    497   		local r = getSpanRenderers(tag,elt)
   658    498   		r.block_renderers = getBlockRenderers(tag,elt,r,catenate)
   659    499   		return r
   660    500   	end
   661    501   
   662    502   	local elt = function(t,attrs)
   663    503   		return f('<%s%s>', t,
   664         -			attrs and reduce(function(a,b) return a..b end, '', 
   665         -				map(function(v,k)
          504  +			attrs and ss.reduce(function(a,b) return a..b end, '', 
          505  +				ss.map(function(v,k)
   666    506   					if v == true
   667    507   						then          return ' '..k
   668    508   						elseif v then return f(' %s="%s"', k, v)
   669    509   					end
   670    510   				end, attrs)) or '')
   671    511   	end
   672    512   	local tag = function(t,attrs,body)
................................................................................
   836    676   				return tone(tfg,nil,nil,tonumber(alpha))
   837    677   			elseif var == 'tone' then
   838    678   				local l, sep, sat
   839    679   				for i=1,3 do -- 🙄
   840    680   					l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
   841    681   					if l then break end
   842    682   				end
   843         -				l = lerp(tonumber(l), tbg, tfg)
          683  +				l = ss.math.lerp(tonumber(l), tbg, tfg)
   844    684   				return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha))
   845    685   			end
   846    686   		end
   847    687   		css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace)
   848    688   		css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end)
   849    689   		css = css:gsub('@(%w+)/([0-9.]+)', replace)
   850    690   		css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end)
................................................................................
   887    727   	elseif opts.snippet then
   888    728   		return styletag .. body
   889    729   	else
   890    730   		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
   891    731   	end
   892    732   end
   893    733   
   894         -local function eachcode(str, ascode)
   895         -	local pos = {
   896         -		code = 1;
   897         -		byte = 1;
   898         -	}
   899         -	return function()
   900         -		if pos.byte > #str then return nil end
   901         -		local thischar = utf8.codepoint(str, pos.byte)
   902         -		local lastpos = {
   903         -			code = pos.code;
   904         -			byte = pos.byte;
   905         -			next = pos;
   906         -		}
   907         -		if not ascode then
   908         -			thischar = utf8.char(thischar)
   909         -			pos.byte = pos.byte + #thischar
   910         -		else
   911         -			pos.byte = pos.byte + #utf8.char(thischar)
   912         -		end
   913         -		pos.code = pos.code + 1
   914         -		return thischar, lastpos
   915         -	end
   916         -end
   917         -
   918    734   do -- define span control sequences
   919    735   	local function formatter(sty)
   920    736   		return function(s,c)
   921    737   			return {
   922    738   				kind = 'format';
   923    739   				style = sty;
   924    740   				spans = ct.parse_span(s, c);
................................................................................
   946    762   				raw = raw;
   947    763   				var = not pos and s or nil;
   948    764   				origin = c:clone();
   949    765   			}
   950    766   		end
   951    767   	end
   952    768   	ct.spanctls = {
   953         -		{seq = '$', parse = formatter 'literal'};
   954    769   		{seq = '!', parse = formatter 'emph'};
   955    770   		{seq = '*', parse = formatter 'strong'};
          771  +		{seq = '~', parse = formatter 'strike'};
          772  +		{seq = '+', parse = formatter 'inser'};
   956    773   		{seq = '\\', parse = function(s, c) -- raw
   957    774   			return s
   958    775   		end};
   959    776   		{seq = '$\\', parse = function(s, c) -- raw
   960    777   			return {
   961    778   				kind = 'format';
   962    779   				style = 'literal';
   963    780   				spans = {s};
   964    781   				origin = c:clone();
   965    782   			}
   966    783   		end};
          784  +		{seq = '$', parse = formatter 'literal'};
   967    785   		{seq = '&', parse = function(s, c)
   968    786   			local r, t = s:match '^([^%s]+)%s*(.-)$'
   969    787   			return {
   970    788   				kind = 'term';
   971    789   				spans = (t and t ~= "") and ct.parse_span(t, c) or {};
   972    790   				ref = r; 
   973    791   				origin = c:clone();
................................................................................
   988    806   		{seq = '##', parse = insert_var_ref(true)};
   989    807   		{seq = '#', parse = insert_var_ref(false)};
   990    808   	}
   991    809   end
   992    810   
   993    811   function ct.parse_span(str,ctx)
   994    812   	local function delimited(start, stop, s)
   995         -		local depth = 0
   996         -		if not startswith(s, start) then return nil end
   997         -		for c,p in eachcode(s) do
   998         -			if c == '\\' then
   999         -				p.next.byte = p.next.byte + #utf8.char(utf8.codepoint(s, p.next.byte))
  1000         -				p.next.code = p.next.code + 1
  1001         -			elseif c == start then
  1002         -				depth = depth + 1
  1003         -			elseif c == stop then
  1004         -				depth = depth - 1
  1005         -				if depth == 0 then
  1006         -					return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
  1007         -				elseif depth < 0 then
  1008         -					ctx:fail('out of place %s', stop)
  1009         -				end
  1010         -			end
  1011         -		end
  1012         -
  1013         -		ctx:fail('[%s] expected before end of line', stop)
          813  +		-- local r = { pcall(ss.str.delimit, 'utf8', start, stop, s) }
          814  +		-- if r[1] then return table.unpack(r, 2) end
          815  +		-- ctx:fail(tostring(e))
          816  +		return ss.str.delimit(ss.str.enc.utf8, start, stop, s)
  1014    817   	end
  1015    818   	local buf = ""
  1016    819   	local spans = {}
  1017    820   	local function flush()
  1018    821   		if buf ~= "" then
  1019    822   			table.insert(spans, buf)
  1020    823   			buf = ""
................................................................................
  1077    880   		else
  1078    881   			buf = buf .. c
  1079    882   		end
  1080    883   	end
  1081    884   	flush()
  1082    885   	return spans
  1083    886   end
  1084         -
  1085    887   
  1086    888   local function
  1087    889   blockwrap(fn)
  1088    890   	return function(l,c)
  1089    891   		local block = fn(l,c)
  1090    892   		block.origin = c:clone();
  1091    893   		table.insert(c.sec.blocks, block);
................................................................................
  1166    968   	end;
  1167    969   }
  1168    970   
  1169    971   local function insert_table_row(l,c)
  1170    972   	local row = {}
  1171    973   	local buf
  1172    974   	local flush = function()
  1173         -		if buf then table.insert(row, buf) end
          975  +		if buf then
          976  +			buf.str = buf.str:gsub('%s+$','')
          977  +			table.insert(row, buf)
          978  +		end
  1174    979   		buf = { str = '' }
  1175    980   	end
  1176    981   	for c,p in eachcode(l) do
  1177    982   		if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then
  1178    983   			flush()
  1179    984   			buf.header = c == '+'
  1180    985   		elseif c == ':' then
................................................................................
  1383   1188   				end
  1384   1189   			end
  1385   1190   		end
  1386   1191   	end
  1387   1192   
  1388   1193   	return ctx.doc
  1389   1194   end
  1390         -
  1391         -local default_mode = {
  1392         -	['render:format'] = 'html';
  1393         -	['html:gen-styles'] = true;
  1394         -}
  1395         -
  1396         -local function filter(list, fn)
  1397         -	local new = {}
  1398         -	for i, v in ipairs(list) do
  1399         -		if fn(v,i) then table.insert(new, v) end
  1400         -	end
  1401         -	return new
  1402         -end
  1403         -
  1404         -local function kmap(fn, list)
  1405         -	local new = {}
  1406         -	for k, v in pairs(list) do
  1407         -		local nk,nv = fn(k,v)
  1408         -		new[nk or k] = nv or v
  1409         -	end
  1410         -	return new
  1411         -end
  1412         -local function kfilter(list, fn)
  1413         -	local new = {}
  1414         -	for k, v in pairs(list) do
  1415         -		if fn(k,v) then new[k] = v end
  1416         -	end
  1417         -	return new
  1418         -end
  1419         -
  1420         -local function main(input, output, log, mode, vars)
  1421         -	local doc = ct.parse(input.stream, input.src, mode)
  1422         -	input.stream:close()
  1423         -	if mode['parse:show-tree'] then
  1424         -		log:write(dump(doc))
  1425         -	end
  1426         -
  1427         -	if not mode['render:format'] then
  1428         -		error 'what output format should i translate the input to?'
  1429         -	end
  1430         -	if mode['render:format'] == 'none' then return 0 end
  1431         -	if not ct.render[mode['render:format']] then
  1432         -		ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw()
  1433         -	end
  1434         -	
  1435         -	local render_opts = kmap(function(k,v)
  1436         -		return k:sub(2+#mode['render:format'])
  1437         -	end, kfilter(mode, function(m)
  1438         -		return startswith(m, mode['render:format']..':')
  1439         -	end))
  1440         -
  1441         -	doc.vars = vars
  1442         -	
  1443         -	-- this is kind of gross but the context object belongs to the parser,
  1444         -	-- not the renderer, so that's not a suitable place for this information
  1445         -	doc.stage = {
  1446         -		kind = 'render';
  1447         -		format = mode['render:format'];
  1448         -		mode = mode;
  1449         -	}
  1450         -
  1451         -	output:write(ct.render[mode['render:format']](doc, render_opts))
  1452         -	return 0
  1453         -end
  1454         -
  1455         -local inp,outp,log = io.stdin, io.stdout, io.stderr
  1456         -
  1457         -local function entry_cli()
  1458         -	local mode, vars, input = default_mode, {}, {
  1459         -		stream = inp;
  1460         -		src = {
  1461         -			file = '(stdin)';
  1462         -		}
  1463         -	}
  1464         -
  1465         -	local optnparams = function(o)
  1466         -		local param_opts = {
  1467         -			out = 1;
  1468         -			log = 1;
  1469         -			define = 2; -- key value
  1470         -			['mode-set'] = 1;
  1471         -			['mode-clear'] = 1;
  1472         -			mode = 2;
  1473         -		}
  1474         -		return param_opts[o] or 0
  1475         -	end
  1476         -
  1477         -	local optmap = {
  1478         -		o = 'out';
  1479         -		l = 'log';
  1480         -		d = 'define';
  1481         -		V = 'version';
  1482         -		h = 'help';
  1483         -		y = 'mode-set', n = 'mode-clear';
  1484         -		m = 'mode';
  1485         -	}
  1486         -
  1487         -	local checkmodekey = function(key)
  1488         -		if not key:match '[^:]+:.+' then
  1489         -			ct.exns.cli('invalid mode key %s', key):throw()
  1490         -		end
  1491         -		return key
  1492         -	end
  1493         -	local onswitch = {
  1494         -		out = function(file)
  1495         -			local nf = io.open(file,'wb')
  1496         -			if nf then outp:close() outp = nf else
  1497         -				ct.exns.io('could not open output file for writing', 'open',file):throw()
  1498         -			end
  1499         -		end;
  1500         -		log = function(file)
  1501         -			local nf = io.open(file,'wb')
  1502         -			if nf then log:close() log = nf else
  1503         -				ct.exns.io('could not open log file for writing', 'open',file):throw()
  1504         -			end
  1505         -		end;
  1506         -		define = function(key,value)
  1507         -			if startswith(key, 'cortav.') or startswith(key, 'env.') then
  1508         -				ct.exns.cli 'cannot define variable in restricted namespace':throw()
  1509         -			end
  1510         -			vars[key] = value
  1511         -		end;
  1512         -		mode = function(key,value) mode[checkmodekey(key)] = value end;
  1513         -		['mode-set'] = function(key) mode[checkmodekey(key)] = true end;
  1514         -		['mode-clear'] = function(key) mode[checkmodekey(key)] = false end;
  1515         -	}
  1516         -
  1517         -	local args = {}
  1518         -	local keepParsing = true
  1519         -	do local i = 1 while i <= #arg do local v = arg[i]
  1520         -		local execLongOpt = function(longopt)
  1521         -			if not onswitch[longopt] then
  1522         -				ct.exns.cli('switch --%s unrecognized', longopt):throw()
  1523         -			end
  1524         -			local nargs = optnparams(longopt)
  1525         -
  1526         -			if nargs > 1 then
  1527         -				if i + nargs > #arg then
  1528         -					ct.exns.cli('not enough arguments for switch --%s (%u expected)', longopt, nargs):throw()
  1529         -				end
  1530         -				local nt = {}
  1531         -				for j = i+1, i+nargs do
  1532         -					table.insert(nt, arg[j])
  1533         -				end
  1534         -				onswitch[longopt](table.unpack(nt))
  1535         -			elseif nargs == 1 then
  1536         -				onswitch[longopt](arg[i+1])
  1537         -			end
  1538         -			i = i + nargs
  1539         -		end
  1540         -		if v == '--' then
  1541         -			keepParsing = false
  1542         -		else
  1543         -			local longopt = v:match '^%-%-(.+)$'
  1544         -			if keepParsing and longopt then
  1545         -				execLongOpt(longopt)
  1546         -			else
  1547         -				if keepParsing and v:sub(1,1) == '-' then
  1548         -					for c,p in eachcode(v:sub(2)) do
  1549         -						if optmap[c] then
  1550         -							execLongOpt(optmap[c])
  1551         -						else
  1552         -							ct.exns.cli('switch -%i unrecognized', c):throw()
  1553         -						end
  1554         -					end
  1555         -				else
  1556         -					table.insert(args, v)
  1557         -				end
  1558         -			end
  1559         -
  1560         -		end
  1561         -	i = i + 1 end end
  1562         -
  1563         -	if args[1] and args[1] ~= '' then
  1564         -		local file = io.open(arg[1], "rb")
  1565         -		if not file then error('unable to load file ' .. args[1]) end
  1566         -		input.stream = file
  1567         -		input.src.file = args[1]
  1568         -	end
  1569         -
  1570         -	return main(input, outp, log, mode, vars)
  1571         -end
  1572         -
  1573         -local ok, e = pcall(entry_cli)
  1574         --- local ok, e = true, entry_cli()
  1575         -if not ok then
  1576         -	local str = 'translation failure'
  1577         -	if ct.exn.is(e) then
  1578         -		str = e.kind.desc
  1579         -	end
  1580         -	local color = false
  1581         -	if log:seek() == nil then
  1582         -		-- this is not a very reliable heuristic for detecting
  1583         -		-- attachment to a tty but it's better than nothing
  1584         -		if os.getenv('COLORTERM') then
  1585         -			color = true
  1586         -		else
  1587         -			local term = os.getenv('TERM')
  1588         -			if term:find 'color' then color = true end
  1589         -		end
  1590         -	end
  1591         -	if color then
  1592         -		str = string.format('\27[1;31m%s\27[m', str)
  1593         -	end
  1594         -	log:write(string.format('%s: %s\n', str, e))
  1595         -	os.exit(1)
  1596         -end
  1597         -os.exit(e)

Added ext/toc.lua version [a3dcc0807f].

            1  +local ct = require 'cortav'
            2  +local ss = require 'sirsem'
            3  +
            4  +ct.ext.install {
            5  +	id = 'toc';
            6  +	desc = 'provides a table of contents for HTML renderer plus generic fallback';
            7  +	directive = function(words)
            8  +
            9  +	end;
           10  +}

Modified makefile from [35641b8f47] to [5cfd42ea5f].

     1      1   lua != which lua
     2      2   luac != which luac
     3      3   
     4         -cortav: cortav.lua
            4  +extens ?= $(patsubst ext/%.lua,%,$(wildcard ext/*.lua))
            5  +extens_srcs = $(patsubst %,ext/%.lua,$(extens))
            6  +
            7  +cortav: sirsem.lua cortav.lua $(extens_srcs) cli.lua
     5      8   	echo '#!$(lua)' > $@
     6         -	luac -s -o - $< >> $@
            9  +	luac -o - $^ >> $@
     7     10   	chmod +x $@
           11  +
           12  +cortav.html: cortav.ct cortav
           13  +	./cortav $< -o $@ -m render:format html -y html:fossil-uv
           14  +
           15  +.PHONY: syncdoc
           16  +syncdoc: cortav.html
           17  +	fossil uv add $<
           18  +	fossil uv sync

Added sirsem.lua version [2492fec6e5].

            1  +-- [ʞ] sirsem.lua
            2  +--  ~ lexu hale <lexi@hale.su>
            3  +--  ? utility library with functionality common to
            4  +--    cortav.lua and its extensions
            5  +--    from Ranuir "software utility"
            6  +--  > local ss = require 'sirsem.lua'
            7  +
            8  +local ss
            9  +do -- pull ourselves up by our own bootstraps
           10  +	local package = _G.package -- prevent namespace from being broken by env shenanigans
           11  +	local function namespace(name, tbl)
           12  +		local pkg = tbl or {}
           13  +		if package then
           14  +			package.loaded[name] = pkg
           15  +		end
           16  +		return pkg
           17  +	end
           18  +	ss = namespace 'sirsem'
           19  +	ss.namespace = namespace
           20  +end
           21  +
           22  +function ss.map(fn, lst)
           23  +	local new = {}
           24  +	for k,v in pairs(lst) do
           25  +		table.insert(new, fn(v,k))
           26  +	end
           27  +	return new
           28  +end
           29  +function ss.reduce(fn, acc, lst)
           30  +	for i,v in ipairs(lst) do
           31  +		acc = fn(acc, v, i)
           32  +	end
           33  +	return acc
           34  +end
           35  +function ss.filter(list, fn)
           36  +	local new = {}
           37  +	for i, v in ipairs(list) do
           38  +		if fn(v,i) then table.insert(new, v) end
           39  +	end
           40  +	return new
           41  +end
           42  +
           43  +ss.str = {}
           44  +
           45  +function ss.str.begins(str, pfx)
           46  +	return string.sub(str, 1, #pfx) == pfx
           47  +end
           48  +
           49  +ss.str.enc = {
           50  +	utf8 = {
           51  +		char = utf8.char;
           52  +		codepoint = utf8.codepoint;
           53  +	};
           54  +	c6b = {};
           55  +	ascii = {};
           56  +}
           57  +
           58  +function ss.str.enc.utf8.each(str, ascode)
           59  +	local pos = {
           60  +		code = 1;
           61  +		byte = 1;
           62  +	}
           63  +	return function()
           64  +		if pos.byte > #str then return nil end
           65  +		local thischar = utf8.codepoint(str, pos.byte)
           66  +		local lastpos = {
           67  +			code = pos.code;
           68  +			byte = pos.byte;
           69  +			next = pos;
           70  +		}
           71  +		if not ascode then
           72  +			thischar = utf8.char(thischar)
           73  +			pos.byte = pos.byte + #thischar
           74  +		else
           75  +			pos.byte = pos.byte + #utf8.char(thischar)
           76  +		end
           77  +		pos.code = pos.code + 1
           78  +		return thischar, lastpos
           79  +	end
           80  +end
           81  +
           82  +ss.math = {}
           83  +
           84  +function ss.math.lerp(t, a, b)
           85  +	return (1-t)*a + (t*b)
           86  +end
           87  +
           88  +function ss.dump(o, state, path, depth)
           89  +	state = state or {tbls = {}}
           90  +	depth = depth or 0
           91  +	local pfx = string.rep('   ', depth)
           92  +	if type(o) == "table" then
           93  +		local str = ''
           94  +		for k,p in pairs(o) do
           95  +			local done = false
           96  +			local exp
           97  +			if type(p) == 'table' then
           98  +				if state.tbls[p] then
           99  +					exp = '<' .. state.tbls[p] ..'>'
          100  +					done = true
          101  +				else
          102  +					state.tbls[p] = path and string.format('%s.%s', path, k) or k
          103  +				end
          104  +			end
          105  +			if not done then
          106  +				local function dodump() return dump(
          107  +					p, state,
          108  +					path and string.format("%s.%s", path, k) or k,
          109  +					depth + 1
          110  +				) end
          111  +				-- boy this is ugly
          112  +				if type(p) ~= 'table' or
          113  +					getmetatable(p) == nil or
          114  +					getmetatable(p).__tostring == nil then
          115  +					exp = dodump()
          116  +				end
          117  +				if type(p) == 'table' then
          118  +					exp = string.format('{\n%s%s}', exp, pfx)
          119  +					local meta = getmetatable(p)
          120  +					if meta then
          121  +						if meta.__tostring then
          122  +							exp = tostring(p)
          123  +						end
          124  +						if meta.__name then
          125  +							exp = meta.__name .. ' ' .. exp
          126  +						end
          127  +					end
          128  +				end
          129  +			end
          130  +			str = str .. pfx .. string.format("%s = %s\n", k, exp)
          131  +		end
          132  +		return str
          133  +	elseif type(o) == "string" then
          134  +		return string.format('“%s”', o)
          135  +	else
          136  +		return tostring(o)
          137  +	end
          138  +end
          139  +
          140  +function ss.hexdump(s)
          141  +	local hexlines, charlines = {},{}
          142  +	for i=1,#s do
          143  +		local line = math.floor((i-1)/16) + 1
          144  +		hexlines[line] = (hexlines[line] or '') .. string.format("%02x ",string.byte(s, i))
          145  +		charlines[line] = (charlines[line] or '') .. ' ' .. string.gsub(string.sub(s, i, i), '[^%g ]', '\x1b[;35m·\x1b[36;1m') .. ' '
          146  +	end
          147  +	local str = ''
          148  +	for i=1,#hexlines do
          149  +		str = str .. '\x1b[1;36m' .. charlines[i] .. '\x1b[m\n' .. hexlines[i] .. '\n'
          150  +	end
          151  +	return str
          152  +end
          153  +
          154  +function ss.declare(c)
          155  +	local cls = setmetatable({
          156  +		__name = c.ident;
          157  +	}, {
          158  +		__name = 'class';
          159  +		__tostring = function() return c.ident or '(class)' end;
          160  +	})
          161  +
          162  +	cls.__call = c.call
          163  +	cls.__index = function(self, k)
          164  +		if c.default and c.default[k] then
          165  +			return c.default[k]
          166  +		end
          167  +		if k == 'clone' then
          168  +			return function(self)
          169  +				local new = cls.mk()
          170  +				for k,v in pairs(self) do
          171  +					new[k] = v
          172  +				end
          173  +				if c.clonesetup then
          174  +					c.clonesetup(new, self)
          175  +				end
          176  +				return new
          177  +			end
          178  +		elseif k == 'to' then
          179  +			return function(self, to, ...)
          180  +				if to == 'string' then return tostring(self)
          181  +				elseif to == 'number' then return tonumber(self)
          182  +				elseif to == 'int' then return math.floor(tonumber(self))
          183  +				elseif c.cast and c.cast[to] then
          184  +					return c.cast[to](self, ...)
          185  +				elseif type(to) == 'table' and getmetatable(to) and getmetatable(to).cvt and getmetatable(to).cvt[cls] then
          186  +				else error((c.ident or 'class') .. ' is not convertible to ' .. (type(to) == 'string' and to or tostring(to))) end
          187  +			end
          188  +		end
          189  +		if c.fns then return c.fns[k] end
          190  +	end
          191  +
          192  +	if c.cast then
          193  +		if c.cast.string then
          194  +			cls.__tostring = c.cast.string
          195  +		end
          196  +		if c.cast.number then
          197  +			cls.__tonumber = c.cast.number
          198  +		end
          199  +	end
          200  +
          201  +	cls.mk = function(...)
          202  +		local val = setmetatable(c.mk and c.mk(...) or {}, cls)
          203  +		if c.init then
          204  +			for k,v in pairs(c.init) do
          205  +				val[k] = v
          206  +			end
          207  +		end
          208  +		if c.construct then
          209  +			c.construct(val, ...)
          210  +		end
          211  +		return val
          212  +	end
          213  +	getmetatable(cls).__call = function(_, ...) return cls.mk(...) end
          214  +	cls.is = function(o) return getmetatable(o) == cls end
          215  +	return cls
          216  +end
          217  +
          218  +-- tidy exceptions
          219  +
          220  +ss.exn = ss.declare {
          221  +	ident = 'exn';
          222  +	mk = function(kind, ...)
          223  +		return {
          224  +			vars = {...};
          225  +			kind = kind;
          226  +		}
          227  +	end;
          228  +	cast = {
          229  +		string = function(me)
          230  +			return me.kind.report(table.unpack(me.vars))
          231  +		end;
          232  +	};
          233  +	fns = {
          234  +		throw = function(me) error(me) end;
          235  +	}
          236  +}
          237  +ss.exnkind = ss.declare {
          238  +	ident = 'exn-kind';
          239  +	mk = function(desc, report)
          240  +		return {
          241  +			desc = desc;
          242  +			report = report or function(msg,...)
          243  +				return string.format(msg,...)
          244  +			end;
          245  +		}
          246  +	end;
          247  +	call = function(me, ...)
          248  +		return ss.exn(me, ...)
          249  +	end;
          250  +}
          251  +ss.str.exn = ss.exnkind 'failure while string munging'
          252  +
          253  +function ss.str.delimit(encoding, start, stop, s)
          254  +	local depth = 0
          255  +	encoding = encoding or ss.str.enc.utf8
          256  +	if not ss.str.begins(s, start) then return nil end
          257  +	for c,p in encoding.each(s) do
          258  +		if c == (encoding.escape or '\\') then
          259  +			p.next.byte = p.next.byte + #encoding.char(encoding.codepoint(s, p.next.byte))
          260  +			p.next.code = p.next.code + 1
          261  +		elseif c == start then
          262  +			depth = depth + 1
          263  +		elseif c == stop then
          264  +			depth = depth - 1
          265  +			if depth == 0 then
          266  +				return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
          267  +			elseif depth < 0 then
          268  +				ss.str.exn('out of place %s', stop):throw()
          269  +			end
          270  +		end
          271  +	end
          272  +
          273  +	ss.str.exn('[%s] expected before end of line', stop):throw()
          274  +end