wang-markdown-render
v1.2.1
Published
高级全功能Markdown渲染器
Readme
wang-markdown-render
Marknote Markdown 渲染与所见即所得编辑核心 SDK。它提供一个 MarkdownCore 入口,通过功能命名空间暴露文档、配置、光标、格式、链接、表格、图片、音频、画板、代码块、查找替换、导出快照等能力。
功能概览
核心能力
- Markdown 渲染模式、源码模式与只读渲染。
- 单实例模式切换:同一个
MarkdownCore通过mode/Config.setMode()在渲染视图和源码视图之间切换。 - 领域命名空间 API:
Document、Config、Cursor、ViewState、Content、Format、Link、Table、Image、Audio、Whiteboard、Code等。 - 查找与替换:支持大小写、全词、方向、定位、选中、高亮和批量替换。
- 表格、图片、音频块、画板、代码块等富内容渲染和编辑辅助。
- 大纲、滚动定位、文档指标、渲染快照与资源列表,完整导出流程由调用端实现。
文档目录
- 快速接入
- API 设计约定
- 初始化配置
- 文档与内容 API
- 视图、光标与滚动 API
- 查找与替换 API
- 链接 API
- 表格 API
- 图片 API
- 音频块 API
- 画板 API
- 代码块 API
- 导出与快照 API
- 事件与回调 payload
- 依赖与手动构建
快速接入
入口与样式
import { MarkdownCore } from 'wang-markdown-render';
import 'wang-markdown-render/style.css';|入口|说明|
|---|---|
|wang-markdown-render|默认入口,导出 MarkdownCore 和类型。|
|wang-markdown-render/style.css|SDK 默认样式,需要由调用端显式引入。|
创建编辑器
const editor = new MarkdownCore(document.querySelector('#editor'), {
value: '# 标题\n\n正文',
mode: 'render',
readOnly: false,
});
editor.onMounted(() => {
console.log('mounted');
});
editor.Document.on('contentChange', (source) => {
console.log(source);
});
editor.Cursor.focus();
editor.Format.bold();宿主容器要求
调用者传入的 hostElement 必须同时是渲染区域和唯一纵向滚动容器。SDK 只监听 hostElement 的 scroll 事件,也只读写 hostElement.scrollTop;不会监听父容器、document、window 或页面级 body 滚动。
<div id="editor"></div>#editor {
width: 100%;
height: 70vh;
overflow: auto;
}不要把 SDK 放在由外层页面滚动承载的容器中,否则虚拟渲染、查找定位、大纲跳转、锚点跳转和滚动状态恢复都不会按预期工作。
只读渲染
const viewer = new MarkdownCore(document.querySelector('#viewer'), {
value: markdownSource,
readOnly: true,
});只读状态会阻止输入、粘贴、剪切、删除、格式化、替换、撤销重做等修改操作,渲染、滚动、查找、打开链接和读取快照仍可使用。
API 设计约定
命名空间总览
公开实例只暴露单数命名空间。方法名在命名空间已经表达领域的基础上,仍需清楚说明动作、对象或结果,例如 Audio.play()、Image.copySource()、Whiteboard.toJson()。旧根方法和旧命名空间不保留兼容别名。
|命名空间|职责|
|---|---|
|MarkdownCore.version|SDK 版本元信息,包含 package 版本号和打包时间。|
|MarkdownCore.onMounted()|入口级首轮渲染完成 hook。|
|Document|文档源码、快照、指标和文档事件。|
|Config|视图模式、只读状态、主题色、内容主题和文档目录。|
|Cursor|焦点、源码偏移、光标滚动和光标事件。|
|ViewState|可持久化视图状态和滚动锚点。|
|Lifecycle|下一次渲染完成回调、等待渲染和销毁。|
|Content|文本替换、复制、剪切、粘贴和删除选区。|
|Format|行内格式、块格式、列表、任务列表和分割线。|
|Search|查找、定位、高亮、单项替换和批量替换。|
|Outline|标题大纲、滚动到行和跳转高亮。|
|Link|链接插入和链接打开 hook。|
|Table|表格插入、工具栏、列对齐、行列调整和删除。|
|Image|图片插入、路径选择、上下文、菜单、资源复制和文件动作 hook。|
|Audio|音频块插入、选中、播放控制、源码操作和删除。|
|Whiteboard|画板插入、选中、编辑事件、源码/JSON 操作和删除。|
|Code|代码块插入、缩进、语言、换行、行号、折叠、复制和格式化。|
|History|撤销和重做。|
方法命名规则
- 状态读取使用
getXxx(),例如Config.getMode()、Cursor.getOffset()。 - 状态写入使用
setXxx(),例如Config.setTheme()、Cursor.setOffset()。 - 布尔判断使用
isXxx(),例如Config.isReadOnly()。 - 上下文动作写出对象或结果,例如
Image.copySource()、Whiteboard.copyJson()、Document.createSnapshot()。 - 事件只通过
Namespace.on('event', handler)/Namespace.off('event')订阅或取消。
返回值约定
|返回类型|含义|示例|
|---|---|---|
|boolean|true 表示本次调用被接受或完成;false 通常表示参数无效、缺少上下文、只读拦截、实例销毁或没有可执行目标。|editor.History.undo()|
|Promise<boolean>|异步动作,通常涉及剪贴板、媒体播放或外部 hook。|await editor.Image.copySource(context)|
|对象 / 数组 / 字符串|读取类 API 返回当前状态快照;动作结果对象通常包含 success 和可选 reason。|editor.ViewState.getAnchor()|
|null|当前没有选中项、事件目标不匹配或上下文已失效。|editor.Image.getSelectedContext()|
事件约定
运行时事件使用 Namespace.on(name, handler) 注册,返回 true 表示事件名受支持且处理器已注册。off(name) 返回 true 表示事件名受支持并已清理。传入不支持的事件名会返回 false。
通知类事件还会派发 namespace:event DOM 事件,事件名会从 camelCase 转成 kebab-case,例如 modeChange 对应 config:mode-change。需要返回结果的 hook 只通过对应命名空间的 on() 注册。
所有对象型事件和 hook payload 都包含 editor 字段,值是公开的 MarkdownCore 门面,不会暴露内部运行时实例。
块级插入规则
Code.insert()、Audio.insert()、Image.insert()、Table.insert()、Whiteboard.insert() 和 Format.insertRule() 都会遵循块级插入规则:插入内容独占块,并在块后保留一个实际可点击、可获得光标的空行;在引用块内插入时会继续使用当前引用前缀。
初始化配置
构造参数
const editor = new MarkdownCore(hostElement, {
value: '',
mode: 'render',
readOnly: false,
theme: 'rgb(59, 130, 246)',
cursorColor: '',
contentTheme: {},
documentBasePath: '',
imageBasePath: '',
audioBasePath: '',
resolveImageSource: (context) => null,
resolveAudioSource: (context) => null,
sourceModeTabText: ' ',
codeBlockLineNumbersVisible: true,
codeBlockCollapsedByDefault: false,
whiteboardCanvasOptions: {},
});|选项|类型|默认值|说明|
|---|---|---|---|
|value|string|''|初始 Markdown 源码。|
|mode|'render' \| 'source'|'render'|初始视图模式。|
|readOnly|boolean|false|是否只读。|
|theme|string|'rgb(59, 130, 246)'|SDK 实例主题强调色,只接受 rgb() 颜色值。|
|cursorColor|string|跟随 theme|SDK 实例光标色,只接受 rgb() 颜色值;空值恢复跟随主题色。|
|contentTheme|ContentThemeOptions|-|初始化内容主题,等价于创建后调用 Config.setContentTheme()。|
|documentBasePath|string|''|当前 Markdown 文件所在目录,用于解析相对图片、音频和本地文档链接。|
|imageBasePath|string|''|默认图片目录。|
|audioBasePath|string|''|默认音频目录。|
|resolveImageSource|(context) => string \| object \| null|-|自定义图片渲染解析器。|
|resolveAudioSource|(context) => string \| object \| null|-|自定义音频渲染解析器。|
|sourceModeTabText|string|' '|源码模式按 Tab 时插入的文本。|
|codeBlockLineNumbersVisible|boolean|true|代码块是否显示行号。|
|codeBlockCollapsedByDefault|boolean|false|代码块是否默认折叠。|
|whiteboardCanvasOptions|object|{}|传给 wang-canvas 的只读渲染配置。|
编辑区尾部留白
非只读编辑器会在文档末尾保留一段响应式空白,默认值为 clamp(4.5rem, 12vh, 7.5rem) 并叠加移动端安全区,方便用户在最后一行下方点击并继续输入。调用端可通过 CSS 变量覆盖:
#editor {
--wmr-editor-bottom-space: clamp(5rem, 14vh, 8rem);
}主题与内容颜色
theme 是主题强调色,影响选区、默认光标、激活边框等,不负责页面背景和正文颜色。页面内容色应使用 contentTheme、Config.setContentTheme() 或 Config.setPageContentTheme()。
editor.Config.setTheme('rgb(22, 163, 74)');
editor.Config.setCursorColor('rgb(239, 68, 68)');
editor.Config.setContentTheme({
accentColor: 'rgb(22, 163, 74)',
cursorColor: 'rgb(239, 68, 68)',
pageBackgroundColor: 'rgb(30, 30, 30)',
pageTextColor: 'rgb(229, 231, 235)',
});
editor.Config.setPageContentTheme({
pageTextColor: 'rgb(31, 35, 41)',
});常用内容主题字段:
|字段|说明|
|---|---|
|themeColor / accentColor|主题强调色。|
|cursorColor|光标色。|
|pageBackgroundColor / backgroundColor|页面背景色。|
|pageTextColor / textColor|页面正文色。|
|pageSurfaceColor|浮层、工具面板背景色。|
|pageTextSecondaryColor / pageTextTertiaryColor|次级和弱文字色。|
|pageBorderColor|通用边框色。|
|pageCodeBackgroundColor / pageCodeBlockBackgroundColor|行内代码和代码块背景色。|
|findTargetBackgroundColor|查找结果高亮背景色。|
|codeTokenKeywordColor 等 codeToken*Color|代码高亮 token 色。|
文件路径与资源解析
打开 Markdown 文件时,建议同步当前文件所在目录:
editor.Document.setContent(markdownSource, {
documentBasePath: 'D:/notes/current',
});如果当前文件是 D:/notes/current/note.md,则 documentBasePath 应传 D:/notes/current。另存为后可调用:
editor.Config.setDocumentBasePath('D:/notes/new-folder', {
render: true,
});图片和音频都支持自定义解析器:
const editor = new MarkdownCore(hostElement, {
documentBasePath: 'D:/notes/current',
imageBasePath: './images',
audioBasePath: './records',
resolveImageSource: ({ url, documentBasePath, imageBasePath }) => {
return resolveImage({ url, documentBasePath, imageBasePath });
},
resolveAudioSource: ({ url, documentBasePath, audioBasePath }) => {
return resolveAudio({ url, documentBasePath, audioBasePath });
},
});解析器可返回 src 字符串,也可返回 { src, filePath }。返回 null 或无效值时使用 SDK 默认解析。
虚拟滚动
当文档较大时,SDK 会自动启用虚拟滚动以减少 DOM 数量。虚拟滚动只渲染可视窗口附近的行,通过估算行高和动态测量维护滚动位置。
const editor = new MarkdownCore(hostElement, {
value: hugeMarkdownSource,
virtualRender: true,
virtualRenderLineThreshold: 1000,
virtualRenderSourceThreshold: 20000,
virtualOverscanBefore: 80,
virtualOverscanAfter: 160,
virtualEstimatedLineHeight: 30,
});|选项|类型|默认值|说明|
|---|---|---|---|
|virtualRender|boolean|-|true 强制开启,false 强制关闭,不传则自动判断。|
|virtualRenderLineThreshold|number|1000|自动开启虚拟滚动的行数阈值。|
|virtualRenderSourceThreshold|number|20000|自动开启虚拟滚动的总字符数阈值。|
|virtualOverscanBefore / virtualOverscanAfter|number|80 / 160|视口上下方预渲染行数。|
|virtualOverscanBeforePx / virtualOverscanAfterPx|number|-|视口上下方预渲染像素。|
|virtualOverscanPx|number|-|统一设置上下方预渲染像素。|
|virtualRerenderMargin|number|12|重渲染边距行数。|
|virtualRerenderMarginPx|number|-|重渲染边距像素。|
|virtualEstimatedLineHeight|number|-|估算行高,不传时从 CSS 自动计算。|
自动判断时,满足行数阈值或字符数阈值任一条件即开启。
分批渲染与滚动边距
|选项|类型|默认值|说明|
|---|---|---|---|
|renderBatchSize|number|240|大文档分批渲染批大小。|
|asyncRenderLineThreshold|number|1200|超过该行数后,在非聚焦状态下启用异步分批渲染。|
|firstPaintLineCount|number|420|大文档异步渲染时首批立即渲染的行数。|
|fastRenderBatchSize|number|960|首屏完成后每帧快速补齐的行数。|
|selectionScrollMargin|number|24|选区定位时在滚动容器内保留的最小边距。|
|cursorScrollMargin|number|60|光标滚动到可视区时保留的最小边距。|
文档与内容 API
实例与生命周期入口
|API|参数|返回值|说明|
|---|---|---|---|
|MarkdownCore.version|-|{ version: string, buildTime: string }|读取 SDK 版本元信息。|
|MarkdownCore.onMounted(callback)|callback: () => void|boolean|注册首轮 mounted hook;首轮已触发或参数无效时返回 false。|
editor.onMounted(() => {
console.log(editor.version);
});文档源码 API:Document
|API|参数|返回值|说明|
|---|---|---|---|
|Document.getContent()|-|string|获取当前 Markdown 源码。|
|Document.setContent(source, options)|source: string;options.documentBasePath?: string;options.preserveSelection?: boolean|boolean|设置源码;可同步当前文件目录。|
|Document.createSnapshot(options)|见导出与快照 API|{ htmlFragment, resources }|创建当前渲染 DOM 快照。|
|Document.getMetrics()|-|Promise<{ lineCount, charCount }>|异步统计文档行数和渲染正文字符数;字符数不包含 Markdown 语法、链接地址、图片源码、画板 JSON 等非正文文本。|
|Document.getFirstLine()|-|string|获取文档首行纯文本内容,会去掉 Markdown 行内标记。|
|Document.on(name, handler) / Document.off(name)|name: 'contentChange' \| 'contextMenu'|boolean|订阅或取消文档事件。|
editor.Document.setContent('## 新文档', {
documentBasePath: 'D:/notes/current',
});
const source = editor.Document.getContent();
const metrics = await editor.Document.getMetrics();
const firstLine = editor.Document.getFirstLine();文本编辑与剪贴板 API:Content
|API|参数|返回值|说明|
|---|---|---|---|
|Content.replaceSelection(value)|value: string|boolean|用文本替换当前选区。|
|Content.copy()|-|Promise<boolean>|复制当前 Markdown 选区。|
|Content.cut()|-|Promise<boolean>|复制并删除当前选区。|
|Content.removeSelection()|-|boolean|删除当前非空选区。|
|Content.paste()|-|Promise<boolean>|从系统剪贴板粘贴,优先处理图片资源,再回退到文本。|
|Content.pasteText()|-|Promise<boolean>|只粘贴系统剪贴板文本。|
editor.Content.replaceSelection('Hello');
await editor.Content.copy();
await editor.Content.pasteText();排版 API:Format
|API|参数|返回值|说明|
|---|---|---|---|
|Format.bold() / Format.italic() / Format.strike()|-|boolean|切换加粗、斜体或删除线。|
|Format.underline()|-|boolean|切换 <u>...</u> 下划线。|
|Format.inlineCode() / Format.highlight()|-|boolean|切换行内代码或高亮。|
|Format.subscript() / Format.superscript()|-|boolean|切换下标或上标。|
|Format.toggleComment()|-|boolean|切换 HTML 注释。|
|Format.toggleHeading(level)|level: number,默认 1|boolean|切换当前块标题层级。|
|Format.setParagraph()|-|boolean|转为普通段落。|
|Format.toggleQuote()|-|boolean|切换引用块。|
|Format.toggleOrderedList() / Format.toggleUnorderedList() / Format.toggleTaskList()|-|boolean|切换有序、无序或任务列表。|
|Format.toggleTask(lineIndex)|lineIndex: number|boolean|切换指定任务列表行的 [ ] / [x] 状态。|
|Format.insertRule()|-|boolean|插入分割线。|
editor.Format.toggleHeading(2);
editor.Format.bold();
editor.Format.toggleTaskList();
editor.Format.insertRule();历史记录 API:History
|API|参数|返回值|说明|
|---|---|---|---|
|History.undo()|-|boolean|撤销;只读或没有可撤销记录时返回 false。|
|History.redo()|-|boolean|重做;只读或没有可重做记录时返回 false。|
editor.Content.replaceSelection('可撤销文本');
editor.History.undo();
editor.History.redo();渲染生命周期 API:Lifecycle
|API|参数|返回值|说明|
|---|---|---|---|
|Lifecycle.onNextRender(callback)|callback: () => void|boolean|下一次文档渲染完成后执行一次回调。|
|Lifecycle.waitNextRender()|-|Promise<boolean>|等待下一次文档渲染完成;无法注册时解析为 false。|
|Lifecycle.destroy()|-|boolean|销毁实例、事件监听、浮层和 DOM;重复销毁返回 false。|
editor.Lifecycle.onNextRender(() => {
console.log('rendered');
});
await editor.Lifecycle.waitNextRender();
editor.Lifecycle.destroy();视图、光标与滚动 API
视图模式 API:Config
MarkdownCore 只有一个入口,实例内部通过 mode 切换渲染模式和源码模式。源码模式仍复用同一份源码状态:Document、History、Search、Cursor 等能力不需要单独同步。
|API|参数|返回值|说明|
|---|---|---|---|
|Config.getMode()|-|'render' 或 'source'|读取当前视图模式。|
|Config.setMode(value, options)|value: 'render' 或 'source';options.preserveSelection?: boolean|boolean|切换视图模式。|
|Config.isRenderMode() / Config.isSourceMode()|-|boolean|判断当前是否为渲染模式或源码模式。|
|Config.isReadOnly()|-|boolean|读取只读状态。|
|Config.setReadOnly(value)|value: boolean|boolean|切换只读状态。|
|Config.getTheme() / Config.setTheme(value)|value: string|string / boolean|读取或设置 SDK 实例主题色。|
|Config.getCursorColor() / Config.setCursorColor(value)|value: string|string / boolean|读取或设置光标颜色。|
|Config.setContentTheme(value)|ContentThemeOptions|boolean|批量设置强调色、光标色和页面内容主题。|
|Config.setPageContentTheme(value)|PageContentThemeOptions|boolean|只设置页面内容主题。|
|Config.setDocumentBasePath(value, options)|value: string;options.render?: boolean|boolean|设置当前 Markdown 文件所在目录。|
|Config.on(name, handler) / Config.off(name)|name: 'modeChange' \| 'themeChange'|boolean|订阅或取消配置事件。|
editor.Config.on('modeChange', (mode) => console.log(mode));
editor.Config.setMode('source');
editor.Config.setMode('render', { preserveSelection: true });
editor.Config.setReadOnly(true);光标与选区 API:Cursor
|API|参数|返回值|说明|
|---|---|---|---|
|Cursor.focus() / Cursor.blur()|-|boolean|聚焦或取消编辑器焦点。|
|Cursor.focusAt(offset, options)|offset: number;options.preventScroll?: boolean|boolean|聚焦到指定源码偏移。|
|Cursor.focusAtEnd(options)|options.preventScroll?: boolean|boolean|聚焦并定位到文档末尾。|
|Cursor.setOffset(offset, options)|offset: number;options.focus?: boolean;options.preserveViewport?: boolean|boolean|设置源码光标偏移,默认保持视口。|
|Cursor.setOffsetToEnd(options)|同 Cursor.setOffset()|boolean|只把光标偏移设置到文档末尾。|
|Cursor.getOffset()|-|number \| null|获取当前光标源码偏移。|
|Cursor.scrollIntoView(cursorScrollMargin)|cursorScrollMargin?: number|boolean|把当前光标滚动到宿主可视区域。|
|Cursor.on(name, handler) / Cursor.off(name)|name: 'change'|boolean|订阅或取消光标/选区变更事件。|
editor.Cursor.focusAtEnd({ preventScroll: true });
editor.Cursor.setOffset(42, { focus: true, preserveViewport: true });
editor.Cursor.scrollIntoView(80);视图状态 API:ViewState
|API|参数|返回值|说明|
|---|---|---|---|
|ViewState.getViewState()|-|{ focused, mode, offset, selection, viewportAnchor }|读取可持久化视图状态。|
|ViewState.restoreViewState(state, options)|state: ViewStateSnapshot;options.focus?: boolean|boolean|恢复指定视图状态。|
|ViewState.getAnchor()|-|{ lineIndex, lineOffsetRatio }|读取当前滚动视口锚点。|
|ViewState.restoreAnchor(anchor)|{ lineIndex: number, lineOffsetRatio: number }|boolean|按锚点恢复 hostElement.scrollTop。|
const state = editor.ViewState.getViewState();
localStorage.setItem('wmr:view-state', JSON.stringify(state));
const saved = JSON.parse(localStorage.getItem('wmr:view-state') || 'null');
if (saved) {
editor.ViewState.restoreViewState(saved);
}大纲与滚动 API:Outline
滚动相关 API 只使用 hostElement 作为滚动容器,不会滚动父容器、document、window 或页面级 body。
|API|参数|返回值|说明|
|---|---|---|---|
|Outline.getItems()|-|Promise<OutlineItem[]>|获取文档标题大纲。|
|Outline.scrollTo(lineIndex, options)|lineIndex: number;options.block?: ScrollLogicalPosition;options.behavior?: ScrollBehavior;options.highlight?: boolean;options.select?: boolean;options.pulseDuration?: number|boolean|滚动到指定源码行,高亮默认 3 秒后自动清除。|
|Outline.clearHighlight()|-|boolean|清除跳转高亮。|
const outline = await editor.Outline.getItems();
if (outline[0]) {
editor.Outline.scrollTo(outline[0].lineIndex, {
block: 'center',
highlight: true,
select: true,
});
}OutlineItem 结构:
{
id: 'heading-0-0-title',
level: 1,
title: 'Title',
raw: 'Title',
lineIndex: 0,
sourceOffset: 0,
}查找与替换 API
查找 API:Search
|API|参数|返回值|说明|
|---|---|---|---|
|Search.findAll(query, options)|query: string;options.caseSensitive?: boolean;options.wholeWord?: boolean;options.useRegex?: boolean|SearchMatch[]|返回全部匹配项;空查询返回 []。|
|Search.findAndReveal(query, options)|direction?: 'next' \| 'previous';highlight?: boolean;其他同 findAll()|SearchResult|查找并定位到指定方向的匹配项。|
|Search.findNext(query, options) / Search.findPrevious(query, options)|同 findAndReveal()|SearchResult|查找下一个或上一个匹配项。|
|Search.selectMatch(match, options)|match: SearchMatch;options.highlight?: boolean;options.focus?: boolean;options.block?: ScrollLogicalPosition;options.behavior?: ScrollBehavior|boolean|选中并滚动到匹配项。|
|Search.highlightMatch(match, options)|match: SearchMatch;options.block?: ScrollLogicalPosition;options.behavior?: ScrollBehavior;options.pulseDuration?: number|boolean|高亮匹配项。|
|Search.clearHighlight()|-|boolean|清除查找高亮。|
const result = editor.Search.findAndReveal('关键字', {
direction: 'next',
highlight: true,
});
if (result.match) {
editor.Search.selectMatch(result.match, { focus: true });
}说明:SearchOptions 类型中保留了 useRegex 字段;当前实现仍按普通文本查找,不按正则解析。
替换 API:Search
|API|参数|返回值|说明|
|---|---|---|---|
|Search.replaceMatch(match, replacement)|match: SearchMatch;replacement: string|SearchReplaceMatchResult|替换指定匹配项。|
|Search.replaceAll(query, replacement, options)|query: string;replacement: string;搜索选项同 findAll()|SearchReplaceAllResult|替换全部匹配项。|
const result = editor.Search.findAndReveal('foo', { highlight: true });
if (result.match) {
editor.Search.replaceMatch(result.match, 'bar');
}
editor.Search.replaceAll('foo', 'bar', {
caseSensitive: false,
});失败 reason 常见值包括 read-only、invalid-match、stale-match、no-match、not-changed。
查找数据结构
{
id: '12-15-0',
index: 0,
start: 12,
end: 15,
text: 'foo',
lineIndex: 2,
localStart: 4,
localEnd: 7,
endLineIndex: 2,
endLocalOffset: 7,
lineText: 'let foo = 1;',
}SearchResult 示例:
{
success: false,
matches: [],
index: -1,
match: null,
total: 0,
reason: 'no-match',
}链接 API
链接支持范围
链接功能有独立命名空间 Link,用于插入链接和接管链接打开行为。渲染层支持以下链接形态:
|写法|示例|说明|
|---|---|---|
|普通 Markdown 链接|[官网](https://example.com)|渲染为可点击链接。|
|引用链接|[文档][doc] + [doc]: ./docs/a.md|引用定义会在全文范围内解析。|
|自动链接|https://example.com|普通 URL 会识别为链接。|
|尖括号自动链接|<https://example.com> / <[email protected]>|支持 URL 和 email。|
|HTML 链接|<a href="./a.md">文档</a>|安全 HTML 解析后会接入统一点击逻辑。|
|内部锚点|[跳转](#章节标题)|先在当前文档内查找显式 id/name,再匹配标题文本。|
外部链接分三类:
|类型|判断|默认行为|
|---|---|---|
|web|http:// 或 https://|未注册 hook 时尝试 window.open(url, '_blank', 'noopener,noreferrer')。|
|email|mailto:|未注册 hook 时尝试 window.open()。|
|path|其他本地路径或相对路径|建议宿主通过 Link.on('open') 处理,SDK 默认不打开本地文件系统路径。|
插入链接 API:Link
|API|参数|返回值|说明|
|---|---|---|---|
|Link.insert(payload)|payload?: string \| LinkInsertOptions|boolean|插入 Markdown 链接;未传参数且没有选区时生成 []()。|
editor.Link.insert();
editor.Link.insert('http://www.baidu.com');
editor.Link.insert({ text: '百度', url: 'http://www.baidu.com' });payload 规则:
|传参|生成规则|
|---|---|
|不传|有选区时生成 [选中文本]();无选区时生成 []()。|
|字符串|作为链接地址写入,生成 [选中文本](url) 或 [](url)。|
|对象|支持 { text, label, url, href, title };text/label 优先于选区文本,url/href 写入链接地址,title 写入 Markdown 链接标题。|
插入后会选中链接文本部分;没有链接文本时光标停在 [] 中间,方便用户继续输入标题。
打开链接 hook:Link
|API|参数|返回值|说明|
|---|---|---|---|
|Link.on('open', handler)|handler: (payload) => boolean 或 void|boolean|接管链接打开请求。|
|Link.off('open')|-|boolean|取消链接打开 hook。|
editor.Link.on('open', ({ href, kind, documentBasePath }) => {
if (kind === 'path') {
openLocalPath({ href, documentBasePath });
return true;
}
return false;
});返回规则:
- 返回
true或不返回值:视为宿主已处理,SDK 不再回退。 - 返回
false:交回 SDK 默认逻辑;仅http/https和mailto:会尝试window.open()。 path链接通常需要宿主结合documentBasePath解析和打开。
Link.open payload
{
editor,
href: './docs/api.md',
kind: 'path',
documentBasePath: 'D:/notes/current',
}|字段|类型|说明|
|---|---|---|
|editor|MarkdownCore|当前 SDK 公开实例。|
|href|string|Markdown 链接原始目标。|
|kind|'web' \| 'email' \| 'path'|链接类型。|
|documentBasePath|string|当前 Markdown 文件所在目录,用于宿主解析相对文档路径。|
表格 API
表格能力
表格 API 负责插入 Markdown 表格、显示工具栏、设置列对齐、调整行列数和删除当前表格。工具栏状态依赖当前表格上下文,通常由用户点击表格单元格后自动建立。
editor.Table.insert({ rows: 3, columns: 4 });
editor.Table.showToolbar(0, 1);
editor.Table.setColumnAlignment('center');
editor.Table.resize(4, 4);
editor.Table.insertRowAbove();
editor.Table.insertRowBelow();
editor.Table.insertColumnLeft();
editor.Table.insertColumnRight();
editor.Table.deleteRow();
editor.Table.deleteColumn();
editor.Table.remove();表格方法:Table
|API|参数|返回值|说明|
|---|---|---|---|
|Table.insert(options)|{ rows?: number, columns?: number }|boolean|插入 Markdown 表格;默认 rows: 2、columns: 3,行数钳制到 1~20,列数钳制到 1~8。|
|Table.showToolbar(lineIndex, columnIndex)|lineIndex: number;columnIndex?: number|boolean|显示表格工具栏。|
|Table.hideToolbar()|-|boolean|隐藏工具栏并清理当前表格上下文。|
|Table.updateToolbar()|-|boolean|根据当前选区或已有上下文刷新工具栏位置和状态。|
|Table.setColumnAlignment(alignment)|alignment: 'left' \| 'center' \| 'right'|boolean|设置当前列对齐方式。|
|Table.resize(rows, columns)|rows: number \| string;columns: number \| string|boolean|调整当前表格行列数。|
|Table.insertRowAbove()|-|boolean|在当前正文行上方插入空行;当前上下文为表头时,在首个正文行位置插入。|
|Table.insertRowBelow()|-|boolean|在当前正文行下方插入空行;当前上下文为表头时,在首个正文行位置插入。|
|Table.insertColumnLeft()|-|boolean|在当前列左侧插入空列,新列默认左对齐。|
|Table.insertColumnRight()|-|boolean|在当前列右侧插入空列,新列默认左对齐。|
|Table.deleteRow()|-|boolean|删除当前正文行;表头上下文返回 false。|
|Table.deleteColumn()|-|boolean|删除当前列;只剩一列时返回 false,避免生成无效表格。|
|Table.remove()|-|boolean|删除当前表格。|
除 insert() 外,上述结构操作均依赖当前表格上下文。用户点击单元格时 SDK 会自动建立上下文;外部右键菜单等自定义入口应先调用 Table.showToolbar(lineIndex, columnIndex),再调用对应操作。
图片 API
图片能力
图片功能覆盖插入、路径选择、选中回调、右键菜单、Markdown/HTML 互转、缩放、对齐、复制源码、删除引用、粘贴图片保存和跨文档图片资源复制。
const editor = new MarkdownCore(hostElement, {
imageBasePath: './images',
documentBasePath: 'D:/notes/current',
});
editor.Image.insert({
url: './images/a.png',
alt: '示例图片',
});
editor.Image.on('select', ({ image, context }) => {
console.log(context.url, image.element);
});插入与路径 API:Image
|API|参数|返回值|说明|
|---|---|---|---|
|Image.insert(image)|{ url?: string, alt?: string }|boolean|插入图片并在下方保留空行;不传时插入空路径图片并打开路径输入。|
|Image.pickPath(context)|ImagePickPathContext|Promise<string>|调用 image.pickPath hook;没有 hook 或返回不可用路径时解析为 ''。|
|Image.commitPath(target)|HTMLInputElement 或 { context?: ImageContext, path?: string }|boolean|提交图片路径。|
|Image.browsePath(target)|HTMLInputElement 或 { context?: ImageContext, currentPath?: string, alt?: string }|Promise<boolean>|打开图片路径选择 hook 并提交返回路径。|
editor.Image.on('pickPath', async ({ documentBasePath, imageBasePath }) => {
return pickImageFromDirectory(documentBasePath, imageBasePath);
});
await editor.Image.browsePath({
context: editor.Image.getSelectedContext(),
currentPath: './images/old.png',
});选中与上下文 API:Image
|API|参数|返回值|说明|
|---|---|---|---|
|Image.getSelected()|-|{ image, images, index }|获取当前选中图片、文档内图片列表和索引。|
|Image.getSelectedContext()|-|ImageContext \| null|获取当前图片源码上下文。|
|Image.getContextFromEvent(event)|event: Event|ImageContext \| null|从图片相关事件目标提取上下文。|
|Image.getMenuContext()|-|ImageContext \| null|读取当前图片菜单上下文。|
|Image.clearMenuContext()|-|boolean|清理图片菜单上下文。|
|Image.hideMenu()|-|boolean|关闭图片菜单上下文,关闭时触发 image.menuClose。|
图片编辑 API:Image
|API|参数|返回值|说明|
|---|---|---|---|
|Image.showSource(context)|context?: ImageContext|boolean|显示图片源码并把光标放到图片源码起点。|
|Image.toMarkdown(context) / Image.toHtml(context)|context?: ImageContext|Promise<boolean>|把当前图片源码转换为 Markdown 或 HTML 图片。|
|Image.setZoom(percent, context)|percent: number,必须大于 0|Promise<boolean>|设置图片缩放百分比。|
|Image.setAlignment(alignment, context)|alignment: 'left' \| 'center' \| 'right'|Promise<boolean>|设置图片水平对齐。|
|Image.setHeading(level, context)|level: 1~6|Promise<boolean>|把图片所在块切换为标题。|
|Image.setParagraph(context)|context?: ImageContext|Promise<boolean>|把图片所在块切换为普通段落。|
|Image.copyPath(context)|context?: ImageContext|Promise<boolean>|复制图片原始路径。|
|Image.copySource(context)|context?: ImageContext|Promise<boolean>|复制图片 Markdown/HTML 源码。|
|Image.cutSource(context)|context?: ImageContext|Promise<boolean>|复制并删除图片引用。|
|Image.removeReference(context)|context?: ImageContext|Promise<boolean>|删除图片引用源码。|
图片资源与文件 hook
|API / hook|参数|返回值|说明|
|---|---|---|---|
|Image.createResourcePayload(markdown)|markdown?: string|object|创建跨文档复制图片资源 payload。|
|Image.on('pickPath', handler)|ImagePickPathContext|string \| Promise<string>|图片路径选择。|
|Image.on('exportResources', handler)|{ markdown, images, payload, editor }|boolean \| Promise<boolean>|跨文档复制时导出资源。|
|Image.on('importResources', handler)|{ markdown, images, payload, documentBasePath, imageBasePath, editor }|string \| { success, mappings } \| Promise<...>|跨文档粘贴时导入资源。|
|Image.on('saveFile', handler)|ImageFileContext<'saveFile'>|{ success, url?, alt? } \| Promise<...>|粘贴图片或保存图片文件。|
|Image.on('collectFile' \| 'openFile' \| 'copyFile' \| 'moveFile' \| 'deleteFile', handler)|ImageFileContext|unknown \| Promise<unknown>|图片菜单中的文件动作 hook。|
editor.Image.on('saveFile', async ({ data, fileName, documentBasePath, imageBasePath }) => {
if (!data) return null;
const url = await saveImageToDirectory(data, documentBasePath, imageBasePath, fileName);
return { success: true, url, alt: fileName };
});
editor.Image.on('exportResources', async ({ markdown, payload }) => {
await nativeClipboard.writeMarkdownResource({ markdown, payload });
return true;
});
editor.Image.on('importResources', async ({ payload, documentBasePath, imageBasePath }) => {
const mappings = await nativeClipboard.importMarkdownImages({
payload,
documentBasePath,
imageBasePath,
});
return { success: true, mappings };
});mappings 可按 id 或 originalUrl 匹配图片:
[
{ id: 'image-0', originalUrl: './images/a.png', url: './images/a-copy.png' },
]图片事件
|事件|类型|说明|
|---|---|---|
|Image.on('select', handler)|通知事件,也派发 image:select|图片被选中。|
|Image.on('contextMenu', handler)|通知事件,也派发 image:context-menu|图片右键菜单请求。|
|Image.on('menuClose', handler)|通知事件,也派发 image:menu-close|图片菜单关闭。|
图片上下文
{
src: 'wang-local-image://image/?path=...',
url: './images/a.png',
filePath: 'D:/notes/current/images/a.png',
alt: '示例图片',
title: '',
kind: 'image',
lineIndex: 0,
sourceStart: 0,
sourceEnd: 22,
start: 0,
end: 22,
sourceText: '',
style: '',
}渲染图片信息会在上下文字段之外额外包含 renderedWidth、renderedHeight、naturalWidth、naturalHeight、element 和 tokenElement。
音频块 API
音频块写法
音频块使用 fenced code 保存音频引用,围栏语言为 audio。SDK 负责把源码渲染为可播放的音频块,并提供选中、播放/暂停、源码定位、复制和删除 API;音频采集、权限申请、转码、上传、持久化和路径回写由调用端实现。
```audio
{
"src": "./records/meeting.m4a",
"title": "会议录音",
"duration": 182,
"mime": "audio/mp4"
}
```也支持简写:
```audio
./records/meeting.m4a
```音频方法:Audio
|API|参数|返回值|说明|
|---|---|---|---|
|Audio.insert(value)|string 或音频对象|boolean|插入音频块。|
|Audio.getSelected()|-|{ audio, audios, index }|获取当前音频和列表。|
|Audio.getSelectedContext()|-|AudioContext \| null|获取当前音频上下文。|
|Audio.getContextFromEvent(event)|event: Event|AudioContext \| null|从事件目标提取音频上下文。|
|Audio.getElement(context)|context?: AudioContext|HTMLAudioElement \| null|获取原生 audio DOM。|
|Audio.play(context)|context?: AudioContext|Promise<boolean>|播放音频。|
|Audio.pause(context)|context?: AudioContext|boolean|暂停音频。|
|Audio.seekBy(seconds, context)|seconds: number|boolean|按秒跳转。|
|Audio.togglePlayback(context)|context?: AudioContext|boolean \| Promise<boolean>|切换播放/暂停。|
|Audio.showSource(context)|context?: AudioContext|boolean|定位到音频块源码。|
|Audio.copySource(context)|context?: AudioContext|Promise<boolean> \| false|复制完整围栏源码。|
|Audio.cutSource(context)|context?: AudioContext|Promise<boolean>|复制并删除音频块源码。|
|Audio.remove(context)|context?: AudioContext|boolean|删除音频块。|
|Audio.on('select', handler) / Audio.off('select')|handler: (payload) => void|boolean|订阅或取消音频选中事件。|
音频对象可传:
editor.Audio.insert({
src: 'meeting.m4a',
title: '会议录音',
duration: 182,
mime: 'audio/mp4',
});音频上下文
{
raw: '{"src":"./records/meeting.m4a","title":"会议录音"}',
url: './records/meeting.m4a',
src: 'wang-local-audio://audio/?path=...',
title: '会议录音',
duration: 182,
durationText: '3:02',
mime: 'audio/mp4',
valid: true,
sourceText: '```audio\n{...}\n```',
start: 0,
end: 88,
openLineIndex: 0,
closeLineIndex: 4,
contentStartLineIndex: 1,
contentEndLineIndex: 3,
previewLineIndex: 1,
language: 'audio',
}画板 API
画板写法
画板使用 wang-canvas 渲染。SDK 只接收并渲染 JSON 场景,不内置画板编辑页或编辑按钮;调用端保存 JSON,并通过编辑事件打开自己的编辑页面。
```wang-canvas
{
"type": "wang-canvas",
"version": 1,
"elements": [],
"appState": {
"viewport": { "x": 0, "y": 0, "zoom": 1 }
}
}
```围栏语言只支持 wang-canvas。围栏内必须是完整 JSON;SDK 会在渲染模式隐藏源码行,只显示一个可点击的画板预览块。
详细说明见 docs/whiteboard-rendering.md。
画板方法:Whiteboard
|API|参数|返回值|说明|
|---|---|---|---|
|Whiteboard.insert(scene)|WhiteboardScene 或 string|boolean|插入画板围栏块;字符串必须是有效 JSON。|
|Whiteboard.edit(context)|context?: WhiteboardContext|boolean|触发 whiteboard.edit 事件。|
|Whiteboard.getSelected()|-|{ whiteboard, whiteboards, index }|获取当前画板和列表。|
|Whiteboard.getSelectedContext()|-|WhiteboardContext \| null|获取当前画板上下文。|
|Whiteboard.getContextFromEvent(event)|event: Event|WhiteboardContext \| null|从事件目标提取画板上下文。|
|Whiteboard.getElement(context)|context?: WhiteboardContext|HTMLElement \| null|获取画板预览 DOM。|
|Whiteboard.showSource(context)|context?: WhiteboardContext|boolean|定位到画板源码。|
|Whiteboard.copySource(context)|context?: WhiteboardContext|Promise<boolean> \| false|复制完整围栏源码。|
|Whiteboard.toJson(context)|context?: WhiteboardContext|string \| null|获取围栏内 JSON 字符串。|
|Whiteboard.copyJson(context)|context?: WhiteboardContext|Promise<boolean> \| false|复制围栏内 JSON 字符串。|
|Whiteboard.cutSource(context)|context?: WhiteboardContext|Promise<boolean>|复制并删除画板源码。|
|Whiteboard.remove(context)|context?: WhiteboardContext|boolean|删除画板。|
|Whiteboard.on('click' \| 'edit', handler) / Whiteboard.off(name)|handler: (payload) => void|boolean|订阅或取消画板事件。|
editor.Whiteboard.on('edit', ({ context }) => {
openWhiteboardEditor({ json: context.json });
});
editor.Whiteboard.insert({
type: 'wang-canvas',
version: 1,
elements: [],
appState: {},
});画板上下文
{
json: '{"type":"wang-canvas","version":1,"elements":[]}',
scene: { type: 'wang-canvas', version: 1, elements: [] },
valid: true,
sourceText: '```wang-canvas\n{...}\n```',
start: 0,
end: 48,
openLineIndex: 0,
closeLineIndex: 2,
contentStartLineIndex: 1,
contentEndLineIndex: 1,
previewLineIndex: 1,
language: 'wang-canvas',
}代码块 API
代码块能力
代码块支持语言工具栏、复制、格式化、自动换行、行号、折叠、受限拖选和选区缩进。鼠标从代码块内容中拖选时,选区会被限制在当前代码块内容内;如果选区混入代码块外内容,代码块缩进命令不会执行。
editor.Code.insert();
editor.Code.adjustIndent(1);
editor.Code.adjustIndent(-1);代码块方法:Code
|API|参数|返回值|说明|
|---|---|---|---|
|Code.insert()|-|boolean|在当前选区插入代码块。|
|Code.adjustIndent(direction)|direction: number;小于 0 减少缩进,否则增加缩进|boolean|调整当前代码块选区缩进。|
|Code.setLanguage(lineIndex, language, selection)|lineIndex: number;language: string;selection?: { anchor, focus }|boolean|设置代码块围栏语言。|
|Code.isWrapEnabled()|-|boolean|读取代码块自动换行状态。|
|Code.toggleWrap(force)|force?: boolean|boolean|切换或设置代码块自动换行状态。|
|Code.setLineNumbers(visible)|visible: boolean|boolean|设置代码块行号显隐。|
|Code.setDefaultCollapsed(collapsed)|collapsed: boolean|boolean|设置代码块默认折叠状态并清理单块折叠覆盖。|
|Code.toggleCollapsed(lineIndex, force)|lineIndex: number;force?: boolean|boolean|折叠或展开指定代码块。|
|Code.format()|-|Promise<boolean>|格式化当前代码块。|
|Code.showLanguageEditor(block, language, syncInput)|block: object;language?: string;syncInput?: boolean|boolean|显示代码块语言工具栏。|
|Code.hideLanguageEditor()|-|boolean|隐藏语言工具栏。|
|Code.updateLanguageEditor()|-|boolean|根据当前选区刷新语言工具栏。|
代码块事件
|事件|类型|说明|
|---|---|---|
|Code.on('copy', handler)|需要返回结果的 hook|代码块复制按钮触发;SDK 不直接写系统剪贴板。|
|Code.on('formatError', handler)|通知事件,也派发 code:format-error|格式化失败或语言不支持。|
editor.Code.on('copy', async ({ code, language, origin }) => {
await nativeClipboard.writeText(code);
console.log(language, origin);
return true;
});
editor.Code.on('formatError', ({ language, reason, message }) => {
console.warn(language, reason, message);
});copy hook 返回 true、{ success: true } 或 { handled: true } 表示复制成功。origin 用于区分触发来源,可能是 'block-header' 或 'toolbar'。
内置格式化语言
html, htm, css, scss, javascript, js, jsx, mjs, cjs, markdown, md, vue, dart, kotlin, kt, kts导出与快照 API
快照能力
SDK 只提供当前渲染结果快照,不负责完整导出、文件生成、资源复制、格式转换或保存对话框。
const snapshot = editor.Document.createSnapshot({
fullDocument: true,
resourceTokenPrefix: '__NOTE_RESOURCE_',
});
console.log(snapshot.htmlFragment);
console.log(snapshot.resources);Document.createSnapshot options
|选项|类型|默认值|说明|
|---|---|---|---|
|root|HTMLElement|-|自定义快照根节点;不传时使用当前编辑器根节点。|
|fullDocument|boolean|虚拟滚动时自动为 true|是否按完整文档生成快照,适合虚拟滚动场景。|
|resourceTokenPrefix|string|'__SDK_RENDER_RESOURCE_'|资源占位符前缀。|
|documentBasePath|string|当前配置|覆盖当前文档目录。|
|imageBasePath|string|当前配置|覆盖图片基础目录。|
|audioBasePath|string|当前配置|覆盖音频基础目录。|
|resolveImageSource|(context) => string \| { src?, filePath? } \| null|当前配置|覆盖图片路径解析。|
|resolveAudioSource|(context) => string \| { src?, filePath? } \| null|当前配置|覆盖音频路径解析。|
快照返回结构
{
htmlFragment: '<div class="wmr-root ...">...</div>',
resources: [
{
token: '__NOTE_RESOURCE_0__',
type: 'image',
sourcePath: 'D:/notes/images/a.png',
originalSrc: 'wang-local-image://image/?path=...',
},
{
token: '__NOTE_RESOURCE_1__',
type: 'audio',
sourcePath: 'D:/notes/records/meeting.m4a',
originalSrc: 'wang-local-audio://audio/?path=...',
},
],
}htmlFragment 是当前 .wmr-root 的 DOM 片段,不包含 <!doctype>、<html>、<head> 或 <body>。resources 只包含 SDK 能确认来源的本地图片与音频资源;远程资源、data:image 与 data:audio 默认保留在 HTML 中,不进入资源列表。
调用者负责组装完整 HTML 文档、收集业务侧 CSS、创建资源目录、复制图片、替换 token、调用 PDF/Word/图片转换服务以及写入文件。
let html = snapshot.htmlFragment;
for (const resource of snapshot.resources) {
html = html.split(resource.token).join(resource.sourcePath);
}事件与回调 payload
事件总览
|命名空间|事件|类型|
|---|---|---|
|Document|contentChange / contextMenu|通知事件,派发 document:content-change / document:context-menu。|
|Config|modeChange / themeChange|通知事件,派发 config:mode-change / config:theme-change。|
|Cursor|change|通知事件,派发 cursor:change。|
|Image|select / contextMenu / menuClose|通知事件,派发对应 DOM 事件。|
|Image|pickPath / exportResources / importResources / collectFile / openFile / copyFile / saveFile / moveFile / deleteFile|需要返回结果的 hook。|
|Audio|select|通知事件,派发 audio:select。|
|Whiteboard|click / edit|通知事件,派发 whiteboard:click / whiteboard:edit。|
|Code|copy|需要返回结果的 hook。|
|Code|formatError|通知事件,派发 code:format-error。|
|Link|open|需要返回结果的 hook。|
文档与配置 payload
editor.Document.on('contentChange', (source) => {
console.log(source);
});
editor.Config.on('modeChange', (mode) => {
console.log(mode);
});
editor.Config.on('themeChange', (theme) => {
console.log(theme);
});文档右键菜单 payload:
{
editor,
event,
clientX: 0,
clientY: 0,
position: { x: 0, y: 0 },
readOnly: false,
context: {
block: 'paragraph',
hasSelection: false,
headingLevel: 0,
lineIndex: 0,
source: '',
},
}光标 payload
{
editor,
reason: 'selectionchange',
focused: true,
mode: 'render',
offset: 42,
selection: { anchor: 42, focus: 42 },
viewportAnchor: { lineIndex: 8, lineOffsetRatio: 0.33 },
}图片 payload
{
editor,
event,
context: {
src: 'wang-local-image://image/?path=...',
url: './images/a.png',
alt: '示例图片',
filePath: 'D:/notes/current/images/a.png',
kind: 'image',
lineIndex: 0,
sourceStart: 0,
sourceEnd: 22,
start: 0,
end: 22,
sourceText: '',
},
image: {
src: 'wang-local-image://image/?path=...',
url: './images/a.png',
alt: '示例图片',
renderedWidth: 480,
renderedHeight: 320,
naturalWidth: 960,
naturalHeight: 640,
element: HTMLImageElement,
tokenElement: HTMLSpanElement,
},
images: [],
index: 0,
clientX: 320,
clientY: 260,
position: { x: 320, y: 260 },
readOnly: false,
}音频 payload
{
editor,
event,
context: {
raw: '{"src":"./records/meeting.m4a","title":"会议录音"}',
url: './records/meeting.m4a',
src: 'wang-local-audio://audio/?path=...',
title: '会议录音',
duration: 182,
durationText: '3:02',
mime: 'audio/mp4',
valid: true,
},
audio: {
url: './records/meeting.m4a',
src: 'wang-local-audio://audio/?path=...',
title: '会议录音',
duration: 182,
currentTime: 0,
paused: true,
valid: true,
element: HTMLElement,
audioElement: HTMLAudioElement,
},
audios: [],
index: 0,
clientX: 320,
clientY: 260,
position: { x: 320, y: 260 },
readOnly: false,
}画板 payload
{
action: 'edit',
editor,
event,
interaction: 'edit',
context: {
json: '{"type":"wang-canvas","version":1,"elements":[]}',
scene: { type: 'wang-canvas', version: 1, elements: [] },
valid: true,
sourceText: '```wang-canvas\n{...}\n```',
},
whiteboard: {
json: '{"type":"wang-canvas","version":1,"elements":[]}',
scene: { type: 'wang-canvas', version: 1, elements: [] },
renderedWidth: 960,
renderedHeight: 320,
element: HTMLElement,
},
whiteboards: [],
index: 0,
clientX: 320,
clientY: 260,
position: { x: 320, y: 260 },
readOnly: false,
}链接 payload
{
editor,
href: '码云发布流程.md',
kind: 'path',
documentBasePath: 'D:/notes/current',
}代码格式化错误 payload
{
language: 'js',
reason: 'unsupported',
message: '当前代码语言暂不支持内置格式化',
error: null,
}依赖与手动构建
运行依赖
|依赖|说明|
|---|---|
|mini-jq-tools|DOM 工具能力。|
|wang-canvas|画板只读渲染。|
开发依赖
|依赖|说明|
|---|---|
|vite|打包工具。|
|typescript|类型检查与声明生成。|
|sass|样式预处理。|
可选手动构建
需要生成 dist/ 时,可由使用者手动执行:
npm run build构建产物:
|文件|说明|
|---|---|
|dist/index.es.js|默认入口 JS。|
|dist/style.css|默认入口样式。|
