size: 10 KiB
| 1 | local djot = require("djot") |
| 2 | local ast = require("djot.ast") |
| 3 | local insert_attribute, copy_attributes = |
| 4 | ast.insert_attribute, ast.copy_attributes |
| 5 | local emoji -- require this later, only if emoji encountered |
| 6 | local format = string.format |
| 7 | local find, gsub = string.find, string.gsub |
| 8 | |
| 9 | -- Produce a copy of a table. |
| 10 | local function copy(tbl) |
| 11 | local result = {} |
| 12 | if tbl then |
| 13 | for k,v in pairs(tbl) do |
| 14 | local newv = v |
| 15 | if type(v) == "table" then |
| 16 | newv = copy(v) |
| 17 | end |
| 18 | result[k] = newv |
| 19 | end |
| 20 | end |
| 21 | return result |
| 22 | end |
| 23 | |
| 24 | local Renderer = {} |
| 25 | |
| 26 | function Renderer:new() |
| 27 | local state = { |
| 28 | tight = false, |
| 29 | footnotes = nil, |
| 30 | references = nil |
| 31 | } |
| 32 | setmetatable(state, self) |
| 33 | self.__index = self |
| 34 | return state |
| 35 | end |
| 36 | |
| 37 | local function words(s) |
| 38 | if s then |
| 39 | local res = {} |
| 40 | string.gsub(s, "(%S+)", function(x) table.insert(res, x) end) |
| 41 | return res |
| 42 | else |
| 43 | return {} |
| 44 | end |
| 45 | end |
| 46 | |
| 47 | local function to_attr(attr) |
| 48 | if not attr then |
| 49 | return nil |
| 50 | end |
| 51 | local result = copy(attr) |
| 52 | result.id = nil |
| 53 | result.class = nil |
| 54 | return pandoc.Attr(attr.id or "", words(attr.class), result) |
| 55 | end |
| 56 | |
| 57 | function Renderer:with_optional_span(node, f) |
| 58 | local base = f(self:render_children(node)) |
| 59 | if node.attr then |
| 60 | return pandoc.Span(base, to_attr(node.attr)) |
| 61 | else |
| 62 | return base |
| 63 | end |
| 64 | end |
| 65 | |
| 66 | function Renderer:with_optional_div(node, f) |
| 67 | local base = f(self:render_children(node)) |
| 68 | if node.attr then |
| 69 | return pandoc.Div(base, to_attr(node.attr)) |
| 70 | else |
| 71 | return base |
| 72 | end |
| 73 | end |
| 74 | |
| 75 | function Renderer:render_node(node) |
| 76 | return self[node.tag](self, node) |
| 77 | end |
| 78 | |
| 79 | function Renderer:render_children(node) |
| 80 | local buff = {} |
| 81 | local inline = false |
| 82 | if node.children and #node.children > 0 then |
| 83 | local oldtight |
| 84 | if node.tight ~= nil then |
| 85 | oldtight = self.tight |
| 86 | self.tight = node.tight |
| 87 | end |
| 88 | local function integrate_elt(elt) |
| 89 | if elt.__name == "Inlines" or elt.__name == "Blocks" then |
| 90 | for i=1,#elt do |
| 91 | integrate_elt(elt[i]) |
| 92 | end |
| 93 | else |
| 94 | buff[#buff + 1] = elt |
| 95 | end |
| 96 | end |
| 97 | for _,child in ipairs(node.children) do |
| 98 | local elt = self:render_node(child) |
| 99 | integrate_elt(elt) |
| 100 | end |
| 101 | if node.tight ~= nil then |
| 102 | self.tight = oldtight |
| 103 | end |
| 104 | end |
| 105 | return buff |
| 106 | end |
| 107 | |
| 108 | function Renderer:doc(node) |
| 109 | self.footnotes = node.footnotes |
| 110 | self.references = node.references |
| 111 | return pandoc.Pandoc(self:render_children(node)) |
| 112 | end |
| 113 | |
| 114 | function Renderer:section(node) |
| 115 | local attrs = to_attr(node.attr) |
| 116 | table.insert(attrs.classes, 1, "section") |
| 117 | return pandoc.Div(self:render_children(node), attrs) |
| 118 | end |
| 119 | |
| 120 | function Renderer:raw_block(node) |
| 121 | return pandoc.RawBlock(node.format, node.text) |
| 122 | end |
| 123 | |
| 124 | function Renderer:para(node) |
| 125 | local constructor = pandoc.Para |
| 126 | if self.tight then |
| 127 | constructor = pandoc.Plain |
| 128 | end |
| 129 | return self:with_optional_div(node, constructor) |
| 130 | end |
| 131 | |
| 132 | function Renderer:blockquote(node) |
| 133 | return self:with_optional_div(node, pandoc.BlockQuote) |
| 134 | end |
| 135 | |
| 136 | function Renderer:div(node) |
| 137 | return pandoc.Div(self:render_children(node), to_attr(node.attr)) |
| 138 | end |
| 139 | |
| 140 | function Renderer:heading(node) |
| 141 | return pandoc.Header(node.level, |
| 142 | self:render_children(node), |
| 143 | to_attr(node.attr)) |
| 144 | end |
| 145 | |
| 146 | function Renderer:thematic_break(node) |
| 147 | if node.attr then |
| 148 | return pandoc.Div(pandoc.HorizontalRule(), to_attr(node.attr)) |
| 149 | else |
| 150 | return pandoc.HorizontalRule() |
| 151 | end |
| 152 | end |
| 153 | |
| 154 | function Renderer:code_block(node) |
| 155 | local attr = copy(to_attr(node.attr)) |
| 156 | if not attr.class then |
| 157 | attr.class = node.lang |
| 158 | else |
| 159 | attr.class = node.lang .. " " .. attr.class |
| 160 | end |
| 161 | return pandoc.CodeBlock(node.text:gsub("\n$",""), attr) |
| 162 | end |
| 163 | |
| 164 | function Renderer:table(node) |
| 165 | local rows = {} |
| 166 | local headers = {} |
| 167 | local caption = {} |
| 168 | local aligns = {} |
| 169 | local widths = {} |
| 170 | local content = node.c |
| 171 | for i=1,#content do |
| 172 | local row = content[i] |
| 173 | if row.t == "caption" then |
| 174 | caption = self:render_children(row) |
| 175 | elseif row.t == "row" then |
| 176 | local cells = {} |
| 177 | for j=1,#row.c do |
| 178 | cells[j] = self:render_node(row.c[j]) |
| 179 | if not aligns[j] then |
| 180 | local align = row.c[j].align |
| 181 | if not align then |
| 182 | aligns[j] = "AlignDefault" |
| 183 | elseif align == "center" then |
| 184 | aligns[j] = "AlignCenter" |
| 185 | elseif align == "left" then |
| 186 | aligns[j] = "AlignLeft" |
| 187 | elseif align == "right" then |
| 188 | aligns[j] = "AlignRight" |
| 189 | end |
| 190 | widths[j] = 0 |
| 191 | end |
| 192 | end |
| 193 | if row.head then |
| 194 | headers = cells |
| 195 | else |
| 196 | rows[#rows + 1] = cells |
| 197 | end |
| 198 | end |
| 199 | end |
| 200 | return pandoc.utils.from_simple_table( |
| 201 | pandoc.SimpleTable(caption, aligns, widths, headers, rows)) |
| 202 | end |
| 203 | |
| 204 | function Renderer:cell(node) |
| 205 | return { pandoc.Plain(self:render_children(node)) } |
| 206 | end |
| 207 | |
| 208 | function Renderer:list(node) |
| 209 | local sty = node.style |
| 210 | if sty == "*" or sty == "+" or sty == "-" then |
| 211 | return self:with_optional_div(node, pandoc.BulletList) |
| 212 | elseif sty == "X" then |
| 213 | return self:with_optional_div(node, pandoc.BulletList) |
| 214 | elseif sty == ":" then |
| 215 | return self:with_optional_div(node, pandoc.DefinitionList) |
| 216 | else |
| 217 | local start = 1 |
| 218 | local sty = "DefaultStyle" |
| 219 | local delim = "DefaultDelim" |
| 220 | if node.start and node.start > 1 then |
| 221 | start = node.start |
| 222 | end |
| 223 | local list_type = gsub(node.style, "%p", "") |
| 224 | if list_type == "a" then |
| 225 | sty = "LowerAlpha" |
| 226 | elseif list_type == "A" then |
| 227 | sty = "UpperAlpha" |
| 228 | elseif list_type == "i" then |
| 229 | sty = "LowerRoman" |
| 230 | elseif list_type == "I" then |
| 231 | sty = "UpperRoman" |
| 232 | end |
| 233 | local list_delim = gsub(node.style, "%P", "") |
| 234 | if list_delim == ")" then |
| 235 | delim = "OneParen" |
| 236 | elseif list_delim == "()" then |
| 237 | delim = "TwoParens" |
| 238 | end |
| 239 | return self:with_optional_div(node, function(x) |
| 240 | return pandoc.OrderedList(x, |
| 241 | pandoc.ListAttributes(start, sty, delim)) |
| 242 | end) |
| 243 | end |
| 244 | end |
| 245 | |
| 246 | function Renderer:list_item(node) |
| 247 | local children = self:render_children(node) |
| 248 | if node.checkbox then |
| 249 | local box = (node.checkbox == "checked" and "☒") or "☐" |
| 250 | local tag = children[1].tag |
| 251 | if tag == "Para" or tag == "Plain" then |
| 252 | children[1].content:insert(1, pandoc.Space()) |
| 253 | children[1].content:insert(1, pandoc.Str(box)) |
| 254 | else |
| 255 | children:insert(1, pandoc.Para{pandoc.Str(box), pandoc.Space()}) |
| 256 | end |
| 257 | end |
| 258 | return children |
| 259 | end |
| 260 | |
| 261 | function Renderer:definition_list_item(node) |
| 262 | local term = self:render_node(node.children[1]) |
| 263 | local defn = self:render_node(node.children[2]) |
| 264 | return { term, defn } |
| 265 | end |
| 266 | |
| 267 | function Renderer:term(node) |
| 268 | return self:render_children(node) |
| 269 | end |
| 270 | |
| 271 | function Renderer:definition(node) |
| 272 | return self:render_children(node) |
| 273 | end |
| 274 | |
| 275 | function Renderer:reference_definition() |
| 276 | return "" |
| 277 | end |
| 278 | |
| 279 | function Renderer:footnote_reference(node) |
| 280 | local label = node.text |
| 281 | local note = self.footnotes[label] |
| 282 | if note then |
| 283 | return pandoc.Note(self:render_children(note)) |
| 284 | else |
| 285 | io.stderr:write("Note " .. label .. " not found.") |
| 286 | return pandoc.Str("[^" .. label .. "]") |
| 287 | end |
| 288 | end |
| 289 | |
| 290 | function Renderer:raw_inline(node) |
| 291 | return pandoc.RawInline(node.format, node.text) |
| 292 | end |
| 293 | |
| 294 | function Renderer:str(node) |
| 295 | -- add a span, if needed, to contain attribute on a bare string: |
| 296 | if node.attr then |
| 297 | return pandoc.Span(pandoc.Inlines(node.text), to_attr(node.attr)) |
| 298 | else |
| 299 | return pandoc.Inlines(node.text) |
| 300 | end |
| 301 | end |
| 302 | |
| 303 | function Renderer:softbreak() |
| 304 | return pandoc.SoftBreak() |
| 305 | end |
| 306 | |
| 307 | function Renderer:hardbreak() |
| 308 | return pandoc.LineBreak() |
| 309 | end |
| 310 | |
| 311 | function Renderer:nbsp() |
| 312 | return pandoc.Str(" ") |
| 313 | end |
| 314 | |
| 315 | function Renderer:verbatim(node) |
| 316 | return pandoc.Code(node.text, to_attr(node.attr)) |
| 317 | end |
| 318 | |
| 319 | function Renderer:link(node) |
| 320 | local attrs = {} |
| 321 | local dest = node.destination |
| 322 | if node.reference then |
| 323 | local ref = self.references[node.reference] |
| 324 | if ref then |
| 325 | if ref.attributes then |
| 326 | attrs = copy(ref.attributes) |
| 327 | end |
| 328 | dest = ref.destination |
| 329 | else |
| 330 | dest = "#" -- empty href is illegal |
| 331 | end |
| 332 | end |
| 333 | -- link's attributes override reference's: |
| 334 | copy_attributes(attrs, node.attr) |
| 335 | local title = attrs.title |
| 336 | attrs.title = nil |
| 337 | return pandoc.Link(self:render_children(node), dest, |
| 338 | title, to_attr(attrs)) |
| 339 | end |
| 340 | |
| 341 | function Renderer:image(node) |
| 342 | local attrs = {} |
| 343 | local dest = node.destination |
| 344 | if node.reference then |
| 345 | local ref = self.references[node.reference] |
| 346 | if ref then |
| 347 | if ref.attributes then |
| 348 | attrs = copy(ref.attributes) |
| 349 | end |
| 350 | dest = ref.destination |
| 351 | else |
| 352 | dest = "#" -- empty href is illegal |
| 353 | end |
| 354 | end |
| 355 | -- image's attributes override reference's: |
| 356 | copy_attributes(attrs, node.attr) |
| 357 | return pandoc.Image(self:render_children(node), dest, |
| 358 | title, to_attr(attrs)) |
| 359 | end |
| 360 | |
| 361 | function Renderer:span(node) |
| 362 | return pandoc.Span(self:render_children(node), to_attr(node.attr)) |
| 363 | end |
| 364 | |
| 365 | function Renderer:mark(node) |
| 366 | local attr = copy(node.attr) |
| 367 | if attr.class then |
| 368 | attr.class = "mark " .. attr.class |
| 369 | else |
| 370 | attr = { class = "mark" } |
| 371 | end |
| 372 | return pandoc.Span(self:render_children(node), to_attr(attr)) |
| 373 | end |
| 374 | |
| 375 | function Renderer:insert(node) |
| 376 | local attr = copy(node.attr) |
| 377 | if attr.class then |
| 378 | attr.class = "insert " .. attr.class |
| 379 | else |
| 380 | attr = { class = "insert" } |
| 381 | end |
| 382 | return pandoc.Span(self:render_children(node), to_attr(attr)) |
| 383 | end |
| 384 | |
| 385 | function Renderer:delete(node) |
| 386 | return self:with_optional_span(node, pandoc.Strikeout) |
| 387 | end |
| 388 | |
| 389 | function Renderer:subscript(node) |
| 390 | return self:with_optional_span(node, pandoc.Subscript) |
| 391 | end |
| 392 | |
| 393 | function Renderer:superscript(node) |
| 394 | return self:with_optional_span(node, pandoc.Superscript) |
| 395 | end |
| 396 | |
| 397 | function Renderer:emph(node) |
| 398 | return self:with_optional_span(node, pandoc.Emph) |
| 399 | end |
| 400 | |
| 401 | function Renderer:strong(node) |
| 402 | return self:with_optional_span(node, pandoc.Strong) |
| 403 | end |
| 404 | |
| 405 | function Renderer:double_quoted(node) |
| 406 | return self:with_optional_span(node, |
| 407 | function(x) return pandoc.Quoted("DoubleQuote", x) end) |
| 408 | end |
| 409 | |
| 410 | function Renderer:single_quoted(node) |
| 411 | return self:with_optional_span(node, |
| 412 | function(x) return pandoc.Quoted("SingleQuote", x) end) |
| 413 | end |
| 414 | |
| 415 | function Renderer:left_double_quote() |
| 416 | return "“" |
| 417 | end |
| 418 | |
| 419 | function Renderer:right_double_quote() |
| 420 | return "”" |
| 421 | end |
| 422 | |
| 423 | function Renderer:left_single_quote() |
| 424 | return "‘" |
| 425 | end |
| 426 | |
| 427 | function Renderer:right_single_quote() |
| 428 | return "’" |
| 429 | end |
| 430 | |
| 431 | function Renderer:ellipses() |
| 432 | return "…" |
| 433 | end |
| 434 | |
| 435 | function Renderer:em_dash() |
| 436 | return "—" |
| 437 | end |
| 438 | |
| 439 | function Renderer:en_dash() |
| 440 | return "–" |
| 441 | end |
| 442 | |
| 443 | function Renderer:symbol(node) |
| 444 | return pandoc.Span(":" .. node.alias .. ":", |
| 445 | pandoc.Attr("",{"symbol"},{["alias"] = node.alias})) |
| 446 | end |
| 447 | |
| 448 | function Renderer:math(node) |
| 449 | local math_type = "InlineMath" |
| 450 | if find(node.attr.class, "display") then |
| 451 | math_type = "DisplayMath" |
| 452 | end |
| 453 | return pandoc.Math(math_type, node.text) |
| 454 | end |
| 455 | |
| 456 | function Reader(input) |
| 457 | local doc = djot.parse(tostring(input)) |
| 458 | return Renderer:render_node(doc) |
| 459 | end |