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)
....It ships in two forms that share a single rendering core, so a diagram and its options produce byte-identical output either way:
-
a Ruby gem,
asciidoctor-ldl(the default examples below), and -
an Asciidoctor.js extension,
@openpowershift/asciidoctor-ldlon npm — see Use with Asciidoctor.js.
Both accept the same attributes and emit the same roles.
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).
-
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.
Add the gem to your Gemfile:
gem 'asciidoctor-ldl'or install it directly:
$ gem install asciidoctor-ldlOn the command line, require it when converting:
$ asciidoctor -r asciidoctor-ldl document.adoc
$ asciidoctor-pdf -r asciidoctor-ldl document.adocFrom Ruby, requiring the gem auto-registers it with the global registry:
require 'asciidoctor'
require 'asciidoctor-ldl'
Asciidoctor.convert_file 'document.adoc', safe: :safeTo register against a specific registry instead of globally:
registry = Asciidoctor::Extensions.create
Asciidoctor::Ldl.register(registry)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
....The extension is also published to npm as @openpowershift/asciidoctor-ldl, for
Asciidoctor.js 4. It shares
the same rendering core as the gem, so the same diagram and options yield
byte-identical SVG and the same generated file names. It ships as a single
bundled file (Node ESM + CommonJS, plus a browser build) with TypeScript types.
$ npm install @openpowershift/asciidoctor-ldl @openpowershift/logic-diagram-language
$ npm install @resvg/resvg-js # only needed for format=pngThe renderer (@openpowershift/logic-diagram-language) is a runtime dependency;
@resvg/resvg-js is optional and only used for PNG.
Rendering is asynchronous, so convert returns a promise — always await it.
import { Extensions, convert } from '@asciidoctor/core'
import ldl from '@openpowershift/asciidoctor-ldl'
const registry = Extensions.create()
ldl.register(registry) // or: ldl.register(registry, { packageDir })
const html = await convert(source, {
extension_registry: registry,
safe: 'safe',
})Images are written under imagesdir (or imagesoutdir) exactly as the gem
does, with identical content-hash file names and digest-sidecar caching.
A browser build is exported at @openpowershift/asciidoctor-ldl/browser. It
renders SVG only (PNG needs a native rasteriser) and embeds the diagram
inline; your bundler or import map must provide
@openpowershift/logic-diagram-language.
import { Extensions, convert } from '@asciidoctor/core'
import ldl from '@openpowershift/asciidoctor-ldl/browser'
const registry = Extensions.create()
ldl.register(registry)
const html = await convert(source, { extension_registry: registry })The AsciiDoc
extension loads Asciidoctor.js extensions from .asciidoctor/lib/*/.js in the
workspace: it require`s each file and calls its `register(registry). Enable
Enable Asciidoctor.js extensions registration
(asciidoc.extensions.registerWorkspaceExtensions) and trust the workspace when
prompted. Diagrams render as inline SVG in the preview (PNG is not available
there).
Option A — zero install (recommended). Download
asciidoctor-ldl-standalone.js from the
latest release and
save it as <workspace>/.asciidoctor/lib/ldl.js. It is a single self-contained
file with the renderer bundled in — nothing to npm install, which is ideal
when you cannot add packages. Reload the window; [ldl] blocks and ldl::
macros render.
Option B — via npm. Install into the workspace and point a loader file at the bundled standalone entry:
$ npm install @openpowershift/asciidoctor-ldl @openpowershift/logic-diagram-languagemodule.exports = require('@openpowershift/asciidoctor-ldl/standalone')Use the /standalone subpath: it renders synchronously and inline, so it works
regardless of which Asciidoctor.js version the editor bundles. (The default
entry uses asynchronous rendering, which older bundled engines may not await.)
|
Note
|
If your workspace package.json sets "type": "module", also drop a
.asciidoctor/lib/package.json containing { "type": "commonjs" } so the .js
loader file is treated as CommonJS.
|
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 |
|
|
(see below) |
Font family for diagram text. Defaults to a cross-platform stack
( |
|
|
(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, pdfwidth,
scaledwidth, align, float, id, link, role — pass straight through to
the generated image. To control the rendered size, use width for HTML and
pdfwidth (or scaledwidth) for asciidoctor-pdf, e.g.:
ldl::diagrams/trip.ldl[pdfwidth=80%]Without a sizing attribute, a diagram renders at its intrinsic size and
asciidoctor-pdf scales an over-wide one down to the content width.
|
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.
|
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
....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.
Gate and comparator symbols use − (U+2212) and ≥ (U+2265). In
asciidoctor-pdf, prawn-svg maps the generic sans-serif family to the
built-in Helvetica, which lacks those glyphs — so by default this extension
names real fonts first (see font-family above). For them to take effect in a
PDF, register a matching font in your theme and it will be used automatically:
# in your -theme.yml (font files under a directory on :pdf-fontsdir:)
font:
catalog:
merge: true
DejaVu Sans:
normal: DejaVuSans.ttf
bold: DejaVuSans-Bold.ttf
italic: DejaVuSans-Oblique.ttf
bold_italic: DejaVuSans-BoldOblique.ttfHTML and PNG output already resolve the glyphs without any setup.
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.
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.
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.
= 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.adocThe repository holds both implementations, sharing one rendering core
(lib/asciidoctor/ldl/js/render-core.mjs):
-
the Ruby gem (
lib/,test/), and -
the Asciidoctor.js extension (
js/, TypeScript, bundled with esbuild).
# Ruby gem
$ 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
# Asciidoctor.js extension
$ (cd js && npm install)
$ (cd js && npm run build) # single bundled file per target → js/dist
$ (cd js && npm test) # extension + Ruby↔JS parity testsThe parity suite (js/test/parity.test.mjs) renders identical LDL through the
Ruby CLI and the JS extension and asserts the generated files are byte-identical;
it self-skips when the Ruby toolchain is absent.
Gem and npm package are versioned in lockstep and released together by
release-please: Conventional-Commit
pushes to main maintain a release PR; merging it tags vX.Y.Z, then the
Release workflow publishes the gem (RubyGems) and npm package — both via
Trusted Publishing (OIDC, no
stored secrets) — and attaches the gem, npm tarball and standalone build to the
GitHub Release.
Updating the bundled LDL renderer is automated with Dependabot. See RELEASING.adoc for the full process, trusted-publisher setup, and renderer-update steps.
Contributions are welcome — see CONTRIBUTING.adoc for the repository layout, how to run the Ruby and JavaScript test suites (including the Ruby↔JS parity checks), and the Conventional-Commit convention that drives releases.
MIT © Daniel Mulholland. See LICENSE.