# @dr-ishaan/remake-blocks

Astro 6 + remark plugin for rendering GitHub-style callouts (admonitions), enhanced blockquotes, disclosure widgets, pull quotes, and epigraphs in Markdown content — **27 first-class types + 4 themes + inline callouts + directive syntax + safe-by-default HTML escaping + Lucide/emoji icon sets**.

Each directive maps 1:1 to its own unique visual identity, SVG icon, color palette, and CSS class. No alias resolution.

---

## Features

### Callout types & syntax
- **27 built-in callout types** — GFM standard (5) + Obsidian core (10) + promoted aliases (12)
- **GitHub admonition syntax** — `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, etc.
- **Directive syntax** — `:::note[Title]` ... `:::` (Starlight/Docusaurus compat, opt-in)
- **MkDocs admonition syntax** — `!!! note`, `??? note`, `???+ note` (MkDocs Material compat, opt-in)
- **Custom titles** — `> [!WARNING] Deprecated API`
- **Collapsible callouts** — `> [!FAQ]-` (collapsed) and `> [!TIP]+` (expanded)
- **Disclosure widgets** — `> [!] Title` creates a plain collapsible block
- **Pull quotes** — `> [!PULL]` creates a large, centered, italic display quote with auto-detected `<cite>` attribution
- **Epigraphs** — `> [!EPIGRAPH]` creates a small, centered, bold italic quote with `<cite>` attribution
- **Attribution auto-detection** — `— Author` or `-- Author` at end of body is auto-detected and rendered as `<cite>`
- **Accordion grouping** — consecutive `[!]` blocks auto-group into an accordion
- **Tree view** — nested `[!]` blocks get depth indicators
- **Custom callout types** — define your own with icon, color, and CSS class
- **Configurable aliases** — `aliases: { note: ["n"] }` for short names

### Themes & styling
- **4 swappable CSS themes** — github, obsidian, vitepress, docusaurus
- **Inline callouts** — `{inline}` floats left, `{inline-end}` floats right (responsive)
- **Appearance variants** — `{appearance=minimal}`, `{appearance=hidden}`, etc.
- **Icon sets** — octicon (default), lucide, emoji, none
- **Per-callout named icon** — `{icon="rocket"}` uses a specific Lucide icon
- **Enhanced blockquotes** — rounded left accent line, darker text, auto-styled
- **i18n label customization** — `labels: { note: "注意" }` for localized titles
- **Dark mode** — automatic via `prefers-color-scheme` + manual via `data-theme` attribute
- **Auto CSS injection** — styles injected into every page by default
- **CSS custom properties** — full `var()` system for easy customization

### Accessibility (WCAG-compliant)
- **`role="note"`** for ALL callout types (no `role="alert"` on static content — WCAG fix)
- **`aria-labelledby`** linking container → title id
- **`aria-hidden="true"`** on all decorative icons
- **`dir="auto"`** on container + title (RTL/Unicode support)
- **`data-collapsible`** attribute on all callout containers
- **Semantic HTML** — `<aside>` for non-collapsible, `<details>`/`<summary>` for collapsible
- **Zero JavaScript** for callouts — native HTML5 (collapsibles use `<details>`/`<summary>`)
- **axe-core verified** — 0 critical/serious violations across all 27 types

### Security (safe-by-default)
- **HTML escaping** — raw HTML in markdown body is escaped by default
- **`allowDangerousHtml` opt-in** — set `true` only if you trust your markdown source
- **`className`/`color` escaping** — prevents attribute breakout from config values
- **Custom callout config validation** — missing fields use defaults, no crashes
- **rehype-sanitize schema** — shipped as `sanitize-schema.json` for strict pipelines
- **Directive attribute security** — event handlers (`on*`) and `style` blocked

### Power-user features
- **`tags` option** — override container/title/body element names
- **`props` option** — per-type static or dynamic HTML attributes (function form)
- **`build()` escape hatch** — full custom render function for total output control
- **Directive attributes** — `:::note[Title]{#id .class key="value"}` (remark-directive compat)
- **Exported helpers** — `escapeHtml`, `escapeAttribute`, `sanitizeColor` for `build()` authors
- **Print-optimized** — clean print styles
- **Single dependency** — only `unist-util-visit`

---

## Install

```bash
npm install @dr-ishaan/remake-blocks
```

## Quick Start (Astro)

```js
// astro.config.mjs
import { defineConfig } from "astro/config";
import remakeBlocks from "@dr-ishaan/remake-blocks/astro";

export default defineConfig({
  integrations: [remakeBlocks()],
});
```

Then use callouts in any Markdown or MDX file:

```markdown
> [!NOTE]
> This is a note callout with useful information.

> [!TIP] Keyboard Shortcut
> Press `Ctrl+Shift+P` to open the command palette.

> [!WARNING]-
> This collapsible warning starts collapsed.
```

## Quick Start (Remark only)

```js
import { unified } from "unified";
import remarkParse from "remark-parse";
import { remarkRemakeBlocks } from "@dr-ishaan/remake-blocks";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";

const processor = unified()
  .use(remarkParse)
  .use(remarkRemakeBlocks)
  .use(remarkRehype, { allowDangerousHtml: true })  // pass through callout HTML
  .use(rehypeStringify, { allowDangerousHtml: true });
```

---

## All 27 Callout Types

### GFM Standard (5)

| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| `[!NOTE]` | Note | `callout-note` |
| `[!TIP]` | Tip | `callout-tip` |
| `[!IMPORTANT]` | Important | `callout-important` |
| `[!WARNING]` | Warning | `callout-warning` |
| `[!CAUTION]` | Caution | `callout-caution` |

### Obsidian Core (10)

| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| `[!ABSTRACT]` | Abstract | `callout-abstract` |
| `[!INFO]` | Info | `callout-info` |
| `[!SUCCESS]` | Success | `callout-success` |
| `[!QUESTION]` | Question | `callout-question` |
| `[!FAILURE]` | Failure | `callout-failure` |
| `[!DANGER]` | Danger | `callout-danger` |
| `[!QUOTE]` | Quote | `callout-quote` |
| `[!BUG]` | Bug | `callout-bug` |
| `[!EXAMPLE]` | Example | `callout-example` |
| `[!TODO]` | Todo | `callout-todo` |

### Promoted Aliases — First-Class Types (12)

Each of these is its **own distinct type** with unique colors, not an alias:

| Directive | Default Title | CSS Class |
|-----------|--------------|-----------|
| `[!SUMMARY]` | Summary | `callout-summary` |
| `[!TLDR]` | TL;DR | `callout-tldr` |
| `[!HINT]` | Hint | `callout-hint` |
| `[!CHECK]` | Check | `callout-check` |
| `[!DONE]` | Done | `callout-done` |
| `[!HELP]` | Help | `callout-help` |
| `[!FAQ]` | FAQ | `callout-faq` |
| `[!ATTENTION]` | Attention | `callout-attention` |
| `[!FAIL]` | Fail | `callout-fail` |
| `[!MISSING]` | Missing | `callout-missing` |
| `[!ERROR]` | Error | `callout-error` |
| `[!CITE]` | Cite | `callout-cite` |

### Disclosure Widgets

| Directive | Description |
|-----------|-------------|
| `[!]` | Plain collapsible block (no color, no icon) — uses native `<details>`/`<summary>` |

Consecutive `[!]` blocks are automatically grouped into an accordion. Nested `[!]` blocks get tree-view styling.

### Pull Quotes & Epigraphs

| Directive | Description | CSS Class |
|-----------|-------------|-----------|
| `[!PULL]` | Large, centered, italic display quote. Auto-detects `— Author` → `<cite>`. Supports `{inline}` for floating. | `.pull-quote` |
| `[!EPIGRAPH]` | Small, centered, bold italic quote. Auto-detects `— Author` → `<cite>`. Max-width 70%. | `.epigraph` |

**Attribution auto-detection**: If the last line of the body starts with `—` (em dash) or `--` (double hyphen), it's automatically extracted and rendered as a `<cite>` element. If no attribution is found, the entire body is the quote text.

---

## Syntax

### Blockquote callouts (primary syntax)

```markdown
> [!NOTE]
> Content goes here.

> [!WARNING] Breaking Change
> The v2 API has been deprecated.

> [!FAQ]-
> Click to reveal the answer.

> [!TIP]+
> This starts expanded but can be collapsed.
```

### Directive syntax (Starlight/Docusaurus compat)

Opt-in via `enableDirectiveSyntax: true`:

```markdown
:::note
Basic callout.
:::

:::note[Custom Title]
With title.
:::

:::warning{fold=-}
Collapsed callout.
:::

:::note[Title]{#my-id .custom-class data-type="example"}
With full directive attributes.
:::
```

### MkDocs admonition syntax (MkDocs Material compat)

Opt-in via `enableMkDocsSyntax: true`:

```markdown
!!! note
    Non-collapsible callout (4-space indented body).

??? note
    Collapsed callout.

???+ note
    Expanded collapsible callout.

!!! note "Custom Title"
    With custom title.
```

### Per-callout overrides

Use `{key=value}` syntax after the directive:

```markdown
> [!NOTE]{icon=false}            — hide icon for this callout
> [!NOTE]{icon="rocket"}         — use a specific Lucide icon
> [!NOTE]{appearance=minimal}     — minimal appearance variant
> [!NOTE]{appearance=hidden}      — no title, no icon
> [!NOTE]{inline}                 — float left (responsive)
> [!NOTE]{inline-end}             — float right (responsive)
> [!NOTE]{icon=false inline appearance=minimal}  — multiple overrides
```

### Disclosure widgets

```markdown
> [!] Section Title
> Content hidden by default.

> [!] Another Section
> Content hidden by default.

> [!]+ Expanded Section
> Content visible by default.
```

### Pull quotes

```markdown
> [!PULL]
> The best way to predict the future is to invent it.
> — Alan Kay

> [!PULL]{inline}
> A floated pull quote with text wrapping around it.
> — Someone

> [!PULL]
> A pull quote with no attribution.
```

### Epigraphs

```markdown
> [!EPIGRAPH]
> The fog comes on little cat feet.
> — Carl Sandburg

> [!EPIGRAPH]
> An epigraph with no attribution.
```

---

## Configuration

```js
// astro.config.mjs
import { defineConfig } from "astro/config";
import remakeBlocks from "@dr-ishaan/remake-blocks/astro";

export default defineConfig({
  integrations: [
    remakeBlocks({
      // ── Core ──────────────────────────────────────────────
      injectStyles: true,              // auto-inject CSS (default: true)
      enhanceBlockquotes: true,        // style regular blockquotes (default: true)
      dataCalloutType: true,           // add data-callout-type attribute (default: true)
      allowDangerousHtml: false,       // ⚠️ safe-by-default HTML escaping (default: false)

      // ── Callout classes ──────────────────────────────────
      calloutClass: "callout",
      calloutTitleClass: "callout-title",
      calloutBodyClass: "callout-body",

      // ── Disclosure widgets ───────────────────────────────
      enableDisclosures: true,         // enable [!] syntax (default: true)
      enableAccordion: true,           // auto-group consecutive [!] (default: true)
      enableTreeView: true,            // nested [!] tree view (default: true)

      // ── Alternative syntaxes (opt-in) ────────────────────
      enableDirectiveSyntax: false,    // :::note[Title] syntax (default: false)
      enableMkDocsSyntax: false,       // !!! note / ??? note syntax (default: false)

      // ── Visual options ───────────────────────────────────
      showIndicator: true,             // show icons globally (default: true)
      iconSet: "octicon",              // "octicon" | "lucide" | "emoji" | "none"
      appearance: "default",           // "default" | "minimal" | "simple" | "hidden"

      // ── i18n labels ──────────────────────────────────────
      labels: {
        note: "注意",
        tip: "ヒント",
        warning: "警告",
      },

      // ── Aliases ──────────────────────────────────────────
      aliases: {
        note: ["n"],
        tip: ["t"],
      },

      // ── Power-user options ───────────────────────────────
      tags: {                          // override HTML element names
        container: "aside",
        title: "div",
        body: "div",
      },
      props: {                         // per-type HTML attributes (static or function)
        note: { "data-category": "info" },
        warning: (parsed) => ({ "data-severity": parsed.collapsible ? "collapsible" : "static" }),
      },
      build: (parsed, config, bodyHtml, options) => {  // full custom render
        return `<div class="my-callout">${bodyHtml}</div>`;
      },

      // ── Custom callout types ─────────────────────────────
      customCallouts: [
        {
          type: "machine-learning",
          icon: "🧠",
          className: "callout-ml",
          defaultTitle: "Machine Learning",
          color: "#7c3aed",
          backgroundColor: "#f5f3ff",
        },
      ],
    }),
  ],
});
```

---

## Themes

4 swappable CSS themes are available. Import the one you want:

```js
// Default (github) — auto-injected when injectStyles: true
import "@dr-ishaan/remake-blocks/styles.css";

// Or pick a specific theme:
import "@dr-ishaan/remake-blocks/theme/github.css";       // GitHub style
import "@dr-ishaan/remake-blocks/theme/obsidian.css";     // Obsidian style
import "@dr-ishaan/remake-blocks/theme/vitepress.css";    // VitePress style
import "@dr-ishaan/remake-blocks/theme/docusaurus.css";   // Docusaurus style
```

Each theme includes:
- Full light + dark mode (`prefers-color-scheme` + `data-theme` attribute)
- All 27 callout types with per-type colors
- Enhanced blockquote styling (rounded left accent line)
- Disclosure widget styling (card with rounded left accent line)
- Accordion + tree view styling
- Inline callout CSS (responsive)
- Print-optimized styles

---

## Security: Safe-by-Default

By default, **raw HTML in markdown is HTML-escaped** before being placed in callout bodies. This prevents XSS attacks from untrusted markdown content.

```markdown
> [!NOTE]
> <img src=x onerror=alert(1)>
```

**Default behavior** (`allowDangerousHtml: false`):
```html
<aside class="callout callout-note" ...>
  ...
  <div class="callout-body">
    <p>&lt;img src=x onerror=alert(1)&gt;</p>
  </div>
</aside>
```

**Opt-in to raw HTML** (`allowDangerousHtml: true`):
```html
<aside class="callout callout-note" ...>
  ...
  <div class="callout-body">
    <p><img src=x onerror=alert(1)></p>  <!-- ⚠️ XSS risk! -->
  </div>
</aside>
```

Only set `allowDangerousHtml: true` if:
1. You fully trust your markdown source, **AND**
2. You have sanitized the markdown with a tool like `rehype-sanitize`

### rehype-sanitize schema

A compatible sanitize schema is shipped for strict security pipelines:

```js
import schema from "@dr-ishaan/remake-blocks/sanitize-schema.json";
import rehypeSanitize from "rehype-sanitize";

// In your pipeline:
const processor = unified()
  .use(remarkParse)
  .use(remarkRemakeBlocks)
  .use(remarkRehype, { allowDangerousHtml: true })
  .use(rehypeSanitize, schema)
  .use(rehypeStringify);
```

### Custom Callout Config Validation

Custom callout configurations are validated and normalized:
- Missing fields are filled with safe defaults (no crash)
- `null` or non-object entries are skipped
- `className` is HTML-escaped (prevents attribute breakout)
- `color` is validated against a safe CSS color pattern; invalid values fall back to gray

---

## Custom Callout Types

Define your own callout types with full control over appearance:

```js
remakeBlocks({
  customCallouts: [
    {
      type: "machine-learning",     // [!MACHINE-LEARNING]
      icon: "🧠",                    // Emoji or SVG string
      className: "callout-ml",      // CSS class
      defaultTitle: "Machine Learning",
      color: "#7c3aed",             // Border & accent color
      backgroundColor: "#f5f3ff",   // Background color
      iconColor: "#7c3aed",         // Icon color (optional, defaults to color)
    },
  ],
})
```

If a custom callout's `type` matches a built-in, the built-in is overridden.

---

## i18n Label Customization

Override default titles for any callout type — useful for non-English documentation:

```js
remakeBlocks({
  labels: {
    note: "注意",
    tip: "ヒント",
    warning: "警告",
    caution: "危険",
    important: "重要",
  },
})
```

Works with all 27 builtin types, custom callout types, and all 3 syntaxes (blockquote, directive, MkDocs).

---

## Dark Mode

Dark mode is supported automatically via `prefers-color-scheme: dark` and manually via the `data-theme` attribute:

```html
<html data-theme="dark">
  <!-- callouts switch to dark palette -->
</html>
```

Supported `data-theme` values: `"dark"`, `"dim"`.

---

## CSS Custom Properties

Override any design token at the `:root` level:

```css
:root {
  --callout-radius: 8px;
  --callout-padding-y: 12px;
  --callout-padding-x: 16px;
  --callout-icon-size: 24px;
  --callout-title-font-size: 1.1em;

  /* Per-type overrides */
  --callout-note-border: #3b82f6;
  --callout-note-bg: #eff6ff;
}
```

See `styles.css` for the complete list of CSS custom properties.

---

## HTML Output

Each callout renders as:

```html
<aside class="callout callout-note" data-callout-type="note" data-collapsible="false"
       role="note" aria-labelledby="callout-note-1" dir="auto">
  <div class="callout-title" style="color:#0969da" id="callout-note-1" dir="auto">
    <span class="callout-icon" style="color:#0969da" aria-hidden="true">
      <!-- SVG icon -->
    </span>
    <span class="callout-title-text">Note</span>
  </div>
  <div class="callout-body">
    <p>Content goes here.</p>
  </div>
</aside>
```

Collapsible callouts use `<details>`/`<summary>`:

```html
<details class="callout callout-faq collapsible" data-callout-type="faq" data-collapsible="true"
         role="note" aria-labelledby="callout-faq-1" dir="auto">
  <summary class="callout-title" style="color:#b45309" id="callout-faq-1" dir="auto">
    <span class="callout-icon" style="color:#b45309" aria-hidden="true"><!-- SVG --></span>
    <span class="callout-title-text">FAQ</span>
  </summary>
  <div class="callout-body">
    <p>Content revealed on click.</p>
  </div>
</details>
```

Pull quotes render as:

```html
<aside class="pull-quote" dir="auto">
  <p class="pull-quote-text">The best way to predict the future is to invent it.</p>
  <p class="pull-quote-attribution">— <cite>Alan Kay</cite></p>
</aside>
```

Epigraphs render as:

```html
<aside class="epigraph" dir="auto">
  <p class="epigraph-text">The fog comes on little cat feet.</p>
  <p class="epigraph-attribution">— <cite>Carl Sandburg</cite></p>
</aside>
```

---

## Exported Helpers

For users who supply a custom `build()` render function:

```js
import { escapeHtml, escapeAttribute, sanitizeColor } from "@dr-ishaan/remake-blocks";
```

| Helper | Description |
|--------|-------------|
| `escapeHtml(str)` | Escapes `& < > " '` to HTML entities |
| `escapeAttribute(str)` | Same as `escapeHtml`, for attribute values |
| `sanitizeColor(value, fallback)` | Validates CSS color; returns fallback if dangerous |

---

## Caveats

### Strikethrough (`~~text~~`) requires `remark-gfm`

The plugin's body serializer handles `<del>` nodes correctly, but `remark-parse` alone does **not** enable GFM strikethrough. To use `~~strikethrough~~` syntax in callout bodies, add `remark-gfm`:

```js
import remarkGfm from "remark-gfm";

export default defineConfig({
  markdown: { remarkPlugins: [remarkGfm] },
  integrations: [remakeBlocks()],
});
```

### Horizontal rule after directive becomes a setext H2

In CommonMark, `> [!NOTE]\n> ---` is interpreted as a setext H2 heading, **not** as a callout with a thematic break body. Add a blank line before `---`:

```markdown
> [!NOTE]
> Above
>
> ---
>
> Below
```

### Tables and other GFM features require `remark-gfm`

GFM-only features like tables, autolinks, and task list items require the `remark-gfm` plugin.

---

## How It Works: Parsing Pipeline

The plugin processes markdown through 4 passes. Each syntax is detected at a different stage:

### Pass overview

| Pass | Name | When it runs | What it detects | Output |
|------|------|-------------|-----------------|--------|
| 0a | Directive syntax transformer | `enableDirectiveSyntax: true` | `:::type[Title]{attrs}` ... `:::` paragraphs | Callout HTML node |
| 0b | MkDocs syntax transformer | `enableMkDocsSyntax: true` | `!!! type`, `??? type`, `???+ type` paragraphs | Callout HTML node |
| 1 | Blockquote callout transformer | Always runs | `> [!TYPE]` in blockquote nodes | Callout HTML node or enhanced blockquote |
| 2 | Accordion grouping | `enableAccordion: true` | Consecutive disclosure `<details>` nodes | `<div class="disclosure-accordion">` wrapper |

### Syntax detection methods

| Syntax | How remark-parse sees it | How the plugin detects it | Key parsing logic |
|--------|------------------------|--------------------------|-------------------|
| **Blockquote** (`> [!NOTE]`) | `blockquote` → `paragraph` → `text` starting with `[!TYPE]` | Regex on first text child: `^\[!(NOTE\|TIP\|...)\]([+-]?)(title)?({overrides})?` | Dynamically built regex from registered types + aliases + empty string for `[!]` disclosures |
| **Directive** (`:::note`) | `paragraph` → `text` containing entire `:::type...\n:::` block | `text.startsWith(":::")` → count colons → extract type → parse `[Title]` → parse `{attrs}` → find closing `:::` | String methods (not regex `^` — avoids Node.js anchor issue); body is everything between opening and closing lines |
| **MkDocs** (`!!! note`) | `paragraph` → `text` containing entire `!!! type\n    body` block | `text.startsWith("!!!")` or `text.startsWith("???")` → extract marker → extract type → parse `"Title"` → body is 4-space indented | String methods; strips 4-space indent from each body line |

### Blockquote callout regex capture groups

| Group | Content | Example | Used for |
|-------|---------|---------|----------|
| `[1]` | Type name (or empty for `[!]`) | `note`, `warning`, `""` | Look up callout config or identify as disclosure |
| `[2]` | Fold marker | `+`, `-`, or `""` | `+` = expanded collapsible, `-` = collapsed, `""` = not collapsible |
| `[3]` | Custom title text | `Breaking Change` | Override default title |
| `[4]` | Overrides block | `{icon=false appearance=minimal}` | Parsed by `parseOverrides()` + `parseDirectiveAttrs()` |

### Disclosure, Pull Quote & Epigraph detection

The `[!]`, `[!PULL]`, and `[!EPIGRAPH]` syntaxes are all detected in the **same blockquote pass** as regular callouts. The key difference is what's added to the regex alternatives:

- `[!]` → empty string `""` added to regex (when `enableDisclosures: true`)
- `[!PULL]` → `"PULL"` always added to regex
- `[!EPIGRAPH]` → `"EPIGRAPH"` always added to regex

| Aspect | Regular callout (`> [!NOTE]`) | Disclosure (`> [!]`) |
|--------|------------------------------|---------------------|
| **Type name** | `note`, `warning`, etc. | `disclosure` (synthetic) |
| **Config lookup** | `configMap.get(type)` → icon, color, className | No config — plain |
| **Icon** | SVG icon from iconSet | None |
| **Color** | Per-type border/bg colors | None (gray left line only) |
| **CSS class** | `callout callout-note` | `disclosure` |
| **Container** | `<aside>` or `<details>` | Always `<details>` |
| **Collapsible** | Only if `+`/`-` marker present | Always collapsible |
| **Default title** | From config (e.g., "Note") | "Details" |
| **data-callout-type** | `note`, `warning`, etc. | Not set |
| **role** | `role="note"` | Not set (uses native `<details>`) |
| **Left accent line** | Colored (matches callout type) | Gray (`#d0d7de`) |
| **Fold markers** | `+`/`-` = collapsible, none = not collapsible | `+`/none = expanded, `-` = collapsed |

### `{overrides}` block parsing

Two parsers run on the `{...}` block (used by all 3 syntaxes):

| Parser | Input pattern | Output keys | Purpose |
|--------|--------------|-------------|---------|
| `parseOverrides()` | `icon=false`, `appearance=minimal`, `inline`, `icon="rocket"` | `overrides.icon`, `overrides.appearance`, `overrides.inline`, `overrides.iconName` | Plugin-specific per-callout visual overrides |
| `parseDirectiveAttrs()` | `#my-id`, `.custom-class`, `data-x="y"` | `directiveAttrs.id`, `directiveAttrs.classes`, `directiveAttrs.attrs` | remark-directive compatible HTML attributes |

### Override keys reference

| Key | Values | Effect | Example |
|-----|--------|--------|---------|
| `icon` | `false`, `true`, `"name"` | Hide icon / show icon / use named Lucide icon | `{icon=false}`, `{icon="rocket"}` |
| `appearance` | `default`, `minimal`, `simple`, `hidden` | Visual density variant | `{appearance=minimal}` |
| `inline` | `inline`, `inline-end` (or bare keywords) | Float left / float right (responsive) | `{inline}`, `{inline-end}` |
| `fold` | `+`, `-` | Collapsible expanded / collapsed (directive syntax only) | `{fold=-}` |
| `#id` | Any valid HTML id | Sets element id | `{#my-section}` |
| `.class` | Any valid CSS class name | Adds CSS class | `{.highlight}` |
| `key="value"` | Any key/value pair | Adds HTML attribute (event handlers and `style` blocked) | `{data-type="example"}` |

### Processing order

| Step | What happens | Why this order |
|------|-------------|----------------|
| 1. remark-parse | Raw markdown → mdast tree | Must happen before plugin (external) |
| 2. Pass 0a: Directive | `:::type` paragraphs → callout HTML | Must run before blockquote pass (directive paragraphs are not blockquotes) |
| 3. Pass 0b: MkDocs | `!!! type` paragraphs → callout HTML | Same reason — paragraph-level, not blockquote-level |
| 4. Pass 1: Blockquote | `> [!TYPE]` blockquotes → callout HTML | Processes deepest blockquotes first (inside-out) so nested callouts are resolved before parents |
| 5. Pass 2: Accordion | Groups consecutive disclosure `<details>` | Must run after all disclosures are rendered |

### `ParsedCallout` object (internal)

All 3 syntaxes produce the same `ParsedCallout` object, which is passed to `buildCalloutHtml()`:

| Field | Type | Source | Description |
|-------|------|--------|-------------|
| `type` | `string` | Regex group [1] | Callout type (lowercased), `"disclosure"`, `"pull"`, or `"epigraph"` |
| `customTitle` | `string \| undefined` | Regex group [3] or `[Title]` or `"Title"` | User-provided title (overrides default) |
| `collapsible` | `boolean` | Fold marker or `???`/`???+` | Whether callout is collapsible |
| `collapsibleOpen` | `boolean` | `+` marker or `???+` or no marker (disclosures) | Whether collapsible starts expanded |
| `isDisclosure` | `boolean` | Empty type match | True for `[!]` syntax |
| `isPullQuote` | `boolean` | Type = `"pull"` | True for `[!PULL]` syntax |
| `isEpigraph` | `boolean` | Type = `"epigraph"` | True for `[!EPIGRAPH]` syntax |
| `overrides` | `object \| undefined` | `{overrides}` block via `parseOverrides()` | Per-callout icon/appearance/inline overrides |
| `directiveAttrs` | `object \| undefined` | `{attrs}` block via `parseDirectiveAttrs()` | Per-callout id/class/HTML attributes |

---

## Advanced: Using the Remark Plugin Directly

The remark plugin can be used independently of the Astro integration:

```js
import { unified } from "unified";
import remarkParse from "remark-parse";
import { remarkRemakeBlocks } from "@dr-ishaan/remake-blocks";

const processor = unified()
  .use(remarkParse)
  .use(remarkRemakeBlocks, { enhanceBlockquotes: true });
```

A backward-compatible alias `remarkCalloutBlocks` is also exported:

```js
import { remarkCalloutBlocks } from "@dr-ishaan/remake-blocks";
```

---

## License

MIT

## Plugin Order (v2.2.0+)

When using `remake-blocks` with other remark/rehype plugins, the correct order is:

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import schema from '@dr-ishaan/remake-blocks/sanitize-schema.json';

export default defineConfig({
  integrations: [remakeBlocks()],
  markdown: {
    remarkPlugins: [
      // remake-blocks must run BEFORE remark-rehype (it transforms mdast)
      // No explicit registration needed — the Astro integration handles it.
    ],
    rehypePlugins: [
      // 1. rehype-raw — must come FIRST to parse raw HTML emitted by callouts
      rehypeRaw,
      // 2. rehype-sanitize — must come AFTER rehype-raw, uses the bundled schema
      [rehypeSanitize, schema],
      // 3. Other rehype plugins (syntax highlighters, etc.) run last
    ],
  },
});
```

**Key ordering rules:**
1. `remarkRemakeBlocks` runs as a remark plugin (before remark-rehype) — it transforms the mdast tree.
2. `rehype-raw` must come before `rehype-sanitize` so raw HTML from callouts is parsed into hast nodes before sanitization.
3. The bundled `sanitize-schema.json` allows all callout elements + attributes while stripping `<script>`, `on*=` handlers, and `javascript:` URLs.
4. For syntax highlighters (e.g. `rehype-pretty-code`), place them AFTER `rehype-raw` so they can process code blocks inside callout bodies.

### MDX Compatibility (v2.2.0+)

The plugin works with `.mdx` files. Since `remake-blocks` is a standard remark plugin, it runs during MDX's markdown parsing phase — before MDX component processing. Callout directives in MDX files are transformed the same way as in `.md` files.

```js
// astro.config.mjs
import mdx from '@astrojs/mdx';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';

export default defineConfig({
  integrations: [
    remakeBlocks(),
    mdx(),
  ],
});
```

**MDX-specific notes:**
- Callouts inside MDX components: supported (the remark plugin runs before component rendering).
- MDX components inside callouts: supported (callout bodies accept any markdown content, including MDX expressions).
- JSX inside callout bodies: requires `allowDangerousHtml: true` or proper MDX evaluation.

### Content Collections Compatibility (v2.2.0+)

The plugin works with Astro Content Collections (both legacy and Content Layer API). Since the remark plugin is registered via `updateConfig({ markdown: { remarkPlugins: [...] } })`, it runs during markdown rendering for all content — including collection entries loaded via `glob()`, `file()`, or custom loaders.

```js
// src/content/config.ts
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';

const blog = defineCollection({
  loader: glob({ pattern: '**/*.md', base: './src/content/blog' }),
  schema: z.object({
    title: z.string(),
    date: z.date(),
  }),
});

export const collections = { blog };
```

No special configuration needed — callout directives in collection markdown files are transformed automatically.

### View Transitions Compatibility (v2.2.0+)

The `accordion.js` runtime script handles Astro View Transitions:

- On initial page load: `DOMContentLoaded` event initializes accordion behavior.
- After View Transitions: `astro:page-load` event re-initializes (idempotent — uses `data-enhanced` flag to skip already-processed accordions).
- Collapsible `<details>` state: each page transition starts fresh (collapsed/expanded state does NOT persist across pages — this is correct behavior).

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import remakeBlocks from '@dr-ishaan/remake-blocks/astro';

export default defineConfig({
  integrations: [remakeBlocks()],
});
```

View Transitions work out-of-the-box — no additional configuration needed.
