pandoc2review 1.6.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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