200 lines
5.7 KiB
Lua
200 lines
5.7 KiB
Lua
-- Replace Obsidian embed blocks like ![[macros.tex]] with the content of the
|
|
-- corresponding LaTeX macro file stored in *.tex.md.
|
|
|
|
local macro_cache = {}
|
|
-- Keep a small explicit map for quick lookups if desired, but resolution
|
|
-- will try relative locations from the current document by default.
|
|
local macro_files = {
|
|
-- leave empty or add overrides if needed
|
|
}
|
|
|
|
-- Mapping from macro name to the include path to inject in Quarto shortcode
|
|
local macro_include = {
|
|
["macros.tex"] = "../../macros.tex.md",
|
|
["local_macros.tex"] = "local_macros.tex.md",
|
|
}
|
|
|
|
local function read_file(path)
|
|
local handle = io.open(path, "r")
|
|
if not handle then
|
|
return nil
|
|
end
|
|
|
|
local content = handle:read("*a")
|
|
handle:close()
|
|
return content
|
|
end
|
|
|
|
local function strip_markdown_suffix(target)
|
|
return target:gsub("%.md$", "")
|
|
end
|
|
|
|
local function find_macro_file(target)
|
|
local key = strip_markdown_suffix(target):match("([^/\\]+)$")
|
|
if not key then
|
|
return nil
|
|
end
|
|
|
|
if macro_cache[key] then
|
|
return macro_cache[key]
|
|
end
|
|
|
|
-- Helper utilities
|
|
local function file_exists(path)
|
|
local f = io.open(path, "r")
|
|
if f then
|
|
f:close(); return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function dirname(path)
|
|
if not path then return nil end
|
|
local dir = path:match("(.*/)")
|
|
if dir then
|
|
-- strip trailing '/'
|
|
return dir:gsub("/$", "")
|
|
end
|
|
return '.'
|
|
end
|
|
|
|
local function join(a, b)
|
|
if not a or a == '' or a == '.' then return b end
|
|
if a:sub(-1) == '/' then return a .. b end
|
|
return a .. '/' .. b
|
|
end
|
|
|
|
-- Try quick explicit mapping first
|
|
if macro_files[key] and file_exists(macro_files[key]) then
|
|
macro_cache[key] = macro_files[key]
|
|
return macro_files[key]
|
|
end
|
|
|
|
-- Determine current input file directory from Pandoc state
|
|
local input_file = nil
|
|
if PANDOC_STATE and PANDOC_STATE.input_files and #PANDOC_STATE.input_files > 0 then
|
|
input_file = PANDOC_STATE.input_files[1]
|
|
end
|
|
local cur_dir = dirname(input_file) or '.'
|
|
|
|
-- Search candidates in order of preference
|
|
local candidates = {
|
|
join(cur_dir, key .. ".md"),
|
|
join(cur_dir, key),
|
|
key .. ".md",
|
|
key,
|
|
}
|
|
|
|
-- Walk up directories from cur_dir to try to find a project root (look for _quarto.yml)
|
|
local function find_project_root(start)
|
|
local dir = start or '.'
|
|
for i = 1, 16 do
|
|
local qcfg = join(dir, '_quarto.yml')
|
|
if file_exists(qcfg) then return dir end
|
|
-- go up one
|
|
local parent = dir:match("(.*/).-$")
|
|
if not parent then break end
|
|
dir = parent:gsub("/$", "")
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local project_root = find_project_root(cur_dir)
|
|
if project_root then
|
|
table.insert(candidates, join(project_root, key .. ".md"))
|
|
table.insert(candidates, join(project_root, key))
|
|
end
|
|
|
|
-- Finally check workspace root (pwd) as a fallback
|
|
-- Use os.getenv("PWD") which Quarto/Pandoc normally runs with
|
|
local pwd = os.getenv('PWD')
|
|
if pwd then
|
|
table.insert(candidates, join(pwd, key .. ".md"))
|
|
table.insert(candidates, join(pwd, key))
|
|
end
|
|
|
|
for _, cand in ipairs(candidates) do
|
|
if cand and file_exists(cand) then
|
|
macro_cache[key] = cand
|
|
return cand
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
local function expand_macro_embed(el)
|
|
local text = pandoc.utils.stringify(el)
|
|
local trimmed = text:gsub("^%s+", ""):gsub("%s+$", "")
|
|
local target = trimmed:match("^!%[%[(.-)%]%]$") or trimmed:match("^%[%[(.-)%]%]$")
|
|
if not target then
|
|
return nil
|
|
end
|
|
|
|
local path = find_macro_file(target)
|
|
if not path then
|
|
return nil
|
|
end
|
|
|
|
-- Compute include path relative to current input file directory
|
|
local function dirname(path)
|
|
if not path then return nil end
|
|
local dir = path:match("(.*/)")
|
|
if dir then
|
|
return dir:gsub("/$", "")
|
|
end
|
|
return '.'
|
|
end
|
|
|
|
local input_file = nil
|
|
if PANDOC_STATE and PANDOC_STATE.input_files and #PANDOC_STATE.input_files > 0 then
|
|
input_file = PANDOC_STATE.input_files[1]
|
|
end
|
|
local cur_dir = dirname(input_file) or '.'
|
|
|
|
local function split(path)
|
|
local t = {}
|
|
for part in path:gmatch("[^/]+") do table.insert(t, part) end
|
|
return t
|
|
end
|
|
|
|
local function relative_path(from, to)
|
|
if not from or not to then return to end
|
|
-- handle same path
|
|
if from == to then return "." end
|
|
local from_abs = (from:sub(1, 1) == '/')
|
|
local to_abs = (to:sub(1, 1) == '/')
|
|
-- if one is absolute and other not, fall back to `to`
|
|
if from_abs ~= to_abs then return to end
|
|
local from_parts = split(from)
|
|
local to_parts = split(to)
|
|
-- find common prefix
|
|
local i = 1
|
|
while i <= #from_parts and i <= #to_parts and from_parts[i] == to_parts[i] do
|
|
i = i + 1
|
|
end
|
|
local up = #from_parts - i + 1
|
|
local rel_parts = {}
|
|
for j = 1, up do table.insert(rel_parts, "..") end
|
|
for j = i, #to_parts do table.insert(rel_parts, to_parts[j]) end
|
|
if #rel_parts == 0 then return "." end
|
|
return table.concat(rel_parts, "/")
|
|
end
|
|
|
|
local include_path = relative_path(cur_dir, path)
|
|
if not include_path then
|
|
-- fallback to absolute path if relative couldn't be computed
|
|
include_path = path
|
|
end
|
|
|
|
local shortcode = string.format("{{< include %s >}}", include_path)
|
|
return pandoc.RawBlock("markdown", shortcode)
|
|
end
|
|
|
|
function Para(el)
|
|
return expand_macro_embed(el)
|
|
end
|
|
|
|
function Plain(el)
|
|
return expand_macro_embed(el)
|
|
end
|