Differences From
Artifact [eb3cc08f95]:
2 2 -- ~ lexi hale <lexi@hale.su>
3 3 -- © AGPLv3
4 4 -- ? reference implementation of the cortav document language
5 5
6 6 local ss = require 'sirsem'
7 7 -- aliases for commonly used sirsem funcs
8 8 local startswith = ss.str.begins
9 -local eachcode = ss.str.enc.utf8.each
10 9 local dump = ss.dump
11 10 local declare = ss.declare
12 11
13 12 -- make this module available to require() when linked into a lua bytecode program with luac
14 13 local ct = ss.namespace 'cortav'
15 14 ct.info = {
16 15 version = ss.version {0,1; 'devel'};
................................................................................
81 80 end);
82 81 cli = ss.exnkind 'command line parse error';
83 82 mode = ss.exnkind('bad mode', function(msg, ...)
84 83 return string.format("mode “%s” "..msg, ...)
85 84 end);
86 85 unimpl = ss.exnkind 'feature not implemented';
87 86 ext = ss.exnkind 'extension error';
87 + enc = ss.exnkind('encoding error', function(msg, ...)
88 + return string.format('[%s]' .. msg, ...)
89 + end);
88 90 }
89 91
90 92 ct.ctx = declare {
91 93 mk = function(src) return {src = src} end;
92 94 ident = 'context';
93 95 cast = {
94 96 string = function(me)
................................................................................
112 114 table.insert(self.sec.blocks,block)
113 115 return block
114 116 end;
115 117 ref = function(self,id)
116 118 if not id:find'%.' then
117 119 local rid = self.sec.refs[id]
118 120 if self.sec.refs[id] then
119 - return self.sec.refs[id]
121 + return self.sec.refs[id], id, self.sec
120 122 else self:fail("no such ref %s in current section", id or '') end
121 123 else
122 124 local sec, ref = string.match(id, "(.-)%.(.+)")
123 125 local s = self.doc.sections[sec]
124 126 if s then
125 127 if s.refs[ref] then
126 - return s.refs[ref]
128 + return s.refs[ref], ref, sec
127 129 else self:fail("no such ref %s in section %s", ref, sec) end
128 130 else self:fail("no such section %s", sec) end
129 131 end
130 132 end
131 133 };
132 134 }
133 135
................................................................................
217 219 meta = {};
218 220 vars = {};
219 221 ext = {
220 222 inhibit = {};
221 223 need = {};
222 224 use = {};
223 225 };
226 + enc = ss.str.enc.utf8;
224 227 } end;
225 228 construct = function(me)
226 229 me.docjob = ct.ext.job('doc', me, nil)
227 230 end;
228 231 }
229 232
230 233 -- FP helper functions
................................................................................
397 400
398 401 -- renderer engines
399 402 function ct.render.html(doc, opts)
400 403 local doctitle = opts['title']
401 404 local f = string.format
402 405 local ids = {}
403 406 local canonicalID = {}
404 - local function getSafeID(obj)
407 + local function getSafeID(obj,pfx)
408 + pfx = pfx or ''
405 409 if canonicalID[obj] then
406 410 return canonicalID[obj]
407 - elseif obj.id and ids[obj.id] then
411 + elseif obj.id and ids[pfx .. obj.id] then
412 + local objid = pfx .. obj.id
408 413 local newid
409 414 local i = 1
410 - repeat newid = obj.id .. string.format('-%x', i)
415 + repeat newid = objid .. string.format('-%x', i)
411 416 i = i + 1 until not ids[newid]
412 417 ids[newid] = obj
413 418 canonicalID[obj] = newid
414 419 return newid
415 420 else
416 421 local cid = obj.id
417 422 if not cid then
418 423 local i = 1
419 - repeat cid = string.format('x-%x', i)
424 + repeat cid = string.format('%sx-%x', pfx, i)
420 425 i = i + 1 until not ids[cid]
421 426 end
422 427 ids[cid] = obj
423 428 canonicalID[obj] = cid
424 429 return cid
425 430 end
426 431 end
427 432
433 + local footnotes = {}
434 + local footnotecount = 0
435 +
428 436 local langsused = {}
429 437 local langpairs = {
430 438 lua = { color = 0x9377ff };
431 439 terra = { color = 0xff77c8 };
432 440 c = { name = 'C', color = 0x77ffe8 };
433 441 html = { color = 0xfff877 };
434 442 scheme = { color = 0x77ff88 };
435 443 lisp = { color = 0x77ff88 };
436 444 fortran = { color = 0xff779a };
437 445 python = { color = 0xffd277 };
438 - python = { color = 0xcdd6ff };
446 + ruby = { color = 0xcdd6ff };
439 447 }
440 448
441 449 local stylesets = {
450 + footnote = [[
451 + div.footnote {
452 + font-family: 90%;
453 + display: none;
454 + grid-template-columns: 1em 1fr min-content;
455 + grid-template-rows: 1fr min-content;
456 + position: fixed;
457 + padding: 1em;
458 + background: @tone(0.05);
459 + border: black;
460 + margin:auto;
461 + }
462 + div.footnote:target { display:grid; }
463 + @media screen {
464 + div.footnote {
465 + left: 10em;
466 + right: 10em;
467 + max-width: calc(@width + 2em);
468 + max-height: 30vw;
469 + bottom: 1em;
470 + }
471 + }
472 + @media print {
473 + div.footnote {
474 + position: relative;
475 + }
476 + div.footnote:first-of-type {
477 + border-top: 1px solid black;
478 + }
479 + }
480 +
481 + div.footnote > a[href="#0"]{
482 + grid-row: 2/3;
483 + grid-column: 3/4;
484 + display: block;
485 + padding: 0.2em 0.7em;
486 + text-align: center;
487 + text-decoration: none;
488 + background: @tone(0.2);
489 + color: @tone(1);
490 + border: 1px solid black;
491 + margin-top: 0.6em;
492 + -webkit-user-select: none;
493 + -ms-user-select: none;
494 + user-select: none;
495 + -webkit-user-drag: none;
496 + user-drag: none;
497 + }
498 + div.footnote > a[href="#0"]:hover {
499 + background: @tone(0.3);
500 + color: @tone(2);
501 + }
502 + div.footnote > a[href="#0"]:active {
503 + background: @tone(0.05);
504 + color: @tone(0.4);
505 + }
506 + @media print {
507 + div.footnote > a[href="#0"]{
508 + display:none;
509 + }
510 + }
511 + div.footnote > div.number {
512 + text-align:right;
513 + grid-row: 1/2;
514 + grid-column: 1/2;
515 + }
516 + div.footnote > div.text {
517 + grid-row: 1/2;
518 + grid-column: 2/4;
519 + padding-left: 1em;
520 + overflow-y: scroll;
521 + }
522 + ]];
442 523 header = [[
524 + body { padding: 0 2.5em !important }
443 525 h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
444 526 h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
445 527 h2 { font-size: 130%; margin: 0em -0.7em; }
446 528 h3 { font-size: 110%; margin: 0em -0.5em; }
447 529 h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; }
448 530 h5 { font-size: 90%; font-weight: normal; }
449 531 h6 { font-size: 80%; font-weight: normal; }
................................................................................
490 572 section:target > :is(h1,h2,h3,h4,h5,h6) {
491 573
492 574 }
493 575 ]];
494 576 paragraph = [[
495 577 p {
496 578 margin: 0.7em 0;
579 + text-align: justify;
497 580 }
498 581 section {
499 582 margin: 1.2em 0;
500 583 }
501 584 section:first-child { margin-top: 0; }
502 585 ]];
503 586 accent = [[
504 - body { background: @bg; color: @fg }
505 - a[href] {
506 - color: @tone(0.7 30);
507 - text-decoration-color: @tone/0.4(0.7 30);
587 + @media screen {
588 + body { background: @bg; color: @fg }
589 + a[href] {
590 + color: @tone(0.7 30);
591 + text-decoration-color: @tone/0.4(0.7 30);
592 + }
593 + a[href]:hover {
594 + color: @tone(0.9 30);
595 + text-decoration-color: @tone/0.7(0.7 30);
596 + }
597 + h1 { color: @tone(2); }
598 + h2 { color: @tone(1.5); }
599 + h3 { color: @tone(1.2); }
600 + h4 { color: @tone(1); }
601 + h5,h6 { color: @tone(0.8); }
508 602 }
509 - a[href]:hover {
510 - color: @tone(0.9 30);
511 - text-decoration-color: @tone/0.7(0.7 30);
603 + @media print {
604 + a[href] {
605 + text-decoration: none;
606 + color: black;
607 + font-weight: bold;
608 + }
609 + h1,h2,h3,h4,h5,h6 {
610 + border-bottom: 1px black;
611 + }
512 612 }
513 - h1 { color: @tone(2); }
514 - h2 { color: @tone(1.5); }
515 - h3 { color: @tone(1.2); }
516 - h4 { color: @tone(1); }
517 - h5,h6 { color: @tone(0.8); }
518 613 ]];
614 + aside = [[
615 + section > aside {
616 + text-align: justify;
617 + margin: 0 1.5em;
618 + padding: 0.5em 0.8em;
619 + background: @tone(0.05);
620 + font-size: 90%;
621 + border-left: 5px solid @tone(0.2 15);
622 + border-right: 5px solid @tone(0.2 15);
623 + }
624 + section > aside p {
625 + margin: 0;
626 + margin-top: 0.6em;
627 + }
628 + section > aside p:first-child {
629 + margin: 0;
630 + }
631 + ]];
519 632 code = [[
520 633 code {
521 - background: @fg;
634 + display: inline-block;
635 + background: @tone(0.9);
522 636 color: @bg;
523 637 font-family: monospace;
524 638 font-size: 90%;
525 639 padding: 3px 5px;
526 640 }
641 + ]];
642 + var = [[
643 + var {
644 + font-style: italic;
645 + font-family: monospace;
646 + color: @tone(0.7);
647 + }
648 + code var {
649 + color: @tone(0.25);
650 + }
651 + ]];
652 + math = [[
653 + span.equation {
654 + display: inline-block;
655 + background: @tone(0.08);
656 + color: @tone(2);
657 + padding: 0.1em 0.3em;
658 + border: 1px solid @tone(0.5);
659 + }
527 660 ]];
528 661 abbr = [[
529 662 abbr[title] { cursor: help; }
530 663 ]];
531 664 editors_markup = [[]];
532 665 block_code_listing = [[
533 - section > figure.listing {
666 + figure.listing {
534 667 font-family: monospace;
535 668 background: @tone(0.05);
536 669 color: @fg;
537 670 padding: 0;
538 671 margin: 0.3em 0;
539 672 counter-reset: line-number;
540 673 position: relative;
541 674 border: 1px solid @fg;
542 675 }
543 - section > figure.listing>div {
676 + figure.listing>div {
544 677 white-space: pre-wrap;
678 + tab-size: 3;
679 + -moz-tab-size: 3;
545 680 counter-increment: line-number;
546 681 text-indent: -2.3em;
547 682 margin-left: 2.3em;
548 683 }
549 - section > figure.listing>:is(div,hr)::before {
684 + figure.listing>:is(div,hr)::before {
550 685 width: 1.0em;
551 686 padding: 0.2em 0.4em;
552 687 text-align: right;
553 688 display: inline-block;
554 689 background-color: @tone(0.2);
555 690 border-right: 1px solid @fg;
556 691 content: counter(line-number);
557 692 margin-right: 0.3em;
558 693 }
559 - section > figure.listing>hr::before {
694 + figure.listing>hr::before {
560 695 color: transparent;
561 696 padding-top: 0;
562 697 padding-bottom: 0;
563 698 }
564 - section > figure.listing>div::before {
699 + figure.listing>div::before {
565 700 color: @fg;
566 701 }
567 - section > figure.listing>div:last-child::before {
702 + figure.listing>div:last-child::before {
568 703 padding-bottom: 0.5em;
569 704 }
570 - section > figure.listing>figcaption:first-child {
705 + figure.listing>figcaption:first-child {
571 706 border: none;
572 707 border-bottom: 1px solid @fg;
573 708 }
574 - section > figure.listing>figcaption::after {
709 + figure.listing>figcaption::after {
575 710 display: block;
576 711 float: right;
577 712 font-weight: normal;
578 713 font-style: italic;
579 714 font-size: 70%;
580 715 padding-top: 0.3em;
581 716 }
582 - section > figure.listing>figcaption {
717 + figure.listing>figcaption {
583 718 font-family: sans-serif;
584 719 font-size: 120%;
585 720 padding: 0.2em 0.4em;
586 721 border: none;
587 722 color: @tone(2);
588 723 }
589 - section > figure.listing > hr {
724 + figure.listing > hr {
590 725 border: none;
591 726 margin: 0;
592 727 height: 0.7em;
593 728 counter-increment: line-number;
594 729 }
595 730 ]];
596 731 }
................................................................................
604 739 stylesets = stylesets;
605 740 stylesets_active = stylesNeeded;
606 741 obj_htmlid = getSafeID;
607 742 -- remaining fields added later
608 743 }
609 744
610 745 local renderJob = doc:job('render_html', nil, render_state_handle)
746 + doc.stage.job = renderJob;
611 747
612 748 local runhook = function(h, ...)
613 749 return renderJob:hook(h, render_state_handle, ...)
614 750 end
615 751
616 - local function getSpanRenderers(procs)
752 + local tagproc do
753 + local elt = function(t,attrs)
754 + return f('<%s%s>', t,
755 + attrs and ss.reduce(function(a,b) return a..b end, '',
756 + ss.map(function(v,k)
757 + if v == true
758 + then return ' '..k
759 + elseif v then return f(' %s="%s"', k, v)
760 + end
761 + end, attrs)) or '')
762 + end
763 +
764 + tagproc = {
765 + toTXT = {
766 + tag = function(t,a,v) return v end;
767 + elt = function(t,a) return '' end;
768 + catenate = table.concat;
769 + };
770 + toIR = {
771 + tag = function(t,a,v,o) return {
772 + tag = t, attrs = a;
773 + nodes = type(v) == 'string' and {v} or v, src = o
774 + } end;
775 +
776 + elt = function(t,a,o) return {
777 + tag = t, attrs = a, src = o
778 + } end;
779 +
780 + catenate = function(...) return ... end;
781 + };
782 + toHTML = {
783 + elt = elt;
784 + tag = function(t,attrs,body)
785 + return f('%s%s</%s>', elt(t,attrs), body, t)
786 + end;
787 + catenate = table.concat;
788 + };
789 + }
790 + end
791 +
792 + local function getBaseRenderers(procs, span_renderers)
617 793 local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
618 794 local htmlDoc = function(title, head, body)
619 795 return [[<!doctype html>]] .. tag('html',nil,
620 796 tag('head', nil,
621 797 elt('meta',{charset = 'utf-8'}) ..
622 798 (title and tag('title', nil, title) or '') ..
623 799 (head or '')) ..
624 800 tag('body', nil, body or ''))
625 801 end
626 802
627 - local span_renderers = {}
628 803 local function htmlSpan(spans, block, sec)
629 804 local text = {}
630 805 for k,v in pairs(spans) do
631 806 if type(v) == 'string' then
632 - table.insert(text,(v:gsub('[<>&"]',
633 - function(x)
807 + v=v:gsub('[<>&"]', function(x)
634 808 return string.format('&#%02u;', string.byte(x))
635 - end)))
809 + end)
810 + for fn, ext in renderJob:each('hook','render_html_sanitize') do
811 + v = fn(renderJob:delegate(ext), v)
812 + end
813 + table.insert(text,v)
636 814 else
637 - table.insert(text, span_renderers[v.kind](v, block, sec))
815 + table.insert(text, (span_renderers[v.kind](v, block, sec)))
638 816 end
639 817 end
640 818 return table.concat(text)
641 819 end
820 + return {htmlDoc=htmlDoc, htmlSpan=htmlSpan}
821 + end
822 +
823 + local spanparse = function(...)
824 + local s = ct.parse_span(...)
825 + doc.docjob:hook('meddle_span', s)
826 + return s
827 + end
828 +
829 + local cssRulesFor = {}
830 + local function getSpanRenderers(procs)
831 + local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
832 + local span_renderers = {}
833 + local plainrdr = getBaseRenderers(tagproc.toTXT, span_renderers)
834 + local htmlSpan = getBaseRenderers(procs, span_renderers).htmlSpan
642 835
643 836 function span_renderers.format(sp,...)
644 - local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code' }
837 + local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'}
645 838 if sp.style == 'literal' and not opts['fossil-uv'] then
646 839 stylesNeeded.code = true
647 - end
648 - if sp.style == 'del' or sp.style == 'ins' then
840 + elseif sp.style == 'strike' or sp.style == 'insert' then
649 841 stylesNeeded.editors_markup = true
842 + elseif sp.style == 'variable' then
843 + stylesNeeded.var = true
650 844 end
651 845 return tag(tags[sp.style],nil,htmlSpan(sp.spans,...))
652 846 end
653 847
654 - function span_renderers.term(t,b,s)
848 + function span_renderers.deref(t,b,s)
655 849 local r = b.origin:ref(t.ref)
656 850 local name = t.ref
657 851 if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
658 - if type(r) ~= 'string' then
659 - b.origin:fail('%s is an object, not a reference', t.ref)
852 + if type(r) == 'string' then
853 + stylesNeeded.abbr = true
854 + return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name)
660 855 end
661 - stylesNeeded.abbr = true
662 - return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name)
663 - end
664 -
665 - function span_renderers.macro(m,b,s)
666 - local r = b.origin:ref(m.macro)
667 - if type(r) ~= 'string' then
668 - b.origin:fail('%s is an object, not a reference', t.ref)
856 + if r.kind == 'resource' then
857 + local rid = getSafeID(r, 'res-')
858 + if r.class == 'image' then
859 + if not cssRulesFor[r] then
860 + local css = prepcss(string.format([[
861 + section p > .%s {
862 + }
863 + ]], rid))
864 + stylesets[r] = css
865 + cssRulesFor[r] = css
866 + stylesNeeded[r] = true
867 + end
868 + return tag('div',{class=rid},catenate{'blaah'})
869 + elseif r.class == 'video' then
870 + local vid = {}
871 + return tag('video',nil,vid)
872 + elseif r.class == 'font' then
873 + b.origin:fail('fonts cannot be instantiated, use %font directive instead')
874 + end
875 + else
876 + b.origin:fail('%s is not an object that can be embedded', t.ref)
669 877 end
670 - local mctx = b.origin:clone()
671 - mctx.invocation = m
672 - return htmlSpan(ct.parse_span(r, mctx),b,s)
673 878 end
674 879
675 880 function span_renderers.var(v,b,s)
676 881 local val
677 882 if v.pos then
678 883 if not v.origin.invocation then
679 884 v.origin:fail 'positional arguments can only be used in a macro invocation'
................................................................................
686 891 end
687 892 if v.raw then
688 893 return val
689 894 else
690 895 return htmlSpan(ct.parse_span(val, v.origin), b, s)
691 896 end
692 897 end
898 +
899 + function span_renderers.raw(v,b,s)
900 + return htmlSpan(v.spans, b, s)
901 + end
693 902
694 903 function span_renderers.link(sp,b,s)
695 904 local href
696 905 if b.origin.doc.sections[sp.ref] then
697 906 href = '#' .. sp.ref
698 907 else
699 908 if sp.addr then href = sp.addr else
................................................................................
701 910 if type(r) == 'table' then
702 911 href = '#' .. getSafeID(r)
703 912 else href = r end
704 913 end
705 914 end
706 915 return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href)
707 916 end
708 - return {
709 - span_renderers = span_renderers;
710 - htmlSpan = htmlSpan;
711 - htmlDoc = htmlDoc;
712 - }
917 +
918 + span_renderers['line-break'] = function(sp,b,s)
919 + return elt('br')
920 + end
921 +
922 + function span_renderers.macro(m,b,s)
923 + local macroname = plainrdr.htmlSpan(
924 + ct.parse_span(m.macro, b.origin), b,s)
925 + local r = b.origin:ref(macroname)
926 + if type(r) ~= 'string' then
927 + b.origin:fail('%s is an object, not a reference', t.ref)
928 + end
929 + local mctx = b.origin:clone()
930 + mctx.invocation = m
931 + return htmlSpan(ct.parse_span(r, mctx),b,s)
932 + end
933 + function span_renderers.math(m,b,s)
934 + stylesNeeded.math = true
935 + return tag('span',{class='equation'},htmlSpan(m.spans, b, s))
936 + end;
937 + function span_renderers.directive(d,b,s)
938 + if d.ext == 'html' then
939 + elseif b.origin.doc:allow_ext(d.ext) then
940 + elseif d.crit then
941 + b.origin:fail('critical extension %s unavailable', d.ext)
942 + elseif d.failthru then
943 + return htmlSpan(d.spans, b, s)
944 + end
945 + end
946 + function span_renderers.footnote(f,b,s)
947 + stylesNeeded.footnote = true
948 + local source, sid, ssec = b.origin:ref(f.ref)
949 + local cnc = getSafeID(ssec) .. ' ' .. sid
950 + local fn
951 + if footnotes[cnc] then
952 + fn = footnotes[cnc]
953 + else
954 + footnotecount = footnotecount + 1
955 + fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
956 + fn.id = getSafeID(fn)
957 + footnotes[cnc] = fn
958 + end
959 + return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) ..
960 + tag('sup',nil, fn.num))
961 + end
962 +
963 + return span_renderers
713 964 end
714 -
715 965
716 966 local function getBlockRenderers(procs, sr)
717 967 local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
718 968 local null = function() return catenate{} end
719 969
720 970 local block_renderers = {
721 971 anchor = function(b,s)
................................................................................
766 1016 if #l > 0 then
767 1017 return tag('div',nil,sr.htmlSpan(l, b, s))
768 1018 else
769 1019 return elt('hr')
770 1020 end
771 1021 end, b.lines)
772 1022 if b.title then
773 - table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title)))
1023 + table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title)))
774 1024 end
775 1025 if b.lang then langsused[b.lang] = true end
776 1026 return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
777 1027 end;
778 1028 aside = function(b,s)
779 1029 local bn = {}
780 - for _,v in pairs(b.lines) do
781 - table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s)))
1030 + stylesNeeded.aside = true
1031 + if #b.lines == 1 then
1032 + bn[1] = sr.htmlSpan(b.lines[1], b, s)
1033 + else
1034 + for _,v in pairs(b.lines) do
1035 + table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s)))
1036 + end
782 1037 end
783 1038 return tag('aside', {}, bn)
784 1039 end;
785 - ['break'] = function() --[[nop]] end;
1040 + ['break'] = function() -- HACK
1041 + -- lists need to be rewritten to work like asides
1042 + return '';
1043 + end;
786 1044 }
787 1045 return block_renderers;
788 1046 end
789 1047
790 1048 local function getRenderers(procs)
791 - local r = getSpanRenderers(procs)
1049 + local span_renderers = getSpanRenderers(procs)
1050 + local r = getBaseRenderers(procs,span_renderers)
792 1051 r.block_renderers = getBlockRenderers(procs, r)
793 1052 return r
794 - end
795 -
796 - local tagproc do
797 - local elt = function(t,attrs)
798 - return f('<%s%s>', t,
799 - attrs and ss.reduce(function(a,b) return a..b end, '',
800 - ss.map(function(v,k)
801 - if v == true
802 - then return ' '..k
803 - elseif v then return f(' %s="%s"', k, v)
804 - end
805 - end, attrs)) or '')
806 - end
807 -
808 - tagproc = {
809 - toTXT = {
810 - tag = function(t,a,v) return v end;
811 - elt = function(t,a) return '' end;
812 - catenate = table.concat;
813 - };
814 - toIR = {
815 - tag = function(t,a,v,o) return {
816 - tag = t, attrs = a;
817 - nodes = type(v) == 'string' and {v} or v, src = o
818 - } end;
819 -
820 - elt = function(t,a,o) return {
821 - tag = t, attrs = a, src = o
822 - } end;
823 -
824 - catenate = function(...) return ... end;
825 - };
826 - toHTML = {
827 - elt = elt;
828 - tag = function(t,attrs,body)
829 - return f('%s%s</%s>', elt(t,attrs), body, t)
830 - end;
831 - catenate = table.concat;
832 - };
833 - }
834 1053 end
835 1054
836 1055 local astproc = {
837 1056 toHTML = getRenderers(tagproc.toHTML);
838 1057 toTXT = getRenderers(tagproc.toTXT);
839 1058 toIR = { };
840 1059 }
................................................................................
853 1072 local ir = {}
854 1073 local dr = astproc.toHTML -- default renderers
855 1074 local plainr = astproc.toTXT
856 1075 local irBlockRdrs = astproc.toIR.block_renderers;
857 1076
858 1077 render_state_handle.ir = ir;
859 1078
1079 + local function renderBlocks(blocks, irs)
1080 + for i, block in ipairs(blocks) do
1081 + local rd
1082 + if irBlockRdrs[block.kind] then
1083 + rd = irBlockRdrs[block.kind](block,sec)
1084 + else
1085 + local rdr = renderJob:proc('render',block.kind,'html')
1086 + if rdr then
1087 + rd = rdr({
1088 + state = render_state_handle;
1089 + tagproc = tagproc.toIR;
1090 + astproc = astproc.toIR;
1091 + }, block, sec)
1092 + end
1093 + end
1094 + if rd then
1095 + if opts['heading-anchors'] and block == sec.heading_node then
1096 + stylesNeeded.headingAnchors = true
1097 + table.insert(rd.nodes, ' ')
1098 + table.insert(rd.nodes, {
1099 + tag = 'a';
1100 + attrs = {href = '#' .. irs.attrs.id, class='anchor'};
1101 + nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'};
1102 + })
1103 + end
1104 + if rd.src and rd.src.origin.lang then
1105 + if not rd.attrs then rd.attrs = {} end
1106 + rd.attrs.lang = rd.src.origin.lang
1107 + end
1108 + table.insert(irs.nodes, rd)
1109 + runhook('ir_section_node_insert', rd, irs, sec)
1110 + end
1111 + end
1112 + end
860 1113 runhook('ir_assemble', ir)
861 1114 for i, sec in ipairs(doc.secorder) do
862 1115 if doctitle == nil and sec.depth == 1 and sec.heading_node then
863 1116 doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
864 1117 end
865 1118 local irs
866 1119 if sec.kind == 'ordinary' then
867 1120 if #(sec.blocks) > 0 then
868 1121 irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
869 -
870 1122 runhook('ir_section_build', irs, sec)
871 -
872 - for i, block in ipairs(sec.blocks) do
873 - local rd
874 - if irBlockRdrs[block.kind] then
875 - rd = irBlockRdrs[block.kind](block,sec)
876 - else
877 - local rdr = renderJob:proc('render',block.kind,'html')
878 - if rdr then
879 - rd = rdr({
880 - state = render_state_handle;
881 - tagproc = tagproc.toIR;
882 - astproc = astproc.toIR;
883 - }, block, sec)
884 - end
885 - end
886 - if rd then
887 - if opts['heading-anchors'] and block == sec.heading_node then
888 - stylesNeeded.headingAnchors = true
889 - table.insert(rd.nodes, ' ')
890 - table.insert(rd.nodes, {
891 - tag = 'a';
892 - attrs = {href = '#' .. irs.attrs.id, class='anchor'};
893 - nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '§'};
894 - })
895 - end
896 - table.insert(irs.nodes, rd)
897 - runhook('ir_section_node_insert', rd, irs, sec)
898 - end
899 - end
1123 + renderBlocks(sec.blocks, irs)
900 1124 end
901 1125 elseif sec.kind == 'blockquote' then
902 1126 elseif sec.kind == 'listing' then
903 1127 elseif sec.kind == 'embed' then
904 1128 end
905 1129 if irs then table.insert(ir, irs) end
906 1130 end
1131 +
1132 + for _, fn in pairs(footnotes) do
1133 + local tag = tagproc.toIR.tag
1134 + local body = {nodes={}}
1135 + local ftir = {}
1136 + for l in fn.source:gmatch('([^\n]*)') do
1137 + ct.parse_line(l, fn.origin, ftir)
1138 + end
1139 + renderBlocks(ftir,body)
1140 + local note = tag('div',{class='footnote',id=fn.id}, {
1141 + tag('div',{class='number'}, tostring(fn.num)),
1142 + tag('div',{class='text'}, body.nodes),
1143 + tag('a',{href='#0'},'close')
1144 + })
1145 + table.insert(ir, note)
1146 + end
907 1147
908 1148 -- restructure passes
909 1149 runhook('ir_restructure_pre', ir)
910 1150
911 1151 ---- list insertion pass
912 1152 local lists = {}
913 1153 for _, sec in pairs(ir) do
................................................................................
1033 1273 local tonespan = opts.accent and .1 or 0
1034 1274 local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan
1035 1275 local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan
1036 1276 if var == 'bg' then
1037 1277 return tone(tbg,nil,nil,tonumber(alpha))
1038 1278 elseif var == 'fg' then
1039 1279 return tone(tfg,nil,nil,tonumber(alpha))
1280 + elseif var == 'width' then
1281 + return opts['width'] or '100vw'
1040 1282 elseif var == 'tone' then
1041 1283 local l, sep, sat
1042 1284 for i=1,3 do -- 🙄
1043 1285 l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
1044 1286 if l then break end
1045 1287 end
1046 1288 l = ss.math.lerp(tonumber(l), tbg, tfg)
................................................................................
1124 1366 kind = 'var';
1125 1367 pos = pos;
1126 1368 raw = raw;
1127 1369 var = not pos and s or nil;
1128 1370 origin = c:clone();
1129 1371 }
1130 1372 end
1373 + end
1374 + local function insert_span_directive(crit, failthru)
1375 + return function(s,c)
1376 + local args = ss.str.breakwords(d.doc.enc, s, 1)
1377 + local brksyms = map(enc.encodeUCS, {
1378 + '.', ',', ':', ';', '!', '$', '&', '^',
1379 + '/', '?', '@', '='
1380 + })
1381 + local brkhash = {} for _,s in pairs(brksyms) do
1382 + brkhash[s] = true
1383 + end
1384 +
1385 + local extname = ''
1386 + local sym
1387 + local cmd = ''
1388 + for ch,p in ss.str.each(c.doc.enc, args[1]) do
1389 + if sym == nil then
1390 + if brkhash[ch] then
1391 + sym = ch
1392 + else
1393 + extname = extname .. ch
1394 + end
1395 + elseif brkhash[ch] then
1396 + sym = sym + ch
1397 + else
1398 + cmd = cmd + ch
1399 + end
1400 + end
1401 + if cmd == '' then cmd = nil end
1402 + local spans if failthru then
1403 + spans = ct.parse_span(args[2], c)
1404 + end
1405 + return {
1406 + kind = 'directive';
1407 + ext = extname;
1408 + cmd = cmd;
1409 + args = args;
1410 + crit = crit;
1411 + failthru = failthru;
1412 + spans = spans;
1413 + }
1414 + end
1131 1415 end
1132 1416 ct.spanctls = {
1133 1417 {seq = '!', parse = formatter 'emph'};
1134 1418 {seq = '*', parse = formatter 'strong'};
1135 1419 {seq = '~', parse = formatter 'strike'};
1136 - {seq = '+', parse = formatter 'inser'};
1420 + {seq = '+', parse = formatter 'insert'};
1137 1421 {seq = '\\', parse = function(s, c) -- raw
1138 - return s
1139 - end};
1140 - {seq = '$\\', parse = function(s, c) -- raw
1141 1422 return {
1142 - kind = 'format';
1143 - style = 'literal';
1423 + kind = 'raw';
1144 1424 spans = {s};
1145 1425 origin = c:clone();
1146 1426 }
1147 1427 end};
1148 - {seq = '$', parse = formatter 'literal'};
1428 + {seq = '`\\', parse = function(s, c) -- raw
1429 + local o = c:clone();
1430 + local str = ''
1431 + for c, p in ss.str.each(c.doc.enc, s) do
1432 + local q = p:esc()
1433 + if q then
1434 + str = str .. q
1435 + p.next.byte = p.next.byte + #q
1436 + else
1437 + str = str .. c
1438 + end
1439 + end
1440 + return {
1441 + kind = 'format';
1442 + style = 'literal';
1443 + spans = {{
1444 + kind = 'raw';
1445 + spans = {str};
1446 + origin = o;
1447 + }};
1448 + origin = o;
1449 + }
1450 + end};
1451 + {seq = '`', parse = formatter 'literal'};
1452 + {seq = '$', parse = formatter 'variable'};
1453 + {seq = '^', parse = function(s,c) --footnotes
1454 + local r, t = s:match '^([^%s]+)%s*(.-)$'
1455 + return {
1456 + kind = 'footnote';
1457 + ref = r;
1458 + spans = ct.parse_span(t, c);
1459 + origin = c:clone();
1460 + }
1461 + -- TODO support for footnote sections
1462 + end};
1463 + {seq = '=', parse = function(s,c) --math mode
1464 + local tx = {
1465 + ['%*'] = '×';
1466 + ['/'] = '÷';
1467 + }
1468 + for k,v in pairs(tx) do s = s:gsub(k,v) end
1469 + s=s:gsub('%^([0-9]+)', function(num)
1470 + local sup = {'⁰','¹','²','³','⁴','⁵','⁶','⁷','⁸','⁹'};
1471 + local r = ''
1472 + for i=1,#num do
1473 + r = r .. sup[1 + (num:byte(i) - 0x30)]
1474 + end
1475 + return r
1476 + end)
1477 + local m = {s} --TODO
1478 + return {
1479 + kind = 'math';
1480 + original = s;
1481 + spans = m;
1482 + origin = c:clone();
1483 + };
1484 + end};
1149 1485 {seq = '&', parse = function(s, c)
1150 1486 local r, t = s:match '^([^%s]+)%s*(.-)$'
1151 1487 return {
1152 - kind = 'term';
1488 + kind = 'deref';
1153 1489 spans = (t and t ~= "") and ct.parse_span(t, c) or {};
1154 1490 ref = r;
1155 1491 origin = c:clone();
1156 1492 }
1157 1493 end};
1158 1494 {seq = '^', parse = function(s, c)
1159 1495 local fn, t = s:match '^([^%s]+)%s*(.-)$'
................................................................................
1165 1501 }
1166 1502 end};
1167 1503 {seq = '>', parse = insert_link};
1168 1504 {seq = '→', parse = insert_link};
1169 1505 {seq = '🔗', parse = insert_link};
1170 1506 {seq = '##', parse = insert_var_ref(true)};
1171 1507 {seq = '#', parse = insert_var_ref(false)};
1508 + {seq = '%%', parse = function() --[[NOP]] end};
1509 + {seq = '%!', parse = insert_span_directive(true,false)};
1510 + {seq = '%:', parse = insert_span_directive(false,true)};
1511 + {seq = '%', parse = insert_span_directive(false,false)};
1172 1512 }
1173 1513 end
1174 1514
1175 1515 function ct.parse_span(str,ctx)
1176 1516 local function delimited(start, stop, s)
1177 1517 local r = { pcall(ss.str.delimit, nil, start, stop, s) }
1178 1518 if r[1] then return table.unpack(r, 2) end
1179 1519 ctx:fail(tostring(r[2]))
1180 1520 end
1181 1521 local buf = ""
1182 1522 local spans = {}
1183 1523 local function flush()
1184 1524 if buf ~= "" then
1525 + -- for fn, ext in ctx.doc.docjob:each('hook','doc_meddle_string') do
1526 + -- buf = fn(ctx.doc.docjob:delegate(ext), ctx, buf)
1527 + -- end
1185 1528 table.insert(spans, buf)
1186 1529 buf = ""
1187 1530 end
1188 1531 end
1189 1532 local skip = false
1190 - for c,p in eachcode(str) do
1191 - if skip == true then
1192 - skip = false
1193 - buf = buf .. c
1194 - elseif c == '\\' then
1195 - skip = true
1533 + for c,p in ss.str.each(ctx.doc.enc,str) do
1534 + local ba, ca, es = ctx.doc.enc.parse_escape(str:sub(p.byte))
1535 + if es then
1536 + flush()
1537 + table.insert(spans, {
1538 + kind = 'raw';
1539 + spans = {es};
1540 + origin = ctx:clone()
1541 + })
1542 + p.next.byte = p.next.byte + ba;
1543 + p.next.code = p.next.code + ca;
1196 1544 elseif c == '{' then
1197 1545 flush()
1198 1546 local substr, following = delimited('{','}',str:sub(p.byte))
1199 1547 local splitstart, splitstop = substr:find'%s+'
1200 1548 local id, argstr
1201 1549 if splitstart then
1202 1550 id, argstr = substr:sub(1,splitstart-1), substr:sub(splitstop+1)
................................................................................
1214 1562 local i = 1
1215 1563 while i <= #argstr do
1216 1564 while i<=#argstr and (argstr:sub(i,i) ~= '|' or argstr:sub(i-1,i) == '\\|') do
1217 1565 i = i + 1
1218 1566 end
1219 1567 local arg = argstr:sub(start, i == #argstr and i or i-1)
1220 1568 start = i+1
1569 + arg=arg:gsub('\\|','|')
1221 1570 table.insert(o.args, arg)
1222 1571 i = i + 1
1223 1572 end
1224 1573 end
1225 1574
1226 1575 p.next.byte = p.next.byte + following - 1
1227 1576 table.insert(spans,o)
................................................................................
1236 1585 table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx))
1237 1586 break
1238 1587 end
1239 1588 end
1240 1589 if not found then
1241 1590 ctx:fail('no recognized control sequence in [%s]', substr)
1242 1591 end
1592 + elseif c == '\n' then
1593 + flush()
1594 + table.insert(spans,{kind='line-break',origin=ctx:clone()})
1243 1595 else
1244 1596 buf = buf .. c
1245 1597 end
1246 1598 end
1247 1599 flush()
1248 1600 return spans
1249 1601 end
1250 1602
1251 1603 local function
1252 1604 blockwrap(fn)
1253 - return function(l,c,j)
1254 - local block = fn(l,c,j)
1605 + return function(l,c,j,d)
1606 + local block = fn(l,c,j,d)
1255 1607 block.origin = c:clone();
1256 - table.insert(c.sec.blocks, block);
1608 + table.insert(d, block);
1257 1609 j:hook('block_insert', c, block, l)
1610 + if block.spans then
1611 + c.doc.docjob:hook('meddle_span', block.spans, block)
1612 + end
1258 1613 end
1259 1614 end
1260 1615
1261 1616 local insert_paragraph = blockwrap(function(l,c)
1262 1617 if l:sub(1,1) == '.' then l = l:sub(2) end
1263 1618 return {
1264 1619 kind = "paragraph";
................................................................................
1282 1637 if t and t ~= "" then
1283 1638 local heading = {
1284 1639 kind = "label";
1285 1640 spans = ct.parse_span(t,c);
1286 1641 origin = s.origin;
1287 1642 captions = s;
1288 1643 }
1644 + c.doc.docjob:hook('meddle_span', heading.spans, heading)
1289 1645 table.insert(s.blocks, heading)
1290 1646 s.heading_node = heading
1291 1647 end
1292 1648 c.sec = s
1293 1649
1294 1650 j:hook('section_attach', c, s)
1295 1651 end
................................................................................
1299 1655 c.doc.meta[key] = val
1300 1656 j:hook('metadata_set', key, val)
1301 1657 end
1302 1658 local dextctl = function(w,c)
1303 1659 local mode, exts = w(1)
1304 1660 for e in exts:gmatch '([^%s]+)' do
1305 1661 if mode == 'uses' then
1662 + c.doc.ext.use[e] = true
1306 1663 elseif mode == 'needs' then
1664 + c.doc.ext.need[e] = true
1307 1665 elseif mode == 'inhibits' then
1666 + c.doc.ext.inhibit[e] = true
1308 1667 end
1309 1668 end
1310 1669 end
1311 1670 local dcond = function(w,c)
1312 1671 local mode, cond, exp = w(2)
1313 1672 c.hide_next = mode == 'unless'
1314 1673 end;
................................................................................
1315 1674 ct.directives = {
1316 1675 author = dsetmeta;
1317 1676 license = dsetmeta;
1318 1677 keywords = dsetmeta;
1319 1678 desc = dsetmeta;
1320 1679 when = dcond;
1321 1680 unless = dcond;
1681 + pragma = function(w,c)
1682 + end;
1683 + lang = function(w,c)
1684 + local _, op, l = w(2)
1685 + local langstack = c.doc.stage.langstack
1686 + if op == 'is' then
1687 + langstack[math.max(1, #langstack)] = l
1688 + elseif op == 'push' then
1689 + table.insert(langstack, l)
1690 + elseif op == 'pop' then
1691 + if next(langstack) then
1692 + langstack[#langstack] = nil
1693 + end
1694 + elseif op == 'sec' then
1695 + c.sec.lang = l
1696 + else c:fail('bad language directive “%s”', op) end
1697 + c.lang = langstack[#langstack]
1698 + end;
1322 1699 expand = function(w,c)
1323 1700 local _, m = w(1)
1324 1701 if m ~= 'off' then
1325 - c.expand_next = 1
1702 + c.doc.stage.expand_next = 1
1326 1703 else
1327 - c.expand_next = 0
1704 + c.doc.stage.expand_next = 0
1328 1705 end
1329 1706 end;
1330 1707 }
1331 1708
1332 1709 local function insert_table_row(l,c,j)
1333 1710 local row = {}
1334 1711 local buf
................................................................................
1335 1712 local flush = function()
1336 1713 if buf then
1337 1714 buf.str = buf.str:gsub('%s+$','')
1338 1715 table.insert(row, buf)
1339 1716 end
1340 1717 buf = { str = '' }
1341 1718 end
1342 - for c,p in eachcode(l) do
1719 + for c,p in ss.str.each(c.doc.enc,l) do
1343 1720 if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then
1344 1721 flush()
1345 1722 buf.header = c == '+'
1346 1723 elseif c == ':' then
1347 1724 local lst = l:sub(p.byte-#c,p.byte-#c)
1348 1725 local nxt = l:sub(p.next.byte,p.next.byte)
1349 1726 if lst == '|' or lst == '+' and l:sub(p.byte-2,p.byte-2) ~= '\\' then
................................................................................
1371 1748 else
1372 1749 buf.str = buf.str .. c
1373 1750 end
1374 1751 end
1375 1752 if buf.str ~= '' then flush() end
1376 1753 for _,v in pairs(row) do
1377 1754 v.spans = ct.parse_span(v.str, c)
1755 + c.doc.docjob:hook('meddle_span', v.spans, v)
1378 1756 end
1379 1757 if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then
1380 1758 local tbl = c.sec.blocks[#c.sec.blocks]
1381 1759 table.insert(tbl.rows, row)
1382 1760 j:hook('block_table_attach', c, tbl, row, l)
1383 1761 j:hook('block_table_row_insert', c, tbl, row, l)
1384 1762 else
................................................................................
1398 1776 {seq = '¶', fn = insert_paragraph};
1399 1777 {seq = '❡', fn = insert_paragraph};
1400 1778 {seq = '#', fn = insert_section};
1401 1779 {seq = '§', fn = insert_section};
1402 1780 {seq = '+', fn = insert_table_row};
1403 1781 {seq = '|', fn = insert_table_row};
1404 1782 {seq = '│', fn = insert_table_row};
1405 - {seq = '!', fn = function(l,c,j)
1406 - local last = c.sec.blocks[#c.sec.blocks]
1783 + {seq = '!', fn = function(l,c,j,d)
1784 + local last = d[#d]
1407 1785 local txt = l:match '^%s*!%s*(.-)$'
1408 1786 if (not last) or last.kind ~= 'aside' then
1409 1787 local aside = {
1410 1788 kind = 'aside';
1411 - lines = { ct.parse_span(txt, c) }
1789 + lines = { ct.parse_span(txt, c) };
1790 + origin = c:clone();
1412 1791 }
1413 - c:insert(aside)
1792 + c.doc.docjob:hook('meddle_span', aside.lines[1], aside)
1793 + table.insert(d,aside)
1414 1794 j:hook('block_aside_insert', c, aside, l)
1415 1795 j:hook('block_aside_line_insert', c, aside, aside.lines[1], l)
1416 1796 j:hook('block_insert', c, aside, l)
1417 1797 else
1418 1798 local sp = ct.parse_span(txt, c)
1799 + c.doc.docjob:hook('meddle_span', sp, last)
1419 1800 table.insert(last.lines, sp)
1420 1801 j:hook('block_aside_attach', c, last, sp, l)
1421 1802 j:hook('block_aside_line_insert', c, last, sp, l)
1422 1803 end
1423 1804 end};
1424 1805 {pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
1425 1806 local stars = l:match '^([*:]+)'
................................................................................
1430 1811 return {
1431 1812 kind = 'list-item';
1432 1813 depth = depth;
1433 1814 ordered = ordered;
1434 1815 spans = ct.parse_span(txt, c);
1435 1816 }
1436 1817 end)};
1437 - {seq = '\t', fn = function(l,c,j)
1438 - local ref, val = l:match '\t+([^:]+):%s*(.*)$'
1439 - c.sec.refs[ref] = val
1440 - j:hook('section_ref_attach', c, ref, val, l)
1818 + {seq = '\t\t', fn = function(l,c,j,d)
1819 + local last = d[#d]
1820 + if (not last) or (last.kind ~= 'reference') then
1821 + c:fail('reference continuations must immediately follow a reference')
1822 + end
1823 + local str = l:match '^\t\t(.-)%s*$'
1824 + last.val = last.val .. '\n' .. str
1825 + c.sec.refs[last.key] = last.val
1441 1826 end};
1442 - {seq = '%', fn = function(l,c,j) -- directive
1827 + {seq = '\t', fn = blockwrap(function(l,c,j,d)
1828 + local ref, val = l:match '\t+([^:]+):%s*(.*)$'
1829 + local last = d[#d]
1830 + local rsrc
1831 + if last and last.kind == 'resource' then
1832 + last.props[ref] = val
1833 + rsrc = last
1834 + elseif last and last.kind == 'reference' and last.rsrc then
1835 + last.rsrc.props[ref] = val
1836 + rsrc = last.rsrc
1837 + else
1838 + c.sec.refs[ref] = val
1839 + end
1840 + j:hook('section_ref_attach', c, ref, val, l)
1841 + return {
1842 + kind = 'reference';
1843 + rsrc = rsrc;
1844 + key = ref;
1845 + val = val;
1846 + }
1847 + end)};
1848 + {seq = '%', fn = function(l,c,j,d) -- directive
1443 1849 local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$'
1444 1850 local words = function(i)
1445 1851 local wds = {}
1446 1852 if i == 0 then return cmdline end
1447 1853 for w,pos in cmdline:gmatch '([^%s]+)()' do
1448 1854 table.insert(wds, w)
1449 1855 i = i - 1
1450 1856 if i == 0 then
1451 - table.insert(wds,cmdline:sub(pos))
1857 + table.insert(wds,(cmdline:sub(pos):match('^%s*(.-)%s*$')))
1452 1858 return table.unpack(wds)
1453 1859 end
1454 1860 end
1455 1861 end
1456 1862
1457 1863 local cmd, rest = words(1)
1458 1864 if ct.directives[cmd] then
................................................................................
1459 1865 ct.directives[cmd](words,c,j)
1460 1866 elseif cmd == c.doc.stage.mode['render:format'] then
1461 1867 -- this is a directive for the renderer; insert it into the tree as is
1462 1868 local dir = {
1463 1869 kind = 'directive';
1464 1870 critical = crit == '!';
1465 1871 words = words;
1872 + origin = c;
1466 1873 }
1467 - c:insert(dir)
1874 + table.insert(d, dir)
1468 1875 j:hook('block_directive_render', j, c, dir)
1469 1876 elseif c.doc:allow_ext(cmd) then -- extension directives begin with their id
1470 1877 local ext = ct.ext.loaded[cmd]
1471 1878 if ext.directives then
1472 1879 local _, topcmd = words(2)
1473 1880 if ext.directives[topcmd] then
1474 1881 ext.directives[topcmd](j:delegate(ext), c, words)
................................................................................
1505 1912 kind = 'code';
1506 1913 listing = {
1507 1914 kind = 'listing';
1508 1915 lang = lang, id = id, title = title and ct.parse_span(title,c);
1509 1916 lines = {};
1510 1917 }
1511 1918 }
1919 + if c.doc.stage.expand_next and c.doc.stage.expand_next > 0 then
1920 + c.doc.stage.expand_next = c.doc.stage.expand_next - 1
1921 + mode.expand = true
1922 + end
1512 1923 j:hook('mode_switch', c, mode)
1513 1924 c.mode = mode
1514 1925 if id then
1515 1926 if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
1516 1927 c.sec.refs[id] = c.mode.listing
1517 1928 end
1518 1929 j:hook('block_insert', c, mode.listing, l)
1519 1930 return c.mode.listing;
1520 1931 end)};
1521 1932 {pred = function(s,c)
1522 1933 if s:match '^[%-_][*_%-%s]+' then return true end
1523 1934 if startswith(s, '—') then
1524 - for c, p in eachcode(s) do
1935 + for c, p in ss.str.each(c.doc.enc,s) do
1525 1936 if ({
1526 1937 ['—'] = true, ['-'] = true, [' '] = true;
1527 1938 ['*'] = true, ['_'] = true, ['\t'] = true;
1528 1939 })[c] ~= true then return false end
1529 1940 end
1530 1941 return true
1531 1942 end
1532 1943 end; fn = blockwrap(function()
1533 1944 return { kind = 'horiz-rule' }
1945 + end)};
1946 + {seq='@', fn=blockwrap(function(s,c)
1947 + local id = s:match '^@%s*(.-)%s*$'
1948 + local rsrc = {
1949 + kind = 'resource';
1950 + props = {};
1951 + id = id;
1952 + }
1953 + if c.sec.refs[id] then
1954 + c:fail('an object with id “%s” already exists in that section',id)
1955 + else
1956 + c.sec.refs[id] = rsrc
1957 + end
1958 + return rsrc
1534 1959 end)};
1535 1960 {fn = insert_paragraph};
1536 1961 }
1537 1962
1538 -function ct.parse(file, src, mode)
1539 - local function
1540 - is_whitespace(cp)
1541 - return cp == 0x20 or cp == 0xe390
1963 +function ct.parse_line(l, ctx, dest)
1964 + local newspan
1965 + local job = ctx.doc.stage.job
1966 + job:hook('line_read',ctx,l)
1967 + if ctx.mode then
1968 + if ctx.mode.kind == 'code' then
1969 + if l and l:match '^~~~%s*$' then
1970 + job:hook('block_listing_end',ctx,ctx.mode.listing)
1971 + job:hook('mode_switch', c, nil)
1972 + ctx.mode = nil
1973 + else
1974 + -- TODO handle formatted code
1975 + local newline
1976 + if ctx.mode.expand
1977 + then newline = ct.parse_span(l, ctx)
1978 + else newline = {l}
1979 + end
1980 + table.insert(ctx.mode.listing.lines, newline)
1981 + job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
1982 + end
1983 + else
1984 + local mf = job:proc('modes', ctx.mode.kind)
1985 + if not mf then
1986 + ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
1987 + end
1988 + mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
1989 + end
1990 + else
1991 + if l then
1992 + local function tryseqs(seqs, ...)
1993 + for _, i in pairs(seqs) do
1994 + if ((not i.seq ) or startswith(l, i.seq)) and
1995 + ((not i.pred) or i.pred (l, ctx )) then
1996 + i.fn(l, ctx, job, dest, ...)
1997 + return true
1998 + end
1999 + end
2000 + return false
2001 + end
2002 +
2003 + if not tryseqs(ct.ctlseqs) then
2004 + local found = false
2005 +
2006 + for eb, ext, state in job:each('blocks') do
2007 + if tryseqs(eb, state) then found = true break end
2008 + end
2009 +
2010 + if not found then
2011 + ctx:fail 'incomprehensible input line'
2012 + end
2013 + end
2014 + else
2015 + if next(dest) and dest[#dest].kind ~= 'break' then
2016 + local brk = {kind='break', origin = ctx:clone()}
2017 + job:hook('block_break', ctx, brk, l)
2018 + table.insert(dest, brk)
2019 + end
2020 + end
1542 2021 end
2022 + job:hook('line_end',ctx,l)
2023 +end
2024 +
2025 +function ct.parse(file, src, mode, setup)
1543 2026
1544 2027 local ctx = ct.ctx.mk(src)
1545 2028 ctx.line = 0
1546 2029 ctx.doc = ct.doc.mk()
1547 2030 ctx.doc.src = src
1548 - ctx.doc.stage = {
1549 - kind = 'parse';
1550 - mode = mode;
1551 - }
1552 2031 ctx.sec = ctx.doc:mksec() -- toplevel section
1553 2032 ctx.sec.origin = ctx:clone()
2033 + ctx.lang = mode['meta:lang']
2034 + if mode['parse:enc'] then
2035 + local e = ss.str.enc[mode['parse:enc']]
2036 + if not e then
2037 + ct.exns.enc('requested encoding not supported',mode['parse:enc']):throw()
2038 + end
2039 + ctx.doc.enc = e
2040 + end
1554 2041
1555 2042 -- create states for extension hooks
1556 2043 local job = ctx.doc:job('parse',nil,ctx)
2044 + ctx.doc.stage = {
2045 + kind = 'parse';
2046 + mode = mode;
2047 + job = job;
2048 + langstack = {ctx.lang};
2049 + fontstack = {};
2050 + }
2051 +
2052 + local function
2053 + is_whitespace(cp)
2054 + return ctx.doc.enc.iswhitespace(cp)
2055 + end
2056 +
2057 + if setup then setup(ctx) end
2058 +
1557 2059
1558 2060 for full_line in file:lines() do ctx.line = ctx.line + 1
1559 2061 local l
1560 2062 for p, c in utf8.codes(full_line) do
1561 2063 if not is_whitespace(c) then
1562 2064 l = full_line:sub(p)
1563 2065 break
1564 2066 end
1565 2067 end
1566 - job:hook('line_read',ctx,l)
2068 + ct.parse_line(l, ctx, ctx.sec.blocks)
2069 + end
1567 2070
1568 - if ctx.mode then
1569 - if ctx.mode.kind == 'code' then
1570 - if l and l:match '^~~~%s*$' then
1571 - job:hook('block_listing_end',ctx,ctx.mode.listing)
1572 - job:hook('mode_switch', c, nil)
1573 - ctx.mode = nil
1574 - else
1575 - -- TODO handle formatted code
1576 - local newline = {l}
1577 - table.insert(ctx.mode.listing.lines, newline)
1578 - job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
1579 - end
1580 - else
1581 - ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
1582 - end
1583 - else
1584 - if l then
1585 - local function tryseqs(seqs, ...)
1586 - for _, i in pairs(seqs) do
1587 - if ((not i.seq ) or startswith(l, i.seq)) and
1588 - ((not i.pred) or i.pred (l, ctx )) then
1589 - i.fn(l, ctx, job, ...)
1590 - return true
1591 - end
2071 + for i, sec in ipairs(ctx.doc.secorder) do
2072 + for refid, r in ipairs(sec.refs) do
2073 + if type(r) == 'table' and r.kind == 'resource' and r.props.src then
2074 + local lines = ss.str.breaklines(ctx.doc.enc, r.props.src)
2075 + local srcs = {}
2076 + for i,l in ipairs(lines) do
2077 + local args = ss.str.breakwords(ctx.doc.enc, l, 2, {escape=true})
2078 + if #args < 3 then
2079 + r.origin:fail('invalid syntax for resource %s', t.ref)
2080 + end
2081 + local mimebreak = function(s)
2082 + local wds = ss.str.split(ctx.doc.enc, s, '/', 1, {escape=true})
2083 + return wds
1592 2084 end
1593 - return false
2085 + local mime = mimebreak(args[2]);
2086 + local mimeclasses = {
2087 + ['application/svg+xml'] = 'image';
2088 + }
2089 + local class = mimeclasses[mime]
2090 + table.insert(srcs, {
2091 + mode = args[1];
2092 + mime = mime;
2093 + uri = args[3];
2094 + class = class or mime[1];
2095 + })
1594 2096 end
1595 -
1596 - if not tryseqs(ct.ctlseqs) then
1597 - local found = false
1598 -
1599 - for eb, ext, state in job:each('blocks') do
1600 - if tryseqs(eb, state) then found = true break end
1601 - end
1602 -
1603 - if not found then
1604 - ctx:fail 'incomprehensible input line'
1605 - end
1606 - end
1607 - else
1608 - if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then
1609 - local brk = {kind='break'}
1610 - job:hook('block_break', ctx, brk, l)
1611 - table.insert(ctx.sec.blocks, brk)
1612 - end
2097 + --ideally move this into its own mimetype lib
2098 + local kind = r.props.as or srcs[1].class
2099 + r.class = kind
2100 + r.srcs = srcs
1613 2101 end
1614 2102 end
1615 - job:hook('line_end',ctx,l)
1616 2103 end
1617 -
2104 + ctx.doc.stage = nil
2105 + ctx.doc.docjob:hook('meddle_ast')
1618 2106 return ctx.doc
1619 2107 end