Created
December 7, 2024 18:00
-
-
Save crvdgc/efac01603a035b2765974d119dede467 to your computer and use it in GitHub Desktop.
Lua functions to find and replace macros in LaTeX strings. Good for [`pandoc` Lua filters](https://pandoc.org/lua-filters.html). For a complete example, see [this lua filter](https://github.com/yuxiliu-arm/herdtools7/blob/0f40e05ff1afe08b1aea8c9be951ef5a8b9b3a38/asllib/doc/filters/aslref.lua).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local M = {} | |
--- Find pattern in `s` and capture arguments into an array. | |
--- @param s string | |
--- @param pattern string | |
--- @param arity integer | |
--- @return integer|nil, integer|nil, string[] | |
local function matchMacroHelper(s, pattern, arity) | |
local i, j = s:find(pattern) | |
local args = {} | |
local captured = 0 | |
if i ~= nil then | |
-- j is '{' after "\name" | |
local l = j + 1 | |
-- level of {, we assume pattern already has one | |
local stack = 1 | |
for r = l, s:len() do | |
if captured >= arity or l >= #s then | |
break | |
end | |
local c = s:sub(r, r) | |
if c == "{" then | |
if stack == 0 then | |
l = r + 1 | |
end | |
stack = stack + 1 | |
elseif c == "}" then | |
stack = stack - 1 | |
if stack == 0 then | |
table.insert(args, s:sub(l, r - 1)) | |
captured = captured + 1 | |
j = r | |
end | |
end | |
end | |
end | |
return i, j, args | |
end | |
--- Find the first macro match with `name` and `arity` and capture arguments | |
--- into an array. | |
--- | |
--- For example, to match `\hyperlink{pos}{text}`, call `matchMacro(s, | |
--- "hyperlink", 2)`, which returns the start and end position of the first | |
--- match if there is any and its arguments `{ "pos", "text" }`. | |
--- @param s string | |
--- @param name string | |
--- @param arity integer | |
--- @return integer|nil, integer|nil, string[] | |
M.matchMacro = function(s, name, arity) | |
local pattern = "\\" .. name .. "{" | |
return matchMacroHelper(s, pattern, arity) | |
end | |
--- Find all macro matches with `name` and `arity`, return an array of start, | |
--- end positions, and arguments. | |
--- | |
--- @param s string | |
--- @param name string | |
--- @param arity integer | |
--- @return { [1]:integer|nil, [2]:integer|nil, [3]:string[]}[] | |
M.matchMacros = function(s, name, arity) | |
local pattern = "\\" .. name .. "{" | |
local res = {} | |
local rest = s | |
local cur = 0 | |
while true do | |
if rest == "" then | |
break | |
end | |
local i, j, args = matchMacroHelper(rest, pattern, arity) | |
if not i or not j then | |
break | |
end | |
-- `i` and `j` are positions relative to `rest` | |
-- realign with `s` by adding the offset `cur` | |
table.insert(res, { i + cur, j + cur, args }) | |
cur = cur + j | |
rest = rest:sub(j + 1) | |
end | |
return res | |
end | |
--- Substitute a macro with `name` and `arity` by applying `f` to matches macro | |
--- arguments | |
--- | |
--- For example, to change `\hyperlink{pos}{text}` to `\href{#pos}{text}`, call | |
--- | |
--- ```lua | |
--- subMacros(s, "hyperlink", 2, function(args) | |
--- return "\\href{#" .. args[1] .. "}{" .. args[2] .. "}" | |
--- end) | |
--- ``` | |
--- | |
--- @param s string | |
--- @param name string | |
--- @param arity integer | |
--- @param f fun(args: string[]): string | |
--- @return string | |
local function subMacros(s, name, arity, f) | |
local res = "" | |
local cur = 0 | |
for _, t in pairs(M.matchMacros(s, name, arity)) do | |
local i, j, args = t[1], t[2], t[3] | |
res = res .. s:sub(cur, i - 1) .. f(args) | |
cur = j + 1 | |
end | |
res = res .. s:sub(cur) | |
return res | |
end | |
--- Examples | |
--- rewrite `\ref{A}` in math to `\textrm{\href{#A}{A}}` | |
local function subRef(s) | |
return subMacros(s, "ref", 1, function(args) | |
return "\\textrm{\\href{#" .. args[1] .. "}{" .. args[1] .. "}}" | |
end) | |
end | |
--- rewrite `\hyperlink{A}{B}` to `\href{#A}{B}` | |
local function subHyperlink(s) | |
return subMacros(s, "hyperlink", 2, function(args) | |
local content = subHyperlink(args[2]) | |
return "\\href{#" .. args[1] .. "}{" .. content .. "}" | |
end) | |
end | |
local function subHypertarget(s) | |
return subMacros(s, "hypertarget", 2, function(args) | |
return "\\cssId{" .. args[1] .. "}{" .. args[2] .. "}" | |
end) | |
end | |
--- Tests | |
print(subRef("Lorem ipsum dolor sit amet, qui \\ref{sec:adipisicing}")) | |
-- Lorem ipsum dolor sit amet, qui \textrm{\href{#sec:adipisicing}{sec:adipisicing}} | |
print(subHyperlink("lorem \\hyperlink{subsec:cupidatat}{\\emph{Lorem ipsum dolor sit amet, qui minim}}!")) | |
-- lorem \href{#subsec:cupidatat}{\emph{Lorem ipsum dolor sit amet, qui minim}}! | |
print(subHyperlink("\\section{Lorem} \\hypertarget{chap:lorem}{}")) | |
-- \section{Lorem} \hypertarget{chap:lorem}{} | |
return M |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment