|
-- 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 '<' |
|
elseif x == '>' then |
|
return '>' |
|
elseif x == '&' then |
|
return '&' |
|
elseif in_attribute and x == '"' then |
|
return '"' |
|
elseif in_attribute and x == "'" then |
|
return ''' |
|
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 "‘" .. s .. "’" |
|
end |
|
|
|
function DoubleQuoted(s) |
|
return "“" .. s .. "”" |
|
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 .. |
|
'">↩</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) |