An open API service indexing awesome lists of open source software.

https://github.com/humanspeak/svelte-markdown

๐Ÿ“ Fast, lightweight Markdown renderer component for Svelte applications with full CommonMark support
https://github.com/humanspeak/svelte-markdown

javascript markdown markdown-parser markdown-to-html svelte sveltekit typescript ui-components web-components

Last synced: 2 months ago
JSON representation

๐Ÿ“ Fast, lightweight Markdown renderer component for Svelte applications with full CommonMark support

Awesome Lists containing this project

README

          

# @humanspeak/svelte-markdown

A powerful, customizable markdown renderer for Svelte with TypeScript support. Built as a successor to the original svelte-markdown package by Pablo Berganza, now maintained and enhanced by Humanspeak, Inc.

[![NPM version](https://img.shields.io/npm/v/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)
[![Build Status](https://github.com/humanspeak/svelte-markdown/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/humanspeak/svelte-markdown/actions/workflows/npm-publish.yml)
[![Coverage Status](https://coveralls.io/repos/github/humanspeak/svelte-markdown/badge.svg?branch=main)](https://coveralls.io/github/humanspeak/svelte-markdown?branch=main)
[![License](https://img.shields.io/npm/l/@humanspeak/svelte-markdown.svg)](https://github.com/humanspeak/svelte-markdown/blob/main/LICENSE)
[![Downloads](https://img.shields.io/npm/dm/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)
[![CodeQL](https://github.com/humanspeak/svelte-markdown/actions/workflows/codeql.yml/badge.svg)](https://github.com/humanspeak/svelte-markdown/actions/workflows/codeql.yml)
[![Install size](https://packagephobia.com/badge?p=@humanspeak/svelte-markdown)](https://packagephobia.com/result?p=@humanspeak/svelte-markdown)
[![Code Style: Trunk](https://img.shields.io/badge/code%20style-trunk-blue.svg)](https://trunk.io)
[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
[![Types](https://img.shields.io/npm/types/@humanspeak/svelte-markdown.svg)](https://www.npmjs.com/package/@humanspeak/svelte-markdown)
[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://github.com/humanspeak/svelte-markdown/graphs/commit-activity)

## Features

- โšก **Intelligent Token Caching** - 50-200x faster re-renders with automatic LRU cache (< 1ms for cached content)
- ๐Ÿ–ผ๏ธ **Smart Image Lazy Loading** - Automatic lazy loading with fade-in animation and error handling
- ๐Ÿš€ Full markdown syntax support through Marked
- ๐Ÿ’ช Complete TypeScript support with strict typing
- ๐ŸŽจ Customizable component rendering system
- ๐Ÿ”’ Secure HTML parsing via HTMLParser2
- ๐ŸŽฏ GitHub-style slug generation for headers
- โ™ฟ WCAG 2.1 accessibility compliance
- ๐Ÿงช Comprehensive test coverage (vitest and playwright)
- ๐Ÿ”„ Svelte 5 runes compatibility
- ๐Ÿ›ก๏ธ XSS protection and sanitization
- ๐ŸŽจ Custom Marked extensions support (e.g., GitHub-style alerts)
- ๐Ÿ” Improved attribute handling and component isolation
- ๐Ÿ“ฆ Enhanced token cleanup and nested content support

## Recent Updates

### Performance Improvements

- **๐Ÿš€ NEW: Intelligent Token Caching** - Built-in caching layer provides 50-200x speedup for repeated content
- Automatic cache hits in <1ms (vs 50-200ms parsing)
- LRU eviction with configurable size (default: 50 documents)
- TTL support for fresh content (default: 5 minutes)
- Zero configuration needed - works automatically
- Handles ~95% of re-renders from cache in typical usage

- **๐Ÿ–ผ๏ธ NEW: Smart Image Lazy Loading** - Images automatically lazy load with smooth animations
- 70% bandwidth reduction for image-heavy documents
- IntersectionObserver for early prefetch
- Fade-in animation on load
- Error state handling for broken images
- Opt-out available via custom renderer

### New Features

- Improved HTML attribute isolation for nested components
- Enhanced token cleanup for better nested content handling
- Added proper attribute inheritance control
- Implemented strict debugging checks in CI/CD pipeline

### Testing Improvements

- Enhanced Playwright E2E test coverage
- Added comprehensive tests for custom extensions
- Improved test reliability with proper component mounting checks
- Added specific test cases for nested component scenarios
- **Note:** Performance tests use a higher threshold for Firefox due to slower execution in CI environments. See `tests/performance.test.ts` for details.

### CI/CD Enhancements

- Added automated debugging statement detection
- Improved release workflow with GPG signing
- Enhanced PR validation and automated version bumping
- Added manual workflow triggers for better release control
- Implemented monthly cache cleanup

## Installation

```bash
npm i -S @humanspeak/svelte-markdown
```

Or with your preferred package manager:

```bash
pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdown
```

## External Dependencies

This package carefully selects its dependencies to provide a robust and maintainable solution:

### Core Dependencies

- **marked**
- Industry-standard markdown parser
- Battle-tested in production
- Extensive security features

- **github-slugger**
- GitHub-style heading ID generation
- Unicode support
- Collision handling

- **htmlparser2**
- High-performance HTML parsing
- Streaming capabilities
- Security-focused design

## Basic Usage

```svelte

import SvelteMarkdown from '@humanspeak/svelte-markdown'

const source = `
# This is a header

This is a paragraph with **bold** and <em>mixed HTML</em>.

* List item with \`inline code\`
* And a [link](https://svelte.dev)
* With nested items
* Supporting full markdown
`

```

## โšก Performance

### Built-in Intelligent Caching

The package includes an automatic token caching system that dramatically improves performance for repeated content:

**Performance Gains:**

- **First render:** ~150ms (for 100KB markdown)
- **Cached re-render:** <1ms (50-200x faster!)
- **Memory efficient:** LRU eviction keeps cache bounded
- **Smart invalidation:** TTL ensures fresh content

```svelte

import SvelteMarkdown from '@humanspeak/svelte-markdown'

let content = $state('# Hello World')

// Change content back and forth
const toggle = () => {
content = content === '# Hello World' ? '# Goodbye World' : '# Hello World'
}

Toggle Content

```

**How it works:**

- Automatically caches parsed tokens using fast FNV-1a hashing
- Cache key combines markdown source + parser options
- LRU eviction (default: 50 documents, configurable)
- TTL expiration (default: 5 minutes, configurable)
- Zero configuration required - works automatically!

**Advanced cache control:**

```typescript
import { tokenCache, TokenCache } from '@humanspeak/svelte-markdown'

// Use global cache (shared across app)
const cached = tokenCache.getTokens(markdown, options)

// Create custom cache instance
const myCache = new TokenCache({
maxSize: 100, // Cache up to 100 documents
ttl: 10 * 60 * 1000 // 10 minute TTL
})

// Manual cache management
tokenCache.clearAllTokens() // Clear all
tokenCache.deleteTokens(markdown, options) // Clear specific
```

**Best for:**

- โœ… Static documentation sites
- โœ… Real-time markdown editors
- โœ… Component re-renders with same content
- โœ… Navigation between pages
- โœ… User-generated content viewed multiple times

### Smart Image Lazy Loading

Images are automatically lazy loaded with smooth fade-in animations and error handling:

**Benefits:**

- **70% bandwidth reduction** - Only loads visible images
- **Faster page loads** - Images don't block initial render
- **Better LCP** - Improves Largest Contentful Paint score
- **Error handling** - Broken images shown with visual feedback

**How it works:**

```markdown
![Alt text](/image.png 'Optional title')
```

**Features:**

- โœ… Native browser lazy loading (`loading="lazy"`)
- โœ… IntersectionObserver for early prefetch (50px before visible)
- โœ… Smooth fade-in animation (0.3s transition)
- โœ… Error state styling (grayscale + semi-transparent)
- โœ… Responsive images (max-width: 100%)

**Disable lazy loading (use old behavior):**

If you need eager image loading, create a custom Image renderer:

```svelte

let { href = '', title = undefined, text = '' } = $props()

{text}
```

Then use it:

```svelte

import SvelteMarkdown from '@humanspeak/svelte-markdown'
import EagerImage from './EagerImage.svelte'

const renderers = { image: EagerImage }

```

## TypeScript Support

The package is written in TypeScript and includes full type definitions:

```typescript
import type {
Renderers,
Token,
TokensList,
SvelteMarkdownOptions
} from '@humanspeak/svelte-markdown'
```

## Exports for programmatic overrides

You can import renderer maps and helper keys to selectively override behavior.

```ts
import SvelteMarkdown, {
// Maps
defaultRenderers, // markdown renderer map
Html, // HTML renderer map

// Keys
rendererKeys, // markdown renderer keys (excludes 'html')
htmlRendererKeys, // HTML renderer tag names

// Utility components
Unsupported, // markdown-level unsupported fallback
UnsupportedHTML // HTML-level unsupported fallback
} from '@humanspeak/svelte-markdown'

// Example: override a subset
const customRenderers = {
...defaultRenderers,
link: CustomLink,
html: {
...Html,
span: CustomSpan
}
}

// Optional: iterate keys when building overrides dynamically
for (const key of rendererKeys) {
// if (key === 'paragraph') customRenderers.paragraph = MyParagraph
}
for (const tag of htmlRendererKeys) {
// if (tag === 'div') customRenderers.html.div = MyDiv
}
```

Notes

- `rendererKeys` intentionally excludes `html`. Use `htmlRendererKeys` for HTML tag overrides.
- `Unsupported` and `UnsupportedHTML` are available if you want a pass-through fallback strategy.

## Helper utilities for allow/deny strategies

These helpers make it easy to either allow only a subset or exclude only a subset of renderers without writing huge maps by hand.

- **HTML helpers**
- `buildUnsupportedHTML()`: returns a map where every HTML tag uses `UnsupportedHTML`.
- `allowHtmlOnly(allowed)`: enable only the provided tags; others use `UnsupportedHTML`.
- Accepts tag names like `'strong'` or tuples like `['div', MyDiv]` to plug in custom components.
- `excludeHtmlOnly(excluded, overrides?)`: disable only the listed tags (mapped to `UnsupportedHTML`), with optional overrides for non-excluded tags using tuples.
- **Markdown helpers (non-HTML)**
- `buildUnsupportedRenderers()`: returns a map where all markdown renderers (except `html`) use `Unsupported`.
- `allowRenderersOnly(allowed)`: enable only the provided markdown renderer keys; others use `Unsupported`.
- Accepts keys like `'paragraph'` or tuples like `['paragraph', MyParagraph]` to plug in custom components.
- `excludeRenderersOnly(excluded, overrides?)`: disable only the listed markdown renderer keys, with optional overrides for non-excluded keys using tuples.

### HTML helpers in context

The HTML helpers return an `HtmlRenderers` map to be used inside the `html` key of the overall `renderers` map. They do not replace the entire `renderers` object by themselves.

Basic: keep markdown defaults, allow only a few HTML tags (others become `UnsupportedHTML`):

```ts
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
...defaultRenderers, // keep markdown defaults
html: allowHtmlOnly(['strong', 'em', 'a']) // restrict HTML
}
```

Allow a custom component for one tag while allowing others with defaults:

```ts
import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
...defaultRenderers,
html: allowHtmlOnly([['div', MyDiv], 'a'])
}
```

Exclude just a few HTML tags; keep all other HTML tags as defaults:

```ts
import SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
...defaultRenderers,
html: excludeHtmlOnly(['span', 'iframe'])
}

// Or exclude 'span', but override 'a' to CustomA
const renderersWithOverride = {
...defaultRenderers,
html: excludeHtmlOnly(['span'], [['a', CustomA]])
}
```

Disable all HTML quickly (markdown defaults unchanged):

```ts
import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'

const renderers = {
...defaultRenderers,
html: buildUnsupportedHTML()
}
```

### Markdown-only (non-HTML) scenarios

Allow only paragraph and link with defaults, disable others:

```ts
import { allowRenderersOnly } from '@humanspeak/svelte-markdown'

const md = allowRenderersOnly(['paragraph', 'link'])
```

Exclude just link; keep others as defaults:

```ts
import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'

const md = excludeRenderersOnly(['link'])
```

Disable all markdown renderers (except `html`) quickly:

```ts
import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'

const md = buildUnsupportedRenderers()
```

### Combine HTML and Markdown helpers

You can combine both maps in `renderers` for `SvelteMarkdown`.

```svelte

import SvelteMarkdown, { allowRenderersOnly, allowHtmlOnly } from '@humanspeak/svelte-markdown'

const renderers = {
// Only allow a minimal markdown set
...allowRenderersOnly(['paragraph', 'link']),

// Configure HTML separately (only strong/em/a)
html: allowHtmlOnly(['strong', 'em', 'a'])
}

const source = `# Title\n\nThis has <strong>HTML</strong> and [a link](https://example.com).`

```

## Custom Renderer Example

Here's a complete example of a custom renderer with TypeScript support:

```svelte

import type { Snippet } from 'svelte'

interface Props {
children?: Snippet
href?: string
title?: string
}

const { href = '', title = '', children }: Props = $props()


{@render children?.()}

```

If you would like to extend other renderers please take a look inside the [renderers folder](https://github.com/humanspeak/svelte-markdown/tree/main/src/lib/renderers) for the default implentation of them. If you would like feature additions please feel free to open an issue!

## Advanced Features

### Table Support with Mixed Content

The package excels at handling complex nested structures and mixed content:

```markdown
| Type | Content |
| ---------- | --------------------------------------- |
| Nested |

**bold** and _italic_
|
| Mixed List |

  • Item 1

  • Item 2

|
| Code | `inline code` |
```

### HTML in Markdown

Seamlessly mix HTML and Markdown:

```markdown


### This is a Markdown heading inside HTML
And here's some **bold** text too!

Click to expand

- This is a markdown list
- Inside an HTML details element
- Supporting **bold** and _italic_ text

```

## Available Renderers

- `text` - Text within other elements
- `paragraph` - Paragraph (`

`)
- `em` - Emphasis (``)
- `strong` - Strong/bold (``)
- `hr` - Horizontal rule (`


`)
- `blockquote` - Block quote (`
`)
- `del` - Deleted/strike-through (``)
- `link` - Link (``)
- `image` - Image (``)
- `table` - Table (`
`)
- `tablehead` - Table head (``)
- `tablebody` - Table body (``)
- `tablerow` - Table row (``)
- `tablecell` - Table cell (``/``)
- `list` - List (`
    `/`
      `)
      - `listitem` - List item (`
    1. `)
      - `heading` - Heading (`

      `-`

      `)
      - `codespan` - Inline code (``)
      - `code` - Block of code (`
      `)
      
      - `html` - HTML node
      - `rawtext` - All other text that is going to be included in an object above

      ### Optional List Renderers

      For fine-grained styling:

      - `orderedlistitem` - Items in ordered lists
      - `unorderedlistitem` - Items in unordered lists

      ### HTML Renderers

      The `html` renderer is special and can be configured separately to handle HTML elements:

      | Element | Description |
      | -------- | -------------------- |
      | `div` | Division element |
      | `span` | Inline container |
      | `table` | HTML table structure |
      | `thead` | Table header group |
      | `tbody` | Table body group |
      | `tr` | Table row |
      | `td` | Table data cell |
      | `th` | Table header cell |
      | `ul` | Unordered list |
      | `ol` | Ordered list |
      | `li` | List item |
      | `code` | Code block |
      | `em` | Emphasized text |
      | `strong` | Strong text |
      | `a` | Anchor/link |
      | `img` | Image |

      You can customize HTML rendering by providing your own components:

      ```typescript
      import type { HtmlRenderers } from '@humanspeak/svelte-markdown'

      const customHtmlRenderers: Partial = {
      div: YourCustomDivComponent,
      span: YourCustomSpanComponent
      }
      ```

      ## Events

      The component emits a `parsed` event when tokens are calculated:

      ```svelte

      import SvelteMarkdown from '@humanspeak/svelte-markdown'

      const handleParsed = (tokens: Token[] | TokensList) => {
      console.log('Parsed tokens:', tokens)
      }

      ```

      ## Props

      | Prop | Type | Description |
      | --------- | ----------------------- | ------------------------------------- |
      | source | `string \| Token[]` | Markdown content or pre-parsed tokens |
      | renderers | `Partial` | Custom component overrides |
      | options | `SvelteMarkdownOptions` | Marked parser configuration |
      | isInline | `boolean` | Toggle inline parsing mode |

      ## Security

      The package includes several security features:

      - XSS protection through HTML sanitization
      - Secure HTML parsing with HTMLParser2
      - Safe handling of HTML entities
      - Protection against malicious markdown injection

      ## License

      MIT ยฉ [Humanspeak, Inc.](LICENSE)

      ## Credits

      Made with โค๏ธ by [Humanspeak](https://humanspeak.com)