This is a package that provide Vue 3+ component to render markdown content with unified
pipeline.
And is referenced from react-markdown
.
- Render markdown content
- Support unfied
remark
andrehype
plugins for customization - Can customize attributes of html tags (e.g: class, target, rel ....)
- Can use custom template for rendering tag by vue
scoped slot
- Support
<slot>
in markdown content (needrehype-raw
) - Prevent unsafe html by
rehype-sanitize
(optional)
npm install @crazydos/vue-markdown
Recently, the unified ecosystem is undergoing updates for its plugins. This may result in version incompatibility issues, leading to unexpected runtime errors or problems with Typescript. vue-markdown
uses unified 10
, so please ensure that the unified plugin you intend to use corresponds to unified 10
.
Here are the recommended versions for the plugins:
- rehype-raw:
6.1.1
- remark-gfm:
3.0.1
- others: not tested....
You can pass markdown content through the markdown
prop.
<script setup>
import { ref } from 'vue'
import VueMarkdown from '@crazydos/vue-markdown'
const markdown = ref('## Hello World')
</script>
<template>
<VueMarkdown :markdown="markdown" />
</template>
This package uses the unified
pipeline to render Markdown. By default, it only supports basic Markdown syntax (CommonMark). If you need support for other Markdown syntax, you'll need to insert the corresponding unified
plugin.
For features like tables, footnotes, task lists, etc., you need to include the remark-gfm
plugin. (If you need support for other Markdown syntax like math, please use plugins as well.)
You can use the remark-plugins
prop to pass in remark plugins.
npm install [email protected]
<script setup>
import { ref } from 'vue'
import VueMarkdown from '@crazydos/vue-markdown'
import remarkGfm from 'remark-gfm'
const markdown = ref(`## Hello World
- [x] Add some task
- [ ] Do some task
`)
</script>
<template>
<!-- simple usage -->
<VueMarkdown :markdown="markdown" :remark-plugins="[remarkGfm]" />
</template>
You can customize tags for individual HTML elements, for example, by adding default classes or setting attributes like target
, rel
, lazyload
, etc. The customAttrs will be passed into Vue's h
function, so it will have the same effect as passing attributes to a h
. Please refer to Vue's official documentation to understand the effects of different data types when passed to the h
function.
<script setup>
import { ref } from 'vue'
import VueMarkdown from '@crazydos/vue-markdown'
const markdown = ref(`
# Hello world
## Hello World 2
[Google](https://www.google.com)
`)
const customAttrs = {
// use html tag name as key
h1: { 'class': ["heading"] },
h2: { 'class': ["heading"] },
a: { target: '_blank', rel: "noopener noreferrer" }
}
</script>
<template>
<VueMarkdown :markdown="markdown" :custom-attrs="customAttrs" />
</template>
customAttrs
provides some advanced functionalities: pass in a function for more flexible configurations.
const customAttrs = {
h1: { 'class': ["heading"] },
h2: { 'class': ["heading"] },
a: (node, combinedAttrs) => {
if(node.properties.href.startsWith('https://www.google.com')){
return { target: '_blank', rel: "noopener noreferrer"}
} else {
return {}
}
}
}
- Some tags also have aliases, for example, the
heading
alias can be used forh1
throughh6
tags.
For example, if we want to configure it as follows:
const customAttrs = {
h1: { 'class': ["heading"] },
h2: { 'class': ["heading"] },
}
This is equivalent to:
const customAttrs = {
heading: { class: ["heading"] }
}
It can also receive a function for configuration.
// will set
// class="heading heading-1" for h1
// class="heading heading-2" for h2
// .......
const customAttrs = {
heading: (node, combinedAttrs) => {
return { class: ["heading", `heading-${combinedAttrs.level}`] }
}
}
Please refer to the doc, check parameter the function can accept.
This feature is a bit more cumbersome in Vue
. If customAttrs
doesn't meet your needs, you can customize tags through a scoped slot
.
<VueMarkdown
markdown="## hello world"
>
<template #h2="{ children, ...props }">
<h1 v-bind="props">
<Component :is="children" />
</h1>
</template>
</VueMarkdown>
If you want to display the text "hello world" in the example, you need to render the children
. You can obtain the children
from slot props, and it represents the children
of the h1
. Since I've turned it into a functional component, you must render it using <Component>
. You can also render children
wherever you want.
The parameters received by the scoped slot are essentially the same. However, some slots may receive special parameters; for example, in the ul
element, it might receive depth
, indicating the nesting level in a list and specifying which level it is.
<VueMarkdown
:markdown="`## title
- list 1
- list 2
- list 3
`"
>
<template #ul="{ children, depth, ...props }">
<ul :class="['unordered-list', `list-depth-${depth}`]">
<Component :is="children" />
</ul>
</template>
</VueMarkdown>
Additionally, similarly, the scoped slot also provides the same HTML tag alias as customAttrs
. You can use the alias to insert into the slot.
<template>
<VueMarkdown
:markdown="`
# hello world
## hello world
### hello world
#### hello world
##### hello world
###### hello world
`"
>
<template #heading="{ children, level, key, ...props }">
<MyCustomHeading :level="level" :key="key">
<Component :is="children" />
</MyCustomHeading>
</template>
</VueMarkdown>
</template>
For more, please refer to scoped slot
I would recommend keeping the state in the parent component and then passing it down to the Custom Component. When your Markdown changes, especially when implementing an editor feature, the position of elements in the tree nodes can frequently change, causing the components within to be remounted intermittently, resetting their states (even with keys added).
You can use the <slot>
syntax in Markdown, but you need to configure the following:
- setup
rehype-raw
plugins - set
:sanitize="false"
npm install [email protected]
In the following example, we write Vue-like slot syntax in Markdown and can insert this slot from the parent component. At the same time, you can obtain slot props.
Currently, it does not support Vue template syntax like v-bind
(supporting this would be a bit more challenging).
NOTE: Before
v0.2.0
,please use<slot name="custom />"
to specify slot name
<script setup>
import { ref } from 'vue'
import VueMarkdown from '@crazydos/vue-markdown'
import remarkRaw from 'rehype-raw'
const markdown = ref(`
## Hello Slot
<slot slot-name="custom" msg="Insert from vue component"></slot>
`)
</script>
<template>
<!-- simple usage -->
<VueMarkdown :markdown="markdown" :rehype-plugins="[remarkRaw]" :sanitize="false">
<template #custom="{ msg }">
<span> {{ msg }} </span>
</template>
</VueMarkdown>
</template>
To defend against various attacks (e.g., XSS), we use the rehype-sanitize
unified plugin internally. To enable it, please add :sanitize="true"
.
Refer to the rehype-sanitize
documentation for configuration details.
Here's an example configuration that prevents the rendering of all HTML tags, displaying only text:
<script setup lang="ts">
import { ref } from 'vue'
import VueMarkdown, { SanitizeOptions } from '@crazydos/vue-markdown'
const sanitizeOption: SanitizeOptions = {
// Please pass the parameters of rehype-sanitize into the sanitizeOptions:
sanitizeOptions: {
tagNames: [],
},
// mergeOptions: Optional. Internally, we use the `deepmerge` package to combine `defaultSchema` and `sanitizeOptions`. You can adjust the merging behavior in `mergeOptions`.
mergeOptions: {
arrayMerge: (target, source) => {
return source
},
},
}
</script>
<template>
<VueMarkdown
:markdown="content"
:custom-attrs="customAttrs"
:remark-plugins="[remarkGfm]"
:rehype-plugins="[rehypeRaw]"
:sanitize-options="sanitizeOption"
sanitize
></VueMarkdown>
</template>
In both scoped slots and customAttrs
, you can receive additional parameters. Besides the HTML attributes that can be set in Markdown, vue-markdown
also provides additional parameters.
-
h1
~h6
,heading
:level
:number
,represent heading level
-
code
,inline-code
,block-code
:language
:string
。represent language. e.g:js
,sh
,md
....languageOriginal
:string
. It's typically in the form oflanguage-
followed by the language, for example,language-js
,language-sh
. Similar to the language parameter, but language provides a simpler representation.inline
:boolean
,inline-code
orblock-code
-
th
,td
,tr
:isHead
:boolean
。Is it inthead
.
-
list
,ol
,ul
:ordered
:ordered-list
or notdepth
: In nested lists, it indicates the level at which the list is positioned. The first level is 0.
-
list-item
,li
:ordered
: Is inordered-list
or not.depth
: In nested lists, it indicates the level at which the list is positioned. The first level is 0.index
: It represents the position of the list item within its current level. The first item at any level is considered 0.
Attribute aliases can be used in the configuration of both scoped slot
and customAttrs
. Generally, if you are using aliases for configuration or inserting slots, the alias configuration will take precedence over its corresponding HTML tag.
heading
:h1
,h2
,h3
,h4
,h5
,h6
list
:ol
,ul
list-item
:li
inline-code
:code
which is inlineblock-code
:code
which is block code inpre
tag