transfer from old repo

This commit is contained in:
Andreas Gammelgaard Damsbo 2026-08-19 09:27:27 +02:00
commit 277e2b8cf3
No known key found for this signature in database
1111 changed files with 83736 additions and 0 deletions

View file

@ -0,0 +1,7 @@
title: acronyms
author: rchaput
version: 1.0.0
quarto-required: ">=1.2.0"
contributes:
filters:
- parse-acronyms.lua

View file

@ -0,0 +1,186 @@
--[[
This file defines an Acronym and the Acronyms table.
--]]
local Helpers = require("acronyms_helpers")
-- Define an Acronym with some default values
Acronym = {
-- The acronym's key, or label. Used to identify it. Must be unique.
key = nil,
-- The acronym's short form (i.e., the acronym itself).
shortname = nil,
-- The acronym's definition, or description.
longname = nil,
-- The number of times this acronym was used.
occurrences = 0,
-- The order in which acronyms are defined. 1=first, 2=second, etc.
definition_order = nil,
-- The order in which acronyms appear in the document. 1=first, nil=never.
usage_order = nil,
}
-- Create a new Acronym
function Acronym:new(object)
setmetatable(object, self)
self.__index = self
-- Check that important attributes are non-nil
assert(object.shortname ~= nil,
"An Acronym shortname should not be nil!")
assert(object.longname ~= nil,
"An Acronym longname should not be nil!")
-- If the key is not set, we want to use the shortname instead.
-- (Most of the time, the key is the shortname in lower case anyway...)
object.key = object.key or object.shortname
return object
end
-- Debug (helper) function
function Acronym.__tostring(acronym)
local str = "Acronym{"
str = str .. "key=" .. acronym.key .. ";"
str = str .. "short=" .. acronym.shortname .. ";"
str = str .. "long=" .. acronym.longname .. ";"
str = str .. "occurrences=" .. acronym.occurrences .. ";"
str = str .. "definition_order=" .. tostring(acronym.definition_order) .. ";"
str = str .. "usage_order=" .. tostring(acronym.usage_order)
str = str .. "}"
return str
end
-- Increment the count of occurrences
function Acronym:incrementOccurrences()
self.occurrences = self.occurrences + 1
end
-- Is this the acronym's first occurrence?
function Acronym:isFirstUse()
return self.occurrences <= 1
end
-- The Acronyms database.
Acronyms = {
-- The table that contains all acronyms, indexed by their key.
acronyms = {},
-- The current "definition_order" value.
-- Each time a new acronym is defined, we increment this value to keep
-- count of the order in which acronyms are defined.
current_definition_order = 0,
-- Access to the `Acronym` class, if necessary.
Acronym = Acronym,
}
-- Get the Acronym with the given key, or nil if not found.
function Acronyms:get(key)
return self.acronyms[key]
end
-- Does the table contains the given key?
function Acronyms:contains(key)
return self:get(key) ~= nil
end
-- Add a new acronym to the table. Also handles duplicates.
function Acronyms:add(acronym, on_duplicate)
assert(acronym.key ~= nil,
"The acronym key should not be nil!")
assert(on_duplicate ~= nil,
"on_duplicate should not be nil!")
-- Handling duplicate keys
if self:contains(acronym.key) then
if on_duplicate == "replace" then
-- Do nothing, let us replace the previous acronym.
elseif on_duplicate == "keep" then
-- Do nothing, but do not replace: we return here.
return
elseif on_duplicate == "warn" then
-- Warn, and do not replace.
warn("Duplicate key: ", acronym.key)
return
elseif on_duplicate == "error" then
-- Stop execution.
error("Duplicate key: " .. acronym.key)
else
error("Unrecognized option on_duplicate = " .. on_duplicate)
end
end
self.current_definition_order = self.current_definition_order + 1
acronym.definition_order = self.current_definition_order
self.acronyms[acronym.key] = acronym
end
-- Populate the Acronyms database from a YAML metadata
function Acronyms:parseFromMetadata(metadata, on_duplicate)
-- We expect the acronyms to be in the `metadata.acronyms.keys` field.
if not (metadata and metadata.acronyms and metadata.acronyms.keys) then
return
end
-- This field should be a Pandoc "MetaList" (so we can iter over it).
if not Helpers.isMetaList(metadata.acronyms.keys) then
error("The acronyms.keys should be a list!")
end
-- Iterate over the defined acronyms. We use `ipairs` since we want to
-- keep their original order (useful for the `definition_order`!).
for _, v in ipairs(metadata.acronyms.keys) do
-- Remember that each of these values can be nil!
-- By using `and`, we make sure that `stringify` is applied on non-nil.
local key = v.key and pandoc.utils.stringify(v.key)
local shortname = v.shortname and pandoc.utils.stringify(v.shortname)
local longname = v.longname and pandoc.utils.stringify(v.longname)
local acronym = Acronym:new{
key = key,
shortname = shortname,
longname = longname,
}
Acronyms:add(acronym, on_duplicate)
end
end
-- Populate the Acronyms database from a YAML file
-- Inspired from https://github.com/dsanson/pandoc-abbreviations.lua/
function Acronyms:parseFromYamlFile(filepath, on_duplicate)
assert(filepath ~= nil,
"filepath must not be nil!")
-- First, read the file's content.
local file = io.open(filepath, "r")
if file == nil then
warn("File ", filepath, " could not be read! (does not exist?)")
return
end
local content = file:read("*a")
file:close()
-- Secondly, use Pandoc's read ability to parse the content.
-- Pandoc does not know how to read YAML, so we'll trick it by
-- asking to parse Markdown instead (since the Markdown's metadata
-- is YAML anyway).
local metadata = pandoc.read(content, "markdown").meta
-- Finally, read the metadata as usual.
self:parseFromMetadata(metadata, on_duplicate)
end
return Acronyms

View file

@ -0,0 +1,65 @@
--[[
This file defines a few helper functions, in particular with respect to pandoc.
--]]
local Options = require("acronyms_options")
local Helpers = {}
-- Helper function to determine pandoc's version.
-- `version` must be a table of numbers, e.g., `{2, 17, 0, 1}`
function Helpers.isAtLeastVersion(version)
-- `PANDOC_VERSION` exists since 2.1, but we never know...
if PANDOC_VERSION == nil then
return false
end
-- Loop up over the components
-- e.g., `2.17.0.1` => [0]=2, [1]=17, [2]=0, [3]=1
for k, v in ipairs(version) do
if PANDOC_VERSION[k] == nil or PANDOC_VERSION[k] < version[k] then
-- Examples: 2.17 < 2.17.0.1, or 2.16 < 2.17
return false
elseif PANDOC_VERSION[k] > version[k] then
-- Example: 2.17 > 2.16.2 (we do not need to check the next!)
return true
end
end
-- At this point, all components are equal
return true
end
-- Helper function to determine whether a metadata field is a list.
function Helpers.isMetaList(field)
-- We want to know whether we have multiple values (MetaList).
-- Pandoc 2.17 introduced a compatibility-breaking change for this:
-- the `.tag` is no longer present in >= 2.17 ;
-- the `pandoc.utils.type` function is only available in >= 2.17
if Helpers.isAtLeastVersion({2, 17}) then
-- Use the new `pandoc.utils.type` function
return pandoc.utils.type(field) == "List"
else
-- Use the (old) `.tag` type attribute
return field.t == "MetaList"
end
end
-- Helper function to generate the ID (identifier) from an acronym key.
-- The ID can be used for, e.g., links.
function Helpers.key_to_id(key)
return Options["id_prefix"] .. key
end
-- Similar helper but for the link itself (based on the ID).
function Helpers.key_to_link(key)
return "#" .. Helpers.key_to_id(key)
end
return Helpers

View file

@ -0,0 +1,112 @@
--[[
This file defines the Options table.
--]]
-- The table that holds all options, with their default values.
-- We will also add a few methods to this table, to handle these options
-- (parse them from configuration files, get them, ...).
local Options = {
-- The prefix to prepend to all acronym's ID (to ensure their uniqueness).
-- IDs are especially used to link an acronym to its definition in the List
-- of Acronyms.
id_prefix = "acronyms_",
-- How to sort acronyms in the List of Acronyms.
-- Please refer to the `sort_acronyms.lua` file for allowed values.
sorting = "alphabetical",
-- The title (header) that precedes the List of Acronyms (LoA).
loa_title = pandoc.MetaInlines(pandoc.Str("List Of Acronyms")),
-- Whether to include in the LoA acronyms that have not been used.
include_unused = true,
-- Whether to insert the LoA, and where.
insert_loa = "beginning",
-- How to deal with non-existing acronyms.
non_existing = "key",
-- How to deal with duplicate definitions of acronyms.
on_duplicate = "warn",
-- The style to use when replacing an acronym.
-- Please refer to the `acronyms_styles.lua` for allowed values.
style = "long-short",
-- Whether to insert a link to the acronym's definition in the LoA when
-- replacing (rendering) an acronym.
insert_links = true,
}
--[[
Parse the options from the Metadata (i.e., the YAML fields).
--]]
function Options:parseOptionsFromMetadata(m)
-- The options that we are interested in are all grouped under `acronyms`.
-- If it does not exist, use an empty table.
options = m.acronyms or {}
if options["id_prefix"] ~= nil then
self.id_prefix = pandoc.utils.stringify(options["id_prefix"])
end
if options["sorting"] ~= nil then
self.sorting = pandoc.utils.stringify(options["sorting"])
end
if options["loa_title"] ~= nil then
if pandoc.utils.stringify(options["loa_title"]) == "" then
-- Writing `loa_title: ""` in the YAML returns `{}` (an empty table).
-- `pandoc.utils.stringify({})` returns `""` as well.
-- This value indicates that the user does not want a Header.
self.loa_title = ""
else
-- For any other case, we want to use the exact same value,
-- (not stringified!), i.e., a Pandoc object.
self.loa_title = options["loa_title"]
end
end
if options["include_unused"] ~= nil then
-- This value should be a boolean here, we do not need to stringify it.
self.include_unused = options["include_unused"]
end
if options["insert_loa"] ~= nil then
if options["insert_loa"] == false then
-- Special value: keep it exactly as-is.
self.insert_loa = false
else
-- Default case: it should be "beginning", or "end", we want it
-- as a string.
self.insert_loa = pandoc.utils.stringify(options["insert_loa"])
end
end
if options["non_existing"] ~= nil then
self.non_existing = pandoc.utils.stringify(options["non_existing"])
end
if options["on_duplicate"] ~= nil then
self.on_duplicate = pandoc.utils.stringify(options["on_duplicate"])
end
if options["style"] ~= nil then
self.style = pandoc.utils.stringify(options["style"])
end
if options["insert_links"] ~= nil then
-- This value should be a boolean here, we do not need to stringify it.
self.insert_links = options["insert_links"]
end
end
return Options

View file

@ -0,0 +1,130 @@
--[[
This file defines the "styles" to replace acronyms.
Such styles control how to use the acronym's short name,
long name, whether one should be between parentheses, etc.
Styles are largely inspired from the LaTeX package "glossaries"
(and "glossaries-extra").
A gallery of the their styles can be found at:
https://www.dickimaw-books.com/gallery/index.php?label=sample-abbr-styles
A more complete document (rather long) can be found at:
https://mirrors.chevalier.io/CTAN/macros/latex/contrib/glossaries-extra/samples/sample-abbr-styles.pdf
More specifically, this file defines a table of functions.
Each function takes an acronym, and return one or several Pandoc elements.
These elements will replace the original acronym call in the Markdown
document.
Most styles will depend on whether this is the acronym's first occurrence,
("first use") or not ("next use"), similarly to the LaTeX "glossaries".
For example, a simple (default) style can be to return the acronym's
long name, followed by the short name between parentheses.
When the parser encounters `\acr{RL}`, assuming that `RL` is correctly
defined in the acronyms database, the corresponding function would
return a Pandoc Link, where the text is "Reinforcement Learning (RL)",
and pointing to the definition of "RL" in the List of Acronyms.
Note: the acronym's key MUST exist in the acronyms database.
Functions to replace a non-existing key must be handled elsewhere.
--]]
local Helpers = require("acronyms_helpers")
-- The table containing all styles, indexed by the style's name.
local styles = {}
-- Local helper function to create either a Str or a Link,
-- depending on whether we want to insert links.
local function create_element(content, key, insert_links)
if insert_links then
return pandoc.Link(content, Helpers.key_to_link(key))
else
return pandoc.Str(content)
end
end
-- First use: long name (short name)
-- Next use: short name
styles["long-short"] = function(acronym, insert_links)
local text
if acronym:isFirstUse() then
text = acronym.longname .. " (" .. acronym.shortname .. ")"
else
text = acronym.shortname
end
return create_element(text, acronym.key, insert_links)
end
-- First use: short name (long name)
-- Next use: short name
styles["short-long"] = function(acronym, insert_links)
local text
if acronym:isFirstUse() then
text = acronym.shortname .. " (" .. acronym.longname .. ")"
else
text = acronym.shortname
end
return create_element(text, acronym.key, insert_links)
end
-- First use: long name
-- Next use: long name
styles["long-long"] = function(acronym, insert_links)
local text
text = acronym.longname
return create_element(text, acronym.key, insert_links)
end
-- First use: short name [^1]
-- [^1]: short name: long name
-- Next use: short name
styles["short-footnote"] = function(acronym, insert_links)
if acronym:isFirstUse() then
-- The inline text (before the footnote)
local text = pandoc.Str(acronym.shortname)
-- We create a footnote, which must contain a Block with a Link and
-- the longname (as a simple text).
-- So we create a Pandoc Plain object to hold the link and text.
-- Directly using a list inside the Note seems not to work.
local note = pandoc.Note(
pandoc.Plain({
create_element(acronym.shortname, acronym.key, insert_links),
pandoc.Str(": " .. acronym.longname)
})
)
-- We want to insert both the text and the footnote
return { text, note }
else
-- Simply return the shortname
return create_element(acronym.shortname, acronym.key, insert_links)
end
end
-- The "public" API of this module, the function which is returned by
-- require.
return function(acronym, style_name, insert_links)
-- Check that the requested strategy exists
assert(style_name ~= nil,
"style_name must not be nil!")
assert(styles[style_name] ~= nil,
"Style " .. style_name .. " does not exist!")
-- Check that the acronym exists
assert(acronym ~= nil,
"acronym must not be nil!")
-- Call the style on this acronym
return styles[style_name](acronym, insert_links)
end

View file

@ -0,0 +1,249 @@
--[[
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 },
}

View file

@ -0,0 +1,72 @@
--[[
This file defines the sorting strategies.
A sorting strategy (or comparator) is a function that receives 2
acronyms, and returns a boolean which indicates whether the first
one should be before the second one (according to its criterion).
Sorting strategies are leveraged using `table.sort(table, comp)`
where `comp` is one of these strategies.
--]]
-- The table containing all the sorting strategies.
local sorting_strategies = {}
-- Sort acronyms by their shortname, in alphabetical order.
sorting_strategies["alphabetical"] = function(acronym1, acronym2)
-- TODO: check that this works with UTF-8 characters
return acronym1.shortname < acronym2.shortname
end
-- Sort acronyms by their definition order (first to last).
sorting_strategies["initial"] = function(acronym1, acronym2)
return acronym1.definition_order < acronym2.definition_order
end
-- Sort acronyms by their usage order.
-- Unused acronyms must NOT be included! (Their order is `nil`)
sorting_strategies["usage"] = function(acronym1, acronym2)
return acronym1.usage_order < acronym2.usage_order
end
-- The "public" API, i.e., the function returned by `require`.
function sort_acronyms(acronyms, criterion, include_unused)
assert(acronyms ~= nil,
"The acronyms table must not be nil!")
assert(criterion ~= nil,
"The criterion must be not nil!")
local comparator = sorting_strategies[criterion]
assert(comparator ~= nil,
"Sorting criterion unrecognized: " .. criterion)
-- Special rule: cannot use `usage` criterion if `include_unused` is true.
-- Otherwise, comparison of potentially nil values will crash.
if criterion == "usage" and include_unused then
error("When the 'usage' sorting is used, 'include_unused' must be set to false!")
end
-- The acronyms table is indexed by keys, not by ints. Thus,
-- we cannot use `ipairs` to walk over it in a specific order.
-- To sort the table, we need to create a second table first,
-- indexed by ints (i.e., a sequence).
local sequence_acronyms = {}
for _, acronym in pairs(acronyms) do
if include_unused or acronym.usage_order ~= nil then
table.insert(sequence_acronyms, acronym)
end
end
-- Sort the keys according to the criterion
table.sort(sequence_acronyms, comparator)
return sequence_acronyms
end
return sort_acronyms