PAaSO/Midterm assesment/_extensions/acronyms/parse-acronyms.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 },
}