Markdown 示例

Beta

Markdown 扩展的常见用例的实际示例和方案。

基本示例

读取和写入 Markdown

此示例演示了最常见的 Markdown 操作:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'

const editor = new Editor({
  element: document.querySelector('#editor'),
  extensions: [StarterKit, Markdown],
  content: '# Hello World\n\nStart typing...',
  contentType: 'markdown', // 将初始内容解析为 Markdown
})

// 读取:将当前编辑器内容序列化为 Markdown
console.log(editor.getMarkdown())

// 写入:从 Markdown 字符串设置编辑器内容
editor.commands.setContent('# New title\n\nSome *Markdown* content', { contentType: 'markdown' })

粘贴 Markdown 检测

自动检测并解析粘贴的 Markdown:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'
import { Plugin } from '@tiptap/pm/state'

const PasteMarkdown = Extension.create({
  name: 'pasteMarkdown',

  addProseMirrorPlugins() {
    const { editor } = this;
    return [
      new Plugin({
        props: {
          handlePaste(view, event, slice) {
            const text = event.clipboardData?.getData('text/plain')

            if (!text) {
              return false
            }

            // 检查文本是否看起来像 Markdown
            if (editor.markdown && looksLikeMarkdown(text)) {
              const { state, dispatch } = view
              // 使用 Markdown 管理器将 Markdown 文本解析为 Tiptap JSON
              const json = editor.markdown.parse(text)

              // 在光标位置插入解析后的 JSON 内容
              editor.commands.insertContent(json)
              return true
            }

            return false
          },
        },
      }),
    ]
  },
})

function looksLikeMarkdown(text: string): boolean {
  // 简单启发式规则:检查 Markdown 语法
  return (
    /^#{1,6}\s/.test(text) || // 标题
    /\*\*[^*]+\*\*/.test(text) || // 粗体
    /\[.+\]\(.+\)/.test(text) || // 链接
    /^[-*+]\s/.test(text)
  ) // 列表
}

const editor = new Editor({
  extensions: [StarterKit, Markdown, PasteMarkdown],
})

自定义分词器

下标和上标

支持 ~下标~^上标^

import { Mark } from '@tiptap/core'

export const Subscript = Mark.create({
  name: 'subscript',

  parseHTML() {
    return [{ tag: 'sub' }]
  },

  renderHTML() {
    return ['sub', 0]
  },

  markdownTokenName: 'subscript',

  parseMarkdown: (token, helpers) => {
    const content = helpers.parseInline(token.tokens || [])
    return helpers.applyMark('subscript', content)
  },

  renderMarkdown: (node, helpers) => {
    const content = helpers.renderChildren(node.content || [])
    return `~${content}~`
  },

  markdownTokenizer: {
    name: 'subscript',
    level: 'inline',
    start: (src) => src.indexOf('~'),
    tokenize: (src, tokens, lexer) => {
      const match = /^~([^~]+)~/.exec(src)
      if (!match) return undefined

      return {
        type: 'subscript',
        raw: match[0], // 完整匹配: ~text~
        text: match[1], // 内容: text
        tokens: lexer.inlineTokens(match[1]), // 解析嵌套的行内格式
      }
    },
  },
})

export const Superscript = Mark.create({
  name: 'superscript',

  parseHTML() {
    return [{ tag: 'sup' }]
  },

  renderHTML() {
    return ['sup', 0]
  },

  markdownTokenName: 'superscript',

  parseMarkdown: (token, helpers) => {
    const content = helpers.parseInline(token.tokens || [])
    return helpers.applyMark('superscript', content)
  },

  renderMarkdown: (node, helpers) => {
    const content = helpers.renderChildren(node.content || [])
    return `^${content}^`
  },

  markdownTokenizer: {
    name: 'superscript',
    level: 'inline',
    start: (src) => src.indexOf('^'),
    tokenize: (src, tokens, lexer) => {
      const match = /^\^([^^]+)\^/.exec(src)
      if (!match) return undefined

      return {
        type: 'superscript',
        raw: match[0], // 完整匹配: ^text^
        text: match[1], // 内容: text
        tokens: lexer.inlineTokens(match[1]), // 解析嵌套的行内格式
      }
    },
  },
})

用法:

editor.commands.setContent('H~2~O and E = mc^2^', { contentType: 'markdown' })

集成示例

实时 Markdown 预览

你可以通过监听编辑器更新来创建实时 Markdown 预览:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { Markdown } from '@tiptap/markdown'

const editor = new Editor({
  extensions: [StarterKit, Markdown],
  content: '# Hello',
  onUpdate: ({ editor }) => {
    const markdown = editor.getMarkdown()
    updatePreview(markdown) // 你的预览更新函数
  },
})

function updatePreview(markdown) {
  document.querySelector('#preview').textContent = markdown
}

保存和加载工作流

将内容存储为 Markdown 并在需要时加载:

// 保存到数据库/存储
async function saveContent() {
  const markdown = editor.getMarkdown()
  await fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify({ content: markdown }),
  })
}

// 从数据库/存储加载
async function loadContent() {
  const { content } = await fetch('/api/load').then((r) => r.json())
  editor.commands.setContent(content, { contentType: 'markdown' })
}

服务器端渲染

在服务器上渲染 Markdown:

import StarterKit from '@tiptap/starter-kit'
import { MarkdownManager } from '@tiptap/markdown'
import { generateHTML } from '@tiptap/html'

const markdownManager = new MarkdownManager({
  extensions: [StarterKit, Markdown], // 包含 Markdown 扩展
})

// 在服务器上解析 Markdown 为 JSON
function parseMarkdown(markdown: string) {
  return markdownManager.parse(markdown)
}

// 将 JSON 转换为 HTML 进行渲染
function renderToHTML(json: JSONContent) {
  // 从 Tiptap JSON 生成 HTML(这里不涉及 Markdown)
  return generateHTML(json, [StarterKit])
}

// 完整流程:Markdown → JSON → HTML
function markdownToHTML(markdown: string) {
  const json = parseMarkdown(markdown) // 解析 Markdown 为 JSON
  return renderToHTML(json) // 渲染 JSON 为 HTML
}

// Express 路由示例
app.get('/document/:id', async (req, res) => {
  const doc = await db.getDocument(req.params.id)
  const json = parseMarkdown(doc.markdown) // 解析存储的 markdown
  const html = renderToHTML(json) // 转换为 HTML 进行展示

  res.render('document', { content: html })
})

高级模式

懒加载大型文档

渐进式加载大型文档:

async function loadLargeDocument(documentId: string) {
  // 先加载元数据
  const meta = await fetchDocumentMeta(documentId)

  // 显示骨架屏
  showSkeleton()

  // 按块加载
  const chunks = await fetchDocumentChunks(documentId, meta.chunkCount)

  // 解析每个 Markdown 块并插入到正确位置
  for (const chunk of chunks) {
    const json = editor.markdown.parse(chunk.markdown) // 解析 Markdown 为 JSON
    editor.commands.insertContentAt(chunk.position, json) // 插入到指定位置
  }

  hideSkeleton()
}