181 lines
5.2 KiB
Lua
181 lines
5.2 KiB
Lua
-- Prepare LaTeX headers so quotation marks render correctly in the PDF body
|
||
-- while keeping proper Unicode text in PDF bookmarks.
|
||
|
||
--- Copyright: © 2025–Present Tom Ben
|
||
--- License: MIT License
|
||
|
||
local function format_is(name)
|
||
return FORMAT:match(name) ~= nil
|
||
end
|
||
|
||
local function to_macro_block(inlines)
|
||
return pandoc.walk_block(pandoc.Plain(inlines), {
|
||
Quoted = function(el)
|
||
local opening, closing
|
||
if el.quotetype == "SingleQuote" then
|
||
opening = pandoc.RawInline('latex', "`")
|
||
closing = pandoc.RawInline('latex', "'")
|
||
else
|
||
opening = pandoc.RawInline('latex', "``")
|
||
closing = pandoc.RawInline('latex', "''")
|
||
end
|
||
|
||
local result = pandoc.Inlines { opening }
|
||
for _, inline in ipairs(el.content) do
|
||
result:insert(inline)
|
||
end
|
||
result:insert(closing)
|
||
return result
|
||
end
|
||
})
|
||
end
|
||
|
||
local function to_unicode_block(inlines)
|
||
return pandoc.walk_block(pandoc.Plain(inlines), {
|
||
Quoted = function(el)
|
||
local opening, closing
|
||
if el.quotetype == "SingleQuote" then
|
||
opening, closing = '‘', '’'
|
||
else
|
||
opening, closing = '“', '”'
|
||
end
|
||
|
||
local result = pandoc.Inlines { pandoc.Str(opening) }
|
||
for _, inline in ipairs(el.content) do
|
||
result:insert(inline)
|
||
end
|
||
result:insert(pandoc.Str(closing))
|
||
return result
|
||
end,
|
||
Str = function(el)
|
||
-- Replace guillemets with curly quotes for PDF bookmarks
|
||
local text = el.text
|
||
text = text:gsub('«', '“')
|
||
text = text:gsub('»', '”')
|
||
text = text:gsub('‹', '‘')
|
||
text = text:gsub('›', '’')
|
||
if text ~= el.text then
|
||
return pandoc.Str(text)
|
||
end
|
||
return el
|
||
end
|
||
})
|
||
end
|
||
|
||
function Header(header)
|
||
if not format_is('latex') then
|
||
return header
|
||
end
|
||
|
||
local attr = header.attr
|
||
local macro_block = to_macro_block(header.content)
|
||
local unicode_block = to_unicode_block(header.content)
|
||
|
||
local latex_doc = pandoc.Pandoc({ macro_block }, pandoc.Meta {})
|
||
local latex_body = pandoc.write(latex_doc, 'latex')
|
||
:gsub('%s+$', '')
|
||
:gsub('\n', ' ')
|
||
local bookmark_text = pandoc.utils.stringify(unicode_block)
|
||
:gsub('\n', ' ')
|
||
|
||
attr.attributes['data-tex-body'] = latex_body
|
||
attr.attributes['data-bookmark'] = bookmark_text
|
||
|
||
return header
|
||
end
|
||
|
||
local latex_heading_levels = {
|
||
"section",
|
||
"subsection",
|
||
"subsubsection",
|
||
"paragraph",
|
||
"subparagraph",
|
||
"subparagraph"
|
||
}
|
||
|
||
local function escape_tex_argument(text)
|
||
local map = {
|
||
['\\'] = '\\\\',
|
||
['{'] = '\\{',
|
||
['}'] = '\\}',
|
||
['%'] = '\\%',
|
||
['#'] = '\\#',
|
||
['&'] = '\\&',
|
||
['_'] = '\\_',
|
||
['^'] = '\\^{}',
|
||
['~'] = '\\~{}'
|
||
}
|
||
return text:gsub('[\\%%#&_{}%^~]', map)
|
||
end
|
||
|
||
local function heading_to_raw(header)
|
||
local tex_body = header.attr.attributes['data-tex-body']
|
||
if not tex_body then
|
||
return nil
|
||
end
|
||
|
||
local bookmark = header.attr.attributes['data-bookmark'] or pandoc.utils.stringify(header)
|
||
header.attr.attributes['data-tex-body'] = nil
|
||
header.attr.attributes['data-bookmark'] = nil
|
||
local short_attr = header.attr.attributes['short-title'] or header.attr.attributes['short']
|
||
if short_attr and short_attr ~= '' then
|
||
bookmark = short_attr
|
||
end
|
||
bookmark = tostring(bookmark):gsub('\n', ' ')
|
||
local level_index = math.min(header.level, #latex_heading_levels)
|
||
local command = latex_heading_levels[level_index]
|
||
|
||
local classes = header.attr.classes or {}
|
||
local unnumbered = false
|
||
for _, class in ipairs(classes) do
|
||
if class == 'unnumbered' then
|
||
unnumbered = true
|
||
break
|
||
end
|
||
end
|
||
|
||
local number_attr = header.attr.attributes['number']
|
||
if number_attr == 'no' or number_attr == 'false' or number_attr == '0' then
|
||
unnumbered = true
|
||
end
|
||
|
||
local star = unnumbered and '*' or ''
|
||
local bookmark_escaped = escape_tex_argument(bookmark)
|
||
local texorpdfstring = '\\texorpdfstring{' .. tex_body .. '}{' .. bookmark_escaped .. '}'
|
||
local pieces = { '\\' .. command .. star .. '{' .. texorpdfstring .. '}' }
|
||
|
||
local identifier = header.attr.identifier
|
||
if identifier and identifier ~= '' then
|
||
table.insert(pieces, '\\label{' .. identifier .. '}')
|
||
end
|
||
|
||
if unnumbered then
|
||
table.insert(pieces, '\\addcontentsline{toc}{' .. command .. '}{' .. bookmark_escaped .. '}')
|
||
end
|
||
|
||
return pandoc.RawBlock('latex', table.concat(pieces, '') .. '\n')
|
||
end
|
||
|
||
function Pandoc(doc)
|
||
if not format_is('latex') then
|
||
return doc
|
||
end
|
||
|
||
local blocks = pandoc.List:new()
|
||
for _, block in ipairs(doc.blocks) do
|
||
if block.t == 'Header' then
|
||
blocks:insert(heading_to_raw(block) or block)
|
||
else
|
||
blocks:insert(block)
|
||
end
|
||
end
|
||
|
||
doc.blocks = blocks
|
||
return doc
|
||
end
|
||
|
||
return {
|
||
{ Header = Header },
|
||
{ Pandoc = Pandoc }
|
||
}
|