Differences From
Artifact [a950584594]:
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)