size: 11 KiB
| 1 | -- custom writer for pandoc |
| 2 | |
| 3 | local unpack = unpack or table.unpack |
| 4 | local format = string.format |
| 5 | local layout = pandoc.layout |
| 6 | local 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 |
| 11 | local to_roman = pandoc.utils.to_roman_numeral |
| 12 | |
| 13 | local footnotes = {} |
| 14 | |
| 15 | -- Escape special characters |
| 16 | local function escape(s) |
| 17 | return (s:gsub("[][\\`{}_*<>~^'\"]", function(s) return "\\" .. s end)) |
| 18 | end |
| 19 | |
| 20 | local format_number = {} |
| 21 | format_number.Decimal = function(n) |
| 22 | return format("%d", n) |
| 23 | end |
| 24 | format_number.Example = format_number.Decimal |
| 25 | format_number.DefaultStyle = format_number.Decimal |
| 26 | format_number.LowerAlpha = function(n) |
| 27 | return string.char(96 + (n % 26)) |
| 28 | end |
| 29 | format_number.UpperAlpha = function(n) |
| 30 | return string.char(64 + (n % 26)) |
| 31 | end |
| 32 | format_number.UpperRoman = function(n) |
| 33 | return to_roman(n) |
| 34 | end |
| 35 | format_number.LowerRoman = function(n) |
| 36 | return string.lower(to_roman(n)) |
| 37 | end |
| 38 | |
| 39 | local 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 |
| 55 | end |
| 56 | |
| 57 | local function has_attributes(el) |
| 58 | return el.attr and |
| 59 | (#el.attr.identifier > 0 or #el.attr.classes > 0 or #el.attr.attributes > 0) |
| 60 | end |
| 61 | |
| 62 | local 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 |
| 89 | end |
| 90 | |
| 91 | Blocks = {} |
| 92 | Blocks.mt = {} |
| 93 | Blocks.mt.__index = function(tbl,key) |
| 94 | return function() io.stderr:write("Unimplemented " .. key .. "\n") end |
| 95 | end |
| 96 | setmetatable(Blocks, Blocks.mt) |
| 97 | |
| 98 | Inlines = {} |
| 99 | Inlines.mt = {} |
| 100 | Inlines.mt.__index = function(tbl,key) |
| 101 | return function() io.stderr:write("Unimplemented " .. key .. "\n") end |
| 102 | end |
| 103 | setmetatable(Inlines, Inlines.mt) |
| 104 | |
| 105 | local 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) |
| 112 | end |
| 113 | |
| 114 | local 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) |
| 121 | end |
| 122 | |
| 123 | Blocks.Para = function(el) |
| 124 | return inlines(el.content) |
| 125 | end |
| 126 | |
| 127 | Blocks.Plain = function(el) |
| 128 | return inlines(el.content) |
| 129 | end |
| 130 | |
| 131 | Blocks.BlockQuote = function(el) |
| 132 | return prefixed(nest(blocks(el.content, blankline), 1), ">") |
| 133 | end |
| 134 | |
| 135 | Blocks.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) |
| 139 | end |
| 140 | |
| 141 | Blocks.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 |
| 153 | end |
| 154 | |
| 155 | Blocks.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 |
| 165 | end |
| 166 | |
| 167 | Blocks.Null = function(el) |
| 168 | return empty |
| 169 | end |
| 170 | |
| 171 | Blocks.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}) |
| 177 | end |
| 178 | |
| 179 | Blocks.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} |
| 235 | end |
| 236 | |
| 237 | Blocks.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) |
| 249 | end |
| 250 | |
| 251 | Blocks.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) |
| 262 | end |
| 263 | |
| 264 | Blocks.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) |
| 298 | end |
| 299 | |
| 300 | Blocks.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) |
| 312 | end |
| 313 | |
| 314 | Blocks.HorizontalRule = function(el) |
| 315 | return cblock("* * * * *", PANDOC_WRITER_OPTIONS.columns) |
| 316 | end |
| 317 | |
| 318 | Inlines.Str = function(el) |
| 319 | return escape(el.text) |
| 320 | end |
| 321 | |
| 322 | Inlines.Space = function(el) |
| 323 | return space |
| 324 | end |
| 325 | |
| 326 | Inlines.SoftBreak = function(el) |
| 327 | if PANDOC_WRITER_OPTIONS.wrap_text == "wrap-preserve" then |
| 328 | return cr |
| 329 | else |
| 330 | return space |
| 331 | end |
| 332 | end |
| 333 | |
| 334 | Inlines.LineBreak = function(el) |
| 335 | return concat{ "\\", cr } |
| 336 | end |
| 337 | |
| 338 | Inlines.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 |
| 344 | end |
| 345 | |
| 346 | Inlines.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) |
| 355 | end |
| 356 | |
| 357 | Inlines.Emph = function(el) |
| 358 | return concat{ "_", inlines(el.content), "_" } |
| 359 | end |
| 360 | |
| 361 | Inlines.Strong = function(el) |
| 362 | return concat{ "*", inlines(el.content), "*" } |
| 363 | end |
| 364 | |
| 365 | Inlines.Strikeout = function(el) |
| 366 | return concat{ "{-", inlines(el.content), "-}"} |
| 367 | end |
| 368 | |
| 369 | Inlines.Subscript = function(el) |
| 370 | return concat{ "{~", inlines(el.content), "~}"} |
| 371 | end |
| 372 | |
| 373 | Inlines.Superscript = function(el) |
| 374 | return concat{ "{^", inlines(el.content), "^}"} |
| 375 | end |
| 376 | |
| 377 | Inlines.SmallCaps = function(el) |
| 378 | return concat{ "[", inlines(el.content), "]{.smallcaps}"} |
| 379 | end |
| 380 | |
| 381 | Inlines.Underline = function(el) |
| 382 | return concat{ "[", inlines(el.content), "]{.underline}"} |
| 383 | end |
| 384 | |
| 385 | Inlines.Cite = function(el) |
| 386 | return inlines(el.content) |
| 387 | end |
| 388 | |
| 389 | Inlines.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) } |
| 397 | end |
| 398 | |
| 399 | Inlines.Span = function(el) |
| 400 | local attr = render_attributes(el) |
| 401 | return concat{"[", inlines(el.content), "]", attr} |
| 402 | end |
| 403 | |
| 404 | Inlines.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) |
| 413 | end |
| 414 | |
| 415 | Inlines.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 = {"", attr} |
| 423 | return concat(result) |
| 424 | end |
| 425 | |
| 426 | Inlines.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 |
| 432 | end |
| 433 | |
| 434 | Inlines.Note = function(el) |
| 435 | footnotes[#footnotes + 1] = el.content |
| 436 | local num = #footnotes |
| 437 | return literal(format("[^%d]", num)) |
| 438 | end |
| 439 | |
| 440 | function 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 |
| 454 | end |