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.
Phase 1 — parse
Section titled “Phase 1 — parse”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.
Phase 2 — the AST
Section titled “Phase 2 — the AST”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, TableCellAdjacent Text nodes auto-merge on insert, which keeps the tree small. Element validates that its children are AST::Node instances.
Phase 3 — render
Section titled “Phase 3 — render”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).
Design patterns in use
Section titled “Design patterns in use”- Composite —
Elementcontains children forming a tree. - Strategy — BBCode uses pluggable closing strategies (Strict, Reordering).
- Registry —
HandlerRegistryfor parsers,TagLibraryfor the renderer. - Visitor — the renderer dispatches AST nodes to tag implementations.
- Immutable context —
RenderContextcreates new instances instead of mutating.
Why this shape
Section titled “Why this shape”- 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