diff --git a/plugin/global/core/plugin.js b/plugin/global/core/plugin.js index b84e1b94..46163c96 100644 --- a/plugin/global/core/plugin.js +++ b/plugin/global/core/plugin.js @@ -29,7 +29,7 @@ class IPlugin { /** 一级插件 */ class BasePlugin extends IPlugin { - call(type, meta) {} + call(action, meta) {} } /** 二级插件 */ @@ -77,8 +77,8 @@ const loadPlugin = async (fixedName, setting, isCustom) => { } const LoadPlugins = async (settings, isCustom) => { - const plugins = { enable: {}, disable: {}, stop: {}, error: {}, nosetting: {} }; - await Promise.all(Object.entries(settings).map(async ([fixedName, setting]) => { + const plugins = { enable: {}, disable: {}, stop: {}, error: {}, nosetting: {} } + const promises = Object.entries(settings).map(async ([fixedName, setting]) => { if (!setting) { plugins.nosetting[fixedName] = fixedName; } else if (!setting.ENABLE && !setting.enable) { @@ -96,12 +96,13 @@ const LoadPlugins = async (settings, isCustom) => { plugins.error[fixedName] = error; } } - })) + }) + await Promise.all(promises) // log - const LOG_COLOR = { enable: "32", disable: "33", stop: "34", error: "31", nosetting: "35" }; + const COLORS = { enable: "32", disable: "33", stop: "34", error: "31", nosetting: "35" }; console.group(`${isCustom ? "Custom" : "Base"} Plugin`); - Object.entries(plugins).forEach(([t, p]) => console.debug(`[ \x1B[${LOG_COLOR[t]}m${t}\x1b[0m ] [ ${Object.keys(p).length} ]:`, p)); + Object.entries(plugins).forEach(([t, p]) => console.debug(`[ \x1B[${COLORS[t]}m${t}\x1b[0m ] [ ${Object.keys(p).length} ]:`, p)); console.groupEnd(); return plugins; diff --git a/plugin/global/core/utils/mixin/dialog.js b/plugin/global/core/utils/mixin/dialog.js index 1fe76f25..b5d13be4 100644 --- a/plugin/global/core/utils/mixin/dialog.js +++ b/plugin/global/core/utils/mixin/dialog.js @@ -182,6 +182,9 @@ class dialog { case "span": label = "span"; break + case "blockquote": + label = "blockquote" + break } const class_ = comp.inline ? "form-inline-group" : "form-block-group"; const label_ = comp.label ? `<${label}>${comp.label}${genInfo(comp)}` : ""; diff --git a/plugin/preferences.js b/plugin/preferences.js index 45989c4e..ea53879d 100644 --- a/plugin/preferences.js +++ b/plugin/preferences.js @@ -2,39 +2,32 @@ class preferencesPlugin extends BasePlugin { hotkey = () => [{ hotkey: this.config.HOTKEY, callback: this.call }] getSettings = async () => { - const settings = await this.utils.runtime.readBasePluginSetting(); - const customSettings = await this.utils.runtime.readCustomPluginSetting(); - delete settings.global; - return [settings, customSettings] + const base = await this.utils.runtime.readBasePluginSetting() + const custom = await this.utils.runtime.readCustomPluginSetting() + delete base.global + return [base, custom] } - togglePlugin = async (enablePlugins, enableCustomPlugins, showModal = false) => { - const [settings, customSettings] = await this.getSettings() - - const pluginState = {} - const customPluginState = {} - Object.keys(settings).forEach(fixedName => (pluginState[fixedName] = { ENABLE: enablePlugins.includes(fixedName) })) - Object.keys(customSettings).forEach(fixedName => (customPluginState[fixedName] = { enable: enableCustomPlugins.includes(fixedName) })) - - // check need update file - const settingsHasUpdate = Object.entries(settings).some(([name, plugin]) => plugin.ENABLE !== pluginState[name].ENABLE) - const customSettingsHasUpdate = Object.entries(customSettings).some(([name, plugin]) => plugin.enable !== customPluginState[name].enable) - if (!settingsHasUpdate && !customSettingsHasUpdate) return - - const files = [ - { file: "settings.user.toml", mergeObj: pluginState }, - { file: "custom_plugin.user.toml", mergeObj: customPluginState }, - ] - for (const { file, mergeObj } of files) { - const settingPath = await this.utils.runtime.getActualSettingPath(file) - const settingObj = await this.utils.readTomlFile(settingPath) - const setting = this.utils.merge(settingObj, mergeObj) - const newContent = this.utils.stringifyToml(setting) - const ok = await this.utils.writeFile(settingPath, newContent) - if (!ok) return + togglePlugin = async (enableBasePlugins, enableCustomPlugins) => { + const updateSetting = async (file, setting, enablePlugins, enableKey) => { + const newState = Object.keys(setting).reduce((acc, fixedName) => { + acc[fixedName] = { [enableKey]: enablePlugins.includes(fixedName) } + return acc + }, {}) + const needUpdate = Object.entries(setting).some(([name, plugin]) => plugin[enableKey] !== newState[name][enableKey]) + if (needUpdate) { + const settingPath = await this.utils.runtime.getActualSettingPath(file) + const settingObj = await this.utils.readTomlFile(settingPath) + const mergedSetting = this.utils.merge(settingObj, newState) + const newContent = this.utils.stringifyToml(mergedSetting) + return this.utils.writeFile(settingPath, newContent) + } } - if (showModal) { + const [base, custom] = await this.getSettings() + const baseUpdated = await updateSetting("settings.user.toml", base, enableBasePlugins, "ENABLE") + const customUpdated = await updateSetting("custom_plugin.user.toml", custom, enableCustomPlugins, "enable") + if (baseUpdated || customUpdated) { const option = { type: "info", buttons: ["确定", "取消"], title: "preferences", detail: "配置将于重启 Typora 后生效,确认重启?", message: "设置成功" } const { response } = await this.utils.showMessageBox(option) if (response === 0) { @@ -52,35 +45,34 @@ class preferencesPlugin extends BasePlugin { right_click_menu: "此插件是普通用户调用其他插件的入口", custom: "所有的二级插件都挂载在此插件上,停用会导致所有的二级插件失效", json_rpc: "此插件面向开发者", - ripgrep: "此插件需要您了解 ripgrep 工具", - test: "此插件面向开发者,建议仅在开发插件期间启用", reopenClosedFiles: "此插件依赖「标签页管理」插件", redirectLocalRootUrl: "此插件手动修改配置后才可运行", article_uploader: "此插件面向特殊人群,手动修改配置后才可运行", } - const displayFunc = ([fixedName, plugin]) => ({ + const display = ([fixedName, plugin]) => ({ label: `${plugin.NAME || plugin.name}(${fixedName})`, info: INFO[fixedName], value: fixedName, checked: plugin.ENABLE || plugin.enable, disabled: this.config.IGNORE_PLUGINS.includes(fixedName), }) - const onclick = ev => ev.target.closest("a") && this.utils.runtime.openSettingFolder(); - const [settings, customSettings] = await this.getSettings(); - const plugins = Object.entries(settings).map(displayFunc); - const customPlugins = Object.entries(customSettings).map(displayFunc); + const [base, custom] = await this.getSettings() + const basePlugins = Object.entries(base).map(display) + const customPlugins = Object.entries(custom).map(display) + const onclick = ev => ev.target.closest("a") && this.utils.runtime.openSettingFolder() const components = [ { label: "为了保护用户,此处禁止启停部分插件,如需请 修改配置文件", type: "p", onclick }, - { label: "", legend: "一级插件", type: "checkbox", list: plugins }, + { label: "", legend: "一级插件", type: "checkbox", list: basePlugins }, { label: "", legend: "二级插件", type: "checkbox", list: customPlugins }, - ]; - const { response, submit: [_, p1, p2] } = await this.utils.dialog.modalAsync({ title: "启停插件", width: "450px", components }); + ] + const modal = { title: "启停插件", width: "450px", components } + const { response, submit: [_, _base, _custom] } = await this.utils.dialog.modalAsync(modal) if (response === 1) { - await this.togglePlugin(p1, p2, true); + await this.togglePlugin(_base, _custom) } } } module.exports = { plugin: preferencesPlugin -}; +} diff --git a/plugin/search_multi.js b/plugin/search_multi.js index 0d8a1d8e..5db31db8 100644 --- a/plugin/search_multi.js +++ b/plugin/search_multi.js @@ -39,8 +39,8 @@ class searchMultiKeywordPlugin extends BasePlugin { hotkey = () => [{ hotkey: this.config.HOTKEY, callback: this.call }] init = () => { - this.searchHelper = new SearchHelper(this) - this.highlightHelper = new Highlighter(this) + this.searcher = new Searcher(this) + this.highlighter = new Highlighter(this) this.allowedExtensions = new Set(this.config.ALLOW_EXT.map(ext => ext.toLowerCase())) this.entities = { modal: document.querySelector("#plugin-search-multi"), @@ -55,8 +55,8 @@ class searchMultiKeywordPlugin extends BasePlugin { } process = () => { - this.searchHelper.process() - this.highlightHelper.process() + this.searcher.process() + this.highlighter.process() if (this.config.ALLOW_DRAG) { this.utils.dragFixedModal(this.entities.input, this.entities.modal) } @@ -72,7 +72,7 @@ class searchMultiKeywordPlugin extends BasePlugin { if (!btn) return const action = btn.getAttribute("action") if (action === "searchGrammarModal") { - this.searchHelper.showGrammar() + this.searcher.showGrammar() } else if (action === "toggleCaseSensitive") { btn.classList.toggle("select") this.config.CASE_SENSITIVE = !this.config.CASE_SENSITIVE @@ -122,8 +122,8 @@ class searchMultiKeywordPlugin extends BasePlugin { if (!input) return try { - const ast = this.searchHelper.parse(input) - const explain = this.searchHelper.toExplain(ast) + const ast = this.searcher.parse(input) + const explain = this.searcher.toExplain(ast) this.entities.input.setAttribute("title", explain) this.utils.notification.hide() return ast @@ -139,10 +139,10 @@ class searchMultiKeywordPlugin extends BasePlugin { ast = ast || this.getAST() this.utils.hide(this.entities.highlightResult) if (!ast) return - const tokens = this.searchHelper.getContentTokens(ast).filter(Boolean) + const tokens = this.searcher.getContentTokens(ast).filter(Boolean) if (!tokens || tokens.length === 0) return - const hitGroups = this.highlightHelper.doSearch(tokens) + const hitGroups = this.highlighter.doSearch(tokens) const itemList = Object.entries(hitGroups).map(([cls, { name, hits }]) => { const div = document.createElement("div") div.className = `plugin-highlight-multi-result-item ${cls}` @@ -161,7 +161,7 @@ class searchMultiKeywordPlugin extends BasePlugin { searchMultiByAST = async (rootPath, ast) => { const { fileFilter, dirFilter } = this._getFilter() - const matcher = source => this.searchHelper.match(ast, source) + const matcher = source => this.searcher.match(ast, source) const callback = this._showResultItem(rootPath, matcher) await this._traverseDir(rootPath, fileFilter, dirFilter, callback) } @@ -246,7 +246,7 @@ class searchMultiKeywordPlugin extends BasePlugin { hide = () => { this.utils.hide(this.entities.modal) this.utils.hide(this.entities.info) - this.highlightHelper.clearSearch() + this.highlighter.clearSearch() } show = () => { @@ -376,7 +376,7 @@ class QualifierMixin { * 4. query: Queries the file data to obtain `queryResult`. * 5. match: Matches `castResult` from step 3 with `queryResult` from step 4. */ -class SearchHelper { +class Searcher { constructor(plugin) { this.MIXIN = QualifierMixin this.config = plugin.config @@ -414,6 +414,7 @@ class SearchHelper { default: ({ path, file, stats, buffer }) => `${buffer.toString()}\n${path}`, path: ({ path, file, stats, buffer }) => path, file: ({ path, file, stats, buffer }) => file, + dir: ({ path, file, stats, buffer }) => this.utils.Package.Path.dirname(path), ext: ({ path, file, stats, buffer }) => this.utils.Package.Path.extname(file), content: ({ path, file, stats, buffer }) => buffer.toString(), time: ({ path, file, stats, buffer }) => this.MIXIN.QUERY.toDate(stats.mtime), @@ -440,6 +441,7 @@ class SearchHelper { { scope: "default", name: "内容或路径", is_meta: false, query: QUERY.default }, { scope: "path", name: "路径", is_meta: true, query: QUERY.path }, { scope: "file", name: "文件名", is_meta: true, query: QUERY.file }, + { scope: "dir", name: "所属目录", is_meta: true, query: QUERY.dir }, { scope: "ext", name: "扩展名", is_meta: true, query: QUERY.ext }, { scope: "content", name: "内容", is_meta: false, query: QUERY.content }, { scope: "frontmatter", name: "FrontMatter", is_meta: false, query: QUERY.frontmatter }, @@ -811,8 +813,8 @@ class SearchHelper { const genOperator = (...operators) => operators.map(operator => `${operator}`).join("、") const genUL = (...li) => `` const scopeDesc = genUL( - `文件属性:${genScope(metaScope)}`, - `内容属性:${genScope(contentScope)}`, + `元数据搜索符:${genScope(metaScope)}`, + `内容搜索符:${genScope(contentScope)}`, `默认值 default = path + content(路径+文件内容)`, ) const operatorDesc = genUL( @@ -823,18 +825,18 @@ class SearchHelper { const genInfo = title => `` const scopeInfo = genInfo('具体来说:文件路径或文件内容包含 pear') - const diffInfo = genInfo('注意区分:\n「head=plugin」表示标题为plugin,当标题为”typora plugin“时不可匹配\n「head:plugin」表示标题包含plugin,当标题为”typora plugin“时可以匹配') + const diffInfo = genInfo('注意区分:\nhead=plugin 表示标题为plugin,当标题为”typora plugin“时不可匹配\nhead:plugin 表示标题包含plugin,当标题为”typora plugin“时可以匹配') const keywordDesc = ` - - - - - - - + + + + + + +
关键字说明
空格表示与。文档应该同时满足空格左右两侧的查询条件,等价于 AND
|表示或。文档应该满足 | 左右两侧中至少一个查询条件,等价于 OR
-表示非。文档不可满足 - 右侧的查询条件
""表示词组。引号包裹视为词组
/RegExp/JavaScript 风格的正则表达式
scope查询属性,用于限定查询条件${scopeDesc}
operator操作符,用于比较查询条件和查询结果${operatorDesc}
空格连接两个查询条件,表示逻辑与。文档应该同时满足空格左右两侧的查询条件,等价于 AND
|连接两个查询条件,表示逻辑或。文档应该满足 | 左右两侧中至少一个查询条件,等价于 OR
-后接一个查询条件,表示逻辑非。文档不可满足 - 右侧的查询条件
""引号包裹文本,表示词组。
/regex/JavaScript 风格的正则表达式
scope搜索符,用于限定查询条件${scopeDesc}
operator操作符,用于比较查询关键字和查询结果${operatorDesc}
()小括号,用于调整运算优先级
` @@ -848,9 +850,9 @@ class SearchHelper { sour pear -apple包含 sour 和 pear,且不含 apple /\\bsour\\b/ pear time=2024-03-12匹配正则\\bsour\\b(全字匹配sour),且包含 pear,且文件更新时间为 2024-03-12 frontmatter:开发 | head=plugin | strong:MITYAML Front Matter 包含开发 或者 标题内容为 plugin 或者 加粗文字包含 MIT ${diffInfo} - size>10k (linenum>=1000 | hasimage=true)文件大小超过 10KB,并且文件要么至少有 1000 行,要么包含图片 + size>10kb (linenum>=1000 | hasimage=true)文件大小超过 10KB,并且文件要么至少有 1000 行,要么包含图片 path:(info | warn | err) -ext:md文件路径包含 info 或 warn 或 err,且扩展名不含 md - file:/[a-z]{3}/ content:prometheus blockcode:"kubectl apply"文件名匹配正则 [a-z]{3},且内容包含 prometheus,且代码块内容含有 kubectl apply + file:/[a-z]{3}/ content:prometheus blockcode:"kubectl apply"文件名匹配正则 [a-z]{3}(包含三个小写字母),且文件内容包含 prometheus,且代码块内容包含 kubectl apply ` const content = ` @@ -859,20 +861,25 @@ class SearchHelper { ::= ( )* ::= ? ::= - ::= | '"''"' | '/''/' | '('')' + ::= | '"''"' | '/''/' | '('')' ::= | ::= 'OR' | '|' ::= 'AND' | ' ' ::= '-' ::= [^\\s"()|]+ - ::= [^/]+ + ::= [^/]+ ::= ${operator.map(s => `'${s}'`).join(" | ")} ::= ${[...metaScope, ...contentScope].map(s => `'${s.scope}'`).join(" | ")}` + const desc = `高级搜索通过组合不同的条件来精确查找文件。每个条件由三部分组成:搜索符、操作符、关键字,例如 size>2kb(含义:文件尺寸大于 2KB)、ext:txt(含义:文件扩展名包含 txt)。 +条件之间用空格分隔,表示所有条件都必须满足,例如 size>2kb ext:txt;如果只需满足其一条件,请使用 OR 连接,例如 size>2kb OR ext:txt;如果需满足前者,并且排除后者,请使用 - 连接,例如 size>2kb -ext:txt` const components = [ - { label: keywordDesc, type: "p" }, + { label: desc, type: "blockquote" }, { label: example, type: "p" }, - { label: "", type: "textarea", rows: 20, content, title: "这段文字是语法的形式化表述,你可以把它塞给AI,AI会为你解释" }, + { label: "具体用法", type: "p" }, + { label: keywordDesc, type: "p" }, + { label: "形式文法", type: "p" }, + { label: "", type: "textarea", rows: 20, content }, ] this.utils.dialog.modal({ title: "高级搜索", width: "600px", components }) }