diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f3cb1..6f8a90e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# quarto-d2 1.1.0 + +## NEW FUNCTIONALITY + +- When the output type is html and the image format is svg, also setting the `embed_type="raw"` will embed the svg directly into the html document (#1). This is useful enabling interactive content such as hover or links to work. + + # quarto-d2 1.0.0 Initial release. Main features: diff --git a/README.md b/README.md index 397f3f6..b0a79e4 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,9 @@ layout of the diagram. `"100px"`, `"50%"`, `"3cm"`. - `echo`: Whether to echo the original diagram code in the output. Default is `false`. +- `embed_mode`: How to embed the diagram in the output. Default is + `"inline"` for HTML output and `"link"` for other output formats. + Options are `"inline"`, `"link"`, `"raw"`. Here’s an example that uses multiple attributes: @@ -172,7 +175,7 @@ x -> y -> z ``` ```` -## Setting Output Folder and File Name +## Setting output folder and file name You can specify a folder where the generated diagram will be saved using the `folder` attribute. The `filename` attribute allows you to set a @@ -192,3 +195,31 @@ x -> y -> z > HTML, the image will be embedded inline in the document. + +## Interactive diagrams + +Interactive diagrams will only work when the Quarto output format is +HTML, the figure format is `"svg"`, and the embed mode is `"raw"`. +Example: + +```` markdown +--- +title: "D2 Example" +format: html +filters: + - d2 +d2: + format: svg + embed_mode: raw +--- + +```{.d2 width="40%"} +x { + link: "https://quarto.org" +} +y { + tooltip: "This is a tooltip" +} +x -> y -> z +``` +```` diff --git a/README.qmd b/README.qmd index 1c90831..7d150c2 100644 --- a/README.qmd +++ b/README.qmd @@ -123,6 +123,7 @@ You can specify additional attributes to control the appearance and layout of th - `width`: Width of the output image. Default is `100%`. Examples are `"100px"`, `"50%"`, `"3cm"`. - `height`: Height of the output image. Default is `auto`. Examples are `"100px"`, `"50%"`, `"3cm"`. - `echo`: Whether to echo the original diagram code in the output. Default is `false`. +- `embed_mode`: How to embed the diagram in the output. Default is `"inline"` for HTML output and `"link"` for other output formats. Options are `"inline"`, `"link"`, `"raw"`. Here's an example that uses multiple attributes: @@ -151,7 +152,7 @@ x -> y -> z ``` ```` -## Setting Output Folder and File Name +## Setting output folder and file name You can specify a folder where the generated diagram will be saved using the `folder` attribute. The `filename` attribute allows you to set a custom name for the output file. @@ -165,3 +166,28 @@ x -> y -> z If the `folder` attribute is not provided and the output format is HTML, the image will be embedded inline in the document. ::: +## Interactive diagrams + +Interactive diagrams will only work when the Quarto output format is HTML, the figure format is `"svg"`, and the embed mode is `"raw"`. Example: + +````markdown +--- +title: "D2 Example" +format: html +filters: + - d2 +d2: + format: svg + embed_mode: raw +--- + +```{.d2 width="40%"} +x { + link: "https://quarto.org" +} +y { + tooltip: "This is a tooltip" +} +x -> y -> z +``` +```` diff --git a/_extensions/d2/d2.lua b/_extensions/d2/d2.lua index 59748d0..3fd8815 100644 --- a/_extensions/d2/d2.lua +++ b/_extensions/d2/d2.lua @@ -1,36 +1,42 @@ -- 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 + 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' + dagre = 'dagre', + elk = 'elk' } -- Enum for D2Format local D2Format = { - svg = 'svg', - png = 'png', - pdf = 'pdf' + svg = 'svg', + png = 'png', + pdf = 'pdf' +} +-- Enum for Embed mode +local EmbedMode = { + inline = "inline", + link = "link", + raw = "raw" } -- Helper function to copy a table @@ -83,22 +89,33 @@ local function render_graph(globalOptions) 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 @@ -107,30 +124,36 @@ local function render_graph(globalOptions) options.filename = "diagram-" .. counter end - -- Set the default folder to ./images since inline images are not supported - if not quarto.doc.is_format("html") then - options.folder = "./images" - end - -- Set the default format to pdf since svg is not supported - if quarto.doc.is_format("latex") then + -- 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 - -- Determine output path - local outputPath - if options.folder ~= nil then - os.execute("mkdir -p " .. options.folder) - outputPath = options.folder .. "/" .. options.filename .. "." .. options.format - else - local prefix = os.tmpname() - outputPath = prefix .. "_" .. counter .. "." .. options.format + -- 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) - local tempPath = pandoc.path.join({tmpdir, "temp_" .. counter .. ".txt"}) - - local tmpFile = io.open(tempPath, "w") + 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 @@ -138,58 +161,77 @@ local function render_graph(globalOptions) tmpFile:write(cb.text) tmpFile:close() + -- run d2 os.execute( "d2" .. " --theme=" .. options.theme .. " --layout=" .. options.layout .. " --sketch=" .. tostring(options.sketch) .. " --pad=" .. options.pad .. - " " .. tempPath .. + " " .. inputPath .. " " .. outputPath ) - return outputPath - end) + 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) - -- Read the generated output if need be - if options.folder == nil then - local file = io.open(result, "rb") - if file then - data = file:read('*all') - file:close() + 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 - os.remove(result) -- Remove the output file since it'll be inline + 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.format == "svg" then - imageData = "data:image/svg+xml;base64," .. quarto.base64.encode(data) - elseif options.format == "pdf" then - imageData = result - else - imageData = "data:image/png;base64," .. quarto.base64.encode(data) + 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 - imageData = result - end - - -- Read the generated output into a Pandoc Image element - local img = pandoc.Image({}, imageData) + 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 - -- Set the width and height attributes, if they exist - if options.width ~= nil then - img.attributes.width = options.width - end + if options.height ~= nil then + image.attributes.height = options.height + end - if options.height ~= nil then - img.attributes.height = options.height - end + if options.caption ~= '' then + image.caption = pandoc.Str(options.caption) + end - if options.caption ~= '' then - img.caption = pandoc.Str(options.caption) + output = pandoc.Para({image}) end -- Wrap the Image element in a Para element and return it - local output = pandoc.Para({img}) if options.echo then local codeBlock = pandoc.CodeBlock(cb.text, cb.attr) output = pandoc.Div({codeBlock, output}) @@ -214,7 +256,8 @@ function Pandoc(doc) caption = '', width = nil, height = nil, - echo = false + echo = false, + embed_mode = "inline" } -- Process global attributes