I’ve been working on a report written completely in Obsidian. I use Pandoc to export my report to a PDF using Typst as the pdf engine, and a template.
I wanted to be able to export figures. One option is to use raw attributes, such as is done here. However, this doesn’t really allow me to take advantage of any of Obsidian’s features - I’m just writing Typst code in markdown at that point. There are some libraries such as pandoc-crossref designed for doing this for latex, but they, again, aren’t Obsidian-native syntax.
So the goal is: Use native Obsidian syntax. And have my output on Pandoc conversion (to Typst, but ideally to other formats, too) come out something like this:
See @figureLabel below.
#figure([A figure of some kind, maybe a code block, or a table, or an image.
],
caption: [
A caption for the figure.
]
)
<figureLabel>
Thankfully we have Lua filters :)! So I made one.
So, here is what the Obsidian syntax looks like for the above, which is transformed by a Lua filter into the above example.
See [[#^figureLabel]] below.
> [!figure] A caption for the figure. ^figureLabel
> A figure of some kind, maybe a code block, or a table, or an image.
Because we use a block link, in Obsidian we can click on [[#^figureLabel]] and get sent to the figure callout. This also gives us some syntax to look for in the filter.
The filter:
obsidian.lua
-- Made by https://forum.obsidian.md/u/kacii
local stringify = (require("pandoc.utils")).stringify
-- requires https://github.com/pandoc-ext/logging
-- handy for debugging
-- local logging = require("logging")
local metadata = {}
function Meta(meta)
for key, value in pairs(meta) do
metadata[key] = value
end
end
-- We want to check if the block contains just one element
-- and it is an element of the type.
---@param content Blocks[]
---@param type string
---@return boolean
local function only_has_type(content, type)
if #content ~= 1 then
return false
end
if content[1] and content[1]["tag"] == type then
return true
end
return false
end
-- modified from https://forum.obsidian.md/t/rendering-callouts-similarly-in-pandoc/40020/6
--
-- we are looking for a callout that looks like this:
-- > [!figure] the caption for your figure ^figureLabel
-- > insert whatever for the figure here
--
-- the figure label derives from: the block ID (i.e. ^figureLabel)
-- if the block ID is not present, it will also look for a metadata
-- value, so for the following:
--
-- > [!figure|figureLabel] caption for figure
-- > insert whatever for the figure here
--
-- its label/ID will become figureLabel.
-- If both are present, then the block ID takes priority.
--
-- p.s. untested on what happens with multiple metadata values such as [!figure|a b c] so no promise on what happens then. to do later maybe.
function BlockQuote(el)
local start = el.content[1]
if start.t == "Para" and start.content[1].t == "Str" and start.content[1].text:match("^%[![%w%|]+%][-+]?$") then
local _, _, ctype = start.content[1].text:find("%[!([%w%|]+)%]")
local s, _, figname = ctype:find("|([%w]+)")
if figname then
ctype = ctype:sub(0, s - 1)
end
local title = start.content
local c = 0
local found = -1
local lastloc = nil
for k, i in pairs(title) do
if stringify(i):match("%^%w*") then
lastloc = i
found = k
end
c = k
end
if lastloc and found > -1 and found == c then
figname = stringify(title:remove()):sub(2)
end
el.content:remove(1)
start.content:remove(1)
local blocktitle = pandoc.Blocks({ title })
--- later: maybe customize what callout types we should transform.
local div = el
if ctype:lower() == "figure" then
div = pandoc.Figure(el.content, blocktitle, pandoc.Attr(figname or ctype))
-- while Typst will accept the above as a
-- figure containing a table with a caption,
-- latex is much fussier.
if only_has_type(el.content, "Table") then
---@type Table
div = el.content[1]
div["caption"] = blocktitle
div["attr"].identifier = figname or ctype
end
else
-- fallback.
div = pandoc.Div(el.content, { classes = { "callout" } })
div.attributes["data-callout"] = ctype:lower()
div.attributes["title"] = stringify(start.content):gsub("^ ", "")
div.attributes["metadata"] = figname:lower()
end
return div
else
return el
end
end
-- Make sure you are using the wikilink markdown extension.
-- So you need to run something like `pandoc -f markdown+wikilinks_title_after_pipe
-- We will transform something like "[[#^blockID]]
-- into something that typst or latex will read as a reference/link
-- to the figure we have created above.
function Link(el)
-- during latex output format, let's check for
-- a meta-variable, `cref`, which pandoc-crossref
-- uses to say to use cleveref format (\cref{}).
-- Unlike pandoc-crossref we will not automatically
-- add the usepackage for this...
--
-- let's also check a `pandoc_custom-ref` variable
-- in case you have something else you want to use.
local refstring = "\\ref{"
if metadata["cref"] or metadata["pandoc_cref"] then
refstring = "\\cref{"
elseif metadata["pandoc_custom-ref"] then
refstring = metadata["pandoc_custom-ref"]
end
local _, _, tx = stringify(el.content):find(".*%#%^(%w+)")
if tx then
if FORMAT == "typst" then
-- typst seems to just treat references to a label
-- the same as bibliographic reference cites.
local c = pandoc.Cite("@" .. tx, { pandoc.Citation(tx, "NormalCitation") })
return c
elseif FORMAT == "latex" then
return pandoc.RawInline("latex", refstring .. tx .. "}")
else
-- haven't tested other formats thoroughly. I think this is what the pandoc native referencing does.
el.target = "#" .. tx
end
end
return el
end
-- if we want to access metadata variables from the other
-- filters, we need to do it like this so Meta runs first.
return {
{ Meta = Meta },
{ Link = Link },
{ BlockQuote = BlockQuote },
}
You can put this file in the filters folder of your data-dir. For example, on Linux and Mac, you can save the file as ~/.local/share/pandoc/filters/obsidian.lua.
As noted in the code, we should run pandoc with the “wikilinks_title_after_pipe” extension, for example, by doing pandoc -f markdown+wikilinks_title_after_pipe. See manual.
I have tested this with Typst and LaTeX outputs (as you can see in the code there are some adjustments made for latex output to ensure they come out correctly), and pdf-engine=typst. Feel free to adjust it yourself however. I may update this post over time too if I change the filter. This is working quite nicely for me so far.
So, as a working example, I have files in Obsidian which might look like this.
This is a big long paragraph blah blah. See [@citationHere]. See [[#^figureLabel]].
> [!figure] A caption for the figure. ^figureLabel
>
> A figure of some kind, maybe a code block, or a table, or an image.
Then I might have a bibliography file such as:
@online{citationHere,
abstract = {Abstract for the webpage.},
author = {Lastname, Firstname},
title = {A webpage article title},
urldate = {2025-12-02},
}
Now I can run my output command:
pandoc --lua-filter pagebreak.lua \
--lua-filter obsidian.lua \
--toc \
--number-sections \
--template default \
--bibliography bibliography.bib \
-f markdown+wikilinks_title_after_pipe \
--standalone \
-o ~/Desktop/mytypstfile.typ \
-t typst \
~/Documents/obsidian/my_markdown_file.md
(Bonus: pagebreak filter from here. Handy for easy pagebreaking.)
I don’t use --citeproc, because I’m going to rely on Typst to do the bibliography compilation for me. (As such, if you are compiling to Typst instead of directly to PDF, make sure that wherever you output your .typ file, it can see your bibliography file.)
In my Typst file, I get an output from the above that looks like this.
This is a big long paragraph blah blah. See @citationHere. See
@figureLabel.
#figure([A figure of some kind, maybe a code block, or a table, or an
image.
],
caption: [
A caption for the figure.
]
)
<figureLabel>
#bibliography("bibliography.bib")
Great!
If I want to compile directly to PDF, my command will instead look like:
pandoc --lua-filter pagebreak.lua \
--lua-filter obsidian.lua \
--toc \
--number-sections \
--template default \
--bibliography bibliography.bib \
-f markdown+wikilinks_title_after_pipe \
--standalone \
-t pdf \
--pdf-engine typst \
-o ~/Desktop/mytypstfile.pdf \
~/Documents/obsidian/my_markdown_file.md
Here is what my PDF looks like!
This should generally work on a LaTeX file too, but this is not my primary usecase at the moment. I tried to make sure I could do that if I needed too, though.
So, there you go. I will attach the lua file below in a .zip. Hope this can be useful to some!
obsidian.lua.zip (2.1 KB)
