pandoc2review 1.6.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lua/review.lua CHANGED
@@ -25,7 +25,7 @@ local footnotes = {}
25
25
 
26
26
  -- internal
27
27
  local metadata = nil
28
- local stringify = (require "pandoc.utils").stringify
28
+ local stringify = (require("pandoc.utils")).stringify
29
29
  local inline_commands = {
30
30
  -- processed if given as classes of Span elements
31
31
  -- true if syntax is `@<command>{string}`
@@ -48,7 +48,7 @@ local inline_commands = {
48
48
  title = true,
49
49
  chapref = true,
50
50
  list = true,
51
- img = true,
51
+ img = true,
52
52
  table = true,
53
53
  eq = true,
54
54
  hd = true,
@@ -83,21 +83,19 @@ local function log(s)
83
83
  end
84
84
 
85
85
  local function surround_inline(s)
86
- if (string.match(s, "{") or string.match(s, "}")) then
87
- if (string.match(s, "%$")) then -- use % for regexp escape
88
- if (string.match(s, "|")) then
89
- -- give up. escape } by \}
90
- return "{" .. string.gsub(s, "}", "\\}") .. "}"
91
- else
92
- -- surround by ||
93
- return "|" .. s .. "|"
94
- end
95
- else
96
- -- surround by $$
97
- return "$" .. s .. "$"
98
- end
86
+ if not s:match("[{}]") then
87
+ return "{" .. s .. "}"
88
+ end
89
+ if not s:match("%$") then
90
+ return "$" .. s .. "$"
99
91
  end
100
- return "{" .. s .. "}"
92
+
93
+ -- use % for regexp escape
94
+ if s:match("|") then
95
+ -- give up. escape } by \}
96
+ return "{" .. s:gsub("}", "\\}") .. "}"
97
+ end
98
+ return "|" .. s .. "|"
101
99
  end
102
100
 
103
101
  local function format_inline(fmt, s)
@@ -105,31 +103,18 @@ local function format_inline(fmt, s)
105
103
  end
106
104
 
107
105
  local function html_align(align)
108
- if align == "AlignLeft" then
109
- return ""
110
- elseif align == "AlignRight" then
111
- return "right"
112
- elseif align == "AlignCenter" then
113
- return "center"
114
- else
115
- return ""
116
- end
106
+ return ({ AlignRight = "right", AlignCenter = "center" })[align] or ""
117
107
  end
118
108
 
119
109
  function Blocksep()
120
110
  return "\n\n"
121
111
  end
122
112
 
123
- function Doc(body, metadata, variables)
124
- local buffer = {}
125
- local function add(s)
126
- table.insert(buffer, s)
127
- end
128
- add(body)
129
- if (#footnotes > 0) then
130
- add("\n" .. table.concat(footnotes, "\n"))
113
+ function Doc(body, meta, variables)
114
+ if #footnotes == 0 then
115
+ return body
131
116
  end
132
- return table.concat(buffer, "\n")
117
+ return table.concat({ body, "", table.concat(footnotes, "\n") }, "\n")
133
118
  end
134
119
 
135
120
  function Str(s)
@@ -145,11 +130,7 @@ function LineBreak()
145
130
  end
146
131
 
147
132
  function SoftBreak(s)
148
- if (metadata.softbreak) then
149
- return " "
150
- else
151
- return "<P2RBR/>"
152
- end
133
+ return metadata.softbreak and " " or "<P2RBR/>"
153
134
  end
154
135
 
155
136
  function Plain(s)
@@ -188,25 +169,27 @@ local function attr_scale(attr, key) -- a helper for CaptionedImage
188
169
  return tonumber(scale) / 100
189
170
  end
190
171
 
191
- function Header(level, s, attr)
192
- local headmark = string.rep("=", level)
172
+ local function class_header(classes)
173
+ -- Re:VIEW's behavior
174
+ for _, cls in pairs({ "column", "nonum", "nodisp", "notoc" }) do
175
+ if classes[cls] then
176
+ return string.format("[%s]", cls)
177
+ end
178
+ end
193
179
 
194
- local classes = attr_classes(attr)
180
+ -- Pandoc's behavior
181
+ if classes.unnumbered then
182
+ return classes.unlisted and "[notoc]" or "[nonum]"
183
+ end
184
+
185
+ -- None
186
+ return ""
187
+ end
195
188
 
196
- headmark = headmark .. (
197
- -- Re:VIEW's behavior
198
- classes["column"] and "[column]" or (
199
- classes["nonum"] and "[nonum]" or (
200
- classes["nodisp"] and "[nodisp]" or (
201
- classes["notoc"] and "[notoc]" or (
202
- -- Pandoc's behavior
203
- classes["unnumbered"] and (
204
- classes["unlisted"] and "[notoc]" or "[nonum]") or (
205
- -- None
206
- "")))))
207
- )
208
-
209
- if ((config.use_header_id == "true") and attr.id ~= "" and attr.id ~= s) then
189
+ function Header(level, s, attr)
190
+ local headmark = string.rep("=", level) .. class_header(attr_classes(attr))
191
+
192
+ if (config.use_header_id == "true") and attr.id ~= "" and attr.id ~= s then
210
193
  headmark = headmark .. "{" .. attr.id .. "}"
211
194
  end
212
195
 
@@ -214,17 +197,13 @@ function Header(level, s, attr)
214
197
  end
215
198
 
216
199
  function HorizontalRule()
217
- if (config.use_hr == "true") then
218
- return "//hr"
219
- else
220
- return ""
221
- end
200
+ return config.use_hr == "true" and "//hr" or ""
222
201
  end
223
202
 
224
203
  local function lint_list(s)
225
- return s:gsub("\n+(//beginchild)\n+", '\n\n%1\n\n'
226
- ):gsub("\n+(//endchild)\n+", '\n\n%1\n\n'
227
- ):gsub("\n+(//endchild)\n*$", "\n\n%1")
204
+ return s:gsub("\n+(//beginchild)\n+", "\n\n%1\n\n")
205
+ :gsub("\n+(//endchild)\n+", "\n\n%1\n\n")
206
+ :gsub("\n+(//endchild)\n*$", "\n\n%1")
228
207
  end
229
208
 
230
209
  function BulletList(items)
@@ -270,39 +249,33 @@ end
270
249
  function CodeBlock(s, attr)
271
250
  local classes = attr_classes(attr)
272
251
 
273
- local command = nil
274
- for k,v in pairs({cmd = "cmd", source = "source", quote = "source"}) do
252
+ local command = "list" -- default
253
+ for k, v in pairs({ cmd = "cmd", source = "source", quote = "source" }) do
275
254
  if classes[k] then
276
255
  command = v
277
256
  break
278
257
  end
279
258
  end
280
- command = command or "list"
281
259
 
282
260
  local is_list = command == "list"
283
261
 
284
-
285
- local num = (is_list == false) and "" or (
286
- (classes["numberLines"] or classes["number-lines"] or classes["num"]) and
287
- "num" or ""
288
- )
262
+ local num = (is_list and (classes["numberLines"] or classes["number-lines"] or classes["num"])) and "num" or ""
289
263
 
290
264
  local firstlinenum = ""
291
265
  if is_list and (num == "num") then
292
- for _, key in ipairs({"startFrom", "start-from", "firstlinenum"}) do
293
- firstlinenum = attr_val(attr, key)
294
- if firstlinenum ~= "" then
295
- firstlinenum = "//firstlinenum[" .. firstlinenum .. "]\n"
266
+ for _, key in ipairs({ "startFrom", "start-from", "firstlinenum" }) do
267
+ if attr[key] then
268
+ firstlinenum = "//firstlinenum[" .. attr[key] .. "]\n"
296
269
  break
297
270
  end
298
271
  end
299
272
  end
300
273
 
301
274
  local lang = ""
302
- local not_lang = {numberLines = true, num = true, em = true, source = true}
275
+ local not_lang = { numberLines = true, num = true, em = true, source = true }
303
276
  not_lang["number-lines"] = true
304
277
  if is_list or (command == "source") then
305
- for key,_ in pairs(classes) do
278
+ for key, _ in pairs(classes) do
306
279
  if not_lang[key] ~= true then
307
280
  lang = "[" .. key .. "]"
308
281
  break
@@ -313,9 +286,9 @@ function CodeBlock(s, attr)
313
286
  local caption = (command == "cmd") and "" or attr_val(attr, "caption")
314
287
  local identifier = ""
315
288
  local em = is_list and classes["em"] and "em" or ""
316
- if (caption ~= "") then
289
+ if caption ~= "" then
317
290
  if is_list and (em == "") then
318
- if (attr.id ~= "") then
291
+ if attr.id ~= "" then
319
292
  identifier = "[" .. attr.id .. "]"
320
293
  else
321
294
  list_num = list_num + 1
@@ -332,11 +305,7 @@ function CodeBlock(s, attr)
332
305
  end
333
306
  end
334
307
 
335
- return (
336
- firstlinenum ..
337
- "//" .. em .. command .. num .. identifier .. caption .. lang ..
338
- "{\n" .. s .. "\n//}"
339
- )
308
+ return (firstlinenum .. "//" .. em .. command .. num .. identifier .. caption .. lang .. "{\n" .. s .. "\n//}")
340
309
  end
341
310
 
342
311
  function LineBlock(s)
@@ -345,7 +314,7 @@ function LineBlock(s)
345
314
  end
346
315
 
347
316
  function Link(s, src, tit)
348
- if (src == s) then
317
+ if src == s then
349
318
  return format_inline("href", src)
350
319
  else
351
320
  return format_inline("href", src .. "," .. s)
@@ -412,7 +381,7 @@ function Table(caption, aligns, widths, headers, rows)
412
381
  add("--------------")
413
382
  for _, row in pairs(rows) do
414
383
  tmp = {}
415
- for i, c in pairs(row) do
384
+ for i, c in pairs(row) do
416
385
  local align = html_align(aligns[i])
417
386
  if (config.use_table_align == "true") and (align ~= "") then
418
387
  c = format_inline("dtp", "table align=" .. align) .. c
@@ -426,13 +395,6 @@ function Table(caption, aligns, widths, headers, rows)
426
395
  return table.concat(buffer, "\n")
427
396
  end
428
397
 
429
- function Image(s, src, tit)
430
- -- Re:VIEW @<icon> ignores caption and title
431
- local id = string.gsub(src, "%.%w+$", "")
432
- id = string.gsub(id, "^images/", "")
433
- return format_inline("icon", id)
434
- end
435
-
436
398
  function CaptionedImage(s, src, tit, attr)
437
399
  local path = "[" .. s:gsub("%.%w+$", ""):gsub("^images/", "") .. "]"
438
400
 
@@ -442,7 +404,7 @@ function CaptionedImage(s, src, tit, attr)
442
404
  if scale == "" then
443
405
  local width = attr_scale(attr, "width")
444
406
  local height = attr_scale(attr, "height")
445
- if (width ~= "") then
407
+ if width ~= "" then
446
408
  if (height ~= "") and (width ~= height) then
447
409
  log("WARNING: Image width and height must be same. Using width.\n")
448
410
  end
@@ -455,26 +417,15 @@ function CaptionedImage(s, src, tit, attr)
455
417
  scale = "[scale=" .. scale .. "]"
456
418
  end
457
419
 
458
- local command = "//image"
459
- local caption = ""
460
- if (tit == "") then
461
- command = "//indepimage"
462
- else
463
- caption = "[" .. tit .. "]"
464
- end
420
+ local command = tit == "" and "//indepimage" or "//image"
421
+ local caption = tit == "" and "" or ("[" .. tit .. "]")
465
422
 
466
- return (
467
- command .. path .. caption .. scale .. "{" .. comment .. "\n//}"
468
- )
423
+ return (command .. path .. caption .. scale .. "{" .. comment .. "\n//}")
469
424
  end
470
425
 
471
426
  function Image(s, src, tit, attr)
472
427
  -- Re:VIEW @<icon> ignores caption and title
473
- if attr.is_figure then
474
- return CaptionedImage(src, s, tit, attr)
475
- end
476
- local id = string.gsub(src, "%.%w+$", "")
477
- id = string.gsub(id, "^images/", "")
428
+ local id = src:gsub("%.%w+$", ""):gsub("^images/", "")
478
429
  return format_inline("icon", id)
479
430
  end
480
431
 
@@ -490,12 +441,7 @@ function Cite(s, cs)
490
441
  end
491
442
 
492
443
  function Quoted(quotetype, s)
493
- if (quotetype == "SingleQuote") then
494
- return SingleQuoted(s)
495
- end
496
- if (quotetype == "DoubleQuote") then
497
- return DoubleQuoted(s)
498
- end
444
+ return _G[quotetype](s)
499
445
  end
500
446
 
501
447
  function SingleQuoted(s)
@@ -511,13 +457,9 @@ function SmallCaps(s)
511
457
  end
512
458
 
513
459
  function Div(s, attr)
514
- local blankline = attr_val(attr, "blankline")
515
- if blankline ~= "" then
516
- local buffer = {}
517
- for _ = 1, tonumber(blankline) do
518
- table.insert(buffer, "//blankline")
519
- end
520
- return table.concat(buffer, "\n")
460
+ local blankline = tonumber(attr.blankline)
461
+ if blankline then
462
+ return string.rep("//blankline\n", blankline):gsub("\n$", "")
521
463
  end
522
464
 
523
465
  local classes = attr_classes(attr)
@@ -531,15 +473,11 @@ function Div(s, attr)
531
473
  end
532
474
 
533
475
  if classes["review-internal"] then
534
- s, _ = s:gsub(
535
- "%]{<P2RREMOVEBELOW/>\n", "]{"
536
- ):gsub(
537
- "\n<P2RREMOVEABOVE/>//}", "//}"
538
- )
476
+ s, _ = s:gsub("%]{<P2RREMOVEBELOW/>\n", "]{"):gsub("\n<P2RREMOVEABOVE/>//}", "//}")
539
477
  return s
540
478
  end
541
479
 
542
- for cls,_ in pairs(classes) do
480
+ for cls, _ in pairs(classes) do
543
481
  s = "//" .. cls .. "{\n" .. s .. "\n//}"
544
482
  end
545
483
  return s
@@ -548,7 +486,7 @@ end
548
486
  function Span(s, attr)
549
487
  -- ruby and kw with a supplement
550
488
  local a = ""
551
- for _, cmd in ipairs({"ruby", "kw"}) do
489
+ for _, cmd in ipairs({ "ruby", "kw" }) do
552
490
  a = attr_val(attr, cmd)
553
491
  if a ~= "" then
554
492
  s = format_inline(cmd, s .. "," .. a)
@@ -566,15 +504,15 @@ function Span(s, attr)
566
504
  end
567
505
 
568
506
  function RawInline(format, text)
569
- if (format == "review") then
507
+ if format == "review" then
570
508
  return text
571
509
  end
572
510
 
573
- if (metadata.hideraw) then
511
+ if metadata.hideraw then
574
512
  return ""
575
513
  end
576
514
 
577
- if (format == "tex") then
515
+ if format == "tex" then
578
516
  return format_inline("embed", "|latex|" .. text)
579
517
  else
580
518
  return format_inline("embed", "|" .. format .. "|" .. text)
@@ -582,15 +520,15 @@ function RawInline(format, text)
582
520
  end
583
521
 
584
522
  function RawBlock(format, text)
585
- if (format == "review") then
523
+ if format == "review" then
586
524
  return text
587
525
  end
588
526
 
589
- if (metadata.hideraw) then
527
+ if metadata.hideraw then
590
528
  return ""
591
529
  end
592
530
 
593
- if (format == "tex") then
531
+ if format == "tex" then
594
532
  return "//embed[latex]{\n" .. text .. "\n//}"
595
533
  else
596
534
  return "//embed[" .. format .. "]{\n" .. text .. "\n//}"
@@ -598,16 +536,16 @@ function RawBlock(format, text)
598
536
  end
599
537
 
600
538
  local function configure()
601
- try_catch {
539
+ try_catch({
602
540
  try = function()
603
541
  metadata = PANDOC_DOCUMENT.meta
604
542
  end,
605
543
  catch = function(error)
606
544
  log("Due to your pandoc version is too old, config.yml loader is disabled.\n")
607
- end
608
- }
545
+ end,
546
+ })
609
547
 
610
- if (metadata) then
548
+ if metadata then
611
549
  -- Load config from YAML
612
550
  for k, _ in pairs(config) do
613
551
  if metadata[k] ~= nil then
@@ -617,23 +555,225 @@ local function configure()
617
555
  end
618
556
  end
619
557
 
620
- if PANDOC_VERSION >= "3.0.0" then
621
- -- NOTE: A wrapper to support Pandoc >= 3.0 https://pandoc.org/custom-writers.html#changes-in-pandoc-3.0
622
- function Writer (doc, opts)
623
- PANDOC_DOCUMENT = doc
624
- PANDOC_WRITER_OPTIONS = opts
625
- configure()
626
- return pandoc.write_classic(doc, opts)
627
- end
628
- else
558
+ setmetatable(_G, {
559
+ __index = function(_, key)
560
+ log(string.format("WARNING: Undefined function '%s'\n", key))
561
+ return function()
562
+ return ""
563
+ end
564
+ end,
565
+ })
566
+
567
+ if PANDOC_VERSION < "3.0.0" then
629
568
  configure()
569
+ return
630
570
  end
631
571
 
632
- local meta = {}
633
- meta.__index =
634
- function(_, key)
635
- log(string.format("WARNING: Undefined function '%s'\n", key))
636
- return function() return "" end
572
+ Blocks = setmetatable({}, {
573
+ __index = function(_, key)
574
+ error("NotImplementedError: Blocks.%" .. tostring(key))
575
+ end,
576
+ })
577
+
578
+ Inlines = setmetatable({}, {
579
+ __index = function(_, key)
580
+ error("NotImplementedError: Inlines.%" .. tostring(key))
581
+ end,
582
+ })
583
+
584
+ local tidy_attr = function(el)
585
+ local ret = {}
586
+ for k, v in pairs(el.attr.attributes) do
587
+ ret[k] = v
588
+ end
589
+ ret.id = el.identifier or ""
590
+ ret.class = el.classes and table.concat(el.classes, " ")
591
+ return ret
592
+ end
593
+
594
+ local concat = pandoc.layout.concat
595
+
596
+ local function render(...)
597
+ local x = pandoc.layout.render(...):gsub("\n+$", "")
598
+ return x
599
+ end
600
+
601
+ local function inlines(els)
602
+ local buff = {}
603
+ for _, el in ipairs(els) do
604
+ table.insert(buff, Inlines[el.tag](el))
605
+ end
606
+ return concat(buff)
607
+ end
608
+
609
+ local function blocks(els, sep)
610
+ local buff = {}
611
+ for _, el in ipairs(els) do
612
+ table.insert(buff, Blocks[el.tag](el))
613
+ end
614
+ return concat(buff, sep)
615
+ end
616
+
617
+ Inlines.Str = function(el)
618
+ return Str(el.text)
619
+ end
620
+
621
+ for _, v in pairs({ "Space", "LineBreak", "SoftBreak" }) do
622
+ Inlines[v] = _G[v]
623
+ end
624
+
625
+ Blocks.HorizontalRule = function(_)
626
+ return HorizontalRule() .. "\n"
627
+ end
628
+
629
+ Blocks.Plain = function(el)
630
+ return inlines(el.content) .. "\n"
631
+ end
632
+
633
+ Blocks.Para = function(el)
634
+ if #el.content == 1 and el.content[1].tag == "Image" then
635
+ local img = el.content[1]
636
+ return CaptionedImage(img.src, img.title, render(inlines(img.caption)), tidy_attr(img)) .. "\n"
637
637
  end
638
+ return inlines(el.content) .. "\n"
639
+ end
640
+
641
+ Blocks.Header = function(el)
642
+ return Header(el.level, render(inlines(el.content)), tidy_attr(el)) .. "\n"
643
+ end
644
+
645
+ local function render_blocks(blks, sep)
646
+ local ret = {}
647
+ for _, v in pairs(blks) do
648
+ table.insert(ret, render(blocks(v, sep or "\n")))
649
+ end
650
+ return ret
651
+ end
652
+
653
+ Blocks.BulletList = function(el)
654
+ return BulletList(render_blocks(el.content)) .. "\n"
655
+ end
656
+
657
+ Blocks.OrderedList = function(el)
658
+ return OrderedList(render_blocks(el.content), el.start) .. "\n"
659
+ end
660
+
661
+ Blocks.DefinitionList = function(el)
662
+ local items = {}
663
+ for _, v in pairs(el.content) do
664
+ local term = render(inlines(v[1]))
665
+ table.insert(items, { [term] = render_blocks(v[2]) })
666
+ end
667
+ return DefinitionList(items) .. "\n"
668
+ end
669
+
670
+ Blocks.BlockQuote = function(el)
671
+ return BlockQuote(render(blocks(el.content, "\n"))) .. "\n"
672
+ end
673
+
674
+ Blocks.CodeBlock = function(el)
675
+ return CodeBlock(el.text, tidy_attr(el)) .. "\n"
676
+ end
677
+
678
+ Blocks.LineBlock = function(el)
679
+ local lines = {}
680
+ for _, v in pairs(el.content) do
681
+ table.insert(lines, render(inlines(v)))
682
+ end
683
+ return LineBlock(lines) .. "\n"
684
+ end
685
+
686
+ Inlines.Link = function(el)
687
+ return Link(render(inlines(el.content)), el.target, el.title)
688
+ end
689
+
690
+ Inlines.Code = function(el)
691
+ return Code(el.text, tidy_attr(el))
692
+ end
693
+
694
+ for _, k in pairs({ "Emph", "Strong", "Strikeout", "Underline", "Subscript", "Superscript", "SmallCaps" }) do
695
+ Inlines[k] = function(el)
696
+ return _G[k](render(inlines(el.content)))
697
+ end
698
+ end
638
699
 
639
- setmetatable(_G, meta)
700
+ Inlines.Math = function(el)
701
+ return _G[el.mathtype](el.text)
702
+ end
703
+
704
+ Blocks.Table = function(el)
705
+ local tbl = pandoc.utils.to_simple_table(el)
706
+ local headers = render_blocks(tbl.headers)
707
+ local rows = {}
708
+ for _, row in pairs(tbl.rows) do
709
+ table.insert(rows, render_blocks(row, ""))
710
+ end
711
+ return Table(render(inlines(tbl.caption)), tbl.aligns, tbl.widths, headers, rows) .. "\n"
712
+ end
713
+
714
+ Inlines.Image = function(el)
715
+ return Image(render(inlines(el.caption)), el.src, el.title, tidy_attr(el))
716
+ end
717
+
718
+ Inlines.Note = function(el)
719
+ return Note(render(blocks(el.content, "\n")))
720
+ end
721
+
722
+ Inlines.Cite = function(el)
723
+ return Cite(render(inlines(el.content)))
724
+ end
725
+
726
+ Inlines.Quoted = function(el)
727
+ return Quoted(el.quotetype, render(inlines(el.content)))
728
+ end
729
+
730
+ Blocks.Div = function(el)
731
+ return Div(render(blocks(el.content, "\n")), tidy_attr(el)) .. "\n"
732
+ end
733
+
734
+ Inlines.Span = function(el)
735
+ return Span(render(inlines(el.content)), tidy_attr(el))
736
+ end
737
+
738
+ Inlines.RawInline = function(el)
739
+ return RawInline(el.format, el.text)
740
+ end
741
+
742
+ Blocks.RawBlock = function(el)
743
+ return RawBlock(el.format, el.text) .. "\n"
744
+ end
745
+
746
+ Blocks.Figure = function(el)
747
+ if #el.content > 1 or #el.content[1].content > 1 or el.content[1].content[1].tag ~= "Image" then
748
+ error("NotImplementedError: current implementation assumes Figure contains only a single image.")
749
+ -- because Pandoc 3.1.4 does not support Pandoc's markdown cotaining Figure with multiple images...
750
+ end
751
+
752
+ local img = el.content[1].content[1]
753
+
754
+ return CaptionedImage(img.src, img.title, render(inlines(img.caption)), tidy_attr(img)) .. "\n"
755
+ end
756
+
757
+ function Writer(doc, opts)
758
+ PANDOC_DOCUMENT = doc
759
+ PANDOC_WRITER_OPTIONS = opts
760
+ configure()
761
+
762
+ if metadata.classicwriter then
763
+ if pandoc.write_classic then
764
+ return pandoc.write_classic(
765
+ doc:walk({
766
+ Figure = function(el)
767
+ return pandoc.RawBlock("review", render(Blocks.Figure(el)))
768
+ end,
769
+ }),
770
+ opts
771
+ )
772
+ end
773
+ log("WARNING: pandoc.write_classic is defunct. Using modern writer")
774
+ end
775
+
776
+ -- body should keep trailing new lines but remove one if there are footnotes for the backward-compatibility
777
+ local body = pandoc.layout.render(pandoc.layout.concat({ blocks(doc.blocks, "\n") }))
778
+ return Doc(#footnotes == 0 and body or body:gsub("\n$", ""))
779
+ end