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

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...
  • zxc1204

    编辑器默认获取焦点可以关么,应该怎么关呢

    @Vanessa

  • zxc1204 1 评论

    可以强制失去焦点么,怎么做呢

    blur
    Vanessa
  • zxc1204 1 评论

    "preview": {
    "mode": "editor"
    }

    我用这个还是显示预览页面是什么问题呢

    参见 demo
    Vanessa
  • zxc1204 1 评论

    ir 模式下的代码块为什么要有两栏呢

    1 回复
    可以隐藏
    Vanessa
  • cccp2009 1 评论

    渲染大纲的时候,大纲没有链接到文档内容是为啥呢?

    demo 上面也不行么?
    Vanessa 1
  • zxc1204

    ir 模式下的代码块怎么隐藏两栏呢

  • zxc1204

    "mode" : "sv"

    preview" : {
    "mode" : "editor"
    }

    这个我就是按照 demo 上改的,但没有效果

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

    GitHub 中的 index.js 如下配置是可以不显示双栏的。

    new Vditor('vditor', {
        // _lutePath: `http://192.168.0.107:9090/lute.min.js?${new Date().getTime()}`,
        _lutePath: 'src/js/lute/lute.min.js',
        cdn: 'http://localhost:9000',
        toolbar,
        lang: language,
        mode: 'sv',
        height: window.innerHeight + 100,
        outline: {
          enable: true,
          position: 'right',
        },
        debugger: true,
        typewriterMode: true,
        placeholder: 'Hello, Vditor!',
        preview: {
          mode: 'editor',
          markdown: {
            toc: true,
            mark: true,
            footnotes: true,
            autoSpace: true,
          },
          math: {
            engine: 'KaTeX',
          },
        },
        toolbarConfig: {
          pin: true,
        },
        counter: {
          enable: true,
          type: 'text',
        },
        hint: {
          emojiPath: 'https://cdn.jsdelivr.net/npm/vditor@1.8.3/dist/images/emoji',
          emojiTail: '<a href="https://ld246.com/settings/function" target="_blank">设置常用表情</a>',
          emoji: {
            'sd': '💔',
            'j': 'https://unpkg.com/vditor@1.3.1/dist/images/emoji/j.png',
          },
          parse: false,
          extend: [
            {
              key: '@',
              hint: (key) => {
                console.log(key)
                if ('vanessa'.indexOf(key.toLocaleLowerCase()) > -1) {
                  return [
                    {
                      value: '@Vanessa',
                      html: '<img src="https://avatars0.githubusercontent.com/u/970828?s=60&v=4"/> Vanessa',
                    }]
                }
                return []
              },
            },
            {
              key: '#',
              hint: (key) => {
                console.log(key)
                if ('vditor'.indexOf(key.toLocaleLowerCase()) > -1) {
                  return [
                    {
                      value: '#Vditor',
                      html: '<span style="color: #999;">#Vditor</span> ♏ 一款浏览器端的 Markdown 编辑器,支持所见即所得(富文本)、即时渲染(类似 Typora)和分屏预览模式。',
                    }]
                }
                return []
              },
            }],
        },
        tab: '\t',
        upload: {
          accept: 'image/*,.mp3, .wav, .rar',
          token: 'test',
          url: '/api/upload/editor',
          linkToImgUrl: '/api/upload/fetch',
          filename (name) {
            return name.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, '').
              replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, '').
              replace('/\\s/g', '')
          },
        },
      })
    
  • cccp2009 1 评论

    使用 demo 调用渲染大纲,貌似不太行。是有啥特别的配置还是依赖吗?

    demo 有什么问题么?
    Vanessa
  • zxc1204 1 评论

    ir 模式下代码块显示两栏,应该如何显示成一栏呢,代码语言可以取消掉么,应该如何取消呢

    @Vanessa

    参考下面我发的代码
    Vanessa
  • zxc1204 1 评论

    额,我用你发的方法,sv 还是显示预览页面,ir 代码块还是显示两栏。。。。。。。

    看一下有没有报错,不行的话就到 github 上面把源码下下来试试。我昨晚试过是好的。
    Vanessa
  • zxc1204 1 评论

    我用的是 3.8.6 这个版本,有什么方法可以改变光标的位置,放在最后一位么

    看一下相关的 API
    Vanessa
  • zxc1204 1 评论

    这个大纲功能,点击为什么没有跳到相应的内容呢

    应该是可以的,你看看 demo 上是否可以?
    Vanessa
  • tb1 1 评论
    该回帖仅作者和楼主可见
    参看文档,可配置成本地的
    Vanessa
  • zxc1204 1 评论
    col1 col2 col3

    表格增加不了行和列啊,是怎么回事呢

    @Vanessa

    有没有报错?
    Vanessa
  • zxc1204 1 评论
    col1 col2 col3

    没有报错啊,就这个回帖的编辑器也不能增加行和列

    @Vanessa

    wysiwyg 有工具栏,其余用快捷键
    Vanessa
  • zxc1204 1 评论

    我想问下是出于什么原因的设计 ir 模式下代码块要展示两栏呢,能解答下么

    @Vanessa

    可以进行配置
    Vanessa
  • sutter 1 评论

    将光标设置到内容最后怎么写呢

    要用浏览器带的 getSelection()
    Vanessa
  • East333 1 评论

    vditor: "3.8.8" 从编辑模式跳到预览模式,再跳回来,编辑器的内容的宽度从 100% 变成了预览模式的宽度了

    哪一种编辑模式?wysiwyg 看了下是正常的。
    Vanessa
  • gztrljh 1 1 评论

    bug 反馈

    版本 3.8.8

    浏览器 谷歌

    模式 wysiwyg

    在编辑的情况下,文件上传时如果同时上传 3 个文件,前面两个都是音频文件比如 mp3 和 wav 后面跟随一张图片。那么上传成功时 前面两个音频文件生成的代码块会和图片重叠在一起

    效果如下

    image.png

    查看了一下 dom 结构发现这两个音频文件是父子结构

    image.png

    不过如果我们手动在音频文件位置手动回车一下就恢复正常了,同时也把下面那个播放器也给渲染出来了

    image.png

    期望结果 上传成功让音频文件和图片有点间隔而且自动渲染出播放器。不需要手动回车才能渲染

    感谢反馈,下个版本改进
    Vanessa
  • 547176052 1 评论

    image.png

    填的是本机的 js 使用的还是外网的 js

    1 回复
    参看文档 cdn
    Vanessa
  • zxc1204 1 评论

    @Vanessa 输入链接端口号会被隐藏这是特意设置的么,能否让显示出来

    http://192.168.81.39

    Vanessa
  • zxc1204

    我的版本是 3.8.6

  • dancingcode 1 评论

    问题是怎么隐藏

    用样式控制一下试试
    Vanessa
  • lizp 1 评论

    image.png

    这里为什么需要请求 CDN 的地址,我本地下载下来了,也会请求吗。每次这个请求的速度会很慢

    需要配置
    Vanessa
  • Ethan 3 评论

    请问如何限制字符输入长度呢?超出内容最大长度将不允许输入

    options.counter.max
    Vanessa
    @Vanessa 这个属性只能展示右上角的最大字数,但是用户输入的内容长度超过了这个最大字数一样可以展示,有没有什么办法能在超出最大内容长度后,撤销本次输入
    Ethan
    @Ethan 在 after 里面看看
    Vanessa
  • Ethan 1 评论

    @Vanessa 输入内容撤销时,侧边的大纲并不会同步更新呢,不知道是不是 BUG?

    Vanessa
  • lizp

    1、使用 disabled(), 在禁用模式下,无法禁用自定义按钮

    2、image.png

    这里的按钮还可以使用

    3、在 setContentTheme 时用的时 path 而不是 cdn,这样导致,把 cdn 切换了,内容区域的 css 还是 使用的 npm 的。

    希望关注 版本 3.8.11

    1 回复
  • Vanessa
    支持者 赞助者 订阅者 作者
    1. 自定义按钮存在不编辑的状态,目前无法判断是否需要禁用
    2. 这些按钮不涉及编辑,可以正常使用
    3. path 为了方便自定义主题,这样就不需要把主题上传到 cdn 上。
  • yuhao 1 评论

    编辑器在遮罩层内会导致工具栏左对齐

    ?
    Vanessa
  • lizp 2 评论

    我其实是想知道,在本地内网的情况下,没有 cdn。如何使用本地的样式 css 还有那些 js

    可以教我一下具体的配置吗?感谢
    lizp
    使用本地相对路径
    Vanessa
  • yuhang9580 1 评论

    请问一下,如果粘贴剪贴板中的图片,需要怎么样可以监听到粘贴事件

    我们还没有对应的接口
    Vanessa
  • lizp 1 评论
     VditorPreview.md2html
    

    不会渲染脑图、数学公式吗?

    渲染需要使用静态方法 xxxRender
    Vanessa
  • yuhang9580 1 评论

    可否将 focus 修改一下,入参为光标坐标,用于聚焦到指定位置

    每种编辑模式的坐标都不一致,这个可能处理不了。
    Vanessa
  • yyjTest 1 评论

    为什么用 getHTML()得到的内容,渲染在 html 上没有样式的

    需要调用 render 相关方法
    Vanessa
  • yyjTest 2 评论

    bug.png

    请问一下这个有问题有没有遇到过,渲染成功了(输出是有数据的),但是 dom 节点没有成功挂载上

    已解决
    yyjTest
    @yyjTest 解决就好
    Vanessa
  • lzsk

    asdf

    该回帖因偏离主题而被折叠
    1 操作
    Vanessa 在 2022-05-09 23:14:56 折叠了该回帖
  • lzsk 1 评论

    code 代码块 样式没有对齐问题 请问怎么解决

    对齐后再整体粘贴
    Vanessa
  • fengx1a0

    123

    该回帖因偏离主题而被折叠
    1 操作
    Vanessa 在 2022-07-05 21:24:08 折叠了该回帖
  • fengx1a0
    该回帖仅作者和楼主可见
    1 操作
    Vanessa 在 2022-07-05 21:23:55 折叠了该回帖
  • hfksalj 1 评论

    按照流程试了一下 react demo,美中不足的是录音键按了没有反应,请问如何解决,谢谢

    demo 没有接后台服务,可以在 https://ld246.com/guide/markdown 中尝试
    Vanessa
  • NZH 1 评论

    请问内容主题应该用什么变量名设置

    截屏 2022080309.33.40.png

    ant-design, light,dark,wechat
    Vanessa
  • NZH 1 评论

    请问我想要每一次内容输入都触发一个我自己写的函数,应该加在哪里呢

    input
    Vanessa
  • shenglanzhou 1 评论

    输入 xss 代码,如 <iframe %00 src=" javascript:prompt(1) "%00> 编辑器能不能不执行代码,只当成文本展示,或者过滤掉禁止输入呢?

    参见 sanitize
    Vanessa
  • wf 1 评论

    我想要使用 vditor 当作自己的笔记软件,类似 typora,但是现在发现 vditor 打开一个很大的 markdown 时存在比较明显的卡顿,首次加载需要三四秒才能显示,然后在编辑器里面修改也有些卡顿,ctrl+z 回退的时候卡顿比较明显,我想问问 vditor 有没有设计文档懒加载的功能呀,差不多就是在编辑器中没有阅读到的地方,展示不渲染。

    虽然有提供 preview 渲染方法,但是 preview 渲染出来的结果不能编辑,

    没有设计,只有思源才设计了。
    Vanessa
  • raingad 1 评论

    请问可以动态改变工具栏图标的颜色吗?或者动态修改工具栏的功能

    颜色使用 css, 功能参看 API
    Vanessa
  • xjbuzzle

    好难用。。文档不全,不清晰,不明确,除了 bug 就是脱发 🙏

    1 回复
  • xjbuzzle 1 评论

    能不能更新一下文档啊,900 天前。。。到底哪个 api 能用呀

    API 没有变动
    Vanessa
  • tangxl 3 评论

    我想修改 echarts 的渲染逻辑,想要复写 Vditor.chartRender()方法可以实现么

    理论上是可以的
    Vanessa
    @Vanessa 能具体说下实现的思路么
    tangxl
    @tangxl 加载完后直接复写
    Vanessa
请输入回帖内容 ...