Skip to content

Render AsciiDoc diagram blocks as images - #2099

Draft
jorgembfigueira wants to merge 1 commit into
github:masterfrom
jorgembfigueira:asciidoc-diagram-rendering
Draft

Render AsciiDoc diagram blocks as images#2099
jorgembfigueira wants to merge 1 commit into
github:masterfrom
jorgembfigueira:asciidoc-diagram-rendering

Conversation

@jorgembfigueira

Copy link
Copy Markdown

Diagram blocks in AsciiDoc files — [plantuml], [graphviz], [mermaid] and the other types asciidoctor-diagram supports — are currently emitted as an untagged <pre>, losing the diagram language entirely:

[plantuml]
----
@startuml
Alice -> Bob: hi
@enduml
----
<div class="listingblock"><div class="content">
<pre>@startuml
Alice -&gt; Bob: hi
@enduml</pre>
</div></div>

Markdown already does better. commonmarker's github_pre_lang emits <pre lang="plantuml"> for a fenced block, exactly as it does for mermaid, so the language survives for a client-side renderer to pick up. This brings AsciiDoc in line, and renders the diagram outright when the optional gem is installed.

What changes

A diagram block resolves in two steps:

  1. If asciidoctor-diagram is installed, render locally and inline the result as a data URI, so the output stays self-contained and does not depend on where the generated file landed:

    <img src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0i..." alt="plantuml diagram">
  2. Otherwise, fall back to a source block that keeps the diagram language on the <pre>. This is also what happens when the toolchain a diagram type needs (a JVM for PlantUML, Graphviz for dot, the d2 binary, …) is missing, so a missing toolchain degrades instead of erroring:

    <pre lang="plantuml"><code>@startuml ...</code></pre>

Nothing is enabled by default. Without the optional gem the behaviour is step 2, so a plain install gains the language tag and depends on no new service and no new toolchain. This also covers C4 — !include <C4/C4_Container> plus the C4 macros renders through the PlantUML processor.

Notes on the implementation

Four things that are less obvious, each pinned by a test:

  • Scoped extension registry, never the global one. Requiring asciidoctor-diagram's top level registers 37 extension groups globally as a side effect, which would change how unrelated code in the same process converts documents. The individual asciidoctor-diagram/<type>/extension files are required instead — those register nothing.

  • data-uri is passed per block, not as a document attribute. It is what makes asciidoctor-diagram report an absolute path for the generated file, which is how the bytes are found. Setting it document-wide would also make Asciidoctor try to inline every ordinary image:: in the file.

  • A scratch directory as base_dir. Under safe: :secure Asciidoctor confines file writes to the base dir, so imagesoutdir pointing elsewhere is ignored. Handing the conversion a scratch directory keeps generated images out of the caller's working directory; they are inlined and the directory is discarded, so rendering stays string-in, string-out.

  • Data URIs need the sanitizer's cooperation. The stock html-pipeline config allows only http, https and :relative on img/src, so it keeps the <img> and drops the src, leaving a broken image. Consumers that sanitize need to allow the data: protocol; this is documented in the README and pinned by a test, since it is the difference between a rendered diagram and a broken image.

Testing

asciidoctor-diagram is added as a development dependency (672K). The PlantUML JAR gem (20M) deliberately is not: the tests stub the rendering backend, so CI needs no JVM and no diagram toolchain.

The suite passes at the repository's 100% line and branch coverage gate. I also verified real rendering locally, outside the test suite: a C4 container diagram produced a 6123-byte SVG containing the expected participants and relationships, and format=png produced a data:image/png URI.

Open questions for maintainers

  • Should this be opt-in via options: rather than gem presence? Right now installing asciidoctor-diagram is what turns rendering on. An explicit option would make it impossible to enable by accident; happy to switch.
  • Is the data URI the right output? It is what keeps the string-in/string-out contract, but it costs the sanitizer configuration noted above. The alternative — emitting a file path and letting the consumer serve the directory — breaks that contract.
  • Scope of LOCAL_EXTENSIONS. I mapped the 13 types that are conventionally written as delimited blocks. Trimming to just PlantUML/Graphviz/Mermaid would be a smaller diff if preferred.

Marked as a draft: I would rather agree the shape of the above before polishing.

Diagram blocks in AsciiDoc files -- [plantuml], [graphviz], [mermaid] and the
other types asciidoctor-diagram supports -- were emitted as an untagged <pre>,
losing the diagram language entirely. Markdown does better: commonmarker's
github_pre_lang already emits <pre lang="plantuml">, so the language survives
for a client-side renderer to pick up. This brings AsciiDoc in line and, when
the optional asciidoctor-diagram gem is installed, renders the diagram outright.

A diagram block now resolves in two steps:

  1. If asciidoctor-diagram is installed, render locally and inline the result
     as a data URI, so the output stays self-contained and does not depend on
     where the generated file landed.
  2. Otherwise fall back to a source block that keeps the diagram language on
     the <pre>, which is also what happens when the toolchain a diagram type
     needs (a JVM for PlantUML, Graphviz for dot, ...) is missing.

Nothing is enabled by default: without the optional gem the behaviour is step 2,
so a plain install gains the language tag and depends on no new toolchain.

Notes on the implementation:

  - The extensions live in a scoped registry, never the global one. Requiring
    asciidoctor-diagram's top level registers 37 extension groups globally as a
    side effect, which would change how unrelated code in the same process
    converts documents, so the individual per-type extension files are required
    instead.
  - 'data-uri' is passed per block rather than as a document attribute. It is
    what makes asciidoctor-diagram report an absolute path for the generated
    file, while setting it document-wide would also make Asciidoctor try to
    inline every ordinary image:: in the file.
  - Under safe mode :secure Asciidoctor confines file writes to the base dir, so
    the conversion is handed a scratch directory. Generated images are inlined
    and the directory is discarded, keeping rendering string-in, string-out.
  - A data URI only survives a sanitizer that allows the data: protocol on
    img/src, which the stock html-pipeline config does not. This is documented
    in the README and pinned by a test, as it is the difference between a
    rendered diagram and a broken image for consumers that sanitize.

asciidoctor-diagram is a development dependency (672K) but the PlantUML JAR gem
(20M) deliberately is not: the tests stub the rendering backend, so CI needs no
JVM and no diagram toolchain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant