249 lines
8 KiB
Lua
249 lines
8 KiB
Lua
|
|
--[[
|
||
|
|
Lua Filter to parse acronyms in a Markdown document.
|
||
|
|
|
||
|
|
Acronyms must be in the form `\acr{key}` where key is the acronym key.
|
||
|
|
The first occurrence of an acronym is replaced by its long name, as
|
||
|
|
defined by a list of acronyms in the document's metadata.
|
||
|
|
Other occurrences are simply replaced by the acronym's short name.
|
||
|
|
|
||
|
|
A List of Acronym is also generated (similar to a Glossary in LaTeX),
|
||
|
|
and all occurrences contain a link to the acronym's definition in this
|
||
|
|
List.
|
||
|
|
]]
|
||
|
|
|
||
|
|
-- We want to require the Lua files which are in the same folder.
|
||
|
|
-- However, as we are invoking this file through Pandoc (and potentially
|
||
|
|
-- Quarto), we do not have control over the `LUA_PATH` environment variable,
|
||
|
|
-- nor the current working directory.
|
||
|
|
-- It seems to me that we need to add this current file's directory
|
||
|
|
-- to the list of searched directories, i.e., `package.path`.
|
||
|
|
local current_dir = debug.getinfo(1).source:match("@?(.*/)")
|
||
|
|
package.path = package.path .. ";" .. current_dir .. "/?.lua"
|
||
|
|
|
||
|
|
-- Some helper functions
|
||
|
|
local Helpers = require("acronyms_helpers")
|
||
|
|
|
||
|
|
-- The Acronyms database
|
||
|
|
local Acronyms = require("acronyms")
|
||
|
|
|
||
|
|
-- Sorting function
|
||
|
|
local sortAcronyms = require("sort_acronyms")
|
||
|
|
|
||
|
|
-- Replacement function (handling styles)
|
||
|
|
local replaceExistingAcronymWithStyle = require("acronyms_styles")
|
||
|
|
|
||
|
|
-- The options for the List Of Acronyms, as defined in the document's metadata.
|
||
|
|
local Options = require("acronyms_options")
|
||
|
|
|
||
|
|
--[[
|
||
|
|
The current "usage order" value.
|
||
|
|
We increment this value each time we find a new acronym, and we use it
|
||
|
|
to register the order in which acronyms appear.
|
||
|
|
--]]
|
||
|
|
local current_order = 0
|
||
|
|
|
||
|
|
|
||
|
|
-- A helper function to print warnings
|
||
|
|
function warn(...)
|
||
|
|
-- Handle variadic args: use `tostring` to avoid errors
|
||
|
|
-- (in particular for table or nil values)
|
||
|
|
local t = table.pack(...)
|
||
|
|
for i=1, t.n do
|
||
|
|
t[i] = tostring(t[i])
|
||
|
|
end
|
||
|
|
local msg = table.concat(t, "")
|
||
|
|
io.stderr:write("[WARNING][acronymsdown] ", msg, "\n")
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
function Meta(m)
|
||
|
|
Options:parseOptionsFromMetadata(m)
|
||
|
|
|
||
|
|
-- Parse acronyms directly from the metadata (`acronyms.keys`)
|
||
|
|
Acronyms:parseFromMetadata(m, Options["on_duplicate"])
|
||
|
|
|
||
|
|
-- Parse acronyms from external files
|
||
|
|
if (m and m.acronyms and m.acronyms.fromfile) then
|
||
|
|
if Helpers.isMetaList(m.acronyms.fromfile) then
|
||
|
|
-- We have several files to read
|
||
|
|
for _, filepath in ipairs(m.acronyms.fromfile) do
|
||
|
|
filepath = pandoc.utils.stringify(filepath)
|
||
|
|
Acronyms:parseFromYamlFile(filepath, Options["on_duplicate"])
|
||
|
|
end
|
||
|
|
else
|
||
|
|
-- We have a single file
|
||
|
|
local filepath = pandoc.utils.stringify(m.acronyms.fromfile)
|
||
|
|
Acronyms:parseFromYamlFile(filepath, Options["on_duplicate"])
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Generate the List Of Acronyms.
|
||
|
|
Returns 2 values: the Header, and the DefinitionList.
|
||
|
|
--]]
|
||
|
|
function generateLoA()
|
||
|
|
-- Original idea from https://gist.github.com/RLesur/e81358c11031d06e40b8fef9fdfb2682
|
||
|
|
|
||
|
|
-- We first get the list of sorted acronyms, according to the defined criteria.
|
||
|
|
local sorted = sortAcronyms(Acronyms.acronyms,
|
||
|
|
Options["sorting"],
|
||
|
|
Options["include_unused"])
|
||
|
|
|
||
|
|
-- Create the table that represents the DefinitionList
|
||
|
|
local definition_list = {}
|
||
|
|
for _, acronym in ipairs(sorted) do
|
||
|
|
-- The definition's name. A Span with an ID so we can create a link.
|
||
|
|
local name = pandoc.Span(acronym.shortname,
|
||
|
|
pandoc.Attr(Helpers.key_to_id(acronym.key), {}, {}))
|
||
|
|
-- The definition's value.
|
||
|
|
local definition = pandoc.Plain(acronym.longname)
|
||
|
|
table.insert(definition_list, { name, definition })
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Create the Header (only if the title is not empty)
|
||
|
|
local header = nil
|
||
|
|
if Options["loa_title"] ~= "" then
|
||
|
|
local loa_classes = {"loa"}
|
||
|
|
header = pandoc.Header(1,
|
||
|
|
{ table.unpack(Options["loa_title"]) },
|
||
|
|
pandoc.Attr(Helpers.key_to_id("HEADER_LOA"), loa_classes, {})
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
return header, pandoc.DefinitionList(definition_list)
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Append the List Of Acronyms to the document (at the beginning).
|
||
|
|
--]]
|
||
|
|
function appendLoA(doc)
|
||
|
|
local pos
|
||
|
|
if not Options["insert_loa"] then
|
||
|
|
-- If disabled, do nothing
|
||
|
|
return nil
|
||
|
|
elseif Options["insert_loa"] == "beginning" then
|
||
|
|
-- Insert at the first block in the document
|
||
|
|
pos = 1
|
||
|
|
elseif Options["insert_loa"] == "end" then
|
||
|
|
-- Insert at the last block in the document
|
||
|
|
pos = #doc.blocks + 1
|
||
|
|
else
|
||
|
|
error("Unrecognized option insert_loa="
|
||
|
|
.. tostring(Options["insert_loa"]))
|
||
|
|
end
|
||
|
|
|
||
|
|
local header, definition_list = generateLoA()
|
||
|
|
|
||
|
|
-- Insert the DefinitionList
|
||
|
|
table.insert(doc.blocks, pos, definition_list)
|
||
|
|
|
||
|
|
-- Insert the Header
|
||
|
|
if header ~= nil then
|
||
|
|
table.insert(doc.blocks, pos, header)
|
||
|
|
end
|
||
|
|
|
||
|
|
return pandoc.Pandoc(doc.blocks, doc.meta)
|
||
|
|
end
|
||
|
|
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Place the List Of Acronyms in the document (in place of a `\printacronyms` block).
|
||
|
|
Since Header and DefinitionList are Blocks, we need to replace a Block
|
||
|
|
(Pandoc does not allow to create Blocks from Inlines).
|
||
|
|
Thus, `\printacronyms` needs to be in its own Block (no other text!).
|
||
|
|
--]]
|
||
|
|
function RawBlock(el)
|
||
|
|
-- The block's content must be exactly "\printacronyms"
|
||
|
|
if not (el and el.text == "\\printacronyms") then
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
|
||
|
|
local header, definition_list = generateLoA()
|
||
|
|
|
||
|
|
if header ~= nil then
|
||
|
|
return { header, definition_list }
|
||
|
|
else
|
||
|
|
return definition_list
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Replace an acronym `\acr{KEY}`, where KEY is not in the `acronyms` table.
|
||
|
|
According to the options, we can either:
|
||
|
|
- warn, and return simply the KEY as text
|
||
|
|
- warn, and return "??" as text (similar to bibtex's behaviour)
|
||
|
|
- raise an error
|
||
|
|
--]]
|
||
|
|
function replaceNonExistingAcronym(acr_key)
|
||
|
|
-- TODO: adding the source line to warnings would be useful.
|
||
|
|
-- But maybe not doable in Pandoc?
|
||
|
|
if Options["non_existing"] == "key" then
|
||
|
|
warn("Acronym key ", acr_key, " not recognized")
|
||
|
|
return pandoc.Str(acr_key)
|
||
|
|
elseif Options["non_existing"] == "??" then
|
||
|
|
warn("Acronym key ", acr_key, " not recognized")
|
||
|
|
return pandoc.Str("??")
|
||
|
|
elseif Options["non_existing"] == "error" then
|
||
|
|
error("Acronym key " .. tostring(acr_key)
|
||
|
|
.. " not recognized, stopping!")
|
||
|
|
else
|
||
|
|
error("Unrecognized option non_existing="
|
||
|
|
.. tostring(Options["non_existing"]))
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Replace an acronym `\acr{KEY}`, where KEY is recognized in the `acronyms` table.
|
||
|
|
--]]
|
||
|
|
function replaceExistingAcronym(acr_key)
|
||
|
|
local acronym = Acronyms:get(acr_key)
|
||
|
|
acronym:incrementOccurrences()
|
||
|
|
if acronym:isFirstUse() then
|
||
|
|
-- This acronym never appeared! We first set its usage order.
|
||
|
|
current_order = current_order + 1
|
||
|
|
acronym.usage_order = current_order
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Replace the acronym with the desired style
|
||
|
|
return replaceExistingAcronymWithStyle(
|
||
|
|
acronym,
|
||
|
|
Options["style"],
|
||
|
|
Options["insert_links"]
|
||
|
|
)
|
||
|
|
end
|
||
|
|
|
||
|
|
--[[
|
||
|
|
Replace each `\acr{KEY}` with the correct text and link to the list of acronyms.
|
||
|
|
--]]
|
||
|
|
function replaceAcronym(el)
|
||
|
|
local acr_key = string.match(el.text, "\\acr{(.+)}")
|
||
|
|
if acr_key then
|
||
|
|
-- This is an acronym, we need to parse it.
|
||
|
|
if Acronyms:contains(acr_key) then
|
||
|
|
-- The acronym exists (and is recognized)
|
||
|
|
return replaceExistingAcronym(acr_key)
|
||
|
|
else
|
||
|
|
-- The acronym does not exists
|
||
|
|
return replaceNonExistingAcronym(acr_key)
|
||
|
|
end
|
||
|
|
else
|
||
|
|
-- This is not an acronym, return nil to leave it unchanged.
|
||
|
|
return nil
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
-- Force the execution of the Meta filter before the RawInline
|
||
|
|
-- (we need to load the acronyms first!)
|
||
|
|
-- RawBlock and Doc happen after RawInline so that the actual usage order
|
||
|
|
-- of acronyms is known (and we can sort the List of Acronyms accordingly)
|
||
|
|
return {
|
||
|
|
{ Meta = Meta },
|
||
|
|
{ RawInline = replaceAcronym },
|
||
|
|
{ RawBlock = RawBlock },
|
||
|
|
{ Pandoc = appendLoA },
|
||
|
|
}
|