custom 插件的功能:支持用户 在右键菜单中自定义插件。
简介
custom 插件大量采用声明式代码(声明代替代码开发),比如:
- 只需使用
style = () => "..."
,即可注册 css。 - 只需使用
styleTemplate = () => ({renderArg})
,即可引入 css 文件,并且支持渲染模板。 - 只需使用
html = () => "..."
,即可注册 HTML 元素。 - 只需使用
hint = () => "将当前标题的路径复制到剪切板"
,即可注册 hint。 - 只需使用
selector = () => "..."
,即可注册允许运行命令的光标位置。 - 只需使用
hotkey = () => ["ctrl+shift+y"]
,即可注册快捷键。 - 只需使用
this.modal
函数即可自动生成自定义的模态框。 - init、html、process、callback 等等生命周期函数。
class fullPathCopy extends BaseCustomPlugin {
style = () => "..."
html = () => "..."
hint = () => "将当前标题的路径复制到剪切板"
hotkey = () => ["ctrl+shift+y"]
callback = anchorNode => {
const modal = {
title: "这是模态框标题",
components: [
{ label: "input的label", type: "input", value: "input的默认value", placeholder: "nput的placeholder" },
// password、textarea、checkbox、radio、select
...
]
};
const callback = response => console.log(response)
this.modal(modal, callback)
}
}
如何使用
仅需两步:
- 在
./plugin/custom/custom_plugin.user.toml
添加配置。 - 在
./plugin/custom/plugins
目录下,创建和插件同名的 js 文件,在此文件中创建一个 class 继承自 BaseCustomPlugin,并导出为plugin
。
示例一:快速开始
您可以根据下面的步骤,先把插件跑起来。
步骤一:在 ./plugin/custom/custom_plugin.user.toml
添加如下配置:
[helloWorld]
name = "你好世界" # 右键菜单中展示的名称
enable = true # 是否启用此二级插件
hide = false # 是否在右键菜单中隐藏
order = 1 # 在右键菜单中的出现顺序(越大越排到后面,允许负数)
[helloWorld.config]
hotkey_string = "ctrl+alt+u"
console_message = "i am in process"
show_message = "this is hello world plugin"
如果您对 TOML 不太了解,可以花三分钟了解 TOML 教程
步骤二:创建文件 ./plugin/custom/plugins/helloWorld.js
文件,将下面代码保存到该文件中:
// ./plugin/custom/plugins/helloWorld.js
class helloWorld extends BaseCustomPlugin {
hint = () => "this is hello world hint"
hotkey = () => [this.config.hotkey_string]
// process方法会在插件初始化时自动运行
process = () => {
console.log(this.config.console_message);
console.log("[helloWorldPlugin]:", this);
}
// callback方法会在插件被调用时运行,参数anchorNode:调用此插件时,鼠标光标所在的Element
callback = anchorNode => {
alert(this.config.show_message);
}
}
module.exports = { plugin: helloWorld };
步骤三:验证,完成以上步骤后,重启 Typora。
- 打开【检查元素】,发现控制台输出了
i am in process
和 插件对象。你可以仔细看看 this 自带的属性和方法。 - 鼠标右键,弹出菜单,鼠标悬停在
常用插件 -> 二级插件 -> 你好世界
。发现出现了 hint,显示this is hello world hint
。点击你好世界
,发现弹出了提示框,显示this is hello world plugin
。 - 键入快捷键
ctrl+alt+u
,发现弹出了同样的提示框。
示例二:简易功能
此示例来自 此 issue,简易实现了一个轻量化的需求,旨在让用户了解如何实现一个插件。
需求:md 文档中有几百行的复选框(任务列表)内容,一个个点选中或取消,效率太低了。希望开发全选、反选复选框功能的插件。
步骤一:打开文件 plugin\global\settings\custom_plugin.user.toml
,添加下面内容:
[selectCheckboxes]
name = "反选复选框" # 右键菜单中展示的名称
enable = true # 是否启用此二级插件
hide = false # 是否在右键菜单中隐藏
order = 1 # 在右键菜单中的出现顺序(越大越排到后面,允许负数)
[selectCheckboxes.config]
select_all_hotkey = "ctrl+alt+h" # 全选快捷键
select_none_hotkey = "ctrl+alt+j" # 取消全部选择快捷键
select_reverse_hotkey = "ctrl+alt+k" # 反选快捷键
步骤二:打开目录 plugin\custom\plugins
,在此目录下创建文件 selectCheckboxes.js
,写入如下内容:
class selectCheckboxes extends BaseCustomPlugin {
rangeTaskListItem = fn => document.querySelectorAll('.md-task-list-item input[type="checkbox"]').forEach(fn)
selectAll = input => !input.checked && input.click()
selectNone = input => input.checked && input.click()
selectReverse = input => input.click()
process = () => {
const { select_all_hotkey, select_none_hotkey, select_reverse_hotkey } = this.config;
this.utils.registerHotkey([
{ hotkey: select_all_hotkey, callback: () => this.rangeTaskListItem(this.selectAll) },
{ hotkey: select_none_hotkey, callback: () => this.rangeTaskListItem(this.selectNone) },
{ hotkey: select_reverse_hotkey, callback: () => this.rangeTaskListItem(this.selectReverse) },
])
}
callback = () => this.rangeTaskListItem(this.selectReverse)
}
module.exports = { plugin: selectCheckboxes };
步骤三:重启 Typora,创建一个带有任务列表的 md 文件,尝试以下操作:
- ctrl+alt+h:全选
- ctrl+alt+j:取消全部选择
- ctrl+alt+k:反选
示例三:实战
需求如下:
- 在右键菜单中添加一个
获取标题路径
(类似于messing.md\无 一级标题\开放平台 二级标题\window_tab 三级标题
),然后将其写入剪切板。 - 当光标位于【正文标题】中才可使用。
- 快捷键
ctrl+shift+u
。
实现:
步骤一:修改 ./plugin/global/settings/custom_plugin.user.toml
,添加配置:
- name:(必选)右键菜单中展示的名称
- enable:(必选)是否启用此插件
- hide:(可选)是否在右键菜单中隐藏,默认为 false
- order:(可选)在右键菜单中的出现顺序(越大越排到后面,允许负数),默认为 1
- config:(可选)插件自己的配置,这里的内容将被封装为
插件类的 this.config 属性
# ./plugin/global/settings/custom_plugin.user.toml
[myFullPathCopy]
name = "我的复制标题路径插件"
enable = true
hide = false
order = 1
# 插件配置
[myFullPathCopy.config]
# 快捷键
hotkey = "ctrl+shift+u"
# 如果在空白页调用此插件,使用的文件名(因为尚不存在该文件,需要用一个默认的文件名代替)
untitled_file_name = "untitled"
# 跳过空白的标题
ignore_empty_header = false
# 标题和提示之前添加空格
add_space = true
# 使用绝对路径
full_file_path = false
步骤二:在 ./plugin/custom/plugins
目录下,创建和插件同名的 js 文件(myFullPathCopy.js
),在此文件中创建一个 class 继承自 BaseCustomPlugin,并导出为 plugin
。
// ./plugin/custom/plugins/myFullPathCopy.js
// 1
class myFullPathCopy extends BaseCustomPlugin {
// 2
selector = () => "#write h1, h2, h3, h4, h5, h6"
// 3
hint = () => "将当前标题的路径复制到剪切板"
// 4
init = () => {}
// 5
style = () => {}
// 6
styleTemplate = () => {}
// 7
html = () => {}
// 8
hotkey = () => [this.config.hotkey]
// 9
beforeProcess = async () => {}
// 10
process = () => {}
// 11
callback = anchorNode => {
const paragraphList = ["H1", "H2", "H3", "H4", "H5", "H6"];
const nameList = ["一级标题", "二级标题", "三级标题", "四级标题", "五级标题", "六级标题"];
const pList = [];
let ele = anchorNode;
while (ele) {
const idx = paragraphList.indexOf(ele.tagName);
if (idx !== -1) {
if (pList.length === 0 || (pList[pList.length - 1].idx > idx)) {
pList.push({ele, idx})
if (pList[pList.length - 1].idx === 0) break;
}
}
ele = ele.previousElementSibling;
}
pList.reverse();
let filePath = (this.config.full_file_path) ? this.utils.getFilePath() : File.getFileName();
filePath = filePath || this.config.untitled_file_name;
const result = [filePath];
let headerIdx = 0;
for (const p of pList) {
while (headerIdx < 6 && p.ele.tagName !== paragraphList[headerIdx]) {
if (!this.config.ignore_empty_header) {
const name = this.getHeaderName("无", nameList[headerIdx]);
result.push(name);
}
headerIdx++;
}
if (p.ele.tagName === paragraphList[headerIdx]) {
const name = this.getHeaderName(p.ele.querySelector("span").textContent, nameList[headerIdx]);
result.push(name);
headerIdx++;
}
}
const text = this.utils.Package.Path.join(...result);
navigator.clipboard.writeText(text);
}
getHeaderName = (title, name) => {
const space = this.config.add_space ? " " : "";
return title + space + name
}
}
// 12
module.exports = { plugin: myFullPathCopy };
// 1. 创建 class,继承 BaseCustomPlugin 类。之后 myFullPathCopy 将自动拥有 utils、info、config 属性。
// - utils:插件系统自带的静态工具类,其定义在 `./plugin/global/core/plugin.js/utils`。其中有三个重要的函数:utils.getPlugin(fixedName) 和 utils.getCustomPlugin(fixedName) 用于获取已经实现的全部插件,调用其 API,具体的 API 可看 ./plugin/custom/openPlatformAPI.md 文件。utils.addEventListener(eventType, listener) 用于监听 Typora 的生命周期事件。
// - info:该插件在 custom_plugin.user.toml 里的所有字段
// - config:等同于 info.config,即配置文件里的 config 属性
// 2. selector:允许运行命令的光标位置:当光标位于哪些位置时,此命令才可用。返回 null-like value 表示任何位置都可用,在这里的含义就是:只当光标位于【正文标题】时可用
// 3. hint:当鼠标移动到右键菜单时的提示
// 4. init:在这里初始化你要的变量
// 5. style:给 Typora 插入 style 标签。返回值为 string。若你想指定标签的 id,也可以返回 {textID: "", text: ""}。其中 textID 为此 style 标签的 id,text 为 style 内容。
// 6. styleTemplate: 引入 `./plugin/global/user_styles` 目录下和插件同名的 css 文件。详情请参考`user_styles/请读我.md`
// 7. html:为 Typora 插入 HTML 标签,返回 Element 类型或者 Element-string。
// 8. hotkey:为 callabck 注册快捷键,返回 Array<string> 类型。
// 9. beforeProcess:最先执行的函数,在这里初始化插件需要的数据。若返回 this.utils.stopLoadPluginError,则停止加载插件
// 10. process:在这里添加添加插件业务逻辑,比如添加 listener 和修改 Typora 的第一方函数
// 11. callback:右键菜单中点击/键入快捷键后的回调函数。anchorNode 参数: 鼠标光标所在的 Element
// 12. export:导出名为 plugin
验证:
打开 Typora,将光标置于正文标题出,右键弹出菜单,常用插件 -> 二级插件 -> 复制标题路径
,点击。当前的标题路径就写入剪切板了。在目标文档区域,粘贴,即可把标题路径复制到相应文档区域。
示例四:参考
如果您有心自己写插件,可以参考 noImageMode 插件,仅有 30+ 行代码。
./plugin/custom/plugins/noImageMode.js