cortav  Check-in [d1b7d2fd5f]

Overview
Comment:get math parser working
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d1b7d2fd5f86585c3ffac19191559cb7915035cad3a50ea3d904a060502f97ed
User & Date: lexi on 2021-12-26 17:49:50
Other Links: manifest | tags
Context
2021-12-26
20:01
add C wrapper to generate true standalone binary embedding cortav bytecode check-in: a4a0570841 user: lexi tags: trunk
17:49
get math parser working check-in: d1b7d2fd5f user: lexi tags: trunk
04:08
all kindsa shit check-in: 52b9bce7dd user: lexi tags: trunk
Changes

Modified cortav.lua from [028f351fed] to [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)

Modified makefile from [4482353657] to [ac8c1379b3].

            1  +# [ʞ] makefile
            2  +#  ~ lexi hale <lexi@hale.su>
            3  +#  🄯 AGPLv3
            4  +#  ? this script performs the tasks necessary to produce a mostly
            5  +#    standalone cortav executable from the source files in the
            6  +#    repository. it assumes the presence of the following tools
            7  +#    in $SHELL or in $PATH:
            8  +#
            9  +#     * which    * cat
           10  +#     * mkdir    * echo
           11  +#     * install  * lua
           12  +#     * luac     * sh
           13  +#
           14  +#    if any are not present, the build will fail, although a missing
           15  +#    `which` can be worked around by specifying the paths to lua, luac,
           16  +#    and `sh` directly
           17  +#
           18  +#    eventually you will be able to set a "standalone" variable to
           19  +#    create a truly standalone binary, by embedding the binary in a
           20  +#    C program and statically linking it to lua.
           21  +
     1     22   lua != which lua
     2     23   luac != which luac
     3     24   sh != which sh
     4     25   
     5     26   extens = $(wildcard ext/*.lua)
     6     27   extens-names ?= $(basename $(notdir $(extens)))
           28  +rendrs = $(wildcard render/*.lua)
           29  +rendrs-names ?= $(basename $(notdir $(rendrs)))
           30  +
     7     31   build = build
     8     32   executable = cortav
     9     33   default-format-flags = -m html:width 40em
    10     34   
    11     35   prefix = $(HOME)/.local
    12     36   bin-prefix = $(prefix)/bin
    13     37   share-prefix = $(prefix)/share/$(executable)
................................................................................
    18     42   # this is not necessary for parsing the format, and can be
    19     43   # disabled by blanking the encoding-data list when building
    20     44   # ($ make encoding-data=)
    21     45   encoding-data  = ucstbls
    22     46   encoding-files = $(patsubst %,$(build)/%.lc,$(encoding-data))
    23     47   encoding-data-ucs = https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt
    24     48   
    25         -$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(extens) cli.lua | $(build)/
           49  +$(build)/$(executable): sirsem.lua $(encoding-files) cortav.lua $(rendrs) $(extens) cli.lua | $(build)/
    26     50   	@echo ' » building with extensions $(extens-names)'
           51  +	@echo ' » building with renderers $(rendrs-names)'
    27     52   	echo '#!$(lua)' > $@
    28     53   	luac -o - $^ >> $@
    29     54   	chmod +x $@
    30     55   
    31     56   $(build)/cortav.html: cortav.ct $(build)/$(executable) | $(build)/
    32     57   	$(build)/$(executable) $< -o $@ -m render:format html -y html:fossil-uv
    33     58   
................................................................................
    52     77   	echo "Exec=$(bin-prefix)/cortav-view.sh" >>$@
    53     78   
    54     79   %/:
    55     80   	mkdir -p $@
    56     81   
    57     82   $(build)/unicode.txt: | $(build)/
    58     83   	curl $(encoding-data-ucs) > $@
    59         -$(build)/ucstbls.lc: $(build)/unicode.txt | $(build)/
           84  +$(build)/ucstbls.lc: $(build)/unicode.txt tools/ucs.lua | $(build)/
    60     85   	$(lua) tools/ucs.lua $< | $(luac) -o $@ -
    61     86   
    62     87   .PHONY: install
    63     88   install: $(build)/cortav $(build)/cortav-view.sh $(build)/velartrill-cortav-view.desktop | $(bin-prefix)/
    64     89   	install $(build)/$(executable)  $(bin-prefix)
    65     90   	install $(build)/cortav-view.sh $(bin-prefix)
    66     91   	xdg-mime         install desk/velartrill-cortav.xml

Added render/html.lua version [1e64ee70c7].

            1  +local ct = require 'cortav'
            2  +local ss = require 'sirsem'
            3  +
            4  +-- install rendering function for html
            5  +function ct.render.html(doc, opts)
            6  +	local doctitle = opts['title']
            7  +	local f = string.format
            8  +	local getSafeID = ct.tool.namespace()
            9  +
           10  +	local footnotes = {}
           11  +	local footnotecount = 0
           12  +
           13  +	local langsused = {}
           14  +	local langpairs = {
           15  +		lua = { color = 0x9377ff };
           16  +		terra = { color = 0xff77c8 };
           17  +		c = { name = 'C', color = 0x77ffe8 };
           18  +		html = { color = 0xfff877 };
           19  +		scheme = { color = 0x77ff88 };
           20  +		lisp = { color = 0x77ff88 };
           21  +		fortran = { color = 0xff779a };
           22  +		python = { color = 0xffd277 };
           23  +		ruby = { color = 0xcdd6ff };
           24  +	}
           25  +
           26  +	local stylesets = {
           27  +		footnote = [[
           28  +			div.footnote {
           29  +			font-family: 90%;
           30  +				display: none;
           31  +				grid-template-columns: 1em 1fr min-content;
           32  +				grid-template-rows: 1fr min-content;
           33  +				position: fixed;
           34  +				padding: 1em;
           35  +				background: @tone(0.05);
           36  +				border: black;
           37  +				margin:auto;
           38  +			}
           39  +			div.footnote:target { display:grid; }
           40  +			@media screen {
           41  +				div.footnote {
           42  +					left: 10em;
           43  +					right: 10em;
           44  +					max-width: calc(@width + 2em);
           45  +					max-height: 30vw;
           46  +					bottom: 1em;
           47  +				}
           48  +			}
           49  +			@media print {
           50  +				div.footnote {
           51  +					position: relative;
           52  +				}
           53  +				div.footnote:first-of-type {
           54  +					border-top: 1px solid black;
           55  +				}
           56  +			}
           57  +
           58  +			div.footnote > a[href="#0"]{
           59  +				grid-row: 2/3;
           60  +				grid-column: 3/4;
           61  +				display: block;
           62  +				padding: 0.2em 0.7em;
           63  +				text-align: center;
           64  +				text-decoration: none;
           65  +				background: @tone(0.2);
           66  +				color: @tone(1);
           67  +				border: 1px solid black;
           68  +				margin-top: 0.6em;
           69  +				-webkit-user-select: none;
           70  +				-ms-user-select: none;
           71  +				user-select: none;
           72  +				-webkit-user-drag: none;
           73  +				user-drag: none;
           74  +			}
           75  +			div.footnote > a[href="#0"]:hover {
           76  +				background: @tone(0.3);
           77  +				color: @tone(2);
           78  +			}
           79  +			div.footnote > a[href="#0"]:active {
           80  +				background: @tone(0.05);
           81  +				color: @tone(0.4);
           82  +			}
           83  +			@media print {
           84  +				div.footnote > a[href="#0"]{
           85  +					display:none;
           86  +				}
           87  +			}
           88  +			div.footnote > div.number {
           89  +				text-align:right;
           90  +				grid-row: 1/2;
           91  +				grid-column: 1/2;
           92  +			}
           93  +			div.footnote > div.text {
           94  +				grid-row: 1/2;
           95  +				grid-column: 2/4;
           96  +				padding-left: 1em;
           97  +				overflow-y: scroll;
           98  +			}
           99  +		]];
          100  +		header = [[
          101  +			body { padding: 0 2.5em !important }
          102  +			h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
          103  +			h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
          104  +			h2 { font-size: 130%; margin: 0em -0.7em; }
          105  +			h3 { font-size: 110%; margin: 0em -0.5em; }
          106  +			h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; }
          107  +			h5 { font-size: 90%; font-weight: normal; }
          108  +			h6 { font-size: 80%; font-weight: normal; }
          109  +			h3, h4, h5, h6 { border-bottom-style: dotted !important; }
          110  +			h1,h2,h3,h4,h5,h6 {
          111  +				margin-top: 0;
          112  +				margin-bottom: 0;
          113  +			}
          114  +			:is(h1,h2,h3,h4,h5,h6) + p {
          115  +				margin-top: 0.4em;
          116  +			}
          117  +
          118  +		]];
          119  +		headingAnchors = [[
          120  +			:is(h1,h2,h3,h4,h5,h6) > a[href].anchor {
          121  +				text-decoration: none;
          122  +				font-size: 1.2em;
          123  +				padding: 0.3em;
          124  +				opacity: 0%;
          125  +				transition: 0.3s;
          126  +				font-weight: 100;
          127  +			}
          128  +			:is(h1,h2,h3,h4,h5,h6):hover > a[href].anchor {
          129  +				opacity: 50%;
          130  +			}
          131  +			:is(h1,h2,h3,h4,h5,h6) > a[href].anchor:hover {
          132  +				opacity: 100%;
          133  +			}
          134  +
          135  +			]] .. -- this is necessary to avoid the sections jumping around
          136  +			      -- when focus changes from one to another
          137  +			[[ section {
          138  +				border: 1px solid transparent;
          139  +			}
          140  +
          141  +			section:target {
          142  +				margin-left: -2em;
          143  +				margin-right: -2em;
          144  +				padding: 0 2em;
          145  +				background: @tone(0.04);
          146  +				border: 1px dotted @tone(0.3);
          147  +			}
          148  +
          149  +			section:target > :is(h1,h2,h3,h4,h5,h6) {
          150  +
          151  +			}
          152  +		]];
          153  +		paragraph = [[
          154  +			p {
          155  +				margin: 0.7em 0;
          156  +				text-align: justify;
          157  +			}
          158  +			section {
          159  +				margin: 1.2em 0;
          160  +			}
          161  +			section:first-child { margin-top: 0; }
          162  +		]];
          163  +		accent = [[
          164  +			@media screen {
          165  +				body { background: @bg; color: @fg }
          166  +				a[href] {
          167  +					color: @tone(0.7 30);
          168  +					text-decoration-color: @tone/0.4(0.7 30);
          169  +				}
          170  +				a[href]:hover {
          171  +					color: @tone(0.9 30);
          172  +					text-decoration-color: @tone/0.7(0.7 30);
          173  +				}
          174  +				h1 { color: @tone(2); }
          175  +				h2 { color: @tone(1.5); }
          176  +				h3 { color: @tone(1.2); }
          177  +				h4 { color: @tone(1); }
          178  +				h5,h6 { color: @tone(0.8); }
          179  +			}
          180  +			@media print {
          181  +				a[href] {
          182  +					text-decoration: none;
          183  +					color: black;
          184  +					font-weight: bold;
          185  +				}
          186  +				h1,h2,h3,h4,h5,h6 {
          187  +					border-bottom: 1px black;
          188  +				}
          189  +			}
          190  +		]];
          191  +		aside = [[
          192  +			section > aside {
          193  +				text-align: justify;
          194  +				margin: 0 1.5em;
          195  +				padding: 0.5em 0.8em;
          196  +				background: @tone(0.05);
          197  +				font-size: 90%;
          198  +				border-left: 5px solid @tone(0.2 15);
          199  +				border-right: 5px solid @tone(0.2 15);
          200  +			}
          201  +			section > aside p {
          202  +				margin: 0;
          203  +				margin-top: 0.6em;
          204  +			}
          205  +			section > aside p:first-child {
          206  +				margin: 0;
          207  +			}
          208  +      ]];
          209  +		code = [[
          210  +			code {
          211  +				display: inline-block;
          212  +				background: @tone(0.9);
          213  +				color: @bg;
          214  +				font-family: monospace;
          215  +				font-size: 90%;
          216  +				padding: 3px 5px;
          217  +			}
          218  +		]];
          219  +		var = [[
          220  +			var {
          221  +				font-style: italic;
          222  +				font-family: monospace;
          223  +				color: @tone(0.7);
          224  +			}
          225  +			code var {
          226  +				color: @tone(0.25);
          227  +			}
          228  +		]];
          229  +		math = [[
          230  +			span.equation {
          231  +				display: inline-block;
          232  +				background: @tone(0.08);
          233  +				color: @tone(2);
          234  +				padding: 0.1em 0.3em;
          235  +				border: 1px solid @tone(0.5);
          236  +			}
          237  +		]];
          238  +		abbr = [[
          239  +			abbr[title] { cursor: help; }
          240  +		]];
          241  +		editors_markup = [[]];
          242  +		block_code_listing = [[
          243  +			figure.listing {
          244  +				font-family: monospace;
          245  +				background: @tone(0.05);
          246  +				color: @fg;
          247  +				padding: 0;
          248  +				margin: 0.3em 0;
          249  +				counter-reset: line-number;
          250  +				position: relative;
          251  +				border: 1px solid @fg;
          252  +			}
          253  +			figure.listing>div {
          254  +				white-space: pre-wrap;
          255  +				tab-size: 3;
          256  +				-moz-tab-size: 3;
          257  +				counter-increment: line-number;
          258  +				text-indent: -2.3em;
          259  +				margin-left: 2.3em;
          260  +			}
          261  +			figure.listing>:is(div,hr)::before {
          262  +				width: 1.0em;
          263  +				padding: 0.2em 0.4em;
          264  +				text-align: right;
          265  +				display: inline-block;
          266  +				background-color: @tone(0.2);
          267  +				border-right: 1px solid @fg;
          268  +				content: counter(line-number);
          269  +				margin-right: 0.3em;
          270  +			}
          271  +			figure.listing>hr::before {
          272  +				color: transparent;
          273  +				padding-top: 0;
          274  +				padding-bottom: 0;
          275  +			}
          276  +			figure.listing>div::before {
          277  +				color: @fg;
          278  +			}
          279  +			figure.listing>div:last-child::before {
          280  +				padding-bottom: 0.5em;
          281  +			}
          282  +			figure.listing>figcaption:first-child {
          283  +				border: none;
          284  +				border-bottom: 1px solid @fg;
          285  +			}
          286  +			figure.listing>figcaption::after {
          287  +				display: block;
          288  +				float: right;
          289  +				font-weight: normal;
          290  +				font-style: italic;
          291  +				font-size: 70%;
          292  +				padding-top: 0.3em;
          293  +			}
          294  +			figure.listing>figcaption {
          295  +				font-family: sans-serif;
          296  +				font-size: 120%;
          297  +				padding: 0.2em 0.4em;
          298  +				border: none;
          299  +				color: @tone(2);
          300  +			}
          301  +			figure.listing > hr {
          302  +				border: none;
          303  +				margin: 0;
          304  +				height: 0.7em;
          305  +				counter-increment: line-number;
          306  +			}
          307  +		]];
          308  +	}
          309  +
          310  +	local stylesNeeded = {}
          311  +
          312  +	local render_state_handle = {
          313  +		doc = doc;
          314  +		opts = opts;
          315  +		style_rules = styles; -- use stylesneeded if at all possible
          316  +		stylesets = stylesets;
          317  +		stylesets_active = stylesNeeded;
          318  +		obj_htmlid = getSafeID;
          319  +		-- remaining fields added later
          320  +	}
          321  +
          322  +	local renderJob = doc:job('render_html', nil, render_state_handle)
          323  +	doc.stage.job = renderJob;
          324  +
          325  +	local runhook = function(h, ...)
          326  +		return renderJob:hook(h, render_state_handle, ...)
          327  +	end
          328  +
          329  +	local tagproc do
          330  +		local elt = function(t,attrs)
          331  +			return f('<%s%s>', t,
          332  +				attrs and ss.reduce(function(a,b) return a..b end, '',
          333  +					ss.map(function(v,k)
          334  +						if v == true
          335  +							then          return ' '..k
          336  +							elseif v then return f(' %s="%s"', k, v)
          337  +						end
          338  +					end, attrs)) or '')
          339  +		end
          340  +
          341  +		tagproc = {
          342  +			toTXT = {
          343  +				tag = function(t,a,v) return v  end;
          344  +				elt = function(t,a)   return '' end;
          345  +				catenate = table.concat;
          346  +			};
          347  +			toIR = {
          348  +				tag = function(t,a,v,o) return {
          349  +					tag = t, attrs = a;
          350  +					nodes = type(v) == 'string' and {v} or v, src = o
          351  +				} end;
          352  +
          353  +				elt = function(t,a,o) return {
          354  +					tag = t, attrs = a, src = o
          355  +				} end;
          356  +
          357  +				catenate = function(...) return ... end;
          358  +			};
          359  +			toHTML = {
          360  +				elt = elt;
          361  +				tag = function(t,attrs,body)
          362  +					return f('%s%s</%s>', elt(t,attrs), body, t)
          363  +				end;
          364  +				catenate = table.concat;
          365  +			};
          366  +		}
          367  +	end
          368  +
          369  +	local function getBaseRenderers(procs, span_renderers)
          370  +		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
          371  +		local htmlDoc = function(title, head, body)
          372  +			return [[<!doctype html>]] .. tag('html',nil,
          373  +				tag('head', nil,
          374  +					elt('meta',{charset = 'utf-8'}) ..
          375  +					(title and tag('title', nil, title) or '') ..
          376  +					(head or '')) ..
          377  +				tag('body', nil, body or ''))
          378  +		end
          379  +
          380  +		local function htmlSpan(spans, block, sec)
          381  +			local text = {}
          382  +			for k,v in pairs(spans) do
          383  +				if type(v) == 'string' then
          384  +					v=v:gsub('[<>&"]', function(x)
          385  +							return string.format('&#%02u;', string.byte(x))
          386  +						end)
          387  +					for fn, ext in renderJob:each('hook','render_html_sanitize') do
          388  +						v = fn(renderJob:delegate(ext), v)
          389  +					end
          390  +					table.insert(text,v)
          391  +				else
          392  +					table.insert(text, (span_renderers[v.kind](v, block, sec)))
          393  +				end
          394  +			end
          395  +			return table.concat(text)
          396  +		end
          397  +		return {htmlDoc=htmlDoc, htmlSpan=htmlSpan}
          398  +	end
          399  +
          400  +	local spanparse = function(...)
          401  +		local s = ct.parse_span(...)
          402  +		doc.docjob:hook('meddle_span', s)
          403  +		return s
          404  +	end
          405  +
          406  +	local cssRulesFor = {}
          407  +	local function getSpanRenderers(procs)
          408  +		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
          409  +		local span_renderers = {}
          410  +		local plainrdr = getBaseRenderers(tagproc.toTXT, span_renderers)
          411  +		local htmlSpan = getBaseRenderers(procs, span_renderers).htmlSpan
          412  +
          413  +		function span_renderers.format(sp,...)
          414  +			local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'}
          415  +			if sp.style == 'literal' and not opts['fossil-uv'] then
          416  +				stylesNeeded.code = true
          417  +			elseif sp.style == 'strike' or sp.style == 'insert' then
          418  +				stylesNeeded.editors_markup = true
          419  +			elseif sp.style == 'variable' then
          420  +				stylesNeeded.var = true
          421  +			end
          422  +			return tag(tags[sp.style],nil,htmlSpan(sp.spans,...))
          423  +		end
          424  +
          425  +		function span_renderers.deref(t,b,s)
          426  +			local r = b.origin:ref(t.ref)
          427  +			local name = t.ref
          428  +			if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
          429  +			if type(r) == 'string' then
          430  +				stylesNeeded.abbr = true
          431  +				return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name)
          432  +			end
          433  +			if r.kind == 'resource' then
          434  +				local rid = getSafeID(r, 'res-')
          435  +				if r.class == 'image' then
          436  +					if not cssRulesFor[r] then
          437  +						local css = prepcss(string.format([[
          438  +							section p > .%s {
          439  +							}
          440  +						]], rid))
          441  +						stylesets[r] = css
          442  +						cssRulesFor[r] = css
          443  +						stylesNeeded[r] = true
          444  +					end
          445  +					return tag('div',{class=rid},catenate{'blaah'})
          446  +				elseif r.class == 'video' then
          447  +					local vid = {}
          448  +					return tag('video',nil,vid)
          449  +				elseif r.class == 'font' then
          450  +					b.origin:fail('fonts cannot be instantiated, use %font directive instead')
          451  +				end
          452  +			else
          453  +				b.origin:fail('%s is not an object that can be embedded', t.ref)
          454  +			end
          455  +		end
          456  +
          457  +		function span_renderers.var(v,b,s)
          458  +			local val
          459  +			if v.pos then
          460  +				if not v.origin.invocation then
          461  +					v.origin:fail 'positional arguments can only be used in a macro invocation'
          462  +				elseif not v.origin.invocation.args[v.pos] then
          463  +					v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos)
          464  +				end
          465  +				val = v.origin.invocation.args[v.pos]
          466  +			else
          467  +				val = v.origin.doc:context_var(v.var, v.origin)
          468  +			end
          469  +			if v.raw then
          470  +				return val
          471  +			else
          472  +				return htmlSpan(ct.parse_span(val, v.origin), b, s)
          473  +			end
          474  +		end
          475  +
          476  +		function span_renderers.raw(v,b,s)
          477  +			return htmlSpan(v.spans, b, s)
          478  +		end
          479  +
          480  +		function span_renderers.link(sp,b,s)
          481  +			local href
          482  +			if b.origin.doc.sections[sp.ref] then
          483  +				href = '#' .. sp.ref
          484  +			else
          485  +				if sp.addr then href = sp.addr else
          486  +					local r = b.origin:ref(sp.ref)
          487  +					if type(r) == 'table' then
          488  +						href = '#' .. getSafeID(r)
          489  +					else href = r end
          490  +				end
          491  +			end
          492  +			return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href)
          493  +		end
          494  +
          495  +		span_renderers['line-break'] = function(sp,b,s)
          496  +			return elt('br')
          497  +		end
          498  +
          499  +		function span_renderers.macro(m,b,s)
          500  +			local macroname = plainrdr.htmlSpan(
          501  +				ct.parse_span(m.macro, b.origin), b,s)
          502  +			local r = b.origin:ref(macroname)
          503  +			if type(r) ~= 'string' then
          504  +				b.origin:fail('%s is an object, not a reference', t.ref)
          505  +			end
          506  +			local mctx = b.origin:clone()
          507  +			mctx.invocation = m
          508  +			return htmlSpan(ct.parse_span(r, mctx),b,s)
          509  +		end
          510  +		function span_renderers.math(m,b,s)
          511  +			stylesNeeded.math = true
          512  +			local spans = {}
          513  +			local function fmt(sp, target)
          514  +				for i,v in ipairs(sp) do
          515  +					if type(v) == 'string' then
          516  +						local x = ct.tool.mathfmt(b.origin, v)
          517  +						for _,v in ipairs(x) do
          518  +							table.insert(target, v)
          519  +						end
          520  +					elseif type(v) == 'table' then
          521  +						if v.spans then
          522  +							local tbl = ss.delegate(v)
          523  +							tbl.spans = {}
          524  +							fmt(v.spans, tbl.spans)
          525  +							table.insert(target, tbl)
          526  +						else
          527  +							table.insert(target, v)
          528  +						end
          529  +					end
          530  +				end
          531  +			end
          532  +			fmt(m.spans,spans)
          533  +
          534  +			return tag('span',{class='equation'},htmlSpan(spans, b, s))
          535  +		end;
          536  +		function span_renderers.directive(d,b,s)
          537  +			if d.ext == 'html' then
          538  +			elseif b.origin.doc:allow_ext(d.ext) then
          539  +			elseif d.crit then
          540  +				b.origin:fail('critical extension %s unavailable', d.ext)
          541  +			elseif d.failthru then
          542  +				return htmlSpan(d.spans, b, s)
          543  +			end
          544  +		end
          545  +		function span_renderers.footnote(f,b,s)
          546  +			stylesNeeded.footnote = true
          547  +			local source, sid, ssec = b.origin:ref(f.ref)
          548  +			local cnc = getSafeID(ssec) .. ' ' .. sid
          549  +			local fn
          550  +			if footnotes[cnc] then
          551  +				fn = footnotes[cnc]
          552  +			else
          553  +				footnotecount = footnotecount + 1
          554  +				fn = {num = footnotecount, origin = b.origin, fnid=cnc, source = source}
          555  +				fn.id = getSafeID(fn)
          556  +				footnotes[cnc] = fn
          557  +			end
          558  +			return tag('a', {href='#'..fn.id}, htmlSpan(f.spans) ..
          559  +						tag('sup',nil, fn.num))
          560  +		end
          561  +
          562  +		return span_renderers
          563  +	end
          564  +
          565  +	local function getBlockRenderers(procs, sr)
          566  +		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
          567  +		local null = function() return catenate{} end
          568  +
          569  +		local block_renderers = {
          570  +			anchor = function(b,s)
          571  +				return tag('a',{id = getSafeID(b)},null())
          572  +			end;
          573  +			paragraph = function(b,s)
          574  +				stylesNeeded.paragraph = true;
          575  +				return tag('p', nil, sr.htmlSpan(b.spans, b, s), b)
          576  +			end;
          577  +			directive = function(b,s)
          578  +				-- deal with renderer directives
          579  +				local _, cmd, args = b.words(2)
          580  +				if cmd == 'page-title' then
          581  +					if not opts.title then doctitle = args end
          582  +				elseif b.critical then
          583  +					b.origin:fail('critical HTML renderer directive “%s” not supported', cmd)
          584  +				end
          585  +			end;
          586  +			label = function(b,s)
          587  +				if ct.sec.is(b.captions) then
          588  +					if not (opts['fossil-uv'] or opts.snippet) then
          589  +						stylesNeeded.header = true
          590  +					end
          591  +					local h = math.min(6,math.max(1,b.captions.depth))
          592  +					return tag(f('h%u',h), nil, sr.htmlSpan(b.spans, b, s), b)
          593  +				else
          594  +					-- handle other uses of labels here
          595  +				end
          596  +			end;
          597  +			['list-item'] = function(b,s)
          598  +				return tag('li', nil, sr.htmlSpan(b.spans, b, s), b)
          599  +			end;
          600  +			table = function(b,s)
          601  +				local tb = {}
          602  +				for i, r in ipairs(b.rows) do
          603  +					local row = {}
          604  +					for i, c in ipairs(r) do
          605  +						table.insert(row, tag(c.header and 'th' or 'td',
          606  +						{align=c.align}, sr.htmlSpan(c.spans, b)))
          607  +					end
          608  +					table.insert(tb, tag('tr',nil,catenate(row)))
          609  +				end
          610  +				return tag('table',nil,catenate(tb))
          611  +			end;
          612  +			listing = function(b,s)
          613  +				stylesNeeded.block_code_listing = true
          614  +				local nodes = ss.map(function(l)
          615  +					if #l > 0 then
          616  +						return tag('div',nil,sr.htmlSpan(l, b, s))
          617  +					else
          618  +						return elt('hr')
          619  +					end
          620  +				end, b.lines)
          621  +				if b.title then
          622  +					table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title)))
          623  +				end
          624  +				if b.lang then langsused[b.lang] = true end
          625  +				return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
          626  +			end;
          627  +			aside = function(b,s)
          628  +				local bn = {}
          629  +				stylesNeeded.aside = true
          630  +				if #b.lines == 1 then
          631  +					bn[1] = sr.htmlSpan(b.lines[1], b, s)
          632  +				else
          633  +					for _,v in pairs(b.lines) do
          634  +						table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s)))
          635  +					end
          636  +				end
          637  +				return tag('aside', {}, bn)
          638  +			end;
          639  +			['break'] = function() -- HACK
          640  +				-- lists need to be rewritten to work like asides
          641  +				return '';
          642  +			end;
          643  +		}
          644  +		return block_renderers;
          645  +	end
          646  +
          647  +	local function getRenderers(procs)
          648  +		local span_renderers = getSpanRenderers(procs)
          649  +		local r = getBaseRenderers(procs,span_renderers)
          650  +		r.block_renderers = getBlockRenderers(procs, r)
          651  +		return r
          652  +	end
          653  +
          654  +	local astproc = {
          655  +		toHTML = getRenderers(tagproc.toHTML);
          656  +		toTXT  = getRenderers(tagproc.toTXT);
          657  +		toIR   = { };
          658  +	}
          659  +	astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
          660  +	astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
          661  +		-- note we use HTML here instead of IR span renderers, because as things
          662  +		-- currently stand we don't need that level of resolution. if we ever
          663  +		-- get to the point where we want to be able to twiddle spans around
          664  +		-- we'll need to introduce an IR span renderer
          665  +
          666  +	render_state_handle.astproc = astproc;
          667  +	render_state_handle.tagproc = tagproc;
          668  +
          669  +	-- bind to legacy names
          670  +	-- yikes this needs to be cleaned up so badly
          671  +	local ir = {}
          672  +	local dr = astproc.toHTML -- default renderers
          673  +	local plainr = astproc.toTXT
          674  +	local irBlockRdrs = astproc.toIR.block_renderers;
          675  +
          676  +	render_state_handle.ir = ir;
          677  +
          678  +	local function renderBlocks(blocks, irs)
          679  +		for i, block in ipairs(blocks) do
          680  +			local rd
          681  +			if irBlockRdrs[block.kind] then
          682  +				rd = irBlockRdrs[block.kind](block,sec)
          683  +			else
          684  +				local rdr = renderJob:proc('render',block.kind,'html')
          685  +				if rdr then
          686  +					rd = rdr({
          687  +						state = render_state_handle;
          688  +						tagproc = tagproc.toIR;
          689  +						astproc = astproc.toIR;
          690  +					}, block, sec)
          691  +				end
          692  +			end
          693  +			if rd then
          694  +				if opts['heading-anchors'] and block == sec.heading_node then
          695  +					stylesNeeded.headingAnchors = true
          696  +					table.insert(rd.nodes, ' ')
          697  +					table.insert(rd.nodes, {
          698  +						tag = 'a';
          699  +						attrs = {href = '#' .. irs.attrs.id, class='anchor'};
          700  +						nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '&sect;'};
          701  +					})
          702  +				end
          703  +				if rd.src and rd.src.origin.lang then
          704  +					if not rd.attrs then rd.attrs = {} end
          705  +					rd.attrs.lang = rd.src.origin.lang
          706  +				end
          707  +				table.insert(irs.nodes, rd)
          708  +				runhook('ir_section_node_insert', rd, irs, sec)
          709  +			end
          710  +		end
          711  +	end
          712  +	runhook('ir_assemble', ir)
          713  +	for i, sec in ipairs(doc.secorder) do
          714  +		if doctitle == nil and sec.depth == 1 and sec.heading_node then
          715  +			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
          716  +		end
          717  +		local irs
          718  +		if sec.kind == 'ordinary' then
          719  +			if #(sec.blocks) > 0 then
          720  +				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
          721  +				runhook('ir_section_build', irs, sec)
          722  +				renderBlocks(sec.blocks, irs)
          723  +			end
          724  +		elseif sec.kind == 'blockquote' then
          725  +		elseif sec.kind == 'listing' then
          726  +		elseif sec.kind == 'embed' then
          727  +		end
          728  +		if irs then table.insert(ir, irs) end
          729  +	end
          730  +
          731  +	for _, fn in pairs(footnotes) do
          732  +		local tag = tagproc.toIR.tag
          733  +		local body = {nodes={}}
          734  +		local ftir = {}
          735  +		for l in fn.source:gmatch('([^\n]*)') do
          736  +			ct.parse_line(l, fn.origin, ftir)
          737  +		end
          738  +		renderBlocks(ftir,body)
          739  +		local note = tag('div',{class='footnote',id=fn.id}, {
          740  +			tag('div',{class='number'}, tostring(fn.num)),
          741  +			tag('div',{class='text'}, body.nodes),
          742  +			tag('a',{href='#0'},'close')
          743  +		})
          744  +		table.insert(ir, note)
          745  +	end
          746  +
          747  +	-- restructure passes
          748  +	runhook('ir_restructure_pre', ir)
          749  +
          750  +	---- list insertion pass
          751  +	local lists = {}
          752  +	for _, sec in pairs(ir) do
          753  +		if sec.tag == 'section' then
          754  +			local i = 1 while i <= #sec.nodes do local v = sec.nodes[i]
          755  +				if v.tag == 'li' then
          756  +					local ltag
          757  +					if v.src.ordered
          758  +						then ltag = 'ol'
          759  +						else ltag = 'ul'
          760  +					end
          761  +					local last = i>1 and sec.nodes[i-1]
          762  +					if last and last.embed == 'list' and not (
          763  +						last.ref[#last.ref].src.depth   == v.src.depth and
          764  +						last.ref[#last.ref].src.ordered ~= v.src.ordered
          765  +					) then
          766  +						-- add to existing list
          767  +						table.insert(last.ref, v)
          768  +						table.remove(sec.nodes, i) i = i - 1
          769  +					else
          770  +						-- wrap in list
          771  +						local newls = {v}
          772  +						sec.nodes[i] = {embed = 'list', ref = newls}
          773  +						table.insert(lists,newls)
          774  +					end
          775  +				end
          776  +			i = i + 1 end
          777  +		end
          778  +	end
          779  +
          780  +	for _, sec in pairs(ir) do
          781  +		if sec.tag == 'section' then
          782  +			for i, elt in pairs(sec.nodes) do
          783  +				if elt.embed == 'list' then
          784  +					local function fail_nest()
          785  +						elt.ref[1].src.origin:fail('improper list nesting')
          786  +					end
          787  +					local struc = {attrs={}, nodes={}}
          788  +					if elt.ref[1].src.ordered then struc.tag = 'ol' else struc.tag = 'ul' end
          789  +					if elt.ref[1].src.depth ~= 1 then fail_nest() end
          790  +
          791  +					local stack = {struc}
          792  +					local copyNodes = function(old,new)
          793  +						for i,v in ipairs(old) do new[#new + i] = v end
          794  +					end
          795  +					for i,e in ipairs(elt.ref) do
          796  +						if e.src.depth > #stack then
          797  +							if e.src.depth - #stack > 1 then fail_nest() end
          798  +							local newls = {attrs={}, nodes={e}}
          799  +							copyNodes(e.nodes,newls)
          800  +							if e.src.ordered then newls.tag = 'ol' else newls.tag='ul' end
          801  +							table.insert(stack[#stack].nodes[#stack[#stack].nodes].nodes, newls)
          802  +							table.insert(stack, newls)
          803  +						else
          804  +							if e.src.depth < #stack then
          805  +								-- pop entries off the stack
          806  +								for i=#stack, e.src.depth+1, -1 do stack[i] = nil end
          807  +							end
          808  +							table.insert(stack[#stack].nodes, e)
          809  +						end
          810  +					end
          811  +
          812  +					sec.nodes[i] = struc
          813  +				end
          814  +			end
          815  +		end
          816  +	end
          817  +
          818  +	runhook('ir_restructure_post', ir)
          819  +
          820  +	-- collection pass
          821  +	local function collect_nodes(t)
          822  +		local ts = ''
          823  +		for i,v in ipairs(t) do
          824  +			if type(v) == 'string' then
          825  +				ts = ts .. v
          826  +			elseif v.nodes then
          827  +				ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes))
          828  +			elseif v.text then
          829  +				ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text)
          830  +			else
          831  +				ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs)
          832  +			end
          833  +		end
          834  +		return ts
          835  +	end
          836  +	local body = collect_nodes(ir)
          837  +
          838  +	for k in pairs(langsused) do
          839  +		local spec = langpairs[k] or {color=0xaaaaaa}
          840  +		stylesets.block_code_listing = stylesets.block_code_listing .. string.format(
          841  +			[[section > figure.listing[lang="%s"]>figcaption::after
          842  +				{ content: '%s'; color: #%06x }]],
          843  +			k, spec.name or k, spec.color)
          844  +	end
          845  +
          846  +	local prepcss = function(css)
          847  +		local tone = function(fac, sat, sep, alpha)
          848  +			local hsl = function(h,s,l,a)
          849  +				local v = string.format('%s, %u%%, %u%%', h,s,l)
          850  +				if a then
          851  +					return string.format('hsla(%s, %s)', v,a)
          852  +				else
          853  +					return string.format('hsl(%s)', v)
          854  +				end
          855  +			end
          856  +			sat = sat or 1
          857  +			fac = math.max(math.min(fac, 1), 0)
          858  +			sat = math.max(math.min(sat, 1), 0)
          859  +			if opts.accent then
          860  +				local hue = 'var(--accent)'
          861  +				local hsep = tonumber(opts['hue-spread'])
          862  +				if hsep and sep and sep ~= 0 then
          863  +					hue = string.format('calc(%s - %s)', hue, sep * hsep)
          864  +				end
          865  +				return hsl(hue, math.floor(100*sat), math.floor(100*fac), alpha)
          866  +			else
          867  +				local g = math.floor(0xFF * fac)
          868  +				return string.format('#' .. string.rep('%02x',alpha and 4 or 3), g,g,g,alpha and math.floor(0xFF*alpha))
          869  +			end
          870  +		end
          871  +		local replace = function(var,alpha,param)
          872  +			local tonespan = opts.accent and .1 or 0
          873  +			local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan
          874  +			local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan
          875  +			if var == 'bg' then
          876  +				return tone(tbg,nil,nil,tonumber(alpha))
          877  +			elseif var == 'fg' then
          878  +				return tone(tfg,nil,nil,tonumber(alpha))
          879  +			elseif var == 'width' then
          880  +				return opts['width'] or '100vw'
          881  +			elseif var == 'tone' then
          882  +				local l, sep, sat
          883  +				for i=1,3 do -- 🙄
          884  +					l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
          885  +					if l then break end
          886  +				end
          887  +				l = ss.math.lerp(tonumber(l), tbg, tfg)
          888  +				return tone(l, tonumber(sat), tonumber(sep), tonumber(alpha))
          889  +			end
          890  +		end
          891  +		css = css:gsub('@(%b[]):(%b[])', function(v,d) return opts[v:sub(2,-2)] or v:sub(2,-2) end)
          892  +		css = css:gsub('@(%w+)/([0-9.]+)(%b())', replace)
          893  +		css = css:gsub('@(%w+)(%b())', function(a,b) return replace(a,nil,b) end)
          894  +		css = css:gsub('@(%w+)/([0-9.]+)', replace)
          895  +		css = css:gsub('@(%w+)', function(a,b) return replace(a,nil,b) end)
          896  +		return (css:gsub('%s+',' '))
          897  +	end
          898  +
          899  +	local styles = {}
          900  +	if opts.width then
          901  +		table.insert(styles, string.format([[body {padding:0 1em;margin:auto;max-width:%s}]], opts.width))
          902  +	end
          903  +	if opts.accent then
          904  +		table.insert(styles, string.format(':root {--accent:%s}', opts.accent))
          905  +	end
          906  +	if opts.accent or (not opts['dark-on-light']) and (not opts['fossil-uv']) then
          907  +		stylesNeeded.accent = true
          908  +	end
          909  +
          910  +
          911  +	for k in pairs(stylesNeeded) do
          912  +		if not stylesets[k] then ct.exns.unimpl('styleset %s not implemented (!)',  k):throw() end
          913  +		table.insert(styles, prepcss(stylesets[k]))
          914  +	end
          915  +
          916  +	local head = {}
          917  +	local styletag = ''
          918  +	if opts['link-css'] then
          919  +		local css = opts['link-css']
          920  +		if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
          921  +		styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
          922  +	end
          923  +	if next(styles) then
          924  +		if opts['gen-styles'] then
          925  +			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
          926  +		end
          927  +		table.insert(head, styletag)
          928  +	end
          929  +
          930  +	if opts['fossil-uv'] then
          931  +		return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
          932  +	elseif opts.snippet then
          933  +		return styletag .. body
          934  +	else
          935  +		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
          936  +	end
          937  +end

Modified sirsem.lua from [581e1b0127] to [dc1f0ae1fb].

   212    212   	ascii = {
   213    213   		len = string.len; char = string.char; codepoint = string.byte;
   214    214   		iswhitespace = function(c)
   215    215   			return (c == ' ') or (c == '\t') or (c == '\n')
   216    216         end;
   217    217   		ranges = {
   218    218   			{0x00,0x1a, cc.ctl};
   219         -			{0x1b,0x1b, cc.ctl, cp.disallow};
          219  +			{0x1b,0x1b, cc.ctl | cp.disallow};
   220    220   			{0x1c,0x1f, cc.ctl};
   221    221   			{0x20,0x20, cc.space};
   222    222   			{0x21,0x22, cc.punct};
   223    223   			{0x23,0x26, cc.symbol};
   224    224   			{0x27,0x29, cc.punct};
   225    225   			{0x2a,0x2b, cc.symbol};
   226    226   			{0x2c,0x2f, cc.punct};
   227         -			{0x30,0x39, cc.numeral, cp.hexnumeral};
          227  +			{0x30,0x39, cc.numeral | cp.hexnumeral};
   228    228   			{0x3a,0x3b, cc.punct};
   229         -			{0x3c,0x3e, cc.symbol, cp.mathop};
          229  +			{0x3c,0x3e, cc.symbol | cp.mathop};
   230    230   			{0x3f,0x3f, cc.punct};
   231    231   			{0x40,0x40, cc.symbol};
   232         -			{0x41,0x46, cc.letter, cp.ucase, cp.hexnumeral};
   233         -			{0x47,0x5a, cc.letter, cp.ucase};
   234         -			{0x5b,0x5d, cc.symbol, cp.mathop};
   235         -			{0x5e,0x5e, cc.symbol, mathop};
          232  +			{0x41,0x46, cc.letter | cp.upper | cp.hexnumeral};
          233  +			{0x47,0x5a, cc.letter | cp.upper};
          234  +			{0x5b,0x5d, cc.symbol | cp.mathop};
          235  +			{0x5e,0x5e, cc.symbol | cp.mathop};
   236    236   			{0x5f,0x60, cc.symbol};
   237         -			{0x61,0x66, cc.letter, cp.lcase, cp.hexnumeral};
   238         -			{0x67,0x7a, cc.letter, cp.lcase};
          237  +			{0x61,0x66, cc.letter | cp.lower | cp.hexnumeral};
          238  +			{0x67,0x7a, cc.letter | cp.lower};
   239    239   			{0x7b,0x7e, cc.symbol};
   240    240   			{0x7f,0x7f, cc.ctl, cp.disallow};
   241    241   		}
   242    242   	};
   243    243   	raw = {len = string.len; char = string.char; codepoint = string.byte;
   244    244   		encodeUCS = function(str) return str end;
   245    245   		iswhitespace = function(c)
................................................................................
   250    250   
   251    251   -- unicode ranges are optionally generated from consortium data
   252    252   -- files and injected through a generated source file. if this
   253    253   -- part of the build process is disabled (e.g. due to lack of
   254    254   -- internet access, or to keep the size of the executable as
   255    255   -- small as possible), we still at least can make the ascii
   256    256   -- ranges available to UTF8 (UTF8 being a superset of ascii)
   257         -ss.str.enc.utf8.ranges = ss.delegate(ss.str.enc.ascii.ranges)
          257  +ss.str.enc.utf8.ranges = ss.str.enc.ascii.ranges
   258    258   
   259    259   function ss.str.enc.ascii.encodeUCS(str)
   260    260   	local newstr = ''
   261    261   	for c,p in ss.str.each(ss.str.enc.utf8, str, true) do
   262    262   		if c > 0x7F then
   263    263   			newstr = newstr .. '?'
   264    264   		else
................................................................................
   266    266   		end
   267    267   	end
   268    268   end
   269    269   
   270    270   for _, v in pairs{'utf8','ascii','raw'} do
   271    271   	ss.str.enc[v].parse_escape = ss.str.enc_generics.pfxescape('\\',ss.str.enc[v])
   272    272   end
          273  +
          274  +function ss.bitmask_expand(ty, v)
          275  +	local bitrange = ty[true]
          276  +	local fb
          277  +	if bitrange[1] ~= 0 then
          278  +		fb = v & ((1<<bitrange[1]) - 1) -- first N bits
          279  +	end
          280  +	local tbl = {}
          281  +	for j=bitrange[1], bitrange[2] do
          282  +		if (fb & (1<<j)) ~= 0 then
          283  +			tbl[ty[1<<j]] = true
          284  +		end
          285  +	end
          286  +	return tbl, fb
          287  +end
   273    288   
   274    289   function ss.str.classify(enc, ch)
   275    290   	if not enc.ranges then return {} end
   276    291   	if type(ch)=='string' then ch = enc.codepoint(ch) end
   277         -	-- TODO
          292  +
          293  +	for _, r in pairs(enc.ranges) do
          294  +		if ch >= r[1] and ch <= r[2] then
          295  +			local p,b = ss.bitmask_expand(ss.str.charprop, r[3])
          296  +			if b then p[ss.str.charclass[b]] = true end
          297  +			return p
          298  +		end
          299  +	end
          300  +
          301  +	return {}
   278    302   end
   279    303   
   280    304   
   281    305   function ss.str.each(enc, str, ascode)
   282    306   	if enc.each then return enc.each(enc,str,ascode) end
   283    307   	local pm = {
   284    308   		__index = {

Modified tools/ucs.lua from [3976f4bc78] to [cf6aee3c65].

    21     21   local file = io.stdin
    22     22   local path
    23     23   if arg[1] then
    24     24   	path = arg[1]
    25     25   	file = io.open(path, 'rb')
    26     26   end
    27     27   
    28         -local bitmask_raw = function(n,ofs)
    29         -	ofs = ofs or 0
    30         -	local function rec(i)
    31         -		if i > n then return end
    32         -		return 1<<(i+ofs), rec(i+1)
    33         -	end
    34         -	return 1<<ofs, rec(1)
    35         -end
    36         -
    37         -local bitmask = function(tbl,ofs)
    38         -	local codes = {bitmask_raw(#tbl,ofs)}
    39         -	local m = {}
    40         -	local maxbit
    41         -	for i, s in ipairs(tbl) do
    42         -		m[s] = codes[i]
    43         -		m[codes[i]] = s
    44         -		maxbit = i
    45         -	end
    46         -	m[true] = {ofs or 0,maxbit}
    47         -	return m
    48         -end
    49         -
    50         -local basictype = enum {
    51         -	'numeral';
    52         -	'alpha';
    53         -	'symbol';
    54         -	'punct';
    55         -	'space';
    56         -	'ctl';
    57         -	'glyph'; -- hanji
    58         -}
    59         -local props = bitmask({
    60         -	'hex',
    61         -	'upper', 'lower', 'diac',
    62         -	'wordbreak', 'wordsep',
    63         -	'disallow',
    64         -	'brack', 'right', 'left',
    65         -	'noprint', 'superimpose'
    66         -}, 3)
    67         -
           28  +local ss = require'sirsem'
           29  +local basictype = ss.str.charclass
           30  +local props = ss.str.charprop
    68     31   local overrides = {
    69     32   	[0x200B] = basictype.space | props.wordsep; -- database entry is wrong
    70     33   }
    71     34   
    72     35   local mask = ~0 -- mask out irrelevant properties to compactify database
    73     36   
    74     37   local function parsecat(tbl)
................................................................................
    78     41   	elseif tbl.class == 'Nd' then c = b.numeral
    79     42   	elseif tbl.class == 'No' then c = b.numeral | p.diac
    80     43   	elseif tbl.class == 'Cc' then
    81     44   		if tbl.kind == 'S'
    82     45   		or tbl.kind == 'WS'
    83     46   		or tbl.kind == 'B' then c  = b.space | p.wordsep
    84     47         else c = b.ctl | p.wordbreak | p.disallow end
    85         -	elseif tbl.class == 'Lu' then c = b.alpha | p.upper
    86         -	elseif tbl.class == 'Ll' then c = b.alpha | p.lower
           48  +	elseif tbl.class == 'Lu' then c = b.letter | p.upper
           49  +	elseif tbl.class == 'Ll' then c = b.letter | p.lower
    87     50   	elseif tbl.class == 'Lo'
    88         -	    or tbl.class == 'Lt' then c = b.alpha
           51  +	    or tbl.class == 'Lt' then c = b.letter
    89     52   	elseif tbl.class == 'Po' then c = b.punct | p.wordbreak
    90     53   	elseif tbl.class == 'Sm' then c = b.symbol | p.wordsep
    91     54   	elseif tbl.class == 'Ps' then c = b.punct | p.brack | p.left
    92     55   	elseif tbl.class == 'Pe' then c = b.punct | p.brack | p.right
    93     56   	elseif tbl.class == 'Pc'
    94     57   	    or tbl.class == 'Pd'
    95     58   	    or tbl.class == 'Sk'
................................................................................
   104     67   
   105     68   local ranuirAlpha = {0xe39d, 0xe39f, 0xe3ad, 0xe3af, 0xe3b5, 0xe3b7, 0xe3b9, 0xe3bb, 0xe3bd, 0xe3be, 0xe3bf, 0xe3c5, 0xe3c7, 0xe3c9, 0xe3cb, 0xe3cc, 0xe3cd, 0xe3ce, 0xe3cf}
   106     69   local ranuirSpecial = {
   107     70   	[0xe390] = basictype.space | props.wordsep;
   108     71   }
   109     72   
   110     73   local ranuir = {}
   111         -for _,v in pairs(ranuirAlpha) do ranuir[v] = basictype.alpha end
           74  +for _,v in pairs(ranuirAlpha) do ranuir[v] = basictype.letter end
   112     75   for k,v in pairs(ranuirSpecial) do ranuir[k] = v end
   113     76   local ranuirKeys = {}
   114     77   for k in pairs(ranuir) do table.insert(ranuirKeys, k) end
   115     78   table.sort(ranuirKeys)
   116     79   
   117     80   local recs = {}
   118     81   local ranuirok = false
   119     82   for ln in file:lines() do
   120     83   	local v = {}
   121     84   	for s in ln:gmatch('[^;]*') do
   122     85   		table.insert(v, s)
   123     86   	end
   124     87   	v[1] = tonumber(v[1],0x10)
   125         -	if v[1] > 0x7f then -- discard ASCII, we already have that
           88  +-- 	if v[1] > 0x7f then -- discard ASCII, we already have that
   126     89   		local code = {
   127     90   			codepoint = v[1];
   128     91   			name = v[2];
   129     92   			class = v[3];
   130     93   			kind = v[5];
   131     94   		}
   132     95   		code.cat = parsecat(code)
................................................................................
   140    103   			end
   141    104   			ranuirok = true
   142    105   		end
   143    106   
   144    107   		if code.cat ~= 0 then
   145    108   			table.insert(recs,code)
   146    109   		end
   147         -	end
          110  +-- 	end
   148    111   end
   149    112   
   150    113   
   151    114   local ranges = {}
   152    115   local last = recs[1]
   153    116   local start = last
   154    117   local altern = false
................................................................................
   179    142   		flush()
   180    143   		start = r
   181    144   	end
   182    145   	last = r
   183    146   end
   184    147   flush()
   185    148   
   186         --- expand bitmask
   187         -	-- for k,v in pairs(ranges) do
   188         -	-- 	local basic = v[3] & ((1<<3) - 1) -- first three bits
   189         -	-- 	if basic ~= 0 then
   190         -	-- 		v[4] = basictype[basic]
   191         -	-- 	end
   192         -	-- 	local bitrange = props[true]
   193         -	-- 	for j=bitrange[1], bitrange[2] do
   194         -	-- 		if (v[3] & (1<<j)) ~= 0 then
   195         -	-- 			table.insert(v, props[1<<j])
   196         -	-- 		end
   197         -	-- 	end
   198         -	-- end
   199         -
   200    149   -- the data has been collected and formatted in the manner we
   201    150   -- need; now we just need to emit it as a lua table
   202    151   
   203    152   local tab = {}
   204    153   local top = 1
   205    154   for k,v in pairs(ranges) do
   206    155   	tab[top] = string.format('{0x%x,0x%x,%u}',table.unpack(v))
   207    156   	top = top + 1
   208    157   end
   209         -io.stdout:write(string.format(tpl, table.concat(tab,',')))
          158  +io.stdout:write(string.format(tpl, table.concat(tab,',\n')))