cortav  Diff

Differences From Artifact [a43bfa19e3]:

To Artifact [ab8e0fd21b]:


    18     18   end
    19     19   local lines = function(...)
    20     20   	local s = ss.strac()
    21     21   	for _, v in pairs{...} do s(v) end
    22     22   	return s
    23     23   end
    24     24   
    25         -function ct.render.groff(doc, opts)
           25  +local function gsan(str)
           26  +	local tocodepoint = function(ch)
           27  +		return string.format('\\[u%04X]', utf8.codepoint(ch))
           28  +	end
           29  +	str = str:gsub('(["\'\\])',tocodepoint)
           30  +	return str
           31  +end
           32  +
           33  +local gtxt = ss.declare {
           34  +	ident = 'groff-text';
           35  +	mk = function() return {
           36  +		lines = {};
           37  +	} end;
           38  +	fns = {
           39  +		raw = function(me, text)
           40  +			if me.linbuf == nil then
           41  +				me.linbuf = ss.strac()
           42  +			end
           43  +			me.linbuf(text)
           44  +		end;
           45  +		txt = function(me, str, ...)
           46  +			if str == nil then return end
           47  +			me:raw(gsan(str))
           48  +			-- WARN this will cause problems if str is ever allowed to
           49  +			-- include a line break. we can sanitize by converting
           50  +			-- every line break into a new entry in the table, but i
           51  +			-- don't think it should be possible for a \n to reach us
           52  +			-- at this point, so i'm omitting the safety check as it
           53  +			-- would involve an excessive hit to performance
           54  +			me:txt(...)
           55  +		end;
           56  +		brk = function(me)
           57  +			me:flush()
           58  +			table.insert(me.lines, '')
           59  +		end;
           60  +		line = function(me, ...)
           61  +			me:flush()
           62  +			me:txt(...)
           63  +		end;
           64  +		req = function(me, r)
           65  +			me:flush()
           66  +			table.insert(me.lines, '.'..r)
           67  +		end;
           68  +		esc = function(me, e)
           69  +			me:raw('\\' .. e)
           70  +		end;
           71  +		flush = function(me)
           72  +			if me.linbuf ~= nil then
           73  +				local line = me.linbuf:compile()
           74  +				local first = line:sub(1,1)
           75  +				-- make sure our lines aren't accidentally interpreted
           76  +				-- as groff requests. groff is kinda hostile to script
           77  +				-- generation, huh?
           78  +				if first == '.' or first == "'" then
           79  +					line = '\\&' ..line
           80  +				end
           81  +				table.insert(me.lines, line)
           82  +				me.linbuf = nil
           83  +			end
           84  +		end;
           85  +		compile = function(me)
           86  +			me:flush()
           87  +			return table.concat(me.lines, '\n')
           88  +		end;
           89  +	}
           90  +}
           91  +
           92  +local function mkColorDef(name, color)
           93  +	return '.defcolor '..name..' rgb ' ..
           94  +		table.concat({color:rgb_t()}, ' ', 1, 3)
           95  +end
           96  +
           97  +local function addAccentTones(rs,hue,spread)
           98  +	local base = ss.color(hue, 1, .5)
           99  +	local right = spread > 0 and ss.color(hue + spread, 1, .5)
          100  +		or ss.color(hue, 0.4, 0.6)
          101  +	local left = spread > 0 and ss.color(hue - spread, 1, .5)
          102  +		or ss.color(hue, 1, 0.3)
          103  +
          104  +	local steps = 6
          105  +	for i=-3,3 do
          106  +		local nc, nm
          107  +		local o if i > 0
          108  +			then o = right nm = 'R'
          109  +			else o = left  nm = 'L'
          110  +		end
          111  +		nc = base + o:alt('alpha', math.abs(i) / 3)
          112  +		rs.addColor('accent'..nm..tostring(math.abs(i)),nc)
          113  +	end
          114  +end
          115  +local function mkrc()
          116  +	return {
          117  +		clone = function(self, origin)
          118  +			return {
          119  +				origin = origin;
          120  +				clone = self.clone;
          121  +				prop = ss.clone(self.prop);
          122  +				mk = self.mk;
          123  +				add = self.add;
          124  +				block = self.block;
          125  +				blocks = self.blocks;
          126  +				span = self.span;
          127  +				spans = self.spans;
          128  +			}
          129  +		end;
          130  +		blocks = {};
          131  +		prop = {};
          132  +		block = function(self)
          133  +			local sub = self:clone()
          134  +			sub.spans = {}
          135  +			sub.blocks = nil
          136  +			sub.span = function(me, ln)
          137  +				local p = ss.clone(me.prop)
          138  +				p.txt = ln
          139  +				p.block = sub
          140  +				p.origin = me.origin
          141  +				table.insert(me.spans, p)
          142  +				return p
          143  +			end;
          144  +			table.insert(self.blocks, sub)
          145  +			return sub
          146  +		end;
          147  +	}
          148  +end
          149  +
          150  +function ct.render.groff(doc, opts, setup)
    26    151   	-- rs contains state specific to this render job
    27    152   	-- that modules will need access to
          153  +	local fail = function(msg, ...)
          154  +		ct.exns.rdr(msg, 'groff', ...):throw()
          155  +	end
    28    156   	local rs = {};
    29    157   	rs.macsets = {
    30    158   		strike = {
    31    159   			'.de ST';
    32    160   			[[.nr ww \w'\\$1']];
    33         -			[[\Z@\v'-.25m'\l'\\n[ww]u'@\\$1']];
          161  +			[[\Z@\v'-.25m'\l'\\n[ww]u'@\\$1]];
          162  +			'..';
          163  +		};
          164  +		color = {'.color'};
          165  +		insert = {};
          166  +		footnote = {
          167  +			'.de footnote-blank';
          168  +			'.  sp 0.25m';
          169  +			'..';
          170  +			'.ev footnote-env';
          171  +			'.  ps 8p';
          172  +			'.  in 0.5c';
          173  +			'.  blm footnote-blank';
          174  +			'.ev';
          175  +			'.de footnote-print';
          176  +-- 			'.  sp |\\\\n[.p]u-\\\\n[footnote-pos]u';
          177  +			'.  sp 0.5c';
          178  +			'.  ev footnote-env';
          179  +			'.    fn';
          180  +			'.  ev';
          181  +			'.  rm fn';
          182  +			'.  nr footnote-pos 0';
          183  +			-- move the trap past the bottom of the page so it's not
          184  +			-- invoked again until more footnotes have been assembled
          185  +			'.  ch footnote-print |\\\\n[.p]u+10';
          186  +			'.  bp';
          187  +			'..';
          188  +			'.wh |\\n[.p]u footnote-print';
          189  +		};
          190  +		root = {
          191  +		-- these are macros included in all documents
          192  +		-- page offset is hideously broken and unusable; we
          193  +		-- zero it out so we can use .in to control indents
          194  +		-- instead. note that the upshot of this is we need
          195  +		-- to manually specify the indent in every other
          196  +		-- environment from now on, .evc doesn't seem to cut it
          197  +		-- set up the page title environment & trap
          198  +			"'in 2c";
          199  +			"'ll 18c";
          200  +			"'po 0";
          201  +			"'ps 13p";
          202  +			"'vs 15p";
          203  +			".ev pgti";
          204  +			".  evc 0";
          205  +			".  fam H";
          206  +			".  ps 10pt";
          207  +			".ev";
          208  +			'.de ph';
          209  +			'.  sp 0.6c';
          210  +			'.  ev pgti';
          211  +			'.  po 1c';
          212  +			'.  lt 19c';
          213  +			".  tl '\\\\*[doctitle]'\\fB\\\\*[title]\\f[]'%'";
          214  +			'.  po 0';
          215  +			".  br";
          216  +			'.  ev';
          217  +			'.  sp 1.2c';
          218  +			'..';
          219  +			'.wh 0 ph';
          220  +			'.de np';
          221  +			'.  sp 0.2c';
    34    222   			'..';
          223  +			'.blm np'
          224  +
    35    225   		};
    36    226   	}
    37    227   	rs.macsNeeded = {
    38    228   		order = {};
          229  +		map = {};
    39    230   		count = 0;
          231  +		deps = {
          232  +			insert = {'color'};
          233  +			strike = {'color'};
          234  +		};
    40    235   	}
          236  +	rs.linkctr = 0
          237  +
    41    238   	function rs.macAdd(id)
    42         -		if rs.macsets[id] then
    43         -			rs.macsNeeded.count = macsNeeded.count + 1
          239  +		if rs.macsets[id] and not rs.macsNeeded.map[id] then
          240  +			rs.macsNeeded.count = rs.macsNeeded.count + 1
    44    241   			rs.macsNeeded.order[rs.macsNeeded.count] = id
          242  +			rs.macsNeeded.map[id] = true
          243  +			if not rs.macsNeeded.deps[id] then
          244  +				return true
          245  +			end
          246  +
          247  +			for k,v in pairs(rs.macsNeeded.deps[id]) do
          248  +				if not rs.macsNeeded.map[v] then
          249  +					rs.macAdd(v)
          250  +				end
          251  +			end
          252  +
    45    253   			return true
    46    254   		else return false end
    47    255   	end
          256  +
          257  +	rs.macAdd 'root'
          258  +
          259  +	rs.colors = {}
          260  +	rs.addColor = function(name,color)
          261  +		if not ss.color.is(color) then
          262  +			ss.bug('%s is not a color value', color):throw()
          263  +		end
          264  +		rs.colors[name] = color
          265  +	end
          266  +
          267  +	if opts.accent then
          268  +		addAccentTones(rs, tonumber(opts.accent), tonumber(opts['hue-spread']) or 0)
          269  +		rs.addColor('new', rs.colors.accentR3)
          270  +		rs.addColor('del', rs.colors.accentL3)
          271  +	else
          272  +		rs.addColor('new', ss.color(80, 1, .3))
          273  +		rs.addColor('del', ss.color(0, 1, .3))
          274  +	end
          275  +
          276  +	doc.stage = {
          277  +		type = 'render';
          278  +		format = 'groff';
          279  +		groff_render_state = rs;
          280  +	}
          281  +
          282  +	setup(doc.stage)
    48    283   	local job = doc:job('render_groff',nil,rs)
          284  +
          285  +	local function collect(rc, spans, b, s)
          286  +		local rcc = rc:clone()
          287  +		rcc.spans = {}
          288  +		rs.renderSpans(rcc, spans, b, s)
          289  +		return rcc.spans
          290  +	end
          291  +	local function collectText(...)
          292  +		local text = collect(...)
          293  +		local s = ss.strac()
          294  +		for i, l in ipairs(text) do
          295  +			s(l.txt)
          296  +		end
          297  +		return s
          298  +	end
          299  +
    49    300   
    50    301   	-- the way this module works is we build up a table for each block
    51    302   	-- of individual strings paired with attributes that say how they
    52    303   	-- should be rendered. we then iterate over the table, applying
    53    304   	-- formats as need be, and inserting blanks after each block
          305  +
          306  +
    54    307   
    55    308   	local spanRenderers = {}
    56    309   	function spanRenderers.format(rc, s, b, sec)
    57    310   		local rcc = rc:clone()
    58    311   		if s.style == 'strong' then
    59    312   			rcc.prop.bold = true
    60    313   		elseif s.style == 'emph' then
    61    314   			rcc.prop.emph = true
    62    315   		elseif s.style == 'strike' then
    63    316   			rcc.prop.strike = true
    64    317   			rs.macAdd 'strike'
          318  +			rcc.prop.color = 'del'
    65    319   		elseif s.style == 'insert' then
          320  +			rs.macAdd 'insert'
          321  +			rcc.prop.color = 'new'
    66    322   		end
    67    323   		rs.renderSpans(rcc, s.spans, b, sec)
    68    324   	end;
          325  +
          326  +	function spanRenderers.link(rc, l, b, sec)
          327  +		rs.renderSpans(rc, l.spans, b, sec)
          328  +		rs.linkctr = rs.linkctr + 1
          329  +		rs.macAdd 'footnote'
          330  +		local p = rc:span(string.format('[%u]', rs.linkctr))
          331  +		if type(l.ref) == 'string' then
          332  +			local t = ''
          333  +			if b.origin.doc.sections[l.ref] then
          334  +				local hn = b.origin.doc.sections[l.ref].heading_node
          335  +				if hn then
          336  +					t = collectText(rc, hn.spans, b, sec):compile()
          337  +				end
          338  +			else
          339  +				local obj = l.origin:ref(l.ref)
          340  +				if type(obj) == 'string' then
          341  +					t = l.origin:ref(l.ref)
          342  +				end
          343  +			end
          344  +			p.div = { fn = tostring(rs.linkctr) .. ') ' .. t }
          345  +		end
          346  +	end;
          347  +
          348  +	function spanRenderers.raw(rc, s, b, sec)
          349  +		rs.renderSpans(rc, s.spans, b, sec)
          350  +	end;
          351  +
          352  +	function spanRenderers.var(rc,v,b,s)
          353  +		local t, raw = ct.expand_var(v)
          354  +		if raw then rc:span(t) else
          355  +			rs.renderSpans(rc,t,b,s)
          356  +		end
          357  +	end
          358  +	function spanRenderers.macro(rc, m,b,s)
          359  +		local macroname = collectText(rc,
          360  +			ct.parse_span(m.macro, b.origin),
          361  +			b, s):compile()
          362  +
          363  +		local r = b.origin:ref(macroname)
          364  +		if type(r) ~= 'string' then
          365  +			b.origin:fail('%s is an object, not a reference', t.ref)
          366  +		end
          367  +		local mctx = b.origin:clone()
          368  +		      mctx.invocation = m
          369  +		rs.renderSpans(rc, ct.parse_span(r, mctx))
          370  +	end
    69    371   
    70    372   	function rs.renderSpans(rc, sp, b, sec)
          373  +		rc = rc or mkrc(b.origin)
    71    374   		for i, v in ipairs(sp) do
    72    375   			if type(v) == 'string' then
    73         -				rc:add(v)
          376  +				rc:span(v)
    74    377   			elseif spanRenderers[v.kind] then
    75    378   				spanRenderers[v.kind](rc, v, b, sec)
    76    379   			end
    77    380   		end
    78    381   	end
    79    382   
    80    383   	local blockRenderers = {}
          384  +	function	blockRenderers.label(rc, b, sec)
          385  +		if ct.sec.is(b.captions) then
          386  +			local sizes = {36,24,12,8,4,2}
          387  +			local margins = {0,5,2,1,0.5}
          388  +			local dedents = {2.5,1.3,0.8,0.4}
          389  +			rc.prop.dsz = sizes[b.captions.depth] or 10
          390  +			rc.prop.underline = b.captions.depth < 4
          391  +			rc.prop.bold = b.captions.depth > 3
          392  +			rc.prop.margin = {
          393  +				top = margins[b.captions.depth] or 0;
          394  +				bottom = 0.1;
          395  +			}
          396  +			rc.prop.indent = -(dedents[b.captions.depth] or 0)
          397  +			rc.prop.underline = true
          398  +			rc.prop.chtitle = collectText(rc, b.spans, b.spec):compile()
          399  +			if b.captions.depth == 1 then
          400  +				rc.prop.breakBefore = true
          401  +			end
          402  +			rs.renderSpans(rc, b.spans, b, sec)
          403  +		else
          404  +			ss.bug 'tried to render label for an unknown object type':throw()
          405  +		end
          406  +	end
    81    407   	function	blockRenderers.paragraph(rc, b, sec)
    82    408   		rs.renderSpans(rc, b.spans, b, sec)
    83    409   	end
    84         -	function rs.renderBlock(b, sec)
    85         -		local rc = {
    86         -			clone = function(self)
    87         -				return {
    88         -					clone = self.clone;
    89         -					lines = self.lines;
    90         -					prop = ss.clone(self.prop);
    91         -					mk = self.mk;
    92         -					add = self.add;
    93         -				}
    94         -			end;
    95         -			lines = {};
    96         -			prop = {};
    97         -			mk = function(self, ln)
    98         -				local p = ss.clone(self.prop)
    99         -				p.txt = ln
   100         -				return p
          410  +	function rs.renderBlock(rc, b, sec, outerBlockRenderContext)
          411  +		if blockRenderers[b.kind] then
          412  +			local rcc = rc:block()
          413  +			blockRenderers[b.kind](rcc, b, sec)
          414  +		end
          415  +	end
          416  +
          417  +	rs.sanitize = gsan
          418  +
          419  +	local skippedFirstPagebreak = doc.secorder[1]:visible()
          420  +	local deferrer = ss.declare {
          421  +		ident = 'groff-deferrer';
          422  +		mk = function(buf) return {ops={}, tgt=buf} end;
          423  +		fns = {
          424  +			esc = function(me, str) table.insert(me.ops, {0, str}) end;
          425  +			req = function(me, str) table.insert(me.ops, {1, str}) end;
          426  +			flush = function(me)
          427  +				for i=#me.ops,1,-1 do
          428  +					local d = me.ops[i]
          429  +					if d[1] == 0 then
          430  +						me.tgt:esc(d[2])
          431  +					elseif d[1] == 1 then
          432  +						me.tgt:req(d[2])
          433  +					end
          434  +				end
          435  +				me.ops = {}
   101    436   			end;
   102         -			add = function(self, ln)
   103         -				table.insert(self.lines, self:mk(ln))
   104         -			end;
   105         -		}
   106         -		if blockRenderers[b.kind] then
   107         -			blockRenderers[b.kind](rc, b, sec)
          437  +		};
          438  +	}
          439  +	function rs.emitSpan(gtxt, s)
          440  +		local defer = deferrer(gtxt)
          441  +		if s.bold or s.emph then
          442  +			if s.bold and s.emph then
          443  +				gtxt:esc 'f(BI'
          444  +			elseif s.bold then
          445  +				gtxt:esc 'fB'
          446  +			elseif s.emph then
          447  +				gtxt:esc 'fI'
          448  +			end
          449  +			defer:esc'f[]'
   108    450   		end
   109         -		return rc.lines
          451  +
          452  +		if s.color and opts.color then
          453  +			gtxt:esc('m[' .. s.color .. ']')
          454  +			defer:esc('m[]')
          455  +		end
          456  +		if s.strike then
          457  +			gtxt:req('ST "'..s.txt..'"')
          458  +		else
          459  +			gtxt:txt(s.txt)
          460  +		end
          461  +		defer:flush()
          462  +		if s.div then
          463  +			for div, body in pairs(s.div) do
          464  +				if div == 'fn' then
          465  +					gtxt:req 'ev footnote-env'
          466  +				end
          467  +				gtxt:req('boxa '..div)
          468  +				gtxt:txt(body)
          469  +				gtxt:raw '\n'
          470  +				gtxt:req 'boxa'
          471  +				if div == 'fn' then
          472  +					gtxt:req 'ev'
          473  +					gtxt:req 'nr footnote-pos (\\n[footnote-pos]u+\\n[dn]u)'
          474  +					gtxt:req 'ch footnote-print -(\\n[footnote-pos]u+1c)'
          475  +				end
          476  +			end
          477  +		end
   110    478   	end
          479  +	function rs.emitBlock(gtxt, b)
          480  +		local didfinalbreak = false
          481  +		local defer = deferrer(gtxt)
          482  +		local ln = b.prop
          483  +		if ln.chtitle then
          484  +			gtxt:req('ds title '..ln.chtitle)
          485  +		end
          486  +		if ln.breakBefore then
          487  +			if skippedFirstPagebreak then
          488  +				gtxt:req 'bp'
          489  +			else
          490  +				skippedFirstPagebreak = true
          491  +			end
          492  +		end
          493  +		if ln.indent then
          494  +			if ln.indent < 0 then
          495  +				gtxt:req('in '..tostring(ln.indent)..'m')
          496  +				defer:req 'in'
          497  +				gtxt:req('ll +'..tostring(-ln.indent)..'m')
          498  +				defer:req 'll'
          499  +			else
          500  +				gtxt:req('in +'..tostring(ln.indent)..'m')
          501  +				defer:req 'in'
          502  +			end
          503  +			defer:req 'br'
          504  +		end
          505  +		if ln.margin then
          506  +			if ln.margin.top then
          507  +				gtxt:req(string.format('sp %sm', ln.margin.top))
          508  +			end
          509  +		end
          510  +
          511  +		if ln.underline then
          512  +			defer:esc("D'l \\n[.ll]u-\\n[.in]u 0'")
          513  +			defer:esc"v'-0.5'"
          514  +			defer:req'br'
          515  +		end
          516  +
          517  +		if ln.dsz and ln.dsz > 0 then
          518  +			gtxt:req('ps +' .. tostring(ln.dsz) .. 'p')
          519  +			defer:req('ps -' .. tostring(ln.dsz) .. 'p')
          520  +		elseif ln.sz or ln.dsz then
          521  +			if ln.sz and ln.sz <= 0 then
          522  +				ln.origin:fail 'font sizes must be greater than 0'
          523  +			end
          524  +			gtxt:req('ps ' .. tostring(ln.sz or ln.dsz) ..'p')
          525  +			if ln.dsz then
          526  +				defer:req('ps +' .. tostring(0 - ln.dsz) .. 'p')
          527  +			else
          528  +				defer:req'ps'
          529  +			end
          530  +		end
          531  +
          532  +		for i,s in pairs(b.spans) do
          533  +			rs.emitSpan(gtxt, s)
          534  +		end
          535  +
   111    536   
   112         -	function rs.emitLine(ln)
   113         -		local q = ss.strac()
   114         -		if ln.dsz then
   115         -			q('\\ps +' .. tostring(ln.dsz))
   116         -		elseif ln.sz then
   117         -			q('\\ps ' .. tostring(ln.dsz))
          537  +		if ln.margin then
          538  +			if ln.margin.bottom then
          539  +				gtxt:req(string.format('sp %sm', ln.margin.bottom))
          540  +			end
   118    541   		end
   119    542   
   120         -		if ln.bold and ln.emph then
   121         -			q '\\f(BI'
   122         -		elseif ln.bold then
   123         -			q '\\fB'
   124         -		elseif ln.emph then
   125         -			q '\\fI'
   126         -		end
          543  +		defer:flush()
   127    544   
   128         -
   129         -		q(ln.txt)
   130         -
   131         -		if ln.bold or ln.emph then
   132         -			q'\\f[]'
   133         -		end
   134         -
   135         -		if ln.dsz then
   136         -			q('.ps -' .. tostring(ln.dsz))
   137         -		elseif ln.sz then
   138         -			q '.ps'
   139         -		end
   140         -		return q
          545  +		if not ln.margin then gtxt:brk() end
   141    546   	end
   142    547   
   143    548   	local ir = {}
   144    549   	for i, sec in ipairs(doc.secorder) do
   145    550   		if sec.kind == 'ordinary' then
   146         -			local blks = {}
          551  +			local rc = mkrc()
   147    552   			for j, b in ipairs(sec.blocks) do
   148         -				local r = rs.renderBlock(b, sec)
   149         -				if r then table.insert(blks, r) end
          553  +				rs.renderBlock(rc, b, sec)
   150    554   			end
   151         -			table.insert(ir, blks)
          555  +			table.insert(ir, {blocks = rc.blocks, src = sec})
   152    556   		end
   153    557   	end
   154    558   
   155         -	local rd = ss.strac()
          559  +	local gd = gtxt()
   156    560   	for i, s in ipairs(ir) do
   157         -		for j, b in ipairs(s) do
   158         -			for z, l in ipairs(b) do
   159         -				rd(rs.emitLine(l))
   160         -			end
   161         -			rd'\n'
          561  +		for j, b in ipairs(s.blocks) do
          562  +			rs.emitBlock(gd,b)
   162    563   		end
   163    564   	end
   164    565   
   165    566   	local macs = ss.strac()
   166    567   	for _, m in pairs(rs.macsNeeded.order) do
   167         -		for _, ln in pairs(m) do macs(ln) end
          568  +		for _,ln in pairs(rs.macsets[m]) do macs(ln) end
          569  +	end
          570  +	if rs.macsNeeded.map.color and opts.color then
          571  +		for k,v in pairs(rs.colors) do
          572  +			macs(mkColorDef(k,v))
          573  +		end
          574  +	end
          575  +
          576  +	local doctitle = '' if opts.title then
          577  +		doctitle = opts.title
          578  +	else
          579  +		local top = math.huge
          580  +		for i,s in ipairs(doc.secorder) do
          581  +			if s.heading_node and s.depth < top then
          582  +				top = s.depth
          583  +				doctitle = collectText(mkrc():block(), s.heading_node.spans, s.heading_node, s):compile()
          584  +			end
          585  +		end
   168    586   	end
   169         -	return macs:compile'\n' .. rd:compile''
          587  +	macs('.ds doctitle '..doctitle)
          588  +
          589  +	return macs:compile'\n' .. '\n' .. gd:compile()
   170    590   end