An Asciidoctor extension that renders logic and protection-relay diagrams written in the Logic Diagram Language (LDL) into SVG or PNG at conversion time, driven by AsciiDoc attributes.
You write boolean equations in an AsciiDoc block; the extension renders a clean, laid-out diagram and embeds it as an image — no manual placement, no separate build step.
[ldl]
....
TRIP.Name = "Trip"
TRIP = OVERCURRENT AND NOT BLOCK
OR (EARTH AND MANUAL)
....
How it works
Rendering is delegated to the official @openpowershift/logic-diagram-language
npm package (the same renderer used by the
LDL playground). The gem ships a tiny Node helper that imports the
package and returns the rendered artifact; the Ruby side handles attributes,
output paths and caching.
-
SVG output is produced by the LDL renderer directly (isomorphic — pure Node). It needs only Node and the npm package. Being vector, it embeds cleanly in
asciidoctor-pdf, scales without loss, and stays small. -
PNG output additionally rasterises the SVG with
@resvg/resvg-js— a single prebuilt, dependency-free native module (no system libraries such as ImageMagick, Cairo or a headless browser required).
The dependency footprint is deliberately small: Ruby, Node, and the LDL npm
package (plus @resvg/resvg-js only if you want PNG).
Prerequisites
-
Ruby (works on older versions — 2.5+) with Asciidoctor 2.0+.
-
Node.js 20 or newer, on
PATH. -
The LDL npm package, installed where you build your documents:
$ npm install @openpowershift/logic-diagram-language $ npm install @resvg/resvg-js # only needed for PNG output
The extension resolves the npm package from your project’s node_modules
(your build directory), or from a location you point it at with the
ldl-package-dir attribute or the LDL_PACKAGE_DIR environment variable.
Installation
Add the gem to your Gemfile:
gem 'asciidoctor-ldl'
or install it directly:
$ gem install asciidoctor-ldl
Registering the extension
On the command line, require it when converting:
$ asciidoctor -r asciidoctor-ldl document.adoc
$ asciidoctor-pdf -r asciidoctor-ldl document.adoc
From Ruby, requiring the gem auto-registers it with the global registry:
require 'asciidoctor'
require 'asciidoctor-ldl'
Asciidoctor.convert_file 'document.adoc', safe: :safe
To register against a specific registry instead of globally:
registry = Asciidoctor::Extensions.create
Asciidoctor::Ldl.register(registry)
Usage
Inline block
Put LDL source in a block named ldl. Use a literal (….) delimiter so LDL’s
own punctuation is never interpreted as AsciiDoc:
[ldl]
....
O1.Name = "Trip"
I1.Name = "Overcurrent"
O1 = I1 AND NOT I2
....
Optional positional attributes are target (a stable output file name) and
format:
[ldl,trip-logic,svg]
....
O1 = I1 AND NOT I2
....
From a file
Use the ldl:: block macro to render an external .ldl file (path relative to
the document):
ldl::diagrams/trip-matrix.ldl[format=png,scale=2]
Attributes
Every option can be set on the individual block/macro, or document-wide using
the ldl- prefixed form (block/macro wins). Set document attributes in the
header or on the command line (-a ldl-format=png).
| Block attribute | Document attribute | Default | Description |
|---|---|---|---|
|
|
|
Output format: |
|
|
|
Scale multiplier. For |
|
|
|
Colour theme: |
|
|
|
Draw bare identifiers (e.g. |
|
|
|
Draw |
|
|
(hash) |
Output file basename (without extension). When omitted, a content-addressed
name ( |
|
|
|
Path to the Node executable. |
|
|
(auto) |
Directory from which to resolve the LDL npm package (and |
|
|
|
Set to |
Standard image attributes — alt, title, width, height, align,
float, id, link, role — pass straight through to the generated image.
|
Tip
|
Most diagram styling (inversion bubbles vs NOT gates, input bars,
compactness, stroke width, margins, …) is authored in the LDL source itself with
OPTION lines — see the LDL documentation. This extension only
surfaces the render-time view options above.
|
Language OPTION lines vs. attributes
The LDL source can carry its own OPTION lines; the extension honours them and
then applies the attribute-driven overrides (theme, show-ids,
show-labels) on top. For example:
[ldl,theme=dark]
....
OPTION INVERSION = BUBBLES
OPTION MARGIN = 16
O1 = I1 AND NOT I2
....
Output formats
Choosing a format:
| Format | When to use |
|---|---|
|
Default and recommended for |
|
When a raster image is required (e.g. a target that cannot embed SVG). Use
|
For asciidoctor-pdf, prefer svg: it keeps PDFs small and crisp. If you must
raster for PDF, png with scale=2 or higher gives good print quality at the
cost of size.
Styling with roles
Every generated image is tagged with roles so it can be targeted from CSS or an
asciidoctor-pdf theme without touching each block:
-
ldl— on every LDL diagram. -
ldl-svg/ldl-png— the output format. -
ldl-light/ldl-dark— the theme.
Any role you set on the block is preserved and appended.
In HTML the roles appear as classes on the image block, so you can style them with CSS:
.imageblock.ldl { margin-block: 1.5rem; }
.imageblock.ldl img { max-width: 100%; }
.imageblock.ldl-dark { background: #1b1b1f; padding: 0.5rem; border-radius: 6px; }
In asciidoctor-pdf, target the same roles from your theme (roles map to
role_<name> keys). For example, to centre and pad LDL diagrams:
role:
ldl:
align: center
ldl-dark:
background-color: '1B1B1F'
Because the roles are stable, a document-wide look needs no per-block markup.
Caching
Rendered files are content-addressed and cached: the source plus the resolved
options (format, scale, theme, label toggles, and the gem version) are hashed
into a small sidecar (<image>.ldlcache). On the next build, an unchanged
diagram is reused and Node is not invoked. Editing the diagram or changing an
option transparently regenerates it. Disable with -a ldl-cache=false.
Generated images are written to the usual Asciidoctor image output location —
imagesoutdir if set, otherwise imagesdir under the output directory — so
they interoperate with the rest of your image pipeline.
Error handling
If a diagram fails to parse or render, the extension emits a visible error block
(role ldl-error) with the message and logs a warning, rather than aborting the
whole conversion. This keeps a single broken diagram from failing a large book.
Example document
= Protection Scheme
:ldl-format: svg
:ldl-theme: light
:imagesdir: images
== Transformer trip
[ldl,xfmr-trip]
....
TRIP.Name = "Transformer trip"
DIFF.Name = "Differential (87T)"
OC.Name = "Overcurrent (50/51)"
BUCH.Name = "Buchholz (63)"
TRIP = DIFF OR OC OR BUCH
....
== SEL-style seal-in (from a file)
ldl::diagrams/seal-in.ldl[]
Convert to HTML and PDF:
$ asciidoctor -r asciidoctor-ldl protection-scheme.adoc
$ asciidoctor-pdf -r asciidoctor-ldl protection-scheme.adoc
Development
$ bundle install
$ (cd test/js && npm install) # LDL package + resvg for the test suite
$ bundle exec rake test # Ruby unit + end-to-end tests
$ bundle exec rake jstest # Node helper tests
The Ruby end-to-end tests convert real AsciiDoc through Asciidoctor and assert on
the generated files, roles and markup. They resolve the npm package from
test/js (or LDL_PACKAGE_DIR) and skip cleanly if Node or the package is
unavailable.
Releasing
Releases publish to RubyGems via Trusted
Publishing (OIDC) — no API key is stored in the repository. Push a version tag
(git tag v0.1.0 && git push origin v0.1.0) and the Release workflow builds,
tests and publishes the gem. Configure the trusted publisher once on RubyGems to
match .github/workflows/release.yml.
License
MIT © Daniel Mulholland. See LICENSE.