npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

wang-markdown-render

v1.2.1

Published

高级全功能Markdown渲染器

Readme

wang-markdown-render

Marknote Markdown 渲染与所见即所得编辑核心 SDK。它提供一个 MarkdownCore 入口,通过功能命名空间暴露文档、配置、光标、格式、链接、表格、图片、音频、画板、代码块、查找替换、导出快照等能力。

功能概览

核心能力

  • Markdown 渲染模式、源码模式与只读渲染。
  • 单实例模式切换:同一个 MarkdownCore 通过 mode / Config.setMode() 在渲染视图和源码视图之间切换。
  • 领域命名空间 API:DocumentConfigCursorViewStateContentFormatLinkTableImageAudioWhiteboardCode 等。
  • 查找与替换:支持大小写、全词、方向、定位、选中、高亮和批量替换。
  • 表格、图片、音频块、画板、代码块等富内容渲染和编辑辅助。
  • 大纲、滚动定位、文档指标、渲染快照与资源列表,完整导出流程由调用端实现。

文档目录

快速接入

入口与样式

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 只监听 hostElementscroll 事件,也只读写 hostElement.scrollTop;不会监听父容器、documentwindow 或页面级 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 是主题强调色,影响选区、默认光标、激活边框等,不负责页面背景和正文颜色。页面内容色应使用 contentThemeConfig.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|查找结果高亮背景色。| |codeTokenKeywordColorcodeToken*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: stringoptions.documentBasePath?: stringoptions.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 切换渲染模式和源码模式。源码模式仍复用同一份源码状态:DocumentHistorySearchCursor 等能力不需要单独同步。

|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: stringoptions.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: numberoptions.preventScroll?: boolean|boolean|聚焦到指定源码偏移。| |Cursor.focusAtEnd(options)|options.preventScroll?: boolean|boolean|聚焦并定位到文档末尾。| |Cursor.setOffset(offset, options)|offset: numberoptions.focus?: booleanoptions.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: ViewStateSnapshotoptions.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 作为滚动容器,不会滚动父容器、documentwindow 或页面级 body

|API|参数|返回值|说明| |---|---|---|---| |Outline.getItems()|-|Promise<OutlineItem[]>|获取文档标题大纲。| |Outline.scrollTo(lineIndex, options)|lineIndex: numberoptions.block?: ScrollLogicalPositionoptions.behavior?: ScrollBehavioroptions.highlight?: booleanoptions.select?: booleanoptions.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: stringoptions.caseSensitive?: booleanoptions.wholeWord?: booleanoptions.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: SearchMatchoptions.highlight?: booleanoptions.focus?: booleanoptions.block?: ScrollLogicalPositionoptions.behavior?: ScrollBehavior|boolean|选中并滚动到匹配项。| |Search.highlightMatch(match, options)|match: SearchMatchoptions.block?: ScrollLogicalPositionoptions.behavior?: ScrollBehavioroptions.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: SearchMatchreplacement: string|SearchReplaceMatchResult|替换指定匹配项。| |Search.replaceAll(query, replacement, options)|query: stringreplacement: 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-onlyinvalid-matchstale-matchno-matchnot-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/httpsmailto: 会尝试 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: 2columns: 3,行数钳制到 1~20,列数钳制到 1~8。| |Table.showToolbar(lineIndex, columnIndex)|lineIndex: numbercolumnIndex?: number|boolean|显示表格工具栏。| |Table.hideToolbar()|-|boolean|隐藏工具栏并清理当前表格上下文。| |Table.updateToolbar()|-|boolean|根据当前选区或已有上下文刷新工具栏位置和状态。| |Table.setColumnAlignment(alignment)|alignment: 'left' \| 'center' \| 'right'|boolean|设置当前列对齐方式。| |Table.resize(rows, columns)|rows: number \| stringcolumns: 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 可按 idoriginalUrl 匹配图片:

[
  { 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: '![示例图片](./images/a.png)',
  style: '',
}

渲染图片信息会在上下文字段之外额外包含 renderedWidthrenderedHeightnaturalWidthnaturalHeightelementtokenElement

音频块 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)|WhiteboardScenestring|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: numberlanguage: stringselection?: { 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: numberforce?: boolean|boolean|折叠或展开指定代码块。| |Code.format()|-|Promise<boolean>|格式化当前代码块。| |Code.showLanguageEditor(block, language, syncInput)|block: objectlanguage?: stringsyncInput?: 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:imagedata: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: '![示例图片](./images/a.png)',
  },
  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|默认入口样式。|