Skip to main content

Software Guidebook: remark-kroki-a11y

This Software Guidebook follows the structure defined by Simon Brown in his book Software Architecture for Developers. It provides a comprehensive overview of the remark-kroki-a11y plugin architecture.

1. Context​

The remark-kroki-a11y plugin operates in the intersection of:

  • Static Site Generators (Docusaurus, mdBook, etc.) that use the unified/remark ecosystem
  • Diagram-as-Code tools (Kroki, PlantUML, Mermaid) that render diagrams from text
  • Web Accessibility requirements (WCAG, European Accessibility Act) that mandate alternatives for visual content

System Context: remark-kroki-a11y

PlantUML source for "System Context: remark-kroki-a11y"
@startuml

title System Context: remark-kroki-a11y Plugin

actor "Content Author" as author
actor "Reader" as reader

rectangle "remark-kroki-a11y\n(plugin)" as plugin

rectangle "Static Site Generator\n(Docusaurus, mdBook, Jekyll)" as ssg
rectangle "Kroki Service" as kroki
rectangle "Web Browser" as browser

author --> ssg : writes Markdown with diagrams
ssg --> plugin : processes via remark pipeline
plugin --> kroki : requests diagram images
ssg --> browser : generates HTML site
reader --> browser : views documentation
@enduml

Sequence diagram with 6 participants: Content Author, Reader, ssg, plugin, kroki and browser.

Interactions:

  • Content Author responds to ssg: 'writes Markdown with diagrams'
  • ssg responds to plugin: 'processes via remark pipeline'
  • plugin responds to kroki: 'requests diagram images'
  • ssg responds to browser: 'generates HTML site'
  • Reader responds to browser: 'views documentation'

For detailed context including stakeholders and external systems, see the README page.

2. Functional Overview​

The plugin provides the following core functionality:

Use Cases​

Use Cases: Core Actors

PlantUML source for "Use Cases: Core Actors"
@startuml
left to right direction
skinparam actorStyle awesome

actor "UML Content\nAuthor" as author
actor "Visually impaired\nprogrammer" as reader
actor "Beginner UML\ndiagram reader" as beginner
actor "CI/CD Pipeline" as ci

rectangle "remark-kroki-a11y Plugin" {
usecase "Generate accessible diagram\nwith natural language description" as UC_BASE
}

author --> UC_BASE : writes diagrams for
reader --> UC_BASE : benefits from
beginner --> UC_BASE : benefits from
ci --> UC_BASE : builds documentation with
@enduml

Sequence diagram with 5 participants: UML Content Author, Visually impaired programmer, Beginner UML diagram reader, CI/CD Pipeline and UC_BASE.

Interactions:

  • UML Content Author responds to UC_BASE: 'writes diagrams for'
  • Visually impaired programmer responds to UC_BASE: 'benefits from'
  • Beginner UML diagram reader responds to UC_BASE: 'benefits from'
  • CI/CD Pipeline responds to UC_BASE: 'builds documentation with'

Configuration Options​

OptionUse CaseDefault
showSourceDisplay source code tab for all diagramstrue
hideSourcePer-diagram flag to hide the source tab while keeping A11y outputfalse
hideA11yPer-diagram flag to hide the natural language descriptionfalse
localeSet language for generated descriptions ('en' or 'nl')'en'
a11yDescriptionOverridePer-diagram natural language description override-
Extended use cases: configuration options

Use Cases: Plugin Configuration Options

PlantUML source for "Use Cases: Plugin Configuration Options"
@startuml
left to right direction
skinparam actorStyle awesome

actor "UML Content\nAuthor" as author

rectangle "remark-kroki-a11y Plugin" {
usecase "Generate accessible diagram\nwith natural language description" as UC_BASE

usecase "Show/hide source code\n(showSource)" as UC_SOURCE
usecase "Hide source tab\nfor anti-copy scenarios (hideSource)" as UC_HIDE_SOURCE
usecase "Hide A11y description\nfor simplicity (hideA11y)" as UC_HIDE_A11Y
usecase "Explicitly choose language if\nscreenreader detection is wrong\n(locale: 'en' | 'nl')" as UC_LANG
usecase "Override A11y description if it\ninterferes with existing text\n(a11yDescriptionOverride)" as UC_OVERRIDE

' Force UC_BASE to appear at the top
UC_BASE -[hidden]down-> UC_SOURCE
UC_SOURCE -[hidden]down-> UC_HIDE_SOURCE
UC_HIDE_SOURCE -[hidden]down-> UC_HIDE_A11Y
UC_HIDE_A11Y -[hidden]down-> UC_LANG
UC_LANG -[hidden]down-> UC_OVERRIDE

UC_SOURCE .> UC_BASE : <<extend>>
UC_HIDE_SOURCE .> UC_BASE : <<extend>>
UC_HIDE_A11Y .> UC_BASE : <<extend>>
UC_LANG .> UC_BASE : <<extend>>
UC_OVERRIDE .> UC_BASE : <<extend>>
}

author --> UC_BASE : writes diagrams for
author --> UC_SOURCE : shows/hides source code
author --> UC_HIDE_SOURCE : hides source tab\nwhile keeping A11y tab
author --> UC_HIDE_A11Y : hides A11y description\nfor simplicity
author --> UC_LANG : explicitly chooses language
author --> UC_OVERRIDE : overrides generated description\nwhen it interferes with text

note right of UC_BASE
Base use case: Plugin parses
diagram source and generates
a natural language description
using deterministic parsers.
end note

note right of UC_OVERRIDE
Escape hatch when automatic
generation is not suitable.
See ADR-0011.
end note
@enduml

Sequence diagram with 7 participants: UML Content Author, UC_BASE, UC_SOURCE, UC_HIDE_SOURCE, UC_HIDE_A11Y, UC_LANG and UC_OVERRIDE.

Interactions:

  • UML Content Author responds to UC_BASE: 'writes diagrams for'
  • UML Content Author responds to UC_SOURCE: 'shows/hides source code'
  • UML Content Author responds to UC_HIDE_SOURCE: 'hides source tab\nwhile keeping A11y tab'
  • UML Content Author responds to UC_HIDE_A11Y: 'hides A11y description\nfor simplicity'
  • UML Content Author responds to UC_LANG: 'explicitly chooses language'
  • UML Content Author responds to UC_OVERRIDE: 'overrides generated description\nwhen it interferes with text'

3. Quality Attributes​

The key quality attributes for this plugin are:

Accessibility (Primary)​

  • All diagrams must have alternative text descriptions
  • Generated descriptions must be screen reader friendly
  • UI components must be keyboard navigable

Determinism​

  • Descriptions are generated by parsers, not AI (see ADR-0000)
  • Same input always produces same output
  • No external API calls for description generation

Extensibility​

  • New diagram types can be added via new parsers
  • Support for multiple diagram-as-code formats (PlantUML, Mermaid)

Performance​

  • No runtime API calls (build-time only)
  • Minimal impact on build performance

4. Constraints​

Technical Constraints​

  • Must work within the unified/remark ecosystem
  • Must be compatible with Docusaurus (primary target)
  • Browser support limited by native <details> element support

Organizational Constraints​

  • Plugin used in educational context
  • Student contributions welcome for new diagram parsers

External Constraints​

  • Depends on Kroki service for diagram rendering
  • PlantUML/Mermaid syntax defines what can be parsed

5. Principles​

Core Architectural Principles​

  1. Deterministic over AI-generated - Descriptions are generated by parsers that understand diagram syntax, not by LLMs (see ADR-0000)

  2. PlantUML as Internal Representation - PlantUML data structures serve as the canonical format, with adapters for other formats (see ADR-0006)

  3. Build-time processing - All work happens during static site generation, not at runtime

  4. Progressive enhancement - Diagrams work without the plugin; plugin adds accessibility layer

Development Principles​

  • BDD Testing - Features defined in Gherkin, tested with Cucumber
  • Single Source Documentation - README/CONTRIBUTING maintained in one place (see ADR-0007)
  • Eating our own dog food - This documentation site uses the plugin to render its diagrams

6. Software Architecture​

High-Level Architecture​

The plugin operates as a remark plugin in the unified.js processing pipeline:

Markdown → remark-kroki-a11y → remark-kroki-plugin → rehype → HTML
↓
Parse diagram source
Generate description
Add tabs/details HTML

Processing Flow​

Sequence Diagram: Build-time Flow

PlantUML source for "Sequence Diagram: Build-time Flow"
@startuml
hide footbox
title Build-time Processing Flow

participant "Static Site\nGenerator" as SSG
participant "remark-kroki-a11y\n(this plugin)" as Plugin
participant "Parser\n(class/state/sequence)" as Parser
participant "remark-kroki-plugin" as KrokiPlugin
participant "Kroki Service" as Kroki

SSG -> SSG: Start build
SSG -> SSG: Load Markdown page

loop for each diagram code block
SSG -> Plugin: Process code block

Plugin -> Plugin: Detect diagram type\n(class/state/sequence)
Plugin -> Parser: Parse diagram source
Parser --> Plugin: Parsed structure\n(classes, relations, etc.)

Plugin -> Plugin: Generate natural language\ndescription (NL or EN)
Plugin -> Plugin: Create tabs HTML:\n- Source code tab\n- Description tab

Plugin --> SSG: Modified AST with\ntabs and description

SSG -> KrokiPlugin: Process same code block
KrokiPlugin -> Kroki: Request SVG/PNG
Kroki --> KrokiPlugin: Diagram image
KrokiPlugin --> SSG: Image embedded in AST
end

SSG -> SSG: Generate HTML output
SSG -> SSG: Write to build folder

@enduml

Sequence diagram with 5 participants: Static Site Generator, remark-kroki-a11y (this plugin), Parser (class/state/sequence), remark-kroki-plugin and Kroki Service.

Interactions:

  • Static Site Generator calls Static Site Generator.Start build()
  • Static Site Generator calls Static Site Generator.Load Markdown page()
  • Static Site Generator calls remark-kroki-a11y (this plugin).Process code block()
  • remark-kroki-a11y (this plugin) calls remark-kroki-a11y (this plugin).Detect diagram type\n(class/state/sequence)
  • remark-kroki-a11y (this plugin) calls Parser (class/state/sequence).Parse diagram source()
  • Parser (class/state/sequence) responds to remark-kroki-a11y (this plugin): 'Parsed structure\n(classes, relations, etc.)'
  • remark-kroki-a11y (this plugin) calls remark-kroki-a11y (this plugin).Generate natural language\ndescription (NL or EN)
  • remark-kroki-a11y (this plugin) calls remark-kroki-a11y (this plugin).Create tabs HTML:\n- Source code tab\n- Description tab()
  • remark-kroki-a11y (this plugin) responds to Static Site Generator: 'Modified AST with\ntabs and description'
  • Static Site Generator calls remark-kroki-plugin.Process same code block()
  • remark-kroki-plugin calls Kroki Service.Request SVG/PNG()
  • Kroki Service responds to remark-kroki-plugin: 'Diagram image'
  • remark-kroki-plugin responds to Static Site Generator: 'Image embedded in AST'
  • Static Site Generator calls Static Site Generator.Generate HTML output()
  • Static Site Generator calls Static Site Generator.Write to build folder()

7. Code​

Source Code Structure​

src/
├── index.js # Main plugin entry point and parser registry (946 lines)
├── diagramTabs.js # Client-side tab switching
├── diagram-a11y.css # Styling for the tabs and accessibility UI
├── rehype-kroki-a11y-img.js # Rehype plugin linking Kroki <img> to A11y descriptions
├── mermaid-classdiagram-a11y.js # Native Mermaid class diagram A11y injection (479 lines)
├── TextToSpeechService.js # Browser Web Speech API wrapper for the "speak" button
├── parsers/ # One parser per diagram type, CommonJS (build-time, used by index.js)
│ ├── classDiagramParser.js # PlantUML and Mermaid class diagrams (1114 lines)
│ ├── stateDiagramParser.js # PlantUML state diagrams
│ ├── sequenceDiagramParser.js # PlantUML and Mermaid sequence diagrams
│ ├── activityDiagramParser.js # PlantUML activity diagrams (536 lines)
│ ├── c4DiagramParser.js # PlantUML C4 context/container/component diagrams
│ ├── pieDiagramParser.js # Mermaid pie charts
│ ├── domainStoryParser.js # PlantUML Domain Story diagrams
│ ├── classDiagramSimplifier.js # Simplifies class diagrams for the "dev mode" toggle
│ ├── unsupportedDiagramParser.js # Fallback descriptions for unsupported diagrams
│ └── *.cjs # Byte-identical duplicates of the parsers above,
│ # required by runtime/a11yRuntime.cjs (see below)
├── runtime/
│ ├── a11yRuntime.js # ESM entry used by unit tests (vitest)
│ └── a11yRuntime.cjs # CommonJS twin, exposed through index.js for tests
├── mermaid-a11y/ # Separate, self-contained sub-package (own package.json/README/ADRs)
│ ├── index.js # for native Mermaid class diagram A11y (no Kroki)
│ └── parsers/classDiagramParser.js
└── stories/ # Storybook stories for manual/visual testing
├── DiagramTabs.stories.js
└── Quizdown.stories.js
Known duplication

Every file in parsers/ exists twice: once as .js (CommonJS, loaded by index.js during the remark build step) and once as a byte-identical .cjs twin (loaded by runtime/a11yRuntime.cjs, which index.js also exposes for tests). The two copies are kept in sync by hand — there is no build step that generates one from the other. This is a direct violation of the "Try to be DRY" LIFT principle below and a good target for cleanup (see issue tracker for an open issue about this).

The LIFT principle​

Source code organization in this project is intended to follow LIFT, from John Papa's Angular Style Guide (see also Papa's article Do you LIFT? Sensible Style Guidelines):

  • Locate code at a glance
  • Identify code at a glance (name files after what they contain; one responsibility/component per file)
  • Flat structure as long as possible (avoid deep nesting)
  • Try to be DRY (Don't Repeat Yourself)

The current codebase only partially meets this bar:

LIFT principleCurrent state
Locate at a glanceMostly OK — parsers/ and mermaid-a11y/ group related files
Identify by nameMostly OK, but src/index.js (946 lines) and classDiagramParser.js (1114 lines) do far more than their name suggests
Flat structureOK — nesting is shallow (src/parsers/, src/runtime/, src/mermaid-a11y/parsers/)
Try to be DRYViolated — every parser is duplicated as .js/.cjs (see above), and mermaid-a11y/parsers/classDiagramParser.js overlaps with parsers/classDiagramParser.js

Parser Architecture​

Each parser:

  1. Receives raw diagram source code
  2. Parses it into a structured representation
  3. Generates natural language description in the requested locale

For implementation details, see src/index.js and src/parsers/. The parser registry in src/index.js is the authoritative list for currently wired diagram support.

8. Containers​

As a remark plugin, remark-kroki-a11y is an npm package that runs within the static site generator's build process. In a local development setup, the Docusaurus dev server runs on the host machine and communicates with a local Kroki service running in Docker containers.

Container Diagram: Local Development Setup

PlantUML source for "Container Diagram: Local Development Setup"
@startuml

title Container Diagram: Local Development Setup

actor "Content Author" as author

rectangle "Host Machine" {
rectangle "Docusaurus Dev Server\n(Node.js)" as docusaurus
rectangle "remark-kroki-a11y\n(npm package)" as plugin
rectangle "remark-kroki-plugin\n(npm package)" as krokiPlugin
}

rectangle "Docker" {
rectangle "Kroki\n(container)" as kroki
rectangle "kroki-mermaid\n(container)" as mermaid
}

rectangle "Web Browser" as browser

author --> docusaurus : writes Markdown files
docusaurus --> plugin : processes Markdown
docusaurus --> krokiPlugin : processes diagram code blocks
krokiPlugin --> kroki : requests diagram images
kroki --> mermaid : delegates Mermaid diagrams
docusaurus --> browser : serves HTML
@enduml

Sequence diagram with 7 participants: Content Author, docusaurus, plugin, krokiPlugin, kroki, mermaid and browser.

Interactions:

  • Content Author responds to docusaurus: 'writes Markdown files'
  • docusaurus responds to plugin: 'processes Markdown'
  • docusaurus responds to krokiPlugin: 'processes diagram code blocks'
  • krokiPlugin responds to kroki: 'requests diagram images'
  • kroki responds to mermaid: 'delegates Mermaid diagrams'
  • docusaurus responds to browser: 'serves HTML'

The plugin interacts with:

  • remark-kroki-plugin (runtime dependency) - Handles Kroki API calls for diagram rendering
  • rehype-raw (required) - Enables raw HTML in MDX output
  • Kroki service - Can be local (Docker) or remote (kroki.io)

9. Components​

Component Diagram: Current Implementation

PlantUML source for "Component Diagram: Current Implementation"
@startuml

title Component Diagram: remark-kroki-a11y (Current)

rectangle "remark-kroki-a11y Plugin" {
rectangle "index.js\n(main plugin entry)" as index
rectangle "classDiagramParser.js" as classParser
rectangle "stateDiagramParser.js" as stateParser
rectangle "sequenceDiagramParser.js" as seqParser
rectangle "activity/c4/pie/domainStory parsers" as extraParsers
rectangle "diagramTabs.js" as tabsGen
}

index --> classParser : delegates class diagrams
index --> stateParser : delegates state diagrams
index --> seqParser : delegates sequence diagrams
index --> extraParsers : delegates activity, C4, pie, and Domain Story diagrams
index --> tabsGen : uses for UI generation
@enduml
State diagram with 6 states and 5 transitions. States: 1. index 2. classParser 3. stateParser 4. seqParser 5. extraParsers 6. tabsGen Transitions: 1. index transitions to classParser when "delegates class diagrams" 2. index transitions to stateParser when "delegates state diagrams" 3. index transitions to seqParser when "delegates sequence diagrams" 4. index transitions to extraParsers when "delegates activity, C4, pie, and Domain Story diagrams" 5. index transitions to tabsGen when "uses for UI generation"

10. Deployment​

npm Package​

The plugin is published to npm:

npm install remark-kroki-a11y

GitHub Pages​

The documentation site is deployed automatically via GitHub Actions on push to main:

  1. Run tests
  2. Build Docusaurus site
  3. Deploy to GitHub Pages

See .github/workflows/ for pipeline configuration.

Local Development​

# From repository root
./start-docs.sh

# Or manually
cd test-docusaurus-site
npm install
npm run start:strict # Recommended: validates broken links

11. Views and Perspectives​

Diagram Type Support Matrix​

Diagram TypePlantUMLMermaidA11y Status
Class diagrams✅ Full⚠️ BetaPartial
State diagrams✅ Full❌Partial
Sequence diagrams⚠️ Beta⚠️ BetaPartial
Activity diagrams⚠️ Beta❌Partial
C4 diagrams⚠️ BetaN/APartial
Domain Story diagrams⚠️ BetaN/APartial
Pie chartsN/A⚠️ BetaPartial

Accessibility Compliance​

The plugin contributes to WCAG 2.1 compliance:

  • 1.1.1 Non-text Content (A) - Provides text alternatives for diagrams
  • 2.1.1 Keyboard (A) - Tab/details UI is keyboard accessible
  • 4.1.2 Name, Role, Value (A) - Proper ARIA attributes on diagram images

12. Decisions​

Architecture decisions are recorded as ADRs (Architecture Decision Records). This guidebook especially relies on the following decisions:

  • ADR-0000 explains why descriptions are generated deterministically instead of with live LLM calls.
  • ADR-0006 explains why PlantUML structures are used as the internal architectural representation.
  • ADR-0007 explains the documentation sync approach between repository sources and the published Docusaurus site.
  • ADR-0011 explains why authored source details must remain faithfully represented, including when authors use a11yDescriptionOverride.
  • ADR-0012 explains the integration boundary with the upstream Kroki remark plugin.
  • ADR-0013 explains the locale strategy for generated accessibility descriptions.

For the complete ADR catalog, see the source ADR index on GitHub and the published ADR index.


This Software Guidebook follows the structure from Simon Brown's book "Software Architecture for Developers".