Files
Albert Krewinkel 909ced5153 Support complex figures. [API change]
Thanks and credit go to Aner Lucero, who laid the groundwork for this
feature in the 2021 GSoC project. He contributed many changes, including
modifications to the readers for HTML, JATS, and LaTeX, and to the HTML
and JATS writers.

Shared (Albert Krewinkel):

- The new function `figureDiv`, exported from `Text.Pandoc.Shared`,
  offers a standardized way to convert a figure into a Div element.

Readers (Aner Lucero):

- HTML reader: `<figure>` elements are parsed as figures, with the
  caption taken from the respective `<figcaption>` elements.

- JATS reader: The `<fig>` and `<caption>` elements are parsed into
  figure elements, even if the contents is more complex.

- LaTeX reader: support for figures with non-image contents and for
  subfigures.

- Markdown reader: paragraphs containing just an image are treated as
  figures if the `implicit_figures` extension is enabled. The identifier
  is used as the figure's identifier and the image description is also
  used as figure caption; all other attributes are treated as belonging
  to the image.

Writers (Aner Lucero, Albert Krewinkel):

- DokuWiki, Haddock, Jira, Man, MediaWiki, Ms, Muse, PPTX, RTF, TEI,
  ZimWiki writers: Figures are rendered like Div elements.

- Asciidoc writer: The figure contents is unwrapped; each image in the
  the figure becomes a separate figure.

- Classic custom writers: Figures are passed to the global function
  `Figure(caption, contents, attr)`, where `caption` and `contents` are
  strings and `attr` is a table of key-value pairs.

- ConTeXt writer: Figures are wrapped in a "placefigure" environment
  with `\startplacefigure`/`\endplacefigure`, adding the features
  caption and listing title as properties. Subfigures are place in a
  single row with the `\startfloatcombination` environment.

- DocBook writer: Uses `mediaobject` elements, unless the figure contains
  subfigures or tables, in which case the figure content is unwrapped.

- Docx writer: figures with multiple content blocks are rendered as
  tables with style `FigureTable`; like before, single-image figures are
  still output as paragraphs with style `Figure` or `Captioned Figure`,
  depending on whether a caption is attached.

- DokuWiki writer: Caption and "alt-text" are no longer combined. The
  alt text of a figure will now be lost in the conversion.

- FB2 writer: The figure caption is added as alt text to the images in
  the figure; pre-existing alt texts are kept.

- ICML writer: Only single-image figures are supported. The contents of
  figures with additional elements gets unwrapped.

- HTML writer: the alt text is no longer constructed from the caption,
  as was the case with implicit figures. This reduces duplication, but
  comes at the risk of images that are missing alt texts. Authors should
  take care to provide alt texts for all images.

  Some readers, most notably the Markdown reader with the
  `implicit_figures` extension, add a caption that's identical to the
  image description. The writer checks for this and adds an
  `aria-hidden` attribute to the `<figcaption>` element in that case.

- JATS writer: The `<fig>` and `<caption>` elements are used write
  figures.

- LaTeX writer: complex figures, e.g. with non-image contents and
  subfigures, are supported. The `subfigure` template variable is set if
  the document contains subfigures, triggering the conditional loading
  of the *subcaption* package. Contants of figures that contain tables
  are become unwrapped, as longtable environments are not allowed within
  figures.

- Markdown writer: figures are output as implicit figures if possible,
  via HTML if the `raw_html` extension is enabled, and as Div elements
  otherwise.

- OpenDocument writer: A separate paragraph is generated for each block
  element in a figure, each with style `FigureWithCaption`. Behavior for
  single-image figures therefore remains unchanged.

- Org writer: Only the first element in a figure is given a caption;
  additional block elements in the figure are appended without any
  caption being added.

- RST writer: Single-image figures are supported as before; the contents
  of more complex images become nested in a container of type `float`.

- Texinfo writer: Figures are rendered as float with type `figure`.

- Textile writer: Figures are rendered with the help of HTML elements.

- XWiki: Figures are placed in a group.

Co-authored-by: Aner Lucero <4rgento@gmail.com>
2023-01-13 09:13:27 -08:00

370 lines
9.6 KiB
Lua

-- This is a sample custom writer for pandoc. It produces output
-- that is very similar to that of pandoc's HTML writer.
-- There is one new feature: code blocks marked with class 'dot'
-- are piped through graphviz and images are included in the HTML
-- output using 'data:' URLs. The image format can be controlled
-- via the `image_format` metadata field.
--
-- Invoke with: pandoc -t sample.lua
--
-- Note: you need not have lua installed on your system to use this
-- custom writer. However, if you do have lua installed, you can
-- use it to test changes to the script. 'lua sample.lua' will
-- produce informative error messages if your code contains
-- syntax errors.
function Writer (doc, opts)
PANDOC_DOCUMENT = doc
PANDOC_WRITER_OPTIONS = opts
loadfile(PANDOC_SCRIPT_FILE)()
return pandoc.write_classic(doc, opts)
end
local pipe = pandoc.pipe
local stringify = (require 'pandoc.utils').stringify
-- Choose the image format based on the value of the
-- `image_format` environment variable.
local image_format = os.getenv 'image_format' or 'png'
local image_mime_type = ({
jpeg = 'image/jpeg',
jpg = 'image/jpeg',
gif = 'image/gif',
png = 'image/png',
svg = 'image/svg+xml',
})[image_format]
or error('unsupported image format `' .. image_format .. '`')
-- Character escaping
local function escape(s, in_attribute)
return s:gsub('[<>&"\']',
function(x)
if x == '<' then
return '&lt;'
elseif x == '>' then
return '&gt;'
elseif x == '&' then
return '&amp;'
elseif in_attribute and x == '"' then
return '&quot;'
elseif in_attribute and x == "'" then
return '&#39;'
else
return x
end
end)
end
-- Helper function to convert an attributes table into
-- a string that can be put into HTML tags.
local function attributes(attr)
local attr_table = {}
for x,y in pairs(attr) do
if y and y ~= '' then
table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"')
end
end
return table.concat(attr_table)
end
-- Table to store footnotes, so they can be included at the end.
local notes = {}
-- Blocksep is used to separate block elements.
function Blocksep()
return '\n\n'
end
-- This function is called once for the whole document. Parameters:
-- body is a string, metadata is a table, variables is a table.
-- This gives you a fragment. You could use the metadata table to
-- fill variables in a custom lua template. Or, pass `--template=...`
-- to pandoc, and pandoc will do the template processing as usual.
function Doc(body, metadata, variables)
local buffer = {}
local function add(s)
table.insert(buffer, s)
end
add(body)
if #notes > 0 then
add('<ol class="footnotes">')
for _,note in pairs(notes) do
add(note)
end
add('</ol>')
end
return table.concat(buffer,'\n') .. '\n'
end
-- The functions that follow render corresponding pandoc elements.
-- s is always a string, attr is always a table of attributes, and
-- items is always an array of strings (the items in a list).
-- Comments indicate the types of other variables.
function Str(s)
return escape(s)
end
function Space()
return ' '
end
function SoftBreak()
return '\n'
end
function LineBreak()
return '<br/>'
end
function Emph(s)
return '<em>' .. s .. '</em>'
end
function Strong(s)
return '<strong>' .. s .. '</strong>'
end
function Subscript(s)
return '<sub>' .. s .. '</sub>'
end
function Superscript(s)
return '<sup>' .. s .. '</sup>'
end
function SmallCaps(s)
return '<span style="font-variant: small-caps;">' .. s .. '</span>'
end
function Strikeout(s)
return '<del>' .. s .. '</del>'
end
function Link(s, tgt, tit, attr)
return '<a href="' .. escape(tgt,true) .. '" title="' ..
escape(tit,true) .. '"' .. attributes(attr) .. '>' .. s .. '</a>'
end
function Image(s, src, tit, attr)
return '<img src="' .. escape(src,true) .. '" title="' ..
escape(tit,true) .. '"/>'
end
function Code(s, attr)
return '<code' .. attributes(attr) .. '>' .. escape(s) .. '</code>'
end
function InlineMath(s)
return '\\(' .. escape(s) .. '\\)'
end
function DisplayMath(s)
return '\\[' .. escape(s) .. '\\]'
end
function SingleQuoted(s)
return '&lsquo;' .. s .. '&rsquo;'
end
function DoubleQuoted(s)
return '&ldquo;' .. s .. '&rdquo;'
end
function Note(s)
local num = #notes + 1
-- insert the back reference right before the final closing tag.
s = string.gsub(s,
'(.*)</', '%1 <a href="#fnref' .. num .. '">&#8617;</a></')
-- add a list item with the note to the note table.
table.insert(notes, '<li id="fn' .. num .. '">' .. s .. '</li>')
-- return the footnote reference, linked to the note.
return '<a id="fnref' .. num .. '" href="#fn' .. num ..
'"><sup>' .. num .. '</sup></a>'
end
function Span(s, attr)
return '<span' .. attributes(attr) .. '>' .. s .. '</span>'
end
function RawInline(format, str)
if format == 'html' then
return str
else
return ''
end
end
function Cite(s, cs)
local ids = {}
for _,cit in ipairs(cs) do
table.insert(ids, cit.citationId)
end
return '<span class="cite" data-citation-ids="' .. table.concat(ids, ',') ..
'">' .. s .. '</span>'
end
function Plain(s)
return s
end
function Para(s)
return '<p>' .. s .. '</p>'
end
-- lev is an integer, the header level.
function Header(lev, s, attr)
return '<h' .. lev .. attributes(attr) .. '>' .. s .. '</h' .. lev .. '>'
end
function BlockQuote(s)
return '<blockquote>\n' .. s .. '\n</blockquote>'
end
function HorizontalRule()
return "<hr/>"
end
function LineBlock(ls)
return '<div style="white-space: pre-line;">' .. table.concat(ls, '\n') ..
'</div>'
end
function CodeBlock(s, attr)
-- If code block has class 'dot', pipe the contents through dot
-- and base64, and include the base64-encoded png as a data: URL.
if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then
local img = pipe('base64', {}, pipe('dot', {'-T' .. image_format}, s))
return '<img src="data:' .. image_mime_type .. ';base64,' .. img .. '"/>'
-- otherwise treat as code (one could pipe through a highlighter)
else
return '<pre><code' .. attributes(attr) .. '>' .. escape(s) ..
'</code></pre>'
end
end
function BulletList(items)
local buffer = {}
for _, item in pairs(items) do
table.insert(buffer, '<li>' .. item .. '</li>')
end
return '<ul>\n' .. table.concat(buffer, '\n') .. '\n</ul>'
end
function OrderedList(items)
local buffer = {}
for _, item in pairs(items) do
table.insert(buffer, '<li>' .. item .. '</li>')
end
return '<ol>\n' .. table.concat(buffer, '\n') .. '\n</ol>'
end
function DefinitionList(items)
local buffer = {}
for _,item in pairs(items) do
local k, v = next(item)
table.insert(buffer, '<dt>' .. k .. '</dt>\n<dd>' ..
table.concat(v, '</dd>\n<dd>') .. '</dd>')
end
return '<dl>\n' .. table.concat(buffer, '\n') .. '\n</dl>'
end
-- Convert pandoc alignment to something HTML can use.
-- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault.
local function html_align(align)
if align == 'AlignLeft' then
return 'left'
elseif align == 'AlignRight' then
return 'right'
elseif align == 'AlignCenter' then
return 'center'
else
return 'left'
end
end
function CaptionedImage(src, tit, caption, attr)
if #caption == 0 then
return '<p><img src="' .. escape(src,true) .. '" id="' .. attr.id ..
'"/></p>'
else
local ecaption = escape(caption)
return '<figure>\n<img src="' .. escape(src,true) ..
'" id="' .. attr.id .. '" alt="' .. ecaption .. '"/>' ..
'<figcaption>' .. ecaption .. '</figcaption>\n</figure>'
end
end
function Figure(caption, contents, attr)
return '<figure' .. attributes(attr) .. '>\n' .. contents ..
'\n<figcaption>' .. caption .. '</figcaption>\n' ..
'</figure>'
end
-- Caption is a string, aligns is an array of strings,
-- widths is an array of floats, headers is an array of
-- strings, rows is an array of arrays of strings.
function Table(caption, aligns, widths, headers, rows)
local buffer = {}
local function add(s)
table.insert(buffer, s)
end
add('<table>')
if caption ~= '' then
add('<caption>' .. escape(caption) .. '</caption>')
end
if widths and widths[1] ~= 0 then
for _, w in pairs(widths) do
add('<col width="' .. string.format('%.0f%%', w * 100) .. '" />')
end
end
local header_row = {}
local empty_header = true
for i, h in pairs(headers) do
local align = html_align(aligns[i])
table.insert(header_row,'<th align="' .. align .. '">' .. h .. '</th>')
empty_header = empty_header and h == ''
end
if not empty_header then
add('<tr class="header">')
for _,h in pairs(header_row) do
add(h)
end
add('</tr>')
end
local class = 'even'
for _, row in pairs(rows) do
class = (class == 'even' and 'odd') or 'even'
add('<tr class="' .. class .. '">')
for i,c in pairs(row) do
add('<td align="' .. html_align(aligns[i]) .. '">' .. c .. '</td>')
end
add('</tr>')
end
add('</table>')
return table.concat(buffer,'\n')
end
function RawBlock(format, str)
if format == 'html' then
return str
else
return ''
end
end
function Div(s, attr)
return '<div' .. attributes(attr) .. '>\n' .. s .. '</div>'
end
-- The following code will produce runtime warnings when you haven't defined
-- all of the functions you need for the custom writer, so it's useful
-- to include when you're working on a writer.
local meta = {}
meta.__index =
function(_, key)
io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key))
return function() return '' end
end
setmetatable(_G, meta)