diff --git a/CHANGELOG.md b/CHANGELOG.md index 0213dbf..b88125b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,21 @@ * Added minimal CI testing using GitHub actions (#10). +* Make SVG as the default diagram format when using the [Typst output format](https://quarto.org/docs/output-formats/typst.html) (#7, @elipousson). + +* Add support for reading d2 diagrams from external files using `file` parameter. Block text is replaced with file contents (#7, @elipousson). + +* Use Pandoc mediabag for rendered diagram images when `embed_type="link"` (#7, @elipousson). + +* Add support for alternate code block syntax without curly braces (#7, @elipousson). + ## BUG FIXES -* Added tala to the list of layouts (#9, thanks @tosaddler!). +* Added [TALA](https://d2lang.com/tour/tala/) to the list of layouts (#9, @tosaddler). + +## OTHER + +* Refactor to add helper functions `setPreD2RenderOptions` and `is_nonempty_string`. # quarto-d2 1.1.0 @@ -14,11 +26,10 @@ - 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: - Render [D2](https://d2lang.com) diagrams directly within your [Quarto](https://quarto.org) markdown documents. - Control the appearance and layout of your diagrams using global settings or code block attributes. -- Tune the width and height of the resulting figures using the "width" and "height" arguments. \ No newline at end of file +- Tune the width and height of the resulting figures using the "width" and "height" arguments. diff --git a/README.md b/README.md index b0a79e4..d428faa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # D2 Extension For Quarto + This [Quarto](https://quarto.org) extension allows you to render [D2](https://d2lang.com) diagrams directly within your markdown documents. @@ -34,7 +35,13 @@ quarto add data-intuitive/quarto-d2 This will install the extension under the `_extensions` subdirectory. If you’re using version control, you will want to check in this directory. -## Example +## Examples + +D2 can be used for simple diagrams. + +![](images/diagram-1.svg) + +And for more complex diagrams.
@@ -85,68 +92,61 @@ user -> network.portal.UI: access { } ``` - +
-## Usage +The enclosing curly brakets are optional if you are only using document +level options. Quarto block-level options, e.g. `#|`, are not currently +supported. -To use the d2 filter, add the d2 filter to your quarto document. Next, -add the `.d2` class to any code blocks containing D2 diagram code. Here -is a basic example: - -```` markdown ---- -title: "D2 Example" -filters: - - d2 ---- - -```{.d2} -x -> y -``` -```` - -With this setup, the `d2` filter will process any code blocks with the -`.d2` class, applying the attributes you specify. - -That’s it! Now you know how to use the `d2` filter to generate diagrams -in your quarto documents. +![](images/diagram-3.svg) ## Attributes You can specify additional attributes to control the appearance and -layout of the diagram. +layout of the diagram and document: - `theme`: Specifies the theme of the diagram. Default is `"NeutralDefault"`. Options are `"NeutralDefault"`, `"NeutralGrey"`, - `"FlagshipTerrastruc"t`, `"CoolClassics"`, `"MixedBerryBlue"`, - `"GrapeSoda"`, `"Aubergine"`, `"ColorblindClear"`, - `"VanillaNitroCola"`, `"ShirelyTemple"`, `"EarthTones"`, - `"EvergladeGreen"`, `"ButteredToast"`, `"DarkMauve"`, `"Terminal"`, - `"TerminalGrayscale"`, `"Origami"`. + `"FlagshipTerrastruct"`, `"DarkFlagshipTerrastruct"`, + `"CoolClassics"`, `"MixedBerryBlue"`, `"GrapeSoda"`, `"Aubergine"`, + `"ColorblindClear"`, `"VanillaNitroCola"`, `"ShirelyTemple"`, + `"EarthTones"`, `"EvergladeGreen"`, `"ButteredToast"`, `"DarkMauve"`, + `"Terminal"`, `"TerminalGrayscale"`, and `"Origami"`. - `layout`: Specifies the layout algorithm to use. Default is `"elk"`. - Options are `"dagre"`, `"elk"`, `"tala"`. + Options are `"dagre"`, `"elk"`, `"tala"`. layout is not case sensitive + so `"ELK"` or `"TALA"` are also supported. - `format`: Specifies the format of the output image. Default is `svg`. - Option are `"svg"`, `"png"`, `"pdf"`. + Option are `"svg"`, `"png"`, `"pdf"`, `"gif"`. - `sketch`: Whether to use a “sketch” style for the diagram. Default is `false`. -- `pad`: Amount of padding around the diagram. Default is `100`. +- `pad`: Amount of padding around the diagram in pixels. Default is + `100`. - `caption`: Caption to add to the diagram. -- `folder`: Folder where the generated diagram will be saved. If not - provided, the image will be embedded inline in the document (HTML - only). -- `filename`: Name of the output file. - `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`. + +You can also replace the contents of the block with an external D2 file +by using the `file` parameter. `file` must be an existing file ending in +a “d2” or “txt” file extension. Other parameters related to rendering +and embedding diagrams include: + +- `folder`: Folder where the generated diagram will be saved. If not + provided, the image will be embedded inline in the document (HTML + only). +- `filename`: Name of the output file. - `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"`. +Note that for Typst format output the width and height can’t be supplied +as a percent value. + Here’s an example that uses multiple attributes: ```` markdown @@ -175,6 +175,16 @@ x -> y -> z ``` ```` +## Setting an input file + +You can specify an input d2 file. If `echo=true`, the contents of the +file block is replaced by the contents of the file. + +```` markdown +```{.d2 file="./diagram.d2"} +``` +```` + ## Setting output folder and file name You can specify a folder where the generated diagram will be saved using @@ -187,15 +197,11 @@ x -> y -> z ``` ```` -
- -> **Note** +> [!NOTE] > > 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 diff --git a/README.qmd b/README.qmd index 7d150c2..57b1868 100644 --- a/README.qmd +++ b/README.qmd @@ -5,6 +5,7 @@ filters: - d2 d2: layout: "elk" + folder: "images" --- This [Quarto](https://quarto.org) extension allows you to render [D2](https://d2lang.com) diagrams directly within your markdown documents. @@ -34,10 +35,17 @@ quarto add data-intuitive/quarto-d2 This will install the extension under the `_extensions` subdirectory. If you're using version control, you will want to check in this directory. +## Examples +D2 can be used for simple diagrams. -## Example +```{.d2} +x -> y: hello world +``` + + +And for more complex diagrams. ```{.d2 width="50%" echo="true"} logs: { @@ -87,44 +95,36 @@ user -> network.portal.UI: access { ``` -## Usage +The enclosing curly brakets are optional if you are only using document level options. Quarto block-level options, e.g. `#|`, are not currently supported. -To use the d2 filter, add the d2 filter to your quarto document. Next, add the `.d2` class to any code blocks containing D2 diagram code. Here is a basic example: - - -````markdown ---- -title: "D2 Example" -filters: - - d2 ---- - -```{.d2} -x -> y +```d2 +Database -> S3: backup +Database -> S3 +Database -> S3: backup ``` -```` - -With this setup, the `d2` filter will process any code blocks with the `.d2` class, applying the attributes you specify. - -That's it! Now you know how to use the `d2` filter to generate diagrams in your quarto documents. ## Attributes -You can specify additional attributes to control the appearance and layout of the diagram. +You can specify additional attributes to control the appearance and layout of the diagram and document: -- `theme`: Specifies the theme of the diagram. Default is `"NeutralDefault"`. Options are `"NeutralDefault"`, `"NeutralGrey"`, `"FlagshipTerrastruc"t`, `"CoolClassics"`, `"MixedBerryBlue"`, `"GrapeSoda"`, `"Aubergine"`, `"ColorblindClear"`, `"VanillaNitroCola"`, `"ShirelyTemple"`, `"EarthTones"`, `"EvergladeGreen"`, `"ButteredToast"`, `"DarkMauve"`, `"Terminal"`, `"TerminalGrayscale"`, `"Origami"`. -- `layout`: Specifies the layout algorithm to use. Default is `"elk"`. Options are `"dagre"`, `"elk"`, `"tala"`. -- `format`: Specifies the format of the output image. Default is `svg`. Option are `"svg"`, `"png"`, `"pdf"`. +- `theme`: Specifies the theme of the diagram. Default is `"NeutralDefault"`. Options are `"NeutralDefault"`, `"NeutralGrey"`, `"FlagshipTerrastruct"`, `"DarkFlagshipTerrastruct"`, `"CoolClassics"`, `"MixedBerryBlue"`, `"GrapeSoda"`, `"Aubergine"`, `"ColorblindClear"`, `"VanillaNitroCola"`, `"ShirelyTemple"`, `"EarthTones"`, `"EvergladeGreen"`, `"ButteredToast"`, `"DarkMauve"`, `"Terminal"`, `"TerminalGrayscale"`, and `"Origami"`. +- `layout`: Specifies the layout algorithm to use. Default is `"elk"`. Options are `"dagre"`, `"elk"`, `"tala"`. layout is not case sensitive so `"ELK"` or `"TALA"` are also supported. +- `format`: Specifies the format of the output image. Default is `svg`. Option are `"svg"`, `"png"`, `"pdf"`, `"gif"`. - `sketch`: Whether to use a "sketch" style for the diagram. Default is `false`. -- `pad`: Amount of padding around the diagram. Default is `100`. +- `pad`: Amount of padding around the diagram in pixels. Default is `100`. - `caption`: Caption to add to the diagram. -- `folder`: Folder where the generated diagram will be saved. If not provided, the image will be embedded inline in the document (HTML only). -- `filename`: Name of the output file. - `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`. + +You can also replace the contents of the block with an external D2 file by using the `file` parameter. `file` must be an existing file ending in a "d2" or "txt" file extension. Other parameters related to rendering and embedding diagrams include: + +- `folder`: Folder where the generated diagram will be saved. If not provided, the image will be embedded inline in the document (HTML only). +- `filename`: Name of the output file. - `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"`. +Note that for Typst format output the width and height can't be supplied as a percent value. + Here's an example that uses multiple attributes: ````markdown @@ -152,6 +152,15 @@ x -> y -> z ``` ```` +## Setting an input file + +You can specify an input d2 file. If `echo=true`, the contents of the file block is replaced by the contents of the file. + +````markdown +```{.d2 file="./diagram.d2"} +``` +```` + ## 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. diff --git a/_extensions/d2/d2.lua b/_extensions/d2/d2.lua index fccfd6d..f92a262 100644 --- a/_extensions/d2/d2.lua +++ b/_extensions/d2/d2.lua @@ -15,6 +15,7 @@ local D2Theme = { EvergladeGreen = 104, ButteredToast = 105, DarkMauve = 200, + DarkFlagshipTerrastruct = 201, Terminal = 300, TerminalGrayscale = 301, Origami = 302 @@ -31,8 +32,10 @@ local D2Layout = { local D2Format = { svg = 'svg', png = 'png', + gif = 'gif', pdf = 'pdf' } + -- Enum for Embed mode local EmbedMode = { inline = "inline", @@ -68,15 +71,90 @@ function dump(o) end end +-- Helper for non empty string +function is_nonempty_string(x) + return x ~= nil and type(x) == "string" +end -- Counter for the diagram files local counter = 0 +-- Transform and validate options +function setPreD2RenderOptions(options) + if is_nonempty_string(options.theme) then + assert(D2Theme[options.theme] ~= nil, + "Invalid theme: " .. options.theme .. ". Options are: " .. dump(D2Theme)) + options.theme = D2Theme[options.theme] + end + if is_nonempty_string(options.layout) then + assert(D2Layout[string.lower(options.layout)] ~= nil, + "Invalid layout: " .. options.layout .. ". Options are: " .. dump(D2Layout)) + options.layout = D2Layout[string.lower(options.layout)] + end + if is_nonempty_string(options.format) then + assert(D2Format[options.format] ~= nil, + "Invalid format: " .. options.format .. ". Options are: " .. dump(D2Format)) + options.format = D2Format[options.format] + end + if is_nonempty_string(options.embed_mode) 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 is_nonempty_string(options.sketch) then + assert(options.sketch == "true" or options.sketch == "false", + "Invalid sketch: " .. options.sketch .. ". Options are: true, false") + options.sketch = tostring(options.sketch == "true") + end + if is_nonempty_string(options.pad) then + assert(tonumber(options.pad) ~= nil, + "Invalid pad: " .. options.pad .. ". Must be a number") + end + if is_nonempty_string(options.echo) then + assert(options.echo == "true" or options.echo == "false", + "Invalid echo: " .. options.echo .. ". Options are: true, false") + options.echo = options.echo == "true" + end + if is_nonempty_string(options.animate_interval) and options.format == D2Format.gif then + assert(tonumber(options.animate_interval) > 0, + "Invalid animate_interval: " .. options.animate_interval .. ". Must be greater than 0 for .gif outputs") + end + -- Check file extension + if is_nonempty_string(options.file) then + local d2path,d2ext = pandoc.path.split_extension(options.file) + assert(d2ext == ".d2" or d2ext == ".txt", + "Invalid file: " .. options.file .. ". Must use a 'd2' or 'txt' file extension") + end + + -- Set default filename + if not is_nonempty_string(options.filename) 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 format to svg since pdf is not supported in Typst output + if options.format == D2Format.pdf and quarto.doc.is_format("typst") then + options.format = D2Format.svg + 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 project output directory when embed_mode is link + if options.folder == nil and options.embed_mode == EmbedMode.link then + options.folder = quarto.project.output_directory + end + + return options +end + local function render_graph(globalOptions) - local filter = { - CodeBlock = function(cb) + local CodeBlock = function(cb) -- Check if the CodeBlock has the 'd2' class - if not cb.classes:includes('d2') or cb.text == nil then + if not cb.classes:includes('d2') then return nil end @@ -89,69 +167,22 @@ local function render_graph(globalOptions) 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 + + options = setPreD2RenderOptions(options) + + if options.echo then + cb.classes:insert('sourceCode') + cb.classes:insert('cell-code') 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" + + if options.file == nil and cb.text == nil then + return nil end -- Generate diagram using `d2` CLI utility - local result = pandoc.system.with_temporary_directory('svg-convert', function (tmpdir) + local result = pandoc.system.with_temporary_directory('d2-render', 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 + local inputPath = pandoc.path.join({tmpdir, "diagram-" .. counter .. ".d2"}) -- write graph text to file local tmpFile = io.open(inputPath, "w") @@ -159,9 +190,33 @@ local function render_graph(globalOptions) print("Error: Could not open file for writing") return nil end + + if is_nonempty_string(options.file) then + local d2File = io.open(options.file) + if d2File == nil then + print("Error: Diagram file " .. options.file .. " can't be opened") + return nil + end + + local d2Text = d2File:read('*all') + cb.text = d2Text + cb.attributes.filename = pandoc.path.filename(options.file) + end + tmpFile:write(cb.text) tmpFile:close() + + -- determine path name of output file + local outputPath + local outputFilename = options.filename .. "." .. options.format + if options.folder ~= nil then + os.execute("mkdir -p " .. options.folder) + outputPath = pandoc.path.join({options.folder, outputFilename}) + else + outputPath = pandoc.path.join({tmpdir, outputFilename}) + end + -- run d2 os.execute( "d2" .. @@ -169,34 +224,48 @@ local function render_graph(globalOptions) " --layout=" .. options.layout .. " --sketch=" .. tostring(options.sketch) .. " --pad=" .. options.pad .. + " --animate-interval=" .. options.animate_interval .. " " .. inputPath .. " " .. outputPath ) + local outputFile = io.open(outputPath, "rb") + local data + + if outputFile then + data = outputFile:read('*all') + outputFile:close() + end + + local mimetype + + if options.format == "svg" then + mimetype = "image/svg+xml" + elseif options.format == "png" then + mimetype = "image/png" + elseif options.format == "pdf" then + mimetype = "application/pdf" + elseif options.format == "gif" then + mimetype = "image/gif" + 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() + if options.folder ~= nil then + return outputPath end + + pandoc.mediabag.insert(outputFilename, mt, data) + return outputFilename + elseif options.embed_mode == EmbedMode.raw then 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 + return data + elseif options.embed_mode == EmbedMode.inline then + if options.format ~= "pdf" then + os.remove(outputPath) + return "data:" .. mimetype .. ";base64," .. quarto.base64.encode(data) + else + print("Error: Unsupported format") + return nil end end end) @@ -214,7 +283,10 @@ local function render_graph(globalOptions) end else - local image = pandoc.Image({}, result) + local image = pandoc.Image({ + classes = cb.classes, + identifier = cb.identifier + }, result) -- Set the width and height attributes, if they exist if options.width ~= nil then @@ -239,8 +311,15 @@ local function render_graph(globalOptions) end return output end + -- see https://github.com/quarto-dev/quarto-cli/discussions/8926#discussioncomment-8624950 + local DecoratedCodeBlock = function(node) + return CodeBlock(node.code_block) + end + + return { + CodeBlock = CodeBlock, + DecoratedCodeBlock = DecoratedCodeBlock } - return filter end @@ -253,11 +332,13 @@ function Pandoc(doc) sketch = false, pad = 100, folder = nil, + file = nil, filename = nil, caption = '', width = nil, height = nil, echo = false, + animate_interval = 0, embed_mode = "inline" } diff --git a/images/diagram-1.svg b/images/diagram-1.svg index bb2eca9..b94a35f 100644 --- a/images/diagram-1.svg +++ b/images/diagram-1.svg @@ -1,24 +1,17 @@ -logsUserNetworkAPI ServerCell TowerData ProcessorOnline PortalsatellitestransmitterStorageUI phone logsMake callpersistdisplay access - - - - - - - - - - - - - - - - - + .d2-2290820312 .fill-N1{fill:#0A0F25;} + .d2-2290820312 .fill-N2{fill:#676C7E;} + .d2-2290820312 .fill-N3{fill:#9499AB;} + .d2-2290820312 .fill-N4{fill:#CFD2DD;} + .d2-2290820312 .fill-N5{fill:#DEE1EB;} + .d2-2290820312 .fill-N6{fill:#EEF1F8;} + .d2-2290820312 .fill-N7{fill:#FFFFFF;} + .d2-2290820312 .fill-B1{fill:#0D32B2;} + .d2-2290820312 .fill-B2{fill:#0D32B2;} + .d2-2290820312 .fill-B3{fill:#E3E9FD;} + .d2-2290820312 .fill-B4{fill:#E3E9FD;} + .d2-2290820312 .fill-B5{fill:#EDF0FD;} + .d2-2290820312 .fill-B6{fill:#F7F8FE;} + .d2-2290820312 .fill-AA2{fill:#4A6FF3;} + .d2-2290820312 .fill-AA4{fill:#EDF0FD;} + .d2-2290820312 .fill-AA5{fill:#F7F8FE;} + .d2-2290820312 .fill-AB4{fill:#EDF0FD;} + .d2-2290820312 .fill-AB5{fill:#F7F8FE;} + .d2-2290820312 .stroke-N1{stroke:#0A0F25;} + .d2-2290820312 .stroke-N2{stroke:#676C7E;} + .d2-2290820312 .stroke-N3{stroke:#9499AB;} + .d2-2290820312 .stroke-N4{stroke:#CFD2DD;} + .d2-2290820312 .stroke-N5{stroke:#DEE1EB;} + .d2-2290820312 .stroke-N6{stroke:#EEF1F8;} + .d2-2290820312 .stroke-N7{stroke:#FFFFFF;} + .d2-2290820312 .stroke-B1{stroke:#0D32B2;} + .d2-2290820312 .stroke-B2{stroke:#0D32B2;} + .d2-2290820312 .stroke-B3{stroke:#E3E9FD;} + .d2-2290820312 .stroke-B4{stroke:#E3E9FD;} + .d2-2290820312 .stroke-B5{stroke:#EDF0FD;} + .d2-2290820312 .stroke-B6{stroke:#F7F8FE;} + .d2-2290820312 .stroke-AA2{stroke:#4A6FF3;} + .d2-2290820312 .stroke-AA4{stroke:#EDF0FD;} + .d2-2290820312 .stroke-AA5{stroke:#F7F8FE;} + .d2-2290820312 .stroke-AB4{stroke:#EDF0FD;} + .d2-2290820312 .stroke-AB5{stroke:#F7F8FE;} + .d2-2290820312 .background-color-N1{background-color:#0A0F25;} + .d2-2290820312 .background-color-N2{background-color:#676C7E;} + .d2-2290820312 .background-color-N3{background-color:#9499AB;} + .d2-2290820312 .background-color-N4{background-color:#CFD2DD;} + .d2-2290820312 .background-color-N5{background-color:#DEE1EB;} + .d2-2290820312 .background-color-N6{background-color:#EEF1F8;} + .d2-2290820312 .background-color-N7{background-color:#FFFFFF;} + .d2-2290820312 .background-color-B1{background-color:#0D32B2;} + .d2-2290820312 .background-color-B2{background-color:#0D32B2;} + .d2-2290820312 .background-color-B3{background-color:#E3E9FD;} + .d2-2290820312 .background-color-B4{background-color:#E3E9FD;} + .d2-2290820312 .background-color-B5{background-color:#EDF0FD;} + .d2-2290820312 .background-color-B6{background-color:#F7F8FE;} + .d2-2290820312 .background-color-AA2{background-color:#4A6FF3;} + .d2-2290820312 .background-color-AA4{background-color:#EDF0FD;} + .d2-2290820312 .background-color-AA5{background-color:#F7F8FE;} + .d2-2290820312 .background-color-AB4{background-color:#EDF0FD;} + .d2-2290820312 .background-color-AB5{background-color:#F7F8FE;} + .d2-2290820312 .color-N1{color:#0A0F25;} + .d2-2290820312 .color-N2{color:#676C7E;} + .d2-2290820312 .color-N3{color:#9499AB;} + .d2-2290820312 .color-N4{color:#CFD2DD;} + .d2-2290820312 .color-N5{color:#DEE1EB;} + .d2-2290820312 .color-N6{color:#EEF1F8;} + .d2-2290820312 .color-N7{color:#FFFFFF;} + .d2-2290820312 .color-B1{color:#0D32B2;} + .d2-2290820312 .color-B2{color:#0D32B2;} + .d2-2290820312 .color-B3{color:#E3E9FD;} + .d2-2290820312 .color-B4{color:#E3E9FD;} + .d2-2290820312 .color-B5{color:#EDF0FD;} + .d2-2290820312 .color-B6{color:#F7F8FE;} + .d2-2290820312 .color-AA2{color:#4A6FF3;} + .d2-2290820312 .color-AA4{color:#EDF0FD;} + .d2-2290820312 .color-AA5{color:#F7F8FE;} + .d2-2290820312 .color-AB4{color:#EDF0FD;} + .d2-2290820312 .color-AB5{color:#F7F8FE;}.appendix text.text{fill:#0A0F25}.md{--color-fg-default:#0A0F25;--color-fg-muted:#676C7E;--color-fg-subtle:#9499AB;--color-canvas-default:#FFFFFF;--color-canvas-subtle:#EEF1F8;--color-border-default:#0D32B2;--color-border-muted:#0D32B2;--color-neutral-muted:#EEF1F8;--color-accent-fg:#0D32B2;--color-accent-emphasis:#0D32B2;--color-attention-subtle:#676C7E;--color-danger-fg:red;}.sketch-overlay-B1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B2{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-B3{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-B6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-AA4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AA5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB4{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-AB5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N1{fill:url(#streaks-darker);mix-blend-mode:lighten}.sketch-overlay-N2{fill:url(#streaks-dark);mix-blend-mode:overlay}.sketch-overlay-N3{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N4{fill:url(#streaks-normal);mix-blend-mode:color-burn}.sketch-overlay-N5{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N6{fill:url(#streaks-bright);mix-blend-mode:darken}.sketch-overlay-N7{fill:url(#streaks-bright);mix-blend-mode:darken}.light-code{display: block}.dark-code{display: none}]]>xy hello world + + + + diff --git a/images/diagram-2.svg b/images/diagram-2.svg new file mode 100644 index 0000000..2a4609b --- /dev/null +++ b/images/diagram-2.svg @@ -0,0 +1,124 @@ +logsUserNetworkAPI ServerCell TowerData ProcessorOnline PortalsatellitestransmitterStorageUI phone logsMake callpersistdisplay access + + + + + + + + + + + + + + + + + + diff --git a/images/diagram-3.svg b/images/diagram-3.svg new file mode 100644 index 0000000..5f650da --- /dev/null +++ b/images/diagram-3.svg @@ -0,0 +1,105 @@ +DatabaseS3 backupbackup + + + + + + diff --git a/test.qmd b/test.qmd new file mode 100644 index 0000000..483ae40 --- /dev/null +++ b/test.qmd @@ -0,0 +1,216 @@ +--- +title: D2 Extension For Quarto +format: html +eval: false +filters: + - d2 +d2: + layout: "elk" +--- + +This [Quarto](https://quarto.org) extension allows you to render [D2](https://d2lang.com) diagrams directly within your markdown documents. + +Main features: + +- Render [D2](https://d2lang.com) diagrams directly within your [Quarto](https://quarto.org) markdown documents. +- Control the appearance and layout of your diagrams using global settings or code block attributes. +- Tune the width and height of the resulting figures using the "width" and "height" arguments. + +This extension was inspired by [`ram02z/d2-filter`](https://github.com/ram02z/d2-filter). + +## Installation + +### Prerequisites + +Ensure that you have [D2](https://d2lang.com/tour/install) installed on your system. + +### Install + +Run the following command to add this extension to your current project: + +``` bash +quarto add data-intuitive/quarto-d2 +``` + +This will install the extension under the `_extensions` subdirectory. +If you're using version control, you will want to check in this directory. + +## Examples + +D2 can be used for simple diagrams. + + +```{.d2} +x -> y: hello world +``` + + +And for more complex diagrams. + +```{.d2 width="50%" echo="true"} +logs: { + shape: page + style.multiple: true +} +user: User {shape: person} +network: Network { + tower: Cell Tower { + satellites: { + shape: stored_data + style.multiple: true + } + + satellites -> transmitter + satellites -> transmitter + satellites -> transmitter + transmitter + } + processor: Data Processor { + storage: Storage { + shape: cylinder + style.multiple: true + } + } + portal: Online Portal { + UI + } + + tower.transmitter -> processor: phone logs +} +server: API Server + +user -> network.tower: Make call +network.processor -> server +network.processor -> server +network.processor -> server + +server -> logs +server -> logs +server -> logs: persist + +server -> network.portal.UI: display +user -> network.portal.UI: access { + style.stroke-dash: 3 +} +``` + + +The enclosing curly brakets are optional if you are only using document level options. Quarto block-level options, e.g. `#|`, are not currently supported. + + +```d2 +Database -> S3: backup +Database -> S3 +Database -> S3: backup +``` + + +## Usage + +To use the d2 filter, add the d2 filter to your quarto document. Next, add the `.d2` class to any code blocks containing D2 diagram code. Here is a basic example: + + +````markdown +--- +title: "D2 Example" +filters: + - d2 +--- + +```{.d2} +x -> y +``` +```` + +With this setup, the `d2` filter will process any code blocks with the `.d2` class, applying the attributes you specify. + +That's it! Now you know how to use the `d2` filter to generate diagrams in your quarto documents. + +## Attributes + +You can specify additional attributes to control the appearance and layout of the diagram and document: + +- `theme`: Specifies the theme of the diagram. Default is `"NeutralDefault"`. Options are `"NeutralDefault"`, `"NeutralGrey"`, `"FlagshipTerrastruct"`, `"DarkFlagshipTerrastruct"`, `"CoolClassics"`, `"MixedBerryBlue"`, `"GrapeSoda"`, `"Aubergine"`, `"ColorblindClear"`, `"VanillaNitroCola"`, `"ShirelyTemple"`, `"EarthTones"`, `"EvergladeGreen"`, `"ButteredToast"`, `"DarkMauve"`, `"Terminal"`, `"TerminalGrayscale"`, and `"Origami"`. +- `layout`: Specifies the layout algorithm to use. Default is `"elk"`. Options are `"dagre"`, `"elk"`, `"tala"`. layout is not case sensitive so `"ELK"` or `"TALA"` are also supported. +- `format`: Specifies the format of the output image. Default is `svg`. Option are `"svg"`, `"png"`, `"pdf"`. +- `sketch`: Whether to use a "sketch" style for the diagram. Default is `false`. +- `pad`: Amount of padding around the diagram. Default is `100`. +- `caption`: Caption to add to the diagram. +- `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`. + +You can also replace the contents of the block with an external d2 file using the `file` parameter. Other parameters related to rendering and embedding diagrams include: + +- `folder`: Folder where the generated diagram will be saved. If not provided, the image will be embedded inline in the document (HTML only). +- `filename`: Name of the output file. +- `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"`. + +Note that for Typst format output the width and height can't be supplied as a percent value. + +Here's an example that uses multiple attributes: + +````markdown +```{.d2 theme="CoolClassics" layout="elk" pad=20 caption="This is a caption" width="50%"} +x -> y -> z +``` +```` + +## Global Options + +You can set global options for the d2 filter using the `d2` field in the document metadata. Here's an example: + +````markdown +--- +title: "D2 Example" +filters: + - d2 +d2: + layout: elk + theme: "GrapeSoda" +--- + +```{.d2 width="40%" echo=true} +x -> y -> z +``` +```` + +## 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. + +````markdown +```{.d2 folder="./images" filename="my_diagram"} +x -> y -> z +``` +```` + +:::{.callout-note} +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/tests/global.qmd b/tests/global.qmd index fafd284..e30604c 100644 --- a/tests/global.qmd +++ b/tests/global.qmd @@ -10,3 +10,7 @@ d2: ```{.d2 width="40%" echo=true} x -> y -> z ``` + +```{.d2 file=test.d2 echo=true} +x -> y -> z +``` diff --git a/tests/test.d2 b/tests/test.d2 new file mode 100644 index 0000000..cbe7ff3 --- /dev/null +++ b/tests/test.d2 @@ -0,0 +1 @@ +file -> is -> working diff --git a/tests/typst.qmd b/tests/typst.qmd new file mode 100644 index 0000000..2751bd0 --- /dev/null +++ b/tests/typst.qmd @@ -0,0 +1,20 @@ +--- +title: "D2 Example" +format: typst +filters: + - d2 +d2: + layout: elk + format: svg + theme: "GrapeSoda" +--- + +```{.d2 width="6in" echo=true} +direction: right +x -> y -> z +``` + + +```{.d2 format=png width="3in" echo=true} +a -> b +```