Markweave:开源 Markdown-first WYSIWYG 编辑器
作者:refinex
项目:GitHub · Refinex-Space/markweave ·
0.9.2· MITnpm:
@markweave/react·@markweave/vue3·@markweave/vue2·markweaveTip:如果 Markweave 对比有帮助,欢迎点一个免费的 Fork + Star 😊
知识库、PRD、工单备注这类页面,编辑器几乎总会卡在同一处:作者想用 Markdown 写,产品又必须给一套可视化界面,存下来的还得是 Markdown。Typora 把体验做完整了,可它是桌面软件,嵌不进 React / Vue。自己用 Tiptap 拼,三个月后通常会卡在表格、媒体、Vue 2 和 AI 审阅上。
Markweave 是我们做的开源嵌入式编辑器,基于 Tiptap / ProseMirror。目标很具体:嵌进自家产品,Live 里直接写,View 里直接读,往返仍是 Markdown;上传、外链预览和模型调用留给宿主,编辑器不发请求、不持有密钥。React、Vue 3、Vue 2(含 Webpack 4)共用一份行为。

存进去的是 Markdown
defaultContent 和 content 默认按 Markdown 解析。产品侧该持久化的是 onUpdate.markdown。html、json、text 可以按需取,但不要每个按键都序列化一遍。
颜色、合并单元格、段落缩进这些标准 Markdown 保不住的状态,会落到受控 HTML fallback。它仍走完整 Schema,不是另开一套文档格式。宿主通过 editorExtensions 加进去的节点,只要实现了 renderHTML / parseHTML,往返时不会被丢掉。
Live 和 View 共用同一份文档。mode 只改交互:View 里编辑句柄会消失,代码复制、Mermaid 预览、安全链接打开还在。序列化结果不变。

主题可以运行时切亮 / 暗,也可以只改画布颜色,文档不会重建。

它是四个 npm 包,不是笔记 SaaS。仓库里没有后端、没有协同、没有账号。对象存储、鉴权和模型供应商都在你这边。
四个包,一份行为
| 包 | 做什么 |
|---|---|
markweave |
Schema、命令、插件、序列化、搜索和 AI controller |
@markweave/react |
React 组件、hook、NodeView |
@markweave/vue3 |
Vue 3 组件、composable |
@markweave/vue2 |
Vue 2 组件,另有 legacy / webpack4 入口 |
playground 只用于本地核对,不进 npm。
#mermaid-svg-7zJrW6vtDrHK0G4Q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7zJrW6vtDrHK0G4Q .error-icon{fill:#552222;}#mermaid-svg-7zJrW6vtDrHK0G4Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7zJrW6vtDrHK0G4Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .marker.cross{stroke:#333333;}#mermaid-svg-7zJrW6vtDrHK0G4Q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7zJrW6vtDrHK0G4Q p{margin:0;}#mermaid-svg-7zJrW6vtDrHK0G4Q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster-label text{fill:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster-label span{color:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster-label span p{background-color:transparent;}#mermaid-svg-7zJrW6vtDrHK0G4Q .label text,#mermaid-svg-7zJrW6vtDrHK0G4Q span{fill:#333;color:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .node rect,#mermaid-svg-7zJrW6vtDrHK0G4Q .node circle,#mermaid-svg-7zJrW6vtDrHK0G4Q .node ellipse,#mermaid-svg-7zJrW6vtDrHK0G4Q .node polygon,#mermaid-svg-7zJrW6vtDrHK0G4Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .rough-node .label text,#mermaid-svg-7zJrW6vtDrHK0G4Q .node .label text,#mermaid-svg-7zJrW6vtDrHK0G4Q .image-shape .label,#mermaid-svg-7zJrW6vtDrHK0G4Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-7zJrW6vtDrHK0G4Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .rough-node .label,#mermaid-svg-7zJrW6vtDrHK0G4Q .node .label,#mermaid-svg-7zJrW6vtDrHK0G4Q .image-shape .label,#mermaid-svg-7zJrW6vtDrHK0G4Q .icon-shape .label{text-align:center;}#mermaid-svg-7zJrW6vtDrHK0G4Q .node.clickable{cursor:pointer;}#mermaid-svg-7zJrW6vtDrHK0G4Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .arrowheadPath{fill:#333333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zJrW6vtDrHK0G4Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7zJrW6vtDrHK0G4Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zJrW6vtDrHK0G4Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster text{fill:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q .cluster span{color:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7zJrW6vtDrHK0G4Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7zJrW6vtDrHK0G4Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-7zJrW6vtDrHK0G4Q .icon-shape,#mermaid-svg-7zJrW6vtDrHK0G4Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7zJrW6vtDrHK0G4Q .icon-shape p,#mermaid-svg-7zJrW6vtDrHK0G4Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7zJrW6vtDrHK0G4Q .icon-shape .label rect,#mermaid-svg-7zJrW6vtDrHK0G4Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7zJrW6vtDrHK0G4Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7zJrW6vtDrHK0G4Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7zJrW6vtDrHK0G4Q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 适配器
宿主应用
markweave
Markdown 解析 / 序列化
Command Registry
Slash / 表格 / TOC
Ask AI / AI Edit
React
Vue 3
Vue 2 + Webpack 4
@markweave/react
@markweave/vue3
@markweave/vue2
Slash、表格句柄、链接展开、Ask AI 冲突检测写在核心里。适配器只做 DOM、图标和生命周期。哪个框架先修了一个交互,另外两端必须跟上。
Webpack 4 还有一条硬约束:Vue、@tiptap/vue-2、@tiptap/core、@tiptap/pm 和 ProseMirror 只能有一份运行时。legacy 入口会预打包 Mermaid、KaTeX、Lowlight,但不会再打一份 Tiptap。否则宿主自己的 extension 和编辑器各拿一套 constructor,插件状态会对不上。
Vue 2 最低基线是 2.6.12 / Vue CLI 4.4.6,还在维护的项目建议 2.7.16 / CLI 4.5.19。Vue 2 已经 EOL,安全补丁仍是宿主的事。
和常见方案差在哪里
按 "要嵌进自己的文档产品" 来看:
| Markweave | 自己拼 Tiptap | Milkdown | vditor | ByteMD | Typora | |
|---|---|---|---|---|---|---|
| 是什么 | 可嵌入的完整编辑器 | 工具包 | 插件化编辑器 | 国产 Markdown 组件 | 轻量预览编辑器 | 桌面软件 |
| 主存储 | Markdown,必要时 HTML fallback | 多为 JSON / HTML | Markdown | Markdown / HTML | Markdown | 本地 .md |
| 交互 | Live + View | 自己拼 | 所见即所得或分栏 | IR / SV / WYSIWYG | 分栏为主 | 源码即预览 |
| React / Vue 3 | 都是完整适配 | React 强,Vue 要自己做产品层 | 有 | Vue 常见,React 弱 | 有 | 嵌不进去 |
| Vue 2 + Webpack 4 | 有验证过的入口 | 基本靠自己 | 弱 | 老项目里常见 | 弱 | 无 |
| 表格 / 媒体 | 句柄、合并、图视频附件 | 要自己做一层 | 看插件 | 有 | 弱 | 桌面文件 |
| AI | 宿主驱动,没接线就没有入口 | 无 | 无统一协议 | 无统一协议 | 无 | 无 |
页面里只要一个能预览的评论框,ByteMD 更合适。节点形状要完全自己定义,Tiptap 或 Milkdown 更合适。作者要的是桌面 Typora,继续用 Typora。国内还有 Vue 2 控制台,又想和新的 React / Vue 3 前台共用行为,这是 Markweave 真正想填的位置。
#mermaid-svg-A8loFJzzwdE1pEoI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A8loFJzzwdE1pEoI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A8loFJzzwdE1pEoI .error-icon{fill:#552222;}#mermaid-svg-A8loFJzzwdE1pEoI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A8loFJzzwdE1pEoI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A8loFJzzwdE1pEoI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A8loFJzzwdE1pEoI .marker.cross{stroke:#333333;}#mermaid-svg-A8loFJzzwdE1pEoI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A8loFJzzwdE1pEoI p{margin:0;}#mermaid-svg-A8loFJzzwdE1pEoI .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-A8loFJzzwdE1pEoI .cluster-label text{fill:#333;}#mermaid-svg-A8loFJzzwdE1pEoI .cluster-label span{color:#333;}#mermaid-svg-A8loFJzzwdE1pEoI .cluster-label span p{background-color:transparent;}#mermaid-svg-A8loFJzzwdE1pEoI .label text,#mermaid-svg-A8loFJzzwdE1pEoI span{fill:#333;color:#333;}#mermaid-svg-A8loFJzzwdE1pEoI .node rect,#mermaid-svg-A8loFJzzwdE1pEoI .node circle,#mermaid-svg-A8loFJzzwdE1pEoI .node ellipse,#mermaid-svg-A8loFJzzwdE1pEoI .node polygon,#mermaid-svg-A8loFJzzwdE1pEoI .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-A8loFJzzwdE1pEoI .rough-node .label text,#mermaid-svg-A8loFJzzwdE1pEoI .node .label text,#mermaid-svg-A8loFJzzwdE1pEoI .image-shape .label,#mermaid-svg-A8loFJzzwdE1pEoI .icon-shape .label{text-anchor:middle;}#mermaid-svg-A8loFJzzwdE1pEoI .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-A8loFJzzwdE1pEoI .rough-node .label,#mermaid-svg-A8loFJzzwdE1pEoI .node .label,#mermaid-svg-A8loFJzzwdE1pEoI .image-shape .label,#mermaid-svg-A8loFJzzwdE1pEoI .icon-shape .label{text-align:center;}#mermaid-svg-A8loFJzzwdE1pEoI .node.clickable{cursor:pointer;}#mermaid-svg-A8loFJzzwdE1pEoI .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-A8loFJzzwdE1pEoI .arrowheadPath{fill:#333333;}#mermaid-svg-A8loFJzzwdE1pEoI .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-A8loFJzzwdE1pEoI .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-A8loFJzzwdE1pEoI .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A8loFJzzwdE1pEoI .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-A8loFJzzwdE1pEoI .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A8loFJzzwdE1pEoI .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-A8loFJzzwdE1pEoI .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-A8loFJzzwdE1pEoI .cluster text{fill:#333;}#mermaid-svg-A8loFJzzwdE1pEoI .cluster span{color:#333;}#mermaid-svg-A8loFJzzwdE1pEoI div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-A8loFJzzwdE1pEoI .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-A8loFJzzwdE1pEoI rect.text{fill:none;stroke-width:0;}#mermaid-svg-A8loFJzzwdE1pEoI .icon-shape,#mermaid-svg-A8loFJzzwdE1pEoI .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A8loFJzzwdE1pEoI .icon-shape p,#mermaid-svg-A8loFJzzwdE1pEoI .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-A8loFJzzwdE1pEoI .icon-shape .label rect,#mermaid-svg-A8loFJzzwdE1pEoI .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A8loFJzzwdE1pEoI .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-A8loFJzzwdE1pEoI .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-A8loFJzzwdE1pEoI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 只回传结果
输入 Markdown / Slash / 快捷键
Markweave
Live
View
onUpdate.markdown
宿主:保存、检索、权限
上传 / 外链 / 模型
日常怎么写
#、>、-、1. 和围栏代码块都可以继续用。选中文字后出现浮动工具栏,加粗、高亮、颜色、对齐、链接不会把文档赶出 Markdown。

点进行内链接,或者把光标移进去,链接会展开成 [文字](地址 "标题")。Enter 或失焦提交,Escape 放弃,Ctrl/Cmd-click 才打开。这是编辑态里的规范化投影,不保证和当初粘贴的源码逐字节相同,也可以关掉。

空段落会提示输入 /。查询就写在文档里,菜单锚在这段 /query 上,不会再浮一层搜索框。内置命令大约 22 条,按样式、标注、插入、上传分组,中英文关键词都能搜。/折叠、/table、/image 这类都能直接打。

右侧默认有一份按标题生成的目录,窄容器会自动收起来。也可以关掉内置 UI,只拿 onTocChange 自己画。查找替换提供 controller,搜索栏长什么样由产品决定。
Callout 和折叠块
文档里很快会需要标注和可收起的说明。五种 Callout 对应 :::info / tip / warning / error / success。折叠块是独立节点,有可编辑标题和可收起正文,正文里还能再放标注和代码,Slash 用 /details 或 /折叠。

任务列表、嵌套列表、引用、分割线、emoji、上标下标都在。这些都能往返 Markdown,不是只能看不能存的装饰。
表格
GFM 表格没有合并、列宽、单元格颜色和行句柄。用户一旦把表格当功能用,最低集就不够。
Markweave 的表有行列和选区句柄:移动、插入、排序、颜色、对齐、清空、复制、删除、合并、拆分。最后一行或最后一列边上可以直接加。句柄能拖。复制行、列或整表时,宿主能拿到 text 和 HTML。

合并单元格靠 HTML fallback 保住结构。tableCapabilities 可以按表关掉结构改动、格式、复制或表格 AI,resolver 出错就关,不会假装成功。

表格 Ask AI 只动单元格内容。单格返回 Markdown 片段;行、列、多格和整表必须返回等形的 GFM 表。合并格上的多格选区直接拒绝,避免模型重排结构后再猜一遍。
代码、Mermaid、公式
代码块有一份可搜索语言目录。你写 ts 就存 ts,高亮用哪套语法是另一件事,不会改围栏名。

Mermaid 还是代码块,只是带 Preview。已经写好的图默认预览;Slash 插进去时先停在源码,方便改。View 里即使保存时停在 Code,阅读也会先出图。下载走系统保存框。Ask AI 对 Mermaid 只给源码,提案阶段不执行生成的图。

公式是 KaTeX。行内 $E = mc^2$,块级单独一行。Live 里可以改 LaTeX 并看预览。

图片、视频、附件
本地文件必须走 onSlashCommandUpload,编辑器不直传对象存储。远端图片只认 HTTP(S) 的 <img>,或带常见图片扩展名的独立 URL,不会为了猜类型去发探测请求。
图片在 Live 里可以对齐、写 Caption、缩放、替换、下载、删除。View 里悬停能进大图。大文档里的图先占位,再按远近解码,最后还会在空闲时补一遍,避免滚出屏幕的图一直转圈。

视频支持本地文件、直链、YouTube 和 Bilibili,不自动播放。任意 iframe 域名会被拒。

附件只存 src、name、mimeType、size。下载走 onAttachmentDownload;没有回调时,只有安全的 http(s) 才会新开标签。不要把短期签名写进文档属性。

整段恰好一个 HTTP(S) 链接时,可以嵌成卡片。行内链接不会变卡片。编辑器自己不去抓网页,元数据来自宿主的 linkCardResolver。

resolveMediaSource 只把入库 URL 映射成展示地址,结果不写回节点,也不写回 Markdown。大约 200 KB 以上的文档按标题边界分片解析,存的仍是一份完整文档,不是多个编辑器拼起来。
AI 怎么接
常见失败就三种:密钥进了前端包,模型在用户没同意时读了全文,流式输出直接改文档,Undo 跟着崩。
Markweave 不选模型,也不收 Token。内置 Ask AI 默认没有入口,要显式 enabled 并给 handler。handler 只看到当前目标:普通文本是一段选区,表格带范围和目标 Markdown。返回值在用户点接受之前只是提案;接受后一次写入、一次撤销。目标被改过就中止。
已经有侧栏 Agent 的产品走 MarkweaveAiEditController。宿主自己 capture 选区、所在块或全文,调完模型再 updateProposal。精确选区可以边流边看;块级和全文等完整结果到了再拆 hunk,最多 200 个。逐块接受只是暂存,全部处理完才落盘。
模型 Markweave 宿主 作者 模型 Markweave 宿主 作者 #mermaid-svg-vxLiilRk9h3OWLhI{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vxLiilRk9h3OWLhI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vxLiilRk9h3OWLhI .error-icon{fill:#552222;}#mermaid-svg-vxLiilRk9h3OWLhI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vxLiilRk9h3OWLhI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vxLiilRk9h3OWLhI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vxLiilRk9h3OWLhI .marker.cross{stroke:#333333;}#mermaid-svg-vxLiilRk9h3OWLhI svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vxLiilRk9h3OWLhI p{margin:0;}#mermaid-svg-vxLiilRk9h3OWLhI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vxLiilRk9h3OWLhI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vxLiilRk9h3OWLhI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vxLiilRk9h3OWLhI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vxLiilRk9h3OWLhI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vxLiilRk9h3OWLhI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vxLiilRk9h3OWLhI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vxLiilRk9h3OWLhI .sequenceNumber{fill:white;}#mermaid-svg-vxLiilRk9h3OWLhI #sequencenumber{fill:#333;}#mermaid-svg-vxLiilRk9h3OWLhI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vxLiilRk9h3OWLhI .messageText{fill:#333;stroke:none;}#mermaid-svg-vxLiilRk9h3OWLhI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vxLiilRk9h3OWLhI .labelText,#mermaid-svg-vxLiilRk9h3OWLhI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vxLiilRk9h3OWLhI .loopText,#mermaid-svg-vxLiilRk9h3OWLhI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vxLiilRk9h3OWLhI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vxLiilRk9h3OWLhI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vxLiilRk9h3OWLhI .noteText,#mermaid-svg-vxLiilRk9h3OWLhI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vxLiilRk9h3OWLhI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vxLiilRk9h3OWLhI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vxLiilRk9h3OWLhI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vxLiilRk9h3OWLhI .actorPopupMenu{position:absolute;}#mermaid-svg-vxLiilRk9h3OWLhI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vxLiilRk9h3OWLhI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vxLiilRk9h3OWLhI .actor-man circle,#mermaid-svg-vxLiilRk9h3OWLhI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vxLiilRk9h3OWLhI :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt 目标已变 审阅结束 Ask AI / 宿主按钮 捕获目标 Markdown + AbortSignal 宿主自己的鉴权 累计 Markdown 更新提案 原位 Diff 接受或放弃 冲突,文档不动 一次事务



业务命令
从 0.6.0 起,业务插入不要在 Vue 组件里直接 insertContent。注册 commandGroups 和 commands,拿 MarkweaveCommandController,Slash 和外部工具栏走同一条路径。
命令 ID 用小写点分名,比如 trm.decision.insert-field,不能盖住内置 ID。文本图标最多四个字,按纯文本渲染。异步结果有 1 MiB 上限。editorExtensions 只在创建时追加,改 Schema 要换 key 重挂。handler 里看不到 Cookie 和当前用户,权限检查写在宿主自己的函数里。
接到产品里
React:
tsx
import { MarkweaveEditor } from "@markweave/react";
import "@markweave/react/styles.css";
export function Editor() {
return (
<MarkweaveEditor
defaultContent={"# Hello Markweave\n\n用 **Markdown** 写,存 Markdown。"}
mode="live"
lang="zh"
onUpdate={({ markdown }) => persistDraft(markdown)}
/>
);
}
function persistDraft(markdown: string) {
console.log(markdown);
}
Vue 3:
vue
<script setup lang="ts">
import { MarkweaveEditor } from "@markweave/vue3";
import "@markweave/vue3/styles.css";
function handleUpdate({ markdown }: { markdown: string }) {
persistDraft(markdown);
}
</script>
<template>
<MarkweaveEditor
default-content="# Hello Markweave\n\n用 **Markdown** 写,存 Markdown。"
mode="live"
lang="zh"
:on-update="handleUpdate"
/>
</template>
Vue 2 的模板同样是 default-content 和 :on-update。Vue CLI 4 生产构建要挂 legacy helper:
js
const applyMarkweaveVue2Webpack4Legacy = require("@markweave/vue2/webpack4");
module.exports = {
chainWebpack(config) {
applyMarkweaveVue2Webpack4Legacy(config, {
projectRoot: __dirname,
});
},
};
上传、Ask AI、链接卡片都是有回调才开:
tsx
<MarkweaveEditor
defaultContent={initialMarkdown}
onSlashCommandUpload={async (request) => {
if (request.source.type !== "file" || !request.source.file) {
return { src: request.source.value ?? "", name: request.source.value };
}
const form = new FormData();
form.append("file", request.source.file);
form.append("kind", request.kind);
const response = await fetch("/api/uploads", { method: "POST", body: form });
if (!response.ok) throw new Error("Upload failed.");
return response.json();
}}
askAi={{
enabled: true,
handler: async ({ signal, ...request }) => {
const response = await fetch("/api/markweave/ask-ai", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(request),
signal,
});
if (!response.ok) throw new Error("Ask AI failed");
return response.text();
},
}}
linkCardResolver={async ({ href, signal }) => {
const response = await fetch(`/api/link-preview?url=${encodeURIComponent(href)}`, { signal });
return response.ok ? response.json() : null;
}}
/>
细节在手册里,按框架看一篇就够:
大文档用非受控 defaultContent,保存时再读 markdown。样式只 import 一次。SSR 里放到客户端再挂。View 会拒 javascript: / data: / vbscript:。密钥不要进 VITE_*。
现在还缺什么
接到自己的项目里,把别扭的交互开 Issue,比空 Star 有用。当然 Star 和 Fork 也需要 😊。