` | `AST::Size` |
| `[center]`, `[left]`, `[right]`, `[justify]` | `` | `AST::Align` |
### Lists
| Tags | Renders as | AST node |
| --------------------------- | ---------- | --------------- |
| `[list]`, `[ul]`, `[ulist]` | `- item` | `AST::List` |
| `[ol]`, `[olist]` | `1. item` | `AST::List` |
| `[*]`, `[li]`, `[.]` | List item | `AST::ListItem` |
### Tables
| Tags | Renders as | AST node |
| --------------------------------- | ---------- | ------------ |
| `[table]`, `[tr]`, `[td]`, `[th]` | GFM table | `AST::Table` |
### Self-closing
| Tags | Renders as | AST node |
| ------ | --------------- | --------------------- |
| `[br]` | Hard line break | `AST::LineBreak` |
| `[hr]` | `---` | `AST::HorizontalRule` |
A few tags take variants:
* **Code language** — `[code=ruby]` or `[code lang=ruby]`.
* **Ordered list** — `[list=1]` (same as `[ol]`).
* **Quote attribution** — `[quote="author, post:1, topic:2"]`.
* **Link forms** — `[url]` also accepts `[url=href]text[/url]` and `[url]href[/url]`; `[email=addr]text[/email]` and `[img=src]alt[/img]` follow the same shape.
* **Bare links** — a `[url]` whose text equals the href (or has no text) renders as the plain link, not `[href](href)`, so Discourse autolinks and oneboxes it.
For the exact registration list, see [`HandlerRegistry.default`](https://github.com/discourse/markbridge/blob/main/lib/markbridge/parsers/bbcode/handler_registry.rb).
## Using the parser directly
The `bbcode_to_markdown` convenience wraps parser + renderer. Calling them directly gives you the AST for inspection or custom rendering:
```ruby
parser = Markbridge::Parsers::BBCode::Parser.new
ast = parser.parse("[b]bold[/b] with [unknown]mystery[/unknown]")
parser.unknown_tags
# => {"unknown" => 2} # count of open + close tokens seen
renderer = Markbridge::Renderers::Discourse::Renderer.new
renderer.render(ast)
# => "**bold** with mystery"
```
For most callers, `parse_bbcode` is the better entry point — it returns a `Parse` object that already exposes `.unknown_tags` and `.diagnostics` without reaching for the parser instance.
## Closing strategies
BBCode inputs from real forums often have mismatched or out-of-order tags. Markbridge ships with two strategies:
* **Reordering** (default) — reconciles sequences of up to 5 mismatched closing tags, e.g. `[b][i]text[/b][/i]` → `Bold(Italic("text"))`.
* **Strict** — only auto-closes; won’t reorder. More predictable, more likely to reject input.
```ruby
require "markbridge/bbcode"
handlers =
Markbridge::Parsers::BBCode::HandlerRegistry.build_from_default do |registry|
reconciler = Markbridge::Parsers::BBCode::ClosingStrategies::TagReconciler.new(registry:)
registry.closing_strategy = Markbridge::Parsers::BBCode::ClosingStrategies::Strict.new(reconciler)
end
Markbridge.bbcode_to_markdown("[b][i]text[/b][/i]", handlers:)
```
## Graceful degradation
Unknown tags don’t raise. The wrapper is skipped; children are parsed normally:
```ruby
Markbridge.bbcode_to_markdown("[unknown]inner text[/unknown]")
# => "inner text"
```
Unknown tag counts are surfaced on `Conversion#unknown_tags` (and `Parse#unknown_tags`) — no need to drop to the parser instance.
## Limits
* **Max nesting depth**: 100. Exceeding raises `MaxDepthExceededError`.
* **Max auto-close depth**: 5 levels — also bounds how deep `Reordering` will look for a matching opener.
## Customizing
Register a custom handler to recognize a new tag, or replace the built-in one:
```ruby
handlers =
Markbridge::Parsers::BBCode::HandlerRegistry.build_from_default do |registry|
registry.register("callout", MyCalloutHandler.new)
end
Markbridge.bbcode_to_markdown(input, handlers:)
```
See [Extending Markbridge](/customization/extending/) for a full walkthrough, or [Wrapping a default handler](/customization/extending/#wrapping-a-default-handler) for `HandlerRegistry#overlay` — the cleanest way to delegate to the default for the cases your handler doesn’t need to change.