The published type declarations (dist/index.d.ts) import MermaidConfig
from the mermaid package, making it part of the public API. Having it as
a devDependency meant consumers could end up resolving different mermaid
versions for streamdown and @streamdown/mermaid, causing type errors due
to incompatible MermaidConfig types.
* fix(playground-editor): add full height class to ConversationContent
* docs: add changeset
* fix(playground-editor): update Textarea class for consistent field sizing
* Delete .changeset/fix-playground-safari-textarea.md
---------
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
* feat: add lineNumbers prop to disable line numbers in code blocks
* docs: add lineNumbers prop and noLineNumbers meta string
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(streamdown): preserve double newlines in literalTagContent tags
When a tag is in literalTagContent mode, double newlines (\n\n) inside
the tag content were incorrectly treated as paragraph separators by the
markdown parser, causing the content after the first blank line to be
excluded from the tag's children.
Fix: in preprocessLiteralTagContent, after escaping markdown metacharacters,
replace \n\n with HTML entities. This prevents the markdown
parser from interpreting blank lines as paragraph breaks while still
preserving the newline characters (rehype-raw decodes back to \n).
Fixesvercel/streamdown#456
* Run pnpm fix to resolve formatting issues
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix pre-existing formatting issue in code-block/index.tsx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix lint and formatting issues
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(streamdown-code): fall back to plain text for unknown/truncated language identifiers
When a code block's language identifier is unrecognised by Shiki (e.g.
'rus' from a network-truncated 'rust'), createHighlighter throws a
ShikiError. Previously the .catch() block only logged the error and
left subscribers hanging, causing a permanent React render-cycle stall.
Fix: resolve the language to the 'text' fallback *before* calling
getHighlighter() when the identifier is not in the bundledLanguages
set. This prevents the exception entirely and ensures the code block
renders with plain-text highlighting instead of breaking.
Fixes#433
* Add changeset for unknown language fallback fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add test for unknown/truncated language fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix TypeScript error: widen safeLanguage type to include SpecialLanguage
`'text'` is a SpecialLanguage in Shiki, not a BundledLanguage, so the
type annotations for getHighlighter and getHighlighterCacheKey need to
accept both.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix formatting: remove trailing comma, use double quotes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Custom renderers now receive the raw metastring from code fences via a
new optional `meta` prop. Everything after the language identifier is
passed through (e.g. ```rust {1} title="foo"``` → meta = '{1} title="foo"').
- Added `meta?: string` to `CustomRendererProps` in `plugin-types.ts`
- Pass `meta={metastring}` to custom renderers in `components.tsx`
- Added tests for meta prop presence and absence
- Added minor changeset
Closes#443
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
* fix(save): prepend UTF-8 BOM to CSV blobs for Excel compatibility
Closes#440
* fix(download): refactor TableDownloadButton to use save() for CSV downloads and ensure UTF-8 BOM compatibility
* feat(remend): add opt-in inline KaTeX completion
* docs(remend): update README to include inline KaTeX completion and clarify option defaults
* fix(remend): complete partial closing $ in block math without duplicating
* Add changeset for inline KaTeX completion feature
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(code-block): enable horizontal scrolling for long lines in code blocks
Change overflow-hidden to overflow-x-auto on the code block body container
so that long single-line comments and other long code lines are scrollable
horizontally instead of being silently clipped.
Fixes#438
* Add changeset for code block horizontal scroll fix
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(code-block): preserve empty lines in code blocks
## Summary
Fix empty lines collapsing in code blocks by inserting a newline character for empty rows.
## Problem
When rendering code blocks with empty lines, the `<span>` elements for empty rows had no content, causing them to collapse to zero height. This affected both visual display and copy behavior.
## Solution
Insert `"\n"` for empty lines instead of rendering an empty span. This ensures:
- **Visual**: Empty lines maintain proper height
- **Copy**: Copied text includes the empty lines correctly
## Changes
- `packages/streamdown/lib/code-block/body.tsx`: Add condition to detect empty rows (`row.length === 0` or single token with empty content) and render `"\n"` instead
* Fix lint issues
* Add tests
* Create fix-empty-lines.md
---------
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
* feat: automatic RTL direction support
Add per-block RTL/LTR text direction detection using the 'first strong
character' Unicode algorithm.
- New `dir` prop on StreamdownProps: 'auto' | 'ltr' | 'rtl'
- When dir='auto', each block's direction is detected automatically
- Exported detectTextDirection() utility for custom usage
- Zero extra DOM elements when dir is not set
Closes#311
* fix: lint errors — sort imports, hoist regex, use block statements
* Add changeset for RTL direction support
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add literalTagContent prop for plain-text custom tag children
* fix: run preprocessLiteralTagContent before preprocessCustomTags to prevent HTML comment corruption
When a tag appears in both allowedTags and literalTagContent and its
content contains blank lines, the '<!---->' HTML comment inserted by
preprocessCustomTags was subsequently corrupted to '<\!\-\-\-\->' by
preprocessLiteralTagContent (which escapes '!' and '-' as markdown
metacharacters).
Fix: swap the execution order so preprocessLiteralTagContent runs first,
then preprocessCustomTags inserts its markers. The HTML comments are
never seen by the markdown escaper.
* Add changeset for literalTagContent
* fix: address review issues for literalTagContent
- changeset patch→minor (new feature)
- add literalTagContent to Streamdown memo comparison
- narrow escape regex to inline-only metacharacters
- use ElementContent type in collectText for proper HAST coverage
- document double-escape limitation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add translations/i18n support via translations prop
Implements #190 - adds a `translations` prop to the Streamdown component
that allows customizing all UI strings for internationalization.
Changes:
- Add `packages/streamdown/lib/translations-context.tsx` with
StreamdownTranslations type, defaultTranslations, TranslationsContext,
and useTranslations hook
- Add `translations?: Partial<StreamdownTranslations>` prop to Streamdown
- Wrap render output with TranslationsContext.Provider (both static and
streaming modes)
- Export StreamdownTranslations type and defaultTranslations from index
- Replace all hardcoded UI strings in sub-components with translations:
- code-block/copy-button: copyCode
- code-block/download-button: downloadFile
- mermaid/download-button: downloadDiagram, downloadDiagramAs{Svg,Png,Mmd},
mermaidFormat{Svg,Png,Mmd}
- mermaid/fullscreen-button: viewFullscreen, exitFullscreen
- table/copy-dropdown: copyTable, copyTableAs{Markdown,Csv,Tsv},
tableFormat{Markdown,Csv,Tsv}
- table/download-dropdown: downloadTable, downloadTableAs{Csv,Markdown},
tableFormat{Csv,Markdown}
- image: imageNotAvailable, downloadImage
- link-modal: openExternalLink, externalLinkWarning, close, copyLink,
copied, openLink
- Add __tests__/translations.test.tsx with 12 tests covering defaults,
custom translations, partial overrides, and context access
- Update download-dropdown.test.tsx to match new translation-based title
All 804 tests pass.
* Add changeset for translations prop
* Fix misplaced comments in translations type and broken test type assertion
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Use value equality for translations memo to support inline objects
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support custom starting line number in code blocks
Add support for specifying a custom starting line number via the code
fence meta string, e.g.:
```js startLine=10
const x = 1;
```
Implementation:
- Add `remarkCodeMeta` remark plugin that forwards the fenced-code meta
string to hast as the `metastring` property so it is available to the
custom React code component.
- Update the rehype-sanitize default schema to allow the `metastring`
attribute on `code` elements.
- Parse `startLine=N` from the meta string in `CodeComponent` and pass
the value down as a `startLine` prop.
- In `CodeBlockBody`, apply `counter-reset: line N-1` as an inline style
when `startLine > 1`, which overrides the Tailwind counter-reset class
and makes CSS counters begin from the specified number.
- Pass `startLine` through the `CodeBlock` and
`HighlightedCodeBlockBody` call chains.
- Add 12 tests covering the remark plugin, CodeBlockBody prop, and
CodeBlock integration.
Closes: resolves#287
* fix: lint errors — use literal keys, hoist regex, sort imports
* feat: allow customizing icons via icons prop
Add an IconContext-based system that lets users override any of the
built-in icons (CheckIcon, CopyIcon, DownloadIcon, etc.) by passing
a Partial<IconMap> via the new `icons` prop on <Streamdown>.
- Created lib/icon-context.tsx with IconMap type, IconProvider, and useIcons hook
- Updated all sub-components to consume icons via useIcons() instead of direct imports
- Added icons prop to StreamdownProps with IconProvider wrapper
- Exported IconMap type from package entry
Closes#412
* style: fix biome lint and formatting errors
* Merge branch 'main' into feat/custom-icons
* fix: review fixes for icons and startLine features
- Add missing changeset for icons feature (minor)
- Fix IconComponent type to include size prop matching actual usage
- Fix IconProvider memoization: use shallow comparison instead of
referential equality so inline icon objects don't cause re-renders
- Move START_LINE_PATTERN constant below all imports
- Add bounds checking for startLine (must be >= 1)
- Convert dynamic imports to top-level imports in startLine tests
- Add icon-context tests (5 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support custom shiki themes in code blocks
Add support for custom shiki theme objects (ThemeRegistrationAny-compatible)
in addition to built-in BundledTheme string names.
- Add CustomTheme interface and ThemeInput type alias
- Widen theme types across CodePluginOptions, CodeHighlighterPlugin,
HighlightOptions, StreamdownProps, and StreamdownContextType
- Extract theme names from objects for cache keys and codeToTokens
- Pass theme objects to createHighlighter for registration
- Add tests for custom theme objects and mixed themes
- Full backward compatibility with existing BundledTheme usage
Closes#409
* fix: pass per-call themes to getHighlighter instead of defaultThemes
* fix: resolve duplicate interface properties and type errors in CodeHighlighterPlugin
* Add changeset for custom Shiki themes
* fix: use shiki's ThemeRegistrationAny, deduplicate types, tighten API
- Replace custom CustomTheme interface with shiki's ThemeRegistrationAny
- Remove duplicated type definitions across packages
- Simplify HighlightOptions.themes to [ThemeInput, ThemeInput]
- Remove unsafe cast in createHighlighter call
- Bump changeset to minor (new feature, not patch)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use HAST char count for prevContentLength instead of raw markdown length
The animate plugin's charCounter counts HAST text node characters (rendered
text, without markdown syntax). Previously, prevContentLengthRef stored
content.length (raw markdown), causing a unit mismatch: markdown syntax
characters (**, #, `, etc.) inflate the raw length vs the HAST count.
This mismatch caused new streaming content to incorrectly skip animation
when prevContentLength (raw) exceeded the actual HAST character count.
Fix: expose getLastRenderCharCount() on AnimatePlugin that returns the
total HAST character count from the last render. Block now uses this value
instead of content.length so both sides measure the same units.
Also fix lint issues in list-animation-retrigger.test.tsx:
- Replace async () => {} with () => Promise.resolve() for empty act() calls
- Remove async from act callbacks that don't use await
- Remove unused renderCount variable
* fix: correct timing of prevContentLength for animate plugin
The previous implementation called resetPrevContentLength() in Block's
function body, but Markdown (which calls processor.runSync synchronously)
renders as a child component — AFTER Block's function body returns. This
meant the animate plugin always saw prevContentLength=0 on every render.
Fix:
- Remove manual resetPrevContentLength() from Block's render body
- Add self-reset inside rehypeAnimate after each run, so sibling blocks
start clean (depth-first rendering ensures Markdown1 runs before Block2)
- Read getLastRenderCharCount() at the TOP of Block's render body: since
React renders depth-first, this value is from the PREVIOUS Markdown run
(exactly the prevContentLength needed for the current render)
- Remove stale useLayoutEffect approach (not needed with depth-first timing)
* fix: reset lastRenderCharCount after reading to prevent sibling block leakage
When a single AnimatePlugin instance is shared across sibling Block
components, getLastRenderCharCount() was returning the accumulated
char count from the previously-rendered Block instead of 0. This
caused subsequent Block components to incorrectly skip animation for
their initial content.
Fix: reset config.lastRenderCharCount to 0 after returning the value
in getLastRenderCharCount(). Since React renders depth-first, each
Block reads this value, uses it as prevContentLength, then its
Markdown child runs rehype (setting a new lastRenderCharCount).
After the read-reset, the next sibling Block starts clean.
Addresses VADE review comment on PR #417.
* fix: resolve animate plugin issues with memo'd components
- Give each animatePlugin instance a unique rehype function name to
prevent ProcessorCache collisions across instances
- Separate immutable AnimateConfig from mutable AnimateRenderState
- Stabilize animatePlugin useMemo deps using value-equality key
(JSON.stringify) instead of reference equality
- Bypass startTransition when animatePlugin is active so displayBlocks
updates synchronously
- Rewrite failing test: the 0ms approach only applies when memo'd
components re-render (node position changes), so test with a
text-growing scenario instead of new-item-added scenario
- Fix lint issues (sorted interfaces, block statements)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
Co-authored-by: Hayden Bleasel <hello@haydenbleasel.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>