Skip to content

feat(ascii): add @streamdown/ascii plugin for ASCII/box-drawing diagrams - #614

Open
sleitor wants to merge 1 commit into
vercel:mainfrom
sleitor:feat/ascii-plugin
Open

feat(ascii): add @streamdown/ascii plugin for ASCII/box-drawing diagrams#614
sleitor wants to merge 1 commit into
vercel:mainfrom
sleitor:feat/ascii-plugin

Conversation

@sleitor

@sleitor sleitor commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Closes #610

Problem

LLMs and coding agents default to ASCII and Unicode box-drawing characters (┌─┐, │ │, └─┘, ──►) for architecture diagrams and flowcharts instead of Mermaid. Rendered as plain Markdown code, these break in a browser for a few concrete reasons:

  • Font ligatures collapse ASCII sequences — --> renders as a single stylized arrow glyph, destroying column alignment
  • Line-by-line span wrappers (used by syntax highlighters) disrupt vertical alignment between rows
  • Standard web monospace fonts don't guarantee uniform advance widths for box-drawing characters (─│┌┐└┘) versus plain ASCII, so columns drift

This PR adds @streamdown/ascii, a plugin package that renders these fences as plain, unstyled-by-highlighter <pre> blocks with ligatures disabled and an advance-consistent monospace stack, so the diagram looks the same as when the model generated it.

Usage

import { Streamdown } from "streamdown";
import { ascii } from "@streamdown/ascii";

<Streamdown plugins={{ renderers: [ascii] }}>{agentOutput}</Streamdown>

Advanced configuration via the factory:

import { createAsciiPlugin } from "@streamdown/ascii";

const ascii = createAsciiPlugin({
  languages: ["ascii", "diagram", "chart", "box"], // default: ["ascii", "diagram", "chart"]
  className: "my-ascii-block",
  fontFamily: "Menlo, Consolas, monospace",
});

Options

Option Type Default Description
languages string[] ["ascii", "diagram", "chart"] Code fence languages bound to the renderer
className string Extra class names applied to the rendered <pre>
fontFamily string ui-monospace, "SF Mono", "Cascadia Mono", "DejaVu Sans Mono", "Liberation Mono", Menlo, Consolas, monospace Override the monospace font stack

Streaming-safety guarantee

  • The rendered <pre> always contains exactly one text child ({code}) — no per-line <span> wrappers, no syntax highlighting — so appending characters during streaming never causes existing rows to reflow. Covered by a test that asserts pre.childNodes.length === 1 and the single child is a text node.
  • The component's tree shape never changes between an incomplete and a complete fence. Incompleteness is only ever surfaced through a stable data-incomplete attribute, never by adding/removing/swapping elements — so React never remounts the block mid-stream (verified by a test asserting the <pre> DOM node reference is identical before/after an isIncomplete: true -> false rerender).
  • Styling sets font-variant-ligatures: none + font-feature-settings: "liga" 0, "calt" 0 (kills arrow-ligature collapsing), white-space: pre (no wrapping, which would destroy ASCII art alignment), and overflow-x: auto (wide diagrams scroll instead).

Why no core changes are required

Streamdown already has a plugins.renderers / CustomRenderer extension point ({ component, language }, resolved in packages/streamdown/lib/plugin-context.tsx) built for exactly this kind of arbitrary-fence-language rendering, and it's already documented at /docs/custom-renderers. @streamdown/ascii is a sibling package (mirroring the layout of @streamdown/math, @streamdown/mermaid, @streamdown/cjk) that plugs into that existing point — PluginConfig, plugin-context.tsx, and every other core file are untouched. This is a purely additive change.

What's included

  • packages/streamdown-ascii — new package (index.tsx, package.json, tsup.config.ts, tsconfig.json, vitest.config.ts, tests), mirroring the sibling plugin package layout (author, license, peerDependencies.react, scripts, etc.)
  • __tests__/index.test.tsx — 11 tests: default/custom language binding, independent instances, single-text-child assertion, ligature/white-space/overflow/tab-size styling, stable tree shape across isIncomplete true→false, no-throw on partial content, custom className/fontFamily
  • .changeset/add-ascii-plugin.md — minor version changeset for the new package
  • apps/website/content/docs/plugins/ascii.mdx + plugins/meta.json — docs page matching the structure of the existing math/cjk/mermaid plugin pages

Scope note: the website has a larger interactive plugin playground (app/[lang]/playground) that lets users toggle code/mermaid/math/cjk live. I did not wire ascii into that (593-line component) to keep this PR focused and reviewable — happy to follow up if maintainers want that too.

Verification

Run from a clean upstream/main checkout plus this branch:

  • pnpm install
  • pnpm build (all packages/apps build; @streamdown/ascii builds cleanly via tsup, ESM + .d.ts)
  • pnpm check-types — no-op repo-wide today (no package defines a check-types script yet); manually ran tsc --noEmit inside the new package with zero errors
  • pnpm check (biome/ultracite, zero errors after pnpm fix reformatted one line in the new file)
  • pnpm test — 7/7 package test suites pass, including the new @streamdown/ascii suite (11/11)

Pre-existing, unrelated failure: apps/test#build fails on this branch with GatewayAuthenticationError: AI Gateway authentication failed (apps/test/app/page.tsx calls gateway.getAvailableModels() at build time, which needs AI_GATEWAY_API_KEY/Vercel OIDC). I confirmed this is not caused by this PR by building a clean upstream/main worktree the same way — it fails identically there with no local changes at all.

No breaking changes. No existing tests modified or weakened.

Adds a new workspace package, @streamdown/ascii, that renders
agent-generated ASCII and Unicode box-drawing diagrams (e.g. `┌─┐`,
`│ │`, `└─┘`, `──►`) as stable preformatted blocks.

- Binds to ```ascii, ```diagram, and ```chart fences by default
  (configurable via createAsciiPlugin({ languages }))
- Disables font ligatures (font-variant-ligatures: none,
  font-feature-settings liga/calt off) so sequences like --> are never
  collapsed into a stylized arrow glyph
- Uses an advance-consistent monospace font stack, overridable via
  createAsciiPlugin({ fontFamily })
- Renders a single unbroken text node inside a <pre> (no per-line span
  wrappers, no syntax highlighting) so streaming appends never reflow
  existing rows
- Surfaces isIncomplete only via a stable data-incomplete attribute;
  tree shape never changes across streaming ticks

Plugs into the existing plugins.renderers / CustomRenderer extension
point, so no core changes are required.

Closes vercel#610
@vercel

vercel Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

@sleitor is attempting to deploy a commit to the Vercel Team on Vercel.

A member of the Team first needs to authorize it.

lofcz added a commit to lofcz/streamdown-ng that referenced this pull request Sep 7, 2026
Port vercel/streamdown#614 as a workspace package that renders ```ascii / ```diagram / ```chart fences as ligature-free, single-text-node <pre> blocks so agent-generated diagrams stay aligned while streaming.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: @streamdown/ascii plugin for agent-generated flowcharts & diagrams

1 participant