cortav  Diff

Differences From Artifact [ed743c91e7]:

To Artifact [706a61f3d9]:


     1      1   local ct = require 'cortav'
     2      2   local ss = require 'sirsem'
            3  +
            4  +local css_toc = [[
            5  +
            6  +]]
            7  +
            8  +local css_toc_fixed = [[
            9  +	@media (min-width: calc(@[width]:[100vw] + 20em)) {
           10  +		ol.toc {
           11  +			position: fixed;
           12  +			padding-top: 1em; padding-bottom: 1em;
           13  +			padding-right: 1em;
           14  +			margin-top: 0; margin-bottom: 0;
           15  +			right: 0; top: 0; bottom: 0;
           16  +			max-width: calc(50vw - ((@[width]:[0]) / 2) - 3.5em);
           17  +			overflow-y: auto;
           18  +		}
           19  +		@media (max-width: calc(@[width]:[100vw] + 30em)) {
           20  +			ol.toc {
           21  +				max-width: calc(100vw - ((@[width]:[0])) - 9.5em);
           22  +			}
           23  +			body {
           24  +				margin-left: 5em;
           25  +			}
           26  +		}
           27  +	}
           28  +]]
     3     29   
     4     30   ct.ext.install {
     5     31   	id = 'toc';
     6     32   	desc = 'provides a table of contents for HTML renderer plus generic fallback';
     7     33   	version = ss.version {0,1; 'devel'};
     8     34   	contributors = {{name='lexi hale', handle='velartrill', mail='lexi@hale.su', homepage='https://hale.su'}};
     9         -	directive = function(words)
           35  +	default = true; -- on unless inhibited
           36  +	hook = {
           37  +		doc_init = function(job)
           38  +			print('initing doc:toc',job.doc)
           39  +			job.state.toc_custom_position = false
           40  +		end;
           41  +
           42  +		render_html_init = function(job, render)
           43  +			render.stylesets.toc = css_toc
           44  +			render.stylesets.tocFixed = css_toc_fixed
           45  +		end;
           46  +
           47  +		render_html_ir_assemble = function(job, render, ir)
           48  +			-- the custom position state is part of the document job,
           49  +			-- but rendering is a separate job, so we need to get the
           50  +			-- state of this extension in the parent job, which is
           51  +			-- done with the job:unwind(depth) call. unwind is a method
           52  +			-- of the delegate we access the job through which gives us
           53  +			-- direct access to the job state of this extension; unwind
           54  +			-- climbs the jobtree and constructs a similar delegate for
           55  +			-- the nth parent. note that this will only work if the
           56  +			-- current extension hasn't been excluded by predicate from
           57  +			-- the nth parent!
           58  +			if not job:unwind(1).state.toc_custom_position then
           59  +				-- TODO insert %toc end of first section
           60  +			end
           61  +		end;
           62  +	};
           63  +	directives = {
           64  +		mark = function (job, ctx, words) 
           65  +			local _, _, text = words(2)
           66  +			ctx:insert {kind = 'anchor', _toc_label = ct.parse_span(text,ctx)}
           67  +		end;
           68  +		name = function (job, ctx, words) 
           69  +			local _, _, id, text = words(3)
           70  +			ctx:insert {kind = 'anchor', id=id, _toc_label = ct.parse_span(text,ctx)}
           71  +		end;
           72  +		[true] = function (job, ctx, words) 
           73  +			local _, op, val = words(2)
           74  +			if op == nil then
           75  +				local toc = {kind='toc'}
           76  +				ctx:insert(toc)
           77  +				-- same deal here -- directives are processed as part of
           78  +				-- the parse job, which is forked off the document job,
           79  +				-- so we need to climb the jobstack
           80  +				job:unwind(1).state.toc_custom_position = true
           81  +				job:hook('ext_toc_position', ctx, toc)
           82  +			else
           83  +				ctx:fail 'bad %toc directive'
           84  +			end
           85  +		end;
           86  +	};
           87  +	render = {
           88  +		toc = {
           89  +			html = function(job, renderer, block, section)
           90  +				-- “tagproc” contains the functions that determine what kind
           91  +				-- of data our abstract tags will be transformed into. this
           92  +				-- is needed to that plain text, HTML, and HTML IR can be
           93  +				-- produced from the same functions just by varying the
           94  +				-- proc set.
           95  +				-- 
           96  +				-- “astproc” contains the functions that determine what form
           97  +				-- our span arrays (and blocks, but not relevant here) will
           98  +				-- be transformed into, and is analogous to “tagproc”
           99  +				local tag = renderer.tagproc.tag;
          100  +				local elt = renderer.tagproc.elt;
          101  +				local catenate = renderer.tagproc.catenate;
          102  +				local sr = renderer.astproc.span_renderers;
          103  +				local getSafeID = renderer.state.obj_htmlid;
          104  +				
          105  +				-- toplevel HTML IR
          106  +				local lst = {tag = 'ol', attrs={class='toc'}, nodes={}}
          107  +
          108  +				-- "renderer.state" contains the stateglob of the renderer
          109  +				-- itself, not to be confused with the "state" parameter
          110  +				-- which contains this extension's share of the job state
          111  +				-- we use it to activate the stylesets we injected earlier
          112  +				renderer.state.stylesets_active.toc = true
          113  +				if renderer.state.opts['width'] then
          114  +					renderer.state.stylesets_active.tocFixed = true
          115  +				end
          116  +
          117  +				-- assemble a tree of links from the document section
          118  +				-- structure. this is tricky, because we need a tree, but
          119  +				-- all we have is a flat list with depth values attached to
          120  +				-- each node.
          121  +				local stack = {lst}
          122  +				local top = function() return stack[#stack] end
          123  +				-- job.doc is the document the render job is bound to, and
          124  +				-- its secorder field is a list of all the doc's sections in
          125  +				-- the order they occur ("doc.sections" is a hashmap from name
          126  +				-- to section object)
          127  +				local all = job.doc.secorder
          128  +
          129  +				for i, sec in ipairs(all) do
          130  +					if sec.heading_node then -- does this section have a label?
          131  +						local ent = tag('li',nil,
          132  +							 catenate{tag('a', {href='#'..getSafeID(sec)},
          133  +								sr.htmlSpan(sec.heading_node.spans, sec.heading_node, sec))})
          134  +						if sec.depth > #stack then
          135  +							local n = {tag = 'ol', attrs={}, nodes={ent}}
          136  +							table.insert(top().nodes[#top().nodes].nodes, n)
          137  +							table.insert(stack, n)
          138  +						else
          139  +							if sec.depth < #stack then
          140  +								for j=#stack,sec.depth+1,-1 do stack[j] = nil end
          141  +							end
          142  +							table.insert(top().nodes, ent)
          143  +						end
          144  +
          145  +						-- now we need to assemble a list of items within the
          146  +						-- section worthy of an entry on their own. currently
          147  +						-- this is only anchors created with %toc mark|name
          148  +						local innerlinks = {}
          149  +						local noteworthy = { anchor = true }
          150  +						for j, block in pairs(sec.blocks) do
          151  +							if noteworthy[block.kind] then
          152  +								local label = ss.coalesce(block._toc_label, block.label, block.spans)
          153  +								if label then
          154  +									table.insert(innerlinks, {
          155  +										id = renderer.state.obj_htmlid(block);
          156  +										label = label;
          157  +										block = block;
          158  +									})
          159  +								end
          160  +							end
          161  +						end
          162  +
          163  +						if next(innerlinks) then
          164  +							local n = {tag = 'ol', attrs = {}, nodes = {}}
          165  +							for i, l in ipairs(innerlinks) do
          166  +								local nn = {
          167  +									tag = 'a';
          168  +									attrs = {href = '#' .. l.id};
          169  +									nodes = {sr.htmlSpan(l.label, l.block, sec)};
          170  +								}
          171  +								table.insert(n.nodes, {tag = 'li', attrs = {}, nodes={nn}})
          172  +							end
          173  +							table.insert(ent.nodes, n)
          174  +						end
          175  +            print(ss.dump(ent))
          176  +					end
          177  +				end
          178  +				return lst
          179  +			end;
    10    180   
    11         -	end;
          181  +			[true] = function() end; -- fallback // convert to different node types
          182  +		};
          183  +	};
    12    184   }