diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index d1216a4d..ab69dafc 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -24,3 +24,17 @@ updates:
gems:
patterns:
- "*"
+
+ - package-ecosystem: npm
+ directory: "/docs"
+ schedule:
+ interval: "weekly"
+ commit-message:
+ prefix: "DEPS"
+ versioning-strategy: increase
+ allow:
+ - dependency-type: "all"
+ groups:
+ docs:
+ patterns:
+ - "*"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000..72e60480
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,52 @@
+name: Deploy docs to Pages
+
+on:
+ push:
+ branches: [main]
+ paths:
+ - 'docs/**'
+ - '.github/workflows/docs.yml'
+ pull_request:
+ paths:
+ - 'docs/**'
+ - '.github/workflows/docs.yml'
+ release:
+ types: [published, edited, deleted]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Per-ref so a PR build never queues behind (or blocks) a main deploy.
+# Cancel superseded PR builds; never cancel an in-flight deploy.
+concurrency:
+ group: pages-${{ github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+ - uses: withastro/action@v6
+ with:
+ path: ./docs
+ # Astro requires Node 22.12 or newer.
+ node-version: 22
+ # pnpm version comes from the "packageManager" field in
+ # docs/package.json — passing it here too makes pnpm/action-setup
+ # error with "Multiple versions of pnpm specified".
+
+ deploy:
+ needs: build
+ # PRs validate the build only; deploy happens on push to main / release.
+ if: github.event_name != 'pull_request'
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - id: deployment
+ uses: actions/deploy-pages@v5
diff --git a/.rubocop.yml b/.rubocop.yml
index 5538df44..f5d818d4 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -18,5 +18,6 @@ Lint/UnusedMethodArgument:
RSpec/DescribeClass:
Exclude:
+ - spec/docs/**/*
- spec/integration/**/*
- spec/system/**/*
diff --git a/.silo.yml b/.silo.yml
index 08fb601c..94364939 100644
--- a/.silo.yml
+++ b/.silo.yml
@@ -12,20 +12,27 @@ use:
default: "4.0"
setup:
+ - sudo dnf update -y
+ # Node + pnpm for the docs site (Astro/Starlight); both ship as Fedora packages.
+ - sudo dnf install -y nodejs pnpm
- bundle install
- - bundle exec lefthook install
+ - bundle exec lefthook install -f
+ - (cd docs && CI=true pnpm install)
sync:
- bundle install
+ - (cd docs && CI=true pnpm install)
update:
- sudo dnf update -y
- rv self update
-ports:
- - 4567:4567
-
daemons:
playground:
cmd: bin/playground
+ ports: ["4567"]
+ autostart: true
+ docs:
+ cmd: bin/docs
+ ports: ["4321"]
autostart: true
diff --git a/AGENTS.md b/AGENTS.md
index 62849978..3164c1da 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -4,7 +4,7 @@ Quick reference guide for AI assistants working on the Markbridge codebase.
## Project Overview
-**Markbridge** converts BBCode to Discourse-flavored Markdown using a **Parse → AST → Render** pipeline.
+**Markbridge** converts BBCode, HTML, MediaWiki, and s9e TextFormatter XML to Markdown using a **Parse → AST → Render** pipeline. The shipped renderer produces Discourse-flavored Markdown. The parsers and AST are renderer-agnostic.
**Design Philosophy:**
- Graceful degradation (unknown tags preserved, no exceptions)
@@ -155,7 +155,7 @@ check ships as a shared RSpec example (`require "markbridge/rspec"`,
then `it_behaves_like "an html_mode safe tag"`), so consumer projects
can run it against their own tags.
-**See `examples/` for complete examples.**
+**See `docs/src/content/docs/customization/extending.md` for complete examples.**
## Development Workflow
@@ -217,7 +217,7 @@ automatically when mutation work comes up.
bundle exec ruby --yjit bench/bench.rb --isolated`). It pins the process
to the fastest cores and reports power and governor state. Results from
a laptop on battery, or from an efficiency core, don't compare with
-the numbers in `docs/benchmarks.md`.
+the numbers in `docs/src/content/docs/concepts/benchmarks.md`.
**MarkdownEscaper** is a hot path. Benchmark before/after any change to
`lib/markbridge/renderers/discourse/markdown_escaper.rb` with the
@@ -299,17 +299,17 @@ refactors when behavior is equivalent.
- **This file**: Quick reference and architecture
- **README.md**: User-facing quick start
-- **docs/architecture.md**: System architecture and design patterns
-- **docs/parsers/**: BBCode, HTML, and TextFormatter parser guides
-- **docs/renderers/**: Discourse renderer guide
-- **docs/extending.md**: How to add custom tags and handlers
-- **docs/performance.md**: Performance optimization guide
-- **examples/**: Runnable code examples
+- **docs/src/content/docs/concepts/architecture.md**: System architecture and design patterns
+- **docs/src/content/docs/format-guides/**: BBCode, HTML, and TextFormatter parser guides
+- **docs/src/content/docs/concepts/renderers.md**: Discourse renderer guide
+- **docs/src/content/docs/customization/extending.md**: How to add custom tags and handlers
+- **docs/src/content/docs/concepts/performance.md**: Performance optimization guide
+- **spec/docs/**: Checks for runnable documentation examples and AST coverage
- **spec/**: Executable documentation (tests show expected behavior)
---
-**Maintenance**: This file should be updated when core architecture changes. Details that change frequently (file counts, specific line numbers, step-by-step tutorials) are intentionally excluded. Point to examples/ and spec/ for those.
+**Maintenance**: This file should be updated when core architecture changes. Details that change frequently (file counts, specific line numbers, step-by-step tutorials) are intentionally excluded. Point to the docs site and spec/ for those.
**Last Updated**: 2025-11-26
**Version**: 0.1.0
diff --git a/Gemfile b/Gemfile
index 2d95443d..81b96eb7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,7 +4,9 @@ source "https://rubygems.org"
gemspec
+gem "benchmark"
gem "benchmark-ips"
+gem "csv"
gem "commonmarker", install_if: -> { RUBY_ENGINE == "ruby" }
gem "lefthook"
gem "nokogiri"
diff --git a/Gemfile.lock b/Gemfile.lock
index a8badf0a..3cb544c4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -14,6 +14,7 @@ GEM
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
base64 (0.3.0)
+ benchmark (0.5.0)
benchmark-ips (2.15.1)
bigdecimal (4.1.3)
bigdecimal (4.1.3-java)
@@ -31,6 +32,7 @@ GEM
commonmarker (2.10.0-x86_64-linux)
commonmarker (2.10.0-x86_64-linux-musl)
concurrent-ruby (1.3.8)
+ csv (3.3.5)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
diff-lcs (1.6.2)
@@ -277,8 +279,10 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
+ benchmark
benchmark-ips
commonmarker
+ csv
lefthook
markbridge!
mutant
@@ -300,6 +304,7 @@ CHECKSUMS
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
axiom-types (0.1.1) sha256=c1ff113f3de516fa195b2db7e0a9a95fd1b08475a502ff660d04507a09980383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
benchmark-ips (2.15.1) sha256=07a1a9f3c6105ecaf68c174fc3fbcddd71a0e9ada6236ae03093a0dcfd812d59
bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66
bigdecimal (4.1.3-java) sha256=9a6a1fa67723a27ab1b3a6d5526322d8c82170af03199da7c915b8d9a1770532
@@ -315,6 +320,7 @@ CHECKSUMS
commonmarker (2.10.0-x86_64-linux) sha256=945c510c0bfca9022245928e2248468fe3ddad58c939be0c37b20082cc392880
commonmarker (2.10.0-x86_64-linux-musl) sha256=288cd4fb9f17eed2ffa73551ef241dcf594e0f484b2aee2a0999540333c0618e
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
+ csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
descendants_tracker (0.0.4) sha256=e9c41dd4cfbb85829a9301ea7e7c48c2a03b26f09319db230e6479ccdc780897
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
dry-configurable (1.4.0) sha256=e35d1b5f3c081753ef361f564919db79000f32cfa6f20ee3a3ba5921b41b73ce
diff --git a/README.md b/README.md
index 6b643c1a..1c03f771 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,16 @@
# Markbridge
-Markbridge converts BBCode into Discourse-flavored Markdown through a clean parse → AST → render pipeline. It is intended for forum migrations and any workflow that needs predictable BBCode handling.
+Markbridge converts BBCode, HTML, MediaWiki wikitext, and s9e/TextFormatter XML into Discourse-flavored Markdown through a clean parse → AST → render pipeline. It's built for forum migrations into Discourse, but works for any job that needs predictable, repeatable conversion.
-## How it works
+Full documentation lives at **[markbridge.dev](https://markbridge.dev)**.
-1. **Parse BBCode** – `Markbridge::Parsers::BBCode::Parser` tokenizes input and builds an `AST::Document`, reconciling nesting and collecting raw content where needed.
-2. **Transform AST** – The AST captures semantic nodes such as text, formatting elements, lists, URLs, and code blocks that are renderer-agnostic.
-3. **Render to Markdown** – `Markbridge::Renderers::Discourse::Renderer` walks the tree with a tag library to emit Discourse-compatible Markdown, then normalizes spacing for final output.
+## How it works
-Refer to the component guides for more detail:
+Every conversion runs the same three steps:
-* [BBCode parser](docs/parsers/bbcode.md)
-* [Discourse renderer](docs/renderers/discourse.md)
+1. **Parse** – a format-specific parser turns the input into an `AST::Document`. Unknown tags are counted, not raised — the parser keeps going.
+2. **AST** – a renderer-agnostic tree of text, formatting, lists, links, and so on. The same tree comes out no matter which format went in.
+3. **Render** – `Markbridge::Renderers::Discourse::Renderer` walks the tree and emits Discourse-compatible Markdown.
## Installation
@@ -33,33 +32,29 @@ gem install markbridge
require "markbridge/bbcode"
bbcode = "[b]Hello[/b] [url=https://example.com]world[/url]!"
-markdown = Markbridge.bbcode_to_markdown(bbcode)
+result = Markbridge.bbcode_to_markdown(bbcode)
-puts markdown
+puts result.markdown
# => "**Hello** [world](https://example.com)!"
```
-## Configuration
+Swap `bbcode` for `html`, `mediawiki`, or `textformatter` for the other formats, or `require "markbridge/all"` to load everything.
-```ruby
-Markbridge.configure do |config|
- # Strip trailing spaces before newlines to prevent hard line breaks (
).
- # Defaults to false (Discourse has this disabled by default).
- config.escape_hard_line_breaks = true
-end
-```
+`*_to_markdown` returns a `Markbridge::Conversion`, not a plain string. The rendered Markdown is on `.markdown` (and `.to_s` delegates to it, so `puts result` works). The same object also carries `.unknown_tags`, `.diagnostics`, and `.errors` — handy when you're migrating a forum and want to know what showed up.
+
+## Customizing output
-Configuration applies to all `*_to_markdown` convenience methods (`bbcode_to_markdown`, `html_to_markdown`, etc.).
+Build a renderer once with `Markbridge.discourse_renderer(...)` and pass it via `renderer:` — custom tags, a custom escaper, dropping tags, and more. See [Customizing the renderer](https://markbridge.dev/customization/customizing-renderer/), and [Migrating to Discourse](https://markbridge.dev/migrating/overview/) for the full forum-migration workflow.
## Learn more
-* See `examples/` for runnable scripts such as `examples/basic_usage.rb`.
-* Browse integration and unit coverage under `spec/` to understand supported tags and edge cases.
-* Use `bin/console` during development for interactive exploration.
+* [markbridge.dev](https://markbridge.dev) – guides, format references, and the architecture deep-dive.
+* `spec/` – executable documentation of every supported tag and edge case.
+* `bin/console` – an interactive prompt for poking at things during development.
## Development
-This repository is set up to run inside [silo](https://github.com/gschlager/silo), a lightweight dev-environment tool. The `.silo.yml` file provisions a Fedora container with JRuby and multiple CRuby versions (via [rv](https://rv.dev)), installs dependencies, and starts the playground daemon. If you prefer your own setup, `bin/setup` and `bundle install` are all you need.
+This repository is set up to run inside [silo](https://github.com/gschlager/silo), a development environment tool. The `.silo.yml` file provisions a Fedora container with CRuby, JRuby, TruffleRuby, Node.js, and pnpm. It installs dependencies and starts the playground on port 4567 and the docs site on port 4321. For a Ruby setup outside Silo, use `bin/setup` and `bundle install`. See [docs/README.md](docs/README.md) to run the documentation site.
## Playground
diff --git a/UPGRADING.md b/UPGRADING.md
index 265aad10..6bb3ba12 100644
--- a/UPGRADING.md
+++ b/UPGRADING.md
@@ -1,688 +1,5 @@
# Upgrading Markbridge
-## 0.4.2 — aligned blocks and nested list indentation
+Read the [upgrade guide](https://markbridge.dev/reference/upgrading/) for API and output changes between releases.
-### Aligned blocks are Markdown islands
-
-`[center]…[/center]` and friends render a `
`, so an -aligned block picks up a paragraph margin. Inside the HTML fallback of -a table the tight form stays, because a blank line would end the -surrounding block. - -### Nested list items are indented relative to their parent item - -A list item used to compute its indentation from the number of -ancestor lists, and the item that held it indented every continuation -line once more on top of that. The two mechanisms added up, so -continuation lines drifted two columns per level, and a code fence -three levels deep ended up far enough right that CommonMark read it as -an indented code block — the fence characters showed up as text. - -An item no longer indents itself. It renders its content at column -zero, and the item that holds it moves the whole content right by the -width of its own marker: - -```ruby -Markbridge.bbcode_to_markdown("[list][*]a[list][*]b[br]more[/list][/list]").markdown -# 0.4.1: "- a\n - b\n more" -# 0.4.2: "- a\n - b\n more" -``` - -Continuation lines under an ordered marker now get 3 spaces instead of -2, which is the content column CommonMark expects for `1. `. If you -assert on exact Markdown anywhere, those expectations need updating. - -### `HtmlBlockSafety` is now `HtmlBlock` - -Everything about CommonMark HTML blocks lives in one module. `safe?` -is unchanged, two methods came along: - -```ruby -HtmlBlock = Markbridge::Renderers::Discourse::HtmlBlock -HtmlBlock.safe?(output) # as before -HtmlBlock.opens?("
Body:{}
========
`, s9e `CODE`, and MediaWiki indented and ``
-blocks. Single-line samples from these sources render as a fenced
-block and keep their language:
-
-```ruby
-# Before # After
-Markbridge.bbcode_to_markdown("[code=ruby]x[/code]").markdown
-# => "`x`" # => "```ruby\nx\n```"
-```
-
-Inline constructs (`[tt]`, ``, ``) keep the old behavior:
-inline when single-line, fence when the content has a newline.
-Consumers building ASTs directly can pass
-`AST::Code.new(language: "ruby", block: true)` to force a fence.
-
-### Other output changes
-
-- Empty ``, details, and spoiler elements render to nothing
- instead of `` `` ``, `[details]…[/details]`, and
- `[spoiler][/spoiler]` shells.
-- The HTML parser converts ``–`` to `AST::Heading` (they were
- unknown tags before, keeping only their text).
-- The HTML code language comes from `language-*` classes (on the
- element or its `` child), then the `lang` attribute, then a
- lone class; styling classes like `hljs` no longer end up on the
- fence line, and an explicit `lang` now beats a lone class.
-
-### New, non-breaking
-
-`require "markbridge/rspec"` ships the shared example
-`"an html_mode safe tag"` to test custom renderer tags against the
-html_mode contract — see "Testing Custom Tags Against the html_mode
-Contract" in [docs/extending.md](docs/extending.md).
-
-## 0.3.1 — AST normalization runs by default
-
-A new `Markbridge::Normalizer` pass runs between the parse-time `yield` hook
-and rendering. It rewrites the AST so the renderer only gets markup the target
-format can express. It is **on by default** for every `*_to_markdown` call,
-`convert`, and `render`.
-
-What changes in the output, with no code change on your side:
-
-- A link inside a link (`[url][url]…[/url][/url]`) collapses to a single link.
- CommonMark does not allow nested links.
-- A block element inside an inline container — a quote, list, table, or a
- `Poll`/`Event` node inside a link, bold, or a heading — is moved out, so the
- inline element does not break. This is not link-specific.
-- A fenced or multi-line code block inside an inline container is moved out; a
- one-line code span stays.
-- A formatting wrapper left empty by the above is removed (no empty `**` `**`).
- An empty link is kept, because it renders as a plain URL.
-
-Each change is reported under `conversion.diagnostics[:normalization]`, next
-to `unknown_tags`.
-
-The default rules are legality only. Discourse policy is not built in. A
-linked image (`[](url)`) is valid CommonMark, so the default leaves
-it alone. To move image-likes out of links, add the rules yourself. This is
-the pattern that replaces hoisting logic a consumer used to have in a custom
-`Url` tag:
-
-```ruby
-NORMALIZER =
- Markbridge::Normalizer
- .default
- .rule(parent: Markbridge::AST::Url, child: Markbridge::AST::Image, strategy: :hoist_after)
- .rule(parent: Markbridge::AST::Url, child: Markbridge::AST::Upload, strategy: :hoist_after)
- .rule(parent: Markbridge::AST::Url, child: Markbridge::AST::Attachment, strategy: :hoist_after)
- .freeze
-
-Markbridge.convert(input, format: :bbcode, normalize: NORMALIZER)
-```
-
-Build the normalizer once (a constant) and reuse it. `#normalize` keeps no
-state on the instance, so one frozen instance is safe for every conversion,
-also across threads, and passing it is as fast as the default path.
-
-To skip normalization:
-
-```ruby
-Markbridge.convert(input, format: :bbcode, normalize: false)
-```
-
-See [docs/normalization.md](docs/normalization.md).
-
-## 0.3.0 — quote attribution fields and URL rendering
-
-### `AST::Quote` attribution fields renamed and typed
-
-`post` and `topic` are gone. The fields now say what they hold, and
-all numbers/ids are `Integer` (previously `String`):
-
-```ruby
-# Before
-quote.post # => "123" (documented as "post ID", actually a
-quote.topic # => "456" post *number* for Discourse quotes)
-
-# After
-quote.post_number # => 123 position within the topic (Discourse)
-quote.topic_id # => 456
-quote.post_id # => 9001 database id (phpBB/XenForo-style sources)
-quote.user_id # => 12 new — id-based user attribution
-```
-
-The TextFormatter parser no longer funnels phpBB's `post_id` into the
-rendered Discourse attribution — a database id in a `post:N` reference
-links the wrong post. Id-attributed quotes now render name-only
-(`[quote="alice"]`) and carry `post_id`/`user_id` on the AST for you
-to remap (typically in the block yielded between parse and render).
-
-### Bare and relative URLs render differently
-
-- A bare URL (link text equal to the href, or no text) renders as the
- plain href instead of `[url](url)`, so Discourse can autolink and
- onebox it. `AST::Url#bare?` exposes the same judgment for consumers.
-- Relative hrefs (`/t/5`, `#anchor`, wiki page names) are kept as
- links instead of being silently dropped; unknown schemes
- (`javascript:` etc.) are still removed. Destinations containing
- whitespace use the `<...>` CommonMark form.
-- A text-less link no longer renders as `[](url)`.
-
-### Custom tags must return a String
-
-A tag returning `nil` (or anything else) now raises a descriptive
-`TypeError` immediately instead of failing later inside string
-concatenation. To intercept only some nodes and keep the stock
-rendering for the rest, use the new fall-through:
-
-```ruby
-Tag.new do |node, interface|
- next interface.render_default(node) unless node.username&.start_with?("legacy_")
- # custom rendering...
-end
-```
-
-## 0.2.0 — migration-API redesign
-
-This release reshapes the top-level API around `Conversion`/`Parse`
-result types and a single `renderer:` kwarg for render-side
-customization. There is no backwards-compatibility shim — the changes
-are mechanical but every importer call site needs to be updated.
-
-### Convenience methods now return a `Conversion`, not a `String`
-
-```ruby
-# Before
-markdown = Markbridge.bbcode_to_markdown(input)
-markdown.gsub(/.../, "...") # String operation
-
-# After
-result = Markbridge.bbcode_to_markdown(input)
-result.markdown.gsub(/.../, "...") # explicit access
-
-# Or, if you only need the string for puts/interpolation:
-puts result # to_s delegates to markdown
-"got #{result}" # works
-```
-
-`Conversion` carries `markdown`, `ast`, `format`, `unknown_tags`,
-`diagnostics`, `errors`. It does *not* delegate other String methods —
-`result.gsub(...)` will raise `NoMethodError`. Use
-`result.markdown.gsub(...)`.
-
-### Singleton config and per-process default registries are gone
-
-The following are removed:
-
-- `Markbridge.configuration`
-- `Markbridge.configure { |c| c.escape_hard_line_breaks = ... }`
-- `Markbridge.reset_defaults!`
-- `Markbridge.default_handlers`
-- `Markbridge.default_html_handlers`
-- `Markbridge.default_text_formatter_handlers`
-- `Markbridge.default_tag_library`
-- `Markbridge::Configuration` (the class)
-
-To customize rendering, build a `Renderer` once via the new factory
-and pass it through `renderer:`:
-
-```ruby
-# Before
-Markbridge.configure { |c| c.escape_hard_line_breaks = true }
-Markbridge.default_tag_library.register(MyAst::Bold, MyTag.new)
-Markbridge.bbcode_to_markdown(input)
-
-# After
-RENDERER =
- Markbridge.discourse_renderer(
- tags: { MyAst::Bold => MyTag.new },
- escape_hard_line_breaks: true,
- )
-Markbridge.bbcode_to_markdown(input, renderer: RENDERER)
-```
-
-Build the renderer once outside your migration loop and reuse it
-across thousands of posts.
-
-### `tags:`, `tag_library:`, `escaper:`, `escape_hard_line_breaks:` removed from per-call signature
-
-All four moved into `Markbridge.discourse_renderer(...)`. The four
-`*_to_markdown` methods plus `Markbridge.convert` now accept only:
-
-- `handlers:` — parser handler registry
-- `renderer:` — pre-built Renderer
-- `raise_on_error:` — boolean (default `true`)
-
-### MediaWiki kwarg renamed: `inline_tag_registry:` → `handlers:`
-
-```ruby
-# Before
-Markbridge.parse_mediawiki(input, inline_tag_registry: my_registry)
-Markbridge::Parsers::MediaWiki::Parser.new(inline_tag_registry: my_registry)
-
-# After
-Markbridge.parse_mediawiki(input, handlers: my_registry)
-Markbridge::Parsers::MediaWiki::Parser.new(handlers: my_registry)
-```
-
-The accepted *type* is unchanged — still an `InlineTagRegistry`. Only
-the parameter name moves, for parity with the BBCode/HTML/TextFormatter
-parsers.
-
-### TextFormatter handlers must accept `processor:`
-
-`Parsers::TextFormatter::Handlers::BaseHandler#process` now has a
-three-arg signature:
-
-```ruby
-# Before
-def process(element:, parent:)
-
-# After
-def process(element:, parent:, processor: nil)
-```
-
-Update every custom subclass under your importer's TextFormatter
-handler tree. The `processor:` argument is the parser instance and
-exposes `process_children(xml_element, ast_node)` for handlers that
-want to recurse into children manually.
-
-### Proc/lambda handlers no longer supported
-
-Both HTML and TextFormatter previously accepted a `Proc`/lambda as a
-handler. They now accept only objects responding to `#process(...)`.
-Existing default handlers were already class-based; the only places
-this affected built-in code were `
`/`
` lambdas (now
-`HTML::Handlers::SelfClosingHandler`) and the
-`examples/custom_text_formatter_mappings.rb` lambdas (now Handler
-classes).
-
-Migration: define a tiny class extending the parser's `BaseHandler`
-and move your lambda body into `#process(element:, parent:[, processor:])`.
-
-```ruby
-# Before
-registry.register("HIGHLIGHT", ->(element:, parent:, processor:) {
- parent << HighlightNode.new(...)
- nil
-})
-
-# After
-class HighlightHandler < Markbridge::Parsers::TextFormatter::Handlers::BaseHandler
- def initialize; @element_class = HighlightNode; end
- attr_reader :element_class
-
- def process(element:, parent:, processor:)
- parent << HighlightNode.new(...)
- nil
- end
-end
-registry.register("HIGHLIGHT", HighlightHandler.new)
-```
-
-The `BBCode` parser has always required class handlers (its
-`on_open`/`on_close` lifecycle doesn't fit the lambda shape). All
-three parsers now follow the same rule.
-
-### Resolution lives in handlers, not Tags
-
-The migration use case resolves placeholders (uploads, mentions,
-internal links) at parse time via custom handler subclasses. The
-handler stores the source-side reference in the converter's
-upload/user/topic store, gets back a stable identifier, and pins
-it on the AST node directly. Renderer Tags remain trivial output
-formatting — no per-post state, no side-channel.
-
-```ruby
-# Custom AST node carrying the resolved id
-class AttachmentPlaceholder < Markbridge::AST::Node
- attr_reader :upload_id
- def initialize(upload_id:); super(); @upload_id = upload_id; end
-end
-
-# Handler: resolves at parse, pins id on the node
-class AttachmentHandler < Markbridge::Parsers::BBCode::Handlers::BaseHandler
- def initialize(uploads:); @uploads = uploads; end
- def on_open(token:, context:, registry:, tokens: nil)
- upload = @uploads.store_or_lookup(token.attrs[:option])
- context.add_child(AttachmentPlaceholder.new(upload_id: upload.id))
- end
- def element_class; AttachmentPlaceholder; end
-end
-
-# Tag: trivial output formatter, no state
-class AttachmentTag < Markbridge::Renderers::Discourse::Tag
- def render(element, _interface) = "[upload|#{element.upload_id}]"
-end
-
-RENDERER = Markbridge.discourse_renderer(
- tags: { AttachmentPlaceholder => AttachmentTag.new },
-)
-```
-
-`interface.emit` and `Conversion#emissions` (intermediate API in
-earlier drafts of this redesign) are not part of the shipped API.
-Resolution-aware base handlers belong in the converter framework
-that wraps Markbridge; per-format converters (phpBB, vBulletin,
-SMF, IPB attachment handlers) subclass them.
-
-### `RawHandler` no longer requires `language:` on the AST class
-
-`Markbridge::Parsers::BBCode::Handlers::RawHandler` used to call
-`@element_class.new(language:)` unconditionally. Custom AST classes
-reused with `RawHandler` had to declare a `language:` kwarg even when
-unused. Now the handler introspects the AST class once and only passes
-`language:` when the class accepts it. No code action needed unless
-you'd previously added a dummy `def initialize(language: nil); super(); end`
-just to satisfy the handler — you can remove it.
-
-### Selective Markdown escaping (`allow:`)
-
-Importers that want list markers (or other block-level constructs)
-to survive escaping no longer need to subclass `MarkdownEscaper`:
-
-```ruby
-# Before
-class ListPermissiveEscaper < Markbridge::Renderers::Discourse::MarkdownEscaper
- private
- def escape_block_level(content, prev_was_paragraph)
- case content.getbyte(0)
- when 0x2D, 0x2A, 0x2B then return content, false if content.match?(/\A[-*+]\s/)
- when 0x30..0x39 then return content, false if content.match?(/\A\d+[.)]\s/)
- end
- super
- end
-end
-RENDERER = Markbridge.discourse_renderer(escaper: ListPermissiveEscaper.new)
-
-# After
-RENDERER = Markbridge.discourse_renderer(allow: :lists)
-```
-
-Recognised keys: `:bullet_list`, `:ordered_list`, `:atx_heading`,
-`:block_quote`. Aliases: `:lists` → `[:bullet_list, :ordered_list]`.
-Unknown keys raise `ArgumentError`. Thematic breaks (`---`, `***`)
-and setext underlines (`===`) are still escaped — the kwarg
-allow-lists specific block markers, not whole sections of the
-escaper.
-
-### Disabling Markdown escaping wholesale
-
-For migration paths where the source content is already trusted
-Markdown:
-
-```ruby
-NO_ESCAPE = Markbridge.discourse_renderer(escape: false)
-Markbridge.bbcode_to_markdown(input, renderer: NO_ESCAPE)
-```
-
-Internally this swaps in `Markbridge::Renderers::Discourse::IdentityEscaper`
-(a tiny `#escape(text) → text || ""` class). `escape: false` is
-mutually exclusive with `escape_hard_line_breaks:` / `allow:` —
-those configure `MarkdownEscaper`, which `escape: false` replaces
-wholesale. An explicit `escaper:` always wins over either.
-
-For *per-AST-node* opt-out, `AST::MarkdownText` already exists and
-bypasses the escaper for that node only.
-
-### Modifying the AST between parse and render
-
-Two new shapes let you mutate the parsed AST before rendering, e.g.
-to append attachments that weren't in the source post:
-
-```ruby
-# Block form on every *_to_markdown / convert method
-Markbridge.bbcode_to_markdown(input, renderer: RENDERER) do |ast|
- attachments.each { |a| ast << OrphanAttachment.new(source_id: a.id) }
-end
-
-# Or pass a Parse explicitly to .render
-parse = Markbridge.parse_bbcode(input)
-parse.ast << OrphanAttachment.new(source_id: 7)
-result = Markbridge.render(parse, renderer: RENDERER, raise_on_error: false)
-# result.unknown_tags / .diagnostics / .format are preserved from the Parse.
-```
-
-`Markbridge.render` accepts either a `Parse` (preferred — preserves
-`unknown_tags`/`diagnostics`/source `format`) or a bare AST node
-(fields default to empty, `format` is `nil` since there was no source
-document; a non-`Document` node is wrapped in an `AST::Document` so
-`Conversion#ast` is always one). Mutations made between parse and
-render persist in `Conversion#ast`. The wrapped `Parse` is reachable
-via `Conversion#parsed` for direct re-render.
-
-### Per-row failure isolation
-
-For migration loops, set `raise_on_error: false` to surface render
-exceptions on `Conversion#errors` instead of crashing the loop:
-
-```ruby
-posts.each do |post|
- result = Markbridge.bbcode_to_markdown(post.body, renderer: RENDERER, raise_on_error: false)
- if result.errors.any?
- log_failure(post, result.errors)
- else
- write_markdown(post, result.markdown)
- end
-end
-```
-
-The default is still `raise_on_error: true`, preserving the prior
-behavior of letting exceptions propagate.
-
-### HTML / TextFormatter parsers accept pre-parsed Nokogiri input
-
-Both Nokogiri-backed parsers now take either a String *or* a
-pre-parsed `Nokogiri::XML::Node`. Importers that already run their
-own DOM preprocessing pass (signature detection, reply-trailer
-stripping, structural classification) can hand the live fragment to
-Markbridge with no serialize → re-parse round-trip in between:
-
-```ruby
-# Before — two Nokogiri parses per post
-processed = MyImporter::SignatureWrapper.wrap(html) # parse + mutate + to_html
-result = Markbridge.html_to_markdown(processed) # parse again
-
-# After — one Nokogiri parse per post
-fragment = Nokogiri::HTML.fragment(html)
-MyImporter::SignatureWrapper.wrap_dom!(fragment) # in-place mutation
-result = Markbridge.html_to_markdown(fragment)
-```
-
-Same affordance on TextFormatter:
-
-```ruby
-xml_doc = Nokogiri.XML(input)
-# … inspect / mutate xml_doc …
-result = Markbridge.text_formatter_xml_to_markdown(xml_doc)
-```
-
-Input shapes are handled differently by parser:
-
-- **HTML parser.** A `Nokogiri::HTML::Document` (from
- `Nokogiri::HTML.parse`) is unwrapped to its `` children, so
- the synthesized ``/``/`` wrappers don't surface
- in `Conversion#unknown_tags`. A `Nokogiri::HTML::DocumentFragment`
- (from `Nokogiri::HTML.fragment`) and bare elements iterate their
- own children — the natural shape for in-place DOM mutation.
-- **TextFormatter parser.** A `Nokogiri::XML::Document` is unwrapped
- via `#root` (the single ``/`` element of the s9e/TextFormatter
- XML schema). Any other node is treated as the root directly.
-
-String callers are unchanged — the `.to_s` fallback covers `Pathname`,
-`IO`, and any other object that historically went through `.to_s`
-coercion.
-
-The HTML round-trip avoidance also fixes a documented side effect:
-re-serialization percent-encodes non-ASCII bytes in URL attributes.
-
-### AST traversal helpers on `Element`
-
-Three new methods replace the recursive-descent boilerplate every
-consumer was rolling on its own:
-
-```ruby
-# Walk every descendant in depth-first pre-order.
-result.ast.each_descendant { |node| ... }
-
-# Filter by class (uses is_a? — abstract bases match subclasses).
-mentions = result.ast.descendants(MyAst::Mention)
-
-# Swap a direct child in place, preserving index.
-result.ast.replace_child(old_paragraph, new_details_block)
-```
-
-`each_descendant` snapshots the children array of each Element at
-iteration entry, so mid-walk `replace_child` is safe — descent uses
-the pre-replacement reference. Appends to the same array during the
-walk are *not* re-visited (prevents unbounded recursion when a node
-appends another node).
-
-### `Markbridge::AST::Details` + `DetailsTag`
-
-The collapsible Discourse `[details=…]…[/details]` block is now a
-core AST node + auto-registered Tag. Importers can drop any local
-`DetailsBlock` / `DetailsBlockTag` shim:
-
-```ruby
-ast << Markbridge::AST::Details.new(title: "Signature").tap do |block|
- block << Markbridge::AST::Text.new("--\nAlex Doe")
-end
-# Markdown: \n\n[details="Signature"]\n--\nAlex Doe\n[/details]\n\n
-# html_mode: Signature
…
-```
-
-`title` is optional — omitting it produces a bare `[details]` (BBCode
-parser default) and `Summary ` in html_mode. The
-title is HTML-escaped in the `` text.
-
-### See also
-
-- `examples/forum_migration.rb` — canonical end-to-end importer shape
- exercising every new path: `discourse_renderer` factory, `tags:`,
- `unregister:`, `allow: :lists`, the AST-mutation block,
- `raise_on_error: false`, `Markbridge.convert(format:)` dispatch,
- pre-parsed Nokogiri input, AST traversal helpers, `AST::Details`.
-- `docs/extending.md` — how to add custom tags and handlers.
+The source is in [docs/src/content/docs/reference/upgrading.md](docs/src/content/docs/reference/upgrading.md).
diff --git a/bin/docs b/bin/docs
new file mode 100755
index 00000000..218ade7f
--- /dev/null
+++ b/bin/docs
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# Run the documentation site locally. Installs dependencies on first run.
+#
+# Usage:
+# bin/docs # pnpm run dev (default)
+# bin/docs build # pnpm run build
+# bin/docs preview # pnpm run preview
+# bin/docs # pnpm run
+set -euo pipefail
+
+cd "$(dirname "$0")/../docs"
+
+if [ ! -d node_modules ]; then
+ echo "Installing dependencies..."
+ pnpm install
+fi
+
+script="${1:-dev}"
+shift || true
+exec pnpm run "${script}" "$@"
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000..6566c400
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,24 @@
+# build output
+dist/
+# generated types
+.astro/
+
+# dependencies
+node_modules/
+
+# logs
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+
+# environment variables
+.env
+.env.production
+
+# macOS-specific files
+.DS_Store
+
+# Generated by docs/scripts/generate-changelog.mjs
+src/content/docs/changelog.md
diff --git a/docs/.vscode/extensions.json b/docs/.vscode/extensions.json
new file mode 100644
index 00000000..22a15055
--- /dev/null
+++ b/docs/.vscode/extensions.json
@@ -0,0 +1,4 @@
+{
+ "recommendations": ["astro-build.astro-vscode"],
+ "unwantedRecommendations": []
+}
diff --git a/docs/.vscode/launch.json b/docs/.vscode/launch.json
new file mode 100644
index 00000000..d6422097
--- /dev/null
+++ b/docs/.vscode/launch.json
@@ -0,0 +1,11 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "command": "./node_modules/.bin/astro dev",
+ "name": "Development server",
+ "request": "launch",
+ "type": "node-terminal"
+ }
+ ]
+}
diff --git a/docs/README.md b/docs/README.md
index 4ee7ab77..a8b4465f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,151 +1,18 @@
-# Markbridge Documentation
+# Documentation site
-Welcome to the Markbridge documentation. This guide will help you understand how Markbridge works and how to use it effectively.
+The site uses Astro and Starlight. Content lives in `src/content/docs/`, navigation in `astro.config.mjs`, and site styles in `src/styles/custom.css`.
-## What is Markbridge?
+Use Node.js 22.12 or newer and the pnpm version in `package.json`. From this directory:
-Markbridge is a Ruby gem that converts BBCode to Discourse-flavored Markdown using a clean **Parse → AST → Render** pipeline. It's designed for forum migrations requiring predictable BBCode handling with graceful degradation for unknown tags.
-
-## Documentation Guide
-
-### Getting Started
-
-- **[Quick Start](../README.md#quick-start)** - Get up and running quickly
-- **[Installation](../README.md#installation)** - How to install Markbridge
-- **[Examples](../examples/)** - Runnable code examples
-
-### Core Documentation
-
-- **[Architecture Overview](architecture.md)** - Understand the three-phase pipeline and design patterns
-- **[Extending Markbridge](extending.md)** - Add custom tags, handlers, and renderers
-- **[Performance Guide](performance.md)** - Optimization tips and best practices
-
-#### Parser Documentation
-
-- **[BBCode Parser Guide](parsers/bbcode.md)** - Deep dive into BBCode parsing, handlers, and closing strategies
-- **[HTML Parser Guide](parsers/html.md)** - Parse HTML content with Nokogiri
-- **[TextFormatter Parser Guide](parsers/text_formatter.md)** - Parse s9e/TextFormatter XML (phpBB 3.2+)
-- **[Parser Comparison](parsers/comparison.md)** - Compare BBCode, HTML, and TextFormatter parsers
-
-#### Renderer Documentation
-
-- **[Discourse Renderer Guide](renderers/discourse.md)** - Learn about rendering AST to Markdown with tags and context
-
-### For Developers
-
-- **[CLAUDE.md](../CLAUDE.md)** - Comprehensive guide for AI assistants and contributors
-- **[Changelog](../CHANGELOG.md)** - Version history and breaking changes
-- **[Contributing](../CONTRIBUTING.md)** - How to contribute to Markbridge
-
-## Quick Reference
-
-### Basic Usage
-
-```ruby
-require "markbridge/all"
-
-# Simple conversion
-markdown = Markbridge.bbcode_to_markdown("[b]Hello[/b] world!")
-# => "**Hello** world!"
-
-# Using the parser and renderer directly
-parser = Markbridge::Parsers::BBCode::Parser.new
-renderer = Markbridge::Renderers::Discourse::Renderer.new
-
-ast = parser.parse("[b]Hello[/b]")
-markdown = renderer.render(ast)
-```
-
-### Key Components
-
-| Component | Purpose | Location |
-|-----------|---------|----------|
-| **Parser** | Converts BBCode to AST | `Markbridge::Parsers::BBCode::Parser` |
-| **AST** | Abstract syntax tree | `Markbridge::AST::*` |
-| **Renderer** | Converts AST to Markdown | `Markbridge::Renderers::Discourse::Renderer` |
-| **Handlers** | Process BBCode tags during parsing | `Markbridge::Parsers::BBCode::Handlers::*` |
-| **Tags** | Render AST nodes to Markdown | `Markbridge::Renderers::Discourse::Tags::*` |
-
-### Supported BBCode Tags
-
-| BBCode | Markdown | AST Node |
-|--------|----------|----------|
-| `[b]...[/b]` | `**...**` | `AST::Bold` |
-| `[i]...[/i]` | `*...*` | `AST::Italic` |
-| `[s]...[/s]` | `~~...~~` | `AST::Strikethrough` |
-| `[u]...[/u]` | `[u]...[/u]` | `AST::Underline` |
-| `[code]...[/code]` | ` ```...``` ` | `AST::Code` |
-| `[tt]...[/tt]` | `` `...` `` | `AST::Code` |
-| `[url=...]...[/url]` | `[...](...)` | `AST::Url` |
-| `[list]...[/list]` | `- ...` or `1. ...` | `AST::List` |
-| `[*]...` | `- ...` | `AST::ListItem` |
-| `[br]` | `\n` | `AST::LineBreak` |
-| `[hr]` | `---` | `AST::HorizontalRule` |
-
-See [BBCode Parser Guide](parsers/bbcode.md#supported-tags) for the complete list with all tag aliases.
-
-## Design Philosophy
-
-Markbridge follows these core principles:
-
-- **Graceful degradation** - Unknown tags preserved as text, no parse exceptions
-- **Performance-conscious** - O(n) parsing with minimal allocations
-- **Extensible** - Handler and Tag registries for customization
-- **Clean separation** - Parsing logic separate from rendering logic via AST
-- **Test-driven** - Comprehensive unit, integration, and system tests
-
-## Architecture at a Glance
-
-```
-BBCode Input → Parser → AST → Renderer → Markdown Output
- ↓ ↓ ↓
- Scanner Nodes Tags
- Handlers Context
- Registry Library
-```
-
-See [Architecture Overview](architecture.md) for detailed information about each component.
-
-## Common Use Cases
-
-### Forum Migration
-
-```ruby
-# Convert forum posts from BBCode to Markdown
-posts.each do |post|
- markdown = Markbridge.bbcode_to_markdown(post.content)
- post.update!(content: markdown, format: :markdown)
-end
-```
-
-### Custom Tag Support
-
-```ruby
-# Add support for [quote] tags
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- registry.register("quote", QuoteHandler.new)
-end
-
-library = Markbridge::Renderers::Discourse::TagLibrary.new
-library.auto_register!
-library.register(AST::Quote, QuoteTag.new)
-
-renderer = Markbridge::Renderers::Discourse::Renderer.new(tag_library: library)
+```sh
+pnpm install --frozen-lockfile
+pnpm dev
```
-See [Extending Markbridge](extending.md) for complete examples.
-
-## Getting Help
+`pnpm build` creates the production site in `dist/`. `pnpm preview` serves that build locally. Both development and production builds generate the changelog from GitHub Releases. If GitHub is unavailable, the generator keeps an existing page or writes a link to the releases.
-- **Issues** - Report bugs at [GitHub Issues](https://github.com/gschlager/markbridge/issues)
-- **Discussions** - Ask questions in [GitHub Discussions](https://github.com/gschlager/markbridge/discussions)
-- **Examples** - See `examples/` directory for working code
-- **Tests** - Check `spec/` for detailed behavior examples
+If Markdown edits stop appearing in development, run `pnpm astro dev stop` and restart `pnpm dev`.
-## Next Steps
+The site shows a work-in-progress notice and includes a `noindex` robots meta tag on every page. Keep crawling allowed in `public/robots.txt` so search engines can read that tag. When the docs are ready for indexing, remove the robots meta entry and the `Banner` override from `astro.config.mjs`, delete `src/components/DocsBanner.astro`, and restore the sitemap URL in `public/robots.txt`.
-1. Read the [Architecture Overview](architecture.md) to understand how Markbridge works
-2. Explore the [BBCode Parser Guide](parsers/bbcode.md) to learn about BBCode parsing
-3. Review the [Discourse Renderer Guide](renderers/discourse.md) to understand Markdown generation
-4. Check out [Extending Markbridge](extending.md) to customize behavior
-5. Learn about [Performance Optimization](performance.md) for production use
+Run `bundle exec rspec spec/docs` from the repository root to check AST coverage and Ruby examples. Every `ruby` code block runs in a separate process. Use a `spec:before` comment for setup or `spec:continue` to include earlier examples. Use `rb` for API signatures and examples of removed APIs that cannot run.
diff --git a/docs/architecture.md b/docs/architecture.md
deleted file mode 100644
index f1c60300..00000000
--- a/docs/architecture.md
+++ /dev/null
@@ -1,546 +0,0 @@
-# Architecture Overview
-
-Markbridge uses a clean three-phase pipeline to convert BBCode to Markdown. This document explains the high-level architecture, design patterns, and how the components work together.
-
-## Table of Contents
-
-- [Three-Phase Pipeline](#three-phase-pipeline)
-- [Component Overview](#component-overview)
-- [Design Patterns](#design-patterns)
-- [Data Flow](#data-flow)
-- [Design Philosophy](#design-philosophy)
-
-## Three-Phase Pipeline
-
-Markbridge follows a **Parse → AST → Render** architecture:
-
-```
-┌─────────────┐ ┌─────────────┐ ┌─────────────┐
-│ BBCode │ → │ AST │ → │ Markdown │
-│ Input │ │ Tree │ │ Output │
-└─────────────┘ └─────────────┘ └─────────────┘
- │ │ │
- Parser Document Renderer
- Scanner Nodes Tags
- Handlers Elements Context
- Registry Library
-```
-
-Between the AST and Render, an optional `Markbridge::Normalizer` pass
-rewrites the tree so the renderer is only handed markup the target format
-can express (no link inside a link, no block inside an inline container,
-etc.). It runs by default at the conversion level; the three components
-above are unchanged by it. See [AST Normalization](normalization.md).
-
-### Phase 1: Parsing (BBCode → AST)
-
-**Purpose:** Convert BBCode text into a structured tree
-
-**Components:**
-- **Scanner** - Tokenizes input into `TextToken`, `TagStartToken`, `TagEndToken`
-- **Parser** - Orchestrates scanning and delegates to handlers
-- **Handlers** - Convert tokens to AST nodes based on tag type
-- **HandlerRegistry** - Maps tag names to handlers
-- **ParserState** - Manages parsing state (node stack, depth tracking)
-- **Closing Strategies** - Handle closing tag logic (Strict, Reordering)
-
-**Example:**
-```ruby
-"[b]Hello[/b]"
- → Scanner → [TagStart(b), Text("Hello"), TagEnd(b)]
- → Handlers → AST::Bold([AST::Text("Hello")])
-```
-
-### Phase 2: AST (Abstract Syntax Tree)
-
-**Purpose:** Represent document structure independent of input/output formats
-
-**Node Hierarchy:**
-```
-Node (empty base class)
-├── Text (leaf node containing text)
-└── Element (container with children)
- ├── Document (root element)
- ├── Formatting: Bold, Italic, Underline, Strikethrough
- ├── Complex: Code, List, ListItem, Url
- └── Self-closing: LineBreak, HorizontalRule
-```
-
-**Key Features:**
-- **Renderer-agnostic** - AST doesn't know about Markdown or any output format
-- **Text merging** - Adjacent `Text` nodes automatically merge for efficiency
-- **Type safety** - All children validated as `AST::Node` instances
-- **Immutable** - No public setters after construction
-
-### Phase 3: Rendering (AST → Markdown)
-
-**Purpose:** Convert AST tree into Discourse-flavored Markdown
-
-**Components:**
-- **Renderer** - Walks AST tree and generates output
-- **TagLibrary** - Maps AST node classes to Tag renderers
-- **Tags** - Render specific AST nodes to Markdown
-- **RenderingInterface** - Abstraction layer between tags and renderer
-- **RenderContext** - Tracks parent chain for context-aware rendering
-
-**Example:**
-```ruby
-AST::Bold([AST::Text("Hello")])
- → TagLibrary[AST::Bold] → BoldTag
- → interface.wrap_inline("Hello", "**")
- → "**Hello**"
-```
-
-## Component Overview
-
-### Parser Components
-
-#### Scanner (`Markbridge::Parsers::BBCode::Scanner`)
-
-**Responsibility:** Stream characters and produce tokens
-
-**Key Methods:**
-- `next_token` - Returns next token or nil at end of input
-- Character-by-character streaming with minimal allocations
-
-**Token Types:**
-- `TextToken` - Plain text content
-- `TagStartToken` - Opening tag like `[b]` with attributes
-- `TagEndToken` - Closing tag like `[/b]`
-
-#### Parser (`Markbridge::Parsers::BBCode::Parser`)
-
-**Responsibility:** Orchestrate parsing and build AST
-
-**Key Methods:**
-- `parse(input)` - Main entry point, returns `AST::Document`
-- `unknown_tags` - Hash of unrecognized tags encountered
-
-**Features:**
-- Normalizes line endings before parsing
-- Delegates token processing to handlers
-- Tracks unknown tags for debugging
-
-#### Handlers
-
-**Responsibility:** Convert specific tag types to AST nodes
-
-**Types:**
-- **SimpleHandler** - Formatting tags (bold, italic, etc.)
-- **RawHandler** - Code blocks that don't parse inner BBCode
-- **SelfClosingHandler** - Line breaks, horizontal rules
-- **UrlHandler** - Links with URL attributes
-- **ListHandler** - Ordered/unordered lists
-- **ListItemHandler** - List items with auto-closing
-
-**Base Interface:**
-```ruby
-class BaseHandler
- attr_reader :element_class
-
- def on_open(context:, token:, registry:)
- # Called when opening tag encountered
- end
-
- def on_close(token:, context:, registry:, tokens: nil)
- # Called when closing tag encountered
- end
-
- def auto_closeable?
- # Whether this tag can be auto-closed
- end
-end
-```
-
-#### HandlerRegistry
-
-**Responsibility:** Map tag names to handlers
-
-**Key Features:**
-- Tag name normalization and caching
-- Auto-closeable element tracking
-- Element class to handler mapping
-- Configurable closing strategy
-
-**Usage:**
-```ruby
-# Block-based configuration (recommended)
-parser = Parser.new do |registry|
- registry.register("quote", QuoteHandler.new)
-end
-
-# Or customize default registry
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("custom", CustomHandler.new)
-end
-```
-
-#### Closing Strategies
-
-**Responsibility:** Handle mismatched or out-of-order closing tags
-
-**Types:**
-- **Strict** - Auto-close only, no reordering
-- **Reordering** - Look-ahead to match closing sequences (default)
-
-**Key Limits:**
-- Max auto-close depth: 5 levels
-- Max peek-ahead: 5 tokens
-
-### AST Components
-
-#### Node (`Markbridge::AST::Node`)
-
-**Base class for all AST nodes** (empty marker class)
-
-#### Text (`Markbridge::AST::Text`)
-
-**Leaf node containing text content**
-
-```ruby
-text = AST::Text.new("Hello")
-text.text # => "Hello"
-text.append(" World") # Mutate existing text
-```
-
-#### Element (`Markbridge::AST::Element`)
-
-**Container node with children**
-
-**Key Features:**
-- Automatically merges adjacent `Text` nodes
-- Type validation for children
-- Immutable after construction
-
-```ruby
-element = AST::Bold.new
-element << AST::Text.new("Hello")
-element << AST::Text.new(" World")
-# Result: One Text node with "Hello World"
-```
-
-#### Document (`Markbridge::AST::Document`)
-
-**Root element of the AST tree**
-
-Always the top-level node returned by parser.
-
-#### Self-closing Nodes (`Markbridge::AST::LineBreak`, `Markbridge::AST::HorizontalRule`)
-
-**Leaf nodes without children**
-
-These inherit directly from `AST::Node` (not `Element`) and cannot accept children. They are typically produced by self-closing tags like `[br]` and `[hr]`.
-
-### Renderer Components
-
-#### Renderer (`Markbridge::Renderers::Discourse::Renderer`)
-
-**Responsibility:** Walk AST and generate Markdown
-
-**Key Methods:**
-- `render(node, context:)` - Main entry point
-- Dispatches to tags based on node class
-- Normalizes final output spacing
-
-**Types:**
-- **Renderer** - In-memory rendering (outputs complete string)
-
-#### RenderingInterface
-
-**Responsibility:** Decouple tags from renderer implementation
-
-**Provides to tags:**
-- `render_children(element, context:)` - Render child nodes
-- `with_parent(element)` - Create child context
-- `find_parent(klass)` - O(1) parent lookup via cache
-- `count_parents(klass)` - Count ancestors of type
-- `has_parent?(klass)` - Check for ancestor
-- `wrap_inline(content, markers)` - Smart marker wrapping
-- `block_context?(element)` - Check if block or inline
-
-**Benefits:**
-- Tags don't depend on specific renderer
-- Enables streaming rendering
-- Simplifies testing
-- Clear API contract
-
-#### TagLibrary
-
-**Responsibility:** Map AST node classes to Tag renderers
-
-**Key Features:**
-- Auto-registration by naming convention
-- Fallback to default tag for unknown nodes
-- Block-based tag registration
-
-**Auto-Registration:**
-```ruby
-library = TagLibrary.new
-library.auto_register!
-# Discovers: BoldTag → AST::Bold, ItalicTag → AST::Italic, etc.
-```
-
-#### Tags
-
-**Responsibility:** Render specific AST nodes
-
-**Interface:**
-```ruby
-class Tag
- def render(element, interface)
- # Returns Markdown string
- end
-end
-```
-
-**Built-in Tags:**
-- `BoldTag`, `ItalicTag`, `StrikethroughTag`, `UnderlineTag`
-- `CodeTag`, `ListTag`, `ListItemTag`
-- `UrlTag`, `HorizontalRuleTag`
-
-#### RenderContext
-
-**Responsibility:** Track parent chain for context-aware rendering
-
-**Key Features:**
-- **Immutable** - Creates new context instead of mutating
-- **Cached lookups** - O(1) parent finding via hash cache
-- **Parent tracking** - Maintains full ancestor chain
-
-**Usage:**
-```ruby
-# Check if nested in list
-if interface.has_parent?(AST::List)
- # Render differently
-end
-
-# Count nesting level
-depth = interface.count_parents(AST::List)
-indent = " " * depth
-```
-
-## Design Patterns
-
-### 1. Composite Pattern (AST)
-
-Elements contain children forming a tree structure.
-
-```ruby
-document = AST::Document.new
-bold = AST::Bold.new
-bold << AST::Text.new("Hello")
-document << bold
-```
-
-**Benefits:**
-- Uniform interface for all nodes
-- Easy tree traversal
-- Flexible nesting
-
-### 2. Strategy Pattern (Closing Strategies)
-
-Different algorithms for handling closing tags.
-
-```ruby
-# Strict strategy
-parser = Parser.new(closing_strategy: :strict)
-
-# Reordering strategy (default)
-parser = Parser.new(closing_strategy: :reordering)
-```
-
-**Benefits:**
-- Pluggable behavior
-- Easy to test separately
-- User can choose tradeoffs
-
-### 3. Registry Pattern (Extensibility)
-
-Registries map tags to handlers and AST nodes to renderers.
-
-```ruby
-# Parser: HandlerRegistry
-registry.register("quote", QuoteHandler.new)
-
-# Renderer: TagLibrary
-library.register(AST::Quote, QuoteTag.new)
-```
-
-**Benefits:**
-- Decouples tag definitions from core
-- Easy to add custom tags
-- No modification of core classes
-
-### 4. Visitor Pattern (Rendering)
-
-Renderer visits AST nodes, dispatching to appropriate tags.
-
-```ruby
-def render(node, context)
- case node
- when Element
- tag = tag_library[node.class]
- interface = RenderingInterface.new(self, context)
- tag.render(node, interface)
- when Text
- node.text
- end
-end
-```
-
-**Benefits:**
-- Separation of tree structure from operations
-- Easy to add new renderers
-- Single dispatch point
-
-### 5. Immutable Context Pattern
-
-RenderContext creates new instances instead of mutating.
-
-```ruby
-def with_parent(element)
- self.class.new(parents: [@parents, element].flatten)
-end
-```
-
-**Benefits:**
-- No side effects during rendering
-- Safe for concurrent rendering
-- Clear parent tracking
-
-### 6. Builder Pattern (List Items)
-
-ListItemFormatter builds formatted output incrementally.
-
-```ruby
-formatter = ListItemFormatter.new(content: "Item", depth: 0)
-formatter.with_marker("- ")
-formatter.with_trailing_newline
-formatted = formatter.build
-```
-
-**Benefits:**
-- Flexible construction
-- Clear intent
-- Easy to test
-
-## Data Flow
-
-### Complete Example
-
-**Input:** `[b]Hello [i]world[/i]![/b]`
-
-**Phase 1: Scanning**
-```
-Scanner produces:
- 1. TagStartToken(tag: "b")
- 2. TextToken(text: "Hello ")
- 3. TagStartToken(tag: "i")
- 4. TextToken(text: "world")
- 5. TagEndToken(tag: "i")
- 6. TextToken(text: "!")
- 7. TagEndToken(tag: "b")
-```
-
-**Phase 2: Parsing**
-```
-Handler operations:
- 1. SimpleHandler(Bold) → Push AST::Bold
- 2. Append AST::Text("Hello ")
- 3. SimpleHandler(Italic) → Push AST::Italic
- 4. Append AST::Text("world")
- 5. SimpleHandler(Italic) → Pop AST::Italic
- 6. Append AST::Text("!")
- 7. SimpleHandler(Bold) → Pop AST::Bold
-
-Result AST:
- Document
- └─ Bold
- ├─ Text("Hello ")
- ├─ Italic
- │ └─ Text("world")
- └─ Text("!")
-```
-
-**Phase 3: Rendering**
-```
-Rendering walk:
- 1. Renderer visits Document → render children
- 2. Visit Bold → BoldTag.render
- a. Create child context with Bold as parent
- b. Render children: "Hello " + "*world*" + "!"
- c. Wrap with **: "**Hello *world*!**"
- 3. Return final: "**Hello *world*!**"
-```
-
-### Error Handling Flow
-
-**Unknown Tag:** `[unknown]text[/unknown]`
-```
-1. Scanner: TagStartToken(tag: "unknown")
-2. Parser: No handler found → unknown_tags["unknown"] = 1
-3. Parser: Skip wrapper, continue parsing children
-4. TextToken("text") → Text("text")
-5. TagEndToken("unknown") → unknown_tags["unknown"] = 2
-6. Result: Text("text")
-```
-
-**Mismatched Tags:** `[b][i]text[/b][/i]`
-```
-1. Parse [b] → Push Bold
-2. Parse [i] → Push Italic
-3. Parse [/b] → Expected [/i], got [/b]
-4. Closing strategy:
- - Reordering: Look ahead, find [/i]
- - Match sequence: [italic, bold] == [italic, bold]
- - Consume both closers, pop both elements
-5. Result: Bold(Italic(Text("text")))
-```
-
-## Design Philosophy
-
-### Graceful Degradation
-
-Unknown tags don't crash parsing—they're ignored while processing their children:
-
-```ruby
-parser = Parser.new
-ast = parser.parse("[unknown]text[/unknown]")
-# Result: Text("text")
-# parser.unknown_tags => {"unknown" => 2}
-```
-
-### Performance-Conscious
-
-- **O(n) parsing** - Single pass through input
-- **Minimal allocations** - Reuse buffers where possible
-- **Bounded operations** - Depth limits prevent runaway behavior
-- **Smart caching** - Parent lookups cached in context
-
-### Extensible
-
-- **Registry pattern** - Add handlers without modifying core
-- **Tag library** - Custom renderers for any AST node
-- **Strategy pattern** - Pluggable closing behavior
-- **Clean interfaces** - Well-defined extension points
-
-### Clean Separation
-
-- **Parser doesn't know about Markdown** - Only builds AST
-- **Renderer doesn't know about BBCode** - Only walks AST
-- **AST is format-agnostic** - Can add HTML renderer, etc.
-- **Tags don't know about renderer** - Only use interface
-
-### Test-Driven
-
-- **Unit tests** - Individual classes in isolation
-- **Integration tests** - Component interactions
-- **System tests** - Full BBCode → Markdown flows
-- **Executable documentation** - Tests show expected behavior
-
-## Next Steps
-
-- **[BBCode Parser Guide](parsers/bbcode.md)** - Deep dive into parsing BBCode
-- **[Discourse Renderer Guide](renderers/discourse.md)** - Learn about rendering to Markdown
-- **[Extending Markbridge](extending.md)** - Add custom tags
-- **[Performance Guide](performance.md)** - Optimization tips
diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs
new file mode 100644
index 00000000..5b327e3a
--- /dev/null
+++ b/docs/astro.config.mjs
@@ -0,0 +1,133 @@
+// @ts-check
+import { defineConfig } from 'astro/config';
+import starlight from '@astrojs/starlight';
+
+export default defineConfig({
+ site: 'https://markbridge.dev',
+ vite: {
+ server: {
+ // Force fresh fetches in dev so updated SVGs / CSS / JS in public/
+ // appear without restarting the dev server. Without this, Vite's
+ // static-file middleware lets the browser cache `public/` assets
+ // aggressively and even hard-refresh keeps the stale copy.
+ headers: { 'Cache-Control': 'no-store' },
+ },
+ },
+ integrations: [
+ starlight({
+ title: 'Markbridge',
+ components: {
+ Banner: './src/components/DocsBanner.astro',
+ },
+ description:
+ 'Convert BBCode, HTML, MediaWiki, and s9e TextFormatter XML to Markdown in Ruby. Inspect the AST and customize your output.',
+ logo: {
+ src: './src/assets/markbridge-icon.svg',
+ alt: 'Markbridge',
+ replacesTitle: false,
+ },
+ favicon: '/favicon.svg',
+ head: [
+ {
+ tag: 'meta',
+ attrs: { name: 'robots', content: 'noindex' },
+ },
+ {
+ tag: 'link',
+ attrs: { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico', sizes: '32x32' },
+ },
+ {
+ tag: 'link',
+ attrs: { rel: 'apple-touch-icon', href: '/apple-touch-icon.png', sizes: '512x512' },
+ },
+ {
+ tag: 'meta',
+ attrs: { property: 'og:image', content: 'https://markbridge.dev/og-image.png' },
+ },
+ {
+ tag: 'meta',
+ attrs: { name: 'twitter:image', content: 'https://markbridge.dev/og-image.png' },
+ },
+ {
+ tag: 'meta',
+ attrs: { name: 'twitter:card', content: 'summary_large_image' },
+ },
+ {
+ tag: 'script',
+ attrs: { src: '/diagram-zoom.js', defer: true },
+ },
+ {
+ tag: 'script',
+ attrs: { src: '/table-scroll.js', defer: true },
+ },
+ ],
+ social: [
+ { icon: 'github', label: 'GitHub', href: 'https://github.com/discourse/markbridge' },
+ ],
+ customCss: ['./src/styles/custom.css'],
+ editLink: {
+ baseUrl: 'https://github.com/discourse/markbridge/edit/main/docs/',
+ },
+ lastUpdated: true,
+ expressiveCode: {
+ themes: ['github-dark', 'github-light'],
+ styleOverrides: { borderRadius: '0.5rem' },
+ },
+ sidebar: [
+ { label: 'Introduction', slug: 'introduction' },
+ { label: 'Get started', slug: 'getting-started' },
+ {
+ label: 'Format guides',
+ items: [
+ { label: 'Overview', slug: 'format-guides' },
+ { label: 'BBCode', slug: 'format-guides/bbcode' },
+ { label: 'HTML', slug: 'format-guides/html' },
+ { label: 'MediaWiki', slug: 'format-guides/mediawiki' },
+ { label: 'TextFormatter', slug: 'format-guides/textformatter' },
+ ],
+ },
+ {
+ label: 'Customization',
+ items: [
+ { label: 'Overview', slug: 'customization' },
+ { label: 'Customizing the renderer', slug: 'customization/customizing-renderer' },
+ { label: 'Extending Markbridge', slug: 'customization/extending' },
+ ],
+ },
+ {
+ label: 'Concepts',
+ items: [
+ { label: 'Overview', slug: 'concepts' },
+ { label: 'Architecture', slug: 'concepts/architecture' },
+ { label: 'The AST', slug: 'concepts/ast' },
+ { label: 'Result objects', slug: 'concepts/result-objects' },
+ { label: 'Parsers', slug: 'concepts/parsers' },
+ { label: 'Renderers', slug: 'concepts/renderers' },
+ { label: 'AST normalization', slug: 'concepts/normalization' },
+ { label: 'Performance', slug: 'concepts/performance' },
+ { label: 'Benchmark results', slug: 'concepts/benchmarks' },
+ ],
+ },
+ {
+ label: 'Migrating to Discourse',
+ items: [
+ { label: 'Overview', slug: 'migrating/overview' },
+ { label: 'Placeholders', slug: 'migrating/placeholders' },
+ ],
+ },
+ {
+ label: 'Reference',
+ items: [
+ { label: 'Upgrading', slug: 'reference/upgrading' },
+ {
+ label: 'API docs (rubydoc.info)',
+ link: 'https://rubydoc.info/gems/markbridge',
+ attrs: { target: '_blank', rel: 'noopener' },
+ },
+ { label: 'Changelog', slug: 'changelog' },
+ ],
+ },
+ ],
+ }),
+ ],
+});
diff --git a/docs/branding/favicon.ico b/docs/branding/favicon.ico
new file mode 100644
index 00000000..a1904afa
Binary files /dev/null and b/docs/branding/favicon.ico differ
diff --git a/docs/branding/markbridge-icon-128.png b/docs/branding/markbridge-icon-128.png
new file mode 100644
index 00000000..fef68fcf
Binary files /dev/null and b/docs/branding/markbridge-icon-128.png differ
diff --git a/docs/branding/markbridge-icon-16.png b/docs/branding/markbridge-icon-16.png
new file mode 100644
index 00000000..a41aad58
Binary files /dev/null and b/docs/branding/markbridge-icon-16.png differ
diff --git a/docs/branding/markbridge-icon-24.png b/docs/branding/markbridge-icon-24.png
new file mode 100644
index 00000000..28b0935f
Binary files /dev/null and b/docs/branding/markbridge-icon-24.png differ
diff --git a/docs/branding/markbridge-icon-256.png b/docs/branding/markbridge-icon-256.png
new file mode 100644
index 00000000..f21c64fe
Binary files /dev/null and b/docs/branding/markbridge-icon-256.png differ
diff --git a/docs/branding/markbridge-icon-32.png b/docs/branding/markbridge-icon-32.png
new file mode 100644
index 00000000..8ddb3c46
Binary files /dev/null and b/docs/branding/markbridge-icon-32.png differ
diff --git a/docs/branding/markbridge-icon-48.png b/docs/branding/markbridge-icon-48.png
new file mode 100644
index 00000000..655555c0
Binary files /dev/null and b/docs/branding/markbridge-icon-48.png differ
diff --git a/docs/branding/markbridge-icon-512.png b/docs/branding/markbridge-icon-512.png
new file mode 100644
index 00000000..cc1ef154
Binary files /dev/null and b/docs/branding/markbridge-icon-512.png differ
diff --git a/docs/branding/markbridge-icon-64.png b/docs/branding/markbridge-icon-64.png
new file mode 100644
index 00000000..f9c5bd0d
Binary files /dev/null and b/docs/branding/markbridge-icon-64.png differ
diff --git a/docs/branding/markbridge-icon.svg b/docs/branding/markbridge-icon.svg
new file mode 100644
index 00000000..d493d063
--- /dev/null
+++ b/docs/branding/markbridge-icon.svg
@@ -0,0 +1,6 @@
+
diff --git a/docs/branding/markbridge-social-card-no-tagline.png b/docs/branding/markbridge-social-card-no-tagline.png
new file mode 100644
index 00000000..0e1f20b5
Binary files /dev/null and b/docs/branding/markbridge-social-card-no-tagline.png differ
diff --git a/docs/branding/markbridge-social-card-no-tagline.svg b/docs/branding/markbridge-social-card-no-tagline.svg
new file mode 100644
index 00000000..82f6e522
--- /dev/null
+++ b/docs/branding/markbridge-social-card-no-tagline.svg
@@ -0,0 +1,28 @@
+
diff --git a/docs/branding/markbridge-social-card.png b/docs/branding/markbridge-social-card.png
new file mode 100644
index 00000000..eea799c9
Binary files /dev/null and b/docs/branding/markbridge-social-card.png differ
diff --git a/docs/branding/markbridge-social-card.svg b/docs/branding/markbridge-social-card.svg
new file mode 100644
index 00000000..6bb3dd38
--- /dev/null
+++ b/docs/branding/markbridge-social-card.svg
@@ -0,0 +1,32 @@
+
diff --git a/docs/extending.md b/docs/extending.md
deleted file mode 100644
index 859b6004..00000000
--- a/docs/extending.md
+++ /dev/null
@@ -1,1116 +0,0 @@
-# Extending Markbridge
-
-This guide shows you how to add custom BBCode tags and renderers to Markbridge. Whether you need to support forum-specific tags or create custom output formats, this guide covers all the extension points.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Adding a New BBCode Tag](#adding-a-new-bbcode-tag)
-- [Creating Custom Handlers](#creating-custom-handlers)
-- [Creating Custom Renderers](#creating-custom-renderers)
-- [Extending the Normalizer](#extending-the-normalizer)
-- [Plugin Pattern](#plugin-pattern)
-- [Advanced Examples](#advanced-examples)
-- [Best Practices](#best-practices)
-
-## Overview
-
-Markbridge provides three main extension points:
-
-1. **Parser Extension** - Add support for new BBCode tags
-2. **Renderer Extension** - Customize Markdown output
-3. **Both** - Add complete end-to-end support for custom tags
-
-**Extension Flow:**
-```
-1. Create AST node (e.g., AST::Quote)
-2. Create handler (e.g., QuoteHandler)
-3. Register handler in parser
-4. Create renderer tag (e.g., QuoteTag)
-5. Register tag in renderer
-```
-
-## Adding a New BBCode Tag
-
-Let's walk through adding support for `[quote]` tags step by step.
-
-### Step 1: Create AST Node
-
-**File:** `lib/markbridge/ast/quote.rb`
-
-```ruby
-# frozen_string_literal: true
-
-module Markbridge
- module AST
- class Quote < Element
- attr_reader :author
-
- def initialize(author: nil, children: [])
- @author = author
- super(children:)
- end
- end
- end
-end
-```
-
-**Key points:**
-- Extend `AST::Element` for container nodes
-- Use keyword arguments for attributes
-- Call `super(children:)` to initialize children array
-- Add `attr_reader` for custom attributes (e.g., author)
-
-### Step 2: Create Handler
-
-**File:** `lib/markbridge/parsers/bbcode/handlers/quote_handler.rb`
-
-```ruby
-# frozen_string_literal: true
-
-module Markbridge
- module Parsers
- module BBCode
- module Handlers
- class QuoteHandler < SimpleHandler
- def initialize
- super(AST::Quote, auto_closeable: false)
- end
-
- def create_element(token)
- # Get author from attribute or option
- author = token.attrs[:author] || token.attrs[:option]
- AST::Quote.new(author:)
- end
- end
- end
- end
- end
-end
-```
-
-**Key points:**
-- Extend `SimpleHandler` for basic tags
-- Pass element class to `super`
-- Set `auto_closeable: false` for block elements
-- Override `create_element` to handle attributes
-- Extract attributes from `token.attrs`
-
-### Step 3: Register Handler
-
-**Option A: Block-based configuration (recommended)**
-
-```ruby
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- registry.register("quote", QuoteHandler.new)
-end
-```
-
-**Option B: Build from default**
-
-```ruby
-registry = Markbridge::Parsers::BBCode::HandlerRegistry.build_from_default do |reg|
- reg.register("quote", QuoteHandler.new)
-end
-
-parser = Markbridge::Parsers::BBCode::Parser.new(handlers: registry)
-```
-
-**Option C: Add to default (for library maintainers)**
-
-Edit `lib/markbridge/parsers/bbcode/handler_registry.rb`:
-
-```ruby
-def self.default
- new.tap do |registry|
- # ... existing registrations ...
- registry.register("quote", Handlers::QuoteHandler.new)
- end
-end
-```
-
-### Step 4: Create Renderer Tag
-
-**File:** `lib/markbridge/renderers/discourse/tags/quote_tag.rb`
-
-```ruby
-# frozen_string_literal: true
-
-module Markbridge
- module Renderers
- module Discourse
- module Tags
- class QuoteTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- author = element.author ? " #{element.author}" : ""
-
- "[quote#{author}]\n#{content}\n[/quote]"
- end
- end
- end
- end
- end
-end
-```
-
-**Key points:**
-- Extend `Tag` base class
-- Accept `element` and `interface` parameters
-- Create child context with `interface.with_parent(element)`
-- Render children with `interface.render_children`
-- Return Markdown string
-
-### Step 5: Register Renderer Tag
-
-**Option A: Auto-registration (if following naming convention)**
-
-```ruby
-library = TagLibrary.new
-library.auto_register! # Finds QuoteTag → AST::Quote
-
-renderer = Renderer.new(tag_library: library)
-```
-
-**Option B: Manual registration**
-
-```ruby
-library = TagLibrary.default
-library.register(AST::Quote, QuoteTag.new)
-
-renderer = Renderer.new(tag_library: library)
-```
-
-**Option C: Add to default (for library maintainers)**
-
-Edit `lib/markbridge/renderers/discourse/tag_library.rb`:
-
-```ruby
-def self.default
- new.tap do |library|
- # ... existing registrations ...
- library.register(AST::Quote, Tags::QuoteTag.new)
- end
-end
-```
-
-### Auto-passthrough for unregistered AST classes
-
-A custom AST class that has *no* Tag bound to it — and no ancestor
-class with one — doesn't need a "passthrough" Tag: `Renderer#render`
-falls through to `render_children` automatically (see
-`lib/markbridge/renderers/discourse/renderer.rb`). You only need to
-register a Tag when the class needs a non-trivial rendering. Note that
-a subclass of a bound node class inherits that class's tag instead —
-see [Subclasses and Ancestry Matching](#subclasses-and-ancestry-matching)
-and `Tag::PASSTHROUGH` when you really want only the children. To
-remove a built-in binding so the passthrough kicks in, use
-`TagLibrary#unregister`:
-
-```ruby
-library.unregister(AST::Color) # Color now renders as just its children
-library.unregister(AST::Size) # Size too
-```
-
-Or, more concisely, via the `Markbridge.discourse_renderer` factory:
-
-```ruby
-Markbridge.discourse_renderer(unregister: [AST::Color, AST::Size])
-```
-
-### Step 6: Add Requires
-
-**File:** `lib/markbridge/ast.rb`
-
-```ruby
-require_relative "ast/quote"
-```
-
-**File:** `lib/markbridge/parsers/bbcode.rb`
-
-```ruby
-require_relative "bbcode/handlers/quote_handler"
-```
-
-**File:** `lib/markbridge/renderers/discourse.rb`
-
-```ruby
-require_relative "discourse/tags/quote_tag"
-```
-
-### Step 7: Test Your Tag
-
-```ruby
-require "markbridge/all"
-
-# Parse BBCode
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- registry.register("quote", QuoteHandler.new)
-end
-
-# Set up renderer
-library = Markbridge::Renderers::Discourse::TagLibrary.default
-library.register(AST::Quote, QuoteTag.new)
-renderer = Markbridge::Renderers::Discourse::Renderer.new(tag_library: library)
-
-# Test
-bbcode = "[quote author=John]Hello world[/quote]"
-ast = parser.parse(bbcode)
-markdown = renderer.render(ast)
-
-puts markdown
-# => "[quote John]\nHello world\n[/quote]"
-```
-
-## Creating Custom Handlers
-
-### Simple Formatting Handler
-
-For basic formatting tags with no attributes:
-
-```ruby
-class MyFormatHandler < SimpleHandler
- def initialize
- super(AST::MyFormat, auto_closeable: true)
- end
-end
-
-# Register
-registry.register(["myformat", "mf"], MyFormatHandler.new)
-```
-
-### Handler with Attributes
-
-For tags that need to extract attributes:
-
-```ruby
-class ColorHandler < SimpleHandler
- def initialize
- super(AST::Color, auto_closeable: true)
- end
-
- def create_element(token)
- # Extract color from attribute or option
- color = token.attrs[:color] || token.attrs[:option]
- AST::Color.new(color:)
- end
-end
-
-# Usage: [color=red]text[/color] or [color color=red]text[/color]
-```
-
-### Self-Closing Handler
-
-For tags that don't need closing:
-
-```ruby
-# Reuse built-in handler
-handler = SelfClosingHandler.new(AST::MyElement)
-registry.register("mytag", handler)
-
-# Or create custom
-class MyElementHandler < SelfClosingHandler
- def initialize
- super(AST::MyElement)
- end
-end
-```
-
-### Raw Content Handler
-
-For tags that capture unparsed content (like code blocks):
-
-```ruby
-class MyRawHandler < RawHandler
- def initialize
- super(AST::MyRaw)
- end
-
- def create_element(token, raw_content)
- # token: TagStartToken with attributes
- # raw_content: String of unparsed content
- lang = token.attrs[:lang] || token.attrs[:option]
- AST::MyRaw.new(language: lang, children: [AST::Text.new(raw_content)])
- end
-end
-```
-
-### Custom Handler from Scratch
-
-For complex behavior, extend `BaseHandler`:
-
-```ruby
-class CustomHandler < BaseHandler
- attr_reader :element_class
-
- def initialize
- @element_class = AST::Custom
- end
-
- def on_open(context:, token:, registry:)
- # Custom opening logic
- element = create_element(token)
- context.push_element(element)
- # Can modify state, look ahead, etc.
- end
-
- def on_close(token:, context:, registry:, tokens: nil)
- # Custom closing logic
- # Can use tokens for look-ahead
- registry.close_element(token:, context:, tokens:)
- end
-
- def auto_closeable?
- true # or false
- end
-
- private
-
- def create_element(token)
- AST::Custom.new(attrs: token.attrs)
- end
-end
-```
-
-## Creating Custom Renderers
-
-### Simple Tag
-
-For tags that wrap content with markers:
-
-```ruby
-class MyFormatTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- interface.wrap_inline(content, "~~") # Custom markers
- end
-end
-```
-
-### Tag with Attributes
-
-For tags that use element attributes:
-
-```ruby
-class ColorTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- color = element.color || "inherit"
-
- # Render as HTML span
- "#{content}"
- end
-end
-```
-
-### Context-Aware Tag
-
-For tags that behave differently based on parents:
-
-```ruby
-class SmartQuoteTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
-
- # Check nesting depth
- depth = interface.count_parents(AST::Quote)
-
- if depth > 1
- # Nested quote - use different style
- "> #{content}"
- else
- # Top-level quote
- "[quote]\n#{content}\n[/quote]"
- end
- end
-end
-```
-
-### Block Tag
-
-For tags that render as blocks:
-
-```ruby
-class MyBlockTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
-
- # Always render as block with blank lines
- "\n\n#{content}\n\n"
- end
-end
-```
-
-### Self-Rendering Tag
-
-For tags that don't have children:
-
-```ruby
-class IconTag < Tag
- def render(element, interface)
- # element.icon_name set during parsing
- ":#{element.icon_name}:"
- end
-end
-```
-
-### Block-Based Tag
-
-Register inline without creating a class:
-
-```ruby
-library.register(AST::Spoiler) do |element, interface|
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- "[spoiler]#{content}[/spoiler]"
-end
-```
-
-### Testing Custom Tags Against the html_mode Contract
-
-Every tag must also render sensibly when `interface.html_mode?` is
-true — inside a CommonMark HTML block (for example the table HTML
-fallback), raw Markdown output would show up as literal text. A tag
-has two valid forms there: a raw HTML fragment, or its normal Markdown
-wrapped in `\n\n…\n\n` (a Markdown island; the blank lines let
-CommonMark parse the inner content). `HtmlBlock.island` builds that
-wrap, and `HtmlBlock.safe?` is the check behind the shared example
-below.
-
-Markbridge ships this check as a shared RSpec example. Require it from
-your spec setup and give it your tag and a sample element:
-
-```ruby
-require "markbridge/rspec"
-
-RSpec.describe MyQuoteTag do
- it_behaves_like "an html_mode safe tag" do
- let(:tag) { described_class.new }
- let(:element) do
- element = MyQuote.new
- element << Markbridge::AST::Text.new("body *with* sigils")
- element
- end
- end
-end
-```
-
-Give the element children whose text contains Markdown sigils — a tag
-that passes them through unprotected is then caught. Markbridge runs
-its own tags through the same example. When the tag needs a customized
-renderer to resolve its children, override `markbridge_renderer` with
-your configured renderer inside the block.
-
-## Extending the Normalizer
-
-Between parse and render, `Markbridge::Normalizer` rewrites the AST so the
-renderer only gets markup the target format can express — a fenced code block
-inside bold is moved out, a link inside a link is unwrapped. It runs by
-default on every conversion. The pass itself and its default rules are
-described in [AST Normalization](normalization.md); this section covers the
-two parts that matter when you extend Markbridge: adding your own rules, and
-what happens when you subclass an AST node.
-
-### Adding Your Own Rules
-
-Build a fresh normalizer with `Markbridge::Normalizer.default`, add rules
-with `#rule`, and pass it via the `normalize:` keyword (available on all
-`*_to_markdown` methods, `Markbridge.convert`, and `Markbridge.render`):
-
-```ruby
-require "markbridge/all"
-
-normalizer = Markbridge::Normalizer.default
-normalizer.rule(
- parent: Markbridge::AST::Url,
- child: Markbridge::AST::Image,
- strategy: :hoist_after
-)
-
-conversion = Markbridge.convert(
- "[url=https://example.com][img]https://example.com/a.png[/img][/url]",
- format: :bbcode,
- normalize: normalizer
-)
-conversion.diagnostics[:normalization]
-# => [{ parent: "Url", child: "Image", strategy: :hoist_after, count: 1 }]
-```
-
-`#rule` takes a `parent:` class, a `child:` class, and a `strategy:` — one of
-`:keep`, `:hoist_after`, `:unwrap`, `:textify`, `:drop`, or a callable (see
-[Strategies](normalization.md#strategies)). It is chainable. A rule for a
-`(parent, child)` pair that already has one replaces it, so your rules
-override the defaults.
-
-Two things to know:
-
-- `Markbridge::Normalizer.shared_default` — the instance behind
- `normalize: true` — is frozen, so `#rule` raises on it. Always build a
- fresh `.default` when you want to customize.
-- `#violations(ast)` lists what the rules would change without changing the
- tree. Use it as a dry run, or as a lint in your test suite:
-
-```ruby
-Markbridge::Normalizer.default.violations(ast)
-# => [{ parent: "Bold", child: "List", strategy: :hoist_after }]
-```
-
-Build a customized normalizer once and reuse it for every conversion — it
-keeps no per-call state.
-
-### Subclasses and Ancestry Matching
-
-Subclassing a built-in AST node is a common way to mark some nodes for
-special treatment: your handler creates the subclass for certain inputs,
-and everything else keeps working. A subclass inherits its base class
-behavior in all three lookups:
-
-1. **Normalizer rules** — a rule for `Code` also matches a `Code`
- subclass, on the parent side and on the child side.
-2. **Tag dispatch** — a node whose class has no registered tag renders
- through the tag of its nearest ancestor class.
-3. **`interface.render_default`** — same ancestry lookup, against the
- default library.
-
-So this works with no extra registration — a multi-line `LegacyCode`
-inside `Bold` is hoisted out and rendered as a code fence, exactly like a
-plain `Code`:
-
-```ruby
-class LegacyCode < Markbridge::AST::Code
-end
-```
-
-The most specific match wins. To change behavior for the subclass,
-register on the subclass:
-
-- A rule for the exact `(parent, child)` pair overrides an inherited one:
- `normalizer.rule(parent: AST::Bold, child: LegacyCode, strategy: :drop)`.
-- A tag registered for `LegacyCode` beats the inherited `CodeTag`. Inside
- it, `interface.render_default(node)` still reaches the stock `CodeTag`,
- so the custom tag can wrap or delegate.
-- To get plain child rendering instead of the inherited tag, register
- `Markbridge::Renderers::Discourse::Tag::PASSTHROUGH` for the subclass.
- It renders only the children (with the element on the parent chain).
-
-Matching stays inside the AST hierarchy: no rule or tag is registered for
-`AST::Element` or `AST::Node` by default, so `unregister(AST::Color)`
-still means "render children only" — there is no ancestor tag to inherit.
-
-## Plugin Pattern
-
-Create reusable plugins that bundle parser and renderer extensions:
-
-### Plugin Module
-
-```ruby
-module Markbridge
- module Plugins
- module Quote
- # AST Node
- class QuoteElement < AST::Element
- attr_reader :author
-
- def initialize(author: nil, children: [])
- @author = author
- super(children:)
- end
- end
-
- # Handler
- class QuoteHandler < Parsers::BBCode::Handlers::SimpleHandler
- def initialize
- super(QuoteElement, auto_closeable: false)
- end
-
- def create_element(token)
- author = token.attrs[:author] || token.attrs[:option]
- QuoteElement.new(author:)
- end
- end
-
- # Renderer Tag
- class QuoteTag < Renderers::Discourse::Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- author = element.author ? " #{element.author}" : ""
-
- "[quote#{author}]\n#{content}\n[/quote]"
- end
- end
-
- # Plugin registration
- def self.register_parser(registry)
- registry.register("quote", QuoteHandler.new)
- end
-
- def self.register_renderer(library)
- library.register(QuoteElement, QuoteTag.new)
- end
-
- def self.register_all(parser_registry, renderer_library)
- register_parser(parser_registry)
- register_renderer(renderer_library)
- end
- end
- end
-end
-```
-
-### Using the Plugin
-
-```ruby
-require "markbridge/all"
-require "markbridge/plugins/quote"
-
-# Configure parser
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- Markbridge::Plugins::Quote.register_parser(registry)
-end
-
-# Configure renderer
-library = Markbridge::Renderers::Discourse::TagLibrary.default
-Markbridge::Plugins::Quote.register_renderer(library)
-renderer = Markbridge::Renderers::Discourse::Renderer.new(tag_library: library)
-
-# Use
-bbcode = "[quote author=Jane]Hello[/quote]"
-ast = parser.parse(bbcode)
-markdown = renderer.render(ast)
-```
-
-### Plugin Collection
-
-Create a registry of plugins:
-
-```ruby
-module Markbridge
- module Plugins
- class Registry
- def initialize
- @plugins = []
- end
-
- def register(plugin)
- @plugins << plugin
- end
-
- def configure_parser(parser_registry)
- @plugins.each { |plugin| plugin.register_parser(parser_registry) }
- end
-
- def configure_renderer(renderer_library)
- @plugins.each { |plugin| plugin.register_renderer(renderer_library) }
- end
- end
- end
-end
-
-# Usage
-plugins = Markbridge::Plugins::Registry.new
-plugins.register(Markbridge::Plugins::Quote)
-plugins.register(Markbridge::Plugins::Color)
-plugins.register(Markbridge::Plugins::Spoiler)
-
-parser = Parser.new do |registry|
- plugins.configure_parser(registry)
-end
-
-library = TagLibrary.new
-library.auto_register!
-plugins.configure_renderer(library)
-renderer = Renderer.new(tag_library: library)
-```
-
-## Advanced Examples
-
-### Table Support
-
-Complete example adding table support:
-
-```ruby
-# AST Nodes
-class AST::Table < AST::Element
-end
-
-class AST::TableRow < AST::Element
-end
-
-class AST::TableCell < AST::Element
- attr_reader :header
-
- def initialize(header: false, children: [])
- @header = header
- super(children:)
- end
-end
-
-# Handlers
-class TableHandler < SimpleHandler
- def initialize
- super(AST::Table, auto_closeable: false)
- end
-end
-
-class TableRowHandler < SimpleHandler
- def initialize
- super(AST::TableRow, auto_closeable: true)
- end
-
- def on_open(context:, token:, registry:)
- # Auto-close previous row
- if context.current_node.is_a?(AST::TableRow)
- context.pop_element
- end
- super
- end
-end
-
-class TableCellHandler < SimpleHandler
- def initialize
- super(AST::TableCell, auto_closeable: true)
- end
-
- def create_element(token)
- header = token.tag == "th"
- AST::TableCell.new(header:)
- end
-
- def on_open(context:, token:, registry:)
- # Auto-close previous cell
- if context.current_node.is_a?(AST::TableCell)
- context.pop_element
- end
- super
- end
-end
-
-# Renderer Tags
-class TableTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- rows = element.children.map do |row|
- render_row(row, interface, child_context)
- end
-
- header_separator = build_header_separator(element.children.first)
-
- "\n\n#{rows[0]}\n#{header_separator}\n#{rows[1..].join("\n")}\n\n"
- end
-
- private
-
- def render_row(row, interface, context)
- row_context = context.with_parent(row)
- cells = row.children.map do |cell|
- cell_context = row_context.with_parent(cell)
- interface.render_children(cell, context: cell_context).strip
- end
-
- "| #{cells.join(" | ")} |"
- end
-
- def build_header_separator(header_row)
- cell_count = header_row.children.size
- "| " + (["---"] * cell_count).join(" | ") + " |"
- end
-end
-
-# Usage
-parser = Parser.new do |registry|
- registry.register("table", TableHandler.new)
- registry.register("tr", TableRowHandler.new)
- registry.register(["td", "th"], TableCellHandler.new)
-end
-
-library = TagLibrary.new
-library.register(AST::Table, TableTag.new)
-# TableRow and TableCell handled by TableTag
-
-renderer = Renderer.new(tag_library: library)
-
-bbcode = <<~BBCODE
- [table]
- [tr][th]Name[th]Age
- [tr][td]Alice[td]30
- [tr][td]Bob[td]25
- [/table]
-BBCODE
-
-markdown = Markbridge.convert(bbcode, parser:, renderer:)
-puts markdown
-# | Name | Age |
-# | --- | --- |
-# | Alice | 30 |
-# | Bob | 25 |
-```
-
-### Size/Font Tags
-
-```ruby
-class AST::Size < AST::Element
- attr_reader :size
-
- def initialize(size: nil, children: [])
- @size = size
- super(children:)
- end
-end
-
-class SizeHandler < SimpleHandler
- def initialize
- super(AST::Size, auto_closeable: true)
- end
-
- def create_element(token)
- size = token.attrs[:size] || token.attrs[:option]
- AST::Size.new(size:)
- end
-end
-
-class SizeTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
-
- # Map size to Discourse notation or HTML
- size_map = {
- "small" => "#{content}",
- "large" => "#{content}",
- "huge" => "#{content}
"
- }
-
- size_map[element.size] || content
- end
-end
-
-# Register
-registry.register(["size", "font"], SizeHandler.new)
-library.register(AST::Size, SizeTag.new)
-
-# Usage: [size=large]Big text[/size]
-```
-
-### Align Tags
-
-```ruby
-class AST::Align < AST::Element
- attr_reader :alignment
-
- def initialize(alignment: "left", children: [])
- @alignment = alignment
- super(children:)
- end
-end
-
-class AlignHandler < SimpleHandler
- def initialize
- super(AST::Align, auto_closeable: false)
- end
-
- def create_element(token)
- # Get alignment from tag name or attribute
- alignment = case token.tag
- when "left" then "left"
- when "center" then "center"
- when "right" then "right"
- else token.attrs[:option] || "left"
- end
-
- AST::Align.new(alignment:)
- end
-end
-
-class AlignTag < Tag
- def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
-
- # Discourse uses for alignment. A starts a CommonMark
- # HTML block, so the content only gets parsed as Markdown when it
- # is wrapped as an island (HtmlBlock.island puts the blank lines
- # around it and folds the ones the content already has).
- island = Markbridge::Renderers::Discourse::HtmlBlock.island(content)
- "\n\n#{island}\n\n"
- end
-end
-
-# Register
-registry.register(["align", "left", "center", "right"], AlignHandler.new)
-library.register(AST::Align, AlignTag.new)
-
-# Usage: [center]Centered text[/center]
-```
-
-## Best Practices
-
-### DO
-
-✓ **Extend existing base classes** (`SimpleHandler`, `Tag`)
-```ruby
-class MyHandler < SimpleHandler
- # Reuse existing behavior
-end
-```
-
-✓ **Use keyword arguments** for attributes
-```ruby
-def initialize(color: nil, children: [])
- @color = color
- super(children:)
-end
-```
-
-✓ **Create child context** before rendering children
-```ruby
-child_context = interface.with_parent(element)
-content = interface.render_children(element, context: child_context)
-```
-
-✓ **Set auto_closeable appropriately**
-- `true` for inline formatting (bold, italic, etc.)
-- `false` for block elements (lists, tables, etc.)
-
-✓ **Extract attributes from token**
-```ruby
-value = token.attrs[:attr_name] || token.attrs[:option]
-```
-
-✓ **Test your extensions thoroughly**
-- Unit tests for handlers
-- Unit tests for tags
-- Integration tests for full pipeline
-
-✓ **Follow naming conventions** for auto-registration
-- `BoldTag` → `AST::Bold`
-- `QuoteTag` → `AST::Quote`
-
-### DON'T
-
-✗ **Don't mutate context**
-```ruby
-# Bad
-context.add_parent(element)
-
-# Good
-child_context = interface.with_parent(element)
-```
-
-✗ **Don't forget to call super in initialize**
-```ruby
-# Bad
-def initialize(color: nil, children: [])
- @color = color
- # Missing super!
-end
-
-# Good
-def initialize(color: nil, children: [])
- @color = color
- super(children:)
-end
-```
-
-✗ **Don't access renderer directly in tags**
-```ruby
-# Bad (old pattern)
-renderer.render_children(element)
-
-# Good (new pattern)
-interface.render_children(element, context: child_context)
-```
-
-✗ **Don't skip creating child context**
-```ruby
-# Bad
-content = interface.render_children(element, context: interface.context)
-
-# Good
-child_context = interface.with_parent(element)
-content = interface.render_children(element, context: child_context)
-```
-
-✗ **Don't hardcode indentation/spacing**
-```ruby
-# Bad
-" #{content}" # Fixed 2 spaces
-
-# Good
-depth = interface.count_parents(AST::List)
-"#{' ' * (depth * 2)}#{content}"
-```
-
-✗ **Don't forget frozen_string_literal comment**
-```ruby
-# Always start files with:
-# frozen_string_literal: true
-```
-
-### Performance Tips
-
-- Use `SimpleHandler` when possible (optimized)
-- Keep `create_element` fast (called for every tag)
-- Avoid excessive string allocations in renderers
-- Use `wrap_inline` helper (handles fallbacks)
-- Cache expensive lookups in handler initialize
-
-### Testing Your Extensions
-
-```ruby
-RSpec.describe QuoteHandler do
- let(:handler) { described_class.new }
- let(:registry) { double("registry") }
- let(:context) { double("context") }
-
- describe "#create_element" do
- it "creates Quote with author from attribute" do
- token = TagStartToken.new("quote", { author: "John" })
- element = handler.create_element(token)
-
- expect(element).to be_a(AST::Quote)
- expect(element.author).to eq("John")
- end
-
- it "uses option attribute as fallback" do
- token = TagStartToken.new("quote", { option: "Jane" })
- element = handler.create_element(token)
-
- expect(element.author).to eq("Jane")
- end
- end
-end
-
-RSpec.describe QuoteTag do
- let(:tag) { described_class.new }
-
- describe "#render" do
- it "renders quote with author" do
- element = AST::Quote.new(author: "John", children: [
- AST::Text.new("Hello")
- ])
-
- interface = double("interface")
- allow(interface).to receive(:with_parent).and_return(double("context"))
- allow(interface).to receive(:render_children).and_return("Hello")
-
- result = tag.render(element, interface)
-
- expect(result).to eq("[quote John]\nHello\n[/quote]")
- end
- end
-end
-```
-
-## Next Steps
-
-- **[BBCode Parser Guide](parsers/bbcode.md)** - Deep dive into parsing
-- **[Discourse Renderer Guide](renderers/discourse.md)** - Learn about rendering
-- **[AST Normalization](normalization.md)** - The pass between parse and render, and its default rules
-- **[Architecture Overview](architecture.md)** - Understand the pipeline
-- **[Performance Guide](performance.md)** - Optimize your extensions
diff --git a/docs/normalization.md b/docs/normalization.md
deleted file mode 100644
index ad905724..00000000
--- a/docs/normalization.md
+++ /dev/null
@@ -1,126 +0,0 @@
-# AST Normalization
-
-Markup can nest elements in ways Markdown cannot express: a link inside a
-link, a block element inside an inline container (a link label, but also bold
-or a heading), a fenced code block inside emphasis. If the renderer prints
-these as-is, the Markdown breaks — the inner link wins and the outer one turns
-into text, a block's blank lines break out of the emphasis around it.
-
-`Markbridge::Normalizer` walks the AST once, between the parse-time `yield`
-hook and rendering, and rewrites it so the renderer only gets markup the
-target format can express. It runs **by default**. The renderer's tags stay
-simple string emitters; the rules about what may nest in what live here
-instead.
-
-## Where it runs
-
-```
-parse → yield(ast) → normalize → render
-```
-
-Because it runs after the `yield` hook, changes you make to the AST in that
-block are normalized too. It runs for every source format and for
-`Markbridge.render`, because normalization is about the *target* format, not
-the source.
-
-## The default rules
-
-The default rules are CommonMark legality. Break one and the Markdown does
-not parse back as the tree meant:
-
-- No link inside a link, at any depth (§6.3). The inner link is unwrapped.
-- An inline container holds inline content only, so a block element inside one
- is moved out. This is not link-specific: emphasis (`Bold`, `Italic`, …) and
- headings are inline containers too, so a poll inside bold or a list inside a
- heading is handled the same way as a block inside a link. The lists are
- `Normalizer::INLINE_CONTAINERS` and `Normalizer::BLOCK_NODES` (which covers
- `List`, `Table`, `Quote`, `Details`, `HorizontalRule`, `Align`, and the
- Discourse `Poll`/`Event` nodes).
-- A code span inside an inline container is fine while it stays on one line. A
- fenced or multi-line block is moved out.
-
-Discourse-specific policy is **not** built in. Moving an image out of a link,
-for example, is a rule you add yourself (see below). A linked image
-(`[](url)`) is valid CommonMark, so the default leaves it alone.
-
-## Strategies
-
-Each match resolves to one strategy:
-
-| Strategy | Effect |
-|----------|--------|
-| `:keep` | Allow it. This records a decision and keeps it out of the report. |
-| `:hoist_after` | Move the node out and put it right after the outermost matching ancestor, keeping the document order. An image in a bold that sits in a link is moved after the whole link (out of both), because the bold is inside the link. The walker only moves a node out to a sibling; it never puts one into a wrapper it was not already in. |
-| `:unwrap` | Remove the element and put its children in its place. The built-in case is a link inside a link: `[[text](inner)](outer)` becomes `[text](outer)`. The inner link and its href are dropped; its text stays under the outer link. |
-| `:textify` | Replace the subtree with its plain text (`@name` for a mention, the joined text otherwise). |
-| `:drop` | Remove it. |
-| callable | `->(boundary, node) { … }` that returns a strategy symbol, an `Array` to put in its place, or `nil` to drop it. Use this for anything the built-in strategies do not cover. |
-
-A formatting wrapper (bold, italic, color, …) that ends up empty after a
-hoist or drop is removed, so no empty `**` `**` markers are left. A link is
-the exception: an empty link is kept, because it renders as a plain URL.
-
-## Diagnostics
-
-Every change is reported through the same channel as `unknown_tags`:
-
-```ruby
-conversion = Markbridge.convert(input, format: :bbcode)
-conversion.diagnostics[:normalization]
-# => [{ parent: "Url", child: "Url", strategy: :unwrap, count: 1 }]
-```
-
-For a migration this feeds per-post warnings and shows which sources produce
-broken trees. The key is absent when nothing changed.
-
-## Opting out and customizing
-
-`normalize:` takes `true` (default, the shared normalizer), `false` (skip), or
-a `Normalizer` instance:
-
-```ruby
-# Skip normalization
-Markbridge.convert(input, format: :bbcode, normalize: false)
-
-# Add your own rules on top of the defaults
-normalizer = Markbridge::Normalizer.default
-normalizer.rule(parent: Markbridge::AST::Url, child: Markbridge::AST::Image, strategy: :hoist_after)
-Markbridge.convert(input, format: :bbcode, normalize: normalizer)
-```
-
-Build a customized normalizer once and reuse it. `#normalize` and
-`#violations` keep no state on the instance, so one instance (freeze it if you
-like) is safe to use for every conversion, also across threads. Passing your
-own instance is as fast as the default path — there is no per-call rule
-build.
-
-A rule for a `(parent, child)` pair that already exists is replaced, so your
-`#rule` calls override the defaults. Matching follows the class ancestry: a
-rule for `AST::Url` also catches an `AST::Url` subclass, on both sides, and
-a rule registered for the more specific class wins. See
-[Subclasses and Ancestry Matching](extending.md#subclasses-and-ancestry-matching)
-for the details.
-
-`Markbridge::Normalizer.shared_default` is the default normalizer, built once
-and frozen; the `normalize: true` path uses it. Do not change it — call
-`.default` for a fresh, customizable one.
-
-## Validation
-
-The same rules, without changing the tree:
-
-```ruby
-Markbridge::Normalizer.default.violations(ast)
-# => [{ parent: "Url", child: "Url", strategy: :unwrap }]
-```
-
-Two uses: check in your own test suite that the trees your parsers and tag
-fixtures build have no violations, or run it as a lint over a corpus without
-changing any output. After a `normalize`, `violations` returns nothing —
-normalization is done in a single pass.
-
-## Adding a target format
-
-`Normalizer.default` builds the rule table; the engine (`RuleSet`, `Walker`)
-does not care about the format. A second target would add another builder and
-a matching class method next to `default`; nothing else changes.
diff --git a/docs/package.json b/docs/package.json
new file mode 100644
index 00000000..279a56c9
--- /dev/null
+++ b/docs/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "docs",
+ "type": "module",
+ "version": "0.0.1",
+ "packageManager": "pnpm@12.4.2",
+ "scripts": {
+ "dev": "node ./scripts/generate-changelog.mjs && astro dev --host",
+ "start": "node ./scripts/generate-changelog.mjs && astro dev --host",
+ "build": "node ./scripts/generate-changelog.mjs && astro build",
+ "preview": "astro preview",
+ "changelog": "node ./scripts/generate-changelog.mjs",
+ "astro": "astro"
+ },
+ "dependencies": {
+ "@astrojs/starlight": "^0.42.1",
+ "astro": "^7.3.3",
+ "sharp": "^0.35.4"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+}
diff --git a/docs/parsers/bbcode.md b/docs/parsers/bbcode.md
deleted file mode 100644
index 41246a93..00000000
--- a/docs/parsers/bbcode.md
+++ /dev/null
@@ -1,1045 +0,0 @@
-# BBCode Parser Guide
-
-This comprehensive guide explains how the BBCode parser converts forum-style markup into the Markbridge AST, including handlers, closing strategies, limits, and error handling.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Quick Start](#quick-start)
-- [Supported Tags](#supported-tags)
-- [Parser Components](#parser-components)
-- [Handlers](#handlers)
-- [Closing Strategies](#closing-strategies)
-- [Auto-Close Behavior](#auto-close-behavior)
-- [Nesting and Limits](#nesting-and-limits)
-- [Error Handling](#error-handling)
-- [Configuration](#configuration)
-- [Examples](#examples)
-
-## Overview
-
-The BBCode parser (`Markbridge::Parsers::BBCode::Parser`) tokenizes input, dispatches to tag handlers, and builds an `AST::Document` tree. It follows a two-step process:
-
-1. **Scanning** - Convert input string to tokens (text, tag start, tag end)
-2. **Parsing** - Process tokens through handlers to build AST
-
-**Key Features:**
-- Graceful degradation for unknown tags (ignored while processing children)
-- Configurable closing strategies (strict or reordering)
-- Auto-closing of formatting tags
-- Raw content handling for code blocks
-- Depth limits to prevent stack overflow
-
-## Quick Start
-
-### Basic Usage
-
-```ruby
-require "markbridge/all"
-
-# Simple parsing
-parser = Markbridge::Parsers::BBCode::Parser.new
-ast = parser.parse("[b]Hello[/b] world!")
-
-# Check for unknown tags
-parser.unknown_tags # => {}
-
-# With custom configuration
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- registry.closing_strategy = ClosingStrategies::Strict.new
- registry.register("custom", CustomHandler.new)
-end
-```
-
-### Input Handling
-
-The parser automatically:
-- Normalizes line endings (CRLF → LF)
-- Preserves whitespace and formatting
-- Handles EOF without closing tags
-
-## Supported Tags
-
-### Formatting Tags
-
-| Tags | Handler | AST Node | Auto-closeable | Notes |
-|------|---------|----------|----------------|-------|
-| `[b]`, `[bold]`, `[strong]` | `SimpleHandler` | `AST::Bold` | Yes | Nested formatting allowed |
-| `[i]`, `[italic]`, `[em]` | `SimpleHandler` | `AST::Italic` | Yes | Common emphasis tag |
-| `[s]`, `[strike]`, `[del]` | `SimpleHandler` | `AST::Strikethrough` | Yes | Strike-through text |
-| `[u]`, `[underline]` | `SimpleHandler` | `AST::Underline` | Yes | Underline text |
-
-### Code Tags
-
-| Tags | Handler | AST Node | Auto-closeable | Notes |
-|------|---------|----------|----------------|-------|
-| `[code]`, `[pre]`, `[tt]` | `RawHandler` | `AST::Code` | No | Captures unparsed content until closing tag; `[code]` and `[pre]` set `block: true` (always a fenced block), `[tt]` stays inline for single-line content |
-
-**Attributes:**
-- `lang` or option attribute sets language hint
-- Example: `[code lang=ruby]...[/code]` or `[code=ruby]...[/code]`
-
-### Link Tags
-
-| Tags | Handler | AST Node | Auto-closeable | Notes |
-|------|---------|----------|----------------|-------|
-| `[url]`, `[link]`, `[iurl]` | `UrlHandler` | `AST::Url` | Yes | Uses `href`, `url`, or option attribute |
-
-**Examples:**
-```bbcode
-[url=https://example.com]Link text[/url]
-[url href=https://example.com]Link text[/url]
-[url]https://example.com[/url]
-```
-
-### List Tags
-
-| Tags | Handler | AST Node | Auto-closeable | Notes |
-|------|---------|----------|----------------|-------|
-| `[list]`, `[ul]`, `[ol]`, `[ulist]`, `[olist]` | `ListHandler` | `AST::List` | No | Auto-closes open list items before closing |
-| `[*]`, `[li]`, `[.]` | `ListItemHandler` | `AST::ListItem` | Yes | Auto-closes previous list item |
-
-**Ordered Lists:**
-- Use `[ol]` or `[olist]` tags
-- Or `[list type=1]` / `[list=1]`
-
-**Examples:**
-```bbcode
-[list]
-[*]First item
-[*]Second item
-[/list]
-
-[ol]
-[*]Numbered item 1
-[*]Numbered item 2
-[/ol]
-```
-
-### Self-Closing Tags
-
-| Tags | Handler | AST Node | Auto-closeable | Notes |
-|------|---------|----------|----------------|-------|
-| `[br]` | `SelfClosingHandler` | `AST::LineBreak` | N/A | Closing tag treated as text if present |
-| `[hr]` | `SelfClosingHandler` | `AST::HorizontalRule` | N/A | Closing tag treated as text if present |
-
-## Parser Components
-
-### Scanner
-
-**Location:** `Markbridge::Parsers::BBCode::Scanner`
-
-**Responsibility:** Stream characters and produce tokens
-
-**Token Types:**
-
-#### TextToken
-```ruby
-token = TextToken.new("Hello world")
-token.text # => "Hello world"
-```
-
-#### TagStartToken
-```ruby
-token = TagStartToken.new("b", {})
-token.tag # => "b"
-token.attrs # => {}
-
-# With attributes
-token = TagStartToken.new("url", { href: "https://example.com" })
-token.attrs[:href] # => "https://example.com"
-```
-
-#### TagEndToken
-```ruby
-token = TagEndToken.new("b")
-token.tag # => "b"
-```
-
-**Key Features:**
-- Character-by-character streaming
-- Minimal allocations for performance
-- Automatic attribute parsing
-- Position tracking for errors
-
-### Parser
-
-**Location:** `Markbridge::Parsers::BBCode::Parser`
-
-**Responsibility:** Orchestrate scanning and build AST
-
-**Key Methods:**
-
-```ruby
-# Main entry point
-ast = parser.parse("[b]text[/b]")
-
-# Access unknown tags
-parser.unknown_tags # => {"unknown" => count}
-```
-
-**Parsing Flow:**
-1. Normalize line endings
-2. Create scanner from input
-3. Wrap scanner in PeekableEnumerator for look-ahead
-4. Process each token via handlers
-5. Return completed AST::Document
-
-### ParserState
-
-**Location:** `Markbridge::Parsers::BBCode::ParserState`
-
-**Responsibility:** Manage parsing state during traversal
-
-**State Tracking:**
-- Current node (where to add children)
-- Element stack (for nested tags)
-- Depth counter (prevent overflow)
-- Auto-close counter (track auto-closes)
-
-**Key Methods:**
-```ruby
-state.current_node # Current element being built
-state.push_element(element) # Start nested element
-state.pop_element # Close current element
-state.depth # Current nesting depth
-```
-
-**Depth Limit:**
-- Maximum depth: 100 nested elements
-- Exceeding raises `MaxDepthExceededError`
-
-### HandlerRegistry
-
-**Location:** `Markbridge::Parsers::BBCode::HandlerRegistry`
-
-**Responsibility:** Map tag names to handlers
-
-**Default Registry:**
-```ruby
-registry = HandlerRegistry.default
-# Contains all built-in handlers
-```
-
-**Custom Registry:**
-```ruby
-# Build from default and customize
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("quote", QuoteHandler.new)
-end
-
-# Or create new registry
-registry = HandlerRegistry.new
-registry.register("b", SimpleHandler.new(AST::Bold, auto_closeable: true))
-```
-
-**Features:**
-- Tag name normalization (case-insensitive)
-- Tag name caching for performance
-- Auto-closeable tracking
-- Element class mapping
-
-**Recent Improvements (November 2025):**
-- `element_class` is now public (`attr_reader`)
-- Simplified registration (no redundant parameters)
-- Block-based configuration support
-- Settable `closing_strategy` via `attr_writer`
-
-## Handlers
-
-Handlers convert tokens to AST nodes. Each handler type serves a specific purpose.
-
-### BaseHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::BaseHandler`
-
-**Base class for all handlers**
-
-**Interface:**
-```ruby
-class BaseHandler
- # Public accessor to element class
- attr_reader :element_class
-
- # Called when opening tag encountered
- def on_open(context:, token:, registry:)
- element = create_element(token)
- context.push_element(element)
- end
-
- # Called when closing tag encountered
- def on_close(token:, context:, registry:, tokens: nil)
- registry.close_element(token:, context:, tokens:)
- end
-
- # Whether tag can be auto-closed
- def auto_closeable?
- false # Override in subclasses
- end
-
- private
-
- # Subclasses implement to create specific AST node
- def create_element(token)
- raise NotImplementedError
- end
-end
-```
-
-### SimpleHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::SimpleHandler`
-
-**Purpose:** Handle basic formatting tags (bold, italic, etc.)
-
-**Usage:**
-```ruby
-# Create handler for bold tag
-handler = SimpleHandler.new(AST::Bold, auto_closeable: true)
-
-# Register with multiple tag names
-registry.register(["b", "bold", "strong"], handler)
-```
-
-**Features:**
-- Simple element creation
-- Configurable auto-closing
-- No special attribute handling
-
-### RawHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::RawHandler`
-
-**Purpose:** Handle code blocks that don't parse inner BBCode
-
-**Behavior:**
-1. On open tag: Start collecting raw content
-2. Consume all content until matching close tag
-3. Don't parse any inner BBCode
-4. Create `AST::Code` with raw text
-
-**Example:**
-```bbcode
-[code lang=ruby]
-[b]This is not parsed as bold[/b]
-puts "Raw content preserved"
-[/code]
-```
-
-**Result:**
-```ruby
-AST::Code.new(
- language: "ruby",
- children: [AST::Text.new("[b]This is not parsed as bold[/b]\nputs \"Raw content preserved\"")]
-)
-```
-
-**Attributes:**
-- `lang` attribute → `language:` parameter
-- Option attribute (e.g., `[code=ruby]`) → `language:` parameter
-
-### SelfClosingHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::SelfClosingHandler`
-
-**Purpose:** Handle tags that don't need closing (line breaks, horizontal rules)
-
-**Behavior:**
-- On open: Insert element immediately
-- On close: Treat closing tags as text
-
-**Example:**
-```bbcode
-Line 1[br]Line 2
-[hr]
-Horizontal rule above
-```
-
-**Result:**
-```ruby
-AST::Document.new([
- AST::Text.new("Line 1"),
- AST::LineBreak.new,
- AST::Text.new("Line 2\n"),
- AST::HorizontalRule.new,
- AST::Text.new("\nHorizontal rule above")
-])
-```
-
-### UrlHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::UrlHandler`
-
-**Purpose:** Handle link tags with URL attributes
-
-**Attribute Resolution:**
-1. Check `href` attribute
-2. Check `url` attribute
-3. Check option attribute (e.g., `[url=...]`)
-4. If none, use child content as URL
-
-**Examples:**
-```bbcode
-[url=https://example.com]Link[/url]
-→ AST::Url.new(href: "https://example.com", children: [Text("Link")])
-
-[url href=https://example.com]Link[/url]
-→ AST::Url.new(href: "https://example.com", children: [Text("Link")])
-
-[url]https://example.com[/url]
-→ AST::Url.new(href: "https://example.com", children: [Text("https://example.com")])
-```
-
-### ListHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::ListHandler`
-
-**Purpose:** Handle list containers (ordered and unordered)
-
-**Ordered Detection:**
-- Tag is `ol` or `olist`
-- OR `type` attribute is "1"
-- OR option attribute is "1"
-
-**Auto-Close Behavior:**
-- When closing list, auto-closes any open list item first
-- Prevents malformed list structures
-
-**Example:**
-```bbcode
-[list]
-[*]Item 1
-[*]Item 2
-[/list]
-```
-
-**Result:**
-```ruby
-AST::List.new(ordered: false, children: [
- AST::ListItem.new(children: [AST::Text.new("Item 1")]),
- AST::ListItem.new(children: [AST::Text.new("Item 2")])
-])
-```
-
-### ListItemHandler
-
-**Location:** `Markbridge::Parsers::BBCode::Handlers::ListItemHandler`
-
-**Purpose:** Handle list items with auto-closing
-
-**Auto-Close Behavior:**
-- When opening new list item, auto-closes previous list item
-- Allows BBCode without explicit closing: `[*]Item 1 [*]Item 2`
-
-**Example:**
-```bbcode
-[list]
-[*]Item 1
-[*]Item 2
-```
-
-Both items get auto-closed when next item starts or list closes.
-
-## Closing Strategies
-
-Closing strategies determine how the parser handles closing tags that don't match the current element.
-
-### Overview
-
-**Two strategies available:**
-1. **Strict** - Auto-close only, no reordering
-2. **Reordering** - Look-ahead for matching sequences (default)
-
-**Configuration:**
-```ruby
-# Use strict strategy
-parser = Parser.new do |registry|
- reconciler = ClosingStrategies::TagReconciler.new(registry: registry)
- registry.closing_strategy = ClosingStrategies::Strict.new(reconciler)
-end
-
-# Use reordering strategy (default)
-parser = Parser.new # Already uses reordering
-```
-
-### Strict Strategy
-
-**Location:** `Markbridge::Parsers::BBCode::ClosingStrategies::Strict`
-
-**Three-step fallback:**
-1. **Exact Match** - If closing tag matches current element, pop it
-2. **Auto-Close** - Try to auto-close intermediate tags
-3. **Text Fallback** - Treat closing tag as literal text
-
-**Auto-Close Conditions (all must be met):**
-- Target opening tag exists in stack (within 5 levels)
-- Every element between current and target is auto-closeable
-- Matching tag is less than 5 levels deep
-
-**Example: Auto-close success**
-```bbcode
-[b]bold [i]italic[/b] text
-```
-
-Stack: `[root, bold, italic]`
-Closing `[/b]`:
-- Find bold at depth 2
-- Check intermediate: italic is auto-closeable ✓
-- Auto-close italic, then bold ✓
-
-Result: `**_bold italic_** text`
-
-**Example: Auto-close failure**
-```bbcode
-[b]text[i]more[/ul]
-```
-
-Stack: `[root, bold, italic]`
-Closing `[/ul]`:
-- No ul tag in stack ✗
-- Cannot auto-close
-- `[/ul]` becomes text
-
-Result: `**text_more[/ul]_**`
-
-### Reordering Strategy
-
-**Location:** `Markbridge::Parsers::BBCode::ClosingStrategies::Reordering`
-
-**Four-step fallback:**
-1. **Exact Match** - Same as Strict
-2. **Reordering** - Look ahead for matching closing sequence
-3. **Auto-Close** - Falls back to auto-close if reordering fails
-4. **Text Fallback** - Treat as literal text
-
-**Reordering Conditions (all must be met):**
-- Target opening tag exists (within 5 levels)
-- Intermediate elements are auto-closeable
-- Upcoming closing tags match open tags exactly
-
-**Max peek-ahead:** 5 tokens
-
-**Example: Reordering success**
-```bbcode
-[b][i]text[/b][/i]
-```
-
-Stack: `[root, bold, italic]`
-Closing `[/b]`:
-- Expected `[/i]`, got `[/b]`
-- Peek ahead: Find `[/i]` next
-- Match sequence: `[italic, bold]` == `[italic, bold]` ✓
-- Consume both closers, close both properly ✓
-
-Result: `**_text_**`
-
-**With Strict strategy:**
-- Would auto-close at `[/b]`: `**_text_**`
-- Then `[/i]` becomes text: `**_text_**[/i]`
-
-**Example: Wrong closer ahead**
-```bbcode
-[b][i]text[/b][u]more[/u]
-```
-
-Stack: `[root, bold, italic]`
-Closing `[/b]`:
-- Peek ahead: See `[u]` (tag start, not `[/i]` end)
-- Reordering fails (no matching sequence) ✗
-- Fall back to auto-close ✓
-
-Result: Same as Strict
-
-### When to Use Each Strategy
-
-**Use Strict when:**
-- You want predictable, simple behavior
-- Users write well-formed BBCode
-- Performance is critical (no look-ahead overhead)
-- Debugging is easier (no magic reordering)
-
-**Use Reordering when:**
-- Users frequently misordering closing tags (common in forums)
-- You want forgiving parsing
-- Look-ahead overhead is acceptable
-- Better user experience > parsing speed
-
-### TagReconciler
-
-**Location:** `Markbridge::Parsers::BBCode::ClosingStrategies::TagReconciler`
-
-**Purpose:** Helper for closing strategies to match handlers
-
-**Key Methods:**
-```ruby
-# Find handler for element
-handler = reconciler.handler_for_element(element)
-
-# Check if handlers match for reordering
-handlers_match = reconciler.handlers_match?(handler1, handler2)
-```
-
-**Used by:**
-- Reordering strategy for look-ahead matching
-- Both strategies for auto-close logic
-
-## Auto-Close Behavior
-
-### What is Auto-Closing?
-
-Auto-closing automatically closes intermediate tags when a closing tag doesn't match the current element.
-
-**Example:**
-```bbcode
-[b][i]text[/b]
-```
-
-Stack before `[/b]`: `[root, bold, italic]`
-Expected: `[/i]`
-Got: `[/b]`
-
-Auto-close:
-1. Find `bold` in stack (depth 2)
-2. Check `italic` is auto-closeable ✓
-3. Auto-close `italic`, then close `bold` ✓
-
-### The Auto-Close Algorithm
-
-#### Step 1: Find the Target
-
-Search up the stack (max 5 levels) for element matching the closing tag's handler.
-
-```ruby
-# Stack: [root, bold, italic, underline]
-# Closing: [/b]
-# Search: underline (no) → italic (no) → bold (yes!)
-# Target: bold at depth 2
-```
-
-#### Step 2: Check Auto-Closeability
-
-Verify that **every** element between current and target is auto-closeable.
-
-**Auto-closeable elements:**
-- Bold, Italic, Underline, Strikethrough
-- Links (Url)
-- Custom formatting added with `auto_closeable: true`
-
-**Non-auto-closeable elements:**
-- Lists (`[list]`, `[ul]`, `[ol]`)
-- List items (`[*]`, `[li]`) - but special handling
-- Code blocks (`[code]`)
-- Any custom tags with `auto_closeable: false`
-
-#### Step 3: Close the Stack
-
-If checks pass, pop all elements from current to target (inclusive).
-
-```ruby
-# Before: [root, bold, italic, underline]
-# Closing: [/b]
-# Pop: underline, italic, bold
-# After: [root]
-# Auto-closed: 3 elements
-```
-
-### Auto-Close Limits
-
-#### Maximum Depth: 5 Levels
-
-Auto-closing stops at 5 levels to prevent runaway behavior.
-
-```bbcode
-[b][i][u][s][sub][sup]text[/b]
-```
-
-Stack depth to `[b]`: 6 levels
-Depth limit: 5
-Auto-close fails ✗
-Result: `[/b]` becomes text
-
-**Why 5?**
-- Balances flexibility with performance
-- Prevents deeply nested auto-close cascades
-- Matches typical BBCode nesting patterns (rare to have > 5 nested format tags)
-- O(5) = O(1) constant time
-
-### Edge Cases
-
-#### Root Document
-
-The root Document element has no handler, so closing tags at root level always become text.
-
-```bbcode
-[/b]text
-```
-
-No bold tag open → `[/b]` becomes text
-Result: `[/b]text`
-
-#### Mixed Auto-Closeable and Block Elements
-
-```bbcode
-[b]text
-[list]
-[*][i]item[/b]
-```
-
-Stack: `[root, bold, list, list-item, italic]`
-Closing `[/b]`:
-- Find `bold` at depth 4
-- Check intermediate: `list` is not auto-closeable ✗
-- Cannot auto-close ✗
-- `[/b]` becomes text
-
-Result: `[/b]` rendered as text inside list item
-
-#### Multiple Attempts
-
-Auto-close is attempted **only once** per closing tag. If it fails, tag becomes text.
-
-```bbcode
-[b]text[list][*]item[/b][/list]
-```
-
-1. Try to auto-close at `[/b]`
-2. Cannot close past `list` (not auto-closeable)
-3. `[/b]` becomes text inside list item
-4. No retry
-
-### Performance Characteristics
-
-- **Best case:** O(1) - exact match (no searching)
-- **Auto-close:** O(n) where n ≤ 5 - linear scan up stack
-- **Failure:** O(n) - scan completes, tag becomes text
-
-Auto-closing adds minimal overhead due to the depth limit.
-
-## Nesting and Limits
-
-### Maximum Depth: 100 Elements
-
-The parser refuses to descend beyond 100 nested elements to prevent stack overflow.
-
-**Example:**
-```ruby
-bbcode = "[b]" * 101 + "text" + "[/b]" * 101
-parser.parse(bbcode) # Raises MaxDepthExceededError
-```
-
-**Why 100?**
-- Prevents malicious deeply-nested input from crashing
-- Realistic BBCode rarely exceeds 10-20 levels
-- Provides clear error message
-
-**Error:**
-```ruby
-Markbridge::Parsers::BBCode::MaxDepthExceededError:
- Maximum nesting depth (100) exceeded
-```
-
-### Maximum Auto-Close Depth: 5 Levels
-
-Auto-closing and reordering only examine the 5 most recent elements.
-
-**Tags deeper than this limit:**
-- Will not be auto-closed
-- Their stray closers emitted as text
-- Must be explicitly closed in order
-
-**Example:**
-```bbcode
-[1][2][3][4][5][6]text[/1]
-```
-
-Stack depth to `[1]`: 6 levels
-Auto-close limit: 5
-Cannot auto-close `[1]` ✗
-`[/1]` becomes text
-
-### Maximum Peek-Ahead: 5 Tokens
-
-Reordering strategy only looks ahead 5 tokens for matching sequences.
-
-**Why 5?**
-- Balances flexibility with performance
-- Prevents expensive look-ahead scans
-- Most misordering is within 2-3 tags
-
-**Beyond limit:**
-- Reordering won't match the sequence
-- Falls back to auto-close or text
-
-## Error Handling
-
-### Unknown Tags
-
-Unknown tags are tracked and ignored while their children are still parsed.
-
-**Example:**
-```ruby
-parser = Parser.new
-ast = parser.parse("[unknown]text[/unknown]")
-
-# Check unknown tags
-parser.unknown_tags # => {"unknown" => 2}
-
-# AST contains only the child content
-ast.children.first.text # => "text"
-```
-
-**Multiple occurrences:**
-```ruby
-ast = parser.parse("[foo]a[/foo] [foo]b[/foo]")
-parser.unknown_tags # => {"foo" => 2}
-```
-
-### Unclosed Tags
-
-Unclosed tags remain open until end of document.
-
-**Example:**
-```bbcode
-[b]This is bold to EOF
-```
-
-Result: Bold element containing "This is bold to EOF"
-
-### Unexpected Closing Tags
-
-Handled by closing strategy:
-- Try exact match
-- Try reordering (if reordering strategy)
-- Try auto-close
-- Fallback to text
-
-**Example:**
-```bbcode
-[b]text[/i]
-```
-
-No italic open → Cannot match → Cannot auto-close → Text
-Result: `**text[/i]**`
-
-### Raw Content EOF
-
-If raw handler (code block) doesn't find closing tag, returns content to EOF.
-
-**Example:**
-```bbcode
-[code]
-No closing tag
-```
-
-Result: Code element containing "\nNo closing tag\n"
-
-### Self-Closing Unexpected Close
-
-Self-closing tags ignore unexpected closing tags (treat as text).
-
-**Example:**
-```bbcode
-[br][/br]
-```
-
-Scanner sees:
-1. `[br]` → Insert LineBreak
-2. `[/br]` → No handler for close (self-closing) → Text
-
-Result: LineBreak + Text("[/br]")
-
-## Configuration
-
-### Block-Based Configuration (Recommended)
-
-```ruby
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- # Add custom handlers
- registry.register("quote", QuoteHandler.new)
- registry.register("color", ColorHandler.new)
-
- # Set closing strategy
- reconciler = ClosingStrategies::TagReconciler.new(registry: registry)
- registry.closing_strategy = ClosingStrategies::Strict.new(reconciler)
-end
-```
-
-### Using build_from_default
-
-```ruby
-# Start with default handlers and customize
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("custom", CustomHandler.new)
-end
-
-parser = Parser.new(handlers: registry)
-```
-
-### Custom Handler Registry
-
-```ruby
-# Create from scratch
-registry = HandlerRegistry.new
-
-# Add handlers
-registry.register(["b", "bold"], SimpleHandler.new(AST::Bold, auto_closeable: true))
-registry.register("code", RawHandler.new)
-
-# Set closing strategy
-reconciler = ClosingStrategies::TagReconciler.new(registry: registry)
-registry.closing_strategy = ClosingStrategies::Reordering.new(reconciler)
-
-parser = Parser.new(handlers: registry)
-```
-
-## Examples
-
-### Basic Formatting
-
-```ruby
-parser = Parser.new
-ast = parser.parse("[b]Bold [i]and italic[/i][/b]")
-
-# AST structure:
-# Document
-# └─ Bold
-# ├─ Text("Bold ")
-# ├─ Italic
-# │ └─ Text("and italic")
-```
-
-### Nested Lists
-
-```ruby
-ast = parser.parse(<<~BBCODE)
- [list]
- [*]First item
- [*]Second item
- [list]
- [*]Nested item
- [/list]
- [*]Third item
- [/list]
-BBCODE
-
-# AST structure:
-# Document
-# └─ List(ordered: false)
-# ├─ ListItem
-# │ └─ Text("First item")
-# ├─ ListItem
-# │ ├─ Text("Second item\n")
-# │ └─ List(ordered: false)
-# │ └─ ListItem
-# │ └─ Text("Nested item")
-# └─ ListItem
-# └─ Text("Third item")
-```
-
-### Code Blocks
-
-```ruby
-ast = parser.parse(<<~BBCODE)
- [code lang=ruby]
- def hello
- puts "world"
- end
- [/code]
-BBCODE
-
-# AST structure:
-# Document
-# └─ Code(language: "ruby")
-# └─ Text("def hello\n puts \"world\"\nend\n")
-```
-
-### Links
-
-```ruby
-ast = parser.parse("[url=https://example.com]Example[/url]")
-
-# AST structure:
-# Document
-# └─ Url(href: "https://example.com")
-# └─ Text("Example")
-```
-
-### Mixed Content
-
-```ruby
-ast = parser.parse(<<~BBCODE)
- [b]Bold text[/b] and [url=https://example.com]a link[/url].
-
- [list]
- [*]First
- [*]Second
- [/list]
-
- [code]
- Some code
- [/code]
-BBCODE
-
-# Multiple top-level elements under Document
-```
-
-### Unknown Tags
-
-```ruby
-parser = Parser.new
-ast = parser.parse("[unknown]text[/unknown] [b]bold[/b]")
-
-parser.unknown_tags # => {"unknown" => 2}
-
-# AST structure:
-# Document
-# ├─ Text("text ")
-# └─ Bold
-# └─ Text("bold")
-```
-
-### Misordered Tags (Reordering)
-
-```ruby
-ast = parser.parse("[b][i]text[/b][/i]")
-
-# Reordering strategy:
-# - Peeks ahead, sees [/i]
-# - Matches sequence: [italic, bold]
-# - Closes both properly
-
-# AST structure:
-# Document
-# └─ Bold
-# └─ Italic
-# └─ Text("text")
-```
-
-### Misordered Tags (Strict)
-
-```ruby
-registry = HandlerRegistry.build_from_default do |reg|
- reconciler = ClosingStrategies::TagReconciler.new(registry: reg)
- reg.closing_strategy = ClosingStrategies::Strict.new(reconciler)
-end
-parser = Parser.new(handlers: registry)
-
-ast = parser.parse("[b][i]text[/b][/i]")
-
-# Strict strategy:
-# - Auto-closes at [/b]
-# - [/i] becomes text
-
-# AST structure:
-# Document
-# ├─ Bold
-# │ └─ Italic
-# │ └─ Text("text")
-# └─ Text("[/i]")
-```
-
-## Next Steps
-
-- **[Discourse Renderer Guide](../renderers/discourse.md)** - Learn how to render AST to Markdown
-- **[Extending Markbridge](../extending.md)** - Add custom tags and handlers
-- **[Architecture Overview](../architecture.md)** - Understand the full pipeline
diff --git a/docs/parsers/comparison.md b/docs/parsers/comparison.md
deleted file mode 100644
index c4bef9fd..00000000
--- a/docs/parsers/comparison.md
+++ /dev/null
@@ -1,883 +0,0 @@
-# Parser Comparison and Review
-
-This document provides a comprehensive comparison of the four parsers available in Markbridge: BBCode, HTML, TextFormatter, and MediaWiki. It analyzes their APIs, performance characteristics, maintainability, and extensibility.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Architecture Comparison](#architecture-comparison)
-- [API Comparison](#api-comparison)
-- [Performance Analysis](#performance-analysis)
-- [Maintainability](#maintainability)
-- [Extensibility](#extensibility)
-- [Use Cases](#use-cases)
-- [Recommendations](#recommendations)
-
-## Overview
-
-Markbridge includes four parsers, each designed for different input formats:
-
-| Parser | Input Format | Parsing Strategy | Dependencies |
-|--------|-------------|------------------|--------------|
-| **BBCode** | `[b]text[/b]` | Custom tokenizer + stateful parser | None (pure Ruby) |
-| **HTML** | `text` | DOM-based (Nokogiri) | Nokogiri |
-| **TextFormatter** | `text ` | XML-based (Nokogiri) | Nokogiri |
-| **MediaWiki** | `'''bold'''` | Line-based + inline character parser | None (pure Ruby) |
-
-### BBCode Parser
-
-**Purpose:** Parse forum-style BBCode markup into AST
-
-**Location:** `Markbridge::Parsers::BBCode::Parser`
-
-**Key features:**
-- Custom tokenizer (Scanner) for precise control
-- Stateful parsing with depth tracking
-- Multiple closing strategies (Strict, Reordering)
-- Raw content handling for code blocks
-- Auto-closing support for formatting tags
-- Zero dependencies
-
-### HTML Parser
-
-**Purpose:** Parse standard HTML into AST
-
-**Location:** `Markbridge::Parsers::HTML::Parser`
-
-**Key features:**
-- Leverages Nokogiri's HTML parser
-- DOM tree traversal
-- Handles malformed HTML gracefully
-- Void element support (self-closing tags)
-- Simple handler API
-
-### TextFormatter Parser
-
-**Purpose:** Parse s9e/TextFormatter XML format into AST
-
-**Location:** `Markbridge::Parsers::TextFormatter::Parser`
-
-**Key features:**
-- XML parsing with Nokogiri
-- Handles s9e/TextFormatter specific format
-- Ignores markup preservation elements (``, ``)
-- Case-sensitive element names (uppercase convention)
-- Fallback to plain text for invalid XML
-
-### MediaWiki Parser
-
-**Purpose:** Parse MediaWiki wikitext into AST
-
-**Location:** `Markbridge::Parsers::MediaWiki::Parser`
-
-**Key features:**
-- Two-level architecture: line-based block parser + character-based inline parser
-- Extensible HTML-like tag handling via `InlineTagRegistry`
-- Depth limiting for inline recursion (MAX_INLINE_DEPTH = 20)
-- Zero dependencies (pure Ruby)
-- Handles heterogeneous syntax (apostrophes, line-prefixes, brackets, HTML tags)
-
-## Architecture Comparison
-
-### BBCode Parser Architecture
-
-```
-Input → Scanner → Tokens → Handler (via Registry) → AST
- ↓ ↓
- TextToken on_open/on_close
- TagStartToken (stateful)
- TagEndToken
-```
-
-**Components:**
-- **Scanner:** Character-by-character tokenization, attribute parsing
-- **ParserState:** Stack-based state management, depth tracking
-- **HandlerRegistry:** Maps tag names to handlers, manages closing strategy
-- **Handlers:** Stateful, receive `on_open` and `on_close` events
-- **ClosingStrategies:** Pluggable strategies for handling mismatched tags
-
-**Key characteristics:**
-- Streaming tokenization (O(n) single pass)
-- Stateful parsing (maintain element stack)
-- Event-driven handler API (open/close events)
-- Complex closing logic with look-ahead
-
-### HTML Parser Architecture
-
-```
-Input → Nokogiri HTML → DOM Tree → Handler → AST
- ↓ ↓
- Element Nodes process()
- Text Nodes (stateless)
-```
-
-**Components:**
-- **Nokogiri::HTML:** External HTML parser
-- **HandlerRegistry:** Simple tag-to-handler mapping
-- **Handlers:** Stateless, receive entire element at once
-- **Parser:** Walks DOM tree, dispatches to handlers
-
-**Key characteristics:**
-- DOM-based (Nokogiri parses entire document)
-- Stateless parsing (DOM tree already constructed)
-- Simple handler API (single `process` method)
-- Handles malformed HTML via Nokogiri
-
-### TextFormatter Parser Architecture
-
-```
-Input → Nokogiri XML → XML Tree → Handler → AST
- ↓ ↓
- Element Nodes process()
- Text Nodes (stateless)
-```
-
-**Components:**
-- **Nokogiri::XML:** External XML parser
-- **HandlerRegistry:** Element-to-handler mapping (case-sensitive)
-- **Handlers:** Stateless, receive entire element
-- **Parser:** Walks XML tree, filters special elements
-
-**Key characteristics:**
-- XML-based (Nokogiri parses entire document)
-- Stateless parsing (XML tree already constructed)
-- Special handling for s9e/TextFormatter conventions
-- Uppercase element names (convention)
-- Fallback to plain text on parse errors
-
-### MediaWiki Parser Architecture
-
-```
-Input → Line Splitter → Block Parser → AST
- ↓
- InlineParser (per line)
- ↓
- InlineTagRegistry (HTML-like tags)
-```
-
-**Components:**
-- **Parser:** Line-by-line block classification (heading, list, preformatted, etc.)
-- **InlineParser:** Character-by-character inline markup parsing
-- **InlineTagRegistry:** Extensible registry for HTML-like tags (``, ``, etc.)
-
-**Key characteristics:**
-- Two-level parsing (block + inline)
-- Character-based inline scanning with position tracking
-- Recursive inline parsing with depth limiting (MAX_INLINE_DEPTH = 20)
-- Registry-based HTML tag dispatch
-- Zero dependencies (pure Ruby)
-
-## API Comparison
-
-### Parser Initialization
-
-All three parsers share a similar initialization API:
-
-```ruby
-# BBCode
-parser = Markbridge::Parsers::BBCode::Parser.new
-parser = Markbridge::Parsers::BBCode::Parser.new(handlers: custom_registry)
-parser = Markbridge::Parsers::BBCode::Parser.new do |registry|
- registry.register("custom", CustomHandler.new)
-end
-
-# HTML
-parser = Markbridge::Parsers::HTML::Parser.new
-parser = Markbridge::Parsers::HTML::Parser.new(handlers: custom_registry)
-parser = Markbridge::Parsers::HTML::Parser.new do |registry|
- registry.register("custom", CustomHandler.new)
-end
-
-# TextFormatter
-parser = Markbridge::Parsers::TextFormatter::Parser.new
-parser = Markbridge::Parsers::TextFormatter::Parser.new(handlers: custom_registry)
-parser = Markbridge::Parsers::TextFormatter::Parser.new do |registry|
- registry.register("CUSTOM", CustomHandler.new)
-end
-```
-
-**Similarities:**
-- Block-based configuration
-- Optional custom handler registry
-- `build_from_default` support
-
-**Differences:**
-- BBCode supports closing strategy configuration
-- TextFormatter uses uppercase element names
-
-### Parsing API
-
-```ruby
-# All three parsers
-ast = parser.parse(input_string)
-
-# Unknown tags tracking
-parser.unknown_tags
-```
-
-**Similarities:**
-- Single `parse(input)` method returns `AST::Document`
-- Track unknown tags/elements
-
-**Differences:**
-- TextFormatter falls back to plain text for invalid XML
-- HTML handles malformed input via Nokogiri
-
-### Handler API
-
-The handler APIs differ significantly between the three parsers:
-
-#### BBCode Handler API (Stateful)
-
-```ruby
-class CustomHandler < Markbridge::Parsers::BBCode::Handlers::BaseHandler
- def initialize(element_class, auto_closeable: false)
- @element_class = element_class
- @auto_closeable = auto_closeable
- end
-
- # Called when opening tag is encountered
- def on_open(token:, context:, registry:, tokens: nil)
- element = @element_class.new
- context.push(element, token:)
- end
-
- # Called when closing tag is encountered
- def on_close(token:, context:, registry:, tokens: nil)
- registry.close_element(token:, context:, tokens:)
- end
-
- def auto_closeable?
- @auto_closeable
- end
-
- attr_reader :element_class
-end
-```
-
-**Parameters:**
-- `token` - TagStartToken or TagEndToken with tag name and attributes
-- `context` - ParserState with element stack
-- `registry` - HandlerRegistry for element closing
-- `tokens` - PeekableEnumerator for look-ahead (optional)
-
-**Key features:**
-- Event-driven (separate open/close)
-- Access to parser state
-- Look-ahead capability
-- Auto-close support
-
-#### HTML Handler API (Stateless)
-
-```ruby
-class CustomHandler < Markbridge::Parsers::HTML::Handlers::BaseHandler
- def initialize(element_class)
- @element_class = element_class
- end
-
- # Called with complete DOM element
- def process(element:, parent:, processor:)
- ast_element = @element_class.new
- parent << ast_element
- processor.process_children(element, ast_element)
- end
-
- attr_reader :element_class
-end
-```
-
-**Parameters:**
-- `element` - Nokogiri::XML::Element (complete DOM element)
-- `parent` - AST::Element (where to add children)
-- `processor` - Parser (for processing children)
-
-**Key features:**
-- Single method (entire element at once)
-- DOM tree already constructed
-- Simple, functional API
-- No state management needed
-
-#### TextFormatter Handler API (Stateless)
-
-```ruby
-class CustomHandler < Markbridge::Parsers::TextFormatter::Handlers::BaseHandler
- def initialize(element_class)
- @element_class = element_class
- end
-
- # Called with complete XML element
- def process(element:, parent:, processor:)
- node = @element_class.new
- parent << node
- processor.process_children(element, node)
- end
-
- attr_reader :element_class
-end
-```
-
-**Parameters:**
-- `element` - Nokogiri::XML::Element (XML element)
-- `parent` - AST::Element (where to add children)
-- `processor` - Parser (for processing children)
-
-**Key features:**
-- Identical to HTML handler API
-- XML tree already constructed
-- Helper method for attribute extraction
-- Case-sensitive element names
-
-### Handler Registration API
-
-```ruby
-# BBCode - requires handler with element_class and auto_closeable?
-registry.register("b", SimpleHandler.new(AST::Bold, auto_closeable: true))
-registry.register(["b", "bold", "strong"], handler)
-
-# BBCode - closing strategy configuration
-reconciler = ClosingStrategies::TagReconciler.new(registry: registry)
-registry.closing_strategy = ClosingStrategies::Reordering.new(reconciler)
-
-# HTML - simple registration
-registry.register("b", SimpleHandler.new(AST::Bold))
-registry.register(["b", "strong"], handler)
-
-# HTML - lambda support
-registry.register("br", ->(element:, parent:, processor:) {
- parent << AST::LineBreak.new
-})
-
-# TextFormatter - uppercase convention
-registry.register("B", SimpleHandler.new(AST::Bold))
-registry.register("URL", UrlHandler.new)
-
-# TextFormatter - lambda support
-registry.register("BR", ->(element:, parent:, processor:) {
- parent << AST::LineBreak.new
-})
-```
-
-**Similarities:**
-- Array of tag names supported (BBCode, HTML)
-- Lambda/proc handlers supported (HTML, TextFormatter)
-- Simple handler reuse (SimpleHandler)
-
-**Differences:**
-- BBCode requires `auto_closeable?` and `element_class`
-- BBCode supports closing strategy configuration
-- TextFormatter uses uppercase element names
-- BBCode has more complex handler requirements
-
-## Performance Analysis
-
-### Algorithmic Complexity
-
-| Operation | BBCode | HTML | TextFormatter |
-|-----------|--------|------|---------------|
-| Input parsing | O(n) custom | O(n) Nokogiri | O(n) Nokogiri |
-| Token/DOM generation | O(n) streaming | O(n) DOM build | O(n) XML parse |
-| Handler dispatch | O(1) hash | O(1) hash | O(1) hash |
-| Tree building | O(n) nodes | O(n) nodes | O(n) nodes |
-| **Overall** | **O(n)** | **O(n)** | **O(n)** |
-
-All three parsers have linear complexity, but differ in implementation:
-
-### BBCode Performance Characteristics
-
-**Advantages:**
-- ✓ Zero dependencies (no Nokogiri overhead)
-- ✓ Streaming tokenization (constant memory)
-- ✓ Minimal allocations (index-based access)
-- ✓ Bounded operations (depth limits)
-- ✓ Predictable performance
-
-**Disadvantages:**
-- ✗ Custom scanner maintenance
-- ✗ Closing strategy overhead (look-ahead)
-- ✗ State management complexity
-
-**Performance profile:**
-- Best for: BBCode-specific input, minimal dependencies
-- Memory: Low (streaming)
-- CPU: Moderate (custom tokenizer + state management)
-- Typical speed: 1-5 ms for 10 KB document
-
-### HTML Performance Characteristics
-
-**Advantages:**
-- ✓ Mature HTML parser (Nokogiri)
-- ✓ Handles malformed input well
-- ✓ Simple handler API (no state)
-- ✓ Battle-tested parsing
-
-**Disadvantages:**
-- ✗ Nokogiri dependency overhead
-- ✗ DOM tree memory usage
-- ✗ C extension requirement
-
-**Performance profile:**
-- Best for: Standard HTML input
-- Memory: Higher (full DOM tree)
-- CPU: Lower (Nokogiri optimized)
-- Typical speed: 2-6 ms for 10 KB document
-
-### TextFormatter Performance Characteristics
-
-**Advantages:**
-- ✓ XML parsing (well-defined)
-- ✓ Simple handler API (no state)
-- ✓ Error handling (fallback to text)
-
-**Disadvantages:**
-- ✗ Nokogiri dependency overhead
-- ✗ XML tree memory usage
-- ✗ Less forgiving than HTML parser
-
-**Performance profile:**
-- Best for: s9e/TextFormatter XML input
-- Memory: Higher (full XML tree)
-- CPU: Lower (Nokogiri optimized)
-- Typical speed: 2-6 ms for 10 KB document
-
-### Performance Comparison Summary
-
-```
-Memory Usage (10 KB input):
- BBCode: ~300 KB (streaming, minimal allocations)
- HTML: ~500 KB (DOM tree)
- TextFormatter: ~500 KB (XML tree)
-
-CPU Time (10 KB input):
- BBCode: 3-5 ms (custom scanner + state)
- HTML: 2-4 ms (Nokogiri HTML)
- TextFormatter: 2-4 ms (Nokogiri XML)
-
-Dependencies:
- BBCode: 0 (pure Ruby)
- HTML: 1 (Nokogiri)
- TextFormatter: 1 (Nokogiri)
-```
-
-**Key insight:** Nokogiri parsers (HTML, TextFormatter) are slightly faster due to optimized C implementation, but use more memory due to DOM/XML tree construction. BBCode parser uses less memory and has zero dependencies.
-
-## Maintainability
-
-### BBCode Parser Maintainability
-
-**Complexity Score:** Medium-High
-
-**Strengths:**
-- ✓ Well-documented components
-- ✓ Clear separation of concerns
-- ✓ Comprehensive test coverage
-- ✓ Modular design (Scanner, Handlers, Strategies)
-
-**Challenges:**
-- ✗ Custom scanner requires deep understanding
-- ✗ Closing strategies are complex
-- ✗ State management can be tricky
-- ✗ More code to maintain
-
-**Code metrics:**
-- Files: ~20 (parser core + handlers)
-- Lines: ~1500 total
-- Complexity: Medium-High (state + strategies)
-
-### HTML Parser Maintainability
-
-**Complexity Score:** Low
-
-**Strengths:**
-- ✓ Leverages Nokogiri (proven library)
-- ✓ Simple, functional handler API
-- ✓ Minimal state management
-- ✓ Easy to understand
-- ✓ Less code to maintain
-
-**Challenges:**
-- ✗ Nokogiri dependency updates
-- ✗ Limited control over parsing
-
-**Code metrics:**
-- Files: ~8 (parser + handlers)
-- Lines: ~400 total
-- Complexity: Low (DOM traversal only)
-
-### TextFormatter Parser Maintainability
-
-**Complexity Score:** Low
-
-**Strengths:**
-- ✓ Leverages Nokogiri (proven library)
-- ✓ Simple, functional handler API
-- ✓ Clear s9e conventions
-- ✓ Minimal complexity
-- ✓ Easy to understand
-
-**Challenges:**
-- ✗ Nokogiri dependency updates
-- ✗ s9e/TextFormatter format changes
-- ✗ Case-sensitive names (convention)
-
-**Code metrics:**
-- Files: ~11 (parser + handlers)
-- Lines: ~500 total
-- Complexity: Low (XML traversal + conventions)
-
-### Maintainability Comparison
-
-| Aspect | BBCode | HTML | TextFormatter |
-|--------|--------|------|---------------|
-| Code volume | High | Low | Low |
-| Complexity | Medium-High | Low | Low |
-| External deps | None | Nokogiri | Nokogiri |
-| Test coverage | Comprehensive | Good | Good |
-| Documentation | Extensive | Adequate | Adequate |
-| Learning curve | Steep | Gentle | Gentle |
-| Bug surface | Larger | Smaller | Smaller |
-
-**Recommendation:** HTML and TextFormatter parsers are easier to maintain due to simplicity. BBCode parser requires more expertise but provides more control.
-
-## Extensibility
-
-### BBCode Parser Extensibility
-
-**Extensibility Score:** High
-
-**Extension points:**
-- ✓ Custom handlers (on_open/on_close events)
-- ✓ Custom closing strategies
-- ✓ Raw content handlers
-- ✓ Custom token handling (via tokens parameter)
-- ✓ Auto-close configuration
-- ✓ Look-ahead for complex patterns
-
-**Example: Custom BBCode tag**
-```ruby
-class QuoteHandler < Markbridge::Parsers::BBCode::Handlers::BaseHandler
- def initialize
- @element_class = AST::Quote
- end
-
- def on_open(token:, context:, registry:, tokens: nil)
- author = token.attrs[:author] || token.attrs[:option]
- element = AST::Quote.new(author: author)
- context.push(element, token:)
- end
-
- def auto_closeable?
- false
- end
-
- attr_reader :element_class
-end
-
-parser = Parser.new do |registry|
- registry.register("quote", QuoteHandler.new)
-end
-```
-
-**Strengths:**
-- Full control over parsing logic
-- Access to parser state
-- Look-ahead capability
-- Custom closing behavior
-
-**Challenges:**
-- Requires understanding parser state
-- Must implement both open/close
-- Auto-close requires careful design
-
-### HTML Parser Extensibility
-
-**Extensibility Score:** Medium
-
-**Extension points:**
-- ✓ Custom handlers (process method)
-- ✓ Lambda handlers for simple cases
-- ✓ Access to full DOM node
-- ✓ Void element detection
-
-**Example: Custom HTML tag**
-```ruby
-class QuoteHandler < Markbridge::Parsers::HTML::Handlers::BaseHandler
- def initialize
- @element_class = AST::Quote
- end
-
- def process(element:, parent:, processor:)
- author = element["data-author"] || element["author"]
- ast_element = AST::Quote.new(author: author)
- parent << ast_element
- processor.process_children(element, ast_element)
- end
-
- attr_reader :element_class
-end
-
-parser = Parser.new do |registry|
- registry.register("blockquote", QuoteHandler.new)
-end
-```
-
-**Strengths:**
-- Simple handler API
-- Full DOM access
-- Easy to implement
-
-**Challenges:**
-- No access to parser state
-- Can't influence parsing strategy
-- Limited to DOM tree structure
-
-### TextFormatter Parser Extensibility
-
-**Extensibility Score:** Medium
-
-**Extension points:**
-- ✓ Custom handlers (process method)
-- ✓ Lambda handlers for simple cases
-- ✓ Access to full XML element
-- ✓ Attribute extraction helper
-
-**Example: Custom TextFormatter element**
-```ruby
-class QuoteHandler < Markbridge::Parsers::TextFormatter::Handlers::BaseHandler
- def initialize
- @element_class = AST::Quote
- end
-
- def process(element:, parent:, processor:)
- attrs = extract_attributes(element)
- quote = AST::Quote.new(author: attrs[:author])
- parent << quote
- processor.process_children(element, quote)
- end
-
- attr_reader :element_class
-end
-
-parser = Parser.new do |registry|
- registry.register("QUOTE", QuoteHandler.new)
-end
-```
-
-**Strengths:**
-- Simple handler API
-- Full XML access
-- Attribute extraction helper
-- Clear conventions
-
-**Challenges:**
-- No access to parser state
-- Can't influence parsing strategy
-- Limited to XML tree structure
-- Case-sensitive naming
-
-### Extensibility Comparison
-
-| Feature | BBCode | HTML | TextFormatter |
-|---------|--------|------|---------------|
-| Handler complexity | High | Low | Low |
-| Parser state access | ✓ | ✗ | ✗ |
-| Look-ahead support | ✓ | ✗ | ✗ |
-| Custom strategies | ✓ | ✗ | ✗ |
-| Lambda handlers | ✗ | ✓ | ✓ |
-| Attribute access | Token attrs | DOM attrs | XML attrs |
-| Event-driven | ✓ | ✗ | ✗ |
-| Learning curve | Steep | Gentle | Gentle |
-
-**Recommendation:** BBCode parser offers maximum flexibility for complex parsing logic. HTML and TextFormatter parsers are simpler for straightforward tag handling.
-
-## Use Cases
-
-### When to Use BBCode Parser
-
-**Best for:**
-- ✓ Forum migration projects (BBCode → Markdown)
-- ✓ Need zero dependencies (pure Ruby)
-- ✓ Custom BBCode dialects
-- ✓ Complex tag interactions
-- ✓ Fine-grained control over parsing
-- ✓ Memory-constrained environments
-- ✓ BBCode-specific features (auto-close, etc.)
-
-**Examples:**
-- Migrating phpBB, vBulletin, or MyBB forums
-- Custom BBCode processors
-- Embedded systems (minimal memory)
-
-### When to Use HTML Parser
-
-**Best for:**
-- ✓ Standard HTML input
-- ✓ Web scraping → Markdown conversion
-- ✓ HTML email → Markdown
-- ✓ Handling malformed HTML
-- ✓ Leveraging standard HTML parsing
-- ✓ Simple handler requirements
-
-**Examples:**
-- Converting HTML documentation to Markdown
-- Email-to-forum content migration
-- Web content extraction
-
-### When to Use TextFormatter Parser
-
-**Best for:**
-- ✓ phpBB 3.2+ migrations (uses s9e/TextFormatter)
-- ✓ s9e/TextFormatter XML format
-- ✓ Well-formed XML input
-- ✓ Simple handler requirements
-- ✓ Forum software using TextFormatter
-
-**Examples:**
-- Migrating modern phpBB installations
-- Processing s9e/TextFormatter exports
-- Forum software using TextFormatter library
-
-## Recommendations
-
-### Performance Priority
-
-**Choose:** HTML or TextFormatter parser
-
-**Reasoning:** Nokogiri's optimized C implementation provides better performance for most inputs. The DOM/XML tree overhead is negligible for typical forum posts (< 100 KB).
-
-### Memory Priority
-
-**Choose:** BBCode parser
-
-**Reasoning:** Streaming tokenization uses less memory and has no Nokogiri dependency. Best for embedded systems or processing millions of small documents.
-
-### Maintainability Priority
-
-**Choose:** HTML or TextFormatter parser
-
-**Reasoning:** Simpler codebase, fewer components, easier to understand. Nokogiri handles parsing complexity.
-
-### Extensibility Priority
-
-**Choose:** BBCode parser
-
-**Reasoning:** Event-driven API, parser state access, look-ahead support, custom closing strategies. Maximum flexibility for complex parsing requirements.
-
-### Zero Dependencies Priority
-
-**Choose:** BBCode parser
-
-**Reasoning:** Pure Ruby implementation with no external dependencies.
-
-### Quick Start Priority
-
-**Choose:** HTML or TextFormatter parser
-
-**Reasoning:** Simpler API, less learning curve, easier to add custom handlers.
-
-## API Consistency Analysis
-
-### Common Patterns (All Three Parsers)
-
-**Initialization:**
-```ruby
-# All three support these patterns
-parser = Parser.new
-parser = Parser.new(handlers: custom_registry)
-parser = Parser.new { |registry| registry.register(...) }
-```
-
-**Parsing:**
-```ruby
-# All three use the same method
-ast = parser.parse(input)
-```
-
-**Unknown tracking:**
-```ruby
-# All three use the same name
-parser.unknown_tags # BBCode, HTML, TextFormatter
-```
-
-**Registry building:**
-```ruby
-# All three support this
-HandlerRegistry.build_from_default do |registry|
- registry.register(...)
-end
-```
-
-### API Differences
-
-| Feature | BBCode | HTML | TextFormatter | MediaWiki |
-|---------|--------|------|---------------|-----------|
-| Handler method | `on_open`, `on_close` | `process` | `process` | `InlineTagRegistry` |
-| Handler params | `token:, context:, registry:, tokens:` | `element:, parent:, processor:` | `element:, parent:, processor:` | `tag_name, type, element_class` |
-| Tag case | Lowercase | Lowercase | Uppercase | Lowercase |
-| Lambda support | ✗ | ✓ | ✓ | ✗ |
-| State access | ✓ | ✗ | ✗ | ✗ |
-| Auto-close config | ✓ | ✗ | ✗ | ✗ |
-
-### Opportunities for Unification
-
-**Could be unified:**
-- Unknown tracking name (`unknown_tags` everywhere)
-- Handler parameter names (`element:, parent:, processor:` everywhere)
-
-**Should remain different:**
-- Handler methods (stateful vs stateless is fundamental)
-- BBCode closing strategies (unique requirement)
-- TextFormatter uppercase convention (s9e convention)
-
-## Summary
-
-### Quick Comparison Table
-
-| Criteria | BBCode | HTML | TextFormatter | MediaWiki |
-|----------|--------|------|---------------|-----------|
-| **Performance** | Fast | Fastest | Fastest | Fast |
-| **Memory** | Low | Medium | Medium | Low |
-| **Dependencies** | None | Nokogiri | Nokogiri | None |
-| **Complexity** | High | Low | Low | Medium |
-| **Maintainability** | Medium | High | High | High |
-| **Extensibility** | Highest | Medium | Medium | Medium (inline tags) |
-| **Learning Curve** | Steep | Gentle | Gentle | Gentle |
-| **Use Case** | BBCode forums | HTML content | phpBB 3.2+ | Wiki migrations |
-
-### Final Recommendations
-
-1. **For new projects:** Start with HTML or TextFormatter (simpler)
-2. **For BBCode forums:** Use BBCode parser (purpose-built)
-3. **For phpBB 3.2+:** Use TextFormatter parser (native format)
-4. **For wiki migrations:** Use MediaWiki parser
-5. **For custom requirements:** BBCode parser offers most flexibility
-6. **For minimal dependencies:** BBCode or MediaWiki parser (zero deps)
-
-### Future Improvements
-
-**BBCode Parser:**
-- Consider optional Nokogiri backend for HTML entities
-- Benchmark different closing strategies
-- Optimize token allocation
-
-**HTML Parser:**
-- Add HTML5 semantic element support
-- Consider streaming API for large documents
-
-**TextFormatter Parser:**
-- Document s9e/TextFormatter conventions better
-- Add validation for expected XML structure
-
-**All Parsers:**
-- Unify unknown tracking API (`unknown_tags` everywhere)
-- Consider shared handler base class for common patterns
-- Add performance benchmarks comparing all three
-
-## Next Steps
-
-- **[BBCode Parser Guide](bbcode.md)** - Deep dive into BBCode parser
-- **[HTML Parser Guide](html.md)** - Learn about HTML parser
-- **[TextFormatter Parser Guide](text_formatter.md)** - Learn about TextFormatter parser
-- **[MediaWiki Parser Guide](mediawiki.md)** - Learn about MediaWiki parser
-- **[Architecture Overview](../architecture.md)** - Understand the pipeline
-- **[Extending Markbridge](../extending.md)** - Add custom handlers
-- **[Performance Guide](../performance.md)** - Optimization techniques
diff --git a/docs/parsers/html.md b/docs/parsers/html.md
deleted file mode 100644
index 7a75d3a5..00000000
--- a/docs/parsers/html.md
+++ /dev/null
@@ -1,409 +0,0 @@
-# HTML Parser Guide
-
-This guide explains how the HTML parser converts standard HTML into the Markbridge AST using Nokogiri's HTML parser.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Quick Start](#quick-start)
-- [Supported Tags](#supported-tags)
-- [Parser Components](#parser-components)
-- [Handlers](#handlers)
-- [Configuration](#configuration)
-- [Examples](#examples)
-
-## Overview
-
-The HTML parser (`Markbridge::Parsers::HTML::Parser`) uses Nokogiri to convert HTML markup into AST. It provides a simpler alternative to the BBCode parser when working with HTML content.
-
-**Key Features:**
-- Uses Nokogiri's HTML parser (libxml2 on MRI/TruffleRuby, Xerces/NekoHTML on JRuby)
-- Handles malformed HTML gracefully
-- Stateless handler API (simpler than BBCode)
-- Void element detection (self-closing tags)
-
-**Dependencies:**
-- Requires the `nokogiri` gem
-
-## Quick Start
-
-### Basic Usage
-
-```ruby
-require "markbridge/all"
-
-# Parse HTML to AST
-parser = Markbridge::Parsers::HTML::Parser.new
-html = "Hello world!"
-ast = parser.parse(html)
-
-# Render to Markdown
-renderer = Markbridge::Renderers::Discourse::Renderer.new
-markdown = renderer.render(ast)
-# => "**Hello** *world*!"
-```
-
-### With Custom Configuration
-
-```ruby
-parser = Markbridge::Parsers::HTML::Parser.new do |registry|
- registry.register("custom", CustomHandler.new)
-end
-```
-
-## Supported Tags
-
-### Formatting Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| ``, `` | `AST::Bold` | Bold text |
-| ``, `` | `AST::Italic` | Italic text |
-| ``, ``, `` | `AST::Strikethrough` | Strikethrough text |
-| `` | `AST::Underline` | Underline text |
-
-### Code Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| `` | `AST::Code` | Inline code; a block when the content has newlines |
-| `` | `AST::Code` | Code block (`block: true`), even for single-line content |
-
-The language for syntax highlighting is read from the first of: a
-`language-*` class on the element itself or on its direct `` child
-(the CommonMark convention, ``), the
-`lang` attribute, or a lone class used as-is. Styling classes like
-`class="hljs codeblock"` are ignored, and the result must be a single
-clean token (letters, digits, `_`, `+`, `-`) so it is safe on a fence
-line.
-
-### Link Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| `` | `AST::Url` | Uses `href` attribute |
-
-### List Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| `` | `AST::List` | Unordered list |
-| `` | `AST::List` | Ordered list |
-| `- ` | `AST::ListItem` | List item |
-
-### Structure Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| `
` | Paragraph handling | Adds spacing between paragraphs |
-| `
` – `` | `AST::Heading` | Heading with matching level |
-| `
` | `AST::LineBreak` | Line break |
-| `
` | `AST::HorizontalRule` | Horizontal rule |
-
-### Additional Tags
-
-| HTML Tag | AST Node | Notes |
-|----------|----------|-------|
-| `` | `AST::Quote` | Quote block |
-| `
` | `AST::Image` | Image with alt text and URL |
-
-## Parser Components
-
-### Parser
-
-**Location:** `Markbridge::Parsers::HTML::Parser`
-
-**Responsibility:** Orchestrate HTML parsing using Nokogiri
-
-**Key Methods:**
-
-```ruby
-# Main entry point
-ast = parser.parse(html_string)
-
-# Access unknown tags
-parser.unknown_tags # => {"unknown" => count}
-
-# Process children (used by handlers)
-parser.process_children(nokogiri_element, ast_parent)
-```
-
-**Parsing Flow:**
-1. Parse HTML with Nokogiri::HTML.fragment
-2. Walk DOM tree
-3. Dispatch each element to registered handlers
-4. Return completed AST::Document
-
-### HandlerRegistry
-
-**Location:** `Markbridge::Parsers::HTML::HandlerRegistry`
-
-**Responsibility:** Map HTML tag names to handlers
-
-**Key Methods:**
-
-```ruby
-# Default registry
-registry = HandlerRegistry.default
-
-# Custom registry
-registry = HandlerRegistry.new
-registry.register("custom", CustomHandler.new)
-
-# Build from default
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("custom", CustomHandler.new)
-end
-```
-
-## Handlers
-
-### Handler Interface
-
-Handlers receive complete DOM elements (stateless API):
-
-```ruby
-class CustomHandler < Markbridge::Parsers::HTML::Handlers::BaseHandler
- def initialize(element_class)
- @element_class = element_class
- end
-
- def process(element:, parent:)
- # element: Nokogiri::XML::Element (complete DOM element)
- # parent: AST::Element (where to add the new node)
-
- ast_element = @element_class.new
- parent << ast_element
-
- # Return the created element and the parser processes the DOM
- # element's children into it. Return nil to skip the children
- # (for example when the handler consumed the content itself).
- ast_element
- end
-
- attr_reader :element_class
-end
-```
-
-### Built-In Handlers
-
-#### SimpleHandler
-
-For basic formatting tags:
-
-```ruby
-handler = SimpleHandler.new(AST::Bold)
-registry.register(["b", "strong"], handler)
-```
-
-#### RawHandler
-
-For code blocks that preserve content. Takes the AST class to create:
-
-```ruby
-handler = RawHandler.new(Markbridge::AST::Code)
-registry.register(["code", "pre", "tt"], handler)
-```
-
-#### UrlHandler
-
-For links with href attributes:
-
-```ruby
-handler = UrlHandler.new
-registry.register("a", handler)
-```
-
-#### ListHandler & ListItemHandler
-
-For ordered and unordered lists:
-
-```ruby
-registry.register(["ul", "ol"], ListHandler.new)
-registry.register("li", ListItemHandler.new)
-```
-
-#### SelfClosingHandler
-
-For void elements that map to a childless AST node:
-
-```ruby
-registry.register("br", SelfClosingHandler.new(Markbridge::AST::LineBreak))
-registry.register("hr", SelfClosingHandler.new(Markbridge::AST::HorizontalRule))
-```
-
-## Configuration
-
-### Block-Based Configuration
-
-```ruby
-parser = Markbridge::Parsers::HTML::Parser.new do |registry|
- # Add custom handlers
- registry.register("blockquote", QuoteHandler.new)
- registry.register("span", SpanHandler.new)
-end
-```
-
-### Using build_from_default
-
-```ruby
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("custom", CustomHandler.new)
-end
-
-parser = Parser.new(handlers: registry)
-```
-
-## Examples
-
-### Basic Formatting
-
-```ruby
-parser = Parser.new
-ast = parser.parse("Bold and italic")
-
-# AST structure:
-# Document
-# ├─ Bold
-# │ └─ Text("Bold")
-# ├─ Text(" and ")
-# └─ Italic
-# └─ Text("italic")
-```
-
-### Lists
-
-```ruby
-html = <<~HTML
-
- - First item
- - Second item
-
-HTML
-
-ast = parser.parse(html)
-
-# AST structure:
-# Document
-# └─ List(ordered: false)
-# ├─ ListItem
-# │ └─ Text("First item")
-# └─ ListItem
-# └─ Text("Second item")
-```
-
-### Links
-
-```ruby
-html = 'Example'
-ast = parser.parse(html)
-
-# AST structure:
-# Document
-# └─ Url(href: "https://example.com")
-# └─ Text("Example")
-```
-
-### Code Blocks
-
-```ruby
-html = 'def hello\n puts "world"\nend
'
-ast = parser.parse(html)
-
-# AST structure:
-# Document
-# └─ Code
-# └─ Text("def hello\n puts \"world\"\nend")
-```
-
-### Malformed HTML
-
-The HTML parser handles malformed HTML gracefully through Nokogiri:
-
-```ruby
-html = "Unclosed bold and italic"
-ast = parser.parse(html)
-
-# Nokogiri auto-closes tags:
-# Document
-# └─ Bold
-# ├─ Text("Unclosed bold ")
-# └─ Italic
-# └─ Text("and italic")
-```
-
-### Unknown Tags
-
-```ruby
-parser = Parser.new
-html = "content bold"
-ast = parser.parse(html)
-
-parser.unknown_tags # => {"unknown" => 1}
-
-# AST structure:
-# Document
-# ├─ Text("content ") # Unknown tag wrapper ignored
-# └─ Bold
-# └─ Text("bold")
-```
-
-### Custom Handler
-
-```ruby
-class CustomHandler < BaseHandler
- def initialize
- @element_class = AST::Custom
- end
-
- def process(element:, parent:, processor:)
- custom_element = AST::Custom.new(
- data: element["data-custom"]
- )
- parent << custom_element
- processor.process_children(element, custom_element)
- end
-
- attr_reader :element_class
-end
-
-parser = Parser.new do |registry|
- registry.register("custom-tag", CustomHandler.new)
-end
-
-html = 'Content '
-ast = parser.parse(html)
-```
-
-## Comparison with BBCode Parser
-
-| Feature | HTML Parser | BBCode Parser |
-|---------|-------------|---------------|
-| Handler complexity | Low (stateless) | High (stateful) |
-| Dependencies | Nokogiri | None |
-| Malformed input | Handled by Nokogiri | Custom logic |
-| Handler API | `process(element:, parent:, processor:)` | `on_open/on_close(token:, context:, registry:)` |
-| Lambda support | ✓ | ✗ |
-| Parser state access | ✗ | ✓ |
-| Look-ahead | ✗ | ✓ |
-| Closing strategies | ✗ (Nokogiri handles) | ✓ (Strict/Reordering) |
-
-**When to use HTML Parser:**
-- Converting HTML content to Markdown
-- Web scraping to Markdown
-- HTML email content
-- Simpler handler requirements
-- Need robust malformed HTML handling
-
-**When to use BBCode Parser:**
-- Forum migrations (BBCode native format)
-- Zero dependencies required
-- Need fine-grained parsing control
-- Custom closing logic needed
-
-## Next Steps
-
-- **[BBCode Parser Guide](bbcode.md)** - Compare with BBCode parser
-- **[Parser Comparison](comparison.md)** - Detailed comparison of all parsers
-- **[Discourse Renderer Guide](../renderers/discourse.md)** - Learn about rendering
-- **[Extending Markbridge](../extending.md)** - Add custom handlers
diff --git a/docs/parsers/mediawiki.md b/docs/parsers/mediawiki.md
deleted file mode 100644
index c645e2a2..00000000
--- a/docs/parsers/mediawiki.md
+++ /dev/null
@@ -1,167 +0,0 @@
-# MediaWiki Parser Guide
-
-This document describes the MediaWiki parser in Markbridge, its architecture, supported syntax, and how to extend it.
-
-## Overview
-
-The MediaWiki parser converts [MediaWiki wikitext](https://www.mediawiki.org/wiki/Help:Formatting) into Markbridge's AST. It supports the most common MediaWiki formatting constructs used in wiki pages.
-
-**Location:** `Markbridge::Parsers::MediaWiki::Parser`
-
-**Key features:**
-- Two-level architecture: line-based block parser + character-based inline parser
-- Extensible HTML-like tag handling via `InlineTagRegistry`
-- Depth limiting for inline recursion (MAX_INLINE_DEPTH = 20)
-- Zero dependencies (pure Ruby)
-
-## Architecture
-
-```
-Input → Line Splitter → Block Parser → AST
- ↓
- InlineParser (per line)
- ↓
- InlineTagRegistry (HTML-like tags)
-```
-
-### Why Two Levels?
-
-MediaWiki syntax has fundamentally different patterns at the block and inline levels:
-
-**Block-level** constructs are line-based:
-- Headings: `== Heading ==`
-- Lists: `* item` / `# item`
-- Preformatted: lines starting with a space
-- Horizontal rules: `----`
-- Paragraphs: separated by blank lines
-
-**Inline** constructs are character-based:
-- Bold/italic: `'''bold'''` / `''italic''`
-- Links: `[[Page|text]]` / `[url text]`
-- HTML-like tags: ``, ``, `
`, etc.
-
-This is fundamentally different from BBCode's uniform `[tag]...[/tag]` syntax, which is why the MediaWiki parser uses a different architecture. See [comparison.md](comparison.md) for a detailed comparison.
-
-## Supported Syntax
-
-### Block-Level
-
-| Syntax | Description | AST Node |
-|--------|-------------|----------|
-| `= H1 =` through `====== H6 ======` | Headings (levels 1-6) | `Heading` |
-| `* item` | Unordered list (`**` for nesting) | `List` + `ListItem` |
-| `# item` | Ordered list (`##` for nesting) | `List(ordered: true)` + `ListItem` |
-| `----` | Horizontal rule (4+ dashes) | `HorizontalRule` |
-| ` leading space` | Preformatted text | `Code` |
-| `...
` | Preformatted block | `Code` |
-| Blank line | Paragraph separator | (closes lists, separates paragraphs) |
-
-### Inline
-
-| Syntax | Description | AST Node |
-|--------|-------------|----------|
-| `'''text'''` | Bold | `Bold` |
-| `''text''` | Italic | `Italic` |
-| `'''''text'''''` | Bold + Italic | `Bold` > `Italic` |
-| `[[Page]]` | Internal link | `Url(href: "Page")` |
-| `[[Page\|display]]` | Internal link with display text | `Url(href: "Page")` |
-| `[url text]` | External link | `Url(href: url)` |
-| `...` | Inline code (raw) | `Code` |
-| `... ` | Escape wiki markup (raw) | `Text` |
-| `...` / `...` | Strikethrough | `Strikethrough` |
-| `...` / `...` | Underline | `Underline` |
-| `...` | Superscript | `Superscript` |
-| `...` | Subscript | `Subscript` |
-| `
` | Line break | `LineBreak` |
-
-## Usage
-
-### Basic Usage
-
-```ruby
-parser = Markbridge::Parsers::MediaWiki::Parser.new
-ast = parser.parse("'''bold''' and ''italic''")
-```
-
-### With Custom Tags
-
-Register additional HTML-like tags via the `InlineTagRegistry`:
-
-```ruby
-# Block form (extends defaults)
-parser = Markbridge::Parsers::MediaWiki::Parser.new do |registry|
- registry.register("mark", :formatting, Markbridge::AST::Bold)
- registry.register("tt", :raw, Markbridge::AST::Code)
-end
-ast = parser.parse("highlighted")
-
-# Or pass a pre-built registry
-registry = Markbridge::Parsers::MediaWiki::InlineTagRegistry.build_from_default do |r|
- r.register("mark", :formatting, Markbridge::AST::Bold)
-end
-parser = Markbridge::Parsers::MediaWiki::Parser.new(handlers: registry)
-```
-
-### Via Top-Level API
-
-```ruby
-# Parse to AST
-ast = Markbridge.parse_mediawiki("== Hello ==\nWorld")
-
-# Convert to Markdown
-markdown = Markbridge.mediawiki_to_markdown("'''bold''' text")
-```
-
-## InlineTagRegistry
-
-The `InlineTagRegistry` controls how HTML-like tags (`... `) are handled within inline content. It supports three tag types:
-
-| Type | Behavior | Example |
-|------|----------|---------|
-| `:raw` | Content preserved verbatim, not parsed for wiki markup | ``, `` |
-| `:formatting` | Content IS parsed for nested wiki markup | ``, ``, `` |
-| `:self_closing` | No content, produces a leaf AST node | `
` |
-
-### Registration API
-
-```ruby
-registry = Markbridge::Parsers::MediaWiki::InlineTagRegistry.new
-
-# Raw tag (content not parsed)
-registry.register("code", :raw, Markbridge::AST::Code)
-
-# Special: nowiki uses nil element_class (content becomes literal text)
-registry.register("nowiki", :raw, nil)
-
-# Formatting tag (content parsed for wiki markup)
-registry.register("s", :formatting, Markbridge::AST::Strikethrough)
-
-# Self-closing tag
-registry.register("br", :self_closing, Markbridge::AST::LineBreak)
-
-# Query
-registry.known?("code") # => true
-registry["code"] # => Entry(type: :raw, element_class: Code)
-```
-
-## Components
-
-### Parser (`parser.rb`)
-
-The main entry point. Splits input into lines, classifies each line (heading, list, preformatted, blank, or content), and builds block-level AST nodes. Delegates inline content to `InlineParser`.
-
-### InlineParser (`inline_parser.rb`)
-
-Character-by-character parser for inline content within a single line. Handles apostrophe-based formatting, bracket links, and HTML-like tags (via registry). Creates new `InlineParser` instances recursively for nested content, with depth limiting at `MAX_INLINE_DEPTH` (20).
-
-### InlineTagRegistry (`inline_tag_registry.rb`)
-
-Registry mapping HTML-like tag names to their handling type and AST node class. Provides `default` and `build_from_default` factory methods.
-
-## Limitations
-
-- **Templates** (`{{template}}`) are not supported
-- **Tables** (`{| ... |}`) are not supported
-- **Categories** (`[[Category:...]]`) are treated as regular links
-- **Magic words** (`__TOC__`, `__NOTOC__`) are not recognized
-- **Nested formatting edge cases** — MediaWiki's apostrophe-based formatting has complex disambiguation rules that are only partially implemented
diff --git a/docs/parsers/text_formatter.md b/docs/parsers/text_formatter.md
deleted file mode 100644
index 2a3d330c..00000000
--- a/docs/parsers/text_formatter.md
+++ /dev/null
@@ -1,558 +0,0 @@
-# TextFormatter Parser Guide
-
-This guide explains how the TextFormatter parser converts s9e/TextFormatter XML format into the Markbridge AST. This parser is designed specifically for phpBB 3.2+ migrations and other forum software using the s9e/TextFormatter library.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Quick Start](#quick-start)
-- [s9e/TextFormatter Format](#s9etextformatter-format)
-- [Supported Elements](#supported-elements)
-- [Parser Components](#parser-components)
-- [Handlers](#handlers)
-- [Configuration](#configuration)
-- [Examples](#examples)
-
-## Overview
-
-The TextFormatter parser (`Markbridge::Parsers::TextFormatter::Parser`) parses XML output from the [s9e/TextFormatter](https://github.com/s9e/TextFormatter) library, which is used by phpBB 3.2+ and other modern forum software.
-
-**Key Features:**
-- Parses s9e/TextFormatter XML format
-- Handles s9e conventions (uppercase element names, ``/`` markup)
-- Stateless handler API (simple)
-- Lambda handler support
-- Fallback to plain text for invalid XML
-- Built on Nokogiri XML parser
-
-**Dependencies:**
-- Requires the `nokogiri` gem
-
-**Primary Use Case:**
-- phpBB 3.2+ forum migrations
-- Any software using s9e/TextFormatter for BBCode storage
-
-## Quick Start
-
-### Basic Usage
-
-```ruby
-require "markbridge/all"
-
-# Parse s9e/TextFormatter XML to AST
-parser = Markbridge::Parsers::TextFormatter::Parser.new
-xml = '[b]Hello[/b] world! '
-ast = parser.parse(xml)
-
-# Render to Markdown
-renderer = Markbridge::Renderers::Discourse::Renderer.new
-markdown = renderer.render(ast)
-# => "**Hello** world!"
-```
-
-### With Custom Configuration
-
-```ruby
-parser = Markbridge::Parsers::TextFormatter::Parser.new do |registry|
- registry.register("CUSTOM", CustomHandler.new)
-end
-```
-
-## s9e/TextFormatter Format
-
-### Format Overview
-
-s9e/TextFormatter stores BBCode as XML with two formats:
-
-#### Plain Text Format
-```xml
-This is plain text content
-```
-
-#### Rich Text Format
-```xml
-
- bold text
- link
- more text
-
-```
-
-### Special Elements
-
-#### Root Elements
-- `` - Plain text wrapper (no BBCode)
-- `` - Rich text wrapper (contains formatted content)
-
-#### Markup Preservation Elements
-- `` - Start markup (e.g., `[b]`)
-- `` - End markup (e.g., `[/b] `)
-
-These elements preserve the original BBCode for unparsing but are **ignored during parsing**.
-
-#### Line Breaks
-- `
` - Line break element
-
-### Naming Convention
-
-**Element names are UPPERCASE** (s9e convention):
-- `` for bold (not ``)
-- `` for links (not ``)
-- `` for code (not ``)
-
-## Supported Elements
-
-### Formatting Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `` | `AST::Bold` | `bold` |
-| `` | `AST::Italic` | `italic` |
-| `` | `AST::Underline` | `underline` |
-| `` | `AST::Strikethrough` | `strike` |
-
-### Code Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `` | `AST::Code` | `code` |
-
-**Attributes:**
-- `lang` - Language hint for syntax highlighting
-
-### Link Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `` | `AST::Url` | `text ` |
-| `` | `AST::Url` | `text ` |
-
-**Attributes:**
-- `url` - Link destination for URLs
-- `email` - Email address for email links
-
-### List Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `` | `AST::List` | `- item
` |
-| `- ` | `AST::ListItem` | `
- list item
` |
-
-**Attributes:**
-- `type` - List type ("bullet" or "decimal")
-
-### Structure Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `
` | `AST::LineBreak` | `text
more` |
-| `
` | `AST::HorizontalRule` | `
` |
-
-### Additional Elements
-
-| XML Element | AST Node | Example |
-|-------------|----------|---------|
-| `` | `AST::Quote` | `text
` |
-| `
` | `AST::Image` | `
` |
-
-## Parser Components
-
-### Parser
-
-**Location:** `Markbridge::Parsers::TextFormatter::Parser`
-
-**Responsibility:** Parse s9e/TextFormatter XML using Nokogiri
-
-**Key Methods:**
-
-```ruby
-# Main entry point
-ast = parser.parse(xml_string)
-
-# Access unknown tags
-parser.unknown_tags # => {"UNKNOWN" => count}
-
-# Process children (used by handlers)
-parser.process_children(xml_element, ast_parent)
-```
-
-**Parsing Flow:**
-1. Parse XML with Nokogiri::XML
-2. Walk XML tree
-3. Filter out `` and `` elements
-4. Handle root elements (``, ``)
-5. Dispatch elements to handlers
-6. Return completed AST::Document
-
-**Error Handling:**
-- Invalid XML falls back to plain text
-- Parsing errors return content as `AST::Text`
-
-### HandlerRegistry
-
-**Location:** `Markbridge::Parsers::TextFormatter::HandlerRegistry`
-
-**Responsibility:** Map XML element names to handlers
-
-**Key Methods:**
-
-```ruby
-# Default registry
-registry = HandlerRegistry.default
-
-# Custom registry
-registry = HandlerRegistry.new
-registry.register("CUSTOM", CustomHandler.new)
-
-# Build from default
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("CUSTOM", CustomHandler.new)
-end
-```
-
-## Handlers
-
-### Handler Interface
-
-Handlers receive complete XML elements (stateless API):
-
-```ruby
-class CustomHandler < Markbridge::Parsers::TextFormatter::Handlers::BaseHandler
- def initialize(element_class)
- @element_class = element_class
- end
-
- def process(element:, parent:, processor:)
- # element: Nokogiri::XML::Element
- # parent: AST::Element
- # processor: Parser instance
-
- # Extract attributes using helper
- attrs = extract_attributes(element)
-
- ast_element = @element_class.new(custom: attrs[:custom])
- parent << ast_element
- processor.process_children(element, ast_element)
- end
-
- attr_reader :element_class
-end
-```
-
-### Attribute Extraction Helper
-
-BaseHandler provides an `extract_attributes` helper:
-
-```ruby
-def process(element:, parent:, processor:)
- attrs = extract_attributes(element)
- # Returns hash with symbolized keys: { url: "...", lang: "..." }
-
- ast_element = @element_class.new(url: attrs[:url])
- # ...
-end
-```
-
-### Built-In Handlers
-
-#### SimpleHandler
-
-For basic formatting elements:
-
-```ruby
-handler = SimpleHandler.new(AST::Bold)
-registry.register("B", handler)
-```
-
-#### CodeHandler
-
-For code blocks with language support:
-
-```ruby
-handler = CodeHandler.new
-registry.register("CODE", handler)
-```
-
-#### UrlHandler
-
-For links and email addresses:
-
-```ruby
-handler = UrlHandler.new
-registry.register("URL", handler)
-
-email_handler = EmailHandler.new
-registry.register("EMAIL", email_handler)
-```
-
-#### ListHandler
-
-For lists with type detection:
-
-```ruby
-handler = ListHandler.new
-registry.register("LIST", handler)
-```
-
-### Lambda Handlers
-
-For simple cases:
-
-```ruby
-registry.register("HR", ->(element:, parent:, **) {
- parent << AST::HorizontalRule.new
- nil # Return nil to skip children
-})
-```
-
-## Configuration
-
-### Block-Based Configuration
-
-```ruby
-parser = Markbridge::Parsers::TextFormatter::Parser.new do |registry|
- # Add custom handlers
- registry.register("CUSTOM", CustomHandler.new)
- registry.register("SPOILER", SpoilerHandler.new)
-end
-```
-
-### Using build_from_default
-
-```ruby
-registry = HandlerRegistry.build_from_default do |reg|
- reg.register("CUSTOM", CustomHandler.new)
-end
-
-parser = Parser.new(handlers: registry)
-```
-
-## Examples
-
-### Plain Text
-
-```ruby
-parser = Parser.new
-xml = 'This is plain text '
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# └─ Text("This is plain text")
-```
-
-### Basic Formatting
-
-```ruby
-xml = 'Bold and italic '
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# ├─ Bold
-# │ └─ Text("Bold")
-# ├─ Text(" and ")
-# └─ Italic
-# └─ Text("italic")
-```
-
-### With Markup Preservation
-
-```ruby
-xml = '[b]Hello[/b] world! '
-ast = parser.parse(xml)
-
-# and elements are ignored during parsing
-# AST structure:
-# Document
-# ├─ Bold
-# │ └─ Text("Hello")
-# └─ Text(" world!")
-```
-
-### Links
-
-```ruby
-xml = 'Example '
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# └─ Url(href: "https://example.com")
-# └─ Text("Example")
-```
-
-### Code Blocks
-
-```ruby
-xml = '[code=ruby]def hello
- puts "world"
-end[/code] '
-
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# └─ Code(language: "ruby")
-# └─ Text("def hello\n puts \"world\"\nend")
-```
-
-### Lists
-
-```ruby
-xml = '- First
- Second
'
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# └─ List(ordered: false)
-# ├─ ListItem
-# │ └─ Text("First")
-# └─ ListItem
-# └─ Text("Second")
-```
-
-### Nested Elements
-
-```ruby
-xml = 'Bold with italic inside '
-ast = parser.parse(xml)
-
-# AST structure:
-# Document
-# └─ Bold
-# ├─ Text("Bold with ")
-# ├─ Italic
-# │ └─ Text("italic")
-# └─ Text(" inside")
-```
-
-### Invalid XML
-
-```ruby
-parser = Parser.new
-xml = 'This is not valid XML content bold '
-ast = parser.parse(xml)
-
-parser.unknown_tags # => {"UNKNOWN" => 1}
-
-# AST structure:
-# Document
-# ├─ Text("content ") # Unknown wrapper ignored
-# └─ Bold
-# └─ Text("bold")
-```
-
-### Custom Handler
-
-```ruby
-class SpoilerHandler < BaseHandler
- def initialize
- @element_class = AST::Spoiler
- end
-
- def process(element:, parent:, processor:)
- spoiler = AST::Spoiler.new
- parent << spoiler
- processor.process_children(element, spoiler)
- end
-
- attr_reader :element_class
-end
-
-parser = Parser.new do |registry|
- registry.register("SPOILER", SpoilerHandler.new)
-end
-
-xml = 'Hidden content '
-ast = parser.parse(xml)
-```
-
-## phpBB Migration Notes
-
-### phpBB Version Support
-
-- **phpBB 3.2+** - Uses s9e/TextFormatter (use this parser)
-- **phpBB 3.1 and earlier** - Uses traditional BBCode (use BBCode parser)
-
-### Getting XML from phpBB
-
-phpBB stores messages in XML format in the `message` column:
-
-```sql
-SELECT post_id, message FROM phpbb_posts WHERE post_id = 123;
-```
-
-Example output:
-```xml
-
- [b]Hello[/b] world!
-
-```
-
-### Migration Workflow
-
-```ruby
-require "markbridge/all"
-
-parser = Markbridge::Parsers::TextFormatter::Parser.new
-renderer = Markbridge::Renderers::Discourse::Renderer.new
-
-# Fetch posts from phpBB
-posts = database.execute("SELECT post_id, message FROM phpbb_posts")
-
-posts.each do |post|
- # Parse s9e XML
- ast = parser.parse(post[:message])
-
- # Render to Discourse Markdown
- markdown = renderer.render(ast)
-
- # Save to Discourse
- discourse_post = DiscoursePost.create!(
- content: markdown,
- # ... other fields
- )
-end
-```
-
-## Comparison with Other Parsers
-
-| Feature | TextFormatter | BBCode | HTML |
-|---------|---------------|--------|------|
-| Input format | s9e/TextFormatter XML | BBCode | HTML |
-| Dependencies | Nokogiri | None | Nokogiri |
-| Handler complexity | Low (stateless) | High (stateful) | Low (stateless) |
-| Element names | UPPERCASE | lowercase | lowercase |
-| Malformed input | Fallback to text | Custom logic | Nokogiri handles |
-| Lambda support | ✓ | ✗ | ✓ |
-| Primary use case | phpBB 3.2+ | Forum migrations | HTML content |
-
-**When to use TextFormatter Parser:**
-- phpBB 3.2+ migrations
-- Software using s9e/TextFormatter
-- You have s9e XML format
-
-**When to use other parsers:**
-- **BBCode Parser** - Traditional BBCode, no dependencies, fine control
-- **HTML Parser** - HTML content, web scraping
-
-## Next Steps
-
-- **[BBCode Parser Guide](bbcode.md)** - Compare with BBCode parser
-- **[HTML Parser Guide](html.md)** - Compare with HTML parser
-- **[Parser Comparison](comparison.md)** - Detailed comparison of all parsers
-- **[Discourse Renderer Guide](../renderers/discourse.md)** - Learn about rendering
-- **[Extending Markbridge](../extending.md)** - Add custom handlers
diff --git a/docs/performance.md b/docs/performance.md
deleted file mode 100644
index d1548a27..00000000
--- a/docs/performance.md
+++ /dev/null
@@ -1,659 +0,0 @@
-# Performance Guide
-
-This guide covers performance considerations, optimization techniques, and best practices for using Markbridge efficiently.
-
-## Table of Contents
-
-- [Overview](#overview)
-- [Algorithmic Complexity](#algorithmic-complexity)
-- [Parser Performance](#parser-performance)
-- [Renderer Performance](#renderer-performance)
-- [Memory Optimization](#memory-optimization)
-- [Benchmarking](#benchmarking)
-- [Best Practices](#best-practices)
-
-## Overview
-
-Markbridge is designed for performance with these principles:
-
-- **O(n) parsing** - Single pass through input, byte-offset scanning
-- **Minimal allocations** - Shared frozen defaults, copy-on-write text
- nodes, no per-character strings
-- **Bounded operations** - Depth limits prevent runaway behavior
-- **Streaming support** - Constant memory for large documents
-
-**Performance characteristics:**
-- Small documents (< 1 KB): ~1 ms
-- Medium documents (10-100 KB): ~10-100 ms
-- Large documents (> 1 MB): Use streaming renderer
-
-## Algorithmic Complexity
-
-### Parser
-
-| Operation | Complexity | Notes |
-|-----------|-----------|-------|
-| Scanning | O(n) | Single pass through input |
-| Token processing | O(1) per token | Constant time dispatch |
-| Tag lookup | O(1) | Hash-based registry |
-| Auto-close | O(k) where k ≤ 5 | Bounded depth search |
-| Reordering | O(k) where k ≤ 5 | Bounded peek-ahead |
-| Overall | O(n) | Linear in input size |
-
-### Renderer
-
-| Operation | Complexity | Notes |
-|-----------|-----------|-------|
-| Tree traversal | O(n) | Visit each node once |
-| Tag lookup | O(1) | Hash-based library |
-| Parent lookup | O(depth) | Linear scan of the parent chain |
-| Child rendering | O(children) | Linear in child count |
-| Overall | O(n) | Linear in AST size (depth is bounded by MAX_DEPTH) |
-
-## Parser Performance
-
-### Scanner Optimizations
-
-The Scanner is the performance-critical path. Follow these patterns:
-
-#### 1. Byte Offsets, Not Character Indices
-
-CRuby has no character-index cache: on any string containing a multibyte
-character, `str[pos]` and `str.index(pattern, pos)` walk bytes from the
-start of the string — O(pos) *per call* and superlinear per document.
-The Scanner therefore works entirely in byte offsets.
-
-**Good: byte-domain access**
-```ruby
-byte = @input.getbyte(@pos) # Integer, no allocation, O(1)
-idx = @input.byteindex("[", @pos) # C-speed skip to the next tag
-text = @input.byteslice(start, len) # one slice for the whole span
-```
-
-**Avoid: character-domain access and per-char strings**
-```ruby
-char = @input[@pos] # allocates a String per char,
- # O(pos) on multibyte input
-idx = @input.index("[", @pos) # O(pos) on multibyte input
-```
-
-Rules that keep this correct:
-
-- **Boundary invariant**: every offset must sit on a character
- boundary. Held structurally — `byteindex` jumps land on ASCII
- matches; manual advances step over ASCII bytes. Never mix `getbyte`
- with character indices (a half-measure that corrupts scans whenever
- multibyte text precedes a construct).
-- **`byteindex` returns `0` for a match at the start — falsy-looking.**
- Nil-check, never truthiness-check.
-- **Classify bytes with integer range checks** (`byte >= LOWER_A &&
- byte <= LOWER_Z`), not regexes over one-char strings. A byte >= 0x80
- (multibyte lead/continuation) never satisfies an ASCII predicate, so
- no encoding-awareness is needed at probe sites.
-- **Small extracted strings stay character-domain** — only offsets into
- the scanned input change meaning.
-
-#### 2. Bounded Backtracking
-
-**Good: Save position, attempt parse, restore**
-```ruby
-start_pos = @pos
-
-if parse_tag_at_cursor
- return tag
-else
- @pos = start_pos # Backtrack to start
-end
-```
-
-**Avoid: Creating new scanner instances**
-```ruby
-# Creates new scanner object
-backup = Scanner.new(@input[@pos..])
-```
-
-#### 3. Minimal Allocations
-
-**Good: Reuse buffers**
-```ruby
-@buffer ||= String.new
-@buffer.clear
-@buffer << char
-```
-
-**Avoid: Creating many temporary strings**
-```ruby
-result = "" + char + another_char # Multiple string objects
-```
-
-#### 4. Regex Only Where the Regex Engine Does the Work
-
-**Good: let the regex engine skip spans in C**
-```ruby
-UNQUOTED_VALUE_STOP = /[\[\]\s]/
-stop = @input.byteindex(UNQUOTED_VALUE_STOP, @pos) || @length
-```
-
-**Good: integer comparison for single bytes and classes**
-```ruby
-byte == BRACKET_CLOSE
-byte >= DIGIT_0 && byte <= DIGIT_9
-```
-
-**Avoid: per-position regex probes (allocates a string per probe)**
-```ruby
-@input[@pos].match?(/[a-z*]/i) # a String + a regex match per character
-```
-
-### Handler Optimizations
-
-#### Keep create_element Fast
-
-`create_element` is called for every opening tag. Optimize it:
-
-**Good: Simple attribute extraction**
-```ruby
-def create_element(token)
- author = token.attrs[:author] || token.attrs[:option]
- AST::Quote.new(author:)
-end
-```
-
-**Avoid: Complex processing**
-```ruby
-def create_element(token)
- # Expensive operations
- author = token.attrs[:author]&.strip&.downcase&.titleize
- validate_author(author)
- AST::Quote.new(author:)
-end
-```
-
-#### Use SimpleHandler When Possible
-
-`SimpleHandler` is optimized for common cases:
-
-```ruby
-# Fast path - reuses optimized handler
-handler = SimpleHandler.new(AST::Bold, auto_closeable: true)
-
-# Only subclass when you need custom behavior
-class CustomHandler < SimpleHandler
- def create_element(token)
- # Custom logic
- end
-end
-```
-
-### Closing Strategy Performance
-
-**Strict Strategy:**
-- No look-ahead overhead
-- O(k) where k ≤ 5 for auto-close
-- Fastest option
-
-**Reordering Strategy:**
-- Look-ahead overhead (peek 5 tokens)
-- O(k) where k ≤ 5 for reordering
-- Slightly slower but more forgiving
-
-**Benchmark:**
-```ruby
-require "benchmark/ips"
-
-Benchmark.ips do |x|
- x.report("strict") do
- parser = Parser.new(closing_strategy: :strict)
- parser.parse(bbcode)
- end
-
- x.report("reordering") do
- parser = Parser.new # Default reordering
- parser.parse(bbcode)
- end
-
- x.compare!
-end
-
-# Results (typical):
-# strict: 100.0 i/s
-# reordering: 95.0 i/s - 1.05x slower
-```
-
-**Recommendation:** Use reordering unless profiling shows bottleneck.
-
-### Depth Limits
-
-Depth limits prevent performance degradation:
-
-**Max depth: 100**
-- Prevents stack overflow
-- Keeps auto-close bounded
-- Realistic BBCode rarely exceeds 10-20
-
-**Max auto-close: 5**
-- Bounded search = O(5) = O(1)
-- Prevents expensive deep searches
-- Most formatting nesting is shallow
-
-## Renderer Performance
-
-### Context Caching (November 2025)
-
-**Major performance improvement!**
-
-#### Before: O(depth) Parent Lookup
-
-```ruby
-def find_parent(klass)
- @parents.reverse.find { |p| p.is_a?(klass) }
-end
-
-# With 50 nested elements:
-# - Each lookup scans up to 50 elements
-# - O(depth) complexity
-```
-
-#### After: O(1) Parent Lookup
-
-```ruby
-def find_parent(klass)
- @parent_cache[klass]&.last
-end
-
-# With 50 nested elements:
-# - Single hash lookup
-# - O(1) complexity
-# - 5x-10x faster!
-```
-
-**Benchmark:**
-```ruby
-# Deep nesting (50 levels)
-context_uncached = create_deep_context(50, use_cache: false)
-context_cached = create_deep_context(50, use_cache: true)
-
-Benchmark.ips do |x|
- x.report("uncached lookup") do
- context_uncached.find_parent(AST::List)
- end
-
- x.report("cached lookup") do
- context_cached.find_parent(AST::List)
- end
-
- x.compare!
-end
-
-# Results:
-# uncached: 100.0 i/s
-# cached: 1000.0 i/s - 10.0x faster
-```
-
-### Tag Rendering Optimizations
-
-#### Avoid Redundant Tree Walks
-
-**Good: Single pass with context**
-```ruby
-def render(element, interface)
- child_context = interface.with_parent(element)
- content = interface.render_children(element, context: child_context)
- format_with_context(content, interface)
-end
-```
-
-**Avoid: Multiple traversals**
-```ruby
-def render(element, interface)
- count = count_children(element) # Walk 1
- depth = calculate_depth(element) # Walk 2
- content = interface.render_children(element) # Walk 3
-end
-```
-
-#### Reuse Strings
-
-**Good: Build strings efficiently**
-```ruby
-result = String.new
-result << prefix
-result << content
-result << suffix
-result
-```
-
-**Better: Use interpolation for small strings**
-```ruby
-"#{prefix}#{content}#{suffix}"
-```
-
-**Avoid: Excessive concatenation**
-```ruby
-result = ""
-result = result + prefix
-result = result + content
-result = result + suffix
-```
-
-### AST Optimizations
-
-#### Text Node Merging
-
-**Automatic optimization:**
-```ruby
-element << AST::Text.new("Hello ")
-element << AST::Text.new("World")
-# Automatically merged to single Text("Hello World")
-```
-
-**Benefit:**
-- Fewer nodes to traverse
-- Less memory allocation
-- Faster rendering
-
-**How it works:**
-```ruby
-# In Element#<<
-if child.is_a?(Text) && @children.last.is_a?(Text)
- @children.last.append(child.text)
-else
- @children << child
-end
-```
-
-## Memory Notes
-
-The renderer builds a single string in memory. For typical inputs (hundreds of KB), this is fine. If you process very large documents, you can stream the resulting string to disk yourself or process in chunks upstream.
-
-### Object Pooling
-
-For high-throughput scenarios, consider pooling:
-
-```ruby
-class ParserPool
- def initialize(size: 10)
- @pool = Array.new(size) { Parser.new }
- @mutex = Mutex.new
- end
-
- def parse(input)
- parser = acquire
- begin
- parser.parse(input)
- ensure
- release(parser)
- end
- end
-
- private
-
- def acquire
- @mutex.synchronize { @pool.pop || Parser.new }
- end
-
- def release(parser)
- @mutex.synchronize { @pool.push(parser) if @pool.size < 10 }
- end
-end
-```
-
-### Frozen Strings
-
-**Always use frozen string literal comment:**
-
-```ruby
-# frozen_string_literal: true
-```
-
-**Benefits:**
-- Reduces memory allocations
-- Strings are immutable by default
-- Better performance in Ruby 3.x
-
-## Benchmarking
-
-### Repo instruments
-
-Three scripts live in `bench/` — use them before and after any change to
-the parsing or rendering hot paths:
-
-```bash
-bundle exec ruby --yjit bench/bench.rb # per-feature micro-inputs
-bundle exec ruby --yjit bench/corpus_bench.rb # ~1 KB forum posts, isolating variants
-bundle exec ruby bench/alloc_profile.rb # allocations ranked by file:line
-```
-
-`corpus_bench.rb` localizes cost by differencing variants (`fresh` minus
-`shared` = per-call setup; `parse_only` minus `scan_only` = handler/AST
-cost) and runs an ASCII and a multibyte corpus — always compare both,
-character-index pathologies only show up on multibyte input.
-`alloc_profile.rb` names the exact allocation sites; profile before
-optimizing, the ranking is usually not what intuition predicts. Note
-that wall-clock benchmarks understate allocation wins: fewer allocations
-also mean fewer GC cycles in real workloads.
-
-### Setup
-
-```ruby
-require "benchmark/ips"
-require "benchmark/memory"
-require "markbridge/all"
-
-# Sample inputs
-SMALL_INPUT = "[b]Hello[/b]" * 10
-MEDIUM_INPUT = File.read("medium_sample.bbcode") # ~10 KB
-LARGE_INPUT = File.read("large_sample.bbcode") # ~1 MB
-```
-
-### Throughput Benchmark
-
-```ruby
-Benchmark.ips do |x|
- x.config(time: 5, warmup: 2)
-
- x.report("small docs") do
- Markbridge.bbcode_to_markdown(SMALL_INPUT)
- end
-
- x.report("medium docs") do
- Markbridge.bbcode_to_markdown(MEDIUM_INPUT)
- end
-
- x.compare!
-end
-```
-
-### Memory Benchmark
-
-```ruby
-Benchmark.memory do |x|
- x.report("parse + render") do
- Markbridge.bbcode_to_markdown(MEDIUM_INPUT)
- end
-
- x.report("parse only") do
- parser = Parser.new
- parser.parse(MEDIUM_INPUT)
- end
-
- x.report("render only") do
- # Pre-parsed AST
- renderer = Renderer.new
- renderer.render(pre_parsed_ast)
- end
-
- x.compare!
-end
-```
-
-### Component Profiling
-
-```ruby
-require "ruby-prof"
-
-RubyProf.start
-
-1000.times do
- Markbridge.bbcode_to_markdown(MEDIUM_INPUT)
-end
-
-result = RubyProf.stop
-
-printer = RubyProf::FlatPrinter.new(result)
-printer.print(STDOUT)
-```
-
-## Best Practices
-
-### DO
-
-✓ **Reuse parser and renderer instances**
-```ruby
-parser = Parser.new
-renderer = Renderer.new
-
-documents.each do |doc|
- ast = parser.parse(doc)
- markdown = renderer.render(ast)
-end
-```
-
-✓ **Use cached context (automatic in November 2025)**
-```ruby
-# Context caching is automatic - just use it normally
-if interface.has_parent?(AST::List)
- # O(1) lookup via cache
-end
-```
-
-✓ **Keep handlers simple**
-```ruby
-class FastHandler < SimpleHandler
- def create_element(token)
- # Minimal processing
- AST::Element.new(attr: token.attrs[:attr])
- end
-end
-```
-
-✓ **Profile before optimizing**
-```ruby
-# Measure first
-Benchmark.ips do |x|
- x.report("current") { current_implementation }
- x.report("optimized") { optimized_implementation }
- x.compare!
-end
-```
-
-### DON'T
-
-✗ **Don't create parser/renderer per document**
-```ruby
-# Bad - creates new objects for each document
-documents.each do |doc|
- parser = Parser.new # Wasteful!
- renderer = Renderer.new # Wasteful!
- Markbridge.convert(doc, parser:, renderer:)
-end
-```
-
-✗ **Don't walk tree multiple times**
-```ruby
-# Bad
-def render(element, interface)
- count = element.children.count { |c| c.is_a?(Text) }
- depth = element.children.count { |c| c.is_a?(List) }
- # Two passes!
-end
-
-# Good
-def render(element, interface)
- text_count = 0
- list_count = 0
- element.children.each do |c|
- text_count += 1 if c.is_a?(Text)
- list_count += 1 if c.is_a?(List)
- end
- # One pass
-end
-```
-
-✗ **Don't disable caching**
-```ruby
-# Don't do this - caching is automatic and beneficial
-# There's no reason to disable it
-```
-
-✗ **Don't create excessive temporary objects**
-```ruby
-# Bad
-array1 = []
-array2 = []
-combined = array1 + array2 # Creates new array
-
-# Good
-array1.concat(array2) # Mutates array1, no allocation
-```
-
-### Configuration Tuning
-
-#### For Throughput
-
-```ruby
-# Fastest configuration
-parser = Parser.new do |registry|
- # Use strict strategy (no look-ahead)
- reconciler = ClosingStrategies::TagReconciler.new(registry: registry)
- registry.closing_strategy = ClosingStrategies::Strict.new(reconciler)
-end
-
-renderer = Renderer.new # Uses cached context by default
-```
-
-#### For Forgiveness
-
-```ruby
-# Most forgiving (slight performance cost)
-parser = Parser.new # Uses reordering strategy by default
-renderer = Renderer.new
-```
-
-## Performance Checklist
-
-Before deploying, verify:
-
-- [ ] Using reused parser/renderer instances
-- [ ] Handlers keep `create_element` fast
-- [ ] Tags avoid redundant tree walks
-- [ ] Context caching enabled (automatic)
-- [ ] Frozen string literals used (`# frozen_string_literal: true`)
-- [ ] Profiled actual workload
-- [ ] Memory usage acceptable
-- [ ] Throughput meets requirements
-
-## Typical Performance
-
-**Reference hardware:** M1 MacBook Pro, Ruby 3.2
-
-| Document Size | Parse Time | Render Time | Total Time | Memory |
-|---------------|------------|-------------|------------|--------|
-| 1 KB | 0.5 ms | 0.3 ms | 0.8 ms | 50 KB |
-| 10 KB | 3 ms | 2 ms | 5 ms | 300 KB |
-| 100 KB | 25 ms | 15 ms | 40 ms | 2 MB |
-| 1 MB | 250 ms | 150 ms | 400 ms | 15 MB |
-| 10 MB (stream) | 2.5 s | 1.5 s | 4 s | 20 MB |
-
-**Note:** Actual performance varies based on:
-- BBCode complexity (deep nesting slower)
-- Tag distribution (code blocks faster than lists)
-- Hardware (CPU, memory speed)
-- Ruby version (3.4 faster than 3.2)
-
-## Next Steps
-
-- **[Architecture Overview](architecture.md)** - Understand the design
-- **[BBCode Parser Guide](parsers/bbcode.md)** - Learn about parsing
-- **[Discourse Renderer Guide](renderers/discourse.md)** - Learn about rendering
-- **[Extending Markbridge](extending.md)** - Add custom tags efficiently
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
new file mode 100644
index 00000000..a6fc36ce
--- /dev/null
+++ b/docs/pnpm-lock.yaml
@@ -0,0 +1,3582 @@
+---
+lockfileVersion: '9.0'
+
+importers:
+
+ .:
+ configDependencies: {}
+ packageManagerDependencies:
+ pnpm:
+ specifier: 12.4.2
+ version: 12.4.2
+
+packages:
+
+ '@pnpm/exe.android-arm64@12.4.2':
+ resolution: {integrity: sha512-E255MbcQ0V1577M2BV0ajWuP7KmTPYA0jqZnk3rK6Lq3kTvZWulMMb7iqRmnLsQbyWTkMEB2CfyM70loVDA8xg==}
+ cpu: [arm64]
+ os: [android]
+
+ '@pnpm/exe.android-x64@12.4.2':
+ resolution: {integrity: sha512-J1pSeCUwuKxMG70ZzpWn8JzElxiEa8NN/3N0SlZRtU2xbztChaJCKF3HaBNIj9yPfeofWzJo/lwNvDDjraQuZw==}
+ cpu: [x64]
+ os: [android]
+
+ '@pnpm/exe.darwin-arm64@12.4.2':
+ resolution: {integrity: sha512-A0WDo8iErfZBXgrLseQxw8i8Y9ctUpOEl/Uu+cubnTzpD8tT9ykIB548L8YTM2WD4OS+ZOHSxy8aGZcvKq8PaQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pnpm/exe.darwin-x64@12.4.2':
+ resolution: {integrity: sha512-MSgJdovBWHcb5DEOvfPH9yNi/T5O1Xa4ess+E1ESGo/5yuty7S4JoiIjami+fsNXfnoQMlwsMUqYU4zAvBcNCw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pnpm/exe.freebsd-x64@12.4.2':
+ resolution: {integrity: sha512-h2YumlQSNvgbRPv+RXwABohX65f9bOBZn+jMIt7bFDISZPCzQ+Nvpt6Awbp4ip5PwQgYxbu5iREJ1fHE39Fm8A==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pnpm/exe.linux-arm64-musl@12.4.2':
+ resolution: {integrity: sha512-LwSEtSEDTv6S51YLs3YvSkPyun/QmfMic1UGICUkPWFu6ByP43RdMlkKvmVkfGhAYCpnxO057vrmyJqtfZrPCA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-arm64@12.4.2':
+ resolution: {integrity: sha512-2dSiDXyhx+RTHsewxex8f/jVjqQXWJ2oow4kCVHEWdZKeBpgMxvZ6fHkTAUBJXgqhbKIDHvuNlZBP7gJfUWL5A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-ppc64@12.4.2':
+ resolution: {integrity: sha512-8Itc+jQk+MTz04LS9D1RcH+VctAWmzM4l1cJQ+Sx7pAJNo7EKHdRMbC0Tok1jyQ7eEHNJZD5EleYneZqWfZM+g==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-riscv64@12.4.2':
+ resolution: {integrity: sha512-hleOeqhTVpH+z9RVMGnxvU4ZnrkBUClWjCbHD9u6kwyqhGSpevoU1wTGish+CBRhmIgMAy9pAfFpqhbAKOKNfw==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-s390x@12.4.2':
+ resolution: {integrity: sha512-LAsQRRdP9aToENR6dtcIJ9l+e1zMYOX0tQcLGpRyLPVBQcYRLlvAPcmDshsiIHQjp05SDa9FI0czX1ZQ1a7a/A==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-x64-musl@12.4.2':
+ resolution: {integrity: sha512-kzfzH2/0BWdTABK14Yj5a1xsdkTEQUp2eXEPNakaD9jKL025lq3hyaKHIz/gIZaPDMe/1bFFK/En4ztFlbBJxw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-x64@12.4.2':
+ resolution: {integrity: sha512-/pbt0UVTa8NMDhzOWLQRfZ6G9ROKXlJPZtx845BqyWfc7hCrWXhTLBd70yO2y8+E+IWN3oHM1s/JsIQNGk1yvg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.win32-arm64@12.4.2':
+ resolution: {integrity: sha512-PsW19e4dAUNpZ0cS9flaxFuAmpt2dKlH/Vvi8TZ4qyJjjQzue/CEsWm+6wKVP4CJ7IT8RdL4qh+soOPWIgF2Zw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pnpm/exe.win32-x64@12.4.2':
+ resolution: {integrity: sha512-+xGoeE0g55ztWvl8i5QqdmNfW3nIrTVcoQrNshEOwxthm9Ag48oXton3uxOA3/SANyz5AXexEjj2KO20NnOrEw==}
+ cpu: [x64]
+ os: [win32]
+
+ pnpm@12.4.2:
+ resolution: {integrity: sha512-CK3GYTGAJ1x8ntraOdzwjJxhrU5+rzMKTzRh8QKw+QdCNFTRF/mOctR/7wYWBwZE17/8lzpqV/UJCm18NosHyQ==}
+ engines: {node: '>=18.*'}
+ hasBin: true
+
+snapshots:
+
+ '@pnpm/exe.android-arm64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.android-x64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.darwin-arm64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.darwin-x64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.freebsd-x64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-arm64-musl@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-arm64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-ppc64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-riscv64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-s390x@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-x64-musl@12.4.2':
+ optional: true
+
+ '@pnpm/exe.linux-x64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.win32-arm64@12.4.2':
+ optional: true
+
+ '@pnpm/exe.win32-x64@12.4.2':
+ optional: true
+
+ pnpm@12.4.2:
+ optionalDependencies:
+ '@pnpm/exe.android-arm64': 12.4.2
+ '@pnpm/exe.android-x64': 12.4.2
+ '@pnpm/exe.darwin-arm64': 12.4.2
+ '@pnpm/exe.darwin-x64': 12.4.2
+ '@pnpm/exe.freebsd-x64': 12.4.2
+ '@pnpm/exe.linux-arm64': 12.4.2
+ '@pnpm/exe.linux-arm64-musl': 12.4.2
+ '@pnpm/exe.linux-ppc64': 12.4.2
+ '@pnpm/exe.linux-riscv64': 12.4.2
+ '@pnpm/exe.linux-s390x': 12.4.2
+ '@pnpm/exe.linux-x64': 12.4.2
+ '@pnpm/exe.linux-x64-musl': 12.4.2
+ '@pnpm/exe.win32-arm64': 12.4.2
+ '@pnpm/exe.win32-x64': 12.4.2
+
+---
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ '@astrojs/starlight':
+ specifier: ^0.42.1
+ version: 0.42.1(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5))
+ astro:
+ specifier: ^7.3.3
+ version: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5)
+ sharp:
+ specifier: ^0.35.4
+ version: 0.35.4(@types/node@24.13.5)
+
+packages:
+
+ '@astrojs/compiler-binding-android-arm64@0.4.1':
+ resolution: {integrity: sha512-Wtx2DZORuNTLASLLZ8hlrjFaJe1LUloatcCn9EPq7knBhRpjMGXeJANIJMeBZG4igvz7x+3nyAJM2iwCSW9xog==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@astrojs/compiler-binding-darwin-arm64@0.4.1':
+ resolution: {integrity: sha512-7iwXcU+hB60Y1SvOfnWHNioT72gDsDQKaDipXh2Onwp/TvQMozhbvhy13wJR8Af940a0h6PbddCeJFpj791UDA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@astrojs/compiler-binding-darwin-x64@0.4.1':
+ resolution: {integrity: sha512-pifjICos49RXhSVKSDu5gDB0/8j0O7DmxSp1DaWW2PyLG5ekwSkcrfOZto5JqhdI7QYMZB3XOiXpsdOXpS4npA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@astrojs/compiler-binding-linux-arm64-gnu@0.4.1':
+ resolution: {integrity: sha512-hsZMlNWc6LqWHh9LlSwTq1/XCY7pxA5rkkKIdZRV8mgbOz9OwLvjEoe5Jry13eIZjMUQO8XnQyzk8o/Pjw2YFA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@astrojs/compiler-binding-linux-arm64-musl@0.4.1':
+ resolution: {integrity: sha512-a3EW27f+Z9uf57vYmpLgUriA1y9wDhKz57PT8D8XlFZt2HZEWhUxKuxI1jvKVI9p8okYee11WXEm/FHY1G9yoQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@astrojs/compiler-binding-linux-x64-gnu@0.4.1':
+ resolution: {integrity: sha512-XruPTKB/SS4DFzeAjQ1ZyM0ieHDjapHkOSnnzRApZ4FXY2YAI7UY3mETjfJiIS/acyZyrnPH5jObv1gATTdUJQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@astrojs/compiler-binding-linux-x64-musl@0.4.1':
+ resolution: {integrity: sha512-E8j7lGanqNpudtkUbsFaOLJhmwFe4Ecqux7K9hkj4wULIPNZRM2bYZNcMSj8r1oWvaUiwZl8LaaNwv3+14jqhw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@astrojs/compiler-binding-wasm32-wasi@0.4.1':
+ resolution: {integrity: sha512-vNLxIv41ZE+nmPFtw24cRW5ZXG+ZPcAGtgigtjCP8lvcxKI7u2GrXoMsCgEH5fMw8WCEJpwLvsVBg15rJKAkyA==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@astrojs/compiler-binding-win32-arm64-msvc@0.4.1':
+ resolution: {integrity: sha512-/xTDgRsFDoTh0/+KFXJ74DY4Jqn8CoSHwa8UP5RAblHfnRHkenwfSl4zYDET61O9vXKLk09UzHjjq09yP20/AQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@astrojs/compiler-binding-win32-x64-msvc@0.4.1':
+ resolution: {integrity: sha512-0x5J6iHZO0Fjo/CVzoIbXKzWnGESftDbEUQ/oXQ61HhUAFoR7+T1jfToZSNAS+gYyBXHSTpMWYC7lzarbhClcg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@astrojs/compiler-binding@0.4.1':
+ resolution: {integrity: sha512-ZYVDW1P58OXyxqZbOMp+/2U9KA7y2esIx9r4pWdpZ/l6fCZhaw4ul+D7EgpRVxYLkyTWqzANbTTUFGPFP8zDHA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ '@astrojs/compiler-rs@0.4.1':
+ resolution: {integrity: sha512-//NmAuRhy7eU9iP0ceI5Pymy55M+nUZ//kS6XeOyhBWdDib2KbJOyFaVJ52k7sDkJwTKVox7oPu3SpYiVnGm5A==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/internal-helpers@0.11.0':
+ resolution: {integrity: sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==}
+
+ '@astrojs/markdown-satteri@0.4.1':
+ resolution: {integrity: sha512-EniHbFNa6SQHDih7JnpPos3NWXO6hdt4raBcOosfGmlfc3gP9CI/sESA3VOf1PgJZ7SFfewlZW9Livn0JugEZg==}
+
+ '@astrojs/mdx@8.0.1':
+ resolution: {integrity: sha512-VKodp/f3+XE6L0950K+ZOAolY47vdt0R78Ao+L2RdJ1hfAdYffL7IEjc/MsoyuS7CPVvCsd5/I3RP0ZyAIewdQ==}
+ engines: {node: '>=22.12.0'}
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ '@astrojs/markdown-satteri': ^0.4.0
+ astro: ^7.2.6
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ '@astrojs/prism@4.0.2':
+ resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==}
+ engines: {node: '>=22.12.0'}
+
+ '@astrojs/sitemap@3.7.4':
+ resolution: {integrity: sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==}
+
+ '@astrojs/starlight@0.42.1':
+ resolution: {integrity: sha512-ABS/A0IfbJeMbuhCKVkrAcO3EWxhpeLveFuL28+7+BQje7ZHa9OXZbUoiLXdYUhH7XFaiAQjAg44RiF2uuZPJw==}
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ astro: ^7.2.10
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ '@astrojs/telemetry@3.3.3':
+ resolution: {integrity: sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==}
+ engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0}
+
+ '@babel/helper-string-parser@7.29.7':
+ resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/helper-validator-identifier@7.29.7':
+ resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==}
+ engines: {node: '>=6.9.0'}
+
+ '@babel/parser@7.29.8':
+ resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==}
+ engines: {node: '>=6.0.0'}
+ hasBin: true
+
+ '@babel/types@7.29.8':
+ resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==}
+ engines: {node: '>=6.9.0'}
+
+ '@bruits/satteri-darwin-arm64@0.10.5':
+ resolution: {integrity: sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@bruits/satteri-darwin-x64@0.10.5':
+ resolution: {integrity: sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@bruits/satteri-linux-arm64-gnu@0.10.5':
+ resolution: {integrity: sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@bruits/satteri-linux-arm64-musl@0.10.5':
+ resolution: {integrity: sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-linux-x64-gnu@0.10.5':
+ resolution: {integrity: sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@bruits/satteri-linux-x64-musl@0.10.5':
+ resolution: {integrity: sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@bruits/satteri-wasm32-wasi@0.10.5':
+ resolution: {integrity: sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==}
+ engines: {node: '>=14.0.0'}
+ cpu: [wasm32]
+
+ '@bruits/satteri-win32-arm64-msvc@0.10.5':
+ resolution: {integrity: sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@bruits/satteri-win32-x64-msvc@0.10.5':
+ resolution: {integrity: sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==}
+ cpu: [x64]
+ os: [win32]
+
+ '@capsizecss/unpack@4.0.1':
+ resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==}
+ engines: {node: '>=18'}
+
+ '@clack/core@1.5.1':
+ resolution: {integrity: sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==}
+ engines: {node: '>= 20.12.0'}
+
+ '@clack/prompts@1.8.1':
+ resolution: {integrity: sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==}
+ engines: {node: '>= 20.12.0'}
+
+ '@ctrl/tinycolor@4.2.1':
+ resolution: {integrity: sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==}
+ engines: {node: '>=14'}
+
+ '@emnapi/core@1.11.1':
+ resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==}
+
+ '@emnapi/runtime@1.11.1':
+ resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==}
+
+ '@emnapi/runtime@1.11.3':
+ resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
+
+ '@emnapi/wasi-threads@1.2.2':
+ resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
+
+ '@esbuild/aix-ppc64@0.28.2':
+ resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [aix]
+
+ '@esbuild/android-arm64@0.28.2':
+ resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [android]
+
+ '@esbuild/android-arm@0.28.2':
+ resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [android]
+
+ '@esbuild/android-x64@0.28.2':
+ resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [android]
+
+ '@esbuild/darwin-arm64@0.28.2':
+ resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@esbuild/darwin-x64@0.28.2':
+ resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [freebsd]
+
+ '@esbuild/freebsd-x64@0.28.2':
+ resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@esbuild/linux-arm64@0.28.2':
+ resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [linux]
+
+ '@esbuild/linux-arm@0.28.2':
+ resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==}
+ engines: {node: '>=18'}
+ cpu: [arm]
+ os: [linux]
+
+ '@esbuild/linux-ia32@0.28.2':
+ resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [linux]
+
+ '@esbuild/linux-loong64@0.28.2':
+ resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==}
+ engines: {node: '>=18'}
+ cpu: [loong64]
+ os: [linux]
+
+ '@esbuild/linux-mips64el@0.28.2':
+ resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==}
+ engines: {node: '>=18'}
+ cpu: [mips64el]
+ os: [linux]
+
+ '@esbuild/linux-ppc64@0.28.2':
+ resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==}
+ engines: {node: '>=18'}
+ cpu: [ppc64]
+ os: [linux]
+
+ '@esbuild/linux-riscv64@0.28.2':
+ resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==}
+ engines: {node: '>=18'}
+ cpu: [riscv64]
+ os: [linux]
+
+ '@esbuild/linux-s390x@0.28.2':
+ resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==}
+ engines: {node: '>=18'}
+ cpu: [s390x]
+ os: [linux]
+
+ '@esbuild/linux-x64@0.28.2':
+ resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [linux]
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [netbsd]
+
+ '@esbuild/netbsd-x64@0.28.2':
+ resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [netbsd]
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openbsd]
+
+ '@esbuild/openbsd-x64@0.28.2':
+ resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [openbsd]
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@esbuild/sunos-x64@0.28.2':
+ resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [sunos]
+
+ '@esbuild/win32-arm64@0.28.2':
+ resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==}
+ engines: {node: '>=18'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@esbuild/win32-ia32@0.28.2':
+ resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==}
+ engines: {node: '>=18'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@esbuild/win32-x64@0.28.2':
+ resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==}
+ engines: {node: '>=18'}
+ cpu: [x64]
+ os: [win32]
+
+ '@expressive-code/core@0.44.2':
+ resolution: {integrity: sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==}
+
+ '@expressive-code/plugin-frames@0.44.2':
+ resolution: {integrity: sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==}
+
+ '@expressive-code/plugin-shiki@0.44.2':
+ resolution: {integrity: sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==}
+
+ '@expressive-code/plugin-text-markers@0.44.2':
+ resolution: {integrity: sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==}
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.35.4':
+ resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.35.4':
+ resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-freebsd-wasm32@0.35.4':
+ resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
+ '@img/sharp-libvips-darwin-arm64@1.3.3':
+ resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.3.3':
+ resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.3.3':
+ resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-arm@1.3.3':
+ resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-ppc64@1.3.3':
+ resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-riscv64@1.3.3':
+ resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-s390x@1.3.3':
+ resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-x64@1.3.3':
+ resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+ resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+ resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linux-arm64@0.35.4':
+ resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-arm@0.35.4':
+ resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-ppc64@0.35.4':
+ resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-riscv64@0.35.4':
+ resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-s390x@0.35.4':
+ resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-x64@0.35.4':
+ resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linuxmusl-arm64@0.35.4':
+ resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linuxmusl-x64@0.35.4':
+ resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-wasm32@0.35.4':
+ resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.4':
+ resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.35.4':
+ resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.35.4':
+ resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
+ engines: {node: ^20.9.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.35.4':
+ resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@jridgewell/sourcemap-codec@1.6.0':
+ resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==}
+
+ '@napi-rs/wasm-runtime@1.2.4':
+ resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==}
+ engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
+ peerDependencies:
+ '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
+ '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
+
+ '@oslojs/encoding@1.1.0':
+ resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==}
+
+ '@oxc-project/types@0.150.0':
+ resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pagefind/darwin-x64@1.5.2':
+ resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pagefind/default-ui@1.5.2':
+ resolution: {integrity: sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@pagefind/linux-arm64@1.5.2':
+ resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==}
+ cpu: [arm64]
+ os: [linux]
+
+ '@pagefind/linux-x64@1.5.2':
+ resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==}
+ cpu: [x64]
+ os: [linux]
+
+ '@pagefind/windows-arm64@1.5.2':
+ resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pagefind/windows-x64@1.5.2':
+ resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/binding-android-arm-eabi@1.2.9':
+ resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [android]
+
+ '@rolldown/binding-android-arm64@1.2.9':
+ resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [android]
+
+ '@rolldown/binding-darwin-arm64@1.2.9':
+ resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@rolldown/binding-darwin-x64@1.2.9':
+ resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [darwin]
+
+ '@rolldown/binding-freebsd-x64@1.2.9':
+ resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [freebsd]
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.9':
+ resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm]
+ os: [linux]
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.9':
+ resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-arm64-musl@1.2.9':
+ resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.9':
+ resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.9':
+ resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-gnu@1.2.9':
+ resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@rolldown/binding-linux-x64-musl@1.2.9':
+ resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@rolldown/binding-openharmony-arm64@1.2.9':
+ resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.9':
+ resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [win32]
+
+ '@rolldown/binding-win32-x64-msvc@1.2.9':
+ resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [x64]
+ os: [win32]
+
+ '@rolldown/pluginutils@1.0.1':
+ resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
+
+ '@shikijs/core@4.4.3':
+ resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-javascript@4.4.3':
+ resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/engine-oniguruma@4.4.3':
+ resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==}
+ engines: {node: '>=20'}
+
+ '@shikijs/langs@4.4.3':
+ resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==}
+ engines: {node: '>=20'}
+
+ '@shikijs/primitive@4.4.3':
+ resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==}
+ engines: {node: '>=20'}
+
+ '@shikijs/themes@4.4.3':
+ resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==}
+ engines: {node: '>=20'}
+
+ '@shikijs/types@4.4.3':
+ resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==}
+ engines: {node: '>=20'}
+
+ '@shikijs/vscode-textmate@10.0.2':
+ resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
+
+ '@tybys/wasm-util@0.10.4':
+ resolution: {integrity: sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==}
+
+ '@types/debug@4.1.13':
+ resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==}
+
+ '@types/estree-jsx@1.0.5':
+ resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
+
+ '@types/estree@1.0.9':
+ resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
+
+ '@types/hast@3.0.5':
+ resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==}
+
+ '@types/js-yaml@4.0.9':
+ resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
+
+ '@types/mdast@4.0.4':
+ resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==}
+
+ '@types/ms@2.1.0':
+ resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
+
+ '@types/nlcst@2.0.3':
+ resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+
+ '@types/node@24.13.5':
+ resolution: {integrity: sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==}
+
+ '@types/sax@1.2.7':
+ resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==}
+
+ '@types/unist@2.0.11':
+ resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
+
+ '@types/unist@3.0.3':
+ resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
+
+ '@ungap/structured-clone@1.4.0':
+ resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==}
+
+ am-i-vibing@0.4.0:
+ resolution: {integrity: sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==}
+ hasBin: true
+
+ anymatch@3.1.3:
+ resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
+ engines: {node: '>= 8'}
+
+ arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+
+ argparse@2.0.1:
+ resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+ aria-query@5.3.2:
+ resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+ engines: {node: '>= 0.4'}
+
+ astro-expressive-code@0.44.2:
+ resolution: {integrity: sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==}
+ peerDependencies:
+ astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0
+
+ astro@7.3.3:
+ resolution: {integrity: sha512-NF08hk3edFkVmr9avf9HW/rQyf6NrpbVDyYj2cVN3JfijDhfJYh1ivWVTY5PUd5+rRZn/Vtu4iaeyNlP0Iv6mg==}
+ engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'}
+ hasBin: true
+ peerDependencies:
+ '@astrojs/markdown-remark': ^7.3.0
+ peerDependenciesMeta:
+ '@astrojs/markdown-remark':
+ optional: true
+
+ axobject-query@4.1.0:
+ resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+ engines: {node: '>= 0.4'}
+
+ bail@2.0.2:
+ resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
+
+ bcp-47-match@2.0.3:
+ resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==}
+
+ bcp-47@2.1.1:
+ resolution: {integrity: sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==}
+
+ boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+
+ ccount@2.0.1:
+ resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
+
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
+ character-entities@2.0.2:
+ resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
+
+ character-reference-invalid@2.0.1:
+ resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
+
+ chokidar@5.0.0:
+ resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
+ engines: {node: '>= 20.19.0'}
+
+ ci-info@4.4.0:
+ resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
+ engines: {node: '>=8'}
+
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
+ commander@11.1.0:
+ resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==}
+ engines: {node: '>=16'}
+
+ common-ancestor-path@2.0.0:
+ resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==}
+ engines: {node: '>= 18'}
+
+ cookie-es@1.2.3:
+ resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==}
+
+ cookie@2.0.1:
+ resolution: {integrity: sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==}
+ engines: {node: '>=22'}
+
+ crossws@0.3.5:
+ resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
+
+ css-select@6.0.0:
+ resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==}
+
+ css-selector-parser@3.3.0:
+ resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==}
+
+ css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ css-tree@3.2.1:
+ resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
+
+ css-what@7.0.0:
+ resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==}
+ engines: {node: '>= 6'}
+
+ cssesc@3.0.0:
+ resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
+ engines: {node: '>=4'}
+ hasBin: true
+
+ csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ decode-named-character-reference@1.3.0:
+ resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
+
+ defu@6.1.7:
+ resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==}
+
+ dequal@2.0.3:
+ resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
+ engines: {node: '>=6'}
+
+ destr@2.0.5:
+ resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ devalue@5.9.2:
+ resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==}
+
+ devlop@1.1.0:
+ resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
+
+ diff@9.0.0:
+ resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==}
+ engines: {node: '>=0.3.1'}
+
+ direction@2.0.1:
+ resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
+ hasBin: true
+
+ dom-serializer@2.0.0:
+ resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
+
+ domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+
+ domhandler@5.0.3:
+ resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
+ engines: {node: '>= 4'}
+
+ domutils@3.2.2:
+ resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
+
+ dset@3.1.4:
+ resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
+ engines: {node: '>=4'}
+
+ entities@4.5.0:
+ resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
+ engines: {node: '>=0.12'}
+
+ es-module-lexer@2.3.2:
+ resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
+
+ esbuild@0.28.2:
+ resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==}
+ engines: {node: '>=18'}
+ hasBin: true
+
+ eventemitter3@5.0.4:
+ resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
+
+ expressive-code@0.44.2:
+ resolution: {integrity: sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==}
+
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
+ fast-string-truncated-width@3.0.3:
+ resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==}
+
+ fast-string-width@3.0.2:
+ resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
+
+ fast-wrap-ansi@0.2.2:
+ resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
+
+ fdir@6.5.0:
+ resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
+ engines: {node: '>=12.0.0'}
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+
+ find-proc@0.2.0:
+ resolution: {integrity: sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+
+ flattie@1.1.1:
+ resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
+ engines: {node: '>=8'}
+
+ fontace@0.4.1:
+ resolution: {integrity: sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==}
+
+ fontkitten@1.0.3:
+ resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==}
+ engines: {node: '>=20'}
+
+ fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+
+ get-tsconfig@5.0.0-beta.4:
+ resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==}
+ engines: {node: '>=20.20.0'}
+
+ github-slugger@2.0.0:
+ resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
+
+ h3@1.15.11:
+ resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==}
+
+ hast-util-embedded@3.0.0:
+ resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==}
+
+ hast-util-format@1.1.0:
+ resolution: {integrity: sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==}
+
+ hast-util-has-property@3.0.0:
+ resolution: {integrity: sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==}
+
+ hast-util-is-body-ok-link@3.0.1:
+ resolution: {integrity: sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==}
+
+ hast-util-is-element@3.0.0:
+ resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+
+ hast-util-minify-whitespace@1.0.1:
+ resolution: {integrity: sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==}
+
+ hast-util-parse-selector@4.0.0:
+ resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
+
+ hast-util-phrasing@3.0.1:
+ resolution: {integrity: sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==}
+
+ hast-util-select@6.0.4:
+ resolution: {integrity: sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==}
+
+ hast-util-to-html@9.0.5:
+ resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
+
+ hast-util-to-string@3.0.1:
+ resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==}
+
+ hast-util-to-text@4.0.2:
+ resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
+
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
+ hastscript@9.0.1:
+ resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
+
+ html-escaper@3.0.3:
+ resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==}
+
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
+ html-whitespace-sensitive-tag-names@3.0.1:
+ resolution: {integrity: sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==}
+
+ http-cache-semantics@4.2.0:
+ resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
+
+ i18next@26.4.2:
+ resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
+ peerDependencies:
+ typescript: ^5 || ^6 || ^7
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
+ iron-webcrypto@1.2.1:
+ resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
+
+ is-alphabetical@2.0.1:
+ resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
+
+ is-alphanumerical@2.0.1:
+ resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
+
+ is-decimal@2.0.1:
+ resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
+
+ is-docker@4.0.0:
+ resolution: {integrity: sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==}
+ engines: {node: '>=20'}
+ hasBin: true
+
+ is-hexadecimal@2.0.1:
+ resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
+
+ is-plain-obj@4.1.0:
+ resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==}
+ engines: {node: '>=12'}
+
+ js-yaml@4.3.2:
+ resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
+ hasBin: true
+
+ jsonc-parser@3.3.1:
+ resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==}
+
+ klona@2.0.6:
+ resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==}
+ engines: {node: '>= 8'}
+
+ lightningcss-android-arm64@1.33.0:
+ resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [android]
+
+ lightningcss-darwin-arm64@1.33.0:
+ resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ lightningcss-darwin-x64@1.33.0:
+ resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ lightningcss-freebsd-x64@1.33.0:
+ resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [freebsd]
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm]
+ os: [linux]
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ lightningcss-linux-x64-musl@1.33.0:
+ resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==}
+ engines: {node: '>= 12.0.0'}
+ cpu: [x64]
+ os: [win32]
+
+ lightningcss@1.33.0:
+ resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==}
+ engines: {node: '>= 12.0.0'}
+
+ longest-streak@3.1.0:
+ resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
+
+ lru-cache@11.5.2:
+ resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==}
+ engines: {node: 20 || >=22}
+
+ magic-string@1.4.1:
+ resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==}
+
+ magicast@0.5.5:
+ resolution: {integrity: sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==}
+
+ mdast-util-directive@3.1.0:
+ resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==}
+
+ mdast-util-from-markdown@2.0.3:
+ resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
+
+ mdast-util-phrasing@4.1.0:
+ resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+
+ mdast-util-to-hast@13.2.1:
+ resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==}
+
+ mdast-util-to-markdown@2.1.2:
+ resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==}
+
+ mdast-util-to-string@4.0.0:
+ resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==}
+
+ mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+
+ mdn-data@2.27.1:
+ resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
+
+ micromark-core-commonmark@2.0.3:
+ resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
+
+ micromark-extension-directive@4.0.0:
+ resolution: {integrity: sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==}
+
+ micromark-factory-destination@2.0.1:
+ resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==}
+
+ micromark-factory-label@2.0.1:
+ resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==}
+
+ micromark-factory-space@2.0.1:
+ resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==}
+
+ micromark-factory-title@2.0.1:
+ resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==}
+
+ micromark-factory-whitespace@2.0.1:
+ resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==}
+
+ micromark-util-character@2.1.1:
+ resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==}
+
+ micromark-util-chunked@2.0.1:
+ resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==}
+
+ micromark-util-classify-character@2.0.1:
+ resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==}
+
+ micromark-util-combine-extensions@2.0.1:
+ resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==}
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==}
+
+ micromark-util-decode-string@2.0.1:
+ resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==}
+
+ micromark-util-encode@2.0.1:
+ resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==}
+
+ micromark-util-html-tag-name@2.0.1:
+ resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==}
+
+ micromark-util-normalize-identifier@2.0.1:
+ resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==}
+
+ micromark-util-resolve-all@2.0.1:
+ resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==}
+
+ micromark-util-sanitize-uri@2.0.1:
+ resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==}
+
+ micromark-util-subtokenize@2.1.0:
+ resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==}
+
+ micromark-util-symbol@2.0.1:
+ resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==}
+
+ micromark-util-types@2.0.2:
+ resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==}
+
+ micromark@4.0.2:
+ resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==}
+
+ mrmime@2.0.1:
+ resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==}
+ engines: {node: '>=10'}
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.19:
+ resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ neotraverse@1.0.1:
+ resolution: {integrity: sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==}
+ engines: {node: '>= 10'}
+
+ nlcst-to-string@4.0.0:
+ resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
+
+ node-fetch-native@1.6.7:
+ resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==}
+
+ node-mock-http@1.0.5:
+ resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==}
+
+ normalize-path@3.0.0:
+ resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
+ engines: {node: '>=0.10.0'}
+
+ nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+
+ obug@2.2.1:
+ resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==}
+ engines: {node: '>=12.20.0'}
+
+ ofetch@1.5.1:
+ resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
+
+ ohash@2.0.12:
+ resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==}
+
+ oniguruma-parser@0.12.2:
+ resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==}
+
+ oniguruma-to-es@4.3.6:
+ resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==}
+
+ p-limit@7.3.2:
+ resolution: {integrity: sha512-Ll0w3fU24vYpXoZmjjZIee6bJQDgG0oAyo1PdmFYI8UDwJJddaHAypxIH9avUu+t+lSsAwKVsb1jDCMIIChliw==}
+ engines: {node: '>=20'}
+
+ p-queue@9.3.3:
+ resolution: {integrity: sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==}
+ engines: {node: '>=20'}
+
+ p-timeout@7.0.2:
+ resolution: {integrity: sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==}
+ engines: {node: '>=20'}
+
+ package-manager-detector@1.8.0:
+ resolution: {integrity: sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==}
+
+ pagefind@1.5.2:
+ resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==}
+ hasBin: true
+
+ parse-entities@4.0.2:
+ resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
+
+ piccolore@0.1.3:
+ resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ picomatch@2.3.2:
+ resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==}
+ engines: {node: '>=8.6'}
+
+ picomatch@4.0.7:
+ resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
+ engines: {node: '>=12'}
+
+ postcss-nested@6.2.0:
+ resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==}
+ engines: {node: '>=12.0'}
+ peerDependencies:
+ postcss: ^8.2.14
+
+ postcss-selector-parser@6.1.4:
+ resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==}
+ engines: {node: '>=4'}
+
+ postcss@8.5.28:
+ resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ prismjs@1.30.0:
+ resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
+ engines: {node: '>=6'}
+
+ process-ancestry@0.1.0:
+ resolution: {integrity: sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==}
+ engines: {node: '>=18.0.0'}
+
+ property-information@7.2.0:
+ resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+
+ radix3@1.1.2:
+ resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
+
+ readdirp@5.1.1:
+ resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==}
+ engines: {node: '>= 20.19.0'}
+
+ regex-recursion@6.0.2:
+ resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
+
+ regex-utilities@2.3.0:
+ resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==}
+
+ regex@6.1.0:
+ resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==}
+
+ rehype-expressive-code@0.44.2:
+ resolution: {integrity: sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==}
+
+ remark-directive@4.0.0:
+ resolution: {integrity: sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==}
+
+ resolve-pkg-maps@1.0.0:
+ resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+ retext-smartypants@6.2.0:
+ resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==}
+
+ rolldown@1.2.9:
+ resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+
+ satteri@0.10.5:
+ resolution: {integrity: sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==}
+
+ sax@1.6.1:
+ resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==}
+ engines: {node: '>=11.0.0'}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ sharp@0.35.4:
+ resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ shiki@4.4.3:
+ resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==}
+ engines: {node: '>=20'}
+
+ sisteransi@1.0.5:
+ resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
+
+ sitemap@9.0.1:
+ resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==}
+ engines: {node: '>=20.19.5', npm: '>=10.8.2'}
+ hasBin: true
+
+ smol-toml@1.8.0:
+ resolution: {integrity: sha512-kCZr2V3ch9i00x8zXRhjUNVcjG9ijES5dDudkXvUVCT5QlJNQWElSJdZqyPemffHoLNUYwOcou0Fy+ojN0uHSQ==}
+ engines: {node: '>= 18'}
+
+ source-map-js@1.2.1:
+ resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ engines: {node: '>=0.10.0'}
+
+ space-separated-tokens@2.0.2:
+ resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
+
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
+ svgo@4.1.0:
+ resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==}
+ engines: {node: '>=16'}
+ hasBin: true
+
+ tiny-inflate@1.0.3:
+ resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==}
+
+ tinyclip@0.1.15:
+ resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==}
+ engines: {node: ^16.14.0 || >= 17.3.0}
+
+ tinyexec@1.3.1:
+ resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==}
+ engines: {node: '>=18'}
+
+ tinyglobby@0.2.17:
+ resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==}
+ engines: {node: '>=12.0.0'}
+
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
+ trough@2.2.0:
+ resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ ufo@1.6.4:
+ resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
+
+ ultrahtml@1.7.0:
+ resolution: {integrity: sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==}
+
+ uncrypto@0.1.3:
+ resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
+
+ undici-types@7.18.2:
+ resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
+
+ undici@8.10.2:
+ resolution: {integrity: sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==}
+ engines: {node: '>=22.19.0'}
+
+ unified@11.0.5:
+ resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
+
+ unifont@0.7.5:
+ resolution: {integrity: sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==}
+
+ unist-util-find-after@5.0.0:
+ resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==}
+
+ unist-util-is@6.0.1:
+ resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
+
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
+ unist-util-stringify-position@4.0.0:
+ resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
+
+ unist-util-visit-parents@6.0.2:
+ resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==}
+
+ unist-util-visit@5.1.0:
+ resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
+
+ unstorage@1.17.5:
+ resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==}
+ peerDependencies:
+ '@azure/app-configuration': ^1.8.0
+ '@azure/cosmos': ^4.2.0
+ '@azure/data-tables': ^13.3.0
+ '@azure/identity': ^4.6.0
+ '@azure/keyvault-secrets': ^4.9.0
+ '@azure/storage-blob': ^12.26.0
+ '@capacitor/preferences': ^6 || ^7 || ^8
+ '@deno/kv': '>=0.9.0'
+ '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0
+ '@planetscale/database': ^1.19.0
+ '@upstash/redis': ^1.34.3
+ '@vercel/blob': '>=0.27.1'
+ '@vercel/functions': ^2.2.12 || ^3.0.0
+ '@vercel/kv': ^1 || ^2 || ^3
+ aws4fetch: ^1.0.20
+ db0: '>=0.2.1'
+ idb-keyval: ^6.2.1
+ ioredis: ^5.4.2
+ uploadthing: ^7.4.4
+ peerDependenciesMeta:
+ '@azure/app-configuration':
+ optional: true
+ '@azure/cosmos':
+ optional: true
+ '@azure/data-tables':
+ optional: true
+ '@azure/identity':
+ optional: true
+ '@azure/keyvault-secrets':
+ optional: true
+ '@azure/storage-blob':
+ optional: true
+ '@capacitor/preferences':
+ optional: true
+ '@deno/kv':
+ optional: true
+ '@netlify/blobs':
+ optional: true
+ '@planetscale/database':
+ optional: true
+ '@upstash/redis':
+ optional: true
+ '@vercel/blob':
+ optional: true
+ '@vercel/functions':
+ optional: true
+ '@vercel/kv':
+ optional: true
+ aws4fetch:
+ optional: true
+ db0:
+ optional: true
+ idb-keyval:
+ optional: true
+ ioredis:
+ optional: true
+ uploadthing:
+ optional: true
+
+ url-extras@0.1.0:
+ resolution: {integrity: sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==}
+ engines: {node: '>=20'}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+ verkit@0.4.0:
+ resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==}
+ engines: {node: '>=18.12.0'}
+
+ vfile-message@4.0.3:
+ resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
+
+ vfile@6.0.3:
+ resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+
+ vite@8.3.0:
+ resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^20.19.0 || >=22.12.0
+ '@vitejs/devtools': ^0.7.1
+ esbuild: ^0.27.0 || ^0.28.0
+ jiti: '>=1.21.0'
+ less: ^4.0.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: '>=0.54.8'
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ '@vitejs/devtools':
+ optional: true
+ esbuild:
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+
+ vitefu@1.1.3:
+ resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==}
+ peerDependencies:
+ vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ vite:
+ optional: true
+
+ xxhash-wasm@1.1.0:
+ resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
+
+ yargs-parser@22.0.0:
+ resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
+ engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+
+ yocto-queue@1.2.2:
+ resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==}
+ engines: {node: '>=12.20'}
+
+ zod@4.6.5:
+ resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
+
+ zwitch@2.0.4:
+ resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
+
+snapshots:
+
+ '@astrojs/compiler-binding-android-arm64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-darwin-arm64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-darwin-x64@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-arm64-gnu@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-arm64-musl@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-x64-gnu@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-linux-x64-musl@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-wasm32-wasi@0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ optional: true
+
+ '@astrojs/compiler-binding-win32-arm64-msvc@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding-win32-x64-msvc@0.4.1':
+ optional: true
+
+ '@astrojs/compiler-binding@0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)':
+ optionalDependencies:
+ '@astrojs/compiler-binding-android-arm64': 0.4.1
+ '@astrojs/compiler-binding-darwin-arm64': 0.4.1
+ '@astrojs/compiler-binding-darwin-x64': 0.4.1
+ '@astrojs/compiler-binding-linux-arm64-gnu': 0.4.1
+ '@astrojs/compiler-binding-linux-arm64-musl': 0.4.1
+ '@astrojs/compiler-binding-linux-x64-gnu': 0.4.1
+ '@astrojs/compiler-binding-linux-x64-musl': 0.4.1
+ '@astrojs/compiler-binding-wasm32-wasi': 0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)
+ '@astrojs/compiler-binding-win32-arm64-msvc': 0.4.1
+ '@astrojs/compiler-binding-win32-x64-msvc': 0.4.1
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ '@astrojs/compiler-rs@0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@astrojs/compiler-binding': 0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)
+ transitivePeerDependencies:
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+
+ '@astrojs/internal-helpers@0.11.0':
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ js-yaml: 4.3.2
+ picomatch: 4.0.7
+ retext-smartypants: 6.2.0
+ shiki: 4.4.3
+ smol-toml: 1.8.0
+ unified: 11.0.5
+
+ '@astrojs/markdown-satteri@0.4.1':
+ dependencies:
+ '@astrojs/internal-helpers': 0.11.0
+ '@astrojs/prism': 4.0.2
+ github-slugger: 2.0.0
+ satteri: 0.10.5
+
+ '@astrojs/mdx@8.0.1(@astrojs/markdown-satteri@0.4.1)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5))':
+ dependencies:
+ '@astrojs/internal-helpers': 0.11.0
+ '@astrojs/markdown-satteri': 0.4.1
+ astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5)
+ es-module-lexer: 2.3.2
+
+ '@astrojs/prism@4.0.2':
+ dependencies:
+ prismjs: 1.30.0
+
+ '@astrojs/sitemap@3.7.4':
+ dependencies:
+ sitemap: 9.0.1
+ zod: 4.6.5
+
+ '@astrojs/starlight@0.42.1(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5))':
+ dependencies:
+ '@astrojs/markdown-satteri': 0.4.1
+ '@astrojs/mdx': 8.0.1(@astrojs/markdown-satteri@0.4.1)(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5))
+ '@astrojs/sitemap': 3.7.4
+ '@pagefind/default-ui': 1.5.2
+ '@types/hast': 3.0.5
+ '@types/js-yaml': 4.0.9
+ '@types/mdast': 4.0.4
+ astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5)
+ astro-expressive-code: 0.44.2(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5))
+ bcp-47: 2.1.1
+ hast-util-format: 1.1.0
+ hast-util-select: 6.0.4
+ hast-util-to-html: 9.0.5
+ hast-util-to-string: 3.0.1
+ hastscript: 9.0.1
+ i18next: 26.4.2
+ js-yaml: 4.3.2
+ klona: 2.0.6
+ magic-string: 1.4.1
+ mdast-util-directive: 3.1.0
+ mdast-util-to-markdown: 2.1.2
+ mdast-util-to-string: 4.0.0
+ pagefind: 1.5.2
+ remark-directive: 4.0.0
+ satteri: 0.10.5
+ ultrahtml: 1.7.0
+ unified: 11.0.5
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+ transitivePeerDependencies:
+ - supports-color
+ - typescript
+
+ '@astrojs/telemetry@3.3.3':
+ dependencies:
+ ci-info: 4.4.0
+ dset: 3.1.4
+ is-docker: 4.0.0
+ package-manager-detector: 1.8.0
+
+ '@babel/helper-string-parser@7.29.7': {}
+
+ '@babel/helper-validator-identifier@7.29.7': {}
+
+ '@babel/parser@7.29.8':
+ dependencies:
+ '@babel/types': 7.29.8
+
+ '@babel/types@7.29.8':
+ dependencies:
+ '@babel/helper-string-parser': 7.29.7
+ '@babel/helper-validator-identifier': 7.29.7
+
+ '@bruits/satteri-darwin-arm64@0.10.5':
+ optional: true
+
+ '@bruits/satteri-darwin-x64@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-arm64-gnu@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-arm64-musl@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-x64-gnu@0.10.5':
+ optional: true
+
+ '@bruits/satteri-linux-x64-musl@0.10.5':
+ optional: true
+
+ '@bruits/satteri-wasm32-wasi@0.10.5':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)
+ optional: true
+
+ '@bruits/satteri-win32-arm64-msvc@0.10.5':
+ optional: true
+
+ '@bruits/satteri-win32-x64-msvc@0.10.5':
+ optional: true
+
+ '@capsizecss/unpack@4.0.1':
+ dependencies:
+ fontkitten: 1.0.3
+
+ '@clack/core@1.5.1':
+ dependencies:
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@clack/prompts@1.8.1':
+ dependencies:
+ '@clack/core': 1.5.1
+ fast-string-width: 3.0.2
+ fast-wrap-ansi: 0.2.2
+ sisteransi: 1.0.5
+
+ '@ctrl/tinycolor@4.2.1': {}
+
+ '@emnapi/core@1.11.1':
+ dependencies:
+ '@emnapi/wasi-threads': 1.2.2
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.1':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/runtime@1.11.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@emnapi/wasi-threads@1.2.2':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@esbuild/aix-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/android-arm@0.28.2':
+ optional: true
+
+ '@esbuild/android-x64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/darwin-x64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/freebsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-arm@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/linux-loong64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-mips64el@0.28.2':
+ optional: true
+
+ '@esbuild/linux-ppc64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-riscv64@0.28.2':
+ optional: true
+
+ '@esbuild/linux-s390x@0.28.2':
+ optional: true
+
+ '@esbuild/linux-x64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/netbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/openbsd-x64@0.28.2':
+ optional: true
+
+ '@esbuild/openharmony-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/sunos-x64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-arm64@0.28.2':
+ optional: true
+
+ '@esbuild/win32-ia32@0.28.2':
+ optional: true
+
+ '@esbuild/win32-x64@0.28.2':
+ optional: true
+
+ '@expressive-code/core@0.44.2':
+ dependencies:
+ '@ctrl/tinycolor': 4.2.1
+ hast-util-select: 6.0.4
+ hast-util-to-html: 9.0.5
+ hast-util-to-text: 4.0.2
+ hastscript: 9.0.1
+ postcss: 8.5.28
+ postcss-nested: 6.2.0(postcss@8.5.28)
+ unist-util-visit: 5.1.0
+ unist-util-visit-parents: 6.0.2
+
+ '@expressive-code/plugin-frames@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+
+ '@expressive-code/plugin-shiki@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+ shiki: 4.4.3
+
+ '@expressive-code/plugin-text-markers@0.44.2':
+ dependencies:
+ '@expressive-code/core': 0.44.2
+
+ '@img/colour@1.1.0': {}
+
+ '@img/sharp-darwin-arm64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.3.3
+ optional: true
+
+ '@img/sharp-darwin-x64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.3.3
+ optional: true
+
+ '@img/sharp-freebsd-wasm32@0.35.4':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.4
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.3.3
+ optional: true
+
+ '@img/sharp-linux-arm@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.3.3
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.3.3
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.3.3
+ optional: true
+
+ '@img/sharp-linux-s390x@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.3.3
+ optional: true
+
+ '@img/sharp-linux-x64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.3.3
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.35.4':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+ optional: true
+
+ '@img/sharp-wasm32@0.35.4':
+ dependencies:
+ '@emnapi/runtime': 1.11.3
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.4':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.4
+ optional: true
+
+ '@img/sharp-win32-arm64@0.35.4':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.35.4':
+ optional: true
+
+ '@img/sharp-win32-x64@0.35.4':
+ optional: true
+
+ '@jridgewell/sourcemap-codec@1.6.0': {}
+
+ '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.1
+ '@tybys/wasm-util': 0.10.4
+ optional: true
+
+ '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)':
+ dependencies:
+ '@emnapi/core': 1.11.1
+ '@emnapi/runtime': 1.11.3
+ '@tybys/wasm-util': 0.10.4
+ optional: true
+
+ '@oslojs/encoding@1.1.0': {}
+
+ '@oxc-project/types@0.150.0': {}
+
+ '@pagefind/darwin-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/darwin-x64@1.5.2':
+ optional: true
+
+ '@pagefind/default-ui@1.5.2': {}
+
+ '@pagefind/freebsd-x64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/linux-x64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-arm64@1.5.2':
+ optional: true
+
+ '@pagefind/windows-x64@1.5.2':
+ optional: true
+
+ '@rolldown/binding-android-arm-eabi@1.2.9':
+ optional: true
+
+ '@rolldown/binding-android-arm64@1.2.9':
+ optional: true
+
+ '@rolldown/binding-darwin-arm64@1.2.9':
+ optional: true
+
+ '@rolldown/binding-darwin-x64@1.2.9':
+ optional: true
+
+ '@rolldown/binding-freebsd-x64@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm-gnueabihf@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-gnu@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-arm64-musl@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-ppc64-gnu@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-s390x-gnu@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-x64-gnu@1.2.9':
+ optional: true
+
+ '@rolldown/binding-linux-x64-musl@1.2.9':
+ optional: true
+
+ '@rolldown/binding-openharmony-arm64@1.2.9':
+ optional: true
+
+ '@rolldown/binding-win32-arm64-msvc@1.2.9':
+ optional: true
+
+ '@rolldown/binding-win32-x64-msvc@1.2.9':
+ optional: true
+
+ '@rolldown/pluginutils@1.0.1': {}
+
+ '@shikijs/core@4.4.3':
+ dependencies:
+ '@shikijs/primitive': 4.4.3
+ '@shikijs/types': 4.4.3
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+ hast-util-to-html: 9.0.5
+
+ '@shikijs/engine-javascript@4.4.3':
+ dependencies:
+ '@shikijs/types': 4.4.3
+ '@shikijs/vscode-textmate': 10.0.2
+ oniguruma-to-es: 4.3.6
+
+ '@shikijs/engine-oniguruma@4.4.3':
+ dependencies:
+ '@shikijs/types': 4.4.3
+ '@shikijs/vscode-textmate': 10.0.2
+
+ '@shikijs/langs@4.4.3':
+ dependencies:
+ '@shikijs/types': 4.4.3
+
+ '@shikijs/primitive@4.4.3':
+ dependencies:
+ '@shikijs/types': 4.4.3
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/themes@4.4.3':
+ dependencies:
+ '@shikijs/types': 4.4.3
+
+ '@shikijs/types@4.4.3':
+ dependencies:
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ '@shikijs/vscode-textmate@10.0.2': {}
+
+ '@tybys/wasm-util@0.10.4':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@types/debug@4.1.13':
+ dependencies:
+ '@types/ms': 2.1.0
+
+ '@types/estree-jsx@1.0.5':
+ dependencies:
+ '@types/estree': 1.0.9
+
+ '@types/estree@1.0.9': {}
+
+ '@types/hast@3.0.5':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/js-yaml@4.0.9': {}
+
+ '@types/mdast@4.0.4':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/ms@2.1.0': {}
+
+ '@types/nlcst@2.0.3':
+ dependencies:
+ '@types/unist': 3.0.3
+
+ '@types/node@24.13.5':
+ dependencies:
+ undici-types: 7.18.2
+
+ '@types/sax@1.2.7':
+ dependencies:
+ '@types/node': 24.13.5
+
+ '@types/unist@2.0.11': {}
+
+ '@types/unist@3.0.3': {}
+
+ '@ungap/structured-clone@1.4.0': {}
+
+ am-i-vibing@0.4.0:
+ dependencies:
+ process-ancestry: 0.1.0
+
+ anymatch@3.1.3:
+ dependencies:
+ normalize-path: 3.0.0
+ picomatch: 2.3.2
+
+ arg@5.0.2: {}
+
+ argparse@2.0.1: {}
+
+ aria-query@5.3.2: {}
+
+ astro-expressive-code@0.44.2(astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5)):
+ dependencies:
+ astro: 7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5)
+ rehype-expressive-code: 0.44.2
+ url-extras: 0.1.0
+
+ astro@7.3.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@24.13.5):
+ dependencies:
+ '@astrojs/compiler-rs': 0.4.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)
+ '@astrojs/internal-helpers': 0.11.0
+ '@astrojs/markdown-satteri': 0.4.1
+ '@astrojs/telemetry': 3.3.3
+ '@capsizecss/unpack': 4.0.1
+ '@clack/prompts': 1.8.1
+ '@oslojs/encoding': 1.1.0
+ am-i-vibing: 0.4.0
+ aria-query: 5.3.2
+ axobject-query: 4.1.0
+ ci-info: 4.4.0
+ clsx: 2.1.1
+ common-ancestor-path: 2.0.0
+ cookie: 2.0.1
+ devalue: 5.9.2
+ diff: 9.0.0
+ dset: 3.1.4
+ es-module-lexer: 2.3.2
+ esbuild: 0.28.2
+ find-proc: 0.2.0
+ flattie: 1.1.1
+ fontace: 0.4.1
+ get-tsconfig: 5.0.0-beta.4
+ github-slugger: 2.0.0
+ html-escaper: 3.0.3
+ http-cache-semantics: 4.2.0
+ js-yaml: 4.3.2
+ jsonc-parser: 3.3.1
+ magic-string: 1.4.1
+ magicast: 0.5.5
+ mrmime: 2.0.1
+ neotraverse: 1.0.1
+ obug: 2.2.1
+ p-limit: 7.3.2
+ p-queue: 9.3.3
+ package-manager-detector: 1.8.0
+ piccolore: 0.1.3
+ picomatch: 4.0.7
+ shiki: 4.4.3
+ smol-toml: 1.8.0
+ svgo: 4.1.0
+ tinyclip: 0.1.15
+ tinyexec: 1.3.1
+ tinyglobby: 0.2.17
+ ultrahtml: 1.7.0
+ unifont: 0.7.5
+ unstorage: 1.17.5
+ verkit: 0.4.0
+ vite: 8.3.0(@types/node@24.13.5)(esbuild@0.28.2)
+ vitefu: 1.1.3(vite@8.3.0(@types/node@24.13.5)(esbuild@0.28.2))
+ xxhash-wasm: 1.1.0
+ yargs-parser: 22.0.0
+ zod: 4.6.5
+ optionalDependencies:
+ sharp: 0.35.4(@types/node@24.13.5)
+ transitivePeerDependencies:
+ - '@azure/app-configuration'
+ - '@azure/cosmos'
+ - '@azure/data-tables'
+ - '@azure/identity'
+ - '@azure/keyvault-secrets'
+ - '@azure/storage-blob'
+ - '@capacitor/preferences'
+ - '@deno/kv'
+ - '@emnapi/core'
+ - '@emnapi/runtime'
+ - '@netlify/blobs'
+ - '@planetscale/database'
+ - '@types/node'
+ - '@upstash/redis'
+ - '@vercel/blob'
+ - '@vercel/functions'
+ - '@vercel/kv'
+ - '@vitejs/devtools'
+ - aws4fetch
+ - db0
+ - idb-keyval
+ - ioredis
+ - jiti
+ - less
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - terser
+ - tsx
+ - uploadthing
+ - yaml
+
+ axobject-query@4.1.0: {}
+
+ bail@2.0.2: {}
+
+ bcp-47-match@2.0.3: {}
+
+ bcp-47@2.1.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+
+ boolbase@1.0.0: {}
+
+ ccount@2.0.1: {}
+
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
+ character-entities@2.0.2: {}
+
+ character-reference-invalid@2.0.1: {}
+
+ chokidar@5.0.0:
+ dependencies:
+ readdirp: 5.1.1
+
+ ci-info@4.4.0: {}
+
+ clsx@2.1.1: {}
+
+ comma-separated-tokens@2.0.3: {}
+
+ commander@11.1.0: {}
+
+ common-ancestor-path@2.0.0: {}
+
+ cookie-es@1.2.3: {}
+
+ cookie@2.0.1: {}
+
+ crossws@0.3.5:
+ dependencies:
+ uncrypto: 0.1.3
+
+ css-select@6.0.0:
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 7.0.0
+ domhandler: 5.0.3
+ domutils: 3.2.2
+ nth-check: 2.1.1
+
+ css-selector-parser@3.3.0: {}
+
+ css-tree@2.2.1:
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.2.1
+
+ css-tree@3.2.1:
+ dependencies:
+ mdn-data: 2.27.1
+ source-map-js: 1.2.1
+
+ css-what@7.0.0: {}
+
+ cssesc@3.0.0: {}
+
+ csso@5.0.5:
+ dependencies:
+ css-tree: 2.2.1
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ decode-named-character-reference@1.3.0:
+ dependencies:
+ character-entities: 2.0.2
+
+ defu@6.1.7: {}
+
+ dequal@2.0.3: {}
+
+ destr@2.0.5: {}
+
+ detect-libc@2.1.2: {}
+
+ devalue@5.9.2: {}
+
+ devlop@1.1.0:
+ dependencies:
+ dequal: 2.0.3
+
+ diff@9.0.0: {}
+
+ direction@2.0.1: {}
+
+ dom-serializer@2.0.0:
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+ entities: 4.5.0
+
+ domelementtype@2.3.0: {}
+
+ domhandler@5.0.3:
+ dependencies:
+ domelementtype: 2.3.0
+
+ domutils@3.2.2:
+ dependencies:
+ dom-serializer: 2.0.0
+ domelementtype: 2.3.0
+ domhandler: 5.0.3
+
+ dset@3.1.4: {}
+
+ entities@4.5.0: {}
+
+ es-module-lexer@2.3.2: {}
+
+ esbuild@0.28.2:
+ optionalDependencies:
+ '@esbuild/aix-ppc64': 0.28.2
+ '@esbuild/android-arm': 0.28.2
+ '@esbuild/android-arm64': 0.28.2
+ '@esbuild/android-x64': 0.28.2
+ '@esbuild/darwin-arm64': 0.28.2
+ '@esbuild/darwin-x64': 0.28.2
+ '@esbuild/freebsd-arm64': 0.28.2
+ '@esbuild/freebsd-x64': 0.28.2
+ '@esbuild/linux-arm': 0.28.2
+ '@esbuild/linux-arm64': 0.28.2
+ '@esbuild/linux-ia32': 0.28.2
+ '@esbuild/linux-loong64': 0.28.2
+ '@esbuild/linux-mips64el': 0.28.2
+ '@esbuild/linux-ppc64': 0.28.2
+ '@esbuild/linux-riscv64': 0.28.2
+ '@esbuild/linux-s390x': 0.28.2
+ '@esbuild/linux-x64': 0.28.2
+ '@esbuild/netbsd-arm64': 0.28.2
+ '@esbuild/netbsd-x64': 0.28.2
+ '@esbuild/openbsd-arm64': 0.28.2
+ '@esbuild/openbsd-x64': 0.28.2
+ '@esbuild/openharmony-arm64': 0.28.2
+ '@esbuild/sunos-x64': 0.28.2
+ '@esbuild/win32-arm64': 0.28.2
+ '@esbuild/win32-ia32': 0.28.2
+ '@esbuild/win32-x64': 0.28.2
+
+ eventemitter3@5.0.4: {}
+
+ expressive-code@0.44.2:
+ dependencies:
+ '@expressive-code/core': 0.44.2
+ '@expressive-code/plugin-frames': 0.44.2
+ '@expressive-code/plugin-shiki': 0.44.2
+ '@expressive-code/plugin-text-markers': 0.44.2
+
+ extend@3.0.2: {}
+
+ fast-string-truncated-width@3.0.3: {}
+
+ fast-string-width@3.0.2:
+ dependencies:
+ fast-string-truncated-width: 3.0.3
+
+ fast-wrap-ansi@0.2.2:
+ dependencies:
+ fast-string-width: 3.0.2
+
+ fdir@6.5.0(picomatch@4.0.7):
+ optionalDependencies:
+ picomatch: 4.0.7
+
+ find-proc@0.2.0: {}
+
+ flattie@1.1.1: {}
+
+ fontace@0.4.1:
+ dependencies:
+ fontkitten: 1.0.3
+
+ fontkitten@1.0.3:
+ dependencies:
+ tiny-inflate: 1.0.3
+
+ fsevents@2.3.3:
+ optional: true
+
+ get-tsconfig@5.0.0-beta.4:
+ dependencies:
+ resolve-pkg-maps: 1.0.0
+
+ github-slugger@2.0.0: {}
+
+ h3@1.15.11:
+ dependencies:
+ cookie-es: 1.2.3
+ crossws: 0.3.5
+ defu: 6.1.7
+ destr: 2.0.5
+ iron-webcrypto: 1.2.1
+ node-mock-http: 1.0.5
+ radix3: 1.1.2
+ ufo: 1.6.4
+ uncrypto: 0.1.3
+
+ hast-util-embedded@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-is-element: 3.0.0
+
+ hast-util-format@1.1.0:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-minify-whitespace: 1.0.1
+ hast-util-phrasing: 3.0.1
+ hast-util-whitespace: 3.0.0
+ html-whitespace-sensitive-tag-names: 3.0.1
+ unist-util-visit-parents: 6.0.2
+
+ hast-util-has-property@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-is-body-ok-link@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-is-element@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-minify-whitespace@1.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-is-element: 3.0.0
+ hast-util-whitespace: 3.0.0
+ unist-util-is: 6.0.1
+
+ hast-util-parse-selector@4.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-phrasing@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ hast-util-embedded: 3.0.0
+ hast-util-has-property: 3.0.0
+ hast-util-is-body-ok-link: 3.0.1
+ hast-util-is-element: 3.0.0
+
+ hast-util-select@6.0.4:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ bcp-47-match: 2.0.3
+ comma-separated-tokens: 2.0.3
+ css-selector-parser: 3.3.0
+ devlop: 1.1.0
+ direction: 2.0.1
+ hast-util-has-property: 3.0.0
+ hast-util-to-string: 3.0.1
+ hast-util-whitespace: 3.0.0
+ nth-check: 2.1.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ hast-util-to-html@9.0.5:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.1
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
+ hast-util-to-string@3.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hast-util-to-text@4.0.2:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/unist': 3.0.3
+ hast-util-is-element: 3.0.0
+ unist-util-find-after: 5.0.0
+
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.5
+
+ hastscript@9.0.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ comma-separated-tokens: 2.0.3
+ hast-util-parse-selector: 4.0.0
+ property-information: 7.2.0
+ space-separated-tokens: 2.0.2
+
+ html-escaper@3.0.3: {}
+
+ html-void-elements@3.0.0: {}
+
+ html-whitespace-sensitive-tag-names@3.0.1: {}
+
+ http-cache-semantics@4.2.0: {}
+
+ i18next@26.4.2: {}
+
+ iron-webcrypto@1.2.1: {}
+
+ is-alphabetical@2.0.1: {}
+
+ is-alphanumerical@2.0.1:
+ dependencies:
+ is-alphabetical: 2.0.1
+ is-decimal: 2.0.1
+
+ is-decimal@2.0.1: {}
+
+ is-docker@4.0.0: {}
+
+ is-hexadecimal@2.0.1: {}
+
+ is-plain-obj@4.1.0: {}
+
+ js-yaml@4.3.2:
+ dependencies:
+ argparse: 2.0.1
+
+ jsonc-parser@3.3.1: {}
+
+ klona@2.0.6: {}
+
+ lightningcss-android-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-arm64@1.33.0:
+ optional: true
+
+ lightningcss-darwin-x64@1.33.0:
+ optional: true
+
+ lightningcss-freebsd-x64@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm-gnueabihf@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-arm64-musl@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-gnu@1.33.0:
+ optional: true
+
+ lightningcss-linux-x64-musl@1.33.0:
+ optional: true
+
+ lightningcss-win32-arm64-msvc@1.33.0:
+ optional: true
+
+ lightningcss-win32-x64-msvc@1.33.0:
+ optional: true
+
+ lightningcss@1.33.0:
+ dependencies:
+ detect-libc: 2.1.2
+ optionalDependencies:
+ lightningcss-android-arm64: 1.33.0
+ lightningcss-darwin-arm64: 1.33.0
+ lightningcss-darwin-x64: 1.33.0
+ lightningcss-freebsd-x64: 1.33.0
+ lightningcss-linux-arm-gnueabihf: 1.33.0
+ lightningcss-linux-arm64-gnu: 1.33.0
+ lightningcss-linux-arm64-musl: 1.33.0
+ lightningcss-linux-x64-gnu: 1.33.0
+ lightningcss-linux-x64-musl: 1.33.0
+ lightningcss-win32-arm64-msvc: 1.33.0
+ lightningcss-win32-x64-msvc: 1.33.0
+
+ longest-streak@3.1.0: {}
+
+ lru-cache@11.5.2: {}
+
+ magic-string@1.4.1:
+ dependencies:
+ '@jridgewell/sourcemap-codec': 1.6.0
+
+ magicast@0.5.5:
+ dependencies:
+ '@babel/parser': 7.29.8
+ '@babel/types': 7.29.8
+ source-map-js: 1.2.1
+
+ mdast-util-directive@3.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ ccount: 2.0.1
+ devlop: 1.1.0
+ mdast-util-from-markdown: 2.0.3
+ mdast-util-to-markdown: 2.1.2
+ parse-entities: 4.0.2
+ stringify-entities: 4.0.4
+ unist-util-visit-parents: 6.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-from-markdown@2.0.3:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ mdast-util-to-string: 4.0.0
+ micromark: 4.0.2
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-decode-string: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ unist-util-stringify-position: 4.0.0
+ transitivePeerDependencies:
+ - supports-color
+
+ mdast-util-phrasing@4.1.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ unist-util-is: 6.0.1
+
+ mdast-util-to-hast@13.2.1:
+ dependencies:
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@ungap/structured-clone': 1.4.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.1
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.1.0
+ vfile: 6.0.3
+
+ mdast-util-to-markdown@2.1.2:
+ dependencies:
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ longest-streak: 3.1.0
+ mdast-util-phrasing: 4.1.0
+ mdast-util-to-string: 4.0.0
+ micromark-util-classify-character: 2.0.1
+ micromark-util-decode-string: 2.0.1
+ unist-util-visit: 5.1.0
+ zwitch: 2.0.4
+
+ mdast-util-to-string@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+
+ mdn-data@2.0.28: {}
+
+ mdn-data@2.27.1: {}
+
+ micromark-core-commonmark@2.0.3:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-factory-destination: 2.0.1
+ micromark-factory-label: 2.0.1
+ micromark-factory-space: 2.0.1
+ micromark-factory-title: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-classify-character: 2.0.1
+ micromark-util-html-tag-name: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-extension-directive@4.0.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-factory-space: 2.0.1
+ micromark-factory-whitespace: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ parse-entities: 4.0.2
+
+ micromark-factory-destination@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-label@2.0.1:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-space@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-title@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-factory-whitespace@2.0.1:
+ dependencies:
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-character@2.1.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-chunked@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-classify-character@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-combine-extensions@2.0.1:
+ dependencies:
+ micromark-util-chunked: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-decode-numeric-character-reference@2.0.2:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-decode-string@2.0.1:
+ dependencies:
+ decode-named-character-reference: 1.3.0
+ micromark-util-character: 2.1.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-encode@2.0.1: {}
+
+ micromark-util-html-tag-name@2.0.1: {}
+
+ micromark-util-normalize-identifier@2.0.1:
+ dependencies:
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-resolve-all@2.0.1:
+ dependencies:
+ micromark-util-types: 2.0.2
+
+ micromark-util-sanitize-uri@2.0.1:
+ dependencies:
+ micromark-util-character: 2.1.1
+ micromark-util-encode: 2.0.1
+ micromark-util-symbol: 2.0.1
+
+ micromark-util-subtokenize@2.1.0:
+ dependencies:
+ devlop: 1.1.0
+ micromark-util-chunked: 2.0.1
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+
+ micromark-util-symbol@2.0.1: {}
+
+ micromark-util-types@2.0.2: {}
+
+ micromark@4.0.2:
+ dependencies:
+ '@types/debug': 4.1.13
+ debug: 4.4.3
+ decode-named-character-reference: 1.3.0
+ devlop: 1.1.0
+ micromark-core-commonmark: 2.0.3
+ micromark-factory-space: 2.0.1
+ micromark-util-character: 2.1.1
+ micromark-util-chunked: 2.0.1
+ micromark-util-combine-extensions: 2.0.1
+ micromark-util-decode-numeric-character-reference: 2.0.2
+ micromark-util-encode: 2.0.1
+ micromark-util-normalize-identifier: 2.0.1
+ micromark-util-resolve-all: 2.0.1
+ micromark-util-sanitize-uri: 2.0.1
+ micromark-util-subtokenize: 2.1.0
+ micromark-util-symbol: 2.0.1
+ micromark-util-types: 2.0.2
+ transitivePeerDependencies:
+ - supports-color
+
+ mrmime@2.0.1: {}
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.19: {}
+
+ neotraverse@1.0.1: {}
+
+ nlcst-to-string@4.0.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+
+ node-fetch-native@1.6.7: {}
+
+ node-mock-http@1.0.5: {}
+
+ normalize-path@3.0.0: {}
+
+ nth-check@2.1.1:
+ dependencies:
+ boolbase: 1.0.0
+
+ obug@2.2.1: {}
+
+ ofetch@1.5.1:
+ dependencies:
+ destr: 2.0.5
+ node-fetch-native: 1.6.7
+ ufo: 1.6.4
+
+ ohash@2.0.12: {}
+
+ oniguruma-parser@0.12.2: {}
+
+ oniguruma-to-es@4.3.6:
+ dependencies:
+ oniguruma-parser: 0.12.2
+ regex: 6.1.0
+ regex-recursion: 6.0.2
+
+ p-limit@7.3.2:
+ dependencies:
+ yocto-queue: 1.2.2
+
+ p-queue@9.3.3:
+ dependencies:
+ eventemitter3: 5.0.4
+ p-timeout: 7.0.2
+
+ p-timeout@7.0.2: {}
+
+ package-manager-detector@1.8.0: {}
+
+ pagefind@1.5.2:
+ optionalDependencies:
+ '@pagefind/darwin-arm64': 1.5.2
+ '@pagefind/darwin-x64': 1.5.2
+ '@pagefind/freebsd-x64': 1.5.2
+ '@pagefind/linux-arm64': 1.5.2
+ '@pagefind/linux-x64': 1.5.2
+ '@pagefind/windows-arm64': 1.5.2
+ '@pagefind/windows-x64': 1.5.2
+
+ parse-entities@4.0.2:
+ dependencies:
+ '@types/unist': 2.0.11
+ character-entities-legacy: 3.0.0
+ character-reference-invalid: 2.0.1
+ decode-named-character-reference: 1.3.0
+ is-alphanumerical: 2.0.1
+ is-decimal: 2.0.1
+ is-hexadecimal: 2.0.1
+
+ piccolore@0.1.3: {}
+
+ picocolors@1.1.1: {}
+
+ picomatch@2.3.2: {}
+
+ picomatch@4.0.7: {}
+
+ postcss-nested@6.2.0(postcss@8.5.28):
+ dependencies:
+ postcss: 8.5.28
+ postcss-selector-parser: 6.1.4
+
+ postcss-selector-parser@6.1.4:
+ dependencies:
+ cssesc: 3.0.0
+ util-deprecate: 1.0.2
+
+ postcss@8.5.28:
+ dependencies:
+ nanoid: 3.3.19
+ picocolors: 1.1.1
+ source-map-js: 1.2.1
+
+ prismjs@1.30.0: {}
+
+ process-ancestry@0.1.0: {}
+
+ property-information@7.2.0: {}
+
+ radix3@1.1.2: {}
+
+ readdirp@5.1.1: {}
+
+ regex-recursion@6.0.2:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ regex-utilities@2.3.0: {}
+
+ regex@6.1.0:
+ dependencies:
+ regex-utilities: 2.3.0
+
+ rehype-expressive-code@0.44.2:
+ dependencies:
+ expressive-code: 0.44.2
+
+ remark-directive@4.0.0:
+ dependencies:
+ '@types/mdast': 4.0.4
+ mdast-util-directive: 3.1.0
+ micromark-extension-directive: 4.0.0
+ unified: 11.0.5
+ transitivePeerDependencies:
+ - supports-color
+
+ resolve-pkg-maps@1.0.0: {}
+
+ retext-smartypants@6.2.0:
+ dependencies:
+ '@types/nlcst': 2.0.3
+ nlcst-to-string: 4.0.0
+ unist-util-visit: 5.1.0
+
+ rolldown@1.2.9:
+ dependencies:
+ '@oxc-project/types': 0.150.0
+ '@rolldown/pluginutils': 1.0.1
+ optionalDependencies:
+ '@rolldown/binding-android-arm-eabi': 1.2.9
+ '@rolldown/binding-android-arm64': 1.2.9
+ '@rolldown/binding-darwin-arm64': 1.2.9
+ '@rolldown/binding-darwin-x64': 1.2.9
+ '@rolldown/binding-freebsd-x64': 1.2.9
+ '@rolldown/binding-linux-arm-gnueabihf': 1.2.9
+ '@rolldown/binding-linux-arm64-gnu': 1.2.9
+ '@rolldown/binding-linux-arm64-musl': 1.2.9
+ '@rolldown/binding-linux-ppc64-gnu': 1.2.9
+ '@rolldown/binding-linux-s390x-gnu': 1.2.9
+ '@rolldown/binding-linux-x64-gnu': 1.2.9
+ '@rolldown/binding-linux-x64-musl': 1.2.9
+ '@rolldown/binding-openharmony-arm64': 1.2.9
+ '@rolldown/binding-win32-arm64-msvc': 1.2.9
+ '@rolldown/binding-win32-x64-msvc': 1.2.9
+
+ satteri@0.10.5:
+ dependencies:
+ '@types/estree-jsx': 1.0.5
+ '@types/hast': 3.0.5
+ '@types/mdast': 4.0.4
+ '@types/unist': 3.0.3
+ optionalDependencies:
+ '@bruits/satteri-darwin-arm64': 0.10.5
+ '@bruits/satteri-darwin-x64': 0.10.5
+ '@bruits/satteri-linux-arm64-gnu': 0.10.5
+ '@bruits/satteri-linux-arm64-musl': 0.10.5
+ '@bruits/satteri-linux-x64-gnu': 0.10.5
+ '@bruits/satteri-linux-x64-musl': 0.10.5
+ '@bruits/satteri-wasm32-wasi': 0.10.5
+ '@bruits/satteri-win32-arm64-msvc': 0.10.5
+ '@bruits/satteri-win32-x64-msvc': 0.10.5
+
+ sax@1.6.1: {}
+
+ semver@7.8.5: {}
+
+ sharp@0.35.4(@types/node@24.13.5):
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.8.5
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.35.4
+ '@img/sharp-darwin-x64': 0.35.4
+ '@img/sharp-freebsd-wasm32': 0.35.4
+ '@img/sharp-libvips-darwin-arm64': 1.3.3
+ '@img/sharp-libvips-darwin-x64': 1.3.3
+ '@img/sharp-libvips-linux-arm': 1.3.3
+ '@img/sharp-libvips-linux-arm64': 1.3.3
+ '@img/sharp-libvips-linux-ppc64': 1.3.3
+ '@img/sharp-libvips-linux-riscv64': 1.3.3
+ '@img/sharp-libvips-linux-s390x': 1.3.3
+ '@img/sharp-libvips-linux-x64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+ '@img/sharp-linux-arm': 0.35.4
+ '@img/sharp-linux-arm64': 0.35.4
+ '@img/sharp-linux-ppc64': 0.35.4
+ '@img/sharp-linux-riscv64': 0.35.4
+ '@img/sharp-linux-s390x': 0.35.4
+ '@img/sharp-linux-x64': 0.35.4
+ '@img/sharp-linuxmusl-arm64': 0.35.4
+ '@img/sharp-linuxmusl-x64': 0.35.4
+ '@img/sharp-webcontainers-wasm32': 0.35.4
+ '@img/sharp-win32-arm64': 0.35.4
+ '@img/sharp-win32-ia32': 0.35.4
+ '@img/sharp-win32-x64': 0.35.4
+ '@types/node': 24.13.5
+
+ shiki@4.4.3:
+ dependencies:
+ '@shikijs/core': 4.4.3
+ '@shikijs/engine-javascript': 4.4.3
+ '@shikijs/engine-oniguruma': 4.4.3
+ '@shikijs/langs': 4.4.3
+ '@shikijs/themes': 4.4.3
+ '@shikijs/types': 4.4.3
+ '@shikijs/vscode-textmate': 10.0.2
+ '@types/hast': 3.0.5
+
+ sisteransi@1.0.5: {}
+
+ sitemap@9.0.1:
+ dependencies:
+ '@types/node': 24.13.5
+ '@types/sax': 1.2.7
+ arg: 5.0.2
+ sax: 1.6.1
+
+ smol-toml@1.8.0: {}
+
+ source-map-js@1.2.1: {}
+
+ space-separated-tokens@2.0.2: {}
+
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
+ svgo@4.1.0:
+ dependencies:
+ commander: 11.1.0
+ css-select: 6.0.0
+ css-tree: 3.2.1
+ css-what: 7.0.0
+ csso: 5.0.5
+ picocolors: 1.1.1
+ sax: 1.6.1
+
+ tiny-inflate@1.0.3: {}
+
+ tinyclip@0.1.15: {}
+
+ tinyexec@1.3.1: {}
+
+ tinyglobby@0.2.17:
+ dependencies:
+ fdir: 6.5.0(picomatch@4.0.7)
+ picomatch: 4.0.7
+
+ trim-lines@3.0.1: {}
+
+ trough@2.2.0: {}
+
+ tslib@2.8.1:
+ optional: true
+
+ ufo@1.6.4: {}
+
+ ultrahtml@1.7.0: {}
+
+ uncrypto@0.1.3: {}
+
+ undici-types@7.18.2: {}
+
+ undici@8.10.2: {}
+
+ unified@11.0.5:
+ dependencies:
+ '@types/unist': 3.0.3
+ bail: 2.0.2
+ devlop: 1.1.0
+ extend: 3.0.2
+ is-plain-obj: 4.1.0
+ trough: 2.2.0
+ vfile: 6.0.3
+
+ unifont@0.7.5:
+ dependencies:
+ css-tree: 3.2.1
+ ohash: 2.0.12
+ undici: 8.10.2
+
+ unist-util-find-after@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-is@6.0.1:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-stringify-position@4.0.0:
+ dependencies:
+ '@types/unist': 3.0.3
+
+ unist-util-visit-parents@6.0.2:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+
+ unist-util-visit@5.1.0:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-is: 6.0.1
+ unist-util-visit-parents: 6.0.2
+
+ unstorage@1.17.5:
+ dependencies:
+ anymatch: 3.1.3
+ chokidar: 5.0.0
+ destr: 2.0.5
+ h3: 1.15.11
+ lru-cache: 11.5.2
+ node-fetch-native: 1.6.7
+ ofetch: 1.5.1
+ ufo: 1.6.4
+
+ url-extras@0.1.0: {}
+
+ util-deprecate@1.0.2: {}
+
+ verkit@0.4.0: {}
+
+ vfile-message@4.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ unist-util-stringify-position: 4.0.0
+
+ vfile@6.0.3:
+ dependencies:
+ '@types/unist': 3.0.3
+ vfile-message: 4.0.3
+
+ vite@8.3.0(@types/node@24.13.5)(esbuild@0.28.2):
+ dependencies:
+ lightningcss: 1.33.0
+ picomatch: 4.0.7
+ postcss: 8.5.28
+ rolldown: 1.2.9
+ tinyglobby: 0.2.17
+ optionalDependencies:
+ '@types/node': 24.13.5
+ esbuild: 0.28.2
+ fsevents: 2.3.3
+
+ vitefu@1.1.3(vite@8.3.0(@types/node@24.13.5)(esbuild@0.28.2)):
+ optionalDependencies:
+ vite: 8.3.0(@types/node@24.13.5)(esbuild@0.28.2)
+
+ xxhash-wasm@1.1.0: {}
+
+ yargs-parser@22.0.0: {}
+
+ yocto-queue@1.2.2: {}
+
+ zod@4.6.5: {}
+
+ zwitch@2.0.4: {}
diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml
new file mode 100644
index 00000000..dbb26c82
--- /dev/null
+++ b/docs/pnpm-workspace.yaml
@@ -0,0 +1,3 @@
+allowBuilds:
+ esbuild: true
+ sharp: true
diff --git a/docs/public/CNAME b/docs/public/CNAME
new file mode 100644
index 00000000..002be24b
--- /dev/null
+++ b/docs/public/CNAME
@@ -0,0 +1 @@
+markbridge.dev
diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png
new file mode 100644
index 00000000..cc1ef154
Binary files /dev/null and b/docs/public/apple-touch-icon.png differ
diff --git a/docs/public/diagram-zoom.js b/docs/public/diagram-zoom.js
new file mode 100644
index 00000000..bbf5f664
--- /dev/null
+++ b/docs/public/diagram-zoom.js
@@ -0,0 +1,80 @@
+// Click-to-zoom for embedded diagrams.
+//
+// Uses a native