RWEP/_extensions/d2/d2.lua

272 lines
8.2 KiB
Lua

-- Enum for D2Theme
local D2Theme = {
NeutralDefault = 0,
NeutralGrey = 1,
FlagshipTerrastruct = 3,
CoolClassics = 4,
MixedBerryBlue = 5,
GrapeSoda = 6,
Aubergine = 7,
ColorblindClear = 8,
VanillaNitroCola = 100,
OrangeCreamsicle = 101,
ShirelyTemple = 102,
EarthTones = 103,
EvergladeGreen = 104,
ButteredToast = 105,
DarkMauve = 200,
Terminal = 300,
TerminalGrayscale = 301,
Origami = 302
}
-- Enum for D2Layout
local D2Layout = {
dagre = 'dagre',
elk = 'elk'
}
-- Enum for D2Format
local D2Format = {
svg = 'svg',
png = 'png',
pdf = 'pdf'
}
-- Enum for Embed mode
local EmbedMode = {
inline = "inline",
link = "link",
raw = "raw"
}
-- Helper function to copy a table
function copyTable(obj, seen)
-- Handle non-tables and previously-seen tables.
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
-- New table; mark it as seen and copy recursively.
local s = seen or {}
local res = {}
s[obj] = res
for k, v in pairs(obj) do res[copyTable(k, s)] = copyTable(v, s) end
return setmetatable(res, getmetatable(obj))
end
-- Helper function for debugging
function dump(o)
if type(o) == 'table' then
local s = '{ '
for k,v in pairs(o) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. dump(v) .. ','
end
return s .. '} '
else
return tostring(o)
end
end
-- Counter for the diagram files
local counter = 0
local function render_graph(globalOptions)
local filter = {
CodeBlock = function(cb)
-- Check if the CodeBlock has the 'd2' class
if not cb.classes:includes('d2') or cb.text == nil then
return nil
end
counter = counter + 1
-- Initialise options table
local options = copyTable(globalOptions)
-- Process codeblock attributes
for k, v in pairs(cb.attributes) do
options[k] = v
end
-- Transform options
if options.theme ~= nil and type(options.theme) == "string" then
assert(D2Theme[options.theme] ~= nil, "Invalid theme: " .. options.theme .. ". Options are: " .. dump(D2Theme))
options.theme = D2Theme[options.theme]
end
if options.layout ~= nil and type(options.layout) == "string" then
assert(D2Layout[options.layout] ~= nil, "Invalid layout: " .. options.layout .. ". Options are: " .. dump(D2Layout))
options.layout = D2Layout[options.layout]
end
if options.format ~= nil and type(options.format) == "string" then
assert(D2Format[options.format] ~= nil, "Invalid format: " .. options.format .. ". Options are: " .. dump(D2Format))
options.format = D2Format[options.format]
end
if options.embed_mode ~= nil and type(options.embed_mode) == "string" then
assert(EmbedMode[options.embed_mode] ~= nil, "Invalid embed_mode: " .. options.embed_mode .. ". Options are: " .. dump(EmbedMode))
options.embed_mode = EmbedMode[options.embed_mode]
end
if options.sketch ~= nil and type(options.sketch) == "string" then
assert(options.sketch == "true" or options.sketch == "false", "Invalid sketch: " .. options.sketch .. ". Options are: true, false")
options.sketch = options.sketch == "true"
end
if options.pad ~= nil and type(options.pad) == "string" then
assert(tonumber(options.pad) ~= nil, "Invalid pad: " .. options.pad .. ". Must be a number")
options.pad = tonumber(options.pad)
end
if options.echo ~= nil and type(options.echo) == "string" then
assert(options.echo == "true" or options.echo == "false", "Invalid echo: " .. options.echo .. ". Options are: true, false")
options.echo = options.echo == "true"
end
-- Set default filename
if options.filename == nil then
options.filename = "diagram-" .. counter
end
-- Set the default format to pdf since svg is not supported in PDF output
if options.format == D2Format.svg and quarto.doc.is_format("latex") then
options.format = D2Format.pdf
end
-- Set the default embed_mode to link if the quarto format is not html or the figure format is pdf
if not quarto.doc.is_format("html") or options.format == D2Format.pdf then
options.embed_mode = EmbedMode.link
end
-- Set the default folder to ./images when embed_mode is link
if options.folder == nil and options.embed_mode == EmbedMode.link then
options.folder = "./images"
end
-- Generate diagram using `d2` CLI utility
local result = pandoc.system.with_temporary_directory('svg-convert', function (tmpdir)
-- determine path name of input file
local inputPath = pandoc.path.join({tmpdir, "temp_" .. counter .. ".txt"})
-- determine path name of output file
local outputPath
if options.folder ~= nil then
os.execute("mkdir -p " .. options.folder)
outputPath = options.folder .. "/" .. options.filename .. "." .. options.format
else
outputPath = pandoc.path.join({tmpdir, options.filename .. "." .. options.format})
end
-- write graph text to file
local tmpFile = io.open(inputPath, "w")
if tmpFile == nil then
print("Error: Could not open file for writing")
return nil
end
tmpFile:write(cb.text)
tmpFile:close()
-- run d2
os.execute(
"d2" ..
" --theme=" .. options.theme ..
" --layout=" .. options.layout ..
" --sketch=" .. tostring(options.sketch) ..
" --pad=" .. options.pad ..
" " .. inputPath ..
" " .. outputPath
)
if options.embed_mode == EmbedMode.link then
return outputPath
else
local file = io.open(outputPath, "rb")
local data
if file then
data = file:read('*all')
file:close()
end
os.remove(outputPath)
if options.embed_mode == EmbedMode.raw then
return data
elseif options.embed_mode == EmbedMode.inline then
dump(options)
if options.format == "svg" then
return "data:image/svg+xml;base64," .. quarto.base64.encode(data)
elseif options.format == "png" then
return "data:image/png;base64," .. quarto.base64.encode(data)
else
print("Error: Unsupported format")
return nil
end
end
end
end)
-- Read the generated output into a Pandoc Image element
local output
if options.embed_mode == EmbedMode.raw then
output = pandoc.Div({pandoc.RawInline("html", result)})
if options.width ~= nil then
output.attributes.style = "width: " .. options.width .. ";"
end
if options.height ~= nil then
output.attributes.style = output.attributes.style .. "height: " .. options.height .. ";"
end
else
local image = pandoc.Image({}, result)
-- Set the width and height attributes, if they exist
if options.width ~= nil then
image.attributes.width = options.width
end
if options.height ~= nil then
image.attributes.height = options.height
end
if options.caption ~= '' then
image.caption = pandoc.Str(options.caption)
end
output = pandoc.Para({image})
end
-- Wrap the Image element in a Para element and return it
if options.echo then
local codeBlock = pandoc.CodeBlock(cb.text, cb.attr)
output = pandoc.Div({codeBlock, output})
end
return output
end
}
return filter
end
function Pandoc(doc)
local options = {
theme = D2Theme.NeutralDefault,
layout = D2Layout.dagre,
format = D2Format.svg,
sketch = false,
pad = 100,
folder = nil,
filename = nil,
caption = '',
width = nil,
height = nil,
echo = false,
embed_mode = "inline"
}
-- Process global attributes
local globalOptions = doc.meta["d2"]
if type(globalOptions) == "table" then
for k, v in pairs(globalOptions) do
options[k] = pandoc.utils.stringify(v)
end
end
return doc:walk(render_graph(options))
end