sveltedown

A Svelte component for rendering markdown. Inspired by react-markdown.

Check out the demo.

Feature highlights

Contents

Getting started

Install the package:

pnpm i sveltedown # or npm, yarn

Import and use the component:

<script lang="ts">
	import { Markdown } from 'sveltedown';
</script>

<Markdown content="# Hello, world!" />

When should I use this?

This package focusses on making it easy for beginners to safely use markdown in Svelte. It provides sane defaults and options for rendering markdown in a dynamic context. If you reach the end of the customization you can achieve with this component and want to strike out on your own, have no fear! The implementation is actually quite simple, and you can take advantage of svehast to build your own markdown processing pipeline.

API

This package exports two components and one function:

It also exports the following additional TypeScript types:

Components

Markdown

The core export of this package. You can use it like this:

<Markdown content="# Hello, world!" />

content can be any markdown-formatted string.

It also supports custom renderers. Normally, the easiest way to declare these is as snippets that are direct children of Markdown:

<Markdown content="[a link to this package](https://npmjs.com/package/sveltedown)">
  {#snippet a({ tagName, props, children, node })}
    <a {...props} href="/haha-all-links-are-now-the-same">
      {@render children()}
    </a>
  {/snippet}
</Hast>

Remember to render the children!

You can also pass snippets as arguments to the Markdown component (see RendererArg below for argument details):

{#snippet a({ tagName, props, children, node })}
  <a {...props} href="/haha-all-links-are-now-the-same">
    {@render children()}
  </a>
{/snippet}

<Hast node={/* Root */} {a}/>

You can also map nodes to other nodes. For example, if you wanted to only ever render down to a h3, you could map headings 4-6 back to h3:

<Hast node={/* Root */} h4="h3" h5="h3" h6="h3">

That's pretty much it!

MarkdownAsync

If you have an asynchronous plugin in your pipeline, regular Markdown will fail. MarkdownAsync will run your pipeline asynchronously, and is compatible with Svelte's experimental.async compiler option. The API is the same as Markdown, save that it will suspend while rendering your content. You'll want to use a svelte:boundary with pending content:

<svelte:boundary>
  <Markdown content="# Neato burrito">

  {#snippet pending()}
    <Skeleton />
  {/snippet}
<svelte:boundary>

Most of the time, you should avoid asynchronous plugins. Many of them actually have a way to hoist the asynchronous work out of your Markdown pipeline. For example, when using Shiki to highlight code, you can instantiate a global highlighter instance, then share that instance between all of your plugin invocations, which can run synchronously.

Functions

defaultUrlTransform

By default, Markdown does what GitHub does with links. It allows the protocols http, https, irc, ircs, mailto, and xmpp, and URLs relative to the current protocol (such as /something). This function is exported so that if you implement your own URL transform logic, you can reapply the default if necessary.

Types

This package exports a number of types.

Options

The options you can pass to Markdown and MarkdownAsync.

Notes:

URLTransform

Is called every time a HTML property containing a URL is found. Has the opportunity to transform the URL or remove it. Receives the URL, the property the URL came from (src, href, etc.), and the node it came from.

Renderer

The type of a custom renderer. This is either a HTML/SVG tag name (for remapping) or a Snippet accepting a RenderArg as its only argument.

RendererArg

The argument a custom renderer accepts:

A note on tagName: This is the name associated with the resolved renderer, not the one we started with. So if we started with a hast element with a tagName of h6, but h6 had been mapped to h3, the tag name passed to your custom renderer would be h3. If you need the original tag name, you can find it on the node prop, as that remains unchanged.

Renderers

A map of all HTML/SVG tag names that Svelte can render to their corresponding Renderer definition.

Examples

Use custom renderers

<script lang="ts">
	import { Markdown } from 'sveltedown';

	const markdown = '# Neato _burrito_';
</script>

<Markdown content={markdown}>
	<!-- 
   In Svelte, declaring a snippet as the child of a component passes that snippet 
   to the component as a prop! You don't have to do it this way; you can also use
   snippets declared elsewhere by passing them as props to the `Markdown` component.

   `em` captures all `em` elements that would be rendered. We can replace them with
   `strong` elements if we want to.

   Snippets also receive a `tagName` argument (`em` in this case) and a `node` argument,
   which is the original `hast` node. This can be useful if you need to do really fancy
   things with your custom renderers.
   -->
	{#snippet em({ props, children })}
		<strong>{@render children()}</strong>
	{/snippet}
</Markdown>
<details> <summary>Show equivalent HTML</summary>
<h1>
	Neato <strong>burrito</strong>!
</h1>
</details> ```

Plugins

I use unified, specifically remark for markdown and rehype for HTML, which are tools to transform content with plugins. Here are three good ways to find plugins:

Syntax

sveltedown follows CommonMark, which standardizes the differences between markdown implementations, by default. Some syntax extensions are supported through plugins.

remark uses micromark under the hood for its parsing. See its documentation for more information on markdown, CommonMark, and extensions.

Architecture

When you pass a string to Markdown, it passes through a pipeline before it becomes the content you see on the screen: