Page customization extending: Source version: Markbridge 0.4.2. These docs are built from the repository and may include unreleased changes. Source page: https://markbridge.dev/customization/extending/ # Extending Markbridge > Add support for new tags, customize rendering, and swap behavior without patching core. Markbridge has two places to plug in: **handlers** teach a parser to recognize new tags, and **renderer tags** turn AST nodes into Markdown. Both live in registries, so you add your own on top of the defaults without forking the gem. ## Connect parsing and rendering ```plaintext custom BBCode tag → custom handler → custom AST node → custom renderer tag → Markdown ``` To support a new tag, register a handler that creates an AST node. Then choose how that node should render. You can register a renderer tag or inherit one from a built-in AST class. ## Adding a custom BBCode tag ### 1. Define the AST node ```ruby module Markbridge module AST class Callout < Element attr_reader :variant def initialize(variant: "info") super() @variant = variant end end end end ``` ### 2. Write the handler ```ruby module Markbridge module Parsers module BBCode module Handlers class CalloutHandler < BaseHandler def initialize @element_class = AST::Callout end attr_reader :element_class def on_open(token:, context:, registry:, tokens: nil) variant = token.attrs[:option] || "info" context.push(AST::Callout.new(variant:)) end end end end end end ``` ### 3. Register the handler ```ruby handlers = Markbridge::Parsers::BBCode::HandlerRegistry.build_from_default do |registry| registry.register("callout", Markbridge::Parsers::BBCode::Handlers::CalloutHandler.new) end ``` ### 4. Write the renderer tag The block form is the quickest path: ```ruby callout_tag = Markbridge::Renderers::Discourse::Tag.new do |element, interface| context = interface.with_parent(element) inner = interface.render_children(element, context:) if interface.html_mode? "" else "> [!#{element.variant.upcase}]\n> #{inner.gsub("\n", "\n> ")}\n" end end ``` ### 5. Build a renderer with the tag ```ruby renderer = Markbridge.discourse_renderer( tags: { Markbridge::AST::Callout => callout_tag }, ) ``` `tags:` merges on top of the default library, so every other AST class keeps its built-in rendering. See [Customizing the renderer](/customization/customizing-renderer/) for the full set of factory options. ### 6. Use it ```ruby result = Markbridge.bbcode_to_markdown( "[callout=warning]Check this setting.[/callout]", handlers:, renderer:, ) result.markdown # => "> [!WARNING]\n> Check this setting." ``` ## The rendering interface Custom tags receive `(element, interface)`. The interface exposes context-aware helpers: | Method | Purpose | | ------------------------------- | -------------------------------------------------------------------------------------------------------- | | `render_children(element)` | Render child nodes and concatenate their output | | `render_default(node)` | Render `node` with its stock Tag, bypassing your override — intercept only some nodes and defer the rest | | `with_parent(element)` | Return a new context that treats `element` as a parent | | `find_parent(klass)` | Walk up the ancestor chain for a specific AST class | | `has_parent?(klass)` | Boolean parent check | | `count_parents(klass)` | Depth of a specific ancestor type (useful for nested lists) | | `wrap_inline(content, markers)` | Wrap inline content, collapsing adjacent markers cleanly | | `block_context?(element)` | True if the current position is a block context | | `html_mode?` | True inside a CommonMark HTML block — the Tag must emit raw HTML or wrap output as a Markdown island | Use `find_parent` / `has_parent?` to render differently inside specific ancestors (e.g. a code span inside a table cell). A Tag must return a String — returning `nil` (or anything else) raises a `TypeError`. To handle only some nodes, defer the rest with `render_default(node)` instead of falling through to `nil`. ## Rendering inside HTML blocks Tables with uneven rows, multiline cells, or nested tables use HTML output. Inside these tables, `interface.html_mode?` is `true`. Each custom tag must return either: * An HTML equivalent, with user-controlled text and attributes escaped using `Markbridge::Renderers::Discourse::HtmlEscaper`. * Its Markdown wrapped with `Markbridge::Renderers::Discourse::HtmlBlock.island(markdown)`. This adds blank lines so CommonMark can parse the Markdown. It also adds paragraph spacing, so prefer HTML when a suitable element exists. The callout example above uses `