Differences From
Artifact [028f351fed]:
394 394 table.insert(ret, (hookfn(me:delegate(ext),...)))
395 395 end
396 396 return ret
397 397 end;
398 398 };
399 399 }
400 400
401 --- renderer engines
402 -function ct.render.html(doc, opts)
403 - local doctitle = opts['title']
404 - local f = string.format
401 +-- common renderer utility functions
402 +ct.tool = {}
403 +
404 +function ct.tool.mathfmt(ctx, eqn)
405 + local buf = ''
406 + local m = ss.enum {'num','var','op'}
407 + local lsc = 0
408 + local spans = {}
409 +
410 + local flush = function()
411 + local o
412 + if buf ~= '' then
413 + if lsc == 0 then
414 + o = buf
415 + elseif lsc == m.num then
416 + o = {
417 + kind = 'format';
418 + style = 'literal';
419 + spans = {buf};
420 + }
421 + elseif lsc == m.var then
422 + o = {
423 + kind = 'format';
424 + style = 'variable';
425 + spans = {buf};
426 + }
427 + elseif lsc == m.op then
428 + o = {
429 + kind = 'format';
430 + style = 'strong';
431 + spans = {buf};
432 + }
433 + end
434 + if o then
435 + table.insert(spans, o)
436 + end
437 + end
438 + buf = ''
439 + lsc = 0
440 + end
441 +
442 + for c, p in ss.str.each(ctx.doc.enc, eqn) do
443 + local cl = ss.str.classify(ctx.doc.enc, c)
444 + local nc = 0
445 + if not cl.space then
446 + if cl.numeral then
447 + nc = m.num
448 + elseif cl.mathop or cl.symbol then
449 + nc = m.op
450 + elseif cl.letter then
451 + nc = m.var
452 + end
453 + if nc ~= lsc then
454 + flush()
455 + lsc = nc
456 + end
457 + buf = buf .. c
458 + end
459 + end
460 + flush()
461 + return spans
462 +end
463 +
464 +function ct.tool.namespace()
465 +-- some renderers need to be able to generate unique IDs for
466 +-- objects, including ones that users have not assigned IDs
467 +-- to, and objects with the same name in different unlabeled
468 +-- sections. to handle this, we provide a "namespace" mechanism,
469 +-- where some lua table (really its address in memory) is used
470 +-- as a handle for the object and a unique ID is attached to it.
471 +-- if the object has an ID of its own, it is guaranteed to be
472 +-- unique and returned; otherwise, a generic id of the form
473 +-- `x-%u` is generated, where %u is an integer that increments
474 +-- for every new object
405 475 local ids = {}
406 476 local canonicalID = {}
407 - local function getSafeID(obj,pfx)
477 + return function(obj,pfx)
408 478 pfx = pfx or ''
409 479 if canonicalID[obj] then
410 480 return canonicalID[obj]
411 481 elseif obj.id and ids[pfx .. obj.id] then
412 482 local objid = pfx .. obj.id
413 483 local newid
414 484 local i = 1
................................................................................
425 495 i = i + 1 until not ids[cid]
426 496 end
427 497 ids[cid] = obj
428 498 canonicalID[obj] = cid
429 499 return cid
430 500 end
431 501 end
432 -
433 - local footnotes = {}
434 - local footnotecount = 0
435 -
436 - local langsused = {}
437 - local langpairs = {
438 - lua = { color = 0x9377ff };
439 - terra = { color = 0xff77c8 };
440 - c = { name = 'C', color = 0x77ffe8 };
441 - html = { color = 0xfff877 };
442 - scheme = { color = 0x77ff88 };
443 - lisp = { color = 0x77ff88 };
444 - fortran = { color = 0xff779a };
445 - python = { color = 0xffd277 };
446 - ruby = { color = 0xcdd6ff };
447 - }
448 -
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 - ]];
523 - header = [[
524 - body { padding: 0 2.5em !important }
525 - h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
526 - h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
527 - h2 { font-size: 130%; margin: 0em -0.7em; }
528 - h3 { font-size: 110%; margin: 0em -0.5em; }
529 - h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; }
530 - h5 { font-size: 90%; font-weight: normal; }
531 - h6 { font-size: 80%; font-weight: normal; }
532 - h3, h4, h5, h6 { border-bottom-style: dotted !important; }
533 - h1,h2,h3,h4,h5,h6 {
534 - margin-top: 0;
535 - margin-bottom: 0;
536 - }
537 - :is(h1,h2,h3,h4,h5,h6) + p {
538 - margin-top: 0.4em;
539 - }
540 -
541 - ]];
542 - headingAnchors = [[
543 - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
544 - text-decoration: none;
545 - font-size: 1.2em;
546 - padding: 0.3em;
547 - opacity: 0%;
548 - transition: 0.3s;
549 - font-weight: 100;
550 - }
551 - :is(h1,h2,h3,h4,h5,h6):hover > a[href].anchor {
552 - opacity: 50%;
553 - }
554 - :is(h1,h2,h3,h4,h5,h6) > a[href].anchor:hover {
555 - opacity: 100%;
556 - }
557 -
558 - ]] .. -- this is necessary to avoid the sections jumping around
559 - -- when focus changes from one to another
560 - [[ section {
561 - border: 1px solid transparent;
562 - }
563 -
564 - section:target {
565 - margin-left: -2em;
566 - margin-right: -2em;
567 - padding: 0 2em;
568 - background: @tone(0.04);
569 - border: 1px dotted @tone(0.3);
570 - }
571 -
572 - section:target > :is(h1,h2,h3,h4,h5,h6) {
573 -
574 - }
575 - ]];
576 - paragraph = [[
577 - p {
578 - margin: 0.7em 0;
579 - text-align: justify;
580 - }
581 - section {
582 - margin: 1.2em 0;
583 - }
584 - section:first-child { margin-top: 0; }
585 - ]];
586 - accent = [[
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); }
602 - }
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 - }
612 - }
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 - ]];
632 - code = [[
633 - code {
634 - display: inline-block;
635 - background: @tone(0.9);
636 - color: @bg;
637 - font-family: monospace;
638 - font-size: 90%;
639 - padding: 3px 5px;
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 - }
660 - ]];
661 - abbr = [[
662 - abbr[title] { cursor: help; }
663 - ]];
664 - editors_markup = [[]];
665 - block_code_listing = [[
666 - figure.listing {
667 - font-family: monospace;
668 - background: @tone(0.05);
669 - color: @fg;
670 - padding: 0;
671 - margin: 0.3em 0;
672 - counter-reset: line-number;
673 - position: relative;
674 - border: 1px solid @fg;
675 - }
676 - figure.listing>div {
677 - white-space: pre-wrap;
678 - tab-size: 3;
679 - -moz-tab-size: 3;
680 - counter-increment: line-number;
681 - text-indent: -2.3em;
682 - margin-left: 2.3em;
683 - }
684 - figure.listing>:is(div,hr)::before {
685 - width: 1.0em;
686 - padding: 0.2em 0.4em;
687 - text-align: right;
688 - display: inline-block;
689 - background-color: @tone(0.2);
690 - border-right: 1px solid @fg;
691 - content: counter(line-number);
692 - margin-right: 0.3em;
693 - }
694 - figure.listing>hr::before {
695 - color: transparent;
696 - padding-top: 0;
697 - padding-bottom: 0;
698 - }
699 - figure.listing>div::before {
700 - color: @fg;
701 - }
702 - figure.listing>div:last-child::before {
703 - padding-bottom: 0.5em;
704 - }
705 - figure.listing>figcaption:first-child {
706 - border: none;
707 - border-bottom: 1px solid @fg;
708 - }
709 - figure.listing>figcaption::after {
710 - display: block;
711 - float: right;
712 - font-weight: normal;
713 - font-style: italic;
714 - font-size: 70%;
715 - padding-top: 0.3em;
716 - }
717 - figure.listing>figcaption {
718 - font-family: sans-serif;
719 - font-size: 120%;
720 - padding: 0.2em 0.4em;
721 - border: none;
722 - color: @tone(2);
723 - }
724 - figure.listing > hr {
725 - border: none;
726 - margin: 0;
727 - height: 0.7em;
728 - counter-increment: line-number;
729 - }
730 - ]];
731 - }
732 -
733 - local stylesNeeded = {}
734 -
735 - local render_state_handle = {
736 - doc = doc;
737 - opts = opts;
738 - style_rules = styles; -- use stylesneeded if at all possible
739 - stylesets = stylesets;
740 - stylesets_active = stylesNeeded;
741 - obj_htmlid = getSafeID;
742 - -- remaining fields added later
743 - }
744 -
745 - local renderJob = doc:job('render_html', nil, render_state_handle)
746 - doc.stage.job = renderJob;
747 -
748 - local runhook = function(h, ...)
749 - return renderJob:hook(h, render_state_handle, ...)
750 - end
751 -
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)
793 - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
794 - local htmlDoc = function(title, head, body)
795 - return [[<!doctype html>]] .. tag('html',nil,
796 - tag('head', nil,
797 - elt('meta',{charset = 'utf-8'}) ..
798 - (title and tag('title', nil, title) or '') ..
799 - (head or '')) ..
800 - tag('body', nil, body or ''))
801 - end
802 -
803 - local function htmlSpan(spans, block, sec)
804 - local text = {}
805 - for k,v in pairs(spans) do
806 - if type(v) == 'string' then
807 - v=v:gsub('[<>&"]', function(x)
808 - return string.format('&#%02u;', string.byte(x))
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)
814 - else
815 - table.insert(text, (span_renderers[v.kind](v, block, sec)))
816 - end
817 - end
818 - return table.concat(text)
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
835 -
836 - function span_renderers.format(sp,...)
837 - local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'}
838 - if sp.style == 'literal' and not opts['fossil-uv'] then
839 - stylesNeeded.code = true
840 - elseif sp.style == 'strike' or sp.style == 'insert' then
841 - stylesNeeded.editors_markup = true
842 - elseif sp.style == 'variable' then
843 - stylesNeeded.var = true
844 - end
845 - return tag(tags[sp.style],nil,htmlSpan(sp.spans,...))
846 - end
847 -
848 - function span_renderers.deref(t,b,s)
849 - local r = b.origin:ref(t.ref)
850 - local name = t.ref
851 - if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
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)
855 - end
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)
877 - end
878 - end
879 -
880 - function span_renderers.var(v,b,s)
881 - local val
882 - if v.pos then
883 - if not v.origin.invocation then
884 - v.origin:fail 'positional arguments can only be used in a macro invocation'
885 - elseif not v.origin.invocation.args[v.pos] then
886 - v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos)
887 - end
888 - val = v.origin.invocation.args[v.pos]
889 - else
890 - val = v.origin.doc:context_var(v.var, v.origin)
891 - end
892 - if v.raw then
893 - return val
894 - else
895 - return htmlSpan(ct.parse_span(val, v.origin), b, s)
896 - end
897 - end
898 -
899 - function span_renderers.raw(v,b,s)
900 - return htmlSpan(v.spans, b, s)
901 - end
902 -
903 - function span_renderers.link(sp,b,s)
904 - local href
905 - if b.origin.doc.sections[sp.ref] then
906 - href = '#' .. sp.ref
907 - else
908 - if sp.addr then href = sp.addr else
909 - local r = b.origin:ref(sp.ref)
910 - if type(r) == 'table' then
911 - href = '#' .. getSafeID(r)
912 - else href = r end
913 - end
914 - end
915 - return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href)
916 - end
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
964 - end
965 -
966 - local function getBlockRenderers(procs, sr)
967 - local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
968 - local null = function() return catenate{} end
969 -
970 - local block_renderers = {
971 - anchor = function(b,s)
972 - return tag('a',{id = getSafeID(b)},null())
973 - end;
974 - paragraph = function(b,s)
975 - stylesNeeded.paragraph = true;
976 - return tag('p', nil, sr.htmlSpan(b.spans, b, s), b)
977 - end;
978 - directive = function(b,s)
979 - -- deal with renderer directives
980 - local _, cmd, args = b.words(2)
981 - if cmd == 'page-title' then
982 - if not opts.title then doctitle = args end
983 - elseif b.critical then
984 - b.origin:fail('critical HTML renderer directive ā%sā not supported', cmd)
985 - end
986 - end;
987 - label = function(b,s)
988 - if ct.sec.is(b.captions) then
989 - if not (opts['fossil-uv'] or opts.snippet) then
990 - stylesNeeded.header = true
991 - end
992 - local h = math.min(6,math.max(1,b.captions.depth))
993 - return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
994 - else
995 - -- handle other uses of labels here
996 - end
997 - end;
998 - ['list-item'] = function(b,s)
999 - return tag('li', nil, sr.htmlSpan(b.spans, b, s), b)
1000 - end;
1001 - table = function(b,s)
1002 - local tb = {}
1003 - for i, r in ipairs(b.rows) do
1004 - local row = {}
1005 - for i, c in ipairs(r) do
1006 - table.insert(row, tag(c.header and 'th' or 'td',
1007 - {align=c.align}, sr.htmlSpan(c.spans, b)))
1008 - end
1009 - table.insert(tb, tag('tr',nil,catenate(row)))
1010 - end
1011 - return tag('table',nil,catenate(tb))
1012 - end;
1013 - listing = function(b,s)
1014 - stylesNeeded.block_code_listing = true
1015 - local nodes = ss.map(function(l)
1016 - if #l > 0 then
1017 - return tag('div',nil,sr.htmlSpan(l, b, s))
1018 - else
1019 - return elt('hr')
1020 - end
1021 - end, b.lines)
1022 - if b.title then
1023 - table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title)))
1024 - end
1025 - if b.lang then langsused[b.lang] = true end
1026 - return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
1027 - end;
1028 - aside = function(b,s)
1029 - local bn = {}
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
1037 - end
1038 - return tag('aside', {}, bn)
1039 - end;
1040 - ['break'] = function() -- HACK
1041 - -- lists need to be rewritten to work like asides
1042 - return '';
1043 - end;
1044 - }
1045 - return block_renderers;
1046 - end
1047 -
1048 - local function getRenderers(procs)
1049 - local span_renderers = getSpanRenderers(procs)
1050 - local r = getBaseRenderers(procs,span_renderers)
1051 - r.block_renderers = getBlockRenderers(procs, r)
1052 - return r
1053 - end
1054 -
1055 - local astproc = {
1056 - toHTML = getRenderers(tagproc.toHTML);
1057 - toTXT = getRenderers(tagproc.toTXT);
1058 - toIR = { };
1059 - }
1060 - astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
1061 - astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
1062 - -- note we use HTML here instead of IR span renderers, because as things
1063 - -- currently stand we don't need that level of resolution. if we ever
1064 - -- get to the point where we want to be able to twiddle spans around
1065 - -- we'll need to introduce an IR span renderer
1066 -
1067 - render_state_handle.astproc = astproc;
1068 - render_state_handle.tagproc = tagproc;
1069 -
1070 - -- bind to legacy names
1071 - -- yikes this needs to be cleaned up so badly
1072 - local ir = {}
1073 - local dr = astproc.toHTML -- default renderers
1074 - local plainr = astproc.toTXT
1075 - local irBlockRdrs = astproc.toIR.block_renderers;
1076 -
1077 - render_state_handle.ir = ir;
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
1113 - runhook('ir_assemble', ir)
1114 - for i, sec in ipairs(doc.secorder) do
1115 - if doctitle == nil and sec.depth == 1 and sec.heading_node then
1116 - doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
1117 - end
1118 - local irs
1119 - if sec.kind == 'ordinary' then
1120 - if #(sec.blocks) > 0 then
1121 - irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
1122 - runhook('ir_section_build', irs, sec)
1123 - renderBlocks(sec.blocks, irs)
1124 - end
1125 - elseif sec.kind == 'blockquote' then
1126 - elseif sec.kind == 'listing' then
1127 - elseif sec.kind == 'embed' then
1128 - end
1129 - if irs then table.insert(ir, irs) end
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
1147 -
1148 - -- restructure passes
1149 - runhook('ir_restructure_pre', ir)
1150 -
1151 - ---- list insertion pass
1152 - local lists = {}
1153 - for _, sec in pairs(ir) do
1154 - if sec.tag == 'section' then
1155 - local i = 1 while i <= #sec.nodes do local v = sec.nodes[i]
1156 - if v.tag == 'li' then
1157 - local ltag
1158 - if v.src.ordered
1159 - then ltag = 'ol'
1160 - else ltag = 'ul'
1161 - end
1162 - local last = i>1 and sec.nodes[i-1]
1163 - if last and last.embed == 'list' and not (
1164 - last.ref[#last.ref].src.depth == v.src.depth and
1165 - last.ref[#last.ref].src.ordered ~= v.src.ordered
1166 - ) then
1167 - -- add to existing list
1168 - table.insert(last.ref, v)
1169 - table.remove(sec.nodes, i) i = i - 1
1170 - else
1171 - -- wrap in list
1172 - local newls = {v}
1173 - sec.nodes[i] = {embed = 'list', ref = newls}
1174 - table.insert(lists,newls)
1175 - end
1176 - end
1177 - i = i + 1 end
1178 - end
1179 - end
1180 -
1181 - for _, sec in pairs(ir) do
1182 - if sec.tag == 'section' then
1183 - for i, elt in pairs(sec.nodes) do
1184 - if elt.embed == 'list' then
1185 - local function fail_nest()
1186 - elt.ref[1].src.origin:fail('improper list nesting')
1187 - end
1188 - local struc = {attrs={}, nodes={}}
1189 - if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end
1190 - if elt.ref[1].src.depth ~= 1 then fail_nest() end
1191 -
1192 - local stack = {struc}
1193 - local copyNodes = function(old,new)
1194 - for i,v in ipairs(old) do new[#new + i] = v end
1195 - end
1196 - for i,e in ipairs(elt.ref) do
1197 - if e.src.depth > #stack then
1198 - if e.src.depth - #stack > 1 then fail_nest() end
1199 - local newls = {attrs={}, nodes={e}}
1200 - copyNodes(e.nodes,newls)
1201 - if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end
1202 - table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls)
1203 - table.insert(stack, newls)
1204 - else
1205 - if e.src.depth < #stack then
1206 - -- pop entries off the stack
1207 - for i=#stack, e.src.depth+1, -1 do stack[i] = nil end
1208 - end
1209 - table.insert(stack[#stack].nodes, e)
1210 - end
1211 - end
1212 -
1213 - sec.nodes[i] = struc
1214 - end
1215 - end
1216 - end
1217 - end
1218 -
1219 - runhook('ir_restructure_post', ir)
1220 -
1221 - -- collection pass
1222 - local function collect_nodes(t)
1223 - local ts = ''
1224 - for i,v in ipairs(t) do
1225 - if type(v) == 'string' then
1226 - ts = ts .. v
1227 - elseif v.nodes then
1228 - ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes))
1229 - elseif v.text then
1230 - ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text)
1231 - else
1232 - ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs)
1233 - end
1234 - end
1235 - return ts
1236 - end
1237 - local body = collect_nodes(ir)
1238 -
1239 - for k in pairs(langsused) do
1240 - local spec = langpairs[k] or {color=0xaaaaaa}
1241 - stylesets.block_code_listing = stylesets.block_code_listing .. string.format(
1242 - [[section > figure.listing[lang="%s"]>figcaption::after
1243 - { content: '%s'; color: #%06x }]],
1244 - k, spec.name or k, spec.color)
1245 - end
1246 -
1247 - local prepcss = function(css)
1248 - local tone = function(fac, sat, sep, alpha)
1249 - local hsl = function(h,s,l,a)
1250 - local v = string.format('%s, %u%%, %u%%', h,s,l)
1251 - if a then
1252 - return string.format('hsla(%s, %s)', v,a)
1253 - else
1254 - return string.format('hsl(%s)', v)
1255 - end
1256 - end
1257 - sat = sat or 1
1258 - fac = math.max(math.min(fac, 1), 0)
1259 - sat = math.max(math.min(sat, 1), 0)
1260 - if opts.accent then
1261 - local hue = 'var(--accent)'
1262 - local hsep = tonumber(opts['hue-spread'])
1263 - if hsep and sep and sep ~= 0 then
1264 - hue = string.format('calc(%s - %s)', hue, sep * hsep)
1265 - end
1266 - return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha)
1267 - else
1268 - local g = math.floor(0xFF * fac)
1269 - return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha))
1270 - end
1271 - end
1272 - local replace = function(var,alpha,param)
1273 - local tonespan = opts.accent and .1 or 0
1274 - local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan
1275 - local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan
1276 - if var == 'bg' then
1277 - return tone(tbg,nil,nil,tonumber(alpha))
1278 - elseif var == 'fg' then
1279 - return tone(tfg,nil,nil,tonumber(alpha))
1280 - elseif var == 'width' then
1281 - return opts['width'] or '100vw'
1282 - elseif var == 'tone' then
1283 - local l, sep, sat
1284 - for i=1,3 do -- š
1285 - l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
1286 - if l then break end
1287 - end
1288 - l = ss.math.lerp(tonumber(l), tbg, tfg)
1289 - return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha))
1290 - end
1291 - end
1292 - css = css:gsub('@(%b[]):(%b[])', function(v,d) return opts[v:sub(2,-2)] or v:sub(2,-2) end)
1293 - css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace)
1294 - css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end)
1295 - css = css:gsub('@(%w+)/([0-9.]+)', replace)
1296 - css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end)
1297 - return (css:gsub('%s+',' '))
1298 - end
1299 -
1300 - local styles = {}
1301 - if opts.width then
1302 - table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
1303 - end
1304 - if opts.accent then
1305 - table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
1306 - end
1307 - if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then
1308 - stylesNeeded.accent = true
1309 - end
1310 -
1311 -
1312 - for k in pairs(stylesNeeded) do
1313 - if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end
1314 - table.insert(styles, prepcss(stylesets[k]))
1315 - end
1316 -
1317 - local head = {}
1318 - local styletag = ''
1319 - if opts['link-css'] then
1320 - local css = opts['link-css']
1321 - if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
1322 - styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
1323 - end
1324 - if next(styles) then
1325 - if opts['gen-styles'] then
1326 - styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
1327 - end
1328 - table.insert(head, styletag)
1329 - end
1330 -
1331 - if opts['fossil-uv'] then
1332 - return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
1333 - elseif opts.snippet then
1334 - return styletag .. body
1335 - else
1336 - return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
1337 - end
1338 502 end
503 +
504 +-- renderer engines
1339 505
1340 506 do -- define span control sequences
1341 507 local function formatter(sty)
1342 508 return function(s,c)
1343 509 return {
1344 510 kind = 'format';
1345 511 style = sty;
................................................................................
1474 640 end
1475 641 return r
1476 642 end)
1477 643 local m = {s} --TODO
1478 644 return {
1479 645 kind = 'math';
1480 646 original = s;
1481 - spans = m;
647 + spans = {s};
1482 648 origin = c:clone();
1483 649 };
1484 650 end};
1485 651 {seq = '&', parse = function(s, c)
1486 652 local r, t = s:match '^([^%s]+)%s*(.-)$'
1487 653 return {
1488 654 kind = 'deref';
1489 655 spans = (t and t ~= "") and ct.parse_span(t, c) or {};
1490 - ref = r;
656 + ref = r;
1491 657 origin = c:clone();
1492 658 }
1493 659 end};
1494 660 {seq = '^', parse = function(s, c)
1495 661 local fn, t = s:match '^([^%s]+)%s*(.-)$'
1496 662 return {
1497 663 kind = 'footnote';
................................................................................
1576 742 table.insert(spans,o)
1577 743 elseif c == '[' then
1578 744 flush()
1579 745 local substr, following = delimited('[',']',str:sub(p.byte))
1580 746 p.next.byte = following + p.byte
1581 747 local found = false
1582 748 for _,i in pairs(ct.spanctls) do
1583 - if startswith(substr, i.seq) then
749 + if ss.str.begins(substr, i.seq) then
1584 750 found = true
1585 751 table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx))
1586 752 break
1587 753 end
1588 754 end
1589 755 if not found then
1590 756 ctx:fail('no recognized control sequence in [%s]', substr)