Skip to main content
remend is a pre-processor for streaming Markdown text. As an LLM streams tokens, it produces a string that may end mid-way through a formatting construct — an unclosed **, a partial link [text](, or a dangling $ — which would cause the downstream Markdown parser to misinterpret the entire document. remend scans the text and applies a prioritised chain of fixup handlers before the string reaches the parser. Streamdown applies remend automatically in mode="streaming" when parseIncompleteMarkdown={true} (the default). The remend prop on <Streamdown> forwards RemendOptions directly.

Installation

npm install remend

Usage

import remend from "remend";

const fixed = remend(incompleteMarkdown, options);

Standalone example

import remend from "remend";

// Closes the open bold marker
remend("Hello **world");          // → "Hello **world**"

// Removes an incomplete link (placeholder URL)
remend("Click [here](htt");       // → "Click [here](streamdown:incomplete-link)"

// Escapes a single tilde between word chars to prevent false strikethrough
remend("pH~7 is neutral");        // → "pH\~7 is neutral"

// Strips a trailing incomplete HTML tag
remend("Visit <a hre");           // → "Visit"

Default export: remend()

function remend(text: string, options?: RemendOptions): string
text
string
required
The raw streaming Markdown text to process.
options
RemendOptions
Optional configuration. When omitted, all built-in handlers except inlineKatex are enabled with their default behaviour.
Returns the processed string. If text is empty or not a string, it is returned unchanged.

RemendOptions

All options default to true (enabled) unless stated otherwise. Set an option to false to disable that specific handler.
bold
boolean
Complete incomplete bold markers. **text**text**. Default: true.
boldItalic
boolean
Complete incomplete bold-italic markers. ***text***text***. Default: true.
italic
boolean
Complete incomplete italic markers for *, _, and __ variants. Default: true.
inlineCode
boolean
Complete an unclosed inline code span. `code`code`. Default: true.
strikethrough
boolean
Complete incomplete strikethrough markers. ~~text~~text~~. Default: true.
Fix incomplete link syntax. [text](url[text](streamdown:incomplete-link). Default: true.
images
boolean
Fix incomplete image syntax. Enables the links handler when set to true. Default: true.
Controls how incomplete links are handled. "protocol" (default) replaces the partial URL with streamdown:incomplete-link. "text-only" removes the link markup entirely, leaving only the display text.
katex
boolean
Complete an incomplete block KaTeX expression. $$equation$$equation$$. Default: true.
inlineKatex
boolean
Complete an incomplete inline KaTeX expression. $equation$equation$. Default: false — a single $ is ambiguous with currency and this handler is opt-in.
singleTilde
boolean
Escape a single ~ between word characters to prevent false strikethrough. 20~2520\~25. Default: true.
setextHeadings
boolean
Prevent incomplete setext headings (e.g. a line ending with --- that the parser would interpret as an <h2>) from being misrenderd during streaming. Default: true.
comparisonOperators
boolean
Escape > as a comparison operator inside list items (e.g. - > 25- \> 25) to prevent the > from being interpreted as a blockquote. Default: true.
htmlTags
boolean
Strip incomplete HTML tags at the end of streaming text (e.g. text <customtext). Default: true.
handlers
RemendHandler[]
Additional custom handlers to include in the pipeline. Custom handlers run after all enabled built-in handlers unless you set a lower priority.

RemendHandler

The interface for a custom handler added via options.handlers.
const myHandler: RemendHandler = {
  name: "remove-draft-watermark",
  handle: (text) => text.replace(/\[DRAFT\]/g, ""),
  priority: 50,
};

remend(text, { handlers: [myHandler] });
name
string
required
A unique identifier for this handler. Used for debugging and deduplication.
handle
(text: string) => string
required
The transformation function. Receives the current text (after all higher-priority handlers have run) and returns the modified text.
priority
number
Execution order relative to other handlers. Lower values run first. Built-in handler priorities range from 0 to 100. Custom handlers default to 100 when this field is omitted.Built-in priorities for reference:
HandlerPriority
singleTilde0
comparisonOperators5
htmlTags10
setextHeadings15
links20
boldItalic30
bold35
italic (double underscore)40
italic (single asterisk)41
italic (single underscore)42
inlineCode50
strikethrough60
katex70
inlineKatex75
default (custom)100

Named exports

The following utility functions are exported for use in custom handlers or other preprocessing logic.

isWithinCodeBlock()

function isWithinCodeBlock(text: string, position: number): boolean
Returns true if position falls between a pair of triple-backtick fences in text. Useful for skipping transformations that should not affect code block contents.

isWithinMathBlock()

function isWithinMathBlock(text: string, position: number): boolean
Returns true if position is inside an inline $...$ or block $$...$$ math expression. Handles escaped dollar signs (\$) and the precedence of block math over inline math.

isWithinLinkOrImageUrl()

function isWithinLinkOrImageUrl(text: string, position: number): boolean
Returns true if position is inside the URL portion (...) of a Markdown link or image — i.e. between ]( and ). Useful for preventing transformations inside URLs.

isWordChar()

function isWordChar(char: string): boolean
Returns true if char is a word character: ASCII letters (a-z, A-Z), digits (0-9), underscore (_), or any Unicode letter-or-number character outside ASCII. Uses an ASCII fast path before falling back to a Unicode regex.

Build docs developers (and LLMs) love