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

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

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

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • iTanken 1
    捐赠者
    该回帖仅作者和楼主可见
    1 操作
    iTanken 在 2021-04-09 11:36:24 更新了该回帖
  • leesin 5 评论

    这大纲点击后不能自动跳到指定位置了,是 bug 吗?

    3.8.4 版本
    leesin
    你看一下官网 demo,应该是好的
    Vanessa
    @Vanessa 好的
    leesin
    @Vanessa 好的 , 官网用的是什么版本?
    leesin
    @leesin 最新的
    Vanessa
  • gztrljh 1 评论

    V 姐,vditor 微信小程序可以用么?

    没弄过,你可以试试
    Vanessa
  • leesin 3 评论

    请问一下,第一次初始化后,可以修改编辑或预览这两种模式么?

    用的 vue
    leesin
    有没有 api 可以调用进行修改的,或者动态修改属性改变的
    leesin
    @leesin 看一下接口 options
    Vanessa
  • 547176052 3 评论

    image.png

    思维导图 太小了 能否添加 一个扩大的功能 全屏 或者新窗口打开

    收到,后期完善
    Vanessa
    @Vanessa 更新了没
    547176052
  • gztrljh 1 评论

    V 姐

    image.png

    要自己写,不行就用 format
    Vanessa
  • gztrljh 2 评论

    V 姐,内容很多的时候,写作的时候有明显卡顿现象,请问该如何优化?

    大概是有 15000 多个字吧中间还有很多图片,然后具体描述比如没内容的时候,一直按住 a 它会连续弹出 a,但是内容多了的时候按住 a 它会卡住,过一会儿 a 才会全部显示。我个人猜测是内容太大了,每次解析耗费性能了?
    gztrljh
    编辑模式和文档发我测试一下
    Vanessa
  • Keyon 1 评论

    请问在 React 中使用 Vditor,通过 import Vditor from "vditor" 的方式导入。

    在配置使用自定义渲染器时却报错:“Lute 未定义” 应该如何解决?

    Lute 需在在 options.after 后才可使用
    Vanessa
  • gztrljh

    @Vanessa

    测试文档.rar

    wysiwyg 模式
    

    问题:没有内容很少的那种流畅性

  • gztrljh

    有个新的需求:

    二级菜单,无法自定义文本内容

    image.png
    https://b3logfile.com/file/2021/05/image-320ef384.png)

    image.png

  • 547176052 1 评论

    当禁止编辑器时上面这些按钮还能点击

    image.png

  • Keyon 1 评论

    自定义渲染器在 IR 编辑状态下支持吗?我试着在 options.after()内调用 vditor.lute.SetJSRenderers 接口,还是提示 Lute 未定义。

    相关代码:

    after() {
            vditor.vditor.lute.SetJSRenderers({
              renderers: {
                Md2VditorSVDOM: {
                  renderHTML: (node, entering) => {
                    if (entering) {
                      console.log("entering");
                      return [``, Lute.WalkContinue];
                    } else {
                      console.log("entering out ");
                      return [``, Lute.WalkContinue];
                    }
                  },
                },
              },
            });
    
    这个是全局变量,你调试看看。
    Vanessa
  • gztrljh 3 评论

    V 姐那个上传文件那个提示如何关闭呢?

    我想完全用我自己的通知插件来接管

    image.png

    这个通知我想换成我自己的这个

    image.png

    目前没有接口,用样式弄弄?
    Vanessa
    @Vanessa 嗯嗯,V 姐希望尽快把这个接口弄出来完全由开发者接管。支持我现在就在用,发现这个编辑器是真的强
    gztrljh
    @gztrljh 开发者可以使用 css 进行接管
    Vanessa
  • gztrljh 1 评论

    编辑模式下可以自定义渲染么?如何关闭编辑模式下的图片预览呢?

    Vanessa
  • JoeanAmiee 1 评论

    使用 vditor.getValue();提示未定义是什么情况?

    需在 after 中进行回掉
    Vanessa
  • Charlene 5 评论

    怎么把大纲也显示到查看详情页面?

    @Vanessa 我实现了回显大纲,但是我点击大纲,他滚动的整个 window,我想要的是只滚动 preview 那个小的 box,应该怎么处理啊
    Charlene
    @Charlene 把 scrolltop 设置到 preview 上
    Vanessa
    @Vanessa 我试过的,没有找到点击大纲调用 window 滚动的方法,好像是默认的,所以没法设置 preview 的 scrolltop
    Charlene
  • Charlene

    怎么对大纲进行渲染

    该回帖因已过时而被折叠
    1 操作
    Vanessa 在 2021-05-19 12:05:02 折叠了该回帖
  • alexmh 1 评论

    升级到 3.8.5 发现编译时会报错 弱弱的问一句 是不是源码有问题呀 @Vanessa

    image.png

    image.png

    把 node_modules 删除啦重新安装
    Vanessa
  • lydia 1 评论

    image.png

    image.png

    3.8.5 统计字数回调不执行

    😭

    抱歉,文档写错啦,请参见 https://b3log.org/vditor/demo/advanced-counter.html
    Vanessa 1
  • lydia 1 评论

    上传图片后页面显示

    image.png

    预览效果显示效果

    image.png

    代码逻辑

    image.png

    另外开启了缓存 cache 模式,刷新后图片就丢失了,切换编辑模式图片也会丢失,其他文本内容还在,版本 3.8.5

    你用默认的试试。如果要自定义上传的话可以看看相关源码 src/ts/upload/index.ts 75 行
    Vanessa
  • gztrljh 1 评论

    问题 1:是否有类似 UEditor 的 contentChange 事件?没有的话有什么替代方案呢?

    问题 2:按两下 Tab 键会自动生成代码块的开头和结尾标记,请问如何取消?

    input,options.tab
    Vanessa 1 赞同
  • gztrljh 1 评论

    V 姐,如何限制最大输入字数呢?选项中我设置了最大输入字符,它还是可以继续输入。image.png 我想让它输入超过 10 个,就不会再继续输入内容,怎么弄呢?

    用回调 disabled
    Vanessa
  • gztrljh 1 评论

    我要实现 input 框的 maxlength 属性的效果,可以编辑器自定义事件么?比如 keyup 事件,还有通过 setcontent 方法后,如何让光标保持到最后呢?

    有个字数统计的回掉,你可以看看。
    Vanessa
  • wenqvip 1 评论

    options 传什么参数可以打开时就处于非编辑状态?类似于工具栏更多里面的预览

    after 里面 调用 disabled()
    Vanessa 1
  • gztrljh

    bug 反馈

    版本号:3.8.6

    报错:Uncaught TypeError: Failed to execute 'selectNodeContents' on 'Range': parameter 1 is not of type 'Node'.

    重现方式:工具栏直接点击分割线后不要输入任何内容,然后 ctrl+a 全选后删除就会报这个错误

    代码块 ctrl+ 回车选择语言的时候直接报错

    image.png

    在一个段落最后面输入内容光标会另起一行

    不知道这个算不算 bug,明明一个段落后面还有很多空白空间,一输入内容光标就会到下一行开头。

  • gztrljh 3 评论

    复制一片文章后,右边的大纲会渲染目录,导致右侧大纲变形和失效,应该只读取 h1 到 h6 的纯文本

    image.png

    发一下原文
    Vanessa
    @Vanessa 地址:https://v3.bootcss.com/css/#tables 打开后 ctrl+a 到编辑器 ctrl+c 就会复现。
    gztrljh
    @gztrljh 我这里是正常的,具体的问题是?
    Vanessa
  • gztrljh 3 评论

    优化建议:表格出现滚动条后,鼠标滚轮滑动可以让表格横着滑动

    滚轮都是纵向滚动的吧,
    Vanessa
    @Vanessa 嗯,整体内容滚动时纵向滚动,但是鼠标移入表格区域,鼠标滚轮滚动要是可以让出现滚动条的表格可以横向滚动就好了。当然了,只是一个小建议。
    gztrljh
    Vanessa
  • gztrljh 1 评论

    3.8.6 标题有问题,原本的版本是 输入 ##输入标题后,按回车后会渲染,现在一输入#它永远都只是 h1 标题了。

    收到,下个版本修复
    Vanessa
  • gztrljh 4 评论

    bug 反馈

    版本:3.8.6

    1.当编辑器调用 disabled 方法禁用编辑器时,toolbar 上部分按钮任然可以点击,比如 代码块主题 还有 导出 按钮 以及自定义的一些按钮还是可以点击。

    2.禁用编辑器后复制粘贴图片依然可以上传,期望的结果是,禁用后所有的按钮,包括自定义按钮以及图片无法复制粘贴上传。

    是哪一个编辑模式?
    Vanessa
    @Vanessa wysuwyg
    gztrljh
    主题和导出是允许的,这个是特性。自定义按钮需要自己处理,有些用户可能在 disabled 的时候还需要执行的。图片的复制上传下个版本修复
    Vanessa
    @Vanessa 好的 V 姐,我想了想确实也是,禁用后它不仅会上传图片,复制的内容也会粘贴进去。
    gztrljh
  • ajslpzcd

    特意注册账号来留言,markdown 的表格功能实在太难用了,要添加一行内容,或者删除一行内容都太难搞了,表格编辑功能真的有产品经理或者测试测过嘛

    而且不知道怎么就成了我的 vscode 默认 markdown 编辑器,怎么卸载都卸载不掉,🙃

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

    抱歉,Vditor 仅支持浏览器。如果需要使用本地的话推荐使用 Typora 或者思源笔记。

    1 回复
  • ajslpzcd

    不好意思呀,是我太激动了

  • gztrljh 1 评论

    input() 事件触发它会有一个延时效果,因为特殊需求,那如何时时触发呢?内容一改变立马触发,那个延时效果我需要自己实现

    修改 delay 的时间
    Vanessa
  • gztrljh 1 评论

    select(value) 事件触发后,取消选中有对应的事件吗?

    很多方法都会导致 unselect,比如点击,文字输入,剪切等
    Vanessa
  • OrdinaryRoad 1 评论

    自动聚焦怎么取消呢

    其他地方聚焦,编辑器就会取消聚焦了
    Vanessa
  • gztrljh

    回显的时候,代码块有明显闪动现象,该如何解决呢?

  • gztrljh 1 评论

    after()调用完毕但是页面 好像还没完全渲染完成,导致闪烁。是否新增一个页面全部准备完毕的事件?

    用了很多第三方的渲染器,有些没有回调方法,这个只能自己计算一下
    Vanessa
  • gztrljh 1 评论

    异步渲染内容时,每次刷新滚动条都会回到顶部。不知道有最佳解决方案么?目前能解决的方案有:jquery 发送 ajax 设置请求为同步请求内容进行回显。但是 这样页面前面的小图标有一个明显的转圈阻塞效果。 或者假如 after 方法能完全判断第三方渲染器渲染完毕后再触发的话,加一个遮罩层动画然后页面销毁前记录当前滚动条位置,页面一渲染再设置滚动条到原来位置,再把加载动画给渐隐掉。

    有些第三方渲染器没有回调
    Vanessa
  • gztrljh

    版本 3.8.7

    问题一:获取内容时图片丢失

    这篇文章复制部分文字和图片,图片本地化的时候在 获取此时 vditor.getValue() 发现没有图片的 markdown 内容。导致回显内容的时候只有文字 ,这是突然发现就这篇文章有这个问题。

    问题二:复制的时候明明选中部分文字和两张图片可是只会显示一张图片

    image.png

    但是它依然会发送两次请求

    image.png

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

    无法重现,vditor.getValue() 值如下:

    0 年,会宁县成功创建为甘肃省级全域旅游示范区。
    
    [![新时代·新长征 | 甘肃会宁:红色文化融入“新发展” ](https://nimg.ws.126.net/?url=http%3A%2F%2Fcms-bucket.ws.126.net%2F2021%2F1020%2F4b89d776j00r192j1002vc000rs00fnc.jpg&thumbnail=660x2147483647&quality=80&type=jpg)]()
    
    雄关漫道真如铁,而今迈步从头越。用好红色资源,赓续红色血脉,一个以“红色”为底的“多彩会宁”正砥砺前行,续写新的辉煌。
    
    [![新时代·新长征 | 甘肃会宁:红色文化融入“新发展” ](https://nimg.ws.126.net/?url=http%3A%2F%2Fcms-bucket.ws.126.net%2F2021%2F1020%2Feca4ac45j00r192j10024c000rs00fnc.jpg&thumbnail=660x2147483647&quality=80&type=jpg)]()
    
    
  • zxc1204 1 评论

    vue 这个掉接口有例子么

    和普通的 fetch 接口一样
    Vanessa
  • zxc1204 1 评论

    自动补全怎么做到接口成功后在遍历呢

    @Vanessa

    参看 demo
    Vanessa
  • zxc1204 1 评论

    自动补全怎么做到接口成功后在遍历呢,完整例子有代码库地址么

    @Vanessa

    有的
    Vanessa
  • zxc1204 1 评论

    选中 @ 成员,有选中事件么

    选中后会回填 value,目前没有选中后的回调
    Vanessa
  • zxc1204 1 评论

    @Vanessa @ 中人员 空格怎么加呢,我试了 ‘ ’这样,选中后会被取消

    空格在 md 中会被删除
    Vanessa
  • zxc1204 1 评论

    @ 人员列表最多只有 8 个人么

    是的
    Vanessa
  • zxc1204 1 评论

    我看完整例子 @ 选中人有空格,demo 没有空格 可以在 demo 加上

    \xa0
    
    没太明白
    Vanessa
  • zxc1204 1 评论

    我看完整例子选中人员后是有空格的,md 空格禁用,你们不是用的 \xa0 字符串这个空格么

    @Vanessa

    结尾和开头的会被剔除
    Vanessa
  • zxc1204 1 评论

    开头和结尾的空格会被删除,那完整例子的空格是如何加上的呢,我现在用的\xa0 十六进制的空格,提交的时候在 把 \xa0 替换成 普通空格,这样做比较麻烦,想问下,完整例子中的空格是如何加的呢

    @Vanessa

    在 value 后面加个空格试试
    Vanessa
  • zxc1204 1 评论

    选中人背景颜色如何加呢

    编辑器内没有办法添加,这个是 md 的文本
    Vanessa
请输入回帖内容 ...