Vditor 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏预览模式

本贴最后更新于 1118 天前,其中的信息可能已经沧海桑田

Vditor
易于使用的 Markdown 编辑器,为适配不同的应用场景而生

npm bundle size


English  |  Demo

💡 简介

Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。

欢迎到 Vditor 官方讨论区了解更多。同时也欢迎关注 B3log 开源社区微信公众号 B3log开源

b3logos.jpg

🗺️ 背景

随着 Markdown 排版方式的普及,越来越多的应用开始集成 Markdown 编辑器。目前主流可集成的 Markdown 编辑器现状如下:

  • 有的仅支持分屏预览,即编辑区和预览区分离
  • 有的同时支持所见即所得和分屏预览,但所见即所得模式下不能完整支持 Markdown 语法排版
  • 几乎没有类似 Typora 的即时渲染

而这三点恰好对应了三种应用场景:

  • 分屏预览:适配传统的 Markdown 使用场景,适合大屏下编辑排版
  • 所见即所得:对不熟悉 Markdown 的用户友好,熟悉 Markdown 的用户也可以无缝使用
  • 即时渲染:理论上这是最为优雅的 Markdown 编辑方式,让熟悉 Markdown 的用户能够更专注于内容创作

所以,一个能够适配应用场景的 Markdown 编辑器至关重要,它需要考虑到:

  • 传统 Markdown 用户的使用场景,提供分屏预览
  • 富文本编辑用户的使用场景,提供所见即所得
  • 高阶 Markdown 用户的使用场景,提供即时渲染

Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编辑领域做出一些贡献。

✨ 特性

  • 支持三种编辑模式:所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv)
  • 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、多媒体、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、plantumlUML 图
  • 导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能
  • 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持 10+ 项配置
  • 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的快捷键、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义
  • 表情/at/话题等自动补全扩展
  • 可使用拖拽、剪切板粘贴上传,显示实时上传进度,支持 CORS 跨域上传
  • 实时保存内容,防止意外丢失
  • 录音支持,用户可直接发布语音
  • 粘贴 HTML 自动转换为 Markdown,如粘贴中包含外链图片可通过指定接口上传到服务器
  • 支持主窗口大小拖拽、字符计数
  • 多主题支持,内置黑白绿三套主题
  • 多语言支持,内置中、英、韩文本地化
  • 支持主流浏览器,对移动端友好

editor.png

preview.png

🔮 编辑模式

所见即所得(WYSIWYG)

所见即所得模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。

vditor-wysiwyg

即时渲染(IR)

即时渲染模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。

vditor-ir

分屏预览(SV)

传统的分屏预览模式适合大屏下的 Markdown 编辑。

vditor-sv

🍱 语法支持

  • 所有 CommonMark 语法:分隔线、ATX 标题、Setext 标题、缩进代码块、围栏代码块、HTML 块、链接引用定义、段落、块引用、列表、反斜杠转义、HTML 实体、行级代码、强调、加粗、链接、图片、行级 HTML、硬换行、软换行和纯文本。
  • 所有 GFM 语法:表格、任务列表项、删除线、自动链接、XSS 过滤
  • 常用 Markdown 扩展语法:脚注、ToC、自定义标题 ID
  • 图表语法
    • 流程图、时序图、甘特图,通过 Mermaid 支持
    • Graphviz
    • 折线图、饼图、脑图等,通过 ECharts 支持
  • 五线谱:通过 abc.js 支持
  • 数学公式:数学公式块、行级数学公式,通过 MathJax 和 KaTeX 支持
  • YAML Front Matter
  • 中文语境优化
    • 中西文之间插入空格
    • 术语拼写修正
    • 中文后跟英文逗号句号等标点替换为中文对应标点

以上大部分特性可以通过开关配置是否启用,开发者可根据自己的应用场景选择搭配。

🗃 案例

  • Sym 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台
  • Solo & Pipe B3log 分布式社区的博客端节点,欢迎加入下一代社区网络
  • Tditor 基于 React、Vditor、Springboot, 一款打造极致文字创作体验的在线 Markdown 编辑平台
  • Arya 基于 Vue、Vditor,所构建的在线 Markdown 编辑器
  • 更多案例

🛠️ 使用文档

CommonJS

  • 安装依赖
npm install vditor --save
  • 在代码中引入并初始化对象,可参考 index.js
import Vditor from 'vditor'
import "~vditor/src/assets/less/index"

const vditor = new Vditor(id, {options...})

HTML script

  • 在 HTML 中插入 CSS 和 JavaScript,可参考 demo
<!-- ⚠️生产环境请指定版本号,如 https://unpkg.com/vditor@x.x.x/dist... -->
<link rel="stylesheet" href="https://unpkg.com/vditor/dist/index.css" />
<script src="https://unpkg.com/vditor/dist/index.min.js"></script>

示例代码

主题

编辑器主题

编辑器所展现的外观。内置 classic,dark 2 套主题。

  • 编辑器初始化时可通过 options.theme 设置内置主题
  • 初始化完成后可通过 setTheme 更新编辑器主题
  • 可通过修改 index.less 中的变量对主题颜色进行定制
  • 可参考现有结构和类名在原有基础上进行修改

内容主题

Markdown 输出的 HTML 所展现的外观。内置 ant-design, light,dark,wechat 4 套主题。支持内容主题扩展接口。

  • 需在显示元素上添加 class="vditor-reset"
  • 编辑器初始化时可通过 options.preview.theme 设置内置或自己开发的主题列表
  • 内容渲染初始化时可通过 IPreviewOptions.theme 设置内置或自己开发的主题
  • 初始化完成后可通过 setThemesetContentTheme 更新内容主题

代码主题

代码块所展现的外观。内置 github 等 36 套主题。

  • 编辑器初始化时可通过 options.preview.hljs 对代码块样式、行号、是否启用进行设置
  • 内容渲染初始化时可通过 IPreviewOptions.hljs 对代码块样式、行号、是否启用进行设置
  • 初始化完成后可通过 setThemesetCodeTheme 更新代码主题

API

id

可填入元素 id 或元素自身 HTMLElement

⚠️:当填入元素自身的 HTMLElement 时需设置 options.cache.id 或将 options.cache.enable 设置为 false

options

说明 默认值
i18n 多语言,参见 ITips -
undoDelay 历史记录间隔 -
after 编辑器异步渲染完成后的回调方法 -
height 编辑器总高度 'auto'
minHeight 编辑区域最小高度 -
width 编辑器总宽度,支持 % 'auto'
placeholder 输入区域为空时的提示 ''
lang 语言种类:en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW 'zh_CN'
input(value: string) 输入后触发 -
focus(value: string) 聚焦后触发 -
blur(value: string) 失焦后触发 -
keydown(event: KeyboardEvent) 按下后触发 -
esc(value: string) esc 按下后触发 -
ctrlEnter(value: string) ⌘/ctrl+enter 按下后触发 -
select(value: string) 编辑器中选中文字后触发 -
tab tab 键操作字符串,支持 \t 及任意字符串 -
typewriterMode 是否启用打字机模式 false
cdn 配置自建 CDN 地址 https://unpkg.com/vditor@${VDITOR_VERSION}
mode 可选模式:sv, ir, wysiwyg 'ir'
debugger 是否显示日志 false
value 编辑器初始化值 ''
theme 主题:classic, dark 'classic'
icon 图标风格:ant, material 'ant'

options.toolbar

  • 工具栏,可使用 name 进行简写: toolbar: ['emoji', 'br', 'bold', '|', 'line'] 。默认值参见 src/ts/util/Options.ts
  • name 可枚举为: emoji , headings , bold , italic , strike , | , line , quote , list , ordered-list , check ,outdent ,indent , code , inline-code , insert-after , insert-before ,undo , redo , upload , link , table , record , edit-mode , both , preview , fullscreen , outline , code-theme , content-theme , export, devtools , info , help , br
  • name 不在枚举中时,可以添加自定义按钮,格式如下:
new Vditor('vditor', {
  toolbar: [
    {
      hotkey: '⇧⌘S',
      name: 'sponsor',
      tipPosition: 's',
      tip: '成为赞助者',
      className: 'right',
      icon: '<svg t="1589994565028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2808" width="32" height="32"><path d="M506.6 423.6m-29.8 0a29.8 29.8 0 1 0 59.6 0 29.8 29.8 0 1 0-59.6 0Z" fill="#0F0F0F" p-id="2809"></path><path d="M717.8 114.5c-83.5 0-158.4 65.4-211.2 122-52.7-56.6-127.7-122-211.2-122-159.5 0-273.9 129.3-273.9 288.9C21.5 562.9 429.3 913 506.6 913s485.1-350.1 485.1-509.7c0.1-159.5-114.4-288.8-273.9-288.8z" fill="#FAFCFB" p-id="2810"></path><path d="M506.6 926c-22 0-61-20.1-116-59.6-51.5-37-109.9-86.4-164.6-139-65.4-63-217.5-220.6-217.5-324 0-81.4 28.6-157.1 80.6-213.1 53.2-57.2 126.4-88.8 206.3-88.8 40 0 81.8 14.1 124.2 41.9 28.1 18.4 56.6 42.8 86.9 74.2 30.3-31.5 58.9-55.8 86.9-74.2 42.5-27.8 84.3-41.9 124.2-41.9 79.9 0 153.2 31.5 206.3 88.8 52 56 80.6 131.7 80.6 213.1 0 103.4-152.1 261-217.5 324-54.6 52.6-113.1 102-164.6 139-54.8 39.5-93.8 59.6-115.8 59.6zM295.4 127.5c-72.6 0-139.1 28.6-187.3 80.4-47.5 51.2-73.7 120.6-73.7 195.4 0 64.8 78.3 178.9 209.6 305.3 53.8 51.8 111.2 100.3 161.7 136.6 56.1 40.4 88.9 54.8 100.9 54.8s44.7-14.4 100.9-54.8c50.5-36.3 108-84.9 161.7-136.6 131.2-126.4 209.6-240.5 209.6-305.3 0-74.9-26.2-144.2-73.7-195.4-48.2-51.9-114.7-80.4-187.3-80.4-61.8 0-127.8 38.5-201.7 117.9-2.5 2.6-5.9 4.1-9.5 4.1s-7.1-1.5-9.5-4.1C423.2 166 357.2 127.5 295.4 127.5z" fill="#141414" p-id="2811"></path><path d="M353.9 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2812"></path><path d="M659.3 415.6m-33.8 0a33.8 33.8 0 1 0 67.6 0 33.8 33.8 0 1 0-67.6 0Z" fill="#0F0F0F" p-id="2813"></path><path d="M411.6 538.5c0 52.3 42.8 95 95 95 52.3 0 95-42.8 95-95v-31.7h-190v31.7z" fill="#5B5143" p-id="2814"></path><path d="M506.6 646.5c-59.6 0-108-48.5-108-108v-31.7c0-7.2 5.8-13 13-13h190.1c7.2 0 13 5.8 13 13v31.7c0 59.5-48.5 108-108.1 108z m-82-126.7v18.7c0 45.2 36.8 82 82 82s82-36.8 82-82v-18.7h-164z" fill="#141414" p-id="2815"></path><path d="M450.4 578.9a54.7 27.5 0 1 0 109.4 0 54.7 27.5 0 1 0-109.4 0Z" fill="#EA64F9" p-id="2816"></path><path d="M256 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2817"></path><path d="M703.3 502.7a32.1 27.5 0 1 0 64.2 0 32.1 27.5 0 1 0-64.2 0Z" fill="#EFAFF9" p-id="2818"></path></svg>',
      click () {alert('捐赠地址:https://ld246.com/sponsor')},
    }],
})
说明 默认值
name 唯一标示 -
icon svg 图标 -
tip 提示 -
tipPosition 提示位置:'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' -
hotkey 快捷键,格式为⇧⌘//⌥⌘ -
suffix 插入编辑器中的后缀 -
prefix 插入编辑器中的前缀 -
click(event: Event, vditor: IVditor) 自定义按钮点击时触发的事件 -
className 样式名 ''
toolbar?: Array<options.toolbar> 子菜单 -

options.toolbarConfig

说明 默认值
hide 是否隐藏工具栏 false
pin 是否固定工具栏 false

options.counter

说明 默认值
enable 是否启用计数器 false
after(length: number, counter: options.counter): void 字数统计回调 -
max 允许输入的最大值 -
type 统计类型:'markdown', 'text' 'markdown'

options.cache

说明 默认值
enable 是否使用 localStorage 进行缓存 true
id 缓存 key,第一个参数为元素且启用缓存时必填 -
after(html: string): string 缓存后的回调 -

options.comment

⚠️:仅支持 wysiwyg 模式

说明 默认值
enable 是否启用评论模式 false
add(id: string, text: string, commentsData: ICommentsData[]) 添加评论回调 -
remove(ids: string[]) 删除评论回调 -
scroll(top: number) 滚动回调 -
adjustTop(commentsData: ICommentsData[]) 文档修改时,适配评论高度 -

options.preview

说明 默认值
delay 预览 debounce 毫秒间隔 1000
maxWidth 预览区域最大宽度 800
mode 显示模式:both, editor 'both'
url md 解析请求 -
parse(element: HTMLElement) 预览回调 -
transform(html: string): string 渲染之前回调 -

options.preview.hljs

说明 默认值
defaultLang 未指定语言时默认使用该语言 ''
enable 是否启用代码高亮 true
style 可选值参见 Chroma github
lineNumber 是否启用行号 false

options.preview.markdown

说明 默认值
autoSpace 自动空格 false
fixTermTypo 自动矫正术语 false
toc 插入目录 false
footnotes 脚注 true
codeBlockPreview wysiwyg 和 ir 模式下是否对代码块进行渲染 true
mathBlockPreview wysiwyg 和 ir 模式下是否对数学公式进行渲染 true
paragraphBeginningSpace 段落开头空两个 false
sanitize 是否启用过滤 XSS true
listStyle 为列表添加 data-style 属性 false
linkBase 链接相对路径前缀 ''
linkPrefix 链接强制前缀 ''
mark 启用 mark 标记 false

options.preview.theme

说明 默认值
current 当前主题 "light"
list 可选主题列表 { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" }
path 主题样式地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme

options.preview.math

说明 默认值
inlineDigit 内联数学公式起始 $ 后是否允许数字 false
macros 使用 MathJax 渲染时传入的宏定义 {}
engine 数学公式渲染引擎:KaTeX, MathJax 'KaTeX'

options.preview.actions?: Array<IPreviewAction | IPreviewActionCustom>

默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。
可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。

说明 默认值
key 按钮唯一标识,不能为空 -
text 按钮文字 -
tooltip 提示 -
className 按钮类名 -
click(key: string) 按钮点击回调事件 -

options.image

说明 默认值
isPreview 是否预览图片 true
preview(bom: Element) => void 图片预览处理 -
说明 默认值
isOpen 是否打开链接地址 true
click(bom: Element) => void 点击链接事件 -

options.hint

说明 默认值
parse 是否进行 md 解析 true
delay 提示 debounce 毫秒间隔 200
emoji 默认表情,可从 lute/emoji_map 中选取,也可自定义 { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' }
emojiTail 常用表情提示 -
emojiPath 表情图片地址 https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji
extend: IHintExtend[] 对 @/话题等关键字自动补全的扩展 []
interface IHintData {
  html: string;
  value: string;
}

interface IHintExtend {
    key: string;

    hint?(value: string): IHintData[] | Promise<IHintData[]>;
}

options.upload

  • 文件上传的数据结构如下。后端返回的数据结构不一致时,可使用 format 进行转换。
// POST data
xhr.send(formData);  // formData = FormData.append("file[]", File)
// return data
{
 "msg": "",
 "code": 0,
 "data": {
 "errFiles": ['filename', 'filename2'],
 "succMap": {
   "filename3": "filepath3",
   "filename3": "filepath3"
   }
 }
}
  • 为了防止站外图片失效, linkToImgUrl 可将剪贴板中的站外图片地址传到服务器端进行保存处理,其数据结构如下:
// POST data
xhr.send(JSON.stringify({url: src})); // src 为站外图片地址
// return data
{
 msg: '',
 code: 0,
 data : {
   originalURL: '',
   url: ''
 }
}
  • successformaterror 不会同时触发,具体调用情况如下:
if (xhr.status === 200) {
    if (vditor.options.upload.success) {
        vditor.options.upload.success(editorElement, xhr.responseText);
    } else {
        let responseText = xhr.responseText;
        if (vditor.options.upload.format) {
            responseText = vditor.options.upload.format(files as File [], xhr.responseText);
        }
        genUploadedLabel(responseText, vditor);
    }
} else {
    if (vditor.options.upload.error) {
        vditor.options.upload.error(xhr.responseText);
    } else {
        vditor.tip.show(xhr.responseText);
    }
}
说明 默认值
url 上传 url,为空则不会触发上传相关事件 ''
max 上传文件最大 Byte 10 * 1024 * 1024
linkToImgUrl 剪切板中包含图片地址时,使用此 url 重新上传 ''
linkToImgCallback(responseText: string) 图片地址上传回调 -
linkToImgFormat(responseText: string): string 对图片地址上传的返回值进行格式化 -
success(editor: HTMLPreElement, msg: string) 上传成功回调 -
error(msg: string) 上传失败回调 -
token CORS 上传验证,头为 X-Upload-Token -
withCredentials 跨站点访问控制 false
headers 请求头设置 -
filename(name: string): string 文件名安全处理 name => name.replace(/\W/g, '')
accept 文件上传类型,同 input accept -
validate(files: File[]) => string | boolean 校验,成功时返回 true 否则返回错误信息 -
handler(files: File[]) => string | null | Promise | Promise 自定义上传,当发生错误时返回错误信息 -
format(files: File[], responseText: string): string 对服务端返回的数据进行转换,以满足内置的数据结构 -
file(files: File[]): File[] | Promise<File[]> 将上传的文件处理后再返回 -
setHeaders(): { [key: string]: string } 上传前使用返回值设置头 -
extraData: { [key: string]: string | Blob } 为 FormData 添加额外的参数 -
multiple 上传文件是否为多个 true
fieldName 上传字段名称 'file[]'

options.resize

说明 默认值
enable 是否支持大小拖拽 false
position 拖拽栏位置:'top', 'bottom' 'bottom'
after(height: number) 拖拽结束的回调 -

options.classes

说明 默认值
preview 预览元素上的 className ''

options.fullscreen

说明 默认值
index 全屏层级 90

options.outline

说明 默认值
enable 初始化是否展现大纲 false
position 大纲位置:'left', 'right' 'left'

methods

说明
exportJSON(markdown: string) 根据 Markdown 获取对应 JSON
getValue() 获取 Markdown 内容
getHTML() 获取 HTML 内容
insertValue(value: string, render = true) 在焦点处插入内容,并默认进行 Markdown 渲染
focus() 聚焦到编辑器
blur() 让编辑器失焦
disabled() 禁用编辑器
enable() 解除编辑器禁用
getSelection(): string 返回选中的字符串
setValue(markdown: string, clearStack = false) 设置编辑器内容且选中清空历史栈
clearStack() 清空撤销和重做记录栈
renderPreview(value?: string) 设置预览区域内容
getCursorPosition():{top: number, left: number} 获取焦点位置
deleteValue() 删除选中内容
updateValue(value: string) 更新选中内容
isUploading() 上传是否还在进行中
clearCache() 清除缓存
disabledCache() 禁用缓存
enableCache() 启用缓存
html2md(value: string) HTML 转 md
tip(text: string, time: number) 消息提示。time 为 0 将一直显示
setPreviewMode(mode: "both" | "editor") 设置预览模式
setTheme(theme: "dark" | "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) 设置主题、内容主题及代码块风格
getCurrentMode(): string 获取编辑器当前编辑模式
destroy() 销毁编辑器
getCommentIds(): {id: string, top: number}[] 获取所有评论
hlCommentIds(ids: string[]) 高亮评论
unHlCommentIds(ids: string[]) 取消评论高亮
removeCommentIds(removeIds: string[]) 删除评论

static methods

  • 不需要进行编辑操作时,仅需引入 method.min.js 后如下直接调用
Vditor.mermaidRender(document)
import VditorPreview from 'vditor/dist/method.min'
VditorPreview.mermaidRender(document)
  • 需要对页面中的 Markdown 进行渲染时可直接调用 preview 方法,参数如下:
previewElement: HTMLDivElement,   // 使用该元素进行渲染
markdown: string,  // 需要渲染的 markdown 原文
options?: IPreviewOptions {
  mode: "dark" | "light";
  anchor?: number;  // 为标题添加锚点 0:不渲染;1:渲染于标题前;2:渲染于标题后,默认 0
  customEmoji?: { [key: string]: string };    // 自定义 emoji,默认为 {}
  lang?: (keyof II18nLang);    // 语言,默认为 'zh_CN'
  emojiPath?: string;    // 表情图片路径
  hljs?: IHljs; // 参见 options.preview.hljs
  speech?: {  // 对选中后的内容进行阅读
    enable?: boolean,
  };
  math?: IMath; // 数学公式渲染配置
  cdn?: string; // 自建 CDN 地址
  transform?(html: string): string; // 在渲染前进行的回调方法
  after?(); // 渲染完成后的回调
  lazyLoadImage?: string; // 设置为 Loading 图片地址后将启用图片的懒加载
  markdown?: options.preview.markdown;
  theme?: options.preview.theme;
  renderers?: ILuteRender; // 自定义渲染 https://ld246.com/article/1588412297062
}
  • ⚠️ method.min.jsindex.min.js 不可同时引入
说明
previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") 点击图片预览
mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) 流程图/时序图/甘特图
flowchartRender(element: HTMLElement, cdn = options.cdn) flowchart 渲染
codeRender(element: HTMLElement) 为 element 中的代码块添加复制按钮
chartRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 图表渲染
mindmapRender(element: (HTMLElement | Document) = document, cdn = options.cdn, theme = options.theme) 脑图渲染
plantumlRender(element: (HTMLElement | Document) = document, cdn = options.cdn) plantuml 渲染
abcRender(element: (HTMLElement | Document) = document, cdn = options.cdn) 五线谱渲染
md2html(mdText: string, options?: IPreviewOptions): Promise<string> Markdown 文本转换为 HTML,该方法需使用异步编程
preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) 页面 Markdown 文章渲染
highlightRender(hljsOption?: IHljs, element?: HTMLElement | Document, cdn = options.cdn) 为 element 中的代码块进行高亮渲染
mediaRender(element: HTMLElement) 特定链接分别渲染为视频、音频、嵌入的 iframe
mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) 对数学公式进行渲染
speechRender(element: HTMLElement, lang?: (keyof II18nLang)) 对选中的文字进行阅读
graphvizRender(element: HTMLElement, cdn?: string) 对 graphviz 进行渲染
outlineRender(contentElement: HTMLElement, targetElement: Element) 对大纲进行渲染
lazyLoadImageRender(element: (HTMLElement | Document) = document) 对启用懒加载的图片进行渲染
setCodeTheme(codeTheme: string, cdn = options.cdn) 设置代码主题,codeTheme 参见 options.preview.hljs.style
setContentTheme(contentTheme: string, path: string) 设置内容主题,contentTheme 参见 options.preview.theme.list

🏗 开发文档

原理相关

环境

  1. 安装 node LTS 版本
  2. 下载最新代码并解压
  3. 根目录运行 npm install
  4. npm run start 启动本地服务器,打开 http://localhost:9000
  5. 修改代码
  6. npm run build 打包代码到 dist 目录

CDN 切换

由于使用了按需加载的机制,默认 CDN 为 https://unpkg.com/vditor@ 版本号

如果代码有修改或需要使用自建 CDN 的话,可按以下步骤进行操作:

  • 初始化时,需对 optionsIPreviewOptions 中的 cdnemojiPath, themes 进行配置
  • highlightRender , mathRender , abcRender , chartRender , mermaidRenderflowchartRendermindmapRendergraphvizRendersetCodeThemesetContentTheme 方法中需添加 cdn 参数
  • 将 build 成功的 dist 目录或 jsDelivr 中的 dist 目录拷贝至正确的位置

升级

版本升级时请仔细阅读 CHANGELOG 中的升级部分

Ⓜ️ Markdown 使用指南

🏘️ 社区

📄 授权

Vditor 使用 MIT 开源协议。

🙏 鸣谢

  • Lute:🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript
  • highlight.js:JavaScript syntax highlighter
  • mermaid:Generation of diagram and flowchart from text in a similar manner as Markdown
  • incubator-echarts:A powerful, interactive charting and visualization library for browser
  • abcjs:JavaScript library for rendering standard music notation in a browser

📽️ 历史

我们在开发 Sym 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。

后来,Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 CodeMirror,这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。

再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @ 自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发,而这些业务需求恰恰是很多项目场景共有且必备的。

终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代,Sym 的编辑器也日趋成熟。在我们运营的社区链滴上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 V 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。

于是,Vditor 就这样诞生了。

相关帖子

优质回帖
  • Vanessa 1
    支持者 赞助者 订阅者 作者

    前端是这样传送的

        const formData = new FormData();
        for (let i = 0, iMax = uploadFileList.length; i < iMax; i++) {
            formData.append("file[]", uploadFileList[i]);
        }
         xhr.send(formData);
    
  • haaid 1

    文字错误:接触编辑器禁用,「接触」->「解除」

  • someone9891 1
    捐赠者

    此链接 404

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • HerbertHe 2 评论
    支持者

    options 的 value 参数设置初始值无效,在编辑器中无法初始化

    Vanessa
    哈喽,我最近和你一样用的 next.js 在集成大 v,能否看下你在初始化 Vditor 的时候怎么写的,我在 hooks 里面写的,结果强刷出现 js error,谢谢!
    XShellv
  • 547176052 1 评论

    image.png

    Issue #504 · Vanessa219/vditor 3.3.3 发布后可使用 esc
    Vanessa
  • 547176052 1 评论

    能不能添加自定义语法和自定义渲染 比如把 关键字 aaa 渲染成一个按钮

    有自定义渲染功能 http://vditor.b3log.org/demo/preview-custom.html,但是 aaa 的话需要看你在什么节点里面了。
    Vanessa
  • 547176052 1 评论

    即时渲染 和所见即所得 粘贴图片 不能触发 自定义上传

    image.png

    需设置 options.upload.url
    Vanessa
  • 547176052 1 评论

    有没有 7 牛上传图片的例子

    没有哦。都是服务端自己的接口
    Vanessa
  • prh 1 评论

    大佬,输入时有这个黑框是什么问题

    image.pngimage.png

    换行时回车没有效果,必须回车然后输入内容才能看到换行image.png

    image.png

    @Vanessa 这是 f12 截图,能看出来什么问题吗

    使用 3.0.10 版本虽然还是有黑框,都是换行有效果了

    image.png

    2 操作
    prh 在 2020-06-29 11:50:16 更新了该回帖
    prh 在 2020-06-29 10:14:05 更新了该回帖
    你 F12 看一下界面上的黑框。我这里没有遇到这样的情况
    Vanessa
  • MrLucy 2 评论

    Snipaste20200629220750.png

    内容居中显示了,这个如何处理

    查看一下是不是被 CSS 重置了
    Vanessa
    @Vanessa 感谢,已经找到了,集成到项目中,有个全局的 css 设置了 text-align: center; 刚才一直没有找到
    MrLucy
  • 547176052 1 评论

    image.png

    vue 怎么离开页面时销毁掉编辑器

    Vanessa
  • zyk 3 评论
    捐赠者 支持者 订阅者

    @Vanessa V 姐,我在 element-ui 的弹窗中载入了封装好的 vditor 组件,在父组件中通过 this.$refs 获取 vditor 子组件并修改编辑器中内容时,第一次进入弹窗都无法修改编辑器内容,第二次及以后是可以的。

    image.png

          // 修改表单数据(使用 $nextTick 回调解决无法通过 $refs 获取子组件数据)
          this.$nextTick(() => {
            this.$refs.editEditor.setContent(this.editForm.content)
          })
    
    this.$refs.editEditor.setContent 要放在 new 的 after 回调函数中
    Vanessa 1
    @Vanessa 好的,我试试
    zyk
    @Vanessa 搞定了,我在 new 的 after 回调函数里指定编辑器初始值,然后在 vditor 子组件中用 watch 监听编辑器内容变化来改变编辑器内容,谢谢 V 姐 😁
    zyk
  • liwuming
    1. 如果 vditor 可以支持 :::success内容::::::info内容::::::warning内容::::::error内容::: 标签就好了,比单纯的 blockquote 标签要好一些,更适合编写 api 文件,只可惜我技术能力有限不能进行二次开发
    2. 可以支持集成 markdown-table 就更好了,因为 markdown 操作 table 极为不方便,如果可以支持 table 操作就真的完美了
    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者
    1. 不太明白 :::success内容::: 这个语法的作用和展现
    2. 感觉 markdown-table 只是将对象转换成 markdwon 语法的 table,方便的操作指的是?
  • xhaoxiong 1 评论
    捐赠者

    如何关闭 vditor 中的快捷键呢,我自定义的快捷键和它冲突了

    你需要关闭的是哪一个快捷键呢?
    Vanessa
  • sweeter

    npm run start 浏览器打开 9000 页面显示不完整,很多东西加载不出来,根据官方提示下载 zip 包的时候,就报错很多文件损坏,醉了,弄了一天都没法用

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者

    这个应该是 npm 的问题。你用一下代理试试

    2 回复
  • yuanmeng

    insertValue 没有办法插入图片标签

  • yuanmeng

    @Vanessa 每次 insertValue 之后,里面就一串地址,需要手动触发一下(就是删一个或者添加一个)才能显示对应的图片,

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者

    https://b3log.org/vditor/demo/method-CRUD.html 上运行 vditor.insertValue('![](https://avatars0.githubusercontent.com/u/970828?s=60&v=4)'); 是没有问题的。是不是图片加载过慢?

    1 回复
  • yuanmeng

    我插入的是标签格式

    vditor.insertValue('<img src=https://avatars0.githubusercontent.com/u/970828?s=60&v=4 alt=”img.jpg“>')

    在 3.45 之后就插入不了了

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者

    哦。因为插入 html 有其他副作用,所以关闭了。你可以先调用已有方法转换为 md 再进行插入

    1 回复
  • yuanmeng

    好的,谢谢,视频和音频用的都是标签的格式把,这个方法也同样不能用,我看说需要 Vditor.mediaRender()这个方法

    1 回复
  • yuluo 1 赞同 1 评论

    image.png

    怎么修改复制的提示文字与左下角的怎么删除?

    使用 ir 模式
    Vanessa
  • yuluo 3 评论

    image.png

    怎么改成在点击的时候,title 消失.不然 title 在下边的时候,会存在遮挡

    这个目前浏览器不支持
    Vanessa
    @Vanessa 不知道后边这个可以修改一下吗?
    yuluo
    @yuluo 目前还没想到好的解决方案
    Vanessa
  • Vanessa
    支持者 赞助者 订阅者 作者

    直接插入 HTML 会相应的转换为 html 标签。在预览的时候会出现,只是 markdown 没有该语法,就会使用对应的方式展现:
    ir
    image.png
    sv

    image.png

  • yuluo 1 评论

    有没有大神写一下 vue 项目下单图片与多图片上传到服务器的代码,小弟感激不尽.

    你可以到项目案例上看一看有没有人写过的开源的
    Vanessa
  • yuluo

    有没有大神写一下 vue 项目下单图片与多图片上传到服务器的代码,小弟感激不尽.

  • 0995 1 评论

    <链接地址> 直接显示链接地址 为什么要转换成 [链接名称](链接地址) @_@

    方便用户点击
    Vanessa
  • 0995 3 评论

    < 链接地址 > 转换成 [链接名称] (链接地址) 时, 链接地址里的中文会变成 url 编码

    有什么问题么?
    Vanessa 1
    @0995 你的意思是名称还应该保持中文?
    Vanessa 1 赞同
    Vanessa 1
  • sweeter

    v 姐 localhost:9000 可以访问了 我想把这个作为自己博客页面的编辑器 但是不知道怎么集成进去

    刚开始用了这种

    Title
    上面这段代码无法显示出编辑器

    接着我又试了另一种,把 dist 目录拷贝到项目的静态文件夹里,然后也不行 空白

    Snipaste20200905114812.jpg

    实在没办法,editor.md 的教程倒是不少,但是我觉得没有 vditor 好用,无奈关于 vditor 的教程又太少,v 姐能给个页面嵌入 vditor 的例子吗

  • sweeter
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <!-- ⚠️生产环境请指定版本号,如 https://cdn.jsdelivr.net/npm/vditor@x.x.x/dist... -->
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vditor/dist/index.css" />
        <script src="https://cdn.jsdelivr.net/npm/vditor/dist/index.min.js" defer></script>
    </head>
    <body>
    <div class="vditor"></div>
    </body>
    </html>
    <script>
        import Vditor from 'vditor'
        import "~vditor/src/assets/scss/index"
        const vditor = new Vditor('vditor', {
            "height": 360,
            "theme": "dark",
            "cache": {
                "enable": false
            },
            "preview": {
                "theme": {
                    "current": "dark"
                }
            }
        })
    </script>
    
    

    这是刚开始试的代码

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者

    你可以到 https://b3log.org/vditor/demo 上看一下,上面有很多示例

    1 回复
  • sweeter 1 评论

    没有纯 js 的示例,必须基于 vue react angular 之类的框架才能使用吗

    demo 上有纯 js 的示例的
    Vanessa
  • emmm

    编辑器字数统计有回调吗,想拿到这个字数

  • emmm 1 评论

    请问这个编辑器里面输入字数有回调函数能取到吗

    目前没有,可关注 Issue #759 · Vanessa219/vditor
    Vanessa
  • eeezae 3 评论

    从剪贴板粘贴的图片怎么触发 upload.handler 事件?我需要自定义上传

    需求应该就是这个,Issue #469 · Vanessa219/vditor,但是我使用 linkToImgCallback 方法粘贴图片时并没有触发
    eeezae 1
    upload.handler 用于处理文件,两个不能通用。目前针对链接上传的只有成功回调,你可以根据请求在服务端自行处理。
    Vanessa
    这个是指的站外图片地址吧,我指的是截图后存到剪贴板的图片怎么触发呢?
    eeezae
  • XShellv 1 评论

    吐传出来的 markdown 字符串通过 markdown-it 渲染后,流程图等不能显示,这个问题怎么解决?实时编辑的预览能够看得到,难道还要另外处理么?

    markdown 渲染可以参考 https://b3log.org/vditor/demo/ 中的页面渲染示例
    Vanessa
  • XShellv 1 评论

    感谢,这个问题解决了,另外有个问题,我只要一强刷页面就会出现 JS Error,请问这是为什么呢?

    image.png

    初始化时,你调用了什么方法?
    Vanessa
  • XShellv 1 评论

    这是我的代码,我用了 React 的 hooks 编写的,把 vditor 单独封装了,不知道问题出在哪里,只要强刷页面就会出现问题,正常刷新打开都是 ok 的。

    import React, { useEffect } from "react";
    import Vditor from "vditor";
    import "./index.scss";
    
    const Markdown = (props) => {
      useEffect(() => {
        const vditor = new Vditor("vditor", {
          height: 360,
          tab: "      ",
          mode: "sv",
          toolbarConfig: {
            pin: true,
          },
          cache: {
            enable: false,
          },
          input: (value, previewElement) => {
            //   triggerChange(value);
            props.setContent(value);
          },
          after: () => {
            debugger;
            vditor.setValue(props.value);
          },
        });
        return () => vditor && vditor.destroy();
      }, [props.value]);
    
      return <div id="vditor"></div>;
    };
    export default Markdown;
    
    是不是因为没有 dom?
    Vanessa
  • n0ifs 1 评论

    所见即所得与即时渲染来回切换, 会导致部分内容丢失, 是什么原因导致的?

    可以具体说一下么?
    Vanessa
  • yzisme 1 评论

    能不能支持 prismjs 代码高亮, highlightjs 的代码高亮有点不准确.

    目前的在做 vditor 的桌面端-思源笔记,这个可能要稍后了。
    Vanessa
  • XShellv 3 评论

    请问如何实现服务端渲染,前端直接拿到服务端渲染好的 html

    使用 lute
    Vanessa
    @Vanessa 有用法介绍么?
    XShellv
  • TimberKun 1 评论

    在 react 项目中 映入 vidtor 后 页面显示的富文本编辑器没有样式 是什么问题呢?(有引入 index.scss)

    F12 看一下
    Vanessa
  • TimberKun 1 评论

    在 react 项目中 映入 vidtor 后 页面显示的富文本编辑器没有样式(无警告报错)

    webpack 中有解析 scss 了么?
    Vanessa
  • merrylmr 1 评论

    @Vanessa 你好,我看了 vditor 的文档,在编辑器模式下没有可以生成大纲的方法。

    我想在编辑模式下,调用一个生成大纲的方法,现在支持吗?

    https://ld246.com/guide/markdown 你说的是左侧的大纲么?
    Vanessa
  • merrylmr 1 评论

    @Vanessa 我的需求是这样的:大纲在右侧(或则当前页面的任何位置),可以显示或则隐藏。虽然编辑器目前自带了一个大纲,但是不能满足我的需求。

    我的想法是:能否提供一个方法,我可以放在任何 DOM 上,生成当前的大纲?

    1 回复
    outlineRender
    Vanessa
  • shuiniu
    支持者

    厉害,这功能太多了 👍

  • LoseRecall 1 评论

    教程太简洁了,对不会前端的太不友好了,希望能完善一下

    可以看 demo
    Vanessa
  • sweeter 1 评论

    BUG:即使渲染模式下,代码块中代码过长时,点击空白的文本区域,会一直跳到代码块里

    	<style>
            .v-enter,.v-leave-to{
                transform: translateX(80px);
                opacity:0;
            }
            .v-enter-active,.v-leave-active
            {
                transition: all 0.4s ease;
            }
    </style>
    
    <div id="app">
        <button @click="flag=!flag">button</button>
        <transition>
            <div v-if="flag">Vue animate</div>
        </transition>
    </div>
    
    <script>
        new Vue({
            el:"#app",
            data:{
                flag:true,
            }
        })
    </script>
    

    可以把上面代码粘贴到完整实例中,然后把模式改为即使渲染,就 能复原 bug

    需在飘号后下方一点点击才有作用 。点中票号也会算为代码块点击。
    Vanessa
  • thinkido
    支持者

    自己用 jquery 之类的修改大纲位置就把大纲调整到右侧了。

  • qianlei 2 评论

    const params: any = {
    options: {
    markdown: {
    sanitize: true
    }
    }
    }

    console.log(this.content)
    if (vditorCdn) params.cdn = vditorCdn
    await VditorPreview.md2html(this.content, params).then((domStr: string) => {})

    文本内容:content: ""`

    image.png

    文本就是一个 iframe 里面包含代码 直接给执行了 <script>alert('xss')<

    这边显示不了 iframe 的标签, 直接过滤掉了,想知道是怎么实现的

    md2html 方法怎么过滤 xss

    5 操作
    qianlei 在 2020-11-10 19:52:58 更新了该回帖
    qianlei 在 2020-11-10 19:44:58 更新了该回帖
    qianlei 在 2020-11-10 15:39:30 更新了该回帖
    qianlei 在 2020-11-10 14:17:16 更新了该回帖 qianlei 在 2020-11-10 14:16:16 更新了该回帖
    麻烦给一下没有过滤的文本
    Vanessa
    过滤 xss 是使用的 lute
    Vanessa
  • YangPC

    有将编辑器封装成公共组件的兄弟吗?有的话,联系我,有偿。

    Q:327844761

请输入回帖内容 ...