Skip to content

Instantly share code, notes, and snippets.

@crvdgc
Created December 7, 2024 18:00
Show Gist options
  • Save crvdgc/efac01603a035b2765974d119dede467 to your computer and use it in GitHub Desktop.
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).
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