Differences From
Artifact [a43bfa19e3]:
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