* 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>
* feat: add Tailwind CSS prefix support
* fix: prevent double-prefixing of already-prefixed CSS classes
When cn() output (e.g. 'tw:size-full') is passed as className to a child
component that also calls cn() with the same prefix, prefixClasses() was
naively prepending the prefix again, producing 'tw:tw:size-full'.
Fix: skip classes that already start with the prefix+colon sentinel.
Add tests for both the all-already-prefixed and mixed cases.
* fix: lint errors — sort imports, format, hoist regex constants
* Add changeset for Tailwind prefix support
* fix: review fixes for tailwind prefix PR
- Bump changeset to minor (new feature, not patch)
- Rename shadowed cn import to baseCn in code-block/body.tsx
- Document that user className values are also prefixed
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>
Export TableCopyDropdown, TableDownloadButton, TableDownloadDropdown,
and table utility functions (extractTableDataFromElement, tableDataToCSV,
tableDataToTSV, tableDataToMarkdown, escapeMarkdownTableCell, TableData)
from the main entry point, enabling custom table overrides to preserve
copy/download interactivity.
Also fixes a type error in table components where `size` prop was passed
to SVG icons that only accept standard SVG attributes (width/height).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(remend): don't complete bold/italic/strikethrough inside complete inline code spans
Bold, italic, and strikethrough handlers were using isWithinCodeBlock()
which only detects triple-backtick fenced code blocks. Markers inside
single-backtick inline code spans (e.g. `**bold`) were incorrectly
completed, leaking outside the code span.
Fix:
- Add isWithinCompleteInlineCode() to code-block-utils.ts that returns
true only when a position is inside a *complete* (both delimiters
present) inline code span. This preserves the existing streaming
behavior where emphasis inside an *incomplete* inline code span
(still being streamed) continues to be closed correctly.
- Apply the check in handleIncompleteBold, handleIncompleteBoldItalic,
handleIncompleteDoubleUnderscoreItalic, handleIncompleteSingleAsterisk-
Italic, and handleIncompleteStrikethrough.
Fixes#424
* fix(remend): add isWithinCompleteInlineCode guard to handleIncompleteSingleUnderscoreItalic
* chore: add changeset for inline code emphasis fix
---------
Co-authored-by: Dmitrii Troitskii <jsleitor@gmail.com>
* feat: add useIsBlockIncomplete hook for detecting incomplete streaming blocks
Custom components can now detect when the block they're rendering is still
being streamed (incomplete). This is useful for showing loading states in
code blocks during streaming.
- Add BlockIncompleteContext and useIsBlockIncomplete hook
- Add hasIncompleteCodeFence utility to detect unclosed code fences
- Calculate isIncomplete state per block during render
- Default CodeComponent uses the hook to set data-incomplete attribute
- Add comprehensive tests for the new functionality
* docs: add useIsBlockIncomplete documentation
- Add "Streaming State" section to components.mdx explaining the hook
- Add "Avoid Continuous Re-rendering" section to mermaid.mdx with example
* fix: support tilde code fences in hasIncompleteCodeFence
Adds support for detecting incomplete ~~~ code fences in addition to
backtick fences, per CommonMark/GFM spec.
* fix: address PR review — line-based fence detection, rename hook/context
- rewrite hasIncompleteCodeFence to walk line-by-line per CommonMark spec
- fixes 6+ backtick fences being miscounted
- fixes false positives from inline backticks in prose
- respects fence character matching and length requirements
- rename block-position-context.ts → block-incomplete-context.ts
- rename useIsBlockIncomplete → useIsCodeFenceIncomplete
- add tests for edge cases (long fences, inline backticks, indentation)
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>
* Add support for copying table data as Markdown in TableCopyDropdown
* Refactor content generation in TableCopyDropdown to use formatters for cleaner code
* docs: add changeset