size: 11 KiB

1-- custom writer for pandoc
2
3local unpack = unpack or table.unpack
4local format = string.format
5local layout = pandoc.layout
6local literal, empty, cr, concat, blankline, chomp, space, cblock, rblock,
7 prefixed, nest, hang, nowrap =
8 layout.literal, layout.empty, layout.cr, layout.concat, layout.blankline,
9 layout.chomp, layout.space, layout.cblock, layout.rblock,
10 layout.prefixed, layout.nest, layout.hang, layout.nowrap
11local to_roman = pandoc.utils.to_roman_numeral
12
13local footnotes = {}
14
15-- Escape special characters
16local function escape(s)
17 return (s:gsub("[][\\`{}_*<>~^'\"]", function(s) return "\\" .. s end))
18end
19
20local format_number = {}
21format_number.Decimal = function(n)
22 return format("%d", n)
23end
24format_number.Example = format_number.Decimal
25format_number.DefaultStyle = format_number.Decimal
26format_number.LowerAlpha = function(n)
27 return string.char(96 + (n % 26))
28end
29format_number.UpperAlpha = function(n)
30 return string.char(64 + (n % 26))
31end
32format_number.UpperRoman = function(n)
33 return to_roman(n)
34end
35format_number.LowerRoman = function(n)
36 return string.lower(to_roman(n))
37end
38
39local function is_tight_list(el)
40 if not (el.tag == "BulletList" or el.tag == "OrderedList" or
41 el.tag == "DefinitionList") then
42 return false
43 end
44 for i=1,#el.content do
45 if #el.content[i] == 1 and el.content[i][1].tag == "Plain" then
46 -- no change
47 elseif #el.content[i] == 2 and el.content[i][1].tag == "Plain" and
48 el.content[i][2].tag:match("List") then
49 -- no change
50 else
51 return false
52 end
53 end
54 return true
55end
56
57local function has_attributes(el)
58 return el.attr and
59 (#el.attr.identifier > 0 or #el.attr.classes > 0 or #el.attr.attributes > 0)
60end
61
62local function render_attributes(el, isblock)
63 if not has_attributes(el) then
64 return empty
65 end
66 local attr = el.attr
67 local buff = {"{"}
68 if #attr.identifier > 0 then
69 buff[#buff + 1] = "#" .. attr.identifier
70 end
71 for i=1,#attr.classes do
72 if #buff > 1 then
73 buff[#buff + 1] = space
74 end
75 buff[#buff + 1] = "." .. attr.classes[i]
76 end
77 for k,v in pairs(attr.attributes) do
78 if #buff > 1 then
79 buff[#buff + 1] = space
80 end
81 buff[#buff + 1] = k .. '="' .. v:gsub('"', '\\"') .. '"'
82 end
83 buff[#buff + 1] = "}"
84 if isblock then
85 return rblock(nowrap(concat(buff)), PANDOC_WRITER_OPTIONS.columns)
86 else
87 return concat(buff)
88 end
89end
90
91Blocks = {}
92Blocks.mt = {}
93Blocks.mt.__index = function(tbl,key)
94 return function() io.stderr:write("Unimplemented " .. key .. "\n") end
95end
96setmetatable(Blocks, Blocks.mt)
97
98Inlines = {}
99Inlines.mt = {}
100Inlines.mt.__index = function(tbl,key)
101 return function() io.stderr:write("Unimplemented " .. key .. "\n") end
102end
103setmetatable(Inlines, Inlines.mt)
104
105local function inlines(ils)
106 local buff = {}
107 for i=1,#ils do
108 local el = ils[i]
109 buff[#buff + 1] = Inlines[el.tag](el)
110 end
111 return concat(buff)
112end
113
114local function blocks(bs, sep)
115 local dbuff = {}
116 for i=1,#bs do
117 local el = bs[i]
118 dbuff[#dbuff + 1] = Blocks[el.tag](el)
119 end
120 return concat(dbuff, sep)
121end
122
123Blocks.Para = function(el)
124 return inlines(el.content)
125end
126
127Blocks.Plain = function(el)
128 return inlines(el.content)
129end
130
131Blocks.BlockQuote = function(el)
132 return prefixed(nest(blocks(el.content, blankline), 1), ">")
133end
134
135Blocks.Header = function(el)
136 local attr = render_attributes(el, true)
137 local result = {attr, cr, (string.rep("#", el.level)), space, inlines(el.content)}
138 return concat(result)
139end
140
141Blocks.Div = function(el)
142 if el.classes:includes("section") then
143 -- sections are implicit in djot
144 if el.identifier and el.content[1].t == "Header" and
145 el.content[1].identifier == "" then
146 el.content[1].identifier = el.identifier
147 end
148 return blocks(el.content, blankline)
149 else
150 local attr = render_attributes(el, true)
151 return concat{attr, cr, ":::", cr, blocks(el.content, blankline), cr, ":::"}
152 end
153end
154
155Blocks.RawBlock = function(el)
156 if el.format == "djot" then
157 return concat{el.text, cr}
158 else
159 local ticks = 3
160 el.text:gsub("(`+)", function(s) if #s >= ticks then ticks = #s + 1 end end)
161 local fence = string.rep("`", ticks)
162 return concat{fence, " =" .. el.format, cr,
163 el.text, cr, fence, cr}
164 end
165end
166
167Blocks.Null = function(el)
168 return empty
169end
170
171Blocks.LineBlock = function(el)
172 local result = {}
173 for i=1,#el.content do
174 result[#result + 1] = inlines(el.content[i])
175 end
176 return concat(result, concat{"\\", cr})
177end
178
179Blocks.Table = function(el)
180 local attr = render_attributes(el, true)
181 local tbl = pandoc.utils.to_simple_table(el)
182 -- sanity check to make sure a pipe table will work:
183 for i=1,#tbl.rows do
184 for j=1,#tbl.rows[i] do
185 local cell = tbl.rows[i][j]
186 if not (#cell == 0 or
187 (#cell == 1 and (cell.tag == "Plain" or cell.tag == "Para"))) then
188 -- can't be pipe table, so return a code block with plain table
189 local plaintable = pandoc.write(pandoc.Pandoc({el}), "plain")
190 return Blocks.CodeBlock(pandoc.CodeBlock(plaintable))
191 end
192 end
193 end
194 local cellsep = " | "
195 local rows = {}
196 local hdrcells = {}
197 for j=1, #tbl.headers do
198 local cell = tbl.headers[j]
199 hdrcells[#hdrcells + 1] = blocks(cell, blankline)
200 end
201 if #hdrcells > 0 then
202 rows[#rows + 1] =
203 concat{"| ", concat(hdrcells, cellsep), " |", cr}
204 local bordercells = {}
205 for j=1, #hdrcells do
206 local w = layout.offset(hdrcells[j])
207 local lm, rm = "-", "-"
208 local align = tbl.aligns[j]
209 if align == "AlignLeft" or align == "AlignCenter" then
210 lm = ":"
211 end
212 if align == "AlignRight" or align == "AlignCenter" then
213 rm = ":"
214 end
215 bordercells[#bordercells + 1] = lm .. string.rep("-", w) .. rm
216 end
217 rows[#rows + 1] =
218 nowrap(concat{"|", concat(bordercells, "|"), "|", cr})
219 end
220 for i=1, #tbl.rows do
221 local cells = {}
222 local row = tbl.rows[i]
223 for j=1, #row do
224 local cell = row[j]
225 cells[#cells + 1] = blocks(cell, blankline)
226 end
227 rows[#rows + 1] =
228 nowrap(concat{"| ", concat(cells, cellsep), " |", cr})
229 end
230 local caption = empty
231 if #tbl.caption > 0 then
232 caption = concat{blankline, "^ ", inlines(tbl.caption), cr}
233 end
234 return concat{attr, concat(rows), caption}
235end
236
237Blocks.DefinitionList = function(el)
238 local result = {}
239 for i=1,#el.content do
240 local term , defs = unpack(el.content[i])
241 local inner = empty
242 for j=1,#defs do
243 inner = concat{inner, blankline, blocks(defs[j], blankline)}
244 end
245 result[#result + 1] =
246 hang(inner, 2, concat{ ":", space, inlines(term), cr })
247 end
248 return concat(result, blankline)
249end
250
251Blocks.BulletList = function(el)
252 local attr = render_attributes(el, true)
253 local result = {attr, cr}
254 for i=1,#el.content do
255 result[#result + 1] = hang(blocks(el.content[i], blankline), 2, concat{"-",space})
256 end
257 local sep = blankline
258 if is_tight_list(el) then
259 sep = cr
260 end
261 return concat(result, sep)
262end
263
264Blocks.OrderedList = function(el)
265 local attr = render_attributes(el, true)
266 local result = {attr, cr}
267 local num = el.start
268 local width = 3
269 local maxnum = num + #el.content
270 if maxnum > 9 then
271 width = 4
272 end
273 local delimfmt = "%s."
274 if el.delimiter == "OneParen" then
275 delimfmt = "%s)"
276 elseif el.delimiter == "TwoParens" then
277 delimfmt = "(%s)"
278 end
279 local sty = el.style
280 for i=1,#el.content do
281 local barenum = format_number[sty](num)
282 local numstr = format(delimfmt, barenum)
283 local sps = width - #numstr
284 local numsp
285 if sps < 1 then
286 numsp = space
287 else
288 numsp = string.rep(" ", sps)
289 end
290 result[#result + 1] = hang(blocks(el.content[i], blankline), width, concat{numstr,numsp})
291 num = num + 1
292 end
293 local sep = blankline
294 if is_tight_list(el) then
295 sep = cr
296 end
297 return concat(result, sep)
298end
299
300Blocks.CodeBlock = function(el)
301 local ticks = 3
302 el.text:gsub("(`+)", function(s) if #s >= ticks then ticks = #s + 1 end end)
303 local fence = string.rep("`", ticks)
304 local lang = empty
305 if #el.classes > 0 then
306 lang = " " .. el.classes[1]
307 table.remove(el.classes, 1)
308 end
309 local attr = render_attributes(el, true)
310 local result = { attr, cr, fence, lang, cr, el.text, cr, fence, cr }
311 return concat(result)
312end
313
314Blocks.HorizontalRule = function(el)
315 return cblock("* * * * *", PANDOC_WRITER_OPTIONS.columns)
316end
317
318Inlines.Str = function(el)
319 return escape(el.text)
320end
321
322Inlines.Space = function(el)
323 return space
324end
325
326Inlines.SoftBreak = function(el)
327 if PANDOC_WRITER_OPTIONS.wrap_text == "wrap-preserve" then
328 return cr
329 else
330 return space
331 end
332end
333
334Inlines.LineBreak = function(el)
335 return concat{ "\\", cr }
336end
337
338Inlines.RawInline = function(el)
339 if el.format == "djot" then
340 return el.text
341 else
342 return concat{Inlines.Code(el), "{=", el.format, "}"}
343 end
344end
345
346Inlines.Code = function(el)
347 local ticks = 0
348 el.text:gsub("(`+)", function(s) if #s > ticks then ticks = #s end end)
349 local use_spaces = el.text:match("^`") or el.text:match("`$")
350 local start = string.rep("`", ticks + 1) .. (use_spaces and " " or "")
351 local finish = (use_spaces and " " or "") .. string.rep("`", ticks + 1)
352 local attr = render_attributes(el)
353 local result = { start, el.text, finish, attr }
354 return concat(result)
355end
356
357Inlines.Emph = function(el)
358 return concat{ "_", inlines(el.content), "_" }
359end
360
361Inlines.Strong = function(el)
362 return concat{ "*", inlines(el.content), "*" }
363end
364
365Inlines.Strikeout = function(el)
366 return concat{ "{-", inlines(el.content), "-}"}
367end
368
369Inlines.Subscript = function(el)
370 return concat{ "{~", inlines(el.content), "~}"}
371end
372
373Inlines.Superscript = function(el)
374 return concat{ "{^", inlines(el.content), "^}"}
375end
376
377Inlines.SmallCaps = function(el)
378 return concat{ "[", inlines(el.content), "]{.smallcaps}"}
379end
380
381Inlines.Underline = function(el)
382 return concat{ "[", inlines(el.content), "]{.underline}"}
383end
384
385Inlines.Cite = function(el)
386 return inlines(el.content)
387end
388
389Inlines.Math = function(el)
390 local marker
391 if el.mathtype == "DisplayMath" then
392 marker = "$$"
393 else
394 marker = "$"
395 end
396 return concat{ marker, Inlines.Code(el) }
397end
398
399Inlines.Span = function(el)
400 local attr = render_attributes(el)
401 return concat{"[", inlines(el.content), "]", attr}
402end
403
404Inlines.Link = function(el)
405 if el.title and #el.title > 0 then
406 el.attributes.title = el.title
407 el.title = nil
408 end
409 local attr = render_attributes(el)
410 local result = {"[", inlines(el.content), "](",
411 el.target, ")", attr}
412 return concat(result)
413end
414
415Inlines.Image = function(el)
416 if el.title and #el.title > 0 then
417 el.attributes.title = el.title
418 el.title = nil
419 end
420 local attr = render_attributes(el)
421 local result = {"![", inlines(el.caption), "](",
422 el.src, ")", attr}
423 return concat(result)
424end
425
426Inlines.Quoted = function(el)
427 if el.quotetype == "DoubleQuote" then
428 return concat{'"', inlines(el.content), '"'}
429 else
430 return concat{"'", inlines(el.content), "'"}
431 end
432end
433
434Inlines.Note = function(el)
435 footnotes[#footnotes + 1] = el.content
436 local num = #footnotes
437 return literal(format("[^%d]", num))
438end
439
440function Writer (doc, opts)
441 PANDOC_WRITER_OPTIONS = opts
442 local d = blocks(doc.blocks, blankline)
443 local notes = {}
444 for i=1,#footnotes do
445 local note = hang(blocks(footnotes[i], blankline), 4, concat{format("[^%d]:",i),space})
446 table.insert(notes, note)
447 end
448 local formatted = concat{d, blankline, concat(notes, blankline)}
449 if PANDOC_WRITER_OPTIONS.wrap_text == "wrap-none" then
450 return layout.render(formatted)
451 else
452 return layout.render(formatted, opts.columns)
453 end
454end