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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 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) |
Modified cortav.ct from [fcb217abd6] to [96194d0b88].
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
<A> …oh, [!fuck]. (signal lost) ~~~ # 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. 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 # reads from readme.ct, writes to readme.html $ cortav -o readme.html |
| < > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
<A> …oh, [!fuck]. (signal lost) ~~~ # 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. 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 # reads from readme.ct, writes to readme.html $ cortav -o readme.html |
Modified cortav.lua from [a950584594] to [2576638f1f].
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 ... 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 ... 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 ... 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 ... 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 ... 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 ... 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 ... 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 .... 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 .... 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 .... 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 |
-- [ʞ] cortav.lua -- ~ lexi hale <lexi@hale.su> -- © 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; } ct.exns = { tx = ct.exnkind('translation error', function(msg,...) return string.format("(%s:%u) "..msg, ...) end); io = ct.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, ...) return string.format("mode “%s” "..msg, ...) end); unimpl = ct.exnkind 'feature not implemented'; } ct.ctx = declare { mk = function(src) return {src = src} end; ident = 'context'; cast = { string = function(me) ................................................................................ secorder = {}; embed = {}; 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 local function fmtfn(str) return function(...) return string.format(str, ...) end end function ct.render.html(doc, opts) local doctitle = opts['title'] local f = string.format local ids = {} local canonicalID = {} local function getSafeID(obj) if canonicalID[obj] then ................................................................................ end table.insert(tb, tag('tr',nil,catenate(row))) end return tag('table',nil,catenate(tb)) end; listing = function(b,s) stylesNeeded.block_code_listing = true local nodes = map(function(l) if #l > 0 then return tag('div',nil,sr.htmlSpan(l, b, s)) else return elt('hr') end end, b.lines) if b.title then ................................................................................ local r = getSpanRenderers(tag,elt) r.block_renderers = getBlockRenderers(tag,elt,r,catenate) 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) if v == true then return ' '..k elseif v then return f(' %s="%s"', k, v) end end, attrs)) or '') end local tag = function(t,attrs,body) ................................................................................ return tone(tfg,nil,nil,tonumber(alpha)) elseif var == 'tone' then 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) 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) css = css:gsub('@(%w+)/([0-9.]+)', replace) css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) ................................................................................ elseif opts.snippet then return styletag .. body 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'; style = sty; spans = ct.parse_span(s, c); ................................................................................ raw = raw; var = not pos and s or nil; origin = c:clone(); } end end ct.spanctls = { {seq = '$', parse = formatter 'literal'}; {seq = '!', parse = formatter 'emph'}; {seq = '*', parse = formatter 'strong'}; {seq = '\\', parse = function(s, c) -- raw return s end}; {seq = '$\\', parse = function(s, c) -- raw return { kind = 'format'; style = 'literal'; spans = {s}; origin = c:clone(); } end}; {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 {}; ref = r; origin = c:clone(); ................................................................................ {seq = '##', parse = insert_var_ref(true)}; {seq = '#', parse = insert_var_ref(false)}; } 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) end local buf = "" local spans = {} local function flush() if buf ~= "" then table.insert(spans, buf) buf = "" ................................................................................ else buf = buf .. c end end flush() return spans end local function blockwrap(fn) return function(l,c) local block = fn(l,c) block.origin = c:clone(); table.insert(c.sec.blocks, block); ................................................................................ end; } local function insert_table_row(l,c) local row = {} local buf local flush = function() if buf then 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() buf.header = c == '+' elseif c == ':' then ................................................................................ end end 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) |
| < > | | < < < < < < < < < < < < < < < < < < < < | < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < | < < < < < | < < < < < < > > | < | < < < < < < < < < < < < < < < < | | | | | > | < < < < < < < < < < < < > > > > > > > > > > > > > | | | | < < < < < < < < < < < < < < < < < < < < < < < < < > > > | | | | < < < < < < < < < < < < < < < < > > | > < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ... 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 ... 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 ... 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 ... 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 ... 727 728 729 730 731 732 733 734 735 736 737 738 739 740 ... 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 ... 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 ... 880 881 882 883 884 885 886 887 888 889 890 891 892 893 ... 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 .... 1188 1189 1190 1191 1192 1193 1194 |
-- [ʞ] cortav.lua -- ~ lexi hale <lexi@hale.su> -- © AGPLv3 -- ? reference implementation of the cortav document language 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 = ss.exnkind('translation error', function(msg,...) return string.format("(%s:%u) "..msg, ...) end); io = ss.exnkind('IO error', function(msg, ...) return string.format("<%s %s> "..msg, ...) end); cli = ss.exnkind 'command line parse error'; mode = ss.exnkind('bad mode', function(msg, ...) return string.format("mode “%s” "..msg, ...) end); unimpl = ss.exnkind 'feature not implemented'; ext = ss.exnkind 'extension error'; } ct.ctx = declare { mk = function(src) return {src = src} end; ident = 'context'; cast = { string = function(me) ................................................................................ secorder = {}; embed = {}; meta = {}; vars = {}; } 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 = {} local function getSafeID(obj) if canonicalID[obj] then ................................................................................ end table.insert(tb, tag('tr',nil,catenate(row))) end return tag('table',nil,catenate(tb)) end; listing = function(b,s) stylesNeeded.block_code_listing = true local nodes = ss.map(function(l) if #l > 0 then return tag('div',nil,sr.htmlSpan(l, b, s)) else return elt('hr') end end, b.lines) if b.title then ................................................................................ local r = getSpanRenderers(tag,elt) r.block_renderers = getBlockRenderers(tag,elt,r,catenate) return r end local elt = function(t,attrs) return f('<%s%s>', t, 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 '') end local tag = function(t,attrs,body) ................................................................................ return tone(tfg,nil,nil,tonumber(alpha)) elseif var == 'tone' then 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 = 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) css = css:gsub('@(%w+)/([0-9.]+)', replace) css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end) ................................................................................ elseif opts.snippet then return styletag .. body else return dr.htmlDoc(doctitle, next(head) and table.concat(head), body) end end do -- define span control sequences local function formatter(sty) return function(s,c) return { kind = 'format'; style = sty; spans = ct.parse_span(s, c); ................................................................................ raw = raw; var = not pos and s or nil; origin = c:clone(); } end end ct.spanctls = { {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 { kind = 'format'; 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 {}; ref = r; origin = c:clone(); ................................................................................ {seq = '##', parse = insert_var_ref(true)}; {seq = '#', parse = insert_var_ref(false)}; } end function ct.parse_span(str,ctx) local function delimited(start, stop, s) -- 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 table.insert(spans, buf) buf = "" ................................................................................ else buf = buf .. c end end flush() return spans end local function blockwrap(fn) return function(l,c) local block = fn(l,c) block.origin = c:clone(); table.insert(c.sec.blocks, block); ................................................................................ end; } local function insert_table_row(l,c) local row = {} local buf local flush = function() 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() buf.header = c == '+' elseif c == ':' then ................................................................................ end end end end return ctx.doc end |
Added ext/toc.lua version [a3dcc0807f].
> > > > > > > > > > |
1 2 3 4 5 6 7 8 9 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; } |
Modified makefile from [35641b8f47] to [5cfd42ea5f].
1 2 3 4 5 6 7 |
lua != which lua luac != which luac cortav: cortav.lua echo '#!$(lua)' > $@ luac -s -o - $< >> $@ chmod +x $@ |
| > > > | > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
lua != which lua luac != which luac 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 -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 version [2492fec6e5].
> > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
-- [ʞ] sirsem.lua -- ~ lexu hale <lexi@hale.su> -- ? 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 |