@humanspeak/svelte-markdown
Version:
Markdown and HTML renderer for Svelte 5 — built for rendering streaming AI agent output from Claude Code, ChatGPT, and agentic workflows. XSS-safe defaults, streaming-aware sanitization, token caching, TypeScript types, and Svelte 5 runes.
289 lines (288 loc) • 9.26 kB
TypeScript
/**
* Type definitions for the Svelte Markdown component.
*
* This module provides TypeScript type definitions for the core functionality
* of the Svelte Markdown parser and renderer. It defines the primary interface
* for component props and integrates with the marked library's token system.
*
* Typical usage example:
* ```typescript
* import type { SvelteMarkdownProps } from './types';
*
* const markdownProps: SvelteMarkdownProps = {
* source: "# Hello World",
* isInline: false
* };
* ```
*
* @packageDocumentation
*/
import type { MarkedExtension, Token, TokensList } from 'marked';
import type { Snippet } from 'svelte';
import type { MarkedOptions, Renderers } from './utils/markdown-parser.js';
import type { HtmlKey } from './utils/rendererKeys.js';
import type { SanitizeAttributesFn, SanitizeUrlFn } from './utils/sanitize.js';
export interface ParagraphSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export interface HeadingSnippetProps {
depth: number;
raw: string;
text: string;
options: SvelteMarkdownOptions;
slug: (val: string) => string;
children?: Snippet;
}
export interface LinkSnippetProps {
href?: string;
title?: string;
raw?: string;
text?: string;
children?: Snippet;
}
export interface ImageSnippetProps {
href?: string;
title?: string;
text?: string;
raw?: string;
}
export interface CodeSnippetProps {
lang: string;
text: string;
codeBlockStyle?: 'indented';
}
export interface CodespanSnippetProps {
raw: string;
text?: string;
}
export interface BlockquoteSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export interface ListSnippetProps {
ordered?: boolean;
start?: number;
loose?: boolean;
children?: Snippet;
}
export interface ListItemSnippetProps {
text?: string;
task?: boolean;
checked?: boolean;
loose?: boolean;
children?: Snippet;
listItemIndex?: number;
}
export interface TableSnippetProps {
children?: Snippet;
}
export interface TableHeadSnippetProps {
children?: Snippet;
}
export interface TableBodySnippetProps {
children?: Snippet;
}
export interface TableRowSnippetProps {
children?: Snippet;
}
export interface TableCellSnippetProps {
header: boolean;
align: 'left' | 'center' | 'right' | null;
children?: Snippet;
}
export interface EmSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export interface StrongSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export interface DelSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export type HrSnippetProps = Record<string, never>;
export type BrSnippetProps = Record<string, never>;
export interface TextSnippetProps {
raw?: string;
text?: string;
children?: Snippet;
}
export interface RawTextSnippetProps {
text: string;
}
export interface EscapeSnippetProps {
text: string;
raw?: string;
}
export type SnippetOverrides = {
paragraph?: Snippet<[ParagraphSnippetProps]>;
heading?: Snippet<[HeadingSnippetProps]>;
link?: Snippet<[LinkSnippetProps]>;
image?: Snippet<[ImageSnippetProps]>;
code?: Snippet<[CodeSnippetProps]>;
codespan?: Snippet<[CodespanSnippetProps]>;
blockquote?: Snippet<[BlockquoteSnippetProps]>;
list?: Snippet<[ListSnippetProps]>;
listitem?: Snippet<[ListItemSnippetProps]>;
orderedlistitem?: Snippet<[ListItemSnippetProps]>;
unorderedlistitem?: Snippet<[ListItemSnippetProps]>;
table?: Snippet<[TableSnippetProps]>;
tablehead?: Snippet<[TableHeadSnippetProps]>;
tablebody?: Snippet<[TableBodySnippetProps]>;
tablerow?: Snippet<[TableRowSnippetProps]>;
tablecell?: Snippet<[TableCellSnippetProps]>;
em?: Snippet<[EmSnippetProps]>;
strong?: Snippet<[StrongSnippetProps]>;
del?: Snippet<[DelSnippetProps]>;
hr?: Snippet<[HrSnippetProps]>;
br?: Snippet<[BrSnippetProps]>;
text?: Snippet<[TextSnippetProps]>;
rawtext?: Snippet<[RawTextSnippetProps]>;
escape?: Snippet<[EscapeSnippetProps]>;
};
/**
* Props passed to HTML snippet overrides.
*
* **Security note:** By default, `attributes` are sanitized in the Parser before
* reaching any renderer or snippet — event handlers (`on*`) are stripped and
* URL attributes are validated against a protocol allowlist. To customize this
* behavior, pass a `sanitizeUrl` prop to `SvelteMarkdown`.
*/
export interface HtmlSnippetProps {
attributes?: Record<string, string | number | boolean | undefined>;
children?: Snippet;
}
export type HtmlSnippetOverrides = {
[K in HtmlKey as `html_${K}`]?: Snippet<[HtmlSnippetProps]>;
};
export interface StreamingOffsetChunk {
value: string;
offset: number;
}
export type StreamingChunk = string | StreamingOffsetChunk;
export type SvelteMarkdownProps<T extends Renderers = Renderers> = {
/**
* Markdown content to render.
*
* Pass a `string` for raw markdown that will be parsed, or a pre-parsed
* `Token[]` array to skip parsing entirely. An empty string renders nothing.
*/
source: Token[] | string;
/**
* Partial map of renderer overrides merged with the built-in defaults.
*
* Only the keys you provide are replaced — all others keep their default
* component. Use the `allowRenderersOnly` / `excludeRenderersOnly`
* helpers to disable entire categories of renderers.
*/
renderers?: Partial<T>;
/**
* Options forwarded to the `marked` lexer, extended with header-ID
* generation settings. Merged with {@link defaultOptions}.
*/
options?: Partial<SvelteMarkdownOptions>;
/**
* Array of marked extensions to apply when parsing.
*
* Internally creates a scoped `Marked` instance, extracts its resolved
* defaults, and merges them into the parser options so the lexer
* recognises any custom token types the extensions define.
*
* Extension token names are also used to collect snippet overrides,
* enabling both component-renderer and snippet-based rendering of
* custom tokens.
*
* @defaultValue `[]`
*
* @example
* ```svelte
* <script lang="ts">
* import { markedKatex, KatexRenderer } from '@humanspeak/svelte-markdown/extensions'
* </script>
*
* <SvelteMarkdown
* source={markdown}
* extensions={[markedKatex()]}
* renderers={{ inlineKatex: KatexRenderer, blockKatex: KatexRenderer }}
* />
* ```
*/
extensions?: MarkedExtension[];
/**
* When `true`, the source is parsed with `Lexer.lexInline()` instead of
* `Lexer.lex()`, producing only inline tokens (no block elements).
*
* @defaultValue `false`
*/
isInline?: boolean;
/**
* Enables optimized rendering for LLM streaming scenarios.
*
* When `true`, the component performs a full re-parse on each source
* update but diffs the resulting tokens against the previous parse.
* Only changed or appended tokens trigger DOM updates, keeping render
* cost proportional to the change rather than the full document size.
*
* Use this when appending tokens to `source` in a streaming fashion
* (e.g., ChatGPT/Claude SSE responses).
*
* @defaultValue `false`
*/
streaming?: boolean;
/**
* Callback invoked after the source has been parsed into tokens.
*
* Receives the full token array before rendering begins. Useful for
* inspecting or transforming the parsed AST.
*
* @param tokens - The parsed token array or `TokensList`.
*/
parsed?: (tokens: Token[] | TokensList) => void;
/**
* Custom URL sanitizer applied in the Parser before tokens reach any
* renderer component or snippet. Receives a URL string and must return
* a sanitized URL (or `''` to strip it).
*
* The default allowlists `http:`, `https:`, `mailto:`, `tel:`, and
* relative URLs. Override to implement a custom policy.
*
* @defaultValue {@link defaultSanitizeUrl}
*/
sanitizeUrl?: SanitizeUrlFn;
/**
* Custom HTML attribute sanitizer applied in the Parser before tokens
* reach any renderer component or snippet. Receives the attribute map,
* a {@link SanitizeContext} with token type and HTML tag name, and the
* active `sanitizeUrl` function.
*
* The default strips all `on*` event handlers and runs URL-bearing
* attributes (`href`, `src`, `action`, etc.) through `sanitizeUrl`.
* Override to add custom attribute rules per tag.
*
* @defaultValue {@link defaultSanitizeAttributes}
*/
sanitizeAttributes?: SanitizeAttributesFn;
} & Partial<SnippetOverrides> & Partial<HtmlSnippetOverrides>;
export interface SvelteMarkdownOptions extends MarkedOptions {
/**
* When `true`, an `id` attribute is generated for every heading element
* using `github-slugger`.
*
* @defaultValue `true`
*/
headerIds?: boolean;
/**
* String prepended to every generated heading `id`.
*
* @defaultValue `''`
*/
headerPrefix?: string;
}