README.md
March 25, 2026 · View on GitHub
用户自定义插件:开放能力
通过 custom 插件,为您提供了开放能力,允许您在右键菜单中 创建和集成自定义功能。
核心特性
custom 插件通过 声明式配置和代码 实现自定义:
- 注册界面元素与行为: 支持定义 CSS 样式 (
style)、HTML 元素 (html)、右键菜单提示 (hint)、快捷键 (hotkey)。 - 灵活的生命周期: 提供
beforeProcess、init、process、afterProcess、callback等生命周期函数,允许在插件不同阶段执行自定义逻辑。 - 光标位置控制: 使用
selector定义插件可用的光标位置。
使用方法
自定义插件仅需两步:
- 添加插件配置:在
./plugin/global/settings/custom_plugin.user.toml文件中添加插件配置。 - 编写插件代码:在
./plugin/custom/plugins目录下创建与插件同名的.js文件,编写继承BaseCustomPlugin的类并导出为plugin。
示例
快速入门
此示例展示了如何创建一个简单的插件,实现右键菜单点击或快捷键触发时显示提示框,并在控制台输出信息。
步骤一:在 ./plugin/global/settings/custom_plugin.user.toml 添加以下配置:
[helloWorld]
name = "你好世界" # 插件在右键菜单中显示的名称
enable = true # 控制插件是否启用
hide = false # 控制插件是否在右键菜单中隐藏
order = 1 # 插件在右键菜单中的显示顺序(数值越大越靠后,支持负数)
hotkey = "ctrl+alt+u" # 定义触发插件的快捷键
console_message = "I am in process" # 定义将在控制台输出的信息
show_message = "this is hello world plugin" # 定义将在提示框中显示的信息
name、enable、hide、order 是必须项,其余是插件个性化的配置。
步骤二:创建文件 ./plugin/custom/plugins/helloWorld.js 文件,并将以下代码保存到该文件中:
// ./plugin/custom/plugins/helloWorld.js
class helloWorld extends BaseCustomPlugin {
// beforeProcess 方法最先执行,用于检查插件运行的前提条件
// 如果条件不满足,应返回 this.utils.PLUGIN_LOAD_ABORT 以终止插件加载
beforeProcess = async () => {
// 在实际开发中,请替换这里的 false 为有意义的条件检查逻辑
if (false) {
return this.utils.PLUGIN_LOAD_ABORT
}
}
// 注册 CSS 样式。返回一个字符串,该字符串会自动作为 <style> 标签插入到 DOM 中
style = () => "#hello-world { margin: 10px; }"
// 注册 DOM 元素。可以返回 Element 类型或表示元素的字符串,它们将自动插入到 DOM 中
html = () => "<div id='hello-world'></div>"
// 注册右键菜单的提示信息
hint = () => "this is hello world hint"
// 注册触发 callback 的快捷键。返回一个字符串数组
hotkey = () => [this.config.hotkey]
// 用于插件的初始化,通常在这里获取或设置 DOM 元素、初始化变量等
init = () => {
// 获取 html() 方法插入的 DOM 元素
this.myDiv = document.querySelector("#hello-world")
}
// process 方法在插件初始化完成后(执行上述注册逻辑后)自动运行
process = () => {
// 可以通过 this.config 获取 TOML 文件中的所有配置项
console.log(this.config.console_message)
console.log("[helloWorldPlugin]: ", this)
console.log(this.myDiv)
}
// callback 方法在点击右键菜单选项或键入快捷键时自动调用
// 注意:如果未定义 callback 函数,该插件将无法通过右键菜单点击触发
// anchorNode 参数表示调用此插件时,光标所在的 Element
callback = anchorNode => {
alert(this.config.show_message)
}
}
// 导出插件类
module.exports = { plugin: helloWorld }
验证:
- 重启 Typora。
- 打开 Chrome devtools,检查控制台是否输出了
I am in process、插件对象和对应的 Element。 - 右键点击编辑器区域,弹出右键菜单。将鼠标悬停在
常用插件 -> 二级插件 -> 你好世界上,应显示提示信息this is hello world hint。点击你好世界菜单项,应弹出一个提示框,显示this is hello world plugin。 - 按下您定义的快捷键
ctrl+alt+u,应弹出同样的提示框。
插入思维导图
此示例演示如何获取当前文档的大纲结构,并将其转换为 Mermaid 图的形式插入到文档中。
实现:
- 步骤一:在
./plugin/global/settings/custom_plugin.user.toml添加配置。 - 步骤二:在
./plugin/custom/plugins目录下,创建和插件同名的 js 文件(insertMindmap.js),在此文件中创建一个 class 继承自 BaseCustomPlugin,并导出为plugin。
# ./plugin/global/settings/custom_plugin.user.toml
[insertMindmap]
name = "插入思维导图" # 插件名称
enable = true # 是否启用此二级插件
hide = false # 是否在右键菜单中隐藏
order = 1 # 在右键菜单中的出现顺序
// ./plugin/custom/plugins/insertMindmap.js
class insertMindmap extends BaseCustomPlugin {
// callback 方法获取文档大纲树,转换为 Mermaid 格式,并插入到文档中
callback = anchorNode => {
const tree = this.utils.getTocTree() // 获取文档大纲树结构
const mermaid = this._toGraph(tree) // 将树结构转换为 Mermaid 图格式
this.utils.insertText(null, mermaid) // 将生成的 Mermaid 代码插入到文档中
}
_toGraph = tree => {
let num = 0
const getName = node => {
if (node._shortName) {
return node._shortName
}
node._shortName = "T" + ++num
const name = node.text.replace(/"/g, "#quot;")
return `${node._shortName}("${name}")`
}
const getTokens = (node, list) => {
node.children.forEach(child => list.push(getName(node), "-->", getName(child), "\n"))
node.children.forEach(child => getTokens(child, list))
return list
}
const tokens = getTokens(tree, ["graph LR", "\n"])
return ["```mermaid", "\n ", ...tokens, "```"].join("")
}
}
// 导出插件类
module.exports = { plugin: insertMindmap }
验证:
打开 Typora,右键点击编辑器区域,弹出右键菜单。选择 常用插件 -> 二级插件 -> 插入思维导图 并点击。根据当前文档的大纲结构,一个对应的 Mermaid 图将插入到文档中。
例如,对于具有以下结构的文档 README.md:
## 用户自定义插件:开放能力
## 核心特性
## 使用方法
## 示例
### 快速入门
### 插入思维导图
### 复制标题路径
生成的 Mermaid 图如下所示:
graph LR
T1("README")-->T2("用户自定义插件:开放能力")
T1-->T3("核心特性")
T1-->T4("使用方法")
T1-->T5("示例")
T5-->T6("快速入门")
T5-->T7("插入思维导图")
T5-->T8("复制标题路径")
复制标题路径
此示例展示了如何在右键菜单中添加选项,当光标位于标题时,将当前标题的完整路径复制到剪切板。
实现:
- 步骤一:在
./plugin/global/settings/custom_plugin.user.toml添加配置。 - 步骤二:在
./plugin/custom/plugins目录下,创建和插件同名的 js 文件(myFullPathCopy.js),在此文件中创建一个 class 继承自 BaseCustomPlugin,并导出为plugin。 - 步骤三:验证。将光标置于标题处,右键点击插件选项或使用快捷键,然后尝试粘贴,验证标题路径是否已复制。
# ./plugin/global/settings/custom_plugin.user.toml
[myFullPathCopy]
name = "复制标题路径" # 插件名称
enable = true # 是否启用此二级插件
hide = false # 是否在右键菜单中隐藏
order = 1 # 在右键菜单中的出现顺序
# 快捷键
hotkey = "ctrl+shift+u"
# 如果在空白页调用此插件,使用的文件名(文件还不存在,需要一个默认文件名)
untitled_file_name = "Untitled"
# 是否在标题和提示之间添加空格
add_space = true
// ./plugin/custom/plugins/myFullPathCopy.js
// 1
class myFullPathCopy extends BaseCustomPlugin {
// 2
selector = () => '#write > [cid]'
// 3
hint = () => "将当前标题的路径复制到剪切板"
// 4
init = () => {}
// 5
style = () => {}
// 6
styleTemplate = () => {}
// 7
html = () => {}
// 8
hotkey = () => [this.config.hotkey]
// 9
beforeProcess = async () => {}
// 10
process = () => {}
// 11
callback = anchorNode => {
const text = this.getFullPath(anchorNode)
navigator.clipboard.writeText(text)
}
getFullPath = (anchorNode) => {
const getHeaders = (startNode) => {
const HEADING_TAGS = ["H1", "H2", "H3", "H4", "H5", "H6"]
const i18nSuffixes = ["一级标题", "二级标题", "三级标题", "四级标题", "五级标题", "六级标题"]
const i18nNoHeader = "无"
const headerMap = new Map()
let minLevel = Infinity
let curNode = startNode
while (curNode && minLevel > 0) {
const level = HEADING_TAGS.indexOf(curNode.tagName)
if (level !== -1 && level < minLevel) {
headerMap.set(level, curNode.textContent)
minLevel = level
}
curNode = curNode.previousElementSibling
}
const maxDepth = (headerMap.size === 0) ? 0 : Math.max(...headerMap.keys()) + 1
return Array.from({ length: maxDepth }, (_, level) => {
const title = headerMap.get(level) || i18nNoHeader
const suffix = i18nSuffixes[level]
const space = this.config.add_space ? " " : ""
return title + space + suffix
})
}
const filePath = this.utils.getFilePath() || this.config.untitled_file_name
const pathSegments = getHeaders(anchorNode)
return this.utils.Package.Path.join(filePath, ...pathSegments)
}
}
// 12
module.exports = { plugin: myFullPathCopy }
// 1. 创建 class,继承 BaseCustomPlugin 类。这将使 myFullPathCopy 自动拥有 utils 和 config 属性
// - config: 该插件在 custom_plugin.user.toml 文件中的所有配置字段
// - utils: 插件系统提供的静态工具类,其定义位于 `./plugin/global/core/utils`
// 2. selector: 定义插件可用的光标位置。当光标位于哪些元素时,此插件才可用。返回 null-like value 表示任何位置都可用。在这里的含义就是:只当光标位于 Markdown 标题元素时可用
// 3. hint: 当鼠标悬停在右键菜单选项上时显示的提示信息
// 4. init: 在这里进行变量的初始化操作
// 5. style: 用于插入 style 标签。返回值为字符串
// 6. styleTemplate: 引入 `./plugin/global/user_styles` 目录下与插件同名的 css 文件。详情请参考 `./plugin/global/user_styles/README.md`
// 7. html: 用于插入 HTML 标签。返回 Element 类型或表示元素的字符串。
// 8. hotkey: 为 callback 函数注册快捷键。返回一个字符串数组,或者 [{ hotkey: "", callback: () => console.log(123) }] 格式的对象数组
// 9. beforeProcess: 最先执行的函数,用于初始化插件所需的数据或进行前置检查。如果返回 utils.PLUGIN_LOAD_ABORT,则停止加载插件
// 10. process: 在这里添加添加插件业务逻辑
// 11. callback: 当用户点击右键菜单选项或键入快捷键时调用的回调函数。anchorNode 参数表示鼠标光标所在的 Element
// 12. export: 将插件类导出为 plugin