Skip to content

Architecture

Markbridge is built around a Parse → AST → Render pipeline. Each phase has a single responsibility and doesn’t know about the others. The parse and AST stages are renderer-agnostic; Discourse-flavored Markdown is what the shipped renderer produces.

Three-phase pipeline: Input (BBCode / HTML / MediaWiki / XML) → AST (Document tree) → Discourse Markdown Three-phase pipeline: Input (BBCode / HTML / MediaWiki / XML) → AST (Document tree) → Discourse Markdown

A format-specific parser consumes the input and produces an AST::Document. There are four parsers today:

  • Parsers::BBCode::Parser — token scanner + handler registry (stateful handler API).
  • Parsers::HTML::Parser — Nokogiri fragment walker (stateless handler API).
  • Parsers::TextFormatter::Parser — Nokogiri XML walker for the s9e format.
  • Parsers::MediaWiki::Parser — line-based wikitext parser with no handler registry.

All four produce the same AST node types.

The AST is a tree of AST::Node instances. It’s renderer-agnostic: nothing in the tree knows about Markdown.

Node (base)
├── Text (leaf)
├── LineBreak, HorizontalRule (leaf)
└── Element (container, has children)
├── Document (root)
├── Inline: Bold, Italic, Underline, Strikethrough, Superscript, Subscript
├── Block: Quote, List, ListItem, Code, Spoiler, Heading, HorizontalRule
└── Content: Url, Image, Attachment, Color, Size, Align, Table, TableRow, TableCell

Adjacent Text nodes auto-merge on insert, which keeps the tree small. Element validates that its children are AST::Node instances.

Before the renderer runs, the AST goes through a normalization pass (parse → yield → normalize → render). It rewrites nesting Markdown can’t express — a link inside a link, a block inside bold — so the renderer’s tags stay simple string emitters. It’s on by default; see AST normalization.

Renderers::Discourse::Renderer walks the tree. For each node it looks up a Tag in the TagLibrary and calls tag.render(element, interface). The interface carries a RenderContext — an immutable parent chain that lets tags ask “am I inside a list?” or “what’s my depth?” without passing state around manually.

RenderContext is a linked parent chain: each nested level adds one small context object, and has_parent? / find_parent walk the chain (nesting depth is shallow in practice).

  • CompositeElement contains children forming a tree.
  • Strategy — BBCode uses pluggable closing strategies (Strict, Reordering).
  • RegistryHandlerRegistry for parsers, TagLibrary for the renderer.
  • Visitor — the renderer dispatches AST nodes to tag implementations.
  • Immutable contextRenderContext creates new instances instead of mutating.
  • Parsers don’t know about Markdown. You can add a new output format without touching them.
  • The renderer doesn’t know about BBCode or HTML. You can add a new input format without touching it.
  • Registries keep customization from forking the core. Add a handler, add a tag — no subclassing required.
  • The AST — node types and invariants
  • Parsers — how each parser works
  • Renderers — how tags and the rendering interface fit together
  • Performance — where the pipeline is tuned