Skip to content

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 content option (and editor.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
}
OptionDescriptionDefault
htmlAllow HTML in the markdown source/output for nodes with no markdown syntax.true
linkifyAuto-link bare URLs and emails while parsing markdown.true
breaksTurn single newlines into <br> when parsing markdown.false
transformPastedTextRun pasted plain text through the markdown parser instead of inserting as-is.false
transformCopiedTextSerialize the copied selection as markdown instead of HTML on copy.false

Imports

ts
import { AllExtensions, Editor, HLTextEditor, History, Markdown } from '@platform-ui/rte'