npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dr-ishaan/remake-blocks

v3.0.2

Published

Astro 6 + remark plugin — 42 callout types, 4 themes, CSS layers, design tokens, type allow/blocklists, default collapse, multi-syntax, icon sprite, minimal theme CSS, dev warnings, config validation, CLASSES constants, per-type ARIA roles, sr-only icon t

Readme

@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 aliasesaliases: { 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 customizationlabels: { 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 helpersescapeHtml, escapeAttribute, sanitizeColor for build() authors
  • Print-optimized — clean print styles
  • Single dependency — only unist-util-visit

Install

npm install @dr-ishaan/remake-blocks

Quick Start (Astro)

// 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:

> [!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)

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)

> [!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:

:::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:

!!! 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:

> [!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

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

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

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

Pull quotes

> [!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

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

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

Configuration

// 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:

// 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.

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

Default behavior (allowDangerousHtml: false):

<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):

<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:

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:

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:

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 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:

: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:

<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>:

<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:

<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:

<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:

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:

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 ---:

> [!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]) | blockquoteparagraphtext 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) | paragraphtext 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) | paragraphtext 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:

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:

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:

// 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.

// 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.

// 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).
// 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.