Skip to content

Instantly share code, notes, and snippets.

@wlupton
Last active November 11, 2022 14:22
Show Gist options
  • Save wlupton/2fc23fc1c4e4b4899121bfafc0fea4bf to your computer and use it in GitHub Desktop.
Save wlupton/2fc23fc1c4e4b4899121bfafc0fea4bf to your computer and use it in GitHub Desktop.
pandoc HTML multi-file writer (example based on https://pandoc.org/MANUAL.html#custom-readers-and-writers sample.lua)

html-multi-writer.lua is a sample pandoc custom writer based on the supplied sample.lua.

You'll need to use the supplied utils.lua (these are stubs and contain only the minimum needed to run the writer) and will need to get logging.lua from https://github.com/wlupton/pandoc-lua-logging.

This is intended as a demo just to show the approach. It is not likely to be directly usable. In particular, all generated YAML files will currently be empty.

With this input (in multi-test.md):

# A section

Some text.

# Another section

## A sub-section

# Yet another section

## Another sub-section {.new-page}

...and this command:

% pandoc multi-test.md -t html-multi-writer.lua -o multi-test.html

...the following output files are generated:

-rw-r--r--    1 william  staff       18  7 Nov 14:29 00-multi-test-contents.yaml
-rw-r--r--@   1 william  staff      600  7 Nov 14:29 00-multi-test-contents.html
-rw-r--r--    1 william  staff       95  7 Nov 14:29 01-multi-test-a-section.yaml
-rw-r--r--    1 william  staff      192  7 Nov 14:29 01-multi-test-a-section.html
-rw-r--r--    1 william  staff      113  7 Nov 14:29 02-multi-test-another-section.yaml
-rw-r--r--    1 william  staff      358  7 Nov 14:29 02-multi-test-another-section.html
-rw-r--r--    1 william  staff      125  7 Nov 14:29 03-multi-test-yet-another-section.yaml
-rw-r--r--    1 william  staff      412  7 Nov 14:29 03-multi-test-yet-another-section.html
-rw-r--r--    1 william  staff      334  7 Nov 14:29 98-multi-test-navigation.html
-rw-r--r--    1 william  staff       92  7 Nov 14:29 98-multi-test-navigation.yaml
-rw-r--r--    1 william  staff      607  7 Nov 14:29 96-multi-test-tocfull.html
-rw-r--r--    1 william  staff       83  7 Nov 14:29 96-multi-test-tocfull.yaml
-rw-r--r--    1 william  staff        3  7 Nov 14:29 97-multi-test-metadata.yaml
-rw-r--r--    1 william  staff      600  7 Nov 14:29 multi-test.html

The 00 and 9x filename prefixes are special, and the others (01, 02 etc.) are assigned sequentially. Taking the first as an example:

% cat 01-multi-test-a-section.html
<!-- <h1 class="display-none" id="a-section">A section<a class="headerlink" href="01-multi-test-a-section.html#a-section" title="Permalink to this header"> ¶</a></h1> -->

<p>Some text.</p>
% cat 01-multi-test-a-section.yaml
author: []
date: ''
doctitle: 'A section'
subtitle: ''
title: 'A section'
titleid: 'a-section'

These output files are intended to be passed through pandoc again to generate standalone documents using commands something like this (this is a cleaned-up version of a command from a different document):

% pandoc \
    --standalone \
    --metadata-file=97-example-metadata.yaml \
    --include-before-body=98-example-navigation.html \
    --include-before-body=96-example-tocfull.html \
    --include-in-header=example.html \
    --include-after-body=98-example-navigation.html \
    --template=github-template.html
    --output=example-final.html \
    /dev/null

This is using a slightly modified template (which I could supply). Including the HTML via --include-in-header was a workaround I think, and might no longer be necessary.

<!DOCTYPE html>
<!-- ORGANIZATION GitHub Pages pandoc template; modified from default.html template -->
<html xmlns="http://www.w3.org/1999/xhtml" lang="$lang$" xml:lang="$lang$"$if(dir)$ dir="$dir$"$endif$>
<head>
$if(analyticstag)$
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=$analyticstag$"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '$analyticstag$');
</script>
$endif$
<meta charset="utf-8"/>
<meta name="generator" content="pandoc"/>
$for(author-meta)$
<meta name="author" content="$author-meta$"/>
$endfor$
$if(description)$
<meta name="description" content="$description$"/>
$endif$
$if(keywords)$
<meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$"/>
$endif$
$if(date-meta)$
<meta name="dcterms.date" content="$date-meta$"/>
$endif$
$if(theme-color)$
<meta name="theme-color" content="$theme-color$"/>
$endif$
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"/>
<title>$if(title-prefix)$$title-prefix$ – $endif$$pagetitle$</title>
<style>
$styles.html()$
</style>
$for(css)$
<link rel="stylesheet" href="$css$"/>
$endfor$
$if(math)$
$math$
$endif$
<!--[if lt IE 9]>
<script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
<![endif]-->
<!-- XXX header includes are included just before the ToC (below) -->
</head>
<body>
<section class="page-header">
<h1 class="project-name">
<a href="$siteurl$" style="text-decoration: none; color: white;">
<img src="bbflogo-reverse-dark.png"/><br>
$project$
</a>
</h1>
<h2 class="project-tagline">$tagline$</h2>
<p>$version$</p>
<p>$status$</p>
$if(buttons)$
<div class="project-buttons">
$for(buttons)$
<a class="btn" href="$buttons.url$"$if(buttons.title)$ title="$buttons.title$"$endif$>$buttons.label$</a>
$endfor$
</div>
$endif$
</section>
<section class="main-content">
$for(include-before)$
$include-before$
$endfor$
$if(doctitle)$
<header id="title-block-header">
<h1 class="title"$if(titleid)$ id="$titleid$"$endif$>$doctitle$$if(titleid)$<a href="#$titleid$" class="headerlink" title="Permalink to this header"> $if(permalinkicon)$<img src="$permalinkicon-url$" style="width: $permalinkicon-width$"/>$else$¶$endif$</a>$endif$</h1>
$if(subtitle)$
<p class="subtitle">$subtitle$</p>
$endif$
$for(author)$
<p class="author">$author$</p>
$endfor$
$if(date)$
<p class="date">$date$</p>
$endif$
</header>
$endif$
$for(header-includes)$
$header-includes$
$endfor$
$if(toc)$
<nav id="$idprefix$TOC" role="doc-toc">
$if(toc-title)$
<h2 id="$idprefix$toc-title">$toc-title$</h2>
$endif$
$table-of-contents$
</nav>
$endif$
$body$
<div style="clear: both;"/>
<footer class="site-footer">
$for(include-after)$
$include-after$
$endfor$
<span class="site-footer-owner">
$if(shortname)$
$shortname$ is developed and maintained
by <a href="https://www.ORGANIZATION.org">ORGANIZATION</a>.
$endif$
$if(copydate)$
&copy; ORGANIZATION $copydate$</a>. All Rights Reserved.
$endif$
</span>
<span class="site-footer-credits">
This page was generated by <a href="https://pandoc.org">pandoc</a>
and <a href="https://pages.github.com">GitHub Pages</a>.
$if(reponame)$
View the <a href="https://github.com/ORGANIZATION/$reponame$">$repodesc$ on GitHub</a>.
$endif$
<span class="release"/>
</span>
</footer>
</section>
</body>
</html>
-- 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.
-- XXX need to update the comments; also consider removing the 'dot' feature
local pipe = pandoc.pipe
local stringify = (require "pandoc.utils").stringify
local utils = require 'utils'
-- logging level
if false then utils.setLogLevel(2) end
-- The global variable PANDOC_DOCUMENT contains the full AST of
-- the document which is going to be written. It can be used to
-- configure the writer.
local meta = PANDOC_DOCUMENT.meta
-- The global variable PANDOC_STATE contains common state.
local output_file = PANDOC_STATE.output_file
if output_file == nil then
error('HTML multi-writer requires -o (--output) to be specified')
end
-- Parse output file name
local output_file_dir, output_file_base = utils.path.split(output_file)
local output_file_name, output_file_ext = utils.path.splitext(output_file_base)
utils.debug('output_file', output_file, '->', 'dir', output_file_dir,
'base', output_file_base, 'name', output_file_name, 'ext',
output_file_ext)
-- Choose the image format based on the value of the `image_format` meta value.
local image_format = meta.image_format
and stringify(meta.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 .. "`")
-- Add link (href) to header or other anchor element
local permalinkicon = nil -- set lazily
local function headerlink(identifier, element)
if permalinkicon == nil then
-- XXX assumed to be Inlines with element 1 an image
local inlines = meta.permalinkicon
local icon = inlines and inlines[1] or nil
if not icon then
permalinkicon = '\xC2\xB6' -- pilcrow (paragraph) sign
else
local attr = pandoc.List()
attr.id = icon.attr.id
attr.class = table.concat(icon.attr.classes, ' ')
for n, v in pairs(icon.attr.attributes) do
attr[n] = v
end
-- this is the Image() writer function, not pandoc.Image()!
permalinkicon = Image(stringify(icon.caption), icon.src,
icon.title, attr)
end
end
local element = element or 'header'
return #identifier > 0 and
('<a class="headerlink" href="#' .. identifier ..
'" title="Permalink to this ' .. element .. '"> ' ..
permalinkicon .. '</a>')
or ''
end
-- 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.
-- YYY order is unpredictable; should sort them
local function attributes(attr)
local attr_table = {}
-- XXX these will be in alphabetical order; want id first?
for x, y in utils.spairs(attr) do
if y and y ~= "" then
table.insert(attr_table, ' ' .. x .. '="' .. escape(y,true) .. '"')
end
end
return table.concat(attr_table)
end
-- Helper function to split a string into a list of lines. The second optional
-- argument should be called to add lines (otherwise return them)
-- XXX sadly there's isn't a standard string.split() function
-- XXX this add() technique looks useful; I invented it! (others will have too)
local function splitlines(body, add)
local lines = {}
if add == nil then
add = function(s)
table.insert(lines, s)
end
end
local index = 1 -- next character to be processed
while index <= #body do
local newline = body:find('\n', index)
if not newline then
add(body:sub(index))
break
end
add(body:sub(index, newline - 1)) -- omit the newline
index = newline + 1 -- next character is after the newline
end
return lines -- will be empty if add() was supplied
end
-- Variables to use as the 'section name' part of filenames
-- XXX these should perhaps be configurable
local section_name = 'section'
local contents_name = 'contents'
local footnotes_name = 'footnotes'
local navigation_name = 'navigation'
local metadata_name = 'metadata'
local tocfull_name = 'tocfull'
-- File info mapping file type to details
-- * num is file number; nil means auto-assigned 1, 2, ...
-- * ext is default extension; can be overridden; nil means output file ext
-- * lab is label; can be overridden; nil means capitalized key
local fileinfo = {
[section_name] = {num=nil, ext=nil},
[contents_name] = {num=0, ext=nil, lab='Cover Page'},
[footnotes_name] = {num=-1, ext=nil},
[navigation_name] = {num=-2, ext=nil},
[metadata_name] = {num=-3, ext='.yaml'},
[tocfull_name] = {num=-4, ext=nil},
}
-- Variable to count the number of output files (excluding the 'before' and
-- 'after' files)
local num_files = 0
-- Variable to map from id to 1-based file number
local idtonum = {}
-- Helper function to record attributes so links can be rewritten to reference
-- the correct files
local function record(id, num)
num = num or num_files -- defaults to num_files (current file number)
if id and #id > 0 then
idtonum[id] = num
end
end
-- Helper function to parse a line's '#bbf' directives
local function parseline(line)
local directives = {}
-- #bbf-new-file NUM ID S
-- XXX need some error checking?
local num, id, s =
line:match('#bbf%-new%-file%s+(%d+)%s+(%S+)%s+(.*)%s%-%->')
if num ~= nil then
directives.new_file = {num=tonumber(num), id=id, s=s}
end
-- #bbf-after-body
local after_body = line:match('#bbf%-after%-body')
if after_body ~= nil then
directives.after_body = true
end
return directives
end
-- Variable to map from file number to filename
-- XXX file number is zero for the 'contents' file; is this a problem?
local filenames = {}
-- Helper functions relating to file numbering
local filenum = {} -- namespace
-- Return the width of the file number format; minimum 2
filenum.width = function()
-- +10 forces at least two digits and puts space between upward-counting
-- and downward-counting file numbers
return #tostring(num_files + 10)
end
-- Return the maximum file number, i.e. the 'after' file number
filenum.max = function()
return tonumber(string.rep('9', filenum.width()))
end
-- Return the number of a given file
filenum.number = function(n, default)
if default == nil then default = 0 end
return n == nil and default or n < 0 and (filenum.max() + n + 1) or n
end
-- Helper function to generate output filenames
-- XXX should be within the filenum (renamed?) namespace; should use classes
-- XXX maybe don't treat num < 0 specially; caller can pass filenum.max()
local function filename(type, opts)
-- XXX should check type is valid?
local info = fileinfo[type]
opts = opts or {}
local function format_number(n, default)
local format = string.format('%%0%dd', filenum.width())
return string.format(format, filenum.number(n, default))
end
local function remove_prefix(s, default)
return s == nil and default or s:gsub('.*:', '')
end
local num = format_number(opts.num or info.num, 0)
local id = remove_prefix(opts.id or info.id, type)
local ext = opts.ext or info.ext or output_file_ext
local name = string.format('%s%s-%s-%s%s', output_file_dir, num,
output_file_name, id, ext)
-- XXX only save first time in case this filenum is used with multiple exts
local num_ = tonumber(num)
if filenames[num_] == nil then
filenames[num_] = name
end
return name
end
-- Helper function to get file labels
local function filelabel(type)
-- XXX should check type is valid?
return fileinfo[type].lab or utils.capitalize(type)
end
-- Helper function to fix references (hrefs) to include the filename; the add
-- argument is as for splitlines()
-- XXX could add a num argument so it knows not to include the filename for
-- local references; or get the num from the first line comment (would
-- need to add it)
local function fixrefs(lines, add)
local buffer = {}
if add == nil then
add = function(s)
table.insert(buffer, s)
end
end
for _, line in ipairs(lines) do
-- XXX this isn't quite safe: should require matching quote type;
-- unfortunately this file isn't consistent about which type of
-- quotes it uses for attributes (could fix this)
line = line:gsub(
'(href%s*=%s*)(["\'])(.-)(["\'])',
function(prefix, lquot, href, rquot)
if prefix ~= nil and href:sub(1, 1) == '#' then
local num = idtonum[href:sub(2)]
if num == nil then
-- XXX this should already have been reported by the
-- check-links.lua filter, so report at the info
-- rather than warning level
utils.info('can\'t determine HTML filename for',
'undefined anchor', href:sub(2))
num = 0
end
-- XXX this is very tricky! assumes knowledge of fragment
-- and final file extensions (needs to be reviewed)
local path = filenames[num]:gsub('%.htmf$', '.html')
local base = utils.path.basename(path)
utils.debug2('href', href:sub(2), '->', num, base)
return prefix .. lquot .. base .. href .. rquot;
end
end)
add(line)
end
return buffer
end
-- Helper function to write an list of lines to the specified file
local function writefile(name, lines)
local file = io.open(name, 'w+')
if file == nil then
error('can\'t create file ' + name)
end
file:write(table.concat(lines, '\n') .. '\n')
file:close()
utils.info('wrote', #lines, 'lines to', name)
end
-- Table to store headers, so can generate table of contents
local headers = {}
-- Table to store footnotes, so they can be included at the end
local footnotes = {}
-- Helper function to add a ToC of the specified style; the add argument is as
-- for splitlines()
local function tableofcontents(style, add, metadata)
-- XXX styles are experimental; maybe it's better to pass in the table
-- rather than the style name
local maxlev = tonumber(meta['toc-depth'])
local styles = {
contents = {header=true, contents=false, list='ul', item='li',
maxlev=maxlev, unlisted='unlisted', sep='', navid=nil},
navigation = {header=nil, contents=true, list='', list0='p', item='',
maxlev=1, unlisted='same-file', sep=' | ', navid=nil},
tocfull = {header=nil, contents=true, list='ul', item='li',
maxlev=maxlev, unlisted='unlisted', sep='',
navid='TOCFULL'},
}
style = style or 'contents'
local buffer = {}
if add == nil then
add = function(s)
table.insert(buffer, s)
end
end
-- helper for opening and closing lists
local lev = 0
local function openclose(header_lev)
local elem = styles[style].list
local elem0 = styles[style].list0 or elem
while lev > header_lev do
lev = lev - 1
local elem_ = lev == 0 and elem0 or elem
if #elem_ > 0 then
add(string.rep(' ', lev) .. '</' .. elem_ .. '>')
end
end
while lev < header_lev do
local elem_ = lev == 0 and elem0 or elem
if #elem_ > 0 then
add(string.rep(' ', lev) .. '<' .. elem_ .. '>')
end
lev = lev + 1
end
end
-- helper for adding items
local first = true
local function additem(id, s)
local elem = styles[style].item
local sep = styles[style].sep
sep = first and string.rep(' ', #sep) or sep
local ind = #sep == 0 and string.rep(' ', lev) or ''
local open = #elem > 0 and '<' .. elem .. '>' or ''
local close = #elem > 0 and '</' .. elem .. '>' or ''
add(string.format('%s%s%s<a href="#%s">%s</a>%s', ind, sep, open, id,
s, close))
first = false
end
-- optionally insert an entry at the start as a contents link
-- XXX should use variables for id and s
local before = {}
if styles[style].contents then
before = {{lev=1, attr={id=contents_name},
s=filelabel(contents_name)}}
idtonum[contents_name] = filenum.number(0)
end
-- if footnotes, insert an entry at the end as a footnotes link
-- XXX should use variables for id and s
local after = {}
if #footnotes > 0 then
after = {{lev=1, attr={id=footnotes_name},
s=filelabel(footnotes_name)}}
idtonum[footnotes_name] = filenum.number(-1)
end
-- count the actual number of items
-- XXX this logic is (unfortunately) duplicated below; should use a
-- visitor pattern
local maxlev = styles[style].maxlev
local unlisted_class = styles[style].unlisted or 'unlisted'
local total = 0
for _, headers_ in ipairs({before, headers, after}) do
for _, header in ipairs(headers_) do
local new_file = utils.contains(header.attr.class, 'new-file')
local unlisted = utils.contains(header.attr.class, unlisted_class)
if ((maxlev == nil or header.lev <= maxlev or new_file) and
(not unlisted)) then
total = total + 1
end
end
end
-- only output the ToC if it contains at least two items
if total >= #before + 2 then
-- optionally insert a ToC header
-- XXX tocTitle should be defined alongside pandoc-crossref lofTitle,
-- lotTitle etc.
if styles[style].header then
local tocTitle = metadata.tocTitle or '<h3 class="unnumbered ' ..
'unlisted" id="table-of-contents">Table of Contents</h3>'
add(tocTitle)
add('')
end
-- use the <nav> tag (this is what the pandoc HTML writer does)
-- XXX maxlev seems off by one sometimes? need to investigate
local navid = styles[style].navid
add(navid and '<nav id="' .. navid .. '">' or '<nav>')
for _, headers_ in ipairs({before, headers, after}) do
for _, header in ipairs(headers_) do
local new_file = utils.contains(header.attr.class, 'new-file')
local unlisted = utils.contains(header.attr.class,
unlisted_class)
if ((maxlev == nil or header.lev <= maxlev or new_file) and
(not unlisted)) then
openclose(header.lev)
additem(header.attr.id, header.s)
end
end
end
openclose(0)
add('</nav>')
add('')
end
return buffer
end
-- 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 lines = {}
local function add(s)
table.insert(lines, s)
end
-- insert the ToC at the beginning; can't use standard pandoc ToC because
-- pandoc doesn't know about multiple output files
tableofcontents('contents', add, metadata)
-- the body is a single string (I don't think this can be avoided),
-- so split it into lines
splitlines(body, add)
-- mark the end of the body
add('<!-- #bbf-after-body -->')
-- process footnotes
-- XXX should get heading from metadata
if #footnotes > 0 then
local title = 'Footnotes'
add('<ol class="footnotes">')
for _, note in pairs(footnotes) do
for _, line in ipairs(splitlines(note)) do
for prefix, lquot, id, rquot in
line:gmatch('(id%s*=%s*)(["\'])(.-)(["\'])') do
record(id, filenum.max())
end
add(line)
end
end
add('</ol>')
end
-- process the lines, generating filenames
-- XXX could do this as we go, except that the filename is a function of
-- #files
filename(contents_name)
for _, line in ipairs(lines) do
local directives = parseline(line)
local new_file = directives.new_file
if new_file then
filename(section_name, {num=new_file.num, id=new_file.id})
end
end
filename(footnotes_name)
filename(navigation_name)
filename(metadata_name)
filename(tocfull_name)
-- process the lines, splitting into multiple output files
local buffer = {}
local retval = {}
local state = 'start'
local file = filename(contents_name)
writefile(filename(contents_name, {ext='.yaml'}), utils.toyaml(
{
title=metadata.title,
titleid=contents_name,
doctitle=metadata.doctitle,
subtitle=metadata.subtitle,
author=metadata.author,
date=metadata.date
}))
utils.debug('state', state)
for _, line in ipairs(lines) do
utils.debug2('line', line)
local directives = parseline(line)
local new_file = directives.new_file
local after_body = directives.after_body
local new_state = state
if not new_file and not after_body then
table.insert(buffer, line)
else
buffer = fixrefs(buffer)
-- return value is the 'contents' file
if state == 'start' then
retval = buffer
utils.info('saved', #buffer, 'lines as return value')
end
writefile(file, buffer)
buffer = {}
new_state = new_file and 'in-file' or 'after-files'
end
if new_file then
file = filename(section_name, {num=new_file.num, id=new_file.id})
writefile(filename(section_name, {num=new_file.num, id=new_file.id,
ext='.yaml'}), utils.toyaml(
{
title=new_file.s,
titleid=new_file.id,
doctitle=new_file.s,
subtitle='',
author={},
date=''
}))
elseif after_body and #footnotes > 0 then
file = filename(footnotes_name)
local Footnotes_name = filelabel(footnotes_name)
writefile(filename(footnotes_name, {ext='.yaml'}),
utils.toyaml(
{
title=Footnotes_name,
titleid=footnotes_name,
doctitle=Footnotes_name,
subtitle='',
author={},
date=''
}))
end
if new_state ~= state then
utils.debug('state', state, '->', new_state)
state = new_state
end
end
-- write any unwritten lines
if #buffer > 0 then
writefile(file, fixrefs(buffer))
end
-- write the 'navigation' file
local Navigation_name = filelabel(navigation_name)
writefile(filename(navigation_name),
fixrefs(tableofcontents('navigation', nil, metadata)))
writefile(filename(navigation_name, {ext='.yaml'}), utils.toyaml(
{
title=Navigation_name,
titleid=navigation_name,
doctitle=Navigation_name,
subtitle='',
author={},
date=''
}))
-- XXX and an experimental full-contents file
local Tocfull_name = filelabel(tocfull_name)
writefile(filename(tocfull_name),
fixrefs(tableofcontents('tocfull', nil, metadata)))
writefile(filename(tocfull_name, {ext='.yaml'}), utils.toyaml(
{
title=Tocfull_name,
titleid=tocfull_name,
doctitle=Tocfull_name,
subtitle='',
author={},
date=''
}))
-- write the 'metadata' file
-- XXX PANDOC_DOCUMENT.meta and the metadata argument are not the same!
-- here we use the argument because it's a simple flat list
writefile(filename(metadata_name), utils.toyaml(metadata))
-- return the previously-saved contents of the 'contents' file
return table.concat(retval, '\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 Underline(s)
return "<u>" .. s .. "</u>"
end
function Link(s, tgt, tit, attr)
record(attr.id)
return "<a href='" .. escape(tgt,true) .. "' title='" ..
escape(tit,true) .. "'" .. attributes(attr) .. ">" .. s .. "</a>"
end
function Image(s, src, tit, attr)
record(attr.id)
return "<img src='" .. escape(src,true) .. "' title='" ..
escape(tit,true) .. "'" .. attributes(attr) .. "/>"
end
function Code(s, attr)
record(attr.id)
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 = #footnotes + 1
local fnid = 'fn' .. num
local fnrefid = 'fnref' .. num
-- insert the back reference right before the final closing tag.
s = string.gsub(s,
'(.*)</', '%1 <a href="#' .. fnrefid ..
'">&#8617;</a></')
-- add a list item with the note to the note table.
-- (don't record the id, because it's not in the current file)
table.insert(footnotes, '<li id="' .. fnid .. '">' .. s .. '</li>')
-- return the footnote reference, linked to the note.
record(fnrefid)
return '<a id="' .. fnrefid .. '" href="#' .. fnid ..
'"><sup>' .. num .. '</sup></a>'
end
function Span(s, attr)
record(attr.id)
return "<span" .. attributes(attr) .. ">" .. s ..
headerlink(attr.id, 'span') .. "</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)
-- save for later ToC generation
table.insert(headers, {lev=lev, s=s, attr=attr})
local directive = ''
local prefix = ''
local suffix = ''
-- XXX attr.id check is (a) common sense, and (b) to omit 'List of...'
-- sections, which are a consequence of pandoc-crossref lotTitle etc.
-- being headers
if #attr.id > 0 and (not utils.contains(attr.class, 'same-file')) and
(lev == 1 or utils.contains(attr.class, 'new-file')) then
num_files = num_files + 1
directive = string.format('<!-- #bbf-new-file %d %s %s -->\n',
num_files, attr.id, s)
utils.debug2(directive:sub(1, -2))
-- XXX the header is commented so it doesn't appear in the ToC
prefix = '<!-- '
suffix = ' -->'
attr.class = attr.class .. (#attr.class == 0 and '' or ' ') ..
'display-none'
end
-- do this here because it needs to be done after num_files++
record(attr.id)
return directive .. prefix ..
"<h" .. lev .. attributes(attr) .. ">" .. s .. headerlink(attr.id) ..
"</h" .. lev .. ">" ..
suffix
end
function BlockQuote(s)
return "<blockquote>\n" .. s .. "\n</blockquote>"
end
function HorizontalRule()
return "<hr/>"
end
function LineBlock(ls)
-- include a line break in case this HTML will be re-processed by pandoc,
-- in which case only the class would indicate line break preservation
return '<div class="line-block">' .. table.concat(ls, '<br/>\n') ..
'</div>'
end
function CodeBlock(s, attr)
record(attr.id)
-- 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)
record(attr.id)
if #caption == 0 then
return '<img src="' .. escape(src,true) .. '"' .. attributes(attr) ..
' alt=""/>'
else
return '<figure>\n<img src="' .. escape(src,true) .. '"' ..
attributes(attr) .. ' alt=""/>' .. '\n<figcaption>' ..
escape(caption) .. headerlink(attr.id, 'figure') ..
'</figcaption>\n</figure>'
end
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.
local table_caption = '' -- see Div below
function Table(caption, aligns, widths, headers, rows)
local buffer = {}
local function add(s)
table.insert(buffer, s)
end
add("<table>")
table_caption = ''
if caption ~= "" then
table_caption = "<caption>" .. escape(caption)
add(table_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)
record(attr.id)
-- if div contains current table caption, add headerlink to it
-- otherwise don't add headerlink; it takes up space and looks bad
if #table_caption > 0 and s:find(table_caption, 1, true) then
local first, last = s:find(table_caption, 1, true)
s = s:sub(1, last) .. headerlink(attr.id, 'table') ..
s:sub(last + 1, -1)
table_caption = ''
end
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)
-- utils.lua stubs (just sufficient to use html-multi-writer.lua)
-- note that creation of YAML files isn't supported (they'll be empty)
local utils = {}
utils.capitalize = function(text)
return text:gsub('%w+',
function(word)
return word:sub(1, 1):upper() .. word:sub(2) end)
end
utils.contains = function(s, p, sep)
if not s or not p then return nil end
local sep_ = sep or ' '
local s_ = sep_ .. s .. sep_
local p_ = sep_ .. p .. sep_
local first, last = s_:find (p_, 1, true)
if first ~= nil then
last = last - 2 * #sep_
end
return first, last
end
utils.path = {}
utils.path.basename = function(path)
local _, base = utils.path.split(path)
return base
end
utils.path.dirname = function(path)
local dir, _ = utils.path.split(path)
return dir
end
utils.path.split = function(path)
local dir, base = path:match('(.*/)(.*)')
if dir == nil then dir, base = '', path end
return dir, base
end
utils.path.splitext = function(path)
local root, ext = path:match('(.-)(%..*)')
if root == nil or root == '' then root, ext = path, '' end
return root, ext
end
utils.spairs = pairs
utils.toyaml = function(...) return {} end
-- get this from https://github.com/wlupton/pandoc-lua-logging
for name, value in pairs(require "logging") do
utils[name] = value
end
utils.setLogLevel = utils.setloglevel
return utils
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment