Differences From
Artifact [39c7338664]:
13 13 function ct.render.html(doc, opts, setup)
14 14 local doctitle = opts['title']
15 15 local f = string.format
16 16 local getSafeID = ct.tool.namespace()
17 17
18 18 local footnotes = {}
19 19 local footnotecount = 0
20 +
21 + local cdata = function(...) return ... end
22 + if opts.epub then
23 + opts.xhtml = true
24 + end
25 +
26 + if opts.xhtml then
27 + cdata = function(s)
28 + return '<![CDATA[' .. s .. ']]>'
29 + end
30 + end
20 31
21 32 local langsused = {}
22 33 local langpairs = {
23 34 lua = { color = 0x9377ff };
24 35 terra = { color = 0xff77c8 };
25 36 c = { name = 'C', color = 0x77ffe8 };
26 37 html = { color = 0xfff877 };
................................................................................
44 55 li {
45 56 padding: 0.1em 0;
46 57 }
47 58 ]];
48 59 list_ordered = [[]];
49 60 list_unordered = [[]];
50 61 footnote = [[
51 - div.footnote {
62 + aside.footnote {
52 63 font-family: 90%;
53 64 grid-template-columns: 1em 1fr min-content;
54 65 grid-template-rows: 1fr min-content;
55 66 position: fixed;
56 67 padding: 1em;
57 68 background: @tone(0.03);
58 69 margin:auto;
59 70 }
60 71 @media screen {
61 - div.footnote {
72 + aside.footnote {
62 73 display: grid;
63 74 left: 10em;
64 75 right: 10em;
65 76 max-width: calc(@width + 2em);
66 77 max-height: 30vw;
67 78 bottom: 1em;
68 79 border: 1px solid black;
69 80 transform: translateY(200%);
70 81 transition: 0.4s;
71 82 z-index: 100;
72 83 }
73 - div.footnote:target {
84 + aside.footnote:target {
74 85 transform: translateY(0%);
75 86 }
76 87 #cover {
77 88 position: fixed;
78 89 top: 0;
79 90 left: 0;
80 91 height: 100vh; width: 100vw;
................................................................................
82 93 @tone/0.8(-0.07),
83 94 @tone/0.4(-0.07));
84 95 opacity: 0%;
85 96 transition: 1s;
86 97 pointer-events: none;
87 98 backdrop-filter: blur(0px);
88 99 }
89 - div.footnote:target ~ #cover {
100 + aside.footnote:target ~ #cover {
90 101 opacity: 100%;
91 102 pointer-events: all;
92 103 backdrop-filter: blur(5px);
93 104 }
94 105 }
95 106 @media print {
96 - div.footnote {
107 + aside.footnote {
97 108 display: grid;
98 109 position: relative;
99 110 }
100 - div.footnote:first-of-type {
111 + aside.footnote:first-of-type {
101 112 border-top: 1px solid black;
102 113 }
103 114 }
104 115
105 - div.footnote > a[href="#0"]{
116 + aside.footnote > a[href="#0"]{
106 117 grid-row: 2/3;
107 118 grid-column: 3/4;
108 119 display: block;
109 120 text-align: center;
110 121 padding: 0 0.3em;
111 122 text-decoration: none;
112 123 background: @tone(0.2);
................................................................................
116 127 font-size: 150%;
117 128 -webkit-user-select: none;
118 129 -ms-user-select: none;
119 130 user-select: none;
120 131 -webkit-user-drag: none;
121 132 user-drag: none;
122 133 }
123 - div.footnote > a[href="#0"]:hover {
134 + aside.footnote > a[href="#0"]:hover {
124 135 background: @tone(0.3);
125 136 color: @tone(2);
126 137 }
127 - div.footnote > a[href="#0"]:active {
138 + aside.footnote > a[href="#0"]:active {
128 139 background: @tone(0.05);
129 140 color: @tone(0.4);
130 141 }
131 142 @media print {
132 - div.footnote > a[href="#0"]{
143 + aside.footnote > a[href="#0"]{
133 144 display:none;
134 145 }
135 146 }
136 - div.footnote > div.number {
147 + aside.footnote > div.number {
137 148 text-align:right;
138 149 grid-row: 1/2;
139 150 grid-column: 1/2;
140 151 }
141 - div.footnote > div.text {
152 + aside.footnote > div.text {
142 153 grid-row: 1/2;
143 154 grid-column: 2/4;
144 155 padding-left: 1em;
145 156 overflow-y: auto;
146 157 }
147 - div.footnote > div.text > p:first-child {
158 + aside.footnote > div.text > p:first-child {
148 159 margin-top: 0;
149 160 }
150 161 ]];
151 162 header = [[
152 163 body { padding: 0 2.5em !important }
153 164 h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
154 165 h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
................................................................................
161 172 h1,h2,h3,h4,h5,h6 {
162 173 margin-top: 0;
163 174 margin-bottom: 0;
164 175 }
165 176 :is(h1,h2,h3,h4,h5,h6) + p {
166 177 margin-top: 0.4em;
167 178 }
168 -
169 179 ]];
170 180 headingAnchors = [[
171 181 :is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
172 182 text-decoration: none;
173 183 font-size: 1.2em;
174 184 padding: 0.3em;
175 185 opacity: 0%;
................................................................................
251 261 }
252 262 section > aside p {
253 263 margin: 0;
254 264 margin-top: 0.6em;
255 265 }
256 266 section > aside p:first-child {
257 267 margin: 0;
268 + }
269 + section aside + aside {
270 + margin-top: 0.5em;
258 271 }
259 272 ]];
260 273 code = [[
261 274 code {
262 275 display: inline-block;
263 276 background: @tone(-1);
264 277 color: @tone(0.7);
................................................................................
436 449 doc.stage.job = renderJob;
437 450
438 451 local runhook = function(h, ...)
439 452 return renderJob:hook(h, render_state_handle, ...)
440 453 end
441 454
442 455 local tagproc do
443 - local elt = function(t,attrs)
444 - return f('<%s%s>', t,
445 - attrs and ss.reduce(function(a,b) return a..b end, '',
456 + local html_open = function(t,attrs)
457 + if attrs then
458 + return t .. ss.reduce(function(a,b) return a..b end, '',
446 459 ss.map(function(v,k)
447 460 if v == true
448 461 then return ' '..k
449 462 elseif v then return f(' %s="%s"', k, v)
450 463 end
451 - end, attrs)) or '')
464 + end, attrs))
465 + else return t end
466 + end
467 +
468 + local elt = function(t,attrs)
469 + if opts.xhtml then
470 + return f('<%s />', html_open(t,attrs))
471 + end
472 + return f('<%s>', html_open(t,attrs))
452 473 end
453 474
454 475 tagproc = {
455 476 toTXT = {
456 477 tag = function(t,a,v) return v end;
457 478 elt = function(t,a) return '' end;
458 479 catenate = table.concat;
................................................................................
468 489 } end;
469 490
470 491 catenate = function(...) return ... end;
471 492 };
472 493 toHTML = {
473 494 elt = elt;
474 495 tag = function(t,attrs,body)
475 - return f('%s%s</%s>', elt(t,attrs), body, t)
496 + return f('<%s>%s</%s>', html_open(t,attrs), body, t)
476 497 end;
477 498 catenate = table.concat;
478 499 };
479 500 }
480 501 end
481 502
482 503 local function getBaseRenderers(procs, span_renderers)
483 504 local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
484 505 local htmlDoc = function(title, head, body)
485 - return [[<!doctype html>]] .. tag('html',nil,
506 + local attrs
507 + local header = [[<!doctype html>]]
508 + if opts['epub'] then
509 + -- so cursed
510 + attrs = {
511 + xmlns = "http://www.w3.org/1999/xhtml";
512 + ['xmlns:epub'] = "http://www.idpf.org/2007/ops";
513 + }
514 + header = [[<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE html>]]
515 + end
516 + return header .. tag('html',attrs,
486 517 tag('head', nil,
487 - elt('meta',{charset = 'utf-8'}) ..
518 + (opts.epub and '' or elt('meta',{charset = 'utf-8'})) ..
488 519 (title and tag('title', nil, title) or '') ..
489 520 (head or '')) ..
490 521 tag('body', nil, body or ''))
491 522 end
492 523
493 524 local function htmlSpan(spans, block, sec)
494 525 local text = {}
................................................................................
645 676 elseif d.crit then
646 677 b.origin:fail('critical extension %s unavailable', d.ext)
647 678 elseif d.failthru then
648 679 return htmlSpan(d.spans, b, s)
649 680 end
650 681 end
651 682 function span_renderers.footnote(f,b,s)
652 - addStyle 'footnote'
683 + local linkattr = {}
684 + if opts.epub then
685 + linkattr['epub:type'] = 'noteref'
686 + else
687 + addStyle 'footnote'
688 + end
653 689 local source, sid, ssec = b.origin:ref(f.ref)
654 690 local cnc = getSafeID(ssec) .. ' ' .. sid
655 691 local fn
656 692 if footnotes[cnc] then
657 693 fn = footnotes[cnc]
658 694 else
659 695 footnotecount = footnotecount + 1
660 696 fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
661 697 fn.id = getSafeID(fn)
662 698 footnotes[cnc] = fn
663 699 end
664 - return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) ..
700 + linkattr.href = '#'..fn.id
701 + return tag('a', linkattr, htmlSpan(f.spans) ..
665 702 tag('sup',nil, fn.num))
666 703 end
667 704
668 705 return span_renderers
669 706 end
707 +
708 + local astproc
670 709
671 710 local function getBlockRenderers(procs, sr)
672 711 local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
673 712 local null = function() return catenate{} end
674 713
675 714 local block_renderers = {
676 715 anchor = function(b,s)
................................................................................
746 785 return tag('aside', {}, bn)
747 786 end;
748 787 ['break'] = function() -- HACK
749 788 -- lists need to be rewritten to work like asides
750 789 return '';
751 790 end;
752 791 }
792 +
793 + function block_renderers.quote(b,s)
794 + local ir = {}
795 + local toIR = block_renderers
796 + for i, sec in ipairs(b.doc.secorder) do
797 + local secnodes = {}
798 + for i, bl in ipairs(sec.blocks) do
799 + if toIR[bl.kind] then
800 + table.insert(secnodes, toIR[bl.kind](bl,sec))
801 + end
802 + end
803 + if next(secnodes) then
804 + if b.doc.secorder[2] then --#secs>1?
805 + -- only wrap in a section if >1 section
806 + table.insert(ir, tag('section',
807 + {id = getSafeID(sec)},
808 + secnodes))
809 + else
810 + ir = secnodes
811 + end
812 + end
813 + end
814 + return tag('blockquote', b.id and {id=getSafeID(b)} or {}, catenate(ir))
815 + end
816 +
753 817 return block_renderers;
754 818 end
755 819
756 820 local function getRenderers(procs)
757 821 local span_renderers = getSpanRenderers(procs)
758 822 local r = getBaseRenderers(procs,span_renderers)
759 823 r.block_renderers = getBlockRenderers(procs, r)
760 824 return r
761 825 end
762 826
763 - local astproc = {
827 + astproc = {
764 828 toHTML = getRenderers(tagproc.toHTML);
765 829 toTXT = getRenderers(tagproc.toTXT);
766 830 toIR = { };
767 831 }
768 832 astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
769 833 astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
770 834 -- note we use HTML here instead of IR span renderers, because as things
................................................................................
776 840 render_state_handle.tagproc = tagproc;
777 841
778 842 -- bind to legacy names
779 843 -- yikes this needs to be cleaned up so badly
780 844 local ir = {}
781 845 local dr = astproc.toHTML -- default renderers
782 846 local plainr = astproc.toTXT
783 - local irBlockRdrs = astproc.toIR.block_renderers;
784 847
785 848 render_state_handle.ir = ir;
786 849
787 850 local function renderBlocks(blocks, irs)
788 851 for i, block in ipairs(blocks) do
789 852 local rd
790 - if irBlockRdrs[block.kind] then
791 - rd = irBlockRdrs[block.kind](block,sec)
853 + if astproc.toIR.block_renderers[block.kind] then
854 + rd = astproc.toIR.block_renderers[block.kind](block,sec)
792 855 else
793 856 local rdr = renderJob:proc('render',block.kind,'html')
794 857 if rdr then
795 858 rd = rdr({
796 859 state = render_state_handle;
797 860 tagproc = tagproc.toIR;
798 861 astproc = astproc.toIR;
................................................................................
814 877 rd.attrs.lang = rd.src.origin.lang
815 878 end
816 879 table.insert(irs.nodes, rd)
817 880 runhook('ir_section_node_insert', rd, irs, sec)
818 881 end
819 882 end
820 883 end
884 +
821 885 runhook('ir_assemble', ir)
822 886 for i, sec in ipairs(doc.secorder) do
823 887 if doctitle == nil and sec.depth == 1 and sec.heading_node then
824 888 doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
825 889 end
826 890 local irs
827 891 if sec.kind == 'ordinary' then
828 892 if #(sec.blocks) > 0 then
829 893 irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
830 894 runhook('ir_section_build', irs, sec)
831 895 renderBlocks(sec.blocks, irs)
832 896 end
833 - elseif sec.kind == 'blockquote' then
897 + elseif sec.kind == 'quote' then
834 898 elseif sec.kind == 'listing' then
835 899 elseif sec.kind == 'embed' then
836 900 end
837 901 if irs then table.insert(ir, irs) end
838 902 end
839 903
840 - for _, fn in pairs(footnotes) do
841 - local tag = tagproc.toIR.tag
842 - local body = {nodes={}}
843 - local ftir = {}
844 - for l in fn.source:gmatch('([^\n]*)') do
845 - ct.parse_line(l, fn.origin, ftir)
904 + do local fnsorted = {}
905 + for _, fn in pairs(footnotes) do
906 + fnsorted[fn.num] = fn
907 + end
908 +
909 + for _, fn in ipairs(fnsorted) do
910 + local tag = tagproc.toIR.tag
911 + local body = {nodes={}}
912 + local ftir = {}
913 + for l in fn.source:gmatch('([^\n]*)') do
914 + ct.parse_line(l, fn.origin, ftir)
915 + end
916 + renderBlocks(ftir,body)
917 + local fattr = {id=fn.id}
918 + if opts.epub then
919 + ---UUUUUUGHHH
920 + local npfx = string.format('(%u) ', fn.num)
921 + if next(body.nodes) then
922 + local n = body.nodes[1]
923 + repeat
924 + if n.nodes[1] then
925 + if type(n.nodes[1]) == 'string' then
926 + n.nodes[1] = npfx .. n.nodes[1]
927 + break
928 + end
929 + n = n.nodes[1]
930 + else
931 + n.nodes[1] = {tag='p',attrs={},nodes={npfx}}
932 + break
933 + end
934 + until false
935 +
936 + else
937 + body.nodes[1] = {tag='p',attrs={},nodes={npfx}}
938 + end
939 + fattr['epub:type'] = 'footnote'
940 + else
941 + fattr.class = 'footnote'
942 + end
943 + local note = tag('aside', fattr, opts.epub and body.nodes or {
944 + tag('div',{class='number'}, tostring(fn.num)),
945 + tag('div',{class='text'}, body.nodes),
946 + tag('a',{href='#0'},'⤫')
947 + })
948 + table.insert(ir, note)
846 949 end
847 - renderBlocks(ftir,body)
848 - local note = tag('div',{class='footnote',id=fn.id}, {
849 - tag('div',{class='number'}, tostring(fn.num)),
850 - tag('div',{class='text'}, body.nodes),
851 - tag('a',{href='#0'},'⤫')
852 - })
853 - table.insert(ir, note)
854 950 end
855 - if next(footnotes) then
951 + if next(footnotes) and not opts.epub then
856 952 table.insert(ir, tagproc.toIR.tag('div',{id='cover'},''))
857 953 end
858 954
859 955 -- restructure passes
860 956 runhook('ir_restructure_pre', ir)
861 957
862 958 ---- list insertion pass
................................................................................
1012 1108 local styles = {}
1013 1109 if opts.width then
1014 1110 table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
1015 1111 end
1016 1112 if opts.accent then
1017 1113 table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
1018 1114 end
1019 - if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then
1115 + if not opts.epub and (opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv'])) then
1020 1116 addStyle 'accent'
1021 1117 end
1022 1118
1023 1119
1024 1120 for _,k in pairs(stylesNeeded.order) do
1025 1121 if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)', k):throw() end
1026 1122 table.insert(styles, prepcss(stylesets[k]))
................................................................................
1031 1127 if opts['link-css'] then
1032 1128 local css = opts['link-css']
1033 1129 if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
1034 1130 styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
1035 1131 end
1036 1132 if next(styles) then
1037 1133 if opts['gen-styles'] then
1038 - styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
1134 + styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},cdata(table.concat(styles)))
1039 1135 end
1040 1136 table.insert(head, styletag)
1041 1137 end
1042 1138
1043 1139 if opts['fossil-uv'] then
1044 1140 return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
1045 1141 elseif opts.snippet then
1046 1142 return styletag .. body
1047 1143 else
1048 1144 return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
1049 1145 end
1050 1146 end