Skip to content

About

transform markdown to html in Svelte files via explicit template

Topics

Resources

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Latest commit

 

History

56 Commits

Folders and files

Repository files navigation

svelte-md-template

transform markdown to html in Svelte files via explicit tagged template

Open on npmx.dev Open on npmx.dev See on bundlephobia See code coverage See license

Installation

With a package manager of choice:

pnpm add -D svelte-md-template

Usage

Add the vite plugin:

/// vite.config.js

import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
import { svelteMdTemplate } from 'svelte-md-template';

export default defineConfig({
	plugins: [
		svelteMdTemplate({/* options */}),
		svelte(), // or import('@sveltejs/kit/vite').sveltekit
	],
});

Write content within the markdown tagged template:

<script>
	import { markdown } from 'svelte-md-template';

	const foo = 'bar';
</script>

{markdown`
# My Markdown Content

Variable can be injected as expected: ${foo}
All templates are merged, e.g. write [link-reference-definition] in one block and use in others.
`}

{markdown`
[link-reference-definition]: https://spec.commonmark.org/0.31.2/#link-reference-definition
`}

The intermediate Svelte code looks something like:

<script>
	const foo = 'bar';
</script>

<h1>My Markdown Content</h1>
<p>Variable can be injected as expected: {foo}</p>
<p>
	All instances are merged, e.g. write
	<a href="https://spec.commonmark.org/0.31.2/#link-reference-definition">
		link-reference-definition
	</a>
	in one block and use in others.
</p>

Why?

Motivation

See the blog post "Rethinking Markdown in Svelte" for more context.

When to not Use This?

When content only relies on basic syntax, e.g. CommonMark Specs, with little or no Svelte code, i recommend sticking to mdsvex or vite-plugin-svelte-md until you have an exact need that this package solves.

Transformer

Markdown-to-HTML transformation may be customised using the transformer option

import { svelteMdTemplate } from 'svelte-md-template';

/// -------- Example --------
svelteMdTemplate({
	transformer: {/** see below */},
});

/// -------- API --------
import type { PluggableList, Processor } from 'unified';

interface SvelteMdTemplateOptions {
	transformer?: SvelteMdTemplateTransformer;
}

type SvelteMdTemplateTransformer =
	| SvelteMdTemplateTransformerUnifiedWithPlugins
	| SvelteMdTemplateTransformerUnified
	| SvelteMdTemplateTransformerCustom;

export type SvelteMdTemplateTransformerUnifiedWithPlugins = {
	type: 'unified';
	remarkPlugins?: PluggableList;
	rehypePlugins?: PluggableList;
};
export type SvelteMdTemplateTransformerUnified = {
	type: 'unified';
	processor: Processor<any, any, any, any, any>;
};
export type SvelteMdTemplateTransformerCustom = {
	type: 'custom';
	transform: (input: {
		/** array of markdown content as they appear from the original Svelte file in tagged templates */
		templates: string[];
		/** `id` from vite transform handler, often the path to current file, unless is a virtual file */
		id?: string;
	}) => string[] | Promise<string[]>;
};

By default, svelteMdTemplate uses a minimal remark-rehype processor:

import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

const processor = unified()
	.use(remarkParse)
	.use(remarkRehype, { allowDangerousHtml: true })
	.use(rehypeStringify, { allowDangerousHtml: true });

The processor is permissive, i.e allowDangerousHtml, for convenience. Security may not be a major concern here since markdown content should be static at compile-time.

Some customisation recipes are listed in the next sub-sections.

Extending the Default

Additional remark & rehype plugins can be added:

import { svelteMdTemplate } from 'svelte-md-template';

svelteMdTemplate({
	transformer: {
		type: 'unified',
		remarkPlugins: [/* ... */],
		rehypePlugins: [/* ... */],
	},
});

There is a definePlugin helper to provide typescript support for plugin options:

import { svelteMdTemplate } from 'svelte-md-template';
import { definePlugin } from 'svelte-md-template/unified';
import remarkEnhanceCodeblock from 'remark-enhance-codeblock';

svelteMdTemplate({
	transformer: {
		type: 'unified',
		remarkPlugins: [definePlugin(remarkEnhanceCodeblock, {/* typing is inferred */})],
	},
});

Providing a Custom unified Processor

A completely custom unified pipeline can be specified. This may be helpful to use a preset, pin specific versions, or when advanced options are necessary.

import { svelteMdTemplate } from 'svelte-md-template';
import { unified } from 'unified';

svelteMdTemplate({
	transformer: {
		type: 'unified',
		processor: unified().use(/* ... */), // specify a preset, for example
	},
});

Bring-Your-Own Transformer

If a use case calls for a strategy other than unified, e.g markdown-it, provide a custom transformer:

import { svelteMdTemplate } from 'svelte-md-template';
import MarkdownIt from 'markdown-it';

svelteMdTemplate({
	transformer: {
		type: 'custom',
		transform: function ({ templates }) {
			const delimiter = '<!-- SVELTE_MD -->';
			const merged = templates.join(`\n\n${delimiter}\n\n`);
			const md = new MarkdownIt({ html: true });
			const transformed = md.render(merged);
			return transformed.split(delimiter);
		},
	},
});

Note that, the transform function:

  • takes an array of strings via the templates property, each corresponding to a markdown`...` template instance in the original Svelte file, and,
  • is expected to return an array of strings matching the aforementioned order.

Including / Excluding Files

By default, svelteMdTemplate should pick up the same files to-be-processed by svelte / sveltekit. In the event it is necessary to explicitly define what to / not to process, specify include / exclude:

import { svelteMdTemplate } from 'svelte-md-template';

/// -------- Example --------
svelteMdTemplate({
	include: /\.md.svelte$/,
	exclude: /\.no-md\.svelte$/,
});

/// -------- API --------
type FilterIdSpecs = (string | RegExp)[] | string | RegExp;
interface SvelteMdTemplateOptions {
	include?: FilterIdSpecs;
	exclude?: FilterIdSpecs;
}

Package Alias

The tag imports are stripped during the process, i.e:

<script>
	import { markdown } from 'svelte-md-template'; // <!-- to be removed automatically -->
</script>

When the package, i.e. svelte-md-template, is aliased to something else, this may not work as expected. In such case, specify importSource:

import { svelteMdTemplate } from 'svelte-md-template';
import MarkdownIt from 'markdown-it';

svelteMdTemplate({
	importSource: 'alias-for-svelte-md-package',
});

Stripping Indentation

It is probably a good idea to not indent your markdown content. This is to prevent language tool (i.e your LSP / syntax highlighting / editor setup) from mistakently parsing content as indended code block.

{markdown(\`
	Don't indent here
	as it will be parsed as
	an indented code block
\`)}

For example, here is a Neovim setup showing the difference between having indentation...

No syntax highlighting with indentation

...and not having indentation:

Syntax highlighting when no indentation

In the event your editor setup is smart enough to strip indentation, or language tool is not a concern, you can turn on the dedent option...

import { svelteMdTemplate } from 'svelte-md-template';
import MarkdownIt from 'markdown-it';

svelteMdTemplate({
	dedent: true,
});

...in which case indentation will be stripped automatically during transform.

Recommended Prettier Config

Make sure necessary prettier plugins are installed:

pnpm add -D prettier-plugin-embed prettier-plugin-svelte

Configure prettier to format Markdown code inside Svelte files:

/// prettier.config.js

/** @type {import('prettier').Config} */
export default {
	/* your regular options here */
	plugins: ['prettier-plugin-embed', 'prettier-plugin-svelte'],
	overrides: [
		{
			files: '**/*.svelte',
			options: /** @satisfies {import('prettier-plugin-embed').PrettierPluginEmbedOptions} */ ({
				embeddedMarkdownTags: ['markdown'],
				noEmbeddedMultiLineIndentation: ['markdown'], // optional, see note below
			}),
		},
	],
};

If using an alias for the markdown template, adjust accordingly.

Note

noEmbeddedMultiLineIndentation is to avoid indenting the code inside markdown`...`, which will be mistakenly picked up by syntax-highlight tooling as an indented code block. See Stripping Indentation for more information.

Tradeoffs & Caveats

Note

The usage is, by design, explicit and verbose.

Escaping Backticks

The most common inconvenience is that backtick has to be escaped:

{markdown`
Backticks, e.g. \`inline code\`, need to be escaped
`}

For code blocks, use tidle, i.e ~, to avoid so:

{markdown`
~~~javascript
console.log('Hello, world!');
~~~
`}

Alternatively, content containing a lot of backticks can be written in some variable / other file and loaded in as needed. For example, using remark-codeblock-source to write code example in separate file:

{markdown`
~~~javascript src="fs:./examples/hello.js"

~~~
`}

{markdown`
~~~language src="github:account/:repo/:commit_or_branch/:filepath"

~~~
`}

Escaping Expressions

${...} is picked up as expected. For example, this input:

<script>
	const foo = 'bar';
</script>

{markdown`
foo is ${foo}
`}

...will be transformed to:

<script>
	const foo = 'bar';
</script>

<p>foo is {foo}</p>

If ${...} is meant to be rendered as is, escape as \${...}.

Similarly {...} will be escaped by default, otherwise it would be picked up as Svelte expression in the output. For example, this input:

{markdown`
{static} is not an expression
`}

...will be be transformed to:

<p>&lbrace;static} is not an expression</p>

Related Projects / Prior Arts

Some remark plugins I wrote that may be helpful:

CONTRIBUTING

See CONTRIBUTING.md for contribution guidelines.


built by human, not agents

About

transform markdown to html in Svelte files via explicit template

Topics

Resources

Contributing

Stars

3 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Contributors

Languages