cortav  Diff

Differences From Artifact [7d896a453b]:

To Artifact [e7bf814ce9]:


   106    106   	fns = {
   107    107   		fail = function(self, msg, ...)
   108    108   			ct.exns.tx(msg, self.src.file, self.line or 0, ...):throw()
   109    109   		end;
   110    110   		insert = function(self, block)
   111    111   			block.origin = self:clone()
   112    112   			table.insert(self.sec.blocks,block)
          113  +			return block
   113    114   		end;
   114    115   		ref = function(self,id)
   115    116   			if not id:find'%.' then
   116    117   				local rid = self.sec.refs[id]
   117    118   				if self.sec.refs[id] then
   118    119   					return self.sec.refs[id]
   119    120   				else self:fail("no such ref %s in current section", id or '') end
................................................................................
   149    150   	fns = {
   150    151   		mksec = function(self, id, depth)
   151    152   			local o = ct.sec(id, depth)
   152    153   			if id then self.sections[id] = o end
   153    154   			table.insert(self.secorder, o)
   154    155   			return o
   155    156   		end;
          157  +		allow_ext = function(self,name)
          158  +			if not ct.ext.loaded[name] then return false end
          159  +			if self.ext.inhibit[name] then return false end
          160  +			if self.ext.need[name] or self.ext.use[name] then
          161  +				return true
          162  +			end
          163  +			return ct.ext.loaded[name].default
          164  +		end;
   156    165   		context_var = function(self, var, ctx, test)
   157    166   			local fail = function(...)
   158    167   				if test then return false end
   159    168   				ctx:fail(...)
   160    169   			end
   161    170   			if startswith(var, 'cortav.') then
   162    171   				local v = var:sub(8)
................................................................................
   193    202   			elseif self.vars[var] then
   194    203   				return self.vars[var]
   195    204   			else
   196    205   				if test then return false end
   197    206   				return '' -- is this desirable behavior?
   198    207   			end
   199    208   		end;
          209  +		job = function(self, name, pred, ...) -- convenience func
          210  +			return self.docjob:fork(name, pred, ...)
          211  +		end
   200    212   	};
   201    213   	mk = function() return {
   202    214   		sections = {};
   203    215   		secorder = {};
   204    216   		embed = {};
   205    217   		meta = {};
   206    218   		vars = {};
          219  +		ext = {
          220  +			inhibit = {};
          221  +			need = {};
          222  +			use = {};
          223  +		};
   207    224   	} end;
          225  +	construct = function(me)
          226  +		me.docjob = ct.ext.job('doc', me, nil)
          227  +	end;
   208    228   }
   209    229   
   210    230   -- FP helper functions
   211    231   
   212    232   local function fmtfn(str)
   213    233   	return function(...)
   214    234   		return string.format(str, ...)
................................................................................
   221    241   		ct.exns.ext 'extension missing “id” field':throw()
   222    242   	end
   223    243   	if ct.ext.loaded[ext.id] then
   224    244   		ct.exns.ext('there is already an extension with ID “%s” loaded', ext.id):throw()
   225    245   	end
   226    246   	ct.ext.loaded[ext.id] = ext
   227    247   end
          248  +
          249  +function ct.ext.bind(doc)
          250  +	local fns = {}
          251  +	function fns.each(...)
          252  +		local cext
          253  +		local args = {...}
          254  +		return function()
          255  +			while true do
          256  +				cext = next(ct.ext.loaded, cext)
          257  +				if cext == nil then return nil end
          258  +				if doc == nil or doc:allow_ext(cext.id) then
          259  +					local v = ss.walk(ct.ext.loaded[cext.id], table.unpack(args))
          260  +					if v ~= nil then
          261  +						return v, cext
          262  +					end
          263  +				end
          264  +			end
          265  +		end
          266  +	end
          267  +
          268  +	function fns.hook(h, ...)
          269  +		-- this is the raw hook invocation function, used when hooks won't need
          270  +		-- private state to hold onto between invocation. if private state is
          271  +		-- necessary, construct a job instead
          272  +		local ret = {} -- for hooks that compile lists of responses from extensions
          273  +		for hook in fns.each('hook', h) do table.insert(ret,(hook(...))) end
          274  +		return ret
          275  +	end
          276  +	
          277  +	return fns
          278  +end
          279  +
          280  +do local globalfns = ct.ext.bind()
          281  +	-- use these functions when document restrictions don't matter
          282  +	ct.ext.each, ct.ext.hook = globalfns.each, globalfns.hook
          283  +end
          284  +
          285  +ct.ext.job = declare {
          286  +	ident = 'ext-job';
          287  +	init = {
          288  +		states = {};
          289  +	};
          290  +	construct = function(me,name,doc,pred,...)
          291  +		print('constructing job',name,'for',doc)
          292  +		-- prepare contexts for relevant extensions
          293  +		me.name = name
          294  +		me.doc = doc -- for reqs + limiting
          295  +		for _, ext in pairs(ct.ext.loaded) do
          296  +			if pred == nil or pred(ext) then
          297  +				me.states[ext] = {}
          298  +			end
          299  +		end
          300  +		me:hook('init', ...)
          301  +	end;
          302  +	fns = {
          303  +		fork = function(me, name, pred, ...)
          304  +			-- generate a branch job linked to this job
          305  +			local branch = getmetatable(me)(name, me.doc, pred, ...)
          306  +			branch.parent = me
          307  +			return branch
          308  +		end;
          309  +		delegate = function(me, ext) -- creates a delegate for state access
          310  +			local submethods = {
          311  +				unwind = function(self, n)
          312  +					local function
          313  +					climb(dlg, job, n)
          314  +						if n == 0 then
          315  +							return job:delegate(dlg.extension)
          316  +						else
          317  +							return climb(dlg, job.parent, n-1)
          318  +						end
          319  +					end
          320  +
          321  +					return climb(self._delegate_state, self._delegate_state.target, n)
          322  +				end;
          323  +			}
          324  +			local d = setmetatable({
          325  +				_delegate_state = {
          326  +					target = (me._delegate_state and me._delegate_state.target) or me;
          327  +					extension = ext;
          328  +				};
          329  +			}, {
          330  +				__name = 'job:delegate';
          331  +				__index = function(self, key)
          332  +					local D = self._delegate_state
          333  +					if key == 'state' then
          334  +						return D.target.states[self._delegate_state.extension]
          335  +					elseif submethods[key] then
          336  +						return submethods[key]
          337  +					end
          338  +					return D.target[key]
          339  +				end;
          340  +				__newindex = function(self, key, value)
          341  +					local D = self._delegate_state
          342  +					if key == 'state' then
          343  +						D.target.states[D.extension] = value
          344  +					else
          345  +						D.target[D.extension] = value
          346  +					end
          347  +				end;
          348  +			});
          349  +			return d;
          350  +		end;
          351  +		each = function(me, ...)
          352  +			local ek
          353  +			local path = {...}
          354  +			return function()
          355  +				while true do
          356  +					ek = next(me.states, ek)
          357  +					if not ek then return nil end
          358  +					if me.doc:allow_ext(ek.id) then
          359  +						local v = ss.walk(ek, table.unpack(path))
          360  +						if v then
          361  +							return v, ek, me.states[ek]
          362  +						end
          363  +					end
          364  +				end
          365  +			end
          366  +		end;
          367  +		proc = function(me, ...)
          368  +			local p
          369  +			local owner
          370  +			local state
          371  +			for func, ext, s in me:each(...) do
          372  +				if p == nil then
          373  +					p = func
          374  +					owner = ext
          375  +					state = s
          376  +				else
          377  +					ct.exn.ext('extensions %s and %s define conflicting procedures for %s', owner.id, ext.id, table.concat({...},'.')):throw()
          378  +				end
          379  +			end
          380  +			if p == nil then return nil end
          381  +			if type(p) ~= 'function' then return p end
          382  +			return function(...)
          383  +				return p(me:delegate(owner), ...)
          384  +			end, owner, state
          385  +		end;
          386  +		hook = function(me, hook, ...)
          387  +			-- used when extensions may need to persist state across
          388  +			-- multiple functions or invocations
          389  +			local ret = {}
          390  +			local hook_id = me.name ..'_'.. hook
          391  +			for hookfn, ext, state in me:each('hook', hook_id) do
          392  +				print(' - running hook for ext',ext.id)
          393  +				table.insert(ret, (hookfn(me:delegate(ext),...)))
          394  +			end
          395  +			return ret
          396  +		end;
          397  +	};
          398  +}
   228    399   
   229    400   -- renderer engines
   230    401   function ct.render.html(doc, opts)
   231    402   	local doctitle = opts['title']
   232    403   	local f = string.format
   233    404   	local ids = {}
   234    405   	local canonicalID = {}
................................................................................
   419    590   			}
   420    591   			section > figure.listing > hr {
   421    592   				border: none;
   422    593   				margin: 0;
   423    594   				height: 0.7em;
   424    595   				counter-increment: line-number;
   425    596   			}
   426         -		]];
   427         -		toc = [[
   428         -
   429         -		]];
   430         -		tocFixed = [[
   431         -			@media (min-width: calc(@[width]:[100vw] + 20em)) {
   432         -				ol.toc {
   433         -					position: fixed;
   434         -					padding-top: 1em; padding-bottom: 1em;
   435         -					padding-right: 1em;
   436         -					margin-top: 0; margin-bottom: 0;
   437         -					right: 0; top: 0; bottom: 0;
   438         -					max-width: calc(50vw - ((@[width]:[0]) / 2) - 3.5em);
   439         -					overflow-y: auto;
   440         -				}
   441         -				@media (max-width: calc(@[width]:[100vw] + 30em)) {
   442         -					ol.toc {
   443         -						max-width: calc(100vw - ((@[width]:[0])) - 9.5em);
   444         -					}
   445         -					body {
   446         -						margin-left: 5em;
   447         -					}
   448         -				}
   449         -			}
   450    597   		]];
   451    598   	}
   452    599   
   453    600   	local stylesNeeded = {}
   454    601   
   455         -	local function getSpanRenderers(tag,elt)
          602  +	local render_state_handle = {
          603  +		doc = doc;
          604  +		opts = opts;
          605  +		style_rules = styles; -- use stylesneeded if at all possible
          606  +		stylesets = stylesets;
          607  +		stylesets_active = stylesNeeded;
          608  +		obj_htmlid = getSafeID;
          609  +		-- remaining fields added later
          610  +	}
          611  +
          612  +	local renderJob = doc:job('render_html', nil, render_state_handle)
          613  +
          614  +	local runhook = function(h, ...)
          615  +		return renderJob:hook(h, render_state_handle, ...)
          616  +	end
          617  +
          618  +	local function getSpanRenderers(procs)
          619  +		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
   456    620   		local htmlDoc = function(title, head, body)
   457    621   			return [[<!doctype html>]] .. tag('html',nil,
   458    622   				tag('head', nil,
   459    623   					elt('meta',{charset = 'utf-8'}) ..
   460    624   					(title and tag('title', nil, title) or '') ..
   461    625   					(head or '')) ..
   462    626   				tag('body', nil, body or ''))
................................................................................
   512    676   
   513    677   		function span_renderers.var(v,b,s)
   514    678   			local val
   515    679   			if v.pos then
   516    680   				if not v.origin.invocation then
   517    681   					v.origin:fail 'positional arguments can only be used in a macro invocation'
   518    682   				elseif not v.origin.invocation.args[v.pos] then
   519         -					v.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos)
          683  +					v.origin.invocation.origin:fail('macro invocation %s missing positional argument #%u', v.origin.invocation.macro, v.pos)
   520    684   				end
   521    685   				val = v.origin.invocation.args[v.pos]
   522    686   			else
   523    687   				val = v.origin.doc:context_var(v.var, v.origin)
   524    688   			end
   525    689   			if v.raw then
   526    690   				return val
................................................................................
   547    711   			span_renderers = span_renderers;
   548    712   			htmlSpan = htmlSpan;
   549    713   			htmlDoc = htmlDoc;
   550    714   		}
   551    715   	end
   552    716   
   553    717   
   554         -	local function getBlockRenderers(tag,elt,sr,catenate)
   555         -		local function insert_toc(b,s)
   556         -			local lst = {tag = 'ol', attrs={class='toc'}, nodes={}}
   557         -			stylesNeeded.toc = true
   558         -			if opts['width'] then
   559         -				stylesNeeded.tocFixed = true
   560         -			end
   561         -			local stack = {lst}
   562         -			local top = function() return stack[#stack] end
   563         -			local all = s.origin.doc.secorder
   564         -			for i, sec in ipairs(all) do
   565         -				if sec.heading_node then
   566         -					local ent = tag('li',nil,
   567         -						 catenate{tag('a', {href='#'..getSafeID(sec)},
   568         -							sr.htmlSpan(sec.heading_node.spans))})
   569         -					if sec.depth > #stack then
   570         -						local n = {tag = 'ol', attrs={}, nodes={ent}}
   571         -						table.insert(top().nodes[#top().nodes].nodes, n)
   572         -						table.insert(stack, n)
   573         -					else
   574         -						if sec.depth < #stack then
   575         -							for j=#stack,sec.depth+1,-1 do stack[j] = nil end
   576         -						end
   577         -						table.insert(top().nodes, ent)
   578         -					end
   579         -				end
   580         -			end
   581         -			return lst
   582         -		end
          718  +	local function getBlockRenderers(procs, sr)
          719  +		local tag, elt, catenate = procs.tag, procs.elt, procs.catenate
          720  +		local null = function() return catenate{} end
   583    721   
   584    722   		local block_renderers = {
          723  +			anchor = function(b,s)
          724  +				return tag('a',{id = getSafeID(b)},null())
          725  +			end;
   585    726   			paragraph = function(b,s)
   586    727   				stylesNeeded.paragraph = true;
   587    728   				return tag('p', nil, sr.htmlSpan(b.spans, b, s), b)
   588    729   			end;
   589    730   			directive = function(b,s)
   590    731   				-- deal with renderer directives
   591    732   				local _, cmd, args = b.words(2)
................................................................................
   605    746   				else
   606    747   					-- handle other uses of labels here
   607    748   				end
   608    749   			end;
   609    750   			['list-item'] = function(b,s)
   610    751   				return tag('li', nil, sr.htmlSpan(b.spans, b, s), b)
   611    752   			end;
   612         -			toc = insert_toc;
   613    753   			table = function(b,s)
   614    754   				local tb = {}
   615    755   				for i, r in ipairs(b.rows) do
   616    756   					local row = {}
   617    757   					for i, c in ipairs(r) do
   618    758   						table.insert(row, tag(c.header and 'th' or 'td',
   619    759   						{align=c.align}, sr.htmlSpan(c.spans, b)))
................................................................................
   633    773   				end, b.lines)
   634    774   				if b.title then
   635    775   					table.insert(nodes,1,tag('figcaption',nil,sr.htmlSpan(b.title)))
   636    776   				end
   637    777   				if b.lang then langsused[b.lang] = true end
   638    778   				return tag('figure', {class='listing', lang=b.lang, id=b.id and getSafeID(b)}, catenate(nodes))
   639    779   			end;
          780  +			aside = function(b,s)
          781  +				local bn = {}
          782  +				for _,v in pairs(b.lines) do
          783  +					table.insert(bn, tag('p', {}, sr.htmlSpan(v, b, s)))
          784  +				end
          785  +				return tag('aside', {}, bn)
          786  +			end;
   640    787   			['break'] = function() --[[nop]] end;
   641    788   		}
   642    789   		return block_renderers;
   643    790   	end
   644    791   
   645         -	local pspan = getSpanRenderers(function(t,a,v) return v  end,
   646         -	                               function(t,a)   return '' end)
   647         -	 
   648         -	local function getRenderers(tag,elt,catenate)
   649         -		local r = getSpanRenderers(tag,elt)
   650         -		r.block_renderers = getBlockRenderers(tag,elt,r,catenate)
          792  +	local function getRenderers(procs)
          793  +		local r = getSpanRenderers(procs)
          794  +		r.block_renderers = getBlockRenderers(procs, r)
   651    795   		return r
   652    796   	end
   653    797   
   654         -	local elt = function(t,attrs)
   655         -		return f('<%s%s>', t,
   656         -			attrs and ss.reduce(function(a,b) return a..b end, '', 
   657         -				ss.map(function(v,k)
   658         -					if v == true
   659         -						then          return ' '..k
   660         -						elseif v then return f(' %s="%s"', k, v)
   661         -					end
   662         -				end, attrs)) or '')
   663         -	end
   664         -	local tag = function(t,attrs,body)
   665         -		return f('%s%s</%s>', elt(t,attrs), body, t)
   666         -	end
   667         -
          798  +	local tagproc do
          799  +		local elt = function(t,attrs)
          800  +			return f('<%s%s>', t,
          801  +				attrs and ss.reduce(function(a,b) return a..b end, '', 
          802  +					ss.map(function(v,k)
          803  +						if v == true
          804  +							then          return ' '..k
          805  +							elseif v then return f(' %s="%s"', k, v)
          806  +						end
          807  +					end, attrs)) or '')
          808  +		end
          809  +
          810  +		tagproc = {
          811  +			toTXT = {
          812  +				tag = function(t,a,v) return v  end;
          813  +				elt = function(t,a)   return '' end;
          814  +				catenate = table.concat;
          815  +			};
          816  +			toIR = {
          817  +				tag = function(t,a,v,o) return {
          818  +					tag = t, attrs = a;
          819  +					nodes = type(v) == 'string' and {v} or v, src = o
          820  +				} end;
          821  +				
          822  +				elt = function(t,a,o) return {
          823  +					tag = t, attrs = a, src = o
          824  +				} end;
          825  +
          826  +				catenate = function(...) return ... end;
          827  +			};
          828  +			toHTML = {
          829  +				elt = elt;
          830  +				tag = function(t,attrs,body)
          831  +					return f('%s%s</%s>', elt(t,attrs), body, t)
          832  +				end;
          833  +				catenate = table.concat;
          834  +			};
          835  +		}
          836  +	end
          837  +
          838  +	local astproc = {
          839  +		toHTML = getRenderers(tagproc.toHTML);
          840  +		toTXT  = getRenderers(tagproc.toTXT);
          841  +		toIR   = { };
          842  +	}
          843  +	astproc.toIR.span_renderers = ss.clone(astproc.toHTML);
          844  +	astproc.toIR.block_renderers = getBlockRenderers(tagproc.toIR,astproc.toHTML);
          845  +		-- note we use HTML here instead of IR span renderers, because as things
          846  +		-- currently stand we don't need that level of resolution. if we ever
          847  +		-- get to the point where we want to be able to twiddle spans around
          848  +		-- we'll need to introduce an IR span renderer
          849  +
          850  +	render_state_handle.astproc = astproc;
          851  +	render_state_handle.tagproc = tagproc;
          852  +
          853  +	-- bind to legacy names
          854  +	-- yikes this needs to be cleaned up so badly
   668    855   	local ir = {}
   669         -	local toc
   670         -	local dr = getRenderers(tag,elt,table.concat) -- default renderers
   671         -	local plainr = getRenderers(function(t,a,v) return v  end,
   672         -	                            function(t,a)   return '' end, table.concat)
   673         -	local irBlockRdrs = getBlockRenderers(
   674         -		function(t,a,v,o) return {tag = t, attrs = a, nodes = type(v) == 'string' and {v} or v, src = o} end,
   675         -		function(t,a,o) return {tag = t, attrs = a, src = o} end,
   676         -		dr, function(...) return ... end)
          856  +	local dr = astproc.toHTML -- default renderers
          857  +	local plainr = astproc.toTXT
          858  +	local irBlockRdrs = astproc.toIR.block_renderers;
   677    859   
          860  +	render_state_handle.ir = ir;
          861  +
          862  +	runhook('ir_assemble', ir)
   678    863   	for i, sec in ipairs(doc.secorder) do
   679    864   		if doctitle == nil and sec.depth == 1 and sec.heading_node then
   680         -			doctitle = plainr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
          865  +			doctitle = astproc.toTXT.htmlSpan(sec.heading_node.spans, sec.heading_node, sec)
   681    866   		end
   682    867   		local irs
   683    868   		if sec.kind == 'ordinary' then
   684    869   			if #(sec.blocks) > 0 then
   685    870   				irs = {tag='section',attrs={id = getSafeID(sec)},nodes={}}
   686    871   
          872  +				runhook('ir_section_build', irs, sec)
          873  +				
   687    874   				for i, block in ipairs(sec.blocks) do
   688         -					local rd = irBlockRdrs[block.kind](block,sec)
          875  +					local rd
          876  +					if irBlockRdrs[block.kind] then
          877  +						rd = irBlockRdrs[block.kind](block,sec)
          878  +					else
          879  +						local rdr = renderJob:proc('render',block.kind,'html')
          880  +						if rdr then
          881  +							rd = rdr({
          882  +								state = render_state_handle;
          883  +								tagproc = tagproc.toIR;
          884  +								astproc = astproc.toIR;
          885  +							}, block, sec)
          886  +						end
          887  +					end
   689    888   					if rd then
   690    889   						if opts['heading-anchors'] and block == sec.heading_node then
   691    890   							stylesNeeded.headingAnchors = true
   692    891   							table.insert(rd.nodes, ' ')
   693    892   							table.insert(rd.nodes, {
   694    893   								tag = 'a';
   695    894   								attrs = {href = '#' .. irs.attrs.id, class='anchor'};
   696    895   								nodes = {type(opts['heading-anchors'])=='string' and opts['heading-anchors'] or '&sect;'};
   697    896   							})
   698    897   						end
   699    898   						table.insert(irs.nodes, rd)
          899  +						runhook('ir_section_node_insert', rd, irs, sec)
   700    900   					end
   701    901   				end
   702    902   			end
   703    903   		elseif sec.kind == 'blockquote' then
   704    904   		elseif sec.kind == 'listing' then
   705    905   		elseif sec.kind == 'embed' then
   706    906   		end
   707    907   		if irs then table.insert(ir, irs) end
   708    908   	end
   709    909   
   710    910   	-- restructure passes
          911  +	runhook('ir_restructure_pre', ir)
   711    912   	
   712    913   	---- list insertion pass
   713    914   	local lists = {}
   714    915   	for _, sec in pairs(ir) do
   715    916   		if sec.tag == 'section' then
   716    917   			local i = 1 while i <= #sec.nodes do local v = sec.nodes[i]
   717    918   				if v.tag == 'li' then
................................................................................
   773    974   
   774    975   					sec.nodes[i] = struc
   775    976   				end
   776    977   			end
   777    978   		end
   778    979   	end
   779    980   	
          981  +	runhook('ir_restructure_post', ir)
   780    982   
   781    983   	-- collection pass
   782    984   	local function collect_nodes(t)
   783    985   		local ts = ''
   784    986   		for i,v in ipairs(t) do
   785    987   			if type(v) == 'string' then
   786    988   				ts = ts .. v
   787    989   			elseif v.nodes then
   788         -				ts = ts .. tag(v.tag, v.attrs, collect_nodes(v.nodes))
          990  +				ts = ts .. tagproc.toHTML.tag(v.tag, v.attrs, collect_nodes(v.nodes))
   789    991   			elseif v.text then
   790         -				ts = ts .. tag(v.tag,v.attrs,v.text)
          992  +				ts = ts .. tagproc.toHTML.tag(v.tag,v.attrs,v.text)
   791    993   			else
   792         -				ts = ts .. elt(v.tag,v.attrs)
          994  +				ts = ts .. tagproc.toHTML.elt(v.tag,v.attrs)
   793    995   			end
   794    996   		end
   795    997   		return ts
   796    998   	end
   797    999   	local body = collect_nodes(ir)
   798   1000   
   799   1001   	for k in pairs(langsused) do
................................................................................
   873   1075   	end
   874   1076   
   875   1077   	local head = {}
   876   1078   	local styletag = ''
   877   1079   	if opts['link-css'] then
   878   1080   		local css = opts['link-css']
   879   1081   		if type(css) ~= 'string' then ct.exns.mode('must be a string', 'html:link-css'):throw() end
   880         -		styletag = styletag .. elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
         1082  +		styletag = styletag .. tagproc.toHTML.elt('link',{rel='stylesheet',type='text/css',href=opts['link-css']})
   881   1083   	end
   882   1084   	if next(styles) then
   883   1085   		if opts['gen-styles'] then
   884         -			styletag = styletag .. tag('style',{type='text/css'},table.concat(styles))
         1086  +			styletag = styletag .. tagproc.toHTML.tag('style',{type='text/css'},table.concat(styles))
   885   1087   		end
   886   1088   		table.insert(head, styletag)
   887   1089   	end
   888   1090   
   889   1091   	if opts['fossil-uv'] then
   890         -		return tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
         1092  +		return tagproc.toHTML.tag('div',{class='fossil-doc',['data-title']=doctitle},styletag .. body)
   891   1093   	elseif opts.snippet then
   892   1094   		return styletag .. body
   893   1095   	else
   894   1096   		return dr.htmlDoc(doctitle, next(head) and table.concat(head), body)
   895   1097   	end
   896   1098   end
   897   1099   
................................................................................
  1046   1248   	end
  1047   1249   	flush()
  1048   1250   	return spans
  1049   1251   end
  1050   1252   
  1051   1253   local function
  1052   1254   blockwrap(fn)
  1053         -	return function(l,c)
  1054         -		local block = fn(l,c)
         1255  +	return function(l,c,j)
         1256  +		local block = fn(l,c,j)
  1055   1257   		block.origin = c:clone();
  1056   1258   		table.insert(c.sec.blocks, block);
         1259  +		j:hook('block_insert', c, block, l)
  1057   1260   	end
  1058   1261   end
  1059   1262   
  1060   1263   local insert_paragraph = blockwrap(function(l,c)
  1061   1264   	if l:sub(1,1) == '.' then l = l:sub(2) end
  1062   1265   	return {
  1063   1266   		kind = "paragraph";
  1064   1267   		spans = ct.parse_span(l, c);
  1065   1268   	}
  1066   1269   end)
  1067   1270   
  1068         -local insert_section = function(l,c)
         1271  +local insert_section = function(l,c,j)
  1069   1272   	local depth, id, t = l:match '^([#§]+)([^%s]*)%s*(.-)$'
  1070   1273   	if id and id ~= "" then
  1071   1274   		if c.doc.sections[id] then
  1072   1275   			c:fail('duplicate section name “%s”', id)
  1073   1276   		end
  1074   1277   	else id = nil end
  1075   1278   
................................................................................
  1085   1288   			origin = s.origin;
  1086   1289   			captions = s;
  1087   1290   		}
  1088   1291   		table.insert(s.blocks, heading)
  1089   1292   		s.heading_node = heading
  1090   1293   	end
  1091   1294   	c.sec = s
         1295  +
         1296  +	j:hook('section_attach', c, s)
  1092   1297   end
  1093   1298   
  1094         -local dsetmeta = function(w,c)
         1299  +local dsetmeta = function(w,c,j)
  1095   1300   	local key, val = w(1)
  1096   1301   	c.doc.meta[key] = val
         1302  +	j:hook('metadata_set', key, val)
  1097   1303   end
  1098   1304   local dextctl = function(w,c)
  1099   1305   	local mode, exts = w(1)
  1100   1306   	for e in exts:gmatch '([^%s]+)' do
  1101   1307   		if mode == 'uses' then
  1102   1308   		elseif mode == 'needs' then
  1103   1309   		elseif mode == 'inhibits' then
................................................................................
  1109   1315   	c.hide_next = mode == 'unless'
  1110   1316   end;
  1111   1317   ct.directives = {
  1112   1318   	author = dsetmeta;
  1113   1319   	license = dsetmeta;
  1114   1320   	keywords = dsetmeta;
  1115   1321   	desc = dsetmeta;
  1116         -	toc = function(w,c)
  1117         -		local toc, op, val = w(2)
  1118         -		if op == nil then
  1119         -			table.insert(c.sec.blocks, {kind='toc'})
  1120         -		end
  1121         -	end;
  1122   1322   	when = dcond;
  1123   1323   	unless = dcond;
  1124   1324   	expand = function(w,c)
  1125   1325   		local _, m = w(1)
  1126   1326   		if m ~= 'off' then
  1127   1327   			c.expand_next = 1
  1128   1328   		else
  1129   1329   			c.expand_next = 0
  1130   1330   		end
  1131   1331   	end;
  1132   1332   }
  1133   1333   
  1134         -local function insert_table_row(l,c)
         1334  +local function insert_table_row(l,c,j)
  1135   1335   	local row = {}
  1136   1336   	local buf
  1137   1337   	local flush = function()
  1138   1338   		if buf then
  1139   1339   			buf.str = buf.str:gsub('%s+$','')
  1140   1340   			table.insert(row, buf)
  1141   1341   		end
................................................................................
  1177   1377   	if buf.str ~= '' then flush() end 
  1178   1378   	for _,v in pairs(row) do
  1179   1379   		v.spans = ct.parse_span(v.str, c)
  1180   1380   	end
  1181   1381   	if #c.sec.blocks > 1 and c.sec.blocks[#c.sec.blocks].kind == 'table' then
  1182   1382   		local tbl = c.sec.blocks[#c.sec.blocks]
  1183   1383   		table.insert(tbl.rows, row)
         1384  +		j:hook('block_table_attach', c, tbl, row, l)
         1385  +		j:hook('block_table_row_insert', c, tbl, row, l)
  1184   1386   	else
  1185         -		table.insert(c.sec.blocks, {
         1387  +		local tbl = {
  1186   1388   			kind = 'table';
  1187   1389   			rows = {row};
  1188   1390   			origin = c:clone();
  1189         -		})
         1391  +		}
         1392  +		table.insert(c.sec.blocks, tbl)
         1393  +		j:hook('block_table_insert', c, tbl, l)
         1394  +		j:hook('block_table_row_insert', c, tbl, tbl.rows[1], l)
  1190   1395   	end
  1191   1396   end
  1192   1397   
  1193   1398   ct.ctlseqs = {
  1194   1399   	{seq = '.', fn = insert_paragraph};
  1195   1400   	{seq = '¶', fn = insert_paragraph};
  1196   1401   	{seq = '❡', fn = insert_paragraph};
  1197   1402   	{seq = '#', fn = insert_section};
  1198   1403   	{seq = '§', fn = insert_section};
  1199   1404   	{seq = '+', fn = insert_table_row};
  1200   1405   	{seq = '|', fn = insert_table_row};
  1201   1406   	{seq = '│', fn = insert_table_row};
         1407  +	{seq = '!', fn = function(l,c,j) 
         1408  +		local last = c.sec.blocks[#c.sec.blocks]
         1409  +		local txt = l:match '^%s*!%s*(.-)$'
         1410  +		if (not last) or last.kind ~= 'aside' then
         1411  +			local aside = {
         1412  +				kind = 'aside';
         1413  +				lines = { ct.parse_span(txt, c) }
         1414  +			}
         1415  +			c:insert(aside)
         1416  +			j:hook('block_aside_insert', c, aside, l)
         1417  +			j:hook('block_aside_line_insert', c, aside, aside.lines[1], l)
         1418  +			j:hook('block_insert', c, aside, l)
         1419  +		else
         1420  +			local sp = ct.parse_span(txt, c)
         1421  +			table.insert(last.lines, sp)
         1422  +			j:hook('block_aside_attach', c, last, sp, l)
         1423  +			j:hook('block_aside_line_insert', c, last, sp, l)
         1424  +		end
         1425  +	end};
  1202   1426   	{pred = function(s,c) return s:match'^[*:]' end, fn = blockwrap(function(l,c) -- list
  1203   1427   		local stars = l:match '^([*:]+)'
  1204   1428   		local depth = utf8.len(stars)
  1205   1429   		local id, txt = l:sub(#stars+1):match '^(.-)%s*(.-)$'
  1206   1430   		local ordered = stars:sub(#stars) == ':'
  1207   1431   		if id == '' then id = nil end
  1208   1432   		return {
  1209   1433   			kind = 'list-item';
  1210   1434   			depth = depth;
  1211   1435   			ordered = ordered;
  1212   1436   			spans = ct.parse_span(txt, c);
  1213   1437   		}
  1214   1438   	end)};
  1215         -	{seq = '\t', fn = function(l,c)
         1439  +	{seq = '\t', fn = function(l,c,j)
  1216   1440   		local ref, val = l:match '\t+([^:]+):%s*(.*)$'
  1217   1441   		c.sec.refs[ref] = val
         1442  +		j:hook('section_ref_attach', c, ref, val, l)
  1218   1443   	end};
  1219         -	{seq = '%', fn = function(l,c) -- directive
         1444  +	{seq = '%', fn = function(l,c,j) -- directive
  1220   1445   		local crit, cmdline = l:match '^%%([!%%]?)%s*(.*)$'
  1221   1446   		local words = function(i)
  1222   1447   			local wds = {}
  1223   1448   			if i == 0 then return cmdline end
  1224   1449   			for w,pos in cmdline:gmatch '([^%s]+)()' do
  1225   1450   				table.insert(wds, w)
  1226   1451   				i = i - 1
................................................................................
  1229   1454   					return table.unpack(wds)
  1230   1455   				end
  1231   1456   			end
  1232   1457   		end
  1233   1458   
  1234   1459   		local cmd, rest = words(1)
  1235   1460   		if ct.directives[cmd] then
  1236         -			ct.directives[cmd](words,c)
         1461  +			ct.directives[cmd](words,c,j)
  1237   1462   		elseif cmd == c.doc.stage.mode['render:format'] then
  1238   1463   			-- this is a directive for the renderer; insert it into the tree as is
  1239         -			c:insert {
         1464  +			local dir = {
  1240   1465   				kind = 'directive';
  1241   1466   				critical = crit == '!';
  1242   1467   				words = words;
  1243   1468   			}
         1469  +			c:insert(dir)
         1470  +			j:hook('block_directive_render', j, c, dir)
         1471  +		elseif c.doc:allow_ext(cmd) then -- extension directives begin with their id
         1472  +			local ext = ct.ext.loaded[cmd]
         1473  +			if ext.directives then
         1474  +				local _, topcmd = words(2)
         1475  +				if ext.directives[topcmd] then
         1476  +					ext.directives[topcmd](j:delegate(ext), c, words)
         1477  +				elseif ext.directives[true] then -- catch-all
         1478  +					ext.directives[true](j:delegate(ext), c, words)
         1479  +				elseif crit == '!' then
         1480  +					c:fail('extension %s does not support critical directive %s', cmd, topcmd)
         1481  +				end
         1482  +			end
  1244   1483   		elseif crit == '!' then
  1245   1484   			c:fail('critical directive %s not supported',cmd)
  1246   1485   		end
  1247   1486   	end;};
  1248         -	{seq = '~~~', fn = blockwrap(function(l,c)
         1487  +	{seq = '~~~', fn = blockwrap(function(l,c,j)
  1249   1488   		local extract = function(ptn, str)
  1250   1489   			local start, stop = str:find(ptn)
  1251   1490   			if not start then return nil, str end
  1252   1491   			local ex = str:sub(start,stop)
  1253   1492   			local n = str:sub(1,start-1) .. str:sub(stop+1)
  1254   1493   			return ex, n
  1255   1494   		end
................................................................................
  1260   1499   			lang, s = extract('%b[]', s)
  1261   1500   			if lang then lang = lang:sub(2,-2) end
  1262   1501   			id, title = extract('#[^%s]+', s)
  1263   1502   			if id then id = id:sub(2) end
  1264   1503   		elseif l:match '^~~~' then -- MD shorthand style
  1265   1504   			lang = l:match '^~~~%s*(.-)%s*$'
  1266   1505   		end
  1267         -		c.mode = {
         1506  +		local mode = {
  1268   1507   			kind = 'code';
  1269   1508   			listing = {
  1270   1509   				kind = 'listing';
  1271   1510   				lang = lang, id = id, title = title and ct.parse_span(title,c);
  1272   1511   				lines = {};
  1273   1512   			}
  1274   1513   		}
         1514  +		j:hook('mode_switch', c, mode)
         1515  +		c.mode = mode
  1275   1516   		if id then
  1276   1517   			if c.sec.refs[id] then c:fail('duplicate ID %s', id) end
  1277   1518   			c.sec.refs[id] = c.mode.listing
  1278   1519   		end
         1520  +		j:hook('block_insert', c, mode.listing, l)
  1279   1521   		return c.mode.listing;
  1280   1522   	end)};
  1281   1523   	{pred = function(s,c)
  1282   1524   		if s:match '^[%-_][*_%-%s]+' then return true end
  1283   1525   		if startswith(s, '—') then
  1284   1526   			for c, p in eachcode(s) do
  1285   1527   				if ({
................................................................................
  1294   1536   	end)};
  1295   1537   	{fn = insert_paragraph};
  1296   1538   }
  1297   1539   
  1298   1540   function ct.parse(file, src, mode)
  1299   1541   	local function
  1300   1542   	is_whitespace(cp)
  1301         -		return cp == 0x20
         1543  +		return cp == 0x20 or cp == 0xe390
  1302   1544   	end
  1303   1545   
  1304   1546   	local ctx = ct.ctx.mk(src)
  1305   1547   	ctx.line = 0
  1306   1548   	ctx.doc = ct.doc.mk()
  1307   1549   	ctx.doc.src = src
  1308   1550   	ctx.doc.stage = {
  1309   1551   		kind = 'parse';
  1310   1552   		mode = mode;
  1311   1553   	}
  1312   1554   	ctx.sec = ctx.doc:mksec() -- toplevel section
  1313   1555   	ctx.sec.origin = ctx:clone()
  1314   1556   
         1557  +	-- create states for extension hooks
         1558  +	local job = ctx.doc:job('parse',nil,ctx)
         1559  +
  1315   1560   	for full_line in file:lines() do ctx.line = ctx.line + 1
  1316   1561   		local l
  1317   1562   		for p, c in utf8.codes(full_line) do
  1318   1563   			if not is_whitespace(c) then
  1319   1564   				l = full_line:sub(p)
  1320   1565   				break
  1321   1566   			end
  1322   1567   		end
         1568  +		job:hook('line_read',ctx,l)
         1569  +
  1323   1570   		if ctx.mode then
  1324   1571   			if ctx.mode.kind == 'code' then
  1325   1572   				if l and l:match '^~~~%s*$' then
         1573  +					job:hook('block_listing_end',ctx,ctx.mode.listing)
         1574  +					job:hook('mode_switch', c, nil)
  1326   1575   					ctx.mode = nil
  1327   1576   				else
  1328   1577   					-- TODO handle formatted code
  1329         -					table.insert(ctx.mode.listing.lines, {l})
         1578  +					local newline = {l}
         1579  +					table.insert(ctx.mode.listing.lines, newline)
         1580  +					job:hook('block_listing_newline',ctx,ctx.mode.listing,newline)
  1330   1581   				end
  1331   1582   			else
  1332   1583   				ctx:fail('unimplemented syntax mode %s', ctx.mode.kind)
  1333   1584   			end
  1334   1585   		else
  1335   1586   			if l then
  1336         -				local found = false
  1337         -				for _, i in pairs(ct.ctlseqs) do
  1338         -					if  ((not i.seq ) or startswith(l, i.seq)) and
  1339         -						((not i.pred) or i.pred    (l, ctx  )) then
  1340         -						found = true
  1341         -						i.fn(l, ctx)
  1342         -						break
         1587  +				local function tryseqs(seqs, ...)
         1588  +					for _, i in pairs(seqs) do
         1589  +						if  ((not i.seq ) or startswith(l, i.seq)) and
         1590  +							((not i.pred) or i.pred    (l, ctx  )) then
         1591  +							i.fn(l, ctx, job, ...)
         1592  +							return true
         1593  +						end
  1343   1594   					end
         1595  +					return false
  1344   1596   				end
  1345         -				if not found then
  1346         -					ctx:fail 'incomprehensible input line'
         1597  +
         1598  +				if not tryseqs(ct.ctlseqs) then
         1599  +					local found = false
         1600  +					
         1601  +					for eb, ext, state in job:each('blocks') do
         1602  +						if tryseqs(eb, state) then found = true break end
         1603  +					end
         1604  +
         1605  +					if not found then
         1606  +						ctx:fail 'incomprehensible input line'
         1607  +					end
  1347   1608   				end
  1348   1609   			else
  1349   1610   				if next(ctx.sec.blocks) and ctx.sec.blocks[#ctx.sec.blocks].kind ~= 'break' then
  1350         -					table.insert(ctx.sec.blocks, {kind='break'})
         1611  +					local brk = {kind='break'}
         1612  +					job:hook('block_break', ctx, brk, l)
         1613  +					table.insert(ctx.sec.blocks, brk)
  1351   1614   				end
  1352   1615   			end
  1353   1616   		end
         1617  +		job:hook('line_end',ctx,l)
  1354   1618   	end
  1355   1619   
  1356   1620   	return ctx.doc
  1357   1621   end