Differences From
Artifact [f20a833e35]:
1 1 -- [ʞ] cortav.lua
2 2 -- ~ lexi hale <lexi@hale.su>
3 3 -- © AGPLv3
4 --- ? renderer
4 +-- ? reference implementation of the cortav document language
5 5
6 6 local ct = { render = {} }
7 7
8 8 local function hexdump(s)
9 9 local hexlines, charlines = {},{}
10 10 for i=1,#s do
11 11 local line = math.floor((i-1)/16) + 1
................................................................................
66 66 return str
67 67 elseif type(o) == "string" then
68 68 return string.format('“%s”', o)
69 69 else
70 70 return tostring(o)
71 71 end
72 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
73 83
74 84 local function declare(c)
75 85 local cls = setmetatable({
76 86 __name = c.ident;
77 87 }, {
78 88 __name = 'class';
79 89 __tostring = function() return c.ident or '(class)' end;
................................................................................
173 183 io = ct.exnkind('IO error', function(msg, ...)
174 184 return string.format("<%s %s> "..msg, ...)
175 185 end);
176 186 cli = ct.exnkind 'command line parse error';
177 187 mode = ct.exnkind('bad mode', function(msg, ...)
178 188 return string.format("mode “%s” "..msg, ...)
179 189 end);
190 + unimpl = ct.exnkind 'feature not implemented';
180 191 }
181 192
182 193 ct.ctx = declare {
183 194 mk = function(src) return {src = src} end;
184 195 ident = 'context';
185 196 cast = {
186 197 string = function(me)
................................................................................
195 206 new.generation = 1
196 207 end
197 208 end;
198 209 fns = {
199 210 fail = function(self, msg, ...)
200 211 ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
201 212 end;
213 + insert = function(self, block)
214 + block.origin = self:clone()
215 + table.insert(self.sec.blocks,block)
216 + end;
202 217 ref = function(self,id)
203 218 if not id:find'%.' then
204 219 local rid = self.sec.refs[id]
205 220 if self.sec.refs[id] then
206 221 return self.sec.refs[id]
207 222 else self:fail("no such ref %s in current section", id or '') end
208 223 else
................................................................................
237 252 fns = {
238 253 mksec = function(self, id, depth)
239 254 local o = ct.sec(id, depth)
240 255 if id then self.sections[id] = o end
241 256 table.insert(self.secorder, o)
242 257 return o
243 258 end;
259 + context_var = function(self, var, ctx, test)
260 + local fail = function(...)
261 + if test then return false end
262 + ctx:fail(...)
263 + end
264 + if startswith(var, 'cortav.') then
265 + local v = var:sub(8)
266 + if v == 'page' then
267 + if ctx.page then return tostring(ctx.page)
268 + else return '(unpaged)' end
269 + elseif v == 'renderer' then
270 + if not self.stage then
271 + return fail 'document is not being rendererd'
272 + end
273 + return self.stage.format
274 + elseif v == 'datetime' then
275 + return os.date()
276 + elseif v == 'time' then
277 + return os.date '%H:%M:%S'
278 + elseif v == 'date' then
279 + return os.date '%A %d %B %Y'
280 + elseif v == 'id' then
281 + return 'cortav.lua (reference implementation)'
282 + elseif v == 'file' then
283 + return self.src.file
284 + else
285 + return fail('unimplemented predefined variable %s', var)
286 + end
287 + elseif startswith(var, 'env.') then
288 + local v = var:sub(5)
289 + local val = os.getenv(v)
290 + if not val then
291 + return fail('undefined environment variable %s', v)
292 + end
293 + elseif self.stage.kind == 'render' and startswith(var, self.stage.format..'.') then
294 + -- TODO query the renderer somehow
295 + return fail('renderer %s does not implement variable %s', self.stage.format, var)
296 + elseif self.vars[var] then
297 + return self.vars[var]
298 + else
299 + if test then return false end
300 + return '' -- is this desirable behavior?
301 + end
302 + end;
244 303 };
245 304 mk = function() return {
246 305 sections = {};
247 306 secorder = {};
248 307 embed = {};
249 308 meta = {};
309 + vars = {};
250 310 } end;
251 311 }
252 312
253 313 local function map(fn, lst)
254 314 local new = {}
255 315 for k,v in pairs(lst) do
256 316 table.insert(new, fn(v,k))
................................................................................
266 326 local function fmtfn(str)
267 327 return function(...)
268 328 return string.format(str, ...)
269 329 end
270 330 end
271 331
272 332 function ct.render.html(doc, opts)
333 + local doctitle = opts['title']
273 334 local f = string.format
274 335 local ids = {}
275 336 local canonicalID = {}
276 337 local function getSafeID(obj)
277 338 if canonicalID[obj] then
278 339 return canonicalID[obj]
279 340 elseif obj.id and ids[obj.id] then
................................................................................
307 368 lisp = { color = 0x77ff88 };
308 369 fortran = { color = 0xff779a };
309 370 python = { color = 0xffd277 };
310 371 python = { color = 0xcdd6ff };
311 372 }
312 373
313 374 local stylesets = {
375 + accent = [[
376 + body { background: @bg; color: @fg }
377 + a[href] {
378 + color: @tone(0.7 30);
379 + text-decoration-color: @tone/0.4(0.7 30);
380 + }
381 + a[href]:hover {
382 + color: @tone(0.9 30);
383 + text-decoration-color: @tone/0.7(0.7 30);
384 + }
385 + h1,h2,h3,h4,h5,h6 {
386 + color: @tone(2);
387 + border-bottom: 1px solid @tone(0.7);
388 + }
389 + ]];
314 390 code = [[
315 391 code {
316 - background: #000;
317 - color: #fff;
392 + background: @fg;
393 + color: @bg;
318 394 font-family: monospace;
319 395 font-size: 90%;
320 396 padding: 3px 5px;
321 397 }
322 398 ]];
323 399 abbr = [[
324 400 abbr[title] { cursor: help; }
325 401 ]];
326 402 editors_markup = [[]];
327 403 block_code_listing = [[
328 404 section > figure.listing {
329 405 font-family: monospace;
330 - background: #000;
331 - color: #fff;
406 + background: @tone(0.05);
407 + color: @fg;
332 408 padding: 0;
333 409 margin: 0.3em 0;
334 410 counter-reset: line-number;
335 411 position: relative;
412 + border: 1px solid @fg;
336 413 }
337 414 section > figure.listing>div {
338 415 white-space: pre-wrap;
339 416 counter-increment: line-number;
340 417 text-indent: -2.3em;
341 418 margin-left: 2.3em;
342 419 }
343 420 section > figure.listing>:is(div,hr)::before {
344 421 width: 1.0em;
345 422 padding: 0.2em 0.4em;
346 423 text-align: right;
347 424 display: inline-block;
348 - background-color: #333;
349 - border-right: 1px solid #fff;
425 + background-color: @tone(0.2);
426 + border-right: 1px solid @fg;
350 427 content: counter(line-number);
351 428 margin-right: 0.3em;
352 429 }
353 430 section > figure.listing>hr::before {
354 - color: #333;
431 + color: transparent;
355 432 padding-top: 0;
356 433 padding-bottom: 0;
357 434 }
358 435 section > figure.listing>div::before {
359 - color: #fff;
436 + color: @fg;
360 437 }
361 438 section > figure.listing>div:last-child::before {
362 439 padding-bottom: 0.5em;
363 440 }
364 441 section > figure.listing>figcaption:first-child {
365 442 border: none;
366 - border-bottom: 1px solid #fff;
443 + border-bottom: 1px solid @fg;
367 444 }
368 445 section > figure.listing>figcaption::after {
369 446 display: block;
370 447 float: right;
371 448 font-weight: normal;
372 449 font-style: italic;
373 450 font-size: 70%;
374 451 padding-top: 0.3em;
375 452 }
376 453 section > figure.listing>figcaption {
377 454 font-family: sans-serif;
378 - font-weight: bold;
379 - font-size: 130%;
455 + font-size: 120%;
380 456 padding: 0.2em 0.4em;
381 457 border: none;
458 + color: @tone(2);
382 459 }
383 460 section > figure.listing > hr {
384 461 border: none;
385 462 margin: 0;
386 463 height: 0.7em;
387 464 counter-increment: line-number;
388 465 }
................................................................................
413 490 else
414 491 table.insert(text, span_renderers[v.kind](v, block, sec))
415 492 end
416 493 end
417 494 return table.concat(text)
418 495 end
419 496
420 - function span_renderers.format(sp)
497 + function span_renderers.format(sp,...)
421 498 local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code' }
422 499 if sp.style == 'literal' and not opts['fossil-uv'] then
423 500 stylesNeeded.code = true
424 501 end
425 502 if sp.style == 'del' or sp.style == 'ins' then
426 503 stylesNeeded.editors_markup = true
427 504 end
428 - return tag(tags[sp.style],nil,htmlSpan(sp.spans))
505 + return tag(tags[sp.style],nil,htmlSpan(sp.spans,...))
429 506 end
430 507
431 - function span_renderers.term(t,b)
508 + function span_renderers.term(t,b,s)
432 509 local r = b.origin:ref(t.ref)
433 510 local name = t.ref
434 511 if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
435 512 if type(r) ~= 'string' then
436 513 b.origin:fail('%s is an object, not a reference', t.ref)
437 514 end
438 515 stylesNeeded.abbr = true
439 - return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans) or name)
516 + return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name)
440 517 end
441 518
442 - function span_renderers.link(sp,b)
519 + function span_renderers.macro(m,b,s)
520 + local r = b.origin:ref(m.macro)
521 + if type(r) ~= 'string' then
522 + b.origin:fail('%s is an object, not a reference', t.ref)
523 + end
524 + local mctx = b.origin:clone()
525 + mctx.invocation = m
526 + return htmlSpan(ct.parse_span(r, mctx),b,s)
527 + end
528 +
529 + function span_renderers.var(v,b,s)
530 + local val
531 + if v.pos then
532 + if not v.origin.invocation then
533 + v.origin:fail 'positional arguments can only be used in a macro invocation'
534 + elseif not v.origin.invocation.args[v.pos] then
535 + v.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos)
536 + end
537 + val = v.origin.invocation.args[v.pos]
538 + else
539 + val = v.origin.doc:context_var(v.var, v.origin)
540 + end
541 + if v.raw then
542 + return val
543 + else
544 + return htmlSpan(ct.parse_span(val, v.origin), b, s)
545 + end
546 + end
547 +
548 + function span_renderers.link(sp,b,s)
443 549 local href
444 550 if b.origin.doc.sections[sp.ref] then
445 551 href = '#' .. sp.ref
446 552 else
447 553 if sp.addr then href = sp.addr else
448 554 local r = b.origin:ref(sp.ref)
449 555 if type(r) == 'table' then
450 556 href = '#' .. getSafeID(r)
451 557 else href = r end
452 558 end
453 559 end
454 - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans) or href)
560 + return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href)
455 561 end
456 562 return {
457 563 span_renderers = span_renderers;
458 564 htmlSpan = htmlSpan;
459 565 htmlDoc = htmlDoc;
460 566 }
461 567 end
................................................................................
486 592 end
487 593 return lst
488 594 end
489 595
490 596 local block_renderers = {
491 597 paragraph = function(b,s)
492 598 return tag('p', nil, sr.htmlSpan(b.spans, b, s), b)
599 + end;
600 + directive = function(b,s)
601 + -- deal with renderer directives
602 + local _, cmd, args = b.words(2)
603 + if cmd == 'page-title' then
604 + if not opts.title then doctitle = args end
605 + elseif b.critical then
606 + b.origin:fail('critical HTML renderer directive “%s” not supported', cmd)
607 + end
493 608 end;
494 609 label = function(b,s)
495 610 if ct.sec.is(b.captions) then
496 611 local h = math.min(6,math.max(1,b.captions.depth))
497 612 return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
498 613 else
499 614 -- handle other uses of labels here
................................................................................
523 638 else
524 639 return elt('hr')
525 640 end
526 641 end, b.lines)
527 642 if b.title then
528 643 table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title)))
529 644 end
530 - langsused[b.lang] = true
645 + if b.lang then langsused[b.lang] = true end
531 646 return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
532 647 end;
533 648 ['break'] = function() --[[nop]] end;
534 649 }
535 650 return block_renderers;
536 651 end
537 652
................................................................................
554 669 end
555 670 end, attrs)) or '')
556 671 end
557 672 local tag = function(t,attrs,body)
558 673 return f('%s%s</%s>', elt(t,attrs), body, t)
559 674 end
560 675
561 - local doctitle
562 676 local ir = {}
563 677 local toc
564 678 local dr = getRenderers(tag,elt,table.concat) -- default renderers
565 679 local plainr = getRenderers(function(t,a,v) return v end,
566 680 function(t,a) return '' end, table.concat)
567 681 local irBlockRdrs = getBlockRenderers(
568 682 function(t,a,v,o) return {tag = t, attrs = a, nodes = type(v) == 'string' and {v} or v, src = o} end,
................................................................................
682 796 for k in pairs(langsused) do
683 797 local spec = langpairs[k] or {color=0xaaaaaa}
684 798 stylesets.block_code_listing = stylesets.block_code_listing .. string.format(
685 799 [[section > figure.listing[lang="%s"]>figcaption::after
686 800 { content: '%s'; color: #%06x }]],
687 801 k, spec.name or k, spec.color)
688 802 end
803 +
804 + local prepcss = function(css)
805 + local tone = function(fac, sat, sep, alpha)
806 + local hsl = function(h,s,l,a)
807 + local v = string.format('%s, %u%%, %u%%', h,s,l)
808 + if a then
809 + return string.format('hsla(%s, %s)', v,a)
810 + else
811 + return string.format('hsl(%s)', v)
812 + end
813 + end
814 + sat = sat or 1
815 + fac = math.max(math.min(fac, 1), 0)
816 + sat = math.max(math.min(sat, 1), 0)
817 + if opts.accent then
818 + local hue = 'var(--accent)'
819 + local hsep = tonumber(opts['hue-spread'])
820 + if hsep and sep and sep ~= 0 then
821 + hue = string.format('calc(%s - %s)', hue, sep * hsep)
822 + end
823 + return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha)
824 + else
825 + local g = math.floor(0xFF * fac)
826 + return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha))
827 + end
828 + end
829 + local replace = function(var,alpha,param)
830 + local tonespan = opts.accent and .1 or 0
831 + local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan
832 + local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan
833 + if var == 'bg' then
834 + return tone(tbg,nil,nil,tonumber(alpha))
835 + elseif var == 'fg' then
836 + return tone(tfg,nil,nil,tonumber(alpha))
837 + elseif var == 'tone' then
838 + local l, sep, sat
839 + for i=1,3 do -- 🙄
840 + l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
841 + if l then break end
842 + end
843 + l = lerp(tonumber(l), tbg, tfg)
844 + return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha))
845 + end
846 + end
847 + css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace)
848 + css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end)
849 + css = css:gsub('@(%w+)/([0-9.]+)', replace)
850 + css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end)
851 + return (css:gsub('%s+',' '))
852 + end
689 853
690 854 local styles = {}
855 + if opts.width then
856 + table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
857 + end
858 + if opts.accent then
859 + table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
860 + end
861 + if opts.accent or (not opts['dark-on-light']) then
862 + stylesNeeded.accent = true
863 + end
864 +
865 +
691 866 for k in pairs(stylesNeeded) do
692 - table.insert(styles, (stylesets[k]:gsub('%s+',' ')))
867 + if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end
868 + table.insert(styles, prepcss(stylesets[k]))
693 869 end
694 870
695 871 local head = {}
696 872 local styletag = ''
697 873 if opts['link-css'] then
698 874 local css = opts['link-css']
699 875 if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
................................................................................
711 887 elseif opts.snippet then
712 888 return styletag .. body
713 889 else
714 890 return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
715 891 end
716 892 end
717 893
718 -local function
719 -startswith(str, pfx)
720 - return string.sub(str, 1, #pfx) == pfx
721 -end
722 -
723 894 local function eachcode(str, ascode)
724 895 local pos = {
725 896 code = 1;
726 897 byte = 1;
727 898 }
728 899 return function()
729 900 if pos.byte > #str then return nil end
................................................................................
740 911 pos.byte = pos.byte + #utf8.char(thischar)
741 912 end
742 913 pos.code = pos.code + 1
743 914 return thischar, lastpos
744 915 end
745 916 end
746 917
747 -local function formatter(sty)
748 - return function(s,c)
749 - return {
750 - kind = 'format';
751 - style = sty;
752 - spans = ct.parse_span(s, c);
753 - origin = c:clone();
754 - }
755 - end
756 -end
757 -ct.spanctls = {
758 - {seq = '$', parse = formatter 'literal'};
759 - {seq = '!', parse = formatter 'emph'};
760 - {seq = '*', parse = formatter 'strong'};
761 - {seq = '\\', parse = function(s, c) -- raw
762 - return s
763 - end};
764 - {seq = '$\\', parse = function(s, c) -- raw
765 - return {
766 - kind = 'format';
767 - style = 'literal';
768 - spans = {s};
769 - origin = c:clone();
770 - }
771 - end};
772 - {seq = '&', parse = function(s, c)
773 - local r, t = s:match '^([^%s]+)%s*(.-)$'
774 - return {
775 - kind = 'term';
776 - spans = (t and t ~= "") and ct.parse_span(t, c) or {};
777 - ref = r;
778 - origin = c:clone();
779 - }
780 - end};
781 - {seq = '^', parse = function(s, c)
782 - local fn, t = s:match '^([^%s]+)%s*(.-)$'
783 - return {
784 - kind = 'footnote';
785 - spans = (t and t~='') and ct.parse_span(t, c) or {};
786 - ref = fn;
787 - origin = c:clone();
788 - }
789 - end};
790 - {seq = '>', parse = function(s, c)
918 +do -- define span control sequences
919 + local function formatter(sty)
920 + return function(s,c)
921 + return {
922 + kind = 'format';
923 + style = sty;
924 + spans = ct.parse_span(s, c);
925 + origin = c:clone();
926 + }
927 + end
928 + end
929 + local function insert_link(s, c)
791 930 local to, t = s:match '^([^%s]+)%s*(.-)$'
792 931 if not to then c:fail('invalid link syntax >%s', s) end
793 932 if t == "" then t = nil end
794 933 return {
795 934 kind = 'link';
796 935 spans = (t and t~='') and ct.parse_span(t, c) or {};
797 936 ref = to;
798 937 origin = c:clone();
799 938 }
800 - end};
801 -}
939 + end
940 + local function insert_var_ref(raw)
941 + return function(s, c)
942 + local pos = tonumber(s)
943 + return {
944 + kind = 'var';
945 + pos = pos;
946 + raw = raw;
947 + var = not pos and s or nil;
948 + origin = c:clone();
949 + }
950 + end
951 + end
952 + ct.spanctls = {
953 + {seq = '$', parse = formatter 'literal'};
954 + {seq = '!', parse = formatter 'emph'};
955 + {seq = '*', parse = formatter 'strong'};
956 + {seq = '\\', parse = function(s, c) -- raw
957 + return s
958 + end};
959 + {seq = '$\\', parse = function(s, c) -- raw
960 + return {
961 + kind = 'format';
962 + style = 'literal';
963 + spans = {s};
964 + origin = c:clone();
965 + }
966 + end};
967 + {seq = '&', parse = function(s, c)
968 + local r, t = s:match '^([^%s]+)%s*(.-)$'
969 + return {
970 + kind = 'term';
971 + spans = (t and t ~= "") and ct.parse_span(t, c) or {};
972 + ref = r;
973 + origin = c:clone();
974 + }
975 + end};
976 + {seq = '^', parse = function(s, c)
977 + local fn, t = s:match '^([^%s]+)%s*(.-)$'
978 + return {
979 + kind = 'footnote';
980 + spans = (t and t~='') and ct.parse_span(t, c) or {};
981 + ref = fn;
982 + origin = c:clone();
983 + }
984 + end};
985 + {seq = '>', parse = insert_link};
986 + {seq = '→', parse = insert_link};
987 + {seq = '🔗', parse = insert_link};
988 + {seq = '##', parse = insert_var_ref(true)};
989 + {seq = '#', parse = insert_var_ref(false)};
990 + }
991 +end
802 992
803 993 function ct.parse_span(str,ctx)
804 994 local function delimited(start, stop, s)
805 995 local depth = 0
806 996 if not startswith(s, start) then return nil end
807 997 for c,p in eachcode(s) do
808 998 if c == '\\' then
................................................................................
816 1006 return s:sub(1+#start, p.byte - #stop), p.byte -- FIXME
817 1007 elseif depth < 0 then
818 1008 ctx:fail('out of place %s', stop)
819 1009 end
820 1010 end
821 1011 end
822 1012
823 - ctx:fail('%s expected before end of line', stop)
1013 + ctx:fail('[%s] expected before end of line', stop)
824 1014 end
825 1015 local buf = ""
826 1016 local spans = {}
827 1017 local function flush()
828 1018 if buf ~= "" then
829 1019 table.insert(spans, buf)
830 1020 buf = ""
................................................................................
833 1023 local skip = false
834 1024 for c,p in eachcode(str) do
835 1025 if skip == true then
836 1026 skip = false
837 1027 buf = buf .. c
838 1028 elseif c == '\\' then
839 1029 skip = true
1030 + elseif c == '{' then
1031 + flush()
1032 + local substr, following = delimited('{','}',str:sub(p.byte))
1033 + local splitstart, splitstop = substr:find'%s+'
1034 + local id, argstr
1035 + if splitstart then
1036 + id, argstr = substr:sub(1,splitstart-1), substr:sub(splitstop+1)
1037 + else
1038 + id, argstr = substr, ''
1039 + end
1040 + local o = {
1041 + kind = 'macro';
1042 + macro = id;
1043 + args = {};
1044 + origin = ctx:clone();
1045 + }
1046 +
1047 + do local start = 1
1048 + local i = 1
1049 + while i <= #argstr do
1050 + while i<=#argstr and (argstr:sub(i,i) ~= '|' or argstr:sub(i-1,i) == '\\|') do
1051 + i = i + 1
1052 + end
1053 + local arg = argstr:sub(start, i == #argstr and i or i-1)
1054 + start = i+1
1055 + table.insert(o.args, arg)
1056 + i = i + 1
1057 + end
1058 + end
1059 +
1060 + p.next.byte = p.next.byte + following - 1
1061 + table.insert(spans,o)
840 1062 elseif c == '[' then
841 1063 flush()
842 1064 local substr, following = delimited('[',']',str:sub(p.byte))
843 1065 p.next.byte = following + p.byte
844 1066 local found = false
845 1067 for _,i in pairs(ct.spanctls) do
846 1068 if startswith(substr, i.seq) then
................................................................................
942 1164 c.expand_next = 0
943 1165 end
944 1166 end;
945 1167 }
946 1168
947 1169 local function insert_table_row(l,c)
948 1170 local row = {}
949 - for kind, a1, text, a2 in l:gmatch '([+|])(:?)%s*([^:+|]*)%s*(:?)' do
950 - local header = kind == '+'
951 - local align
952 - if a1 == ':' and a2 ~= ':' then
953 - align = 'left'
954 - elseif a1 == ':' and a2 == ':' then
955 - align = 'center'
956 - elseif a1 ~= ':' and a2 == ':' then
957 - align = 'right'
958 - end
959 - text = text:match '^%s*(.-)%s*$'
960 - table.insert(row, {
961 - spans = ct.parse_span(text, c);
962 - align = align;
963 - header = header;
964 - })
1171 + local buf
1172 + local flush = function()
1173 + if buf then table.insert(row, buf) end
1174 + buf = { str = '' }
1175 + end
1176 + for c,p in eachcode(l) do
1177 + if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then
1178 + flush()
1179 + buf.header = c == '+'
1180 + elseif c == ':' then
1181 + local lst = l:sub(p.byte-#c,p.byte-#c)
1182 + local nxt = l:sub(p.next.byte,p.next.byte)
1183 + if lst == '|' or lst == '+' and l:sub(p.byte-2,p.byte-2) ~= '\\' then
1184 + buf.align = 'left'
1185 + elseif nxt == '|' or nxt == '|' then
1186 + if buf.align == 'left' then
1187 + buf.align = 'center'
1188 + else
1189 + buf.align = 'right'
1190 + end
1191 + else
1192 + buf.str = buf.str .. c
1193 + end
1194 + elseif c:match '%s' then
1195 + if buf.str ~= '' then buf.str = buf.str .. c end
1196 + elseif c == '\\' then
1197 + local nxt = l:sub(p.next.byte,p.next.byte)
1198 + if nxt == '|' or nxt == '+' or nxt == ':' then
1199 + buf.str = buf.str .. nxt
1200 + p.next.byte = p.next.byte + #nxt
1201 + p.next.code = p.next.code + 1
1202 + else
1203 + buf.str = buf.str .. c
1204 + end
1205 + else
1206 + buf.str = buf.str .. c
1207 + end
1208 + end
1209 + if buf.str ~= '' then flush() end
1210 + for _,v in pairs(row) do
1211 + v.spans = ct.parse_span(v.str, c)
965 1212 end
966 1213 if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then
967 1214 local tbl = c.sec.blocks[#c.sec.blocks]
968 1215 table.insert(tbl.rows, row)
969 1216 else
970 1217 table.insert(c.sec.blocks, {
971 1218 kind = 'table';
................................................................................
974 1221 })
975 1222 end
976 1223 end
977 1224
978 1225 ct.ctlseqs = {
979 1226 {seq = '.', fn = insert_paragraph};
980 1227 {seq = '¶', fn = insert_paragraph};
1228 + {seq = '❡', fn = insert_paragraph};
981 1229 {seq = '#', fn = insert_section};
982 1230 {seq = '§', fn = insert_section};
983 1231 {seq = '+', fn = insert_table_row};
984 1232 {seq = '|', fn = insert_table_row};
985 1233 {seq = '│', fn = insert_table_row};
986 1234 {pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
987 1235 local stars = l:match '^([*:]+)'
................................................................................
1005 1253 local words = function(i)
1006 1254 local wds = {}
1007 1255 if i == 0 then return cmdline end
1008 1256 for w,pos in cmdline:gmatch '([^%s]+)()' do
1009 1257 table.insert(wds, w)
1010 1258 i = i - 1
1011 1259 if i == 0 then
1012 - return table.unpack(wds), cmdline:sub(pos)
1260 + table.insert(wds,cmdline:sub(pos))
1261 + return table.unpack(wds)
1013 1262 end
1014 1263 end
1015 1264 end
1016 1265
1017 1266 local cmd, rest = words(1)
1018 1267 if ct.directives[cmd] then
1019 1268 ct.directives[cmd](words,c)
1269 + elseif cmd == c.doc.stage.mode['render:format'] then
1270 + -- this is a directive for the renderer; insert it into the tree as is
1271 + c:insert {
1272 + kind = 'directive';
1273 + critical = crit == '!';
1274 + words = words;
1275 + }
1020 1276 elseif crit == '!' then
1021 1277 c:fail('critical directive %s not supported',cmd)
1022 1278 end
1023 1279 end;};
1024 1280 {seq = '~~~', fn = blockwrap(function(l,c)
1025 1281 local extract = function(ptn, str)
1026 1282 local start, stop = str:find(ptn)
................................................................................
1030 1286 return ex, n
1031 1287 end
1032 1288 local lang, id, title
1033 1289 if l:match '^~~~%s*$' then -- no args
1034 1290 elseif l:match '^~~~.*~~~%s*$' then -- CT style
1035 1291 local s = l:match '^~~~%s*(.-)%s*~~~%s*$'
1036 1292 lang, s = extract('%b[]', s)
1037 - lang = lang:sub(2,-2)
1293 + if lang then lang = lang:sub(2,-2) end
1038 1294 id, title = extract('#[^%s]+', s)
1039 1295 if id then id = id:sub(2) end
1040 1296 elseif l:match '^~~~' then -- MD shorthand style
1041 1297 lang = l:match '^~~~%s*(.-)%s*$'
1042 1298 end
1043 1299 c.mode = {
1044 1300 kind = 'code';
................................................................................
1067 1323 end
1068 1324 end; fn = blockwrap(function()
1069 1325 return { kind = 'horiz-rule' }
1070 1326 end)};
1071 1327 {fn = insert_paragraph};
1072 1328 }
1073 1329
1074 -function ct.parse(file, src)
1330 +function ct.parse(file, src, mode)
1075 1331 local function
1076 1332 is_whitespace(cp)
1077 1333 return cp == 0x20
1078 1334 end
1079 1335
1080 1336 local ctx = ct.ctx.mk(src)
1081 1337 ctx.line = 0
1082 1338 ctx.doc = ct.doc.mk()
1339 + ctx.doc.src = src
1340 + ctx.doc.stage = {
1341 + kind = 'parse';
1342 + mode = mode;
1343 + }
1083 1344 ctx.sec = ctx.doc:mksec() -- toplevel section
1084 1345 ctx.sec.origin = ctx:clone()
1085 1346
1086 1347 for full_line in file:lines() do ctx.line = ctx.line + 1
1087 1348 local l
1088 1349 for p, c in utf8.codes(full_line) do
1089 1350 if not is_whitespace(c) then
................................................................................
1153 1414 for k, v in pairs(list) do
1154 1415 if fn(k,v) then new[k] = v end
1155 1416 end
1156 1417 return new
1157 1418 end
1158 1419
1159 1420 local function main(input, output, log, mode, vars)
1160 - local doc = ct.parse(input.stream, input.src)
1421 + local doc = ct.parse(input.stream, input.src, mode)
1161 1422 input.stream:close()
1162 1423 if mode['parse:show-tree'] then
1163 1424 log:write(dump(doc))
1164 1425 end
1165 1426
1166 1427 if not mode['render:format'] then
1167 1428 error 'what output format should i translate the input to?'
1168 1429 end
1430 + if mode['render:format'] == 'none' then return 0 end
1169 1431 if not ct.render[mode['render:format']] then
1170 - error(string.format('output format “%s” unsupported', mode['render:format']))
1432 + ct.exns.unimpl('output format “%s” unsupported', mode['render:format']):throw()
1171 1433 end
1172 1434
1173 1435 local render_opts = kmap(function(k,v)
1174 1436 return k:sub(2+#mode['render:format'])
1175 1437 end, kfilter(mode, function(m)
1176 1438 return startswith(m, mode['render:format']..':')
1177 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 + }
1178 1450
1179 1451 output:write(ct.render[mode['render:format']](doc, render_opts))
1452 + return 0
1180 1453 end
1181 1454
1182 1455 local inp,outp,log = io.stdin, io.stdout, io.stderr
1183 1456
1184 1457 local function entry_cli()
1185 1458 local mode, vars, input = default_mode, {}, {
1186 1459 stream = inp;
................................................................................
1227 1500 log = function(file)
1228 1501 local nf = io.open(file,'wb')
1229 1502 if nf then log:close() log = nf else
1230 1503 ct.exns.io('could not open log file for writing', 'open',file):throw()
1231 1504 end
1232 1505 end;
1233 1506 define = function(key,value)
1234 - -- set context key
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
1235 1511 end;
1236 1512 mode = function(key,value) mode[checkmodekey(key)] = value end;
1237 1513 ['mode-set'] = function(key) mode[checkmodekey(key)] = true end;
1238 1514 ['mode-clear'] = function(key) mode[checkmodekey(key)] = false end;
1239 1515 }
1240 1516
1241 1517 local args = {}
................................................................................
1287 1563 if args[1] and args[1] ~= '' then
1288 1564 local file = io.open(arg[1], "rb")
1289 1565 if not file then error('unable to load file ' .. args[1]) end
1290 1566 input.stream = file
1291 1567 input.src.file = args[1]
1292 1568 end
1293 1569
1294 - main(input, outp, log, mode, vars)
1570 + return main(input, outp, log, mode, vars)
1295 1571 end
1296 1572
1297 --- local ok, e = pcall(entry_cli)
1298 -local ok, e = true, entry_cli()
1573 +local ok, e = pcall(entry_cli)
1574 +-- local ok, e = true, entry_cli()
1299 1575 if not ok then
1300 1576 local str = 'translation failure'
1577 + if ct.exn.is(e) then
1578 + str = e.kind.desc
1579 + end
1301 1580 local color = false
1302 1581 if log:seek() == nil then
1303 1582 -- this is not a very reliable heuristic for detecting
1304 1583 -- attachment to a tty but it's better than nothing
1305 1584 if os.getenv('COLORTERM') then
1306 1585 color = true
1307 1586 else
................................................................................
1308 1587 local term = os.getenv('TERM')
1309 1588 if term:find 'color' then color = true end
1310 1589 end
1311 1590 end
1312 1591 if color then
1313 1592 str = string.format('\27[1;31m%s\27[m', str)
1314 1593 end
1315 - log:write(string.format('[%s] %s\n\t%s\n', os.date(), str, e))
1594 + log:write(string.format('%s: %s\n', str, e))
1316 1595 os.exit(1)
1317 1596 end
1597 +os.exit(e)