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

@catatsumuri/inkstream

v0.1.3

Published

Markdown engine for Mintlify-style JSX tags, Zenn directives, and GFM: AST-based tag pairing instead of line-based string preprocessing.

Readme

inkstream

Markdown engine for Mintlify-style JSX tags, Zenn directives, and GFM, built as remark/rehype plugins over the mdast tree rather than a line-based string preprocessor. Supersedes an earlier draft (internally called "v1" below) that converted the same JSX tags to colon-fence directives with a ~1,850-line line-based preprocessor; that draft is frozen and unpublished.

Pipeline

  1. normalizeMintlifyBlocks(markdown) — line-based pre-pass that surrounds standalone tag lines with blank lines (skipping code fences) so remark parses each tag as its own html flow node, strips the authoring indentation inside open tags (up to the open tag's indent + 4, mirroring v1) so indented tag bodies don't become indented code blocks, and flattens JSX array attributes (tags={["A", "B"]} → tags="A,B"), which would otherwise make the tag invalid HTML for remark.
  2. remark-parse — standard markdown parsing.
  3. remarkMintlifyTags — pairs open/close html nodes with a stack (once for each parent's flow children, once for each paragraph's phrasing children) and lifts the nodes between a pair into a mintlifyContainer node carrying name, attributes, and data.hName/hProperties for remark-rehype. Block tag names (Note, Card, Steps, ...) pair at flow level or as a whole single-line paragraph; inline tag names (Badge, Tooltip) pair mid-paragraph without disturbing surrounding text.
  4. remarkCodeFenceComponents — converts ```tree, ```quiz, and ```chart:bar/```chart:radar fenced code blocks into mintlifyContainer nodes carrying the parsed structure as one JSON-string property (tree, quiz, chart) for a renderer component to read. Malformed fences emit a vfile warning and are left as plain code blocks.
  5. normalizeZennDirectiveShorthand(markdown) (run before step 1, alongside normalizeMintlifyBlocks) + remark-directive + remarkZennDirective — support for the native :::message / :::details authoring syntax an author can write directly (as opposed to the Mintlify JSX tags, which v2 never routes through colon-fences at all). The shorthand normalizer rewrites the friendly :::message alert / :::details タイトル forms into the {.class} / [label] syntax remark-directive requires; remarkZennDirective then reads the resulting containerDirective nodes remark-directive produces. This is the one piece of the pipeline that still needs a third-party directive parser, since colon-fence syntax itself isn't something a tag-pairing plugin over html nodes can parse. Like v1, the normalizer protects code fences and inline code spans, so literal `:::message alert` examples in prose survive; it also reduces Zenn's @[card](url) / @[github](url) embeds to bare URL lines for a linkify-style renderer to pick up.
  6. remarkGithubAlerts — normalizes GitHub blockquote alerts (> [!NOTE] etc.) onto the same aside.msg contract as the Mintlify callouts and :::message.
  7. remarkTreeTags — converts a paired JSX <Tree><Tree.Folder>…</Tree> block (captured by remarkMintlifyTags) into the same JSON-carrying tree node the ```tree fence produces.
  8. normalizeZennImages(markdown) + parseImageMetadata(url) — Zenn's image sizing/caption syntax (![](url =250x), a *caption* line under the image). The sizing suffix lives inside the markdown image destination, where remark's parser refuses spaces, so this stays a line-based step that encodes the metadata into query parameters; an image renderer reads them back with parseImageMetadata.

Using the library: two layers

inkstream is consumed at two distinct levels, and every feature belongs to exactly one of them. The boundary is the .ink-markdown element: inside it is the library's job; outside it — and everything that needs app-specific knowledge — is the consumer's job.

Layer 1 — Drop-in (zero config)

The /react subpath renders inkstream markdown in one line, and the core package stays React-free:

import { InkstreamMarkdown } from '@catatsumuri/inkstream/react';

<InkstreamMarkdown>{markdownSource}</InkstreamMarkdown>;

Everything below works with no props and no app-side code:

  • Headings with slug ids and copy-link anchors (h1–h4)
  • Callouts in all three syntaxes (Mintlify <Note> tags, :::message, GitHub > [!NOTE] alerts) normalized onto one aside.msg contract
  • Every Mintlify component: Card/CardGroup, Steps, Tabs, Accordion, Badge, Tooltip, Update, ResponseField/ParamField, CodeGroup, Tree
  • Code blocks: Shiki highlighting, copy button, wrap toggle, filename headers (```php:index.php), diff mode (```diff js:app.js)
  • ```mermaid diagrams (lazy-loaded chunk, optional peer dependency)
  • ```tree, ```quiz, ```chart:bar / ```chart:radar fences
  • Zenn image sizing/captions (![](url =250x), *caption* line)
  • YouTube embeds from standalone URLs
  • GitHub file embeds from standalone blob URLs (raw.githubusercontent.com allows CORS, so the browser fetches directly — no server help needed)
  • A default look via an optional stylesheet: import '@catatsumuri/inkstream/styles.css';. Every rule targets a stable ink-* class name inside @layer inkstream, so any unlayered rule a consumer writes overrides it without a specificity fight, and eight semantic colors (border/foreground/muted/primary/accent/card) read from --ink-* custom properties with neutral fallbacks — see the file's header comment for the full list and a theme-bridge example.

The renderers themselves stay style-agnostic (only class names, no inline styles) — styles.css is what turns Layer 1's correct structure into something that looks right without any app code. Skip the import entirely for unstyled markup, or import it and override just the --ink-* bridge (Layer 2) to match an existing design system.

Layer 2 — App integration surface

These features are split "parsing in the library, knowledge in the app": the library ships the syntax support and an injection point, and degrades gracefully when the app doesn't provide one. kb_practice is the reference implementation for each.

| Integration point | What the app supplies | Without it | kb_practice reference | | --- | --- | --- | --- | | Theme bridge | --ink-* custom properties pointing at the app's own design tokens | The package's neutral (zinc-style) default colors | resources/css/inkstream.css maps --ink-border → --border, etc. | | resolveWikilink prop | [[path]] → URL (routing/DB lookup) | [[...]] stays literal text | title→id map; unresolved links route to the create form | | ogpEndpoint prop | Server-side OGP proxy (CORS blocks direct fetch) | URL-only link cards | OgpController (validation + 24h cache) | | extractMarkdownHeadings + headingIdPrefix | Table-of-contents UI outside .ink-markdown | No TOC (anchors still work) | DocumentTableOfContents scrollspy | | components prop | Per-tag renderer overrides | Default ink-* renderers | — | | Dark mode | dark class on the document root + CSS for the --shiki-* variables | Light theme only | Tailwind .dark convention | | Page behaviors | Anything tied to navigation lifecycle (e.g. hash-anchor scroll restore in an SPA, where content mounts after the browser's native jump) | Browser defaults | hash restore on Inertia navigation |

Core-only consumers (no React) can use normalizeInkstreamMarkdown + inkstreamRemarkPlugins from the root entry point in any unified pipeline — the golden corpus renderer does exactly this. react and react-markdown are optional peer dependencies, so this path pulls in no React at all.

What the AST approach fixes structurally

  • No nesting limit — v1 encoded depth in colon-fence length (7 levels max, 10 − depth colons); v2 nesting is just tree structure.
  • Single-line tags — <Note>text</Note> in one paragraph works.
  • Self-closing tags — <Card title="..." />.
  • Inline tags — <Badge> / <Tooltip> pair mid-sentence.
  • Error tolerance with diagnostics — unmatched close tags stay literal, unclosed tags auto-close at end of parent, and both emit vfile warnings instead of failing silently. Malformed tree/quiz/chart fences fall back to a plain code block with a warning instead of silently dropping data.
  • No redundant raw-JSON dump — v1's tree/quiz/chart directives leave the source fence's code child in the tree alongside the JSON attribute, so the raw JSON also renders as a visible <pre><code> block. v2's mintlifyContainer has no children for these, so only the intended component renders.

Not yet implemented (planned)

  • Per-component attribute schemas (currently reuses v1's global allowlist for hProperties; the full parsed attribute map is kept on the node).
  • Tags inside blockquotes/lists currently require blank lines around them (the normalizer only handles top-level tag lines); fixing this means splitting multi-line html nodes or normalizing per container.
  • Multi-line JSX open tags (an open tag with attributes spread across several lines; v1 joins them in joinMultilineJsxTags).
  • Attribute naming convention: v1 prefixes hProperties with data-<component>-<attr> (e.g. data-card-href), presumably because its output was raw custom HTML elements read via dataset. v2 intentionally uses raw names (href) since containers render through React components that read props directly — this is a deliberate v2 API change, not a gap to close.
  • Registry publish. The build (npm run build → dist/ with .d.ts, exports pointing at it, styles.css copied alongside), the inkstream CLI (see below), and CI (typecheck/test/build/golden, plus a check that committed dist/ matches a fresh build) are already in place; this package isn't published to the npm registry yet ("private": true) — consumed via a GitHub commit pin instead, which is why dist/ is committed rather than gitignored: npm's prepare script (which would otherwise build it automatically on install) only runs when the installing environment allows lifecycle scripts, and plenty of reasonable npm configs set ignore-scripts=true. prepare still runs npm run build for anyone who does allow scripts (and will matter again once this is a real registry publish, where it runs on the publisher's machine regardless of the installer's setting).

CLI

npx --package=github:catatsumuri/inkstream inkstream <command> [file]

or, once installed as a project dependency, npx inkstream ... / directly via the inkstream bin.

| Command | Output | Use case | | --- | --- | --- | | inkstream render <file\|-> | HTML | RSS feeds, OGP static pages, email bodies. Same pipeline as the golden corpus renderer, so no heading ids (those come from the React heading renderer, not this pipeline) | | inkstream text <file\|-> | Plain, human-readable text | Full-text search indexing, excerpts, OGP description — the raw markdown source is unusable for these as-is (<Card title=...>, ```quiz syntax noise) | | inkstream headings <file\|-> [--json] [--prefix=<p>] | Indented outline, or a JSON array with --json | Server-side table-of-contents precomputation; same shape as extractMarkdownHeadings |

Every command reads markdown from the given file path, or from stdin when the path is - or omitted.

text resolves the same syntax render does (Mintlify tags, Zenn directives, wikilinks) down to prose: a quiz fence contributes its question/options/explanation, a chart fence its title and labels (not the numeric values), a tree fence its file and folder names, and a wikilink its label (or the path's last segment) — none of these have a sensible plain-text form otherwise, so they're handled explicitly rather than silently dropped or dumping raw JSON. Wikilinks resolve without needing a resolveWikilink callback, the same way heading slugs do.

Known limitations (not planned)

Edge cases identified during review, deliberately left unhandled: no occurrence in this project's real documents justified the added parsing surface, and each is easy to work around in authoring.

  • Setext headings (Title underlined by ===/--- instead of a leading #) aren't picked up by extractMarkdownHeadings — a table of contents silently omits them, even though they render fine as <h1>/<h2>. Write headings with #/## instead.
  • > inside a tag attribute value (<Card title="a > b">) breaks match-tags.ts's line-regex tag matcher, since it isn't a real HTML parser. Avoid a literal > in attribute values (use &gt; or rephrase).
  • Single-quoted attribute values (<Card title='x'>) aren't parsed by parseJsxAttributes' bare/quoted branches (only "x" and brace-wrapped {'x'} are). Use double quotes.

Golden corpus (golden/)

Snapshot regression suite: renders every golden/corpus/*.md fixture through the full pipeline and compares the prettified HTML against the committed baseline in golden/baseline/*.html.

npm run golden             # check; exits non-zero on any diff
npm run golden -- --update # re-render and accept the current output
  • golden/render-v2.ts — the pipeline under test (normalizeInkstreamMarkdown → inkstreamRemarkPlugins → remark-rehype → hast-util-to-html), the same exports consumers use.
  • golden/diff.ts — prettifies output to one tag per line, line-diffs it against the baseline, and writes the current output to golden/output/*.html (gitignored) for inspection.

Fixtures 01–09 are synthetic; 10–14 are the five real syntax-guide documents from thinkstream's SyntaxSeeder (basic markdown, extended markdown/GFM, Zenn syntax, Mintlify syntax, thinkstream syntax) — a far truer signal, and the corpus that surfaced the fence-state, inline-code, indentation, and array-attribute bugs the current pipeline fixes.

The corpus originally drove a v1-vs-v2 comparison (rendering the same fixtures through inkstream v1's directive pipeline); once every difference was either fixed or decided as an intentional v2 change, the v1 renderer and its file: dependency were dropped and the v2 output became the baseline.