cortav  Diff

Differences From Artifact [39c7338664]:

To Artifact [5903337619]:


    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