一背景与动机

传统的所见即所得(WYSIWYG)编辑器擅长可视化排版,但在书写包含大量公式代码长文档时,Markdown 的简洁与可读性更胜一筹。将两者结合,可以既享受 Markdown 的轻量书写,又能即时预览所见效果。本示例基于 CKEditor 4,演示如何编写一个自定义插件,将 Markdown 转换预览以及公式渲染一并集成到对话框中。

二前置条件

  1. 已经在页面中正确加载了 CKEditor 4。

  2. 具备对 CKEditor 插件目录(通常在 static/ckeditor/plugins/)的读写权限。

  3. 可以访问外部 CDN(或自行下载并部署以下依赖):

三插件目录结构

假设将插件放在 plugins/md/ 目录下,建议结构如下:

md/
├── dialogDefinition.js    // 对话框定义(如前端所见输入预览)
├── dialogs/
│   └── md.js              // 对话框脚本
├── icon.png               // 工具栏图标
└── plugin.js              // 插件主入口

四核心代码解析

1. 插件入口:plugin.js

CKEDITOR.plugins.add('md' {
  requires: 'dialog'
  init: function(editor) {
    // 允许并保护 <script> 标签,确保 MathJax 能正常注入
    editor.filter.allow('script[type]')
    if (editor.config.protectedSource) {
      editor.config.protectedSource.push(/<script[\s\S]*?<\/script>/g)
    }

    // 加载 marked.js
    if (!window.marked) {
      CKEDITOR.scriptLoader.load(
        'https://cdn.jsdelivr.net/npm/marked/marked.min.js'
      )
    }

    // 配置并加载 MathJax
    if (!window._mdMathJaxLoading) {
      window._mdMathJaxLoading = true
      window.MathJax = {
        tex: {
          inlineMath: [['$''$'] ['\\(''\\)']]
          displayMath: [['$$''$$'] ['\\[''\\]']]
        }
        options: {
          skipHtmlTags: {'[-]':['script''noscript''style''textarea''pre''code']}
        }
      }
      CKEDITOR.scriptLoader.load(
        'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
        function(){ window._mdMathJaxReady = true }
      )
    }

    // 在编辑器 iframe 中注入 MathJax 配置与脚本
    editor.on('contentDom' function() {
      var head = editor.document.getHead()
      if (!editor.document.getById('_mj_conf')) {
        head.appendHtml(
          '<script id="_mj_conf">window.MathJax={tex:{inlineMath:[["$""$"]["\\\\(""\\\\)"]]displayMath:[["$$""$$"]["\\\\[""\\\\]"]]}options:{skipHtmlTags:{"[-]":["script""noscript""style""textarea""pre""code"]}}}</script>'
        )
      }
      if (!editor.document.getById('_mj_script')) {
        head.appendHtml(
          '<script id="_mj_script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>'
        )
      }
    })

    // 注册对话框命令和按钮
    CKEDITOR.dialog.add('md' this.path + 'dialogs/md.js')
    editor.addCommand('md' new CKEDITOR.dialogCommand('md'))
    editor.ui.addButton('md' {
      label: '插入 Markdown'
      command: 'md'
      toolbar: 'insert'
      icon: this.path + 'icon.png'
    })
  }
})
  • scriptLoader:动态加载外部脚本,以避免手动在页面中插入。
  • protectedSource:保护 <script> 标签,防止 CKEditor 自动过滤掉 MathJax 配置。
  • contentDom 事件:在编辑器内部 iframe 头部注入 MathJax,以保证编辑时也能渲染公式。

2. 对话框定义:dialogs/md.js

(function() {
  function injectCss() {
    if (window._mdCssInjected) return
    CKEDITOR.document.appendStyleSheet(
      'https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown.min.css'
    )
    CKEDITOR.document.appendStyleText(
      'textarea.cke_dialog_ui_input_textarea { font-family: monospace padding:8px border:1px solid #ccc border-radius:4px resize:vertical}'
      + '#md_preview { background:#fafafa border:1px solid #ddd padding:10px height:100% overflow:auto}'
      + '.markdown-body { background:#fff padding:10px border-radius:4px }'
    )
    window._mdCssInjected = true
  }

  function sanitize(html) {
    // 去除中英文逗号分号,并保护 LaTeX 里的方括号
    return html
      .replace(/[]/g '')
      .replace(/\[(\d+)pt\]/g '\\[$1pt]')
  }

  function updatePreview(mdText) {
    injectCss()
    var dv = document.getElementById('md_preview')
    if (!dv) return
    var html = window.marked
      ? marked.parse(mdText { gfm: true breaks: true })
      : mdText
    html = sanitize(html)
    dv.innerHTML = '<div class="markdown-body">' + html + '</div>'
    if (window._mdMathJaxReady && window.MathJax && MathJax.typesetPromise) {
      MathJax.typesetPromise([dv]).catch(console.error)
    }
  }

  CKEDITOR.dialog.add('md' function(editor) {
    return {
      title: 'Markdown 转换'
      minWidth: 700
      minHeight: 500
      contents: [{
        id: 'tab-md'
        label: 'Markdown'
        elements: [{
          type: 'hbox'
          widths: [ཀྵ%' ཱི%']
          children: [
            {
              id: 'inputMd'
              type: 'textarea'
              label: 'Markdown 输入'
              rows: 20
              onKeyUp: function() {
                updatePreview(this.getValue())
              }
            }
            {
              id: 'preview'
              type: 'html'
              html: '<div id="md_preview"><p style="color:#999margin:0">预览中…</p></div>'
            }
          ]
        }]
      }]
      onShow: function() {
        this.getContentElement('tab-md''inputMd').setValue('')
        updatePreview('')
      }
      onOk: function() {
        var mdText = this.getContentElement('tab-md''inputMd').getValue()
        var html = window.marked
          ? marked.parse(mdText { gfm: true breaks: true })
          : mdText
        html = sanitize(html)
        editor.insertHtml('<div class="markdown-body">' + html + '</div>')
        // 在编辑区触发 MathJax 渲染
        var win = editor.document.getWindow().$
        if (win.MathJax && win.MathJax.typesetPromise) {
          win.MathJax.typesetPromise([editor.document.getBody().$]).catch(console.error)
        }
      }
    }
  })
})()
  • injectCss:一次性注入 GitHub Markdown CSS 和自定义样式,保证预览一致性。
  • sanitize:示例去除多余标点,可根据需求扩展,比如过滤 XSS 或自定义标签。
  • updatePreview:实时将 Markdown 渲染为 HTML 并调用 MathJax。

五使用效果

  1. 点击工具栏上的“插入 Markdown”按钮,弹出对话框。
  2. 左侧输入 Markdown(支持 GFM自动换行水平线等),右侧实时预览。
  3. 支持 LaTeX 公式($...$$$...$$)渲染。
  4. 点击确定,将渲染后的 HTML(含 .markdown-body 容器)插入编辑区,并在编辑区内执行一次 MathJax 渲染。

六常见问题与扩展

  • 样式冲突:若页面已有全局 Markdown 样式,请根据选择器优先级调整或限定作用域。
  • 性能考虑:对话框每次 onKeyUp 都触发解析与渲染,大文本时可能卡顿,可考虑节流或延迟执行。
  • 更多语法支持:可替换或扩展 marked.js 配置,引入插件支持表格任务列表自定义容器等。
  • 安全过滤:生产环境下建议对用户输入进行严格过滤(例如使用 DOMPurify)以防 XSS 风险。