custom:用戶自定義插件,提供開放能力
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 文件,嘗試以下操作:
示例三:實戰
需求如下:
- 在右鍵菜單中添加一個
獲取標題路徑(類似于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

浙公網安備 33010602011771號