cortav  Diff

Differences From Artifact [028f351fed]:

To Artifact [5feb0b86b1]:


   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 '&sect;'};
  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)