Markdown
The Markdown extension lets the editor read and write markdown alongside its normal HTML content. It supports markdown shortcuts as you type, serializing content back to markdown, and detecting markdown-like text on paste.
Basic Usage
Initialize the editor with a markdown string. It's parsed into formatted content, and editor.storage.markdown.getMarkdown() serializes it back to markdown on every update.
- The initial
contentoption (andeditor.commands.setContent()) accept a markdown string, which is parsed into the editor's normal document. editor.storage.markdown.getMarkdown()serializes the current document back to a markdown string at any time.
As Markdown
As HTML
html
<HLTextEditor :editor="editor">
<template #header>
<RTEControlHistory :editor="editor" type="undo" />
<RTEControlHistory :editor="editor" type="redo" />
<RTEControlToolbarSeparator />
<RTEControlBold :editor="editor" />
<RTEControlItalic :editor="editor" />
<RTEControlUnderline :editor="editor" />
<RTEControlStrike :editor="editor" />
<RTEControlBlockQuote :editor="editor" />
<RTEControlList :editor="editor" listType="bulletList" />
<RTEControlList :editor="editor" listType="orderedList" />
</template>
</HLTextEditor>ts
import {
AllExtensions,
Editor,
HLTextEditor,
History,
Markdown,
RTEControlBlockQuote,
RTEControlBold,
RTEControlHistory,
RTEControlItalic,
RTEControlList,
RTEControlStrike,
RTEControlToolbarSeparator,
RTEControlUnderline,
} from '@platform-ui/rte'
const markdownSource = `# Welcome
This is **bold**, this is _italic_, and here is a [link](https://gohighlevel.com).
- one
- two
- three`
const editor = new Editor({
content: markdownSource,
extensions: [
History,
AllExtensions.configure({}),
Markdown.configure({
linkify: true,
transformPastedText: true,
}),
],
onUpdate({ editor }) {
const markdown = editor.storage.markdown.getMarkdown()
const html = editor.getHTML()
},
})Detecting Markdown on Paste
You don't always want to silently convert every paste that merely looks like markdown — someone may genuinely want to paste **bold** and see the literal characters. A common pattern is to intercept handlePaste in editorProps, run a lightweight heuristic against the plain-text clipboard payload, and let the user choose what happens next.
html
<HLTextEditor :editor="editor">
<template #header>
<RTEControlHistory :editor="editor" type="undo" />
<RTEControlHistory :editor="editor" type="redo" />
<RTEControlToolbarSeparator />
<RTEControlBold :editor="editor" />
<RTEControlItalic :editor="editor" />
</template>
</HLTextEditor>
<HLModal v-model:show="showMarkdownPastePopup" id="markdown-paste-modal" :showFooter="false" :width="480">
<template #header>
Markdown detected
</template>
<div style="padding: 10px;">
The pasted text looks like markdown. Convert it to formatted content, or paste it as plain text instead?
<div style="display: flex; gap: 8px; justify-content: flex-end;">
<HLButton variant="secondary" @click="pasteMarkdownAsPlainText">Paste as is</HLButton>
<HLButton variant="primary" color="blue" @click="convertPastedMarkdown">Convert markdown</HLButton>
</div>
</div>
</HLModal>ts
import { ref } from 'vue'
import { AllExtensions, Editor, History, Markdown } from '@platform-ui/rte'
const MARKDOWN_PATTERNS = [
/^#{1,6}\s+.+/m, // # Heading
/\*\*[^*\n]+\*\*/, // **bold**
/(^|\s)_[^_\n]+_(\s|$)/, // _italic_
/^\s*[-*+]\s+.+/m, // - bullet list
/^\s*\d+\.\s+.+/m, // 1. ordered list
/^\s*>\s+.+/m, // > blockquote
/```[\s\S]*?```/, // ```code fence```
/\[[^\]]+\]\([^)]+\)/, // [text](link)
]
function looksLikeMarkdown(text) {
if (!text || !text.trim()) return false
return MARKDOWN_PATTERNS.some((pattern) => pattern.test(text))
}
const showMarkdownPastePopup = ref(false)
const pendingPasteText = ref('')
const pendingPasteRange = ref(null)
const editor = new Editor({
content: '<p>Paste some markdown-looking text here.</p>',
extensions: [
History,
AllExtensions.configure({}),
Markdown.configure({ linkify: true, transformPastedText: true }),
],
editorProps: {
handlePaste(view, event) {
const text = event.clipboardData?.getData('text/plain') ?? ''
if (!looksLikeMarkdown(text)) {
return false // not markdown-ish, let the default paste happen
}
const { from, to } = view.state.selection
pendingPasteText.value = text
pendingPasteRange.value = { from, to }
showMarkdownPastePopup.value = true
return true // handled: nothing is inserted until the user picks an option
},
},
})
// Markdown-parses the pasted text before inserting (via the extension's
// overridden `insertContentAt`).
const convertPastedMarkdown = () => {
const { from, to } = pendingPasteRange.value
editor.chain().focus().insertContentAt({ from, to }, pendingPasteText.value).run()
showMarkdownPastePopup.value = false
}
// Builds plain-text paragraph JSON so markdown parsing is skipped entirely
// and `**bold**` stays as visible characters.
const pasteMarkdownAsPlainText = () => {
const { from, to } = pendingPasteRange.value
const paragraphs = pendingPasteText.value.split(/\r\n|\r|\n/).map((line) => ({
type: 'paragraph',
content: line ? [{ type: 'text', text: line }] : [],
}))
editor.chain().focus().insertContentAt({ from, to }, paragraphs).run()
showMarkdownPastePopup.value = false
}Options
ts
export interface MarkdownOptions {
html: boolean
tightLists: boolean
tightListClass: string
bulletListMarker: string
linkify: boolean
breaks: boolean
transformPastedText: boolean
transformCopiedText: boolean
}| Option | Description | Default |
|---|---|---|
html | Allow HTML in the markdown source/output for nodes with no markdown syntax. | true |
linkify | Auto-link bare URLs and emails while parsing markdown. | true |
breaks | Turn single newlines into <br> when parsing markdown. | false |
transformPastedText | Run pasted plain text through the markdown parser instead of inserting as-is. | false |
transformCopiedText | Serialize the copied selection as markdown instead of HTML on copy. | false |
Imports
ts
import { AllExtensions, Editor, HLTextEditor, History, Markdown } from '@platform-ui/rte'