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: |
9c67b0312cf48cba2342fd07fed44011 |
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