cortav  Diff

Differences From Artifact [eb3cc08f95]:

To Artifact [028f351fed]:


     2      2   --  ~ lexi hale <lexi@hale.su>
     3      3   --  © AGPLv3
     4      4   --  ? reference implementation of the cortav document language
     5      5   
     6      6   local ss = require 'sirsem'
     7      7   -- aliases for commonly used sirsem funcs
     8      8   local startswith = ss.str.begins
     9         -local eachcode = ss.str.enc.utf8.each
    10      9   local dump = ss.dump
    11     10   local declare = ss.declare
    12     11   
    13     12   -- make this module available to require() when linked into a lua bytecode program with luac
    14     13   local ct = ss.namespace 'cortav'
    15     14   ct.info = {
    16     15   	version = ss.version {0,1; 'devel'};
................................................................................
    81     80   	end);
    82     81   	cli = ss.exnkind 'command line parse error';
    83     82   	mode = ss.exnkind('bad mode', function(msg, ...)
    84     83   		return string.format("mode “%s” "..msg, ...)
    85     84   	end);
    86     85   	unimpl = ss.exnkind 'feature not implemented';
    87     86   	ext = ss.exnkind 'extension error';
           87  +	enc = ss.exnkind('encoding error', function(msg, ...)
           88  +		return string.format('[%s]' .. msg, ...)
           89  +	end);
    88     90   }
    89     91   
    90     92   ct.ctx = declare {
    91     93   	mk = function(src) return {src = src} end;
    92     94   	ident = 'context';
    93     95   	cast = {
    94     96   		string = function(me)
................................................................................
   112    114   			table.insert(self.sec.blocks,block)
   113    115   			return block
   114    116   		end;
   115    117   		ref = function(self,id)
   116    118   			if not id:find'%.' then
   117    119   				local rid = self.sec.refs[id]
   118    120   				if self.sec.refs[id] then
   119         -					return self.sec.refs[id]
          121  +					return self.sec.refs[id], id, self.sec
   120    122   				else self:fail("no such ref %s in current section", id or '') end
   121    123   			else
   122    124   				local sec, ref = string.match(id, "(.-)%.(.+)")
   123    125   				local s = self.doc.sections[sec]
   124    126   				if s then
   125    127   					if s.refs[ref] then
   126         -						return s.refs[ref]
          128  +						return s.refs[ref], ref, sec
   127    129   					else self:fail("no such ref %s in section %s", ref, sec) end
   128    130   				else self:fail("no such section %s", sec) end
   129    131   			end
   130    132   		end
   131    133   	};
   132    134   }
   133    135   
................................................................................
   217    219   		meta = {};
   218    220   		vars = {};
   219    221   		ext = {
   220    222   			inhibit = {};
   221    223   			need = {};
   222    224   			use = {};
   223    225   		};
          226  +		enc = ss.str.enc.utf8;
   224    227   	} end;
   225    228   	construct = function(me)
   226    229   		me.docjob = ct.ext.job('doc', me, nil)
   227    230   	end;
   228    231   }
   229    232   
   230    233   -- FP helper functions
................................................................................
   397    400   
   398    401   -- renderer engines
   399    402   function ct.render.html(doc, opts)
   400    403   	local doctitle = opts['title']
   401    404   	local f = string.format
   402    405   	local ids = {}
   403    406   	local canonicalID = {}
   404         -	local function getSafeID(obj)
          407  +	local function getSafeID(obj,pfx)
          408  +		pfx = pfx or ''
   405    409   		if canonicalID[obj] then
   406    410   			return canonicalID[obj]
   407         -		elseif obj.id and ids[obj.id] then
          411  +		elseif obj.id and ids[pfx .. obj.id] then
          412  +			local objid = pfx .. obj.id
   408    413   			local newid
   409    414   			local i = 1
   410         -			repeat newid = obj.id .. string.format('-%x', i)
          415  +			repeat newid = objid .. string.format('-%x', i)
   411    416   				i = i + 1 until not ids[newid]
   412    417   			ids[newid] = obj
   413    418   			canonicalID[obj] = newid
   414    419   			return newid
   415    420   		else
   416    421   			local cid = obj.id
   417    422   			if not cid then
   418    423   				local i = 1
   419         -				repeat cid = string.format('x-%x', i)
          424  +				repeat cid = string.format('%sx-%x', pfx, i)
   420    425   					i = i + 1 until not ids[cid]
   421    426   			end
   422    427   			ids[cid] = obj
   423    428   			canonicalID[obj] = cid
   424    429   			return cid
   425    430   		end
   426    431   	end
   427    432   
          433  +	local footnotes = {}
          434  +	local footnotecount = 0
          435  +
   428    436   	local langsused = {}
   429    437   	local langpairs = {
   430    438   		lua = { color = 0x9377ff };
   431    439   		terra = { color = 0xff77c8 };
   432    440   		c = { name = 'C', color = 0x77ffe8 };
   433    441   		html = { color = 0xfff877 };
   434    442   		scheme = { color = 0x77ff88 };
   435    443   		lisp = { color = 0x77ff88 };
   436    444   		fortran = { color = 0xff779a };
   437    445   		python = { color = 0xffd277 };
   438         -		python = { color = 0xcdd6ff };
          446  +		ruby = { color = 0xcdd6ff };
   439    447   	}
   440    448   
   441    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  +		]];
   442    523   		header = [[
          524  +			body { padding: 0 2.5em !important }
   443    525   			h1,h2,h3,h4,h5,h6 { border-bottom: 1px solid @tone(0.7); }
   444    526   			h1 { font-size: 200%; border-bottom-style: double !important; border-bottom-width: 3px !important; margin: 0em -1em; }
   445    527   			h2 { font-size: 130%; margin: 0em -0.7em; }
   446    528   			h3 { font-size: 110%; margin: 0em -0.5em; }
   447    529   			h4 { font-size: 100%; font-weight: normal; margin: 0em -0.2em; }
   448    530   			h5 { font-size: 90%; font-weight: normal; }
   449    531   			h6 { font-size: 80%; font-weight: normal; }
................................................................................
   490    572   			section:target > :is(h1,h2,h3,h4,h5,h6) {
   491    573   
   492    574   			}
   493    575   		]];
   494    576   		paragraph = [[
   495    577   			p {
   496    578   				margin: 0.7em 0;
          579  +				text-align: justify;
   497    580   			}
   498    581   			section {
   499    582   				margin: 1.2em 0;
   500    583   			}
   501    584   			section:first-child { margin-top: 0; }
   502    585   		]];
   503    586   		accent = [[
   504         -			body { background: @bg; color: @fg }
   505         -			a[href] {
   506         -				color: @tone(0.7 30);
   507         -				text-decoration-color: @tone/0.4(0.7 30);
          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); }
   508    602   			}
   509         -			a[href]:hover {
   510         -				color: @tone(0.9 30);
   511         -				text-decoration-color: @tone/0.7(0.7 30);
          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  +				}
   512    612   			}
   513         -			h1 { color: @tone(2); }
   514         -			h2 { color: @tone(1.5); }
   515         -			h3 { color: @tone(1.2); }
   516         -			h4 { color: @tone(1); }
   517         -			h5,h6 { color: @tone(0.8); }
   518    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  +      ]];
   519    632   		code = [[
   520    633   			code {
   521         -				background: @fg;
          634  +				display: inline-block;
          635  +				background: @tone(0.9);
   522    636   				color: @bg;
   523    637   				font-family: monospace;
   524    638   				font-size: 90%;
   525    639   				padding: 3px 5px;
   526    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  +			}
   527    660   		]];
   528    661   		abbr = [[
   529    662   			abbr[title] { cursor: help; }
   530    663   		]];
   531    664   		editors_markup = [[]];
   532    665   		block_code_listing = [[
   533         -			section > figure.listing {
          666  +			figure.listing {
   534    667   				font-family: monospace;
   535    668   				background: @tone(0.05);
   536    669   				color: @fg;
   537    670   				padding: 0;
   538    671   				margin: 0.3em 0;
   539    672   				counter-reset: line-number;
   540    673   				position: relative;
   541    674   				border: 1px solid @fg;
   542    675   			}
   543         -			section > figure.listing>div {
          676  +			figure.listing>div {
   544    677   				white-space: pre-wrap;
          678  +				tab-size: 3;
          679  +				-moz-tab-size: 3;
   545    680   				counter-increment: line-number;
   546    681   				text-indent: -2.3em;
   547    682   				margin-left: 2.3em;
   548    683   			}
   549         -			section > figure.listing>:is(div,hr)::before {
          684  +			figure.listing>:is(div,hr)::before {
   550    685   				width: 1.0em;
   551    686   				padding: 0.2em 0.4em;
   552    687   				text-align: right;
   553    688   				display: inline-block;
   554    689   				background-color: @tone(0.2);
   555    690   				border-right: 1px solid @fg;
   556    691   				content: counter(line-number);
   557    692   				margin-right: 0.3em;
   558    693   			}
   559         -			section > figure.listing>hr::before {
          694  +			figure.listing>hr::before {
   560    695   				color: transparent;
   561    696   				padding-top: 0;
   562    697   				padding-bottom: 0;
   563    698   			}
   564         -			section > figure.listing>div::before {
          699  +			figure.listing>div::before {
   565    700   				color: @fg;
   566    701   			}
   567         -			section > figure.listing>div:last-child::before {
          702  +			figure.listing>div:last-child::before {
   568    703   				padding-bottom: 0.5em;
   569    704   			}
   570         -			section > figure.listing>figcaption:first-child {
          705  +			figure.listing>figcaption:first-child {
   571    706   				border: none;
   572    707   				border-bottom: 1px solid @fg;
   573    708   			}
   574         -			section > figure.listing>figcaption::after {
          709  +			figure.listing>figcaption::after {
   575    710   				display: block;
   576    711   				float: right;
   577    712   				font-weight: normal;
   578    713   				font-style: italic;
   579    714   				font-size: 70%;
   580    715   				padding-top: 0.3em;
   581    716   			}
   582         -			section > figure.listing>figcaption {
          717  +			figure.listing>figcaption {
   583    718   				font-family: sans-serif;
   584    719   				font-size: 120%;
   585    720   				padding: 0.2em 0.4em;
   586    721   				border: none;
   587    722   				color: @tone(2);
   588    723   			}
   589         -			section > figure.listing > hr {
          724  +			figure.listing > hr {
   590    725   				border: none;
   591    726   				margin: 0;
   592    727   				height: 0.7em;
   593    728   				counter-increment: line-number;
   594    729   			}
   595    730   		]];
   596    731   	}
................................................................................
   604    739   		stylesets = stylesets;
   605    740   		stylesets_active = stylesNeeded;
   606    741   		obj_htmlid = getSafeID;
   607    742   		-- remaining fields added later
   608    743   	}
   609    744   
   610    745   	local renderJob = doc:job('render_html', nil, render_state_handle)
          746  +	doc.stage.job = renderJob;
   611    747   
   612    748   	local runhook = function(h, ...)
   613    749   		return renderJob:hook(h, render_state_handle, ...)
   614    750   	end
   615    751   
   616         -	local function getSpanRenderers(procs)
          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)
   617    793   		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
   618    794   		local htmlDoc = function(title, head, body)
   619    795   			return [[<!doctype html>]] .. tag('html',nil,
   620    796   				tag('head', nil,
   621    797   					elt('meta',{charset = 'utf-8'}) ..
   622    798   					(title and tag('title', nil, title) or '') ..
   623    799   					(head or '')) ..
   624    800   				tag('body', nil, body or ''))
   625    801   		end
   626    802   
   627         -		local span_renderers = {}
   628    803   		local function htmlSpan(spans, block, sec)
   629    804   			local text = {}
   630    805   			for k,v in pairs(spans) do
   631    806   				if type(v) == 'string' then
   632         -					table.insert(text,(v:gsub('[<>&"]',
   633         -						function(x)
          807  +					v=v:gsub('[<>&"]', function(x)
   634    808   							return string.format('&#%02u;', string.byte(x))
   635         -						end)))
          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)
   636    814   				else
   637         -					table.insert(text, span_renderers[v.kind](v, block, sec))
          815  +					table.insert(text, (span_renderers[v.kind](v, block, sec)))
   638    816   				end
   639    817   			end
   640    818   			return table.concat(text)
   641    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
   642    835   
   643    836   		function span_renderers.format(sp,...)
   644         -			local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code' }
          837  +			local tags = { strong = 'strong', emph = 'em', strike = 'del', insert = 'ins', literal = 'code', variable = 'var'}
   645    838   			if sp.style == 'literal' and not opts['fossil-uv'] then
   646    839   				stylesNeeded.code = true
   647         -			end
   648         -			if sp.style == 'del' or sp.style == 'ins' then
          840  +			elseif sp.style == 'strike' or sp.style == 'insert' then
   649    841   				stylesNeeded.editors_markup = true
          842  +			elseif sp.style == 'variable' then
          843  +				stylesNeeded.var = true
   650    844   			end
   651    845   			return tag(tags[sp.style],nil,htmlSpan(sp.spans,...))
   652    846   		end
   653    847   
   654         -		function span_renderers.term(t,b,s)
          848  +		function span_renderers.deref(t,b,s)
   655    849   			local r = b.origin:ref(t.ref)
   656    850   			local name = t.ref
   657    851   			if name:find'%.' then name = name:match '^[^.]*%.(.+)$' end
   658         -			if type(r) ~= 'string' then
   659         -				b.origin:fail('%s is an object, not a reference', t.ref)
          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)
   660    855   			end
   661         -			stylesNeeded.abbr = true
   662         -			return tag('abbr',{title=r},next(t.spans) and htmlSpan(t.spans,b,s) or name)
   663         -		end
   664         -
   665         -		function span_renderers.macro(m,b,s)
   666         -			local r = b.origin:ref(m.macro)
   667         -			if type(r) ~= 'string' then
   668         -				b.origin:fail('%s is an object, not a reference', t.ref)
          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)
   669    877   			end
   670         -			local mctx = b.origin:clone()
   671         -			mctx.invocation = m
   672         -			return htmlSpan(ct.parse_span(r, mctx),b,s)
   673    878   		end
   674    879   
   675    880   		function span_renderers.var(v,b,s)
   676    881   			local val
   677    882   			if v.pos then
   678    883   				if not v.origin.invocation then
   679    884   					v.origin:fail 'positional arguments can only be used in a macro invocation'
................................................................................
   686    891   			end
   687    892   			if v.raw then
   688    893   				return val
   689    894   			else
   690    895   				return htmlSpan(ct.parse_span(val, v.origin), b, s)
   691    896   			end
   692    897   		end
          898  +
          899  +		function span_renderers.raw(v,b,s)
          900  +			return htmlSpan(v.spans, b, s)
          901  +		end
   693    902   
   694    903   		function span_renderers.link(sp,b,s)
   695    904   			local href
   696    905   			if b.origin.doc.sections[sp.ref] then
   697    906   				href = '#' .. sp.ref
   698    907   			else
   699    908   				if sp.addr then href = sp.addr else
................................................................................
   701    910   					if type(r) == 'table' then
   702    911   						href = '#' .. getSafeID(r)
   703    912   					else href = r end
   704    913   				end
   705    914   			end
   706    915   			return tag('a',{href=href},next(sp.spans) and htmlSpan(sp.spans,b,s) or href)
   707    916   		end
   708         -		return {
   709         -			span_renderers = span_renderers;
   710         -			htmlSpan = htmlSpan;
   711         -			htmlDoc = htmlDoc;
   712         -		}
          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
   713    964   	end
   714         -
   715    965   
   716    966   	local function getBlockRenderers(procs, sr)
   717    967   		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
   718    968   		local null = function() return catenate{} end
   719    969   
   720    970   		local block_renderers = {
   721    971   			anchor = function(b,s)
................................................................................
   766   1016   					if #l > 0 then
   767   1017   						return tag('div',nil,sr.htmlSpan(l, b, s))
   768   1018   					else
   769   1019   						return elt('hr')
   770   1020   					end
   771   1021   				end, b.lines)
   772   1022   				if b.title then
   773         -					table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title)))
         1023  +					table.insert(nodes,1, tag('figcaption',nil,sr.htmlSpan(b.title)))
   774   1024   				end
   775   1025   				if b.lang then langsused[b.lang] = true end
   776   1026   				return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
   777   1027   			end;
   778   1028   			aside = function(b,s)
   779   1029   				local bn = {}
   780         -				for _,v in pairs(b.lines) do
   781         -					table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s)))
         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
   782   1037   				end
   783   1038   				return tag('aside', {}, bn)
   784   1039   			end;
   785         -			['break'] = function() --[[nop]] end;
         1040  +			['break'] = function() -- HACK
         1041  +				-- lists need to be rewritten to work like asides
         1042  +				return '';
         1043  +			end;
   786   1044   		}
   787   1045   		return block_renderers;
   788   1046   	end
   789   1047   
   790   1048   	local function getRenderers(procs)
   791         -		local r = getSpanRenderers(procs)
         1049  +		local span_renderers = getSpanRenderers(procs)
         1050  +		local r = getBaseRenderers(procs,span_renderers)
   792   1051   		r.block_renderers = getBlockRenderers(procs, r)
   793   1052   		return r
   794         -	end
   795         -
   796         -	local tagproc do
   797         -		local elt = function(t,attrs)
   798         -			return f('<%s%s>', t,
   799         -				attrs and ss.reduce(function(a,b) return a..b end, '', 
   800         -					ss.map(function(v,k)
   801         -						if v == true
   802         -							then          return ' '..k
   803         -							elseif v then return f(' %s="%s"', k, v)
   804         -						end
   805         -					end, attrs)) or '')
   806         -		end
   807         -
   808         -		tagproc = {
   809         -			toTXT = {
   810         -				tag = function(t,a,v) return v  end;
   811         -				elt = function(t,a)   return '' end;
   812         -				catenate = table.concat;
   813         -			};
   814         -			toIR = {
   815         -				tag = function(t,a,v,o) return {
   816         -					tag = t, attrs = a;
   817         -					nodes = type(v) == 'string' and {v} or v, src = o
   818         -				} end;
   819         -				
   820         -				elt = function(t,a,o) return {
   821         -					tag = t, attrs = a, src = o
   822         -				} end;
   823         -
   824         -				catenate = function(...) return ... end;
   825         -			};
   826         -			toHTML = {
   827         -				elt = elt;
   828         -				tag = function(t,attrs,body)
   829         -					return f('%s%s</%s>', elt(t,attrs), body, t)
   830         -				end;
   831         -				catenate = table.concat;
   832         -			};
   833         -		}
   834   1053   	end
   835   1054   
   836   1055   	local astproc = {
   837   1056   		toHTML = getRenderers(tagproc.toHTML);
   838   1057   		toTXT  = getRenderers(tagproc.toTXT);
   839   1058   		toIR   = { };
   840   1059   	}
................................................................................
   853   1072   	local ir = {}
   854   1073   	local dr = astproc.toHTML -- default renderers
   855   1074   	local plainr = astproc.toTXT
   856   1075   	local irBlockRdrs = astproc.toIR.block_renderers;
   857   1076   
   858   1077   	render_state_handle.ir = ir;
   859   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
   860   1113   	runhook('ir_assemble', ir)
   861   1114   	for i, sec in ipairs(doc.secorder) do
   862   1115   		if doctitle == nil and sec.depth == 1 and sec.heading_node then
   863   1116   			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
   864   1117   		end
   865   1118   		local irs
   866   1119   		if sec.kind == 'ordinary' then
   867   1120   			if #(sec.blocks) > 0 then
   868   1121   				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
   869         -
   870   1122   				runhook('ir_section_build', irs, sec)
   871         -				
   872         -				for i, block in ipairs(sec.blocks) do
   873         -					local rd
   874         -					if irBlockRdrs[block.kind] then
   875         -						rd = irBlockRdrs[block.kind](block,sec)
   876         -					else
   877         -						local rdr = renderJob:proc('render',block.kind,'html')
   878         -						if rdr then
   879         -							rd = rdr({
   880         -								state = render_state_handle;
   881         -								tagproc = tagproc.toIR;
   882         -								astproc = astproc.toIR;
   883         -							}, block, sec)
   884         -						end
   885         -					end
   886         -					if rd then
   887         -						if opts['heading-anchors'] and block == sec.heading_node then
   888         -							stylesNeeded.headingAnchors = true
   889         -							table.insert(rd.nodes, ' ')
   890         -							table.insert(rd.nodes, {
   891         -								tag = 'a';
   892         -								attrs = {href = '#' .. irs.attrs.id, class='anchor'};
   893         -								nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '&sect;'};
   894         -							})
   895         -						end
   896         -						table.insert(irs.nodes, rd)
   897         -						runhook('ir_section_node_insert', rd, irs, sec)
   898         -					end
   899         -				end
         1123  +				renderBlocks(sec.blocks, irs)
   900   1124   			end
   901   1125   		elseif sec.kind == 'blockquote' then
   902   1126   		elseif sec.kind == 'listing' then
   903   1127   		elseif sec.kind == 'embed' then
   904   1128   		end
   905   1129   		if irs then table.insert(ir, irs) end
   906   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
   907   1147   
   908   1148   	-- restructure passes
   909   1149   	runhook('ir_restructure_pre', ir)
   910   1150   	
   911   1151   	---- list insertion pass
   912   1152   	local lists = {}
   913   1153   	for _, sec in pairs(ir) do
................................................................................
  1033   1273   			local tonespan = opts.accent and .1 or 0
  1034   1274   			local tbg = opts['dark-on-light'] and 1.0 - tonespan or tonespan
  1035   1275   			local tfg = opts['dark-on-light'] and tonespan or 1.0 - tonespan
  1036   1276   			if var == 'bg' then
  1037   1277   				return tone(tbg,nil,nil,tonumber(alpha))
  1038   1278   			elseif var == 'fg' then
  1039   1279   				return tone(tfg,nil,nil,tonumber(alpha))
         1280  +			elseif var == 'width' then
         1281  +				return opts['width'] or '100vw'
  1040   1282   			elseif var == 'tone' then
  1041   1283   				local l, sep, sat
  1042   1284   				for i=1,3 do -- 🙄
  1043   1285   					l,sep,sat = param:match('^%('..string.rep('([^%s]*)%s*',i)..'%)$')
  1044   1286   					if l then break end
  1045   1287   				end
  1046   1288   				l = ss.math.lerp(tonumber(l), tbg, tfg)
................................................................................
  1124   1366   				kind = 'var';
  1125   1367   				pos = pos;
  1126   1368   				raw = raw;
  1127   1369   				var = not pos and s or nil;
  1128   1370   				origin = c:clone();
  1129   1371   			}
  1130   1372   		end
         1373  +	end
         1374  +	local function insert_span_directive(crit, failthru)
         1375  +		return function(s,c)
         1376  +			local args = ss.str.breakwords(d.doc.enc, s, 1)
         1377  +			local brksyms = map(enc.encodeUCS, {
         1378  +				'.', ',', ':', ';', '!', '$', '&', '^',
         1379  +				'/', '?', '@', '='
         1380  +			})
         1381  +			local brkhash = {} for _,s in pairs(brksyms) do
         1382  +				brkhash[s] = true
         1383  +			end
         1384  +
         1385  +			local extname = ''
         1386  +			local sym
         1387  +			local cmd = ''
         1388  +			for ch,p in ss.str.each(c.doc.enc, args[1]) do
         1389  +				if sym == nil then
         1390  +					if brkhash[ch] then
         1391  +						sym = ch
         1392  +					else
         1393  +						extname = extname .. ch
         1394  +					end
         1395  +				elseif brkhash[ch] then
         1396  +					sym = sym + ch
         1397  +				else
         1398  +					cmd = cmd + ch
         1399  +				end
         1400  +			end
         1401  +			if cmd == '' then cmd = nil end
         1402  +			local spans if failthru then
         1403  +				spans = ct.parse_span(args[2], c)
         1404  +			end
         1405  +			return {
         1406  +				kind = 'directive';
         1407  +				ext = extname;
         1408  +				cmd = cmd;
         1409  +				args = args;
         1410  +				crit = crit;
         1411  +				failthru = failthru;
         1412  +				spans = spans;
         1413  +			}
         1414  +		end
  1131   1415   	end
  1132   1416   	ct.spanctls = {
  1133   1417   		{seq = '!', parse = formatter 'emph'};
  1134   1418   		{seq = '*', parse = formatter 'strong'};
  1135   1419   		{seq = '~', parse = formatter 'strike'};
  1136         -		{seq = '+', parse = formatter 'inser'};
         1420  +		{seq = '+', parse = formatter 'insert'};
  1137   1421   		{seq = '\\', parse = function(s, c) -- raw
  1138         -			return s
  1139         -		end};
  1140         -		{seq = '$\\', parse = function(s, c) -- raw
  1141   1422   			return {
  1142         -				kind = 'format';
  1143         -				style = 'literal';
         1423  +				kind = 'raw';
  1144   1424   				spans = {s};
  1145   1425   				origin = c:clone();
  1146   1426   			}
  1147   1427   		end};
  1148         -		{seq = '$', parse = formatter 'literal'};
         1428  +		{seq = '`\\', parse = function(s, c) -- raw
         1429  +			local o = c:clone();
         1430  +			local str = ''
         1431  +			for c, p in ss.str.each(c.doc.enc, s) do
         1432  +				local q = p:esc()
         1433  +				if q then
         1434  +					str = str ..  q
         1435  +					p.next.byte = p.next.byte + #q
         1436  +				else
         1437  +					str = str .. c
         1438  +				end
         1439  +			end
         1440  +			return {
         1441  +				kind = 'format';
         1442  +				style = 'literal';
         1443  +				spans = {{
         1444  +					kind = 'raw';
         1445  +					spans = {str};
         1446  +					origin = o;
         1447  +				}};
         1448  +				origin = o;
         1449  +			}
         1450  +		end};
         1451  +		{seq = '`', parse = formatter 'literal'};
         1452  +		{seq = '$', parse = formatter 'variable'};
         1453  +		{seq = '^', parse = function(s,c) --footnotes
         1454  +			local r, t = s:match '^([^%s]+)%s*(.-)$'
         1455  +			return {
         1456  +				kind = 'footnote';
         1457  +				ref = r;
         1458  +				spans = ct.parse_span(t, c);
         1459  +				origin = c:clone();
         1460  +			}
         1461  +		-- TODO support for footnote sections
         1462  +		end};
         1463  +		{seq = '=', parse = function(s,c) --math mode
         1464  +			local tx = {
         1465  +				['%*'] = '×';
         1466  +				['/'] = '÷';
         1467  +			}
         1468  +			for k,v in pairs(tx) do s = s:gsub(k,v) end
         1469  +			s=s:gsub('%^([0-9]+)', function(num)
         1470  +				local sup = {'⁰','¹','²','³','⁴','⁵','⁶','⁷','⁸','⁹'};
         1471  +				local r = ''
         1472  +				for i=1,#num do
         1473  +					r = r .. sup[1 + (num:byte(i) - 0x30)]
         1474  +				end
         1475  +				return r
         1476  +			end)
         1477  +			local m = {s} --TODO
         1478  +			return {
         1479  +				kind = 'math';
         1480  +				original = s;
         1481  +				spans = m;
         1482  +				origin = c:clone();
         1483  +			};
         1484  +		end};
  1149   1485   		{seq = '&', parse = function(s, c)
  1150   1486   			local r, t = s:match '^([^%s]+)%s*(.-)$'
  1151   1487   			return {
  1152         -				kind = 'term';
         1488  +				kind = 'deref';
  1153   1489   				spans = (t and t ~= "") and ct.parse_span(t, c) or {};
  1154   1490   				ref = r; 
  1155   1491   				origin = c:clone();
  1156   1492   			}
  1157   1493   		end};
  1158   1494   		{seq = '^', parse = function(s, c)
  1159   1495   			local fn, t = s:match '^([^%s]+)%s*(.-)$'
................................................................................
  1165   1501   			}
  1166   1502   		end};
  1167   1503   		{seq = '>', parse = insert_link};
  1168   1504   		{seq = '→', parse = insert_link};
  1169   1505   		{seq = '🔗', parse = insert_link};
  1170   1506   		{seq = '##', parse = insert_var_ref(true)};
  1171   1507   		{seq = '#', parse = insert_var_ref(false)};
         1508  +		{seq = '%%', parse = function() --[[NOP]] end};
         1509  +		{seq = '%!', parse = insert_span_directive(true,false)};
         1510  +		{seq = '%:', parse = insert_span_directive(false,true)};
         1511  +		{seq = '%', parse = insert_span_directive(false,false)};
  1172   1512   	}
  1173   1513   end
  1174   1514   
  1175   1515   function ct.parse_span(str,ctx)
  1176   1516   	local function delimited(start, stop, s)
  1177   1517   		local r = { pcall(ss.str.delimit, nil, start, stop, s) }
  1178   1518   		if r[1] then return table.unpack(r, 2) end
  1179   1519   		ctx:fail(tostring(r[2]))
  1180   1520   	end
  1181   1521   	local buf = ""
  1182   1522   	local spans = {}
  1183   1523   	local function flush()
  1184   1524   		if buf ~= "" then
         1525  +	-- 			for fn, ext in ctx.doc.docjob:each('hook','doc_meddle_string') do
         1526  +	-- 				buf = fn(ctx.doc.docjob:delegate(ext), ctx, buf)
         1527  +	-- 			end
  1185   1528   			table.insert(spans, buf)
  1186   1529   			buf = ""
  1187   1530   		end
  1188   1531   	end
  1189   1532   	local skip = false
  1190         -	for c,p in eachcode(str) do
  1191         -		if skip == true then
  1192         -			skip = false
  1193         -			buf = buf .. c
  1194         -		elseif c == '\\' then
  1195         -			skip = true
         1533  +	for c,p in ss.str.each(ctx.doc.enc,str) do
         1534  +		local ba, ca, es = ctx.doc.enc.parse_escape(str:sub(p.byte))
         1535  +		if es then
         1536  +			flush()
         1537  +			table.insert(spans, {
         1538  +				kind = 'raw';
         1539  +				spans = {es};
         1540  +				origin = ctx:clone()
         1541  +			})
         1542  +			p.next.byte = p.next.byte + ba;
         1543  +			p.next.code = p.next.code + ca;
  1196   1544   		elseif c == '{' then
  1197   1545   			flush()
  1198   1546   			local substr, following = delimited('{','}',str:sub(p.byte))
  1199   1547   			local splitstart, splitstop = substr:find'%s+'
  1200   1548   			local id, argstr
  1201   1549   			if splitstart then
  1202   1550   				id, argstr = substr:sub(1,splitstart-1), substr:sub(splitstop+1)
................................................................................
  1214   1562   				local i = 1
  1215   1563   				while i <= #argstr do
  1216   1564   					while i<=#argstr and (argstr:sub(i,i) ~= '|' or argstr:sub(i-1,i) == '\\|') do
  1217   1565   						i = i + 1
  1218   1566   					end
  1219   1567   					local arg = argstr:sub(start, i == #argstr and i or i-1)
  1220   1568   					start = i+1
         1569  +					arg=arg:gsub('\\|','|')
  1221   1570   					table.insert(o.args, arg)
  1222   1571   					i = i + 1
  1223   1572   				end
  1224   1573   			end
  1225   1574   
  1226   1575   			p.next.byte = p.next.byte + following - 1
  1227   1576   			table.insert(spans,o)
................................................................................
  1236   1585   					table.insert(spans, i.parse(substr:sub(1+#i.seq), ctx))
  1237   1586   					break
  1238   1587   				end
  1239   1588   			end
  1240   1589   			if not found then
  1241   1590   				ctx:fail('no recognized control sequence in [%s]', substr)
  1242   1591   			end
         1592  +		elseif c == '\n' then
         1593  +			flush()
         1594  +			table.insert(spans,{kind='line-break',origin=ctx:clone()})
  1243   1595   		else
  1244   1596   			buf = buf .. c
  1245   1597   		end
  1246   1598   	end
  1247   1599   	flush()
  1248   1600   	return spans
  1249   1601   end
  1250   1602   
  1251   1603   local function
  1252   1604   blockwrap(fn)
  1253         -	return function(l,c,j)
  1254         -		local block = fn(l,c,j)
         1605  +	return function(l,c,j,d)
         1606  +		local block = fn(l,c,j,d)
  1255   1607   		block.origin = c:clone();
  1256         -		table.insert(c.sec.blocks, block);
         1608  +		table.insert(d, block);
  1257   1609   		j:hook('block_insert', c, block, l)
         1610  +		if block.spans then
         1611  +			c.doc.docjob:hook('meddle_span', block.spans, block)
         1612  +		end
  1258   1613   	end
  1259   1614   end
  1260   1615   
  1261   1616   local insert_paragraph = blockwrap(function(l,c)
  1262   1617   	if l:sub(1,1) == '.' then l = l:sub(2) end
  1263   1618   	return {
  1264   1619   		kind = "paragraph";
................................................................................
  1282   1637   	if t and t ~= "" then
  1283   1638   		local heading = {
  1284   1639   			kind = "label";
  1285   1640   			spans = ct.parse_span(t,c);
  1286   1641   			origin = s.origin;
  1287   1642   			captions = s;
  1288   1643   		}
         1644  +		c.doc.docjob:hook('meddle_span', heading.spans, heading)
  1289   1645   		table.insert(s.blocks, heading)
  1290   1646   		s.heading_node = heading
  1291   1647   	end
  1292   1648   	c.sec = s
  1293   1649   
  1294   1650   	j:hook('section_attach', c, s)
  1295   1651   end
................................................................................
  1299   1655   	c.doc.meta[key] = val
  1300   1656   	j:hook('metadata_set', key, val)
  1301   1657   end
  1302   1658   local dextctl = function(w,c)
  1303   1659   	local mode, exts = w(1)
  1304   1660   	for e in exts:gmatch '([^%s]+)' do
  1305   1661   		if mode == 'uses' then
         1662  +			c.doc.ext.use[e] = true
  1306   1663   		elseif mode == 'needs' then
         1664  +			c.doc.ext.need[e] = true
  1307   1665   		elseif mode == 'inhibits' then
         1666  +			c.doc.ext.inhibit[e] = true
  1308   1667   		end
  1309   1668   	end
  1310   1669   end
  1311   1670   local dcond = function(w,c)
  1312   1671   	local mode, cond, exp = w(2)
  1313   1672   	c.hide_next = mode == 'unless'
  1314   1673   end;
................................................................................
  1315   1674   ct.directives = {
  1316   1675   	author = dsetmeta;
  1317   1676   	license = dsetmeta;
  1318   1677   	keywords = dsetmeta;
  1319   1678   	desc = dsetmeta;
  1320   1679   	when = dcond;
  1321   1680   	unless = dcond;
         1681  +	pragma = function(w,c)
         1682  +	end;
         1683  +	lang = function(w,c)
         1684  +		local _, op, l = w(2)
         1685  +		local langstack = c.doc.stage.langstack
         1686  +		if op == 'is' then
         1687  +			langstack[math.max(1, #langstack)] = l
         1688  +		elseif op == 'push' then
         1689  +			table.insert(langstack, l)
         1690  +		elseif op == 'pop' then
         1691  +			if next(langstack) then
         1692  +				langstack[#langstack] = nil
         1693  +			end
         1694  +		elseif op == 'sec' then
         1695  +			c.sec.lang = l
         1696  +		else c:fail('bad language directive “%s”', op) end
         1697  +		c.lang = langstack[#langstack]
         1698  +	end;
  1322   1699   	expand = function(w,c)
  1323   1700   		local _, m = w(1)
  1324   1701   		if m ~= 'off' then
  1325         -			c.expand_next = 1
         1702  +			c.doc.stage.expand_next = 1
  1326   1703   		else
  1327         -			c.expand_next = 0
         1704  +			c.doc.stage.expand_next = 0
  1328   1705   		end
  1329   1706   	end;
  1330   1707   }
  1331   1708   
  1332   1709   local function insert_table_row(l,c,j)
  1333   1710   	local row = {}
  1334   1711   	local buf
................................................................................
  1335   1712   	local flush = function()
  1336   1713   		if buf then
  1337   1714   			buf.str = buf.str:gsub('%s+$','')
  1338   1715   			table.insert(row, buf)
  1339   1716   		end
  1340   1717   		buf = { str = '' }
  1341   1718   	end
  1342         -	for c,p in eachcode(l) do
         1719  +	for c,p in ss.str.each(c.doc.enc,l) do
  1343   1720   		if c == '|' or c == '+' and (p.code == 1 or l:sub(p.byte-1,p.byte-1)~='\\') then
  1344   1721   			flush()
  1345   1722   			buf.header = c == '+'
  1346   1723   		elseif c == ':' then
  1347   1724   			local lst = l:sub(p.byte-#c,p.byte-#c)
  1348   1725   			local nxt = l:sub(p.next.byte,p.next.byte)
  1349   1726   			if lst == '|' or lst == '+' and l:sub(p.byte-2,p.byte-2) ~= '\\' then
................................................................................
  1371   1748   		else
  1372   1749   			buf.str = buf.str .. c
  1373   1750   		end
  1374   1751   	end
  1375   1752   	if buf.str ~= '' then flush() end 
  1376   1753   	for _,v in pairs(row) do
  1377   1754   		v.spans = ct.parse_span(v.str, c)
         1755  +		c.doc.docjob:hook('meddle_span', v.spans, v)
  1378   1756   	end
  1379   1757   	if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then
  1380   1758   		local tbl = c.sec.blocks[#c.sec.blocks]
  1381   1759   		table.insert(tbl.rows, row)
  1382   1760   		j:hook('block_table_attach', c, tbl, row, l)
  1383   1761   		j:hook('block_table_row_insert', c, tbl, row, l)
  1384   1762   	else
................................................................................
  1398   1776   	{seq = '¶', fn = insert_paragraph};
  1399   1777   	{seq = '❡', fn = insert_paragraph};
  1400   1778   	{seq = '#', fn = insert_section};
  1401   1779   	{seq = '§', fn = insert_section};
  1402   1780   	{seq = '+', fn = insert_table_row};
  1403   1781   	{seq = '|', fn = insert_table_row};
  1404   1782   	{seq = '│', fn = insert_table_row};
  1405         -	{seq = '!', fn = function(l,c,j) 
  1406         -		local last = c.sec.blocks[#c.sec.blocks]
         1783  +	{seq = '!', fn = function(l,c,j,d)
         1784  +		local last = d[#d]
  1407   1785   		local txt = l:match '^%s*!%s*(.-)$'
  1408   1786   		if (not last) or last.kind ~= 'aside' then
  1409   1787   			local aside = {
  1410   1788   				kind = 'aside';
  1411         -				lines = { ct.parse_span(txt, c) }
         1789  +				lines = { ct.parse_span(txt, c) };
         1790  +				origin = c:clone();
  1412   1791   			}
  1413         -			c:insert(aside)
         1792  +			c.doc.docjob:hook('meddle_span', aside.lines[1], aside)
         1793  +			table.insert(d,aside)
  1414   1794   			j:hook('block_aside_insert', c, aside, l)
  1415   1795   			j:hook('block_aside_line_insert', c, aside, aside.lines[1], l)
  1416   1796   			j:hook('block_insert', c, aside, l)
  1417   1797   		else
  1418   1798   			local sp = ct.parse_span(txt, c)
         1799  +			c.doc.docjob:hook('meddle_span', sp, last)
  1419   1800   			table.insert(last.lines, sp)
  1420   1801   			j:hook('block_aside_attach', c, last, sp, l)
  1421   1802   			j:hook('block_aside_line_insert', c, last, sp, l)
  1422   1803   		end
  1423   1804   	end};
  1424   1805   	{pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
  1425   1806   		local stars = l:match '^([*:]+)'
................................................................................
  1430   1811   		return {
  1431   1812   			kind = 'list-item';
  1432   1813   			depth = depth;
  1433   1814   			ordered = ordered;
  1434   1815   			spans = ct.parse_span(txt, c);
  1435   1816   		}
  1436   1817   	end)};
  1437         -	{seq = '\t', fn = function(l,c,j)
  1438         -		local ref, val = l:match '\t+([^:]+):%s*(.*)$'
  1439         -		c.sec.refs[ref] = val
  1440         -		j:hook('section_ref_attach', c, ref, val, l)
         1818  +	{seq = '\t\t', fn = function(l,c,j,d)
         1819  +		local last = d[#d]
         1820  +		if (not last) or (last.kind ~= 'reference') then
         1821  +			c:fail('reference continuations must immediately follow a reference')
         1822  +		end
         1823  +		local str = l:match '^\t\t(.-)%s*$'
         1824  +		last.val = last.val .. '\n' .. str
         1825  +		c.sec.refs[last.key] = last.val
  1441   1826   	end};
  1442         -	{seq = '%', fn = function(l,c,j) -- directive
         1827  +	{seq = '\t', fn = blockwrap(function(l,c,j,d)
         1828  +		local ref, val = l:match '\t+([^:]+):%s*(.*)$'
         1829  +		local last = d[#d]
         1830  +		local rsrc
         1831  +		if last and last.kind == 'resource' then
         1832  +			last.props[ref] = val
         1833  +			rsrc = last
         1834  +		elseif last and last.kind == 'reference' and last.rsrc then
         1835  +			last.rsrc.props[ref] = val
         1836  +			rsrc = last.rsrc
         1837  +		else
         1838  +			c.sec.refs[ref] = val
         1839  +		end
         1840  +		j:hook('section_ref_attach', c, ref, val, l)
         1841  +		return {
         1842  +			kind = 'reference';
         1843  +			rsrc = rsrc;
         1844  +			key = ref;
         1845  +			val = val;
         1846  +		}
         1847  +	end)};
         1848  +	{seq = '%', fn = function(l,c,j,d) -- directive
  1443   1849   		local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$'
  1444   1850   		local words = function(i)
  1445   1851   			local wds = {}
  1446   1852   			if i == 0 then return cmdline end
  1447   1853   			for w,pos in cmdline:gmatch '([^%s]+)()' do
  1448   1854   				table.insert(wds, w)
  1449   1855   				i = i - 1
  1450   1856   				if i == 0 then
  1451         -					table.insert(wds,cmdline:sub(pos))
         1857  +					table.insert(wds,(cmdline:sub(pos):match('^%s*(.-)%s*$')))
  1452   1858   					return table.unpack(wds)
  1453   1859   				end
  1454   1860   			end
  1455   1861   		end
  1456   1862   
  1457   1863   		local cmd, rest = words(1)
  1458   1864   		if ct.directives[cmd] then
................................................................................
  1459   1865   			ct.directives[cmd](words,c,j)
  1460   1866   		elseif cmd == c.doc.stage.mode['render:format'] then
  1461   1867   			-- this is a directive for the renderer; insert it into the tree as is
  1462   1868   			local dir = {
  1463   1869   				kind = 'directive';
  1464   1870   				critical = crit == '!';
  1465   1871   				words = words;
         1872  +				origin = c;
  1466   1873   			}
  1467         -			c:insert(dir)
         1874  +			table.insert(d, dir)
  1468   1875   			j:hook('block_directive_render', j, c, dir)
  1469   1876   		elseif c.doc:allow_ext(cmd) then -- extension directives begin with their id
  1470   1877   			local ext = ct.ext.loaded[cmd]
  1471   1878   			if ext.directives then
  1472   1879   				local _, topcmd = words(2)
  1473   1880   				if ext.directives[topcmd] then
  1474   1881   					ext.directives[topcmd](j:delegate(ext), c, words)
................................................................................
  1505   1912   			kind = 'code';
  1506   1913   			listing = {
  1507   1914   				kind = 'listing';
  1508   1915   				lang = lang, id = id, title = title and ct.parse_span(title,c);
  1509   1916   				lines = {};
  1510   1917   			}
  1511   1918   		}
         1919  +		if c.doc.stage.expand_next and c.doc.stage.expand_next > 0 then
         1920  +			c.doc.stage.expand_next = c.doc.stage.expand_next - 1
         1921  +			mode.expand = true
         1922  +		end
  1512   1923   		j:hook('mode_switch', c, mode)
  1513   1924   		c.mode = mode
  1514   1925   		if id then
  1515   1926   			if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
  1516   1927   			c.sec.refs[id] = c.mode.listing
  1517   1928   		end
  1518   1929   		j:hook('block_insert', c, mode.listing, l)
  1519   1930   		return c.mode.listing;
  1520   1931   	end)};
  1521   1932   	{pred = function(s,c)
  1522   1933   		if s:match '^[%-_][*_%-%s]+' then return true end
  1523   1934   		if startswith(s, '—') then
  1524         -			for c, p in eachcode(s) do
         1935  +			for c, p in ss.str.each(c.doc.enc,s) do
  1525   1936   				if ({
  1526   1937   					['—'] = true, ['-'] = true, [' '] = true;
  1527   1938   					['*'] = true, ['_'] = true, ['\t'] = true;
  1528   1939   				})[c] ~= true then return false end
  1529   1940   			end
  1530   1941   			return true
  1531   1942   		end
  1532   1943   	end; fn = blockwrap(function()
  1533   1944   		return { kind = 'horiz-rule' }
         1945  +	end)};
         1946  +	{seq='@', fn=blockwrap(function(s,c)
         1947  +		local id = s:match '^@%s*(.-)%s*$'
         1948  +		local rsrc = {
         1949  +			kind = 'resource';
         1950  +			props = {};
         1951  +			id = id;
         1952  +		}
         1953  +		if c.sec.refs[id] then
         1954  +			c:fail('an object with id “%s” already exists in that section',id)
         1955  +		else
         1956  +			c.sec.refs[id] = rsrc
         1957  +		end
         1958  +		return rsrc
  1534   1959   	end)};
  1535   1960   	{fn = insert_paragraph};
  1536   1961   }
  1537   1962   
  1538         -function ct.parse(file, src, mode)
  1539         -	local function
  1540         -	is_whitespace(cp)
  1541         -		return cp == 0x20 or cp == 0xe390
         1963  +function ct.parse_line(l, ctx, dest)
         1964  +	local newspan
         1965  +	local job = ctx.doc.stage.job
         1966  +	job:hook('line_read',ctx,l)
         1967  +	if ctx.mode then
         1968  +		if ctx.mode.kind == 'code' then
         1969  +			if l and l:match '^~~~%s*$' then
         1970  +				job:hook('block_listing_end',ctx,ctx.mode.listing)
         1971  +				job:hook('mode_switch', c, nil)
         1972  +				ctx.mode = nil
         1973  +			else
         1974  +				-- TODO handle formatted code
         1975  +				local newline
         1976  +				if ctx.mode.expand
         1977  +					then newline = ct.parse_span(l, ctx)
         1978  +					else newline = {l}
         1979  +				end
         1980  +				table.insert(ctx.mode.listing.lines, newline)
         1981  +				job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
         1982  +			end
         1983  +	  else
         1984  +			local mf = job:proc('modes', ctx.mode.kind)
         1985  +			if not mf then
         1986  +				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
         1987  +			end
         1988  +			mf(job, ctx, l, dest) --NOTE: you are responsible for triggering the appropriate hooks if you insert anything!
         1989  +		end
         1990  +	else
         1991  +		if l then
         1992  +			local function tryseqs(seqs, ...)
         1993  +				for _, i in pairs(seqs) do
         1994  +					if ((not i.seq ) or startswith(l, i.seq)) and
         1995  +					   ((not i.pred) or i.pred    (l, ctx  )) then
         1996  +						i.fn(l, ctx, job, dest, ...)
         1997  +						return true
         1998  +					end
         1999  +				end
         2000  +				return false
         2001  +			end
         2002  +
         2003  +			if not tryseqs(ct.ctlseqs) then
         2004  +				local found = false
         2005  +
         2006  +				for eb, ext, state in job:each('blocks') do
         2007  +					if tryseqs(eb, state) then found = true break end
         2008  +				end
         2009  +
         2010  +				if not found then
         2011  +					ctx:fail 'incomprehensible input line'
         2012  +				end
         2013  +			end
         2014  +		else
         2015  +			if next(dest) and dest[#dest].kind ~= 'break' then
         2016  +				local brk = {kind='break', origin = ctx:clone()}
         2017  +				job:hook('block_break', ctx, brk, l)
         2018  +				table.insert(dest, brk)
         2019  +			end
         2020  +		end
  1542   2021   	end
         2022  +	job:hook('line_end',ctx,l)
         2023  +end
         2024  +
         2025  +function ct.parse(file, src, mode, setup)
  1543   2026   
  1544   2027   	local ctx = ct.ctx.mk(src)
  1545   2028   	ctx.line = 0
  1546   2029   	ctx.doc = ct.doc.mk()
  1547   2030   	ctx.doc.src = src
  1548         -	ctx.doc.stage = {
  1549         -		kind = 'parse';
  1550         -		mode = mode;
  1551         -	}
  1552   2031   	ctx.sec = ctx.doc:mksec() -- toplevel section
  1553   2032   	ctx.sec.origin = ctx:clone()
         2033  +	ctx.lang = mode['meta:lang']
         2034  +	if mode['parse:enc'] then
         2035  +		local e = ss.str.enc[mode['parse:enc']]
         2036  +		if not e then
         2037  +			ct.exns.enc('requested encoding not supported',mode['parse:enc']):throw()
         2038  +		end
         2039  +		ctx.doc.enc = e
         2040  +	end
  1554   2041   
  1555   2042   	-- create states for extension hooks
  1556   2043   	local job = ctx.doc:job('parse',nil,ctx)
         2044  +	ctx.doc.stage = {
         2045  +		kind = 'parse';
         2046  +		mode = mode;
         2047  +		job = job;
         2048  +		langstack = {ctx.lang};
         2049  +		fontstack = {};
         2050  +	}
         2051  +
         2052  +	local function
         2053  +	is_whitespace(cp)
         2054  +		return ctx.doc.enc.iswhitespace(cp)
         2055  +	end
         2056  +
         2057  +	if setup then setup(ctx) end
         2058  +
  1557   2059   
  1558   2060   	for full_line in file:lines() do ctx.line = ctx.line + 1
  1559   2061   		local l
  1560   2062   		for p, c in utf8.codes(full_line) do
  1561   2063   			if not is_whitespace(c) then
  1562   2064   				l = full_line:sub(p)
  1563   2065   				break
  1564   2066   			end
  1565   2067   		end
  1566         -		job:hook('line_read',ctx,l)
         2068  +		ct.parse_line(l, ctx, ctx.sec.blocks)
         2069  +	end
  1567   2070   
  1568         -		if ctx.mode then
  1569         -			if ctx.mode.kind == 'code' then
  1570         -				if l and l:match '^~~~%s*$' then
  1571         -					job:hook('block_listing_end',ctx,ctx.mode.listing)
  1572         -					job:hook('mode_switch', c, nil)
  1573         -					ctx.mode = nil
  1574         -				else
  1575         -					-- TODO handle formatted code
  1576         -					local newline = {l}
  1577         -					table.insert(ctx.mode.listing.lines, newline)
  1578         -					job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
  1579         -				end
  1580         -			else
  1581         -				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
  1582         -			end
  1583         -		else
  1584         -			if l then
  1585         -				local function tryseqs(seqs, ...)
  1586         -					for _, i in pairs(seqs) do
  1587         -						if  ((not i.seq ) or startswith(l, i.seq)) and
  1588         -							((not i.pred) or i.pred    (l, ctx  )) then
  1589         -							i.fn(l, ctx, job, ...)
  1590         -							return true
  1591         -						end
         2071  +	for i, sec in ipairs(ctx.doc.secorder) do
         2072  +		for refid, r in ipairs(sec.refs) do
         2073  +			if type(r) == 'table' and r.kind == 'resource' and r.props.src then
         2074  +				local lines = ss.str.breaklines(ctx.doc.enc, r.props.src)
         2075  +				local srcs = {}
         2076  +				for i,l in ipairs(lines) do
         2077  +					local args = ss.str.breakwords(ctx.doc.enc, l, 2, {escape=true})
         2078  +					if #args < 3 then
         2079  +						r.origin:fail('invalid syntax for resource %s', t.ref)
         2080  +					end
         2081  +					local mimebreak = function(s)
         2082  +						local wds = ss.str.split(ctx.doc.enc, s, '/', 1, {escape=true})
         2083  +						return wds
  1592   2084   					end
  1593         -					return false
         2085  +					local mime = mimebreak(args[2]);
         2086  +					local mimeclasses = {
         2087  +						['application/svg+xml'] = 'image';
         2088  +					}
         2089  +					local class = mimeclasses[mime]
         2090  +					table.insert(srcs, {
         2091  +						mode = args[1];
         2092  +						mime = mime;
         2093  +						uri = args[3];
         2094  +						class = class or mime[1];
         2095  +					})
  1594   2096   				end
  1595         -
  1596         -				if not tryseqs(ct.ctlseqs) then
  1597         -					local found = false
  1598         -					
  1599         -					for eb, ext, state in job:each('blocks') do
  1600         -						if tryseqs(eb, state) then found = true break end
  1601         -					end
  1602         -
  1603         -					if not found then
  1604         -						ctx:fail 'incomprehensible input line'
  1605         -					end
  1606         -				end
  1607         -			else
  1608         -				if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then
  1609         -					local brk = {kind='break'}
  1610         -					job:hook('block_break', ctx, brk, l)
  1611         -					table.insert(ctx.sec.blocks, brk)
  1612         -				end
         2097  +				 --ideally move this into its own mimetype lib
         2098  +				local kind = r.props.as or srcs[1].class
         2099  +				r.class = kind
         2100  +				r.srcs = srcs
  1613   2101   			end
  1614   2102   		end
  1615         -		job:hook('line_end',ctx,l)
  1616   2103   	end
  1617         -
         2104  +	ctx.doc.stage = nil
         2105  +	ctx.doc.docjob:hook('meddle_ast')
  1618   2106   	return ctx.doc
  1619   2107   end