0.0
The project is in a healthy, maintained state
An Asciidoctor extension that renders protection-relay / logic diagrams written in the Logic Diagram Language (LDL) — the @openpowershift/logic-diagram-language npm package — into SVG or PNG at conversion time. SVG output is vector and embeds cleanly in asciidoctor-pdf; PNG is available via @resvg/resvg-js. Format, scale, theme and label visibility are controlled with AsciiDoc attributes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 5.0
~> 13.0

Runtime

>= 2.0, < 3.0
 Project Readme

asciidoctor-ldl

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-ldl on npm — see Use with Asciidoctor.js.

Both accept the same attributes and emit the same roles.

Table of Contents
  • How it works
  • Prerequisites
  • Installation
  • Registering the extension
  • Usage
    • Inline block
    • From a file
  • Use with Asciidoctor.js
    • Programmatic use (Node)
    • Command line
    • Browser
    • Asciidoctor VS Code
  • Attributes
    • Language OPTION lines vs. attributes
  • Output formats
    • Fonts (asciidoctor-pdf)
  • Styling with roles
  • Caching
  • Error handling
  • Example document
  • Development
  • Releasing
  • Contributing
  • License

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]

Use with Asciidoctor.js

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=png

The renderer (@openpowershift/logic-diagram-language) is a runtime dependency; @resvg/resvg-js is optional and only used for PNG.

Programmatic use (Node)

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.

Command line

$ npx asciidoctor -r @openpowershift/asciidoctor-ldl document.adoc

Browser

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 })

Asciidoctor VS Code

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-language
<workspace>/.asciidoctor/lib/ldl.js
module.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.

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

format

ldl-format

svg

Output format: svg or png.

scale

ldl-scale

1

Scale multiplier. For svg, sets explicit width/height from the diagram’s intrinsic size (the viewBox is preserved, so it still scales crisply). For png, multiplies the rasterised device pixels.

theme

ldl-theme

light

Colour theme: light or dark.

show-ids

ldl-show-ids

false

Draw bare identifiers (e.g. I1, AND#G1) instead of / in addition to labels.

show-labels

ldl-show-labels

true

Draw .Name/.Description labels.

font-family

ldl-font-family

(see below)

Font family for diagram text. Defaults to a cross-platform stack (DejaVu Sans, Bitstream Vera Sans, Liberation Sans, Arial, sans-serif). Set to sans-serif (or none) to keep the renderer’s plain generic family.

target

(hash)

Output file basename (without extension). When omitted, a content-addressed name (ldl-<hash>.<ext>) is used so identical diagrams share a file.

(n/a)

ldl-node

node

Path to the Node executable.

(n/a)

ldl-package-dir

(auto)

Directory from which to resolve the LDL npm package (and @resvg/resvg-js). Overrides auto-detection; the LDL_PACKAGE_DIR env var does the same.

(n/a)

ldl-cache

true

Set to false to always re-render, ignoring the cache.

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.

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

svg

Default and recommended for asciidoctor-pdf. Vector, so it is sharp at any zoom or print resolution, embeds via prawn-svg with no rasterisation, and compresses to a fraction of an equivalent PNG. Also ideal for HTML.

png

When a raster image is required (e.g. a target that cannot embed SVG). Use scale to control resolution — scale=2 renders at 2× device pixels. Requires @resvg/resvg-js.

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.

Fonts (asciidoctor-pdf)

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.ttf

HTML and PNG output already resolve the glyphs without any setup.

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

The 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 tests

The 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.

Releasing

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.

Contributing

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.

License

MIT © Daniel Mulholland. See LICENSE.