├── .eslintignore ├── scripts ├── .gitignore ├── elevate.ps1 ├── make_install.js ├── make_dev_link.js ├── update_version.js └── utils.js ├── icon.png ├── preview.png ├── asset └── action.png ├── src ├── assets │ └── imgs │ │ ├── sorting-menu.png │ │ ├── sorting-menu-en.png │ │ ├── search-in-document.png │ │ └── click-result-positioning.gif ├── lib │ ├── siyuan │ │ ├── functions.ts │ │ ├── menus │ │ │ ├── search.ts │ │ │ ├── commonMenuItem.ts │ │ │ └── Menu.ts │ │ ├── util │ │ │ └── selection.ts │ │ ├── protyle │ │ │ ├── toolbar │ │ │ │ └── util.ts │ │ │ └── util │ │ │ │ └── compatibility.ts │ │ └── hasClosest.ts │ └── SearchUtil.ts ├── config │ ├── document-model.ts │ ├── search-model.ts │ ├── env-config.ts │ ├── setting-constant.ts │ └── icon-constant.ts ├── utils │ ├── number-util.ts │ ├── Instance.ts │ ├── object-util.ts │ ├── siyuan-util.ts │ ├── array-util.ts │ ├── string-util.ts │ ├── datetime-util.ts │ ├── html-util.ts │ ├── icon-util.ts │ └── api.ts ├── index.scss ├── types │ ├── setting.d.ts │ ├── api.d.ts │ └── index.d.ts ├── components │ ├── dock │ │ ├── doc-search-dock.svelte │ │ ├── flat-doc-tree-dock.svelte │ │ └── dock-util.ts │ ├── setting │ │ ├── setting-attr.svelte │ │ ├── setting-type.svelte │ │ ├── setting-flat-doc-tree.svelte │ │ ├── setting-util.ts │ │ ├── setting-notebook.svelte │ │ ├── setting-dock.svelte │ │ ├── setting-hub.svelte │ │ └── setting-other.svelte │ ├── doc-tree │ │ └── doc-tree-util.ts │ └── search │ │ └── search-result-item.svelte ├── i18n │ ├── zh_CN.json │ └── en_US.json ├── index.ts └── services │ └── setting-config.ts ├── .gitignore ├── tsconfig.node.json ├── plugin.json ├── svelte.config.js ├── LICENSE ├── .eslintrc.cjs ├── development_log └── 20240319-相关度降序,sql排序和ts排序测试.md ├── package.json ├── tsconfig.json ├── .github └── workflows │ └── release.yml ├── CHANGELOG_zh_CN.md ├── README_zh_CN.md ├── vite.config.ts └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build 3 | dist 4 | *.exe 5 | *.spec 6 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/icon.png -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/preview.png -------------------------------------------------------------------------------- /asset/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/asset/action.png -------------------------------------------------------------------------------- /src/assets/imgs/sorting-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/src/assets/imgs/sorting-menu.png -------------------------------------------------------------------------------- /src/assets/imgs/sorting-menu-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/src/assets/imgs/sorting-menu-en.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | pnpm-lock.yaml 5 | package.zip 6 | node_modules 7 | dev 8 | dist 9 | build 10 | tmp 11 | -------------------------------------------------------------------------------- /src/assets/imgs/search-in-document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/src/assets/imgs/search-in-document.png -------------------------------------------------------------------------------- /src/lib/siyuan/functions.ts: -------------------------------------------------------------------------------- 1 | export const isTouchDevice = () => { 2 | return ("ontouchstart" in window) && navigator.maxTouchPoints > 1; 3 | }; 4 | -------------------------------------------------------------------------------- /src/assets/imgs/click-result-positioning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Misuzu2027/syplugin-document-search/HEAD/src/assets/imgs/click-result-positioning.gif -------------------------------------------------------------------------------- /src/config/document-model.ts: -------------------------------------------------------------------------------- 1 | 2 | export class DocumentTreeItemInfo { 3 | block: Block; 4 | fileName: string; 5 | filePath: string; 6 | ariaLabel: string; 7 | icon: string; 8 | boxName: string; 9 | refCount: number; 10 | index: number; 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/utils/number-util.ts: -------------------------------------------------------------------------------- 1 | export function isNumberValid(value: any): boolean { 2 | // 判断值是否存在且为数字 3 | return value !== null && value !== undefined && typeof value === 'number' && !isNaN(value); 4 | } 5 | export function isNumberNotValid(value: any): boolean { 6 | return !isNumberValid(value); 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/utils/Instance.ts: -------------------------------------------------------------------------------- 1 | export type IClazz = new (...param: any[]) => T; 2 | 3 | export default class Instance { 4 | 5 | public static get(clazz: IClazz, ...param: any[]): T { 6 | if (clazz["__Instance__"] == null) { 7 | clazz["__Instance__"] = new clazz(...param); 8 | } 9 | return clazz["__Instance__"]; 10 | } 11 | } -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | .document-search-plugin__area .block__icon--show.block__icon.disabled { 2 | opacity: 0.38; 3 | cursor: not-allowed; 4 | } 5 | 6 | ::highlight(search-result-mark) { 7 | background-color: var(--b3-protyle-inline-mark-background); 8 | color: var(--b3-protyle-inline-mark-color); 9 | } 10 | 11 | ::highlight(search-result-focus) { 12 | background-color: var(--b3-theme-primary-lighter); 13 | color: var(--b3-protyle-inline-mark-color); 14 | } -------------------------------------------------------------------------------- /src/lib/siyuan/menus/search.ts: -------------------------------------------------------------------------------- 1 | import {MenuItem} from "./Menu"; 2 | import {copySubMenu} from "./commonMenuItem"; 3 | 4 | export const initSearchMenu = (id: string) => { 5 | window.siyuan.menus.menu.remove(); 6 | window.siyuan.menus.menu.append(new MenuItem({ 7 | id: "copy", 8 | icon: "iconCopy", 9 | label: window.siyuan.languages.copy, 10 | type: "submenu", 11 | submenu: copySubMenu([id]) 12 | }).element); 13 | return window.siyuan.menus.menu; 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/siyuan/util/selection.ts: -------------------------------------------------------------------------------- 1 | export const focusByRange = (range: Range) => { 2 | if (!range) { 3 | return; 4 | } 5 | 6 | const startNode = range.startContainer.childNodes[range.startOffset] as HTMLElement; 7 | if (startNode && startNode.nodeType !== 3 && ["INPUT", "TEXTAREA"].includes(startNode.tagName)) { 8 | startNode.focus(); 9 | return; 10 | } 11 | const selection = window.getSelection(); 12 | selection.removeAllRanges(); 13 | selection.addRange(range); 14 | }; -------------------------------------------------------------------------------- /src/types/setting.d.ts: -------------------------------------------------------------------------------- 1 | type SettingDialogType = 2 | | "settingNotebook" // 笔记本 3 | | "settingType" // 类型 4 | | "settingAttr" // 属性 5 | | "settingOther" // 其他 6 | | "settingHub" 7 | ; 8 | 9 | type DocumentSortMethod = 10 | | "modifiedAsc" 11 | | "modifiedDesc" 12 | | "createdAsc" 13 | | "createdDesc" 14 | | "rankAsc" 15 | | "rankDesc" 16 | | "refCountAsc" 17 | | "refCountDesc" 18 | | "alphabeticAsc" 19 | | "alphabeticDesc" 20 | ; 21 | 22 | type ContentBlockSortMethod = 23 | | "type" 24 | | "content" 25 | | "typeAndContent" 26 | | DocumentSortMethod 27 | ; 28 | 29 | 30 | type ClickMode = 31 | | "click" 32 | | "doubleClick" 33 | ; -------------------------------------------------------------------------------- /src/utils/object-util.ts: -------------------------------------------------------------------------------- 1 | export function getObjectSizeInKB(obj: any): number { 2 | try { 3 | // 将 JSON 对象转换为字符串 4 | const jsonString = JSON.stringify(obj); 5 | 6 | // 计算字符串的字节数 7 | const bytes = new Blob([jsonString]).size; 8 | 9 | // 将字节数转换为 KB 10 | const kilobytes = bytes / 1024; 11 | return kilobytes; 12 | } catch (err) { 13 | console.log("计算对象大小报错") 14 | } 15 | return 0; 16 | } 17 | 18 | 19 | export function isBoolean(value: any): value is boolean { 20 | return typeof value === 'boolean'; 21 | } 22 | 23 | export function isObject(value: any): value is object { 24 | return typeof value === 'object' && value !== null && !Array.isArray(value); 25 | } 26 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syplugin-document-search", 3 | "author": "Misuzu2027", 4 | "url": "https://github.com/Misuzu2027/syplugin-document-search", 5 | "version": "0.11.3", 6 | "minAppVersion": "2.10.14", 7 | "backends": [ 8 | "all" 9 | ], 10 | "frontends": [ 11 | "all" 12 | ], 13 | "displayName": { 14 | "default": "基于文档搜索", 15 | "en_US": "Document Search", 16 | "zh_CN": "基于文档搜索" 17 | }, 18 | "description": { 19 | "en_US": "This plugin operates differently from the built-in block-level queries of the system. It conducts searches on a document basis.", 20 | "zh_CN": "这个插件与系统内置的块级别查询不同,它以文档为单位进行查询。" 21 | }, 22 | "readme": { 23 | "default": "README_zh_CN.md", 24 | "en_US": "README.md", 25 | "zh_CN": "README_zh_CN.md" 26 | }, 27 | "keywords": [ 28 | "document", 29 | "search", 30 | "query", 31 | "文档", 32 | "查询", 33 | "搜索" 34 | ] 35 | } -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2023-05-19 19:49:13 5 | * @FilePath : /svelte.config.js 6 | * @LastEditTime : 2024-04-19 19:01:55 7 | * @Description : 8 | */ 9 | import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" 10 | 11 | const NoWarns = new Set([ 12 | "a11y-click-events-have-key-events", 13 | "a11y-no-static-element-interactions", 14 | "a11y-no-noninteractive-element-interactions" 15 | ]); 16 | 17 | export default { 18 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 19 | // for more information about preprocessors 20 | preprocess: vitePreprocess(), 21 | onwarn: (warning, handler) => { 22 | // suppress warnings on `vite dev` and `vite build`; but even without this, things still work 23 | if (NoWarns.has(warning.code)) return; 24 | handler(warning); 25 | } 26 | } -------------------------------------------------------------------------------- /scripts/elevate.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 by frostime. All Rights Reserved. 2 | # @Author : frostime 3 | # @Date : 2024-09-06 19:15:53 4 | # @FilePath : /scripts/elevate.ps1 5 | # @LastEditTime : 2024-09-06 19:39:13 6 | # @Description : Force to elevate the script to admin privilege. 7 | 8 | param ( 9 | [string]$scriptPath 10 | ) 11 | 12 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 13 | $projectDir = Split-Path -Parent $scriptDir 14 | 15 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 16 | $args = "-NoProfile -ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`" -scriptPath `"" + $scriptPath + "`"" 17 | Start-Process powershell.exe -Verb RunAs -ArgumentList $args -WorkingDirectory $projectDir 18 | exit 19 | } 20 | 21 | Set-Location -Path $projectDir 22 | & node $scriptPath 23 | 24 | pause -------------------------------------------------------------------------------- /src/components/dock/doc-search-dock.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | {#if isMobile} 18 |
19 | 22 |
23 | {EnvConfig.ins.i18n.documentBasedSearch} 24 |
25 |
26 |
27 | 28 |
29 | {:else} 30 | 31 | {/if} 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SiYuan 思源笔记 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/dock/flat-doc-tree-dock.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | {#if isMobile} 19 |
20 | 23 |
{EnvConfig.ins.i18n.flatDocumentTree}
24 |
25 |
26 | 27 |
28 | {:else} 29 |
30 | 31 |
32 | {/if} 33 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:svelte/recommended", 6 | "turbo", 7 | "prettier", 8 | ], 9 | 10 | parser: "@typescript-eslint/parser", 11 | 12 | overrides: [ 13 | { 14 | files: ["*.svelte"], 15 | parser: "svelte-eslint-parser", 16 | // Parse the script in `.svelte` as TypeScript by adding the following configuration. 17 | parserOptions: { 18 | parser: "@typescript-eslint/parser", 19 | }, 20 | }, 21 | ], 22 | 23 | plugins: ["@typescript-eslint", "prettier"], 24 | 25 | rules: { 26 | // Note: you must disable the base rule as it can report incorrect errors 27 | semi: "off", 28 | quotes: "off", 29 | "no-undef": "off", 30 | "@typescript-eslint/no-var-requires": "off", 31 | "@typescript-eslint/no-this-alias": "off", 32 | "@typescript-eslint/no-non-null-assertion": "off", 33 | "@typescript-eslint/no-unused-vars": "off", 34 | "@typescript-eslint/no-explicit-any": "off", 35 | "turbo/no-undeclared-env-vars": "off", 36 | "prettier/prettier": "error", 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /development_log/20240319-相关度降序,sql排序和ts排序测试.md: -------------------------------------------------------------------------------- 1 | # 2024-03-19:相关度降序,sql排序和ts排序测试 2 | ## 前提:相关度降序, 3 | ### 测试查询文本:`魔法 禁书 目录 你 我 的` 4 | #### sql 排序 5 | * 获取和处理搜索结果消耗时间 : 2143.300000011921 ms, 内容大小 : 21728.8818359375 6 | * 获取和处理搜索结果消耗时间 : 2172.600000023842 ms, 内容大小 : 21728.8818359375 7 | * 获取和处理搜索结果消耗时间 : 2156.599999964237 ms, 内容大小 : 21728.8818359375 8 | #### ts 排序 9 | * 获取和处理搜索结果消耗时间 : 2330.800000011921 ms, 内容大小 : 21728.8818359375 10 | * 获取和处理搜索结果消耗时间 : 2498.400000035763 ms, 内容大小 : 21728.8818359375 11 | * 获取和处理搜索结果消耗时间 : 2245.900000035763 ms, 内容大小 : 21728.8818359375 12 | 13 | 14 | ### 换一个数据量较小的测试:`魔法 禁书 御坂 美琴` 15 | #### sql 排序 16 | * 获取和处理搜索结果消耗时间 : 1163.300000011921 ms, 内容大小 : 2953.1953125 17 | * 获取和处理搜索结果消耗时间 : 1290.2000000476837 ms, 内容大小 : 2953.1953125 18 | * 获取和处理搜索结果消耗时间 : 1187.4000000357628 ms, 内容大小 : 2953.1953125 19 | #### ts 排序 20 | * 获取和处理搜索结果消耗时间 : 1154.8999999761581 ms, 内容大小 : 2953.1953125 21 | * 获取和处理搜索结果消耗时间 : 1202 ms, 内容大小 : 2953.1953125 22 | * 获取和处理搜索结果消耗时间 : 1430.6000000238419 ms, 内容大小 : 2953.1953125 23 | 24 | ## 结论 25 | sql 排序效率更高。不过目前的 sql 相关度排序有一些无法解决的缺陷,sql 只能匹配不同关键字数量来升序降序;而 ts 是所有匹配关键字的数量来升序降序。 26 | * 比如关键字 "a p" 27 | * 在这段文本中 "apple":sql 是2次,ts 是3次。 28 | * "banana":sql 是1次,ts 是3次。 29 | 30 | -------------------------------------------------------------------------------- /src/config/search-model.ts: -------------------------------------------------------------------------------- 1 | import { DocumentQueryCriteria } from "@/services/search-sql"; 2 | 3 | export interface CompareCondition { 4 | operator: string; 5 | value: string; // YYMMDDhhmmss格式 6 | } 7 | 8 | export interface BlockKeywordCondition { 9 | type?: BlockType; 10 | subType?: BlockSubType; 11 | include: string[]; 12 | exclude: string[]; 13 | } 14 | 15 | export interface BlockCriteria { 16 | blockKeyWordConditionArray: BlockKeywordCondition[]; 17 | notebook: { include: string[]; exclude: string[] }; 18 | path: { include: string[]; exclude: string[] }; 19 | createdTimeArray: CompareCondition[]; 20 | updatedTimeArray: CompareCondition[]; 21 | } 22 | 23 | export class DocumentItem { 24 | block: Block; 25 | subItems: BlockItem[]; 26 | isCollapsed: boolean; 27 | icon: string; 28 | index: number; 29 | path: string; 30 | ariaLabel: string; 31 | } 32 | 33 | export class BlockItem { 34 | block: Block; 35 | icon: string; 36 | index: number; 37 | } 38 | 39 | 40 | export class DocumentSqlQueryModel { 41 | searchCriterion: DocumentQueryCriteria; 42 | documentItems: DocumentItem[]; 43 | documentCount: number; 44 | status: "success" | "param_null"; 45 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "syplugin-document-search", 3 | "version": "0.10.3", 4 | "type": "module", 5 | "description": "document search plugin", 6 | "repository": "", 7 | "homepage": "", 8 | "author": "", 9 | "license": "MIT", 10 | "scripts": { 11 | "make-link": "node --no-warnings ./scripts/make_dev_link.js", 12 | "make-link-win": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/elevate.ps1 -scriptPath ./scripts/make_dev_link.js", 13 | "dev": "vite build --watch", 14 | "update-version": "node --no-warnings ./scripts/update_version.js", 15 | "build": "vite build", 16 | "make-install": "vite build && node --no-warnings ./scripts/make_install.js" 17 | }, 18 | "devDependencies": { 19 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 20 | "@tsconfig/svelte": "^4.0.1", 21 | "@types/node": "^20.3.0", 22 | "fast-glob": "^3.2.12", 23 | "glob": "^7.2.3", 24 | "js-yaml": "^4.1.0", 25 | "minimist": "^1.2.8", 26 | "rollup-plugin-livereload": "^2.0.5", 27 | "sass": "^1.63.3", 28 | "siyuan": "1.1.1", 29 | "svelte": "^4.2.0", 30 | "ts-node": "^10.9.1", 31 | "typescript": "^5.1.3", 32 | "vite": "^5.0.0", 33 | "vite-plugin-static-copy": "^1.0.2", 34 | "vite-plugin-zip-pack": "^1.0.5" 35 | } 36 | } -------------------------------------------------------------------------------- /src/utils/siyuan-util.ts: -------------------------------------------------------------------------------- 1 | export function getActiveTab(): HTMLDivElement { 2 | let tab = document.querySelector( 3 | "div.layout__wnd--active ul.layout-tab-bar>li.item--focus", 4 | ); 5 | let dataId: string = tab?.getAttribute("data-id"); 6 | if (!dataId) { 7 | return null; 8 | } 9 | const activeTab: HTMLDivElement = document.querySelector( 10 | `.layout-tab-container.fn__flex-1>div.protyle[data-id="${dataId}"]`, 11 | ) as HTMLDivElement; 12 | return activeTab; 13 | } 14 | 15 | export function determineOpenTabPosition( 16 | event: MouseEvent, 17 | ): "right" | "bottom" | null { 18 | if (!event.ctrlKey && event.altKey && !event.shiftKey) { 19 | return "right"; 20 | } 21 | if (!event.ctrlKey && !event.altKey && event.shiftKey) { 22 | return "bottom"; 23 | } 24 | return null; 25 | } 26 | 27 | 28 | export function clearSyFileTreeItemFocus() { 29 | document 30 | .querySelector("div.file-tree.sy__file") 31 | .querySelectorAll("li.b3-list-item--focus") 32 | .forEach((liItem) => { 33 | liItem.classList.remove("b3-list-item--focus"); 34 | }); 35 | } 36 | 37 | export const isTouchDevice = () => { 38 | return ("ontouchstart" in window) && navigator.maxTouchPoints > 1; 39 | }; 40 | -------------------------------------------------------------------------------- /src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | interface IResGetNotebookConf { 2 | box: string; 3 | conf: NotebookConf; 4 | name: string; 5 | } 6 | 7 | interface IReslsNotebooks { 8 | notebooks: Notebook[]; 9 | } 10 | 11 | interface IResUpload { 12 | errFiles: string[]; 13 | succMap: { [key: string]: string }; 14 | } 15 | 16 | interface IResdoOperations { 17 | doOperations: doOperation[]; 18 | undoOperations: doOperation[] | null; 19 | } 20 | 21 | interface IResGetBlockKramdown { 22 | id: BlockId; 23 | kramdown: string; 24 | } 25 | 26 | interface IResGetChildBlock { 27 | id: BlockId; 28 | type: BlockType; 29 | subtype?: BlockSubType; 30 | } 31 | 32 | interface IResGetTemplates { 33 | content: string; 34 | path: string; 35 | } 36 | 37 | interface IResReadDir { 38 | isDir: boolean; 39 | isSymlink: boolean; 40 | name: string; 41 | } 42 | 43 | interface IResExportMdContent { 44 | hPath: string; 45 | content: string; 46 | } 47 | 48 | interface IResBootProgress { 49 | progress: number; 50 | details: string; 51 | } 52 | 53 | interface IResForwardProxy { 54 | body: string; 55 | contentType: string; 56 | elapsed: number; 57 | headers: { [key: string]: string }; 58 | status: number; 59 | url: string; 60 | } 61 | 62 | interface IResExportResources { 63 | path: string; 64 | } 65 | 66 | 67 | interface IResCheckBlockFold{ 68 | isFolded: boolean; 69 | isRoot: boolean; 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/siyuan/protyle/toolbar/util.ts: -------------------------------------------------------------------------------- 1 | import { fetchSyncPost } from "siyuan"; 2 | import { writeText } from "../util/compatibility"; 3 | // import { writeText } from "siyuan/types/protyle/util/compatibility"; 4 | 5 | export const copyTextByType = async (ids: string[], 6 | type: "ref" | "blockEmbed" | "protocol" | "protocolMd" | "hPath" | "id") => { 7 | let text = ""; 8 | for (let i = 0; i < ids.length; i++) { 9 | const id = ids[i]; 10 | if (ids.length > 1) { 11 | text += "* "; 12 | } 13 | if (type === "ref") { 14 | const response = await fetchSyncPost("/api/block/getRefText", { id }); 15 | text += `((${id} '${response.data}'))`; 16 | } else if (type === "blockEmbed") { 17 | text += `{{select * from blocks where id='${id}'}}`; 18 | } else if (type === "protocol") { 19 | text += `siyuan://blocks/${id}`; 20 | } else if (type === "protocolMd") { 21 | const response = await fetchSyncPost("/api/block/getRefText", { id }); 22 | text += `[${response.data}](siyuan://blocks/${id})`; 23 | } else if (type === "hPath") { 24 | const response = await fetchSyncPost("/api/filetree/getHPathByID", { id }); 25 | text += response.data; 26 | } else if (type === "id") { 27 | text += id; 28 | } 29 | if (ids.length > 1 && i !== ids.length - 1) { 30 | text += "\n"; 31 | } 32 | } 33 | writeText(text); 34 | }; -------------------------------------------------------------------------------- /src/components/setting/setting-attr.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | {#each SETTING_BLOCK_ATTR_ELEMENT() as element} 24 | 39 | {/each} 40 |
41 | -------------------------------------------------------------------------------- /src/components/doc-tree/doc-tree-util.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from "@/config/env-config"; 2 | import { convertDateTimeInBlock, formatRelativeTimeInBlock } from "@/utils/datetime-util"; 3 | import { removePrefixAndSuffix } from "@/utils/string-util"; 4 | 5 | export function getFileArialLabel(block: any, boxName: string): string { 6 | let ariaLabelRow: string[] = []; 7 | // ariaLabelRow.push(block.content); 8 | if (block.name) { 9 | ariaLabelRow.push( 10 | `
${window.siyuan.languages.name} ${block.name}`, 11 | ); 12 | } 13 | if (block.alias) { 14 | ariaLabelRow.push( 15 | `
${window.siyuan.languages.alias} ${block.alias}`, 16 | ); 17 | } 18 | if (block.tag) { 19 | ariaLabelRow.push( 20 | `
${window.siyuan.languages.tag} ${block.tag}`, 21 | ); 22 | } 23 | if (block.memo) { 24 | ariaLabelRow.push( 25 | `
${window.siyuan.languages.memo} ${block.memo}`, 26 | ); 27 | } 28 | 29 | ariaLabelRow.push(`
${EnvConfig.ins.i18n.notebook} ${boxName}`); 30 | ariaLabelRow.push(`
${EnvConfig.ins.i18n.path} ${block.hpath}`); 31 | 32 | let updated = formatRelativeTimeInBlock(block.updated); 33 | let created = convertDateTimeInBlock(block.created); 34 | 35 | ariaLabelRow.push( 36 | `
${window.siyuan.languages.modifiedAt} ${updated}`, 37 | ); 38 | ariaLabelRow.push( 39 | `
${window.siyuan.languages.createdAt} ${created}`, 40 | ); 41 | 42 | let ariaLabel = ariaLabelRow.join(""); 43 | ariaLabel = removePrefixAndSuffix(ariaLabel, "
", "
"); 44 | 45 | return ariaLabel; 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "skipLibCheck": true, 12 | /* Bundler mode */ 13 | "moduleResolution": "Node", 14 | // "allowImportingTsExtensions": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "preserve", 20 | /* Linting */ 21 | "strict": false, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noFallthroughCasesInSwitch": true, 25 | /* Svelte */ 26 | /** 27 | * Typecheck JS in `.svelte` and `.js` files by default. 28 | * Disable checkJs if you'd like to use dynamic types in JS. 29 | * Note that setting allowJs false does not prevent the use 30 | * of JS in `.svelte` files. 31 | */ 32 | "allowJs": true, 33 | "checkJs": true, 34 | "types": [ 35 | "node", 36 | "vite/client", 37 | "svelte" 38 | ], 39 | // "baseUrl": "./src", 40 | "paths": { 41 | "@/*": ["./src/*"], 42 | "@/libs/*": ["./src/libs/*"], 43 | } 44 | }, 45 | "include": [ 46 | "tools/**/*.ts", 47 | "src/**/*.ts", 48 | "src/**/*.d.ts", 49 | "src/**/*.tsx", 50 | "src/**/*.vue", 51 | "src/**/*.svelte" 52 | ], 53 | "references": [ 54 | { 55 | "path": "./tsconfig.node.json" 56 | } 57 | ], 58 | "root": "." 59 | } -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | # Install Node.js 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | registry-url: "https://registry.npmjs.org" 22 | 23 | # Install pnpm 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v2 26 | id: pnpm-install 27 | with: 28 | version: 8 29 | run_install: false 30 | 31 | # Get pnpm store directory 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | 38 | # Setup pnpm cache 39 | - name: Setup pnpm cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | # Install dependencies 48 | - name: Install dependencies 49 | run: pnpm install 50 | 51 | # Build for production, 这一步会生成一个 package.zip 52 | - name: Build for production 53 | run: pnpm build 54 | 55 | - name: Release 56 | uses: ncipollo/release-action@v1 57 | with: 58 | allowUpdates: true 59 | artifactErrorsFailBuild: true 60 | artifacts: "package.zip" 61 | token: ${{ secrets.GITHUB_TOKEN }} 62 | prerelease: false 63 | -------------------------------------------------------------------------------- /scripts/make_install.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-03-28 20:03:59 5 | * @FilePath : /scripts/make_install.js 6 | * @LastEditTime : 2024-09-06 18:08:19 7 | * @Description : 8 | */ 9 | // make_install.js 10 | import fs from 'fs'; 11 | import { log, error, getSiYuanDir, chooseTarget, copyDirectory, getThisPluginName } from './utils.js'; 12 | 13 | let targetDir = ''; 14 | 15 | /** 16 | * 1. Get the parent directory to install the plugin 17 | */ 18 | log('>>> Try to visit constant "targetDir" in make_install.js...'); 19 | if (targetDir === '') { 20 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); 21 | let res = await getSiYuanDir(); 22 | 23 | if (res === null || res === undefined || res.length === 0) { 24 | error('>>> Can not get SiYuan directory automatically'); 25 | process.exit(1); 26 | } else { 27 | targetDir = await chooseTarget(res); 28 | } 29 | log(`>>> Successfully got target directory: ${targetDir}`); 30 | } 31 | if (!fs.existsSync(targetDir)) { 32 | error(`Failed! Plugin directory not exists: "${targetDir}"`); 33 | error('Please set the plugin directory in scripts/make_install.js'); 34 | process.exit(1); 35 | } 36 | 37 | /** 38 | * 2. The dist directory, which contains the compiled plugin code 39 | */ 40 | const distDir = `${process.cwd()}/dist`; 41 | if (!fs.existsSync(distDir)) { 42 | fs.mkdirSync(distDir); 43 | } 44 | 45 | /** 46 | * 3. The target directory to install the plugin 47 | */ 48 | const name = getThisPluginName(); 49 | if (name === null) { 50 | process.exit(1); 51 | } 52 | const targetPath = `${targetDir}/${name}`; 53 | 54 | /** 55 | * 4. Copy the compiled plugin code to the target directory 56 | */ 57 | copyDirectory(distDir, targetPath); -------------------------------------------------------------------------------- /src/lib/siyuan/hasClosest.ts: -------------------------------------------------------------------------------- 1 | export const hasClosestByTag = (element: Node, nodeName: string) => { 2 | if (!element) { 3 | return false; 4 | } 5 | if (element.nodeType === 3) { 6 | element = element.parentElement; 7 | } 8 | let e = element as HTMLElement; 9 | let isClosest = false; 10 | while (e && !isClosest && !e.classList.contains("b3-typography")) { 11 | if (e.nodeName.indexOf(nodeName) === 0) { 12 | isClosest = true; 13 | } else { 14 | e = e.parentElement; 15 | } 16 | } 17 | return isClosest && e; 18 | }; 19 | 20 | export const hasTopClosestByTag = (element: Node, nodeName: string) => { 21 | let closest = hasClosestByTag(element, nodeName); 22 | let parentClosest: boolean | HTMLElement = false; 23 | let findTop = false; 24 | while (closest && !closest.classList.contains("protyle-wysiwyg") && !findTop) { 25 | parentClosest = hasClosestByTag(closest.parentElement, nodeName); 26 | if (parentClosest) { 27 | closest = parentClosest; 28 | } else { 29 | findTop = true; 30 | } 31 | } 32 | return closest || false; 33 | }; 34 | 35 | export const hasClosestByAttribute = (element: Node, attr: string, value: string | null, top = false) => { 36 | if (!element) { 37 | return false; 38 | } 39 | if (element.nodeType === 3) { 40 | element = element.parentElement; 41 | } 42 | let e = element as HTMLElement; 43 | let isClosest = false; 44 | while (e && !isClosest && (top ? e.tagName !== "BODY" : !e.classList.contains("protyle-wysiwyg"))) { 45 | if (typeof value === "string" && e.getAttribute(attr)?.split(" ").includes(value)) { 46 | isClosest = true; 47 | } else if (typeof value !== "string" && e.hasAttribute(attr)) { 48 | isClosest = true; 49 | } else { 50 | e = e.parentElement; 51 | } 52 | } 53 | return isClosest && e; 54 | }; 55 | -------------------------------------------------------------------------------- /src/config/env-config.ts: -------------------------------------------------------------------------------- 1 | import { getNotebookMap, getNotebookMapByApi, } from "@/utils/api"; 2 | import Instance from "@/utils/Instance"; 3 | import { App, Dock, IObject, IPluginDockTab, Plugin, Tab, getFrontend } from "siyuan"; 4 | 5 | export class EnvConfig { 6 | 7 | 8 | public static get ins(): EnvConfig { 9 | return Instance.get(EnvConfig); 10 | } 11 | 12 | private _isMobile: boolean; 13 | get isMobile(): boolean { 14 | return this._isMobile; 15 | } 16 | 17 | private _plugin: Plugin; 18 | get plugin(): Plugin { 19 | return this._plugin; 20 | } 21 | 22 | get app(): App { 23 | return this._plugin.app; 24 | } 25 | 26 | get i18n(): IObject { 27 | if (this._plugin) { 28 | return this._plugin.i18n; 29 | } 30 | const i18nObject: IObject = { 31 | // 添加你需要的属性和方法 32 | }; 33 | return i18nObject; 34 | } 35 | 36 | public lastViewedDocId: string; 37 | 38 | public searchOpenTabPromise: Promise; 39 | 40 | 41 | public init(plugin: Plugin) { 42 | let frontEnd: string = getFrontend(); 43 | this._isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile"; 44 | this._plugin = plugin; 45 | } 46 | 47 | 48 | docSearchDock: { config: IPluginDockTab, model: Dock }; 49 | flatDocTreeDock: { config: IPluginDockTab, model: Dock }; 50 | curDocSearchDock: { config: IPluginDockTab, model: Dock }; 51 | private _notebookMap: Map = new Map(); 52 | public get notebookMap(): Map { 53 | if (!this._notebookMap || this._notebookMap.size == 0) { 54 | this.refreshNotebookMap(); 55 | return getNotebookMap(window.siyuan.notebooks); 56 | } 57 | return this._notebookMap; 58 | } 59 | 60 | public async refreshNotebookMap(): Promise> { 61 | this._notebookMap = await getNotebookMapByApi(false); 62 | return this._notebookMap; 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/utils/array-util.ts: -------------------------------------------------------------------------------- 1 | export function paginate(array: T[], pageNumber: number, pageSize: number): T[] { 2 | // 计算起始索引 3 | const startIndex = (pageNumber - 1) * pageSize; 4 | // 计算结束索引 5 | const endIndex = startIndex + pageSize; 6 | // 返回对应的数组片段 7 | return array.slice(startIndex, endIndex); 8 | } 9 | 10 | export function getLastItem(list: T[]): T | undefined { 11 | return list.length > 0 ? list[list.length - 1] : undefined; 12 | } 13 | 14 | export function isArrayEmpty(array: T[]): boolean { 15 | return !array || array.length == 0; 16 | } 17 | 18 | 19 | export function isArrayNotEmpty(array: T[]): boolean { 20 | return Array.isArray(array) && array.length > 0; 21 | } 22 | 23 | export function isSetEmpty(set: Set): boolean { 24 | return !set || set.size == 0; 25 | } 26 | 27 | export function isSetNotEmpty(set: Set): boolean { 28 | return set && set.size > 0; 29 | } 30 | 31 | // 求交集。 32 | export function intersectionArray(array1: T[], array2: T[]): T[] { 33 | if (isArrayEmpty(array1) || isArrayEmpty(array2)) { 34 | return []; 35 | } 36 | // 使用 Set 来提高查找的效率 37 | // const set1 = new Set(array1); 38 | const set2 = new Set(array2); 39 | 40 | // 过滤 array1 中的元素,只保留那些也在 set2 中的元素 41 | return array1.filter(item => set2.has(item)); 42 | } 43 | 44 | 45 | // 求交集。 46 | export function intersectionSet(set1: Set, set2: Set): T[] { 47 | if (isSetEmpty(set1) || isSetEmpty(set2)) { 48 | return []; 49 | } 50 | 51 | const result = []; 52 | for (const item of set1) { 53 | if (set2.has(item)) { 54 | result.push(item); 55 | } 56 | } 57 | return result; 58 | } 59 | 60 | export function arraysEqual(arr1: any[], arr2: any[]): boolean { 61 | if (arr1.length !== arr2.length) return false; 62 | return arr1.every((value, index) => value === arr2[index]); 63 | } 64 | 65 | 66 | export function arrayRemoveValue(arr: T[], value: T): T[] { 67 | return arr.filter(item => item !== value); 68 | } 69 | -------------------------------------------------------------------------------- /src/utils/string-util.ts: -------------------------------------------------------------------------------- 1 | import { isArrayEmpty } from "./array-util"; 2 | 3 | export function removePrefix(input: string, prefix: string): string { 4 | if (input.startsWith(prefix)) { 5 | return input.substring(prefix.length); 6 | } else { 7 | return input; 8 | } 9 | } 10 | 11 | export function removeSuffix(input: string, suffix: string): string { 12 | if (input.endsWith(suffix)) { 13 | return input.substring(0, input.length - suffix.length); 14 | } else { 15 | return input; 16 | } 17 | } 18 | 19 | export function removePrefixAndSuffix(input: string, prefix: string, suffix: string): string { 20 | let result = input; 21 | 22 | if (result.startsWith(prefix)) { 23 | result = result.substring(prefix.length); 24 | } 25 | 26 | if (result.endsWith(suffix)) { 27 | result = result.substring(0, result.length - suffix.length); 28 | } 29 | 30 | return result; 31 | } 32 | 33 | 34 | export function isStrNotNull(s: any): boolean { 35 | if (s == undefined || s == null) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | 42 | export function isStrNotEmpty(s: any): boolean { 43 | if (s == undefined || s == null || s === '') { 44 | return false; 45 | } 46 | return true; 47 | } 48 | 49 | export function isStrEmpty(s: any): boolean { 50 | return !isStrNotEmpty(s); 51 | } 52 | 53 | export function isStrNotBlank(s: any): boolean { 54 | if (s == undefined || s == null || s.trim() === '') { 55 | return false; 56 | } 57 | 58 | return true; 59 | } 60 | 61 | export function isStrBlank(s: any): boolean { 62 | return !isStrNotBlank(s); 63 | } 64 | 65 | 66 | export function splitKeywordStringToArray(keywordStr: string): string[] { 67 | let keywordArray = []; 68 | if (!isStrNotEmpty(keywordStr)) { 69 | return keywordArray; 70 | } 71 | // 分离空格 72 | keywordArray = keywordStr.trim().replace(/\s+/g, " ").split(" "); 73 | if (isArrayEmpty(keywordArray)) { 74 | return keywordArray; 75 | } 76 | // 去重 77 | keywordArray = Array.from(new Set( 78 | keywordArray.filter((keyword) => keyword.length > 0), 79 | )); 80 | return keywordArray; 81 | 82 | } -------------------------------------------------------------------------------- /scripts/make_dev_link.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2023-07-15 15:31:31 5 | * @FilePath : /scripts/make_dev_link.js 6 | * @LastEditTime : 2024-09-06 18:13:53 7 | * @Description : 8 | */ 9 | // make_dev_link.js 10 | import fs from 'fs'; 11 | import { log, error, getSiYuanDir, chooseTarget, getThisPluginName, makeSymbolicLink } from './utils.js'; 12 | 13 | let targetDir = ''; 14 | 15 | /** 16 | * 1. Get the parent directory to install the plugin 17 | */ 18 | log('>>> Try to visit constant "targetDir" in make_dev_link.js...'); 19 | if (targetDir === '') { 20 | log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); 21 | let res = await getSiYuanDir(); 22 | 23 | if (!res || res.length === 0) { 24 | log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); 25 | let env = process.env?.SIYUAN_PLUGIN_DIR; 26 | if (env) { 27 | targetDir = env; 28 | log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); 29 | } else { 30 | error('\tCan not get SiYuan directory from environment variable "SIYUAN_PLUGIN_DIR", failed!'); 31 | process.exit(1); 32 | } 33 | } else { 34 | targetDir = await chooseTarget(res); 35 | } 36 | 37 | log(`>>> Successfully got target directory: ${targetDir}`); 38 | } 39 | if (!fs.existsSync(targetDir)) { 40 | error(`Failed! Plugin directory not exists: "${targetDir}"`); 41 | error('Please set the plugin directory in scripts/make_dev_link.js'); 42 | process.exit(1); 43 | } 44 | 45 | /** 46 | * 2. The dev directory, which contains the compiled plugin code 47 | */ 48 | const devDir = `${process.cwd()}/dev`; 49 | if (!fs.existsSync(devDir)) { 50 | fs.mkdirSync(devDir); 51 | } 52 | 53 | 54 | /** 55 | * 3. The target directory to make symbolic link to dev directory 56 | */ 57 | const name = getThisPluginName(); 58 | if (name === null) { 59 | process.exit(1); 60 | } 61 | const targetPath = `${targetDir}/${name}`; 62 | 63 | /** 64 | * 4. Make symbolic link 65 | */ 66 | makeSymbolicLink(devDir, targetPath); -------------------------------------------------------------------------------- /src/components/setting/setting-type.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | {#each SETTING_BLOCK_TYPE_ELEMENT() as element} 22 | 48 | {/each} 49 | 50 |
51 |
52 | [1] {window.siyuan.languages.containerBlockTip1} 53 |
54 |
55 | [2] {window.siyuan.languages.containerBlockTip2} 56 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /src/components/setting/setting-flat-doc-tree.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 45 | 46 | 63 |
64 | -------------------------------------------------------------------------------- /src/utils/datetime-util.ts: -------------------------------------------------------------------------------- 1 | 2 | export function parseDateTimeInBlock(dateTimeString: string): Date | null { 3 | if (dateTimeString.length !== 14) { 4 | console.error("Invalid date time string format. It should be 'yyyyMMddhhmmss'."); 5 | return null; 6 | } 7 | 8 | const year = parseInt(dateTimeString.slice(0, 4), 10); 9 | const month = parseInt(dateTimeString.slice(4, 6), 10) - 1; // 月份从 0 开始 10 | const day = parseInt(dateTimeString.slice(6, 8), 10); 11 | const hour = parseInt(dateTimeString.slice(8, 10), 10); 12 | const minute = parseInt(dateTimeString.slice(10, 12), 10); 13 | const second = parseInt(dateTimeString.slice(12, 14), 10); 14 | 15 | return new Date(year, month, day, hour, minute, second); 16 | } 17 | 18 | 19 | export function convertDateTimeInBlock(dateTimeString: string): string { 20 | if (dateTimeString.length !== 14) { 21 | console.error("Invalid date time string format. It should be 'yyyyMMddhhmmss'."); 22 | return null; 23 | } 24 | const year = dateTimeString.slice(0, 4); 25 | const month = dateTimeString.slice(4, 6); 26 | const day = dateTimeString.slice(6, 8); 27 | const hour = dateTimeString.slice(8, 10); 28 | const minute = dateTimeString.slice(10, 12); 29 | const second = dateTimeString.slice(12, 14); 30 | 31 | return `${year}-${month}-${day} ${hour}:${minute}:${second}`; 32 | } 33 | 34 | 35 | 36 | export function formatRelativeTimeInBlock(dateTimeString: string): string { 37 | let timestamp = parseDateTimeInBlock(dateTimeString).getTime(); 38 | return formatRelativeTime(timestamp); 39 | } 40 | 41 | 42 | export function formatRelativeTime(timestamp: number): string { 43 | const now = Date.now(); 44 | const diff = now - timestamp; 45 | const minute = 60 * 1000; 46 | const hour = 60 * minute; 47 | const day = 24 * hour; 48 | const month = 30 * day; 49 | const year = 365 * day; 50 | 51 | if (diff < minute) { 52 | return `${Math.floor(diff / 1000)}秒前`; 53 | } else if (diff < hour) { 54 | return `${Math.floor(diff / minute)}分钟前`; 55 | } else if (diff < day) { 56 | return `${Math.floor(diff / hour)}小时前`; 57 | } else if (diff < month) { 58 | return `${Math.floor(diff / day)}天前`; 59 | } else if (diff < year) { 60 | return `${Math.floor(diff / month)}个月前`; 61 | } else { 62 | return `${Math.floor(diff / year)}年前`; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/setting/setting-util.ts: -------------------------------------------------------------------------------- 1 | import { Dialog } from "siyuan"; 2 | import SettingOther from "@/components/setting/setting-other.svelte"; 3 | import SettingAttr from "@/components/setting/setting-attr.svelte"; 4 | import SettingType from "@/components/setting/setting-type.svelte"; 5 | import SettingNotebook from "@/components/setting/setting-notebook.svelte"; 6 | import SettingHub from "@/components/setting/setting-hub.svelte"; 7 | import { EnvConfig } from "@/config/env-config"; 8 | 9 | export function openSettingsDialog(dialogType: SettingDialogType) { 10 | let dialogTitle: string; 11 | switch (dialogType) { 12 | case "settingNotebook": 13 | dialogTitle = EnvConfig.ins.i18n.notebookFilter; 14 | break; 15 | case "settingType": 16 | dialogTitle = EnvConfig.ins.i18n.type; 17 | break; 18 | case "settingAttr": 19 | dialogTitle = EnvConfig.ins.i18n.attr; 20 | break; 21 | case "settingOther": 22 | dialogTitle = EnvConfig.ins.i18n.other; 23 | break; 24 | case "settingHub": 25 | dialogTitle = EnvConfig.ins.i18n.settingHub; 26 | break; 27 | default: 28 | return; 29 | } 30 | 31 | let dialog = new Dialog({ 32 | title: dialogTitle, 33 | content: `
`, 34 | width: EnvConfig.ins.isMobile ? "92vw" : "600px", 35 | height: "70vh", 36 | destroyCallback: (options) => { 37 | console.log("destroyCallback", options); 38 | }, 39 | }); 40 | let settingSvelteOptions = { 41 | target: dialog.element.querySelector(`#${dialogType}`), 42 | }; 43 | 44 | switch (dialogType) { 45 | case "settingNotebook": 46 | new SettingNotebook(settingSvelteOptions); 47 | break; 48 | case "settingType": 49 | new SettingType(settingSvelteOptions); 50 | break; 51 | case "settingAttr": 52 | new SettingAttr(settingSvelteOptions); 53 | break; 54 | case "settingOther": 55 | new SettingOther(settingSvelteOptions); 56 | break; 57 | case "settingHub": 58 | let dialogElement = settingSvelteOptions.target as HTMLElement; 59 | dialogElement.classList.remove("b3-dialog__content"); 60 | dialogElement.style.height = "100%"; 61 | 62 | 63 | new SettingHub(settingSvelteOptions); 64 | break; 65 | default: 66 | return; 67 | } 68 | 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/lib/siyuan/menus/commonMenuItem.ts: -------------------------------------------------------------------------------- 1 | import { copyTextByType } from "../protyle/toolbar/util"; 2 | 3 | export const copySubMenu = (ids: string[], accelerator = true, focusElement?: Element) => { 4 | return [{ 5 | id: "copyBlockRef", 6 | iconHTML: "", 7 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyBlockRef.custom : undefined, 8 | label: window.siyuan.languages.copyBlockRef, 9 | click: () => { 10 | copyTextByType(ids, "ref"); 11 | // if (focusElement) { 12 | // focusBlock(focusElement); 13 | // } 14 | } 15 | }, { 16 | id: "copyBlockEmbed", 17 | iconHTML: "", 18 | label: window.siyuan.languages.copyBlockEmbed, 19 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyBlockEmbed.custom : undefined, 20 | click: () => { 21 | copyTextByType(ids, "blockEmbed"); 22 | // if (focusElement) { 23 | // focusBlock(focusElement); 24 | // } 25 | } 26 | }, { 27 | id: "copyProtocol", 28 | iconHTML: "", 29 | label: window.siyuan.languages.copyProtocol, 30 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyProtocol.custom : undefined, 31 | click: () => { 32 | copyTextByType(ids, "protocol"); 33 | // if (focusElement) { 34 | // focusBlock(focusElement); 35 | // } 36 | } 37 | }, { 38 | id: "copyProtocolInMd", 39 | iconHTML: "", 40 | label: window.siyuan.languages.copyProtocolInMd, 41 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyProtocolInMd.custom : undefined, 42 | click: () => { 43 | copyTextByType(ids, "protocolMd"); 44 | // if (focusElement) { 45 | // focusBlock(focusElement); 46 | // } 47 | } 48 | }, { 49 | id: "copyHPath", 50 | iconHTML: "", 51 | label: window.siyuan.languages.copyHPath, 52 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyHPath.custom : undefined, 53 | click: () => { 54 | copyTextByType(ids, "hPath"); 55 | // if (focusElement) { 56 | // focusBlock(focusElement); 57 | // } 58 | } 59 | }, { 60 | id: "copyID", 61 | iconHTML: "", 62 | label: window.siyuan.languages.copyID, 63 | accelerator: accelerator ? window.siyuan.config.keymap.editor.general.copyID.custom : undefined, 64 | click: () => { 65 | copyTextByType(ids, "id"); 66 | // if (focusElement) { 67 | // focusBlock(focusElement); 68 | // } 69 | } 70 | }]; 71 | }; 72 | -------------------------------------------------------------------------------- /src/i18n/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "openDocumentSearchTab": "打开搜索页签", 3 | "table": "表格", 4 | "mathBlock": "公式块", 5 | "quoteBlock": "引述块", 6 | "superBlock": "超级块", 7 | "paragraph": "段落", 8 | "doc": "文档", 9 | "headings": "标题", 10 | "list": "列表", 11 | "listItem": "列表项", 12 | "codeBlock": "代码块", 13 | "htmlBlock": "HTML 块", 14 | "embedBlock": "嵌入块", 15 | "database": "数据库", 16 | "video": "视频", 17 | "audio": "音频", 18 | "IFrame": "IFrame", 19 | "widget": "挂件", 20 | "name": "name", 21 | "alias": "别名", 22 | "memo": "备注", 23 | "allAttrs": "所有属性名和属性值", 24 | "sortByRankASC": "按相关度升序", 25 | "sortByRankDESC": "按相关度降序", 26 | "modifiedASC": "修改时间升序", 27 | "modifiedDESC": "修改时间降序", 28 | "createdASC": "创建时间升序", 29 | "createdDESC": "创建时间降序", 30 | "type": "类型", 31 | "sortByContent": "按原文内容顺序", 32 | "sortByTypeAndContent": "类型和原文内容顺序", 33 | "show": "显示", 34 | "hide": "隐藏", 35 | "refCountASC": "引用数升序", 36 | "refCountDESC": "引用数降序", 37 | "fileNameASC": "名称字母升序", 38 | "fileNameDESC": "名称字母降序", 39 | "fileNameNatASC": "名称自然升序", 40 | "fileNameNatDESC": "名称自然降序", 41 | "notebook": "笔记本", 42 | "path": "路径", 43 | "sort": "排序", 44 | "clear": "清空", 45 | "refresh": "刷新", 46 | "reference": "引用", 47 | "documentBasedSearch": "基于文档搜索", 48 | "flatDocumentTree": "扁平化文档树", 49 | "documentBasedSearchDock": "基于文档搜索 Dock", 50 | "flatDocumentTreeDock": "扁平化文档树 Dock", 51 | "previousLabel": "上一页", 52 | "nextLabel": "下一页", 53 | "findInDoc": "匹配到 ${x} 个文档", 54 | "notebookFilter": "笔记本过滤", 55 | "attr": "属性", 56 | "other": "其他", 57 | "expand": "展开", 58 | "collapse": "折叠", 59 | "noContentBelow": "下不存在符合条件的内容", 60 | "dockModifyTips": "注:修改 Dock 会刷新界面。", 61 | "settingDock": "🌈 Dock 设置", 62 | "settingNotebookFilter": "🌈 笔记本过滤", 63 | "settingType": "🌈 类型", 64 | "settingAttr": "🌈 属性", 65 | "settingOther": "🌈 其他", 66 | "docSortMethod": "文档排序方式", 67 | "contentBlockSortMethod": "内容块排序方式", 68 | "documentsPerPage": "每页文档数量", 69 | "blockCountBehaviorTips": "如果查询结果的块数量小于当前值,默认展开全部文档;反之会默认折叠全部文档。", 70 | "defaultExpansionCount": "默认展开数", 71 | "alwaysExpandSingleDoc": "单篇文档始终展开", 72 | "displayDocBlock": "显示文档块", 73 | "doubleClickTimeThreshold": "双击时间阈值(毫秒)", 74 | "previewRefreshHighlightDelayTips": "用于代码块、数据库这种需要时间渲染的块高亮,太短可能会失败,不需要可以设置为0", 75 | "previewRefreshHighlightDelay": "刷新预览区高亮延迟(毫秒)", 76 | "settingHub": "设置中心", 77 | "switchCurrentDocumentSearchFailureMessage": "文档搜索插件: 没有获取到打开的文档,可切换页签或重新打开文档。", 78 | "swapDocumentItemClickLogic": "交换文档项点击逻辑", 79 | "swapDocumentItemClickLogicTips": "禁用时,单击展开搜索结果,双击打开文档;启用时,单击打开文档,双击展开搜索结果。", 80 | "allNotebooks": "所有笔记本", 81 | "specifyNotebook": "指定笔记本", 82 | "searchInTheCurrentDocument": "在当前文档查询" 83 | 84 | } -------------------------------------------------------------------------------- /src/lib/SearchUtil.ts: -------------------------------------------------------------------------------- 1 | export function handleSearchDragMousdown(event: MouseEvent) { 2 | /* 复制 https://vscode.dev/github/siyuan-note/siyuan/blob/master/app/src/search/util.ts#L407 3 | #genSearch 方法下的 const dragElement = element.querySelector(".search__drag"); 处 4 | */ 5 | 6 | const dragElement = event.target as Element; 7 | const documentSelf = document; 8 | const nextElement = dragElement.nextElementSibling as HTMLElement; 9 | const previousElement = 10 | dragElement.previousElementSibling as HTMLElement; 11 | const direction = "lr"; 12 | // window.siyuan.storage[Constants.LOCAL_SEARCHKEYS][ 13 | // closeCB ? "layout" : "layoutTab" 14 | // ] === 1 15 | // ? "lr" 16 | // : "tb"; 17 | const x = event[direction === "lr" ? "clientX" : "clientY"]; 18 | const previousSize = 19 | direction === "lr" 20 | ? previousElement.clientWidth 21 | : previousElement.clientHeight; 22 | const nextSize = 23 | direction === "lr" 24 | ? nextElement.clientWidth 25 | : nextElement.clientHeight; 26 | 27 | nextElement.classList.remove("fn__flex-1"); 28 | nextElement.style[direction === "lr" ? "width" : "height"] = 29 | nextSize + "px"; 30 | 31 | documentSelf.onmousemove = (moveEvent: MouseEvent) => { 32 | moveEvent.preventDefault(); 33 | moveEvent.stopPropagation(); 34 | const previousNowSize = 35 | previousSize + 36 | (moveEvent[direction === "lr" ? "clientX" : "clientY"] - x); 37 | const nextNowSize = 38 | nextSize - 39 | (moveEvent[direction === "lr" ? "clientX" : "clientY"] - x); 40 | if (previousNowSize < 120 || nextNowSize < 120) { 41 | return; 42 | } 43 | nextElement.style[direction === "lr" ? "width" : "height"] = 44 | nextNowSize + "px"; 45 | }; 46 | 47 | documentSelf.onmouseup = () => { 48 | documentSelf.onmousemove = null; 49 | documentSelf.onmouseup = null; 50 | documentSelf.ondragstart = null; 51 | documentSelf.onselectstart = null; 52 | documentSelf.onselect = null; 53 | // window.siyuan.storage[Constants.LOCAL_SEARCHKEYS][ 54 | // direction === "lr" 55 | // ? closeCB 56 | // ? "col" 57 | // : "colTab" 58 | // : closeCB 59 | // ? "row" 60 | // : "rowTab" 61 | // ] = 62 | // nextElement[ 63 | // direction === "lr" ? "clientWidth" : "clientHeight" 64 | // ] + "px"; 65 | // setStorageVal( 66 | // Constants.LOCAL_SEARCHKEYS, 67 | // window.siyuan.storage[Constants.LOCAL_SEARCHKEYS], 68 | // ); 69 | // if (direction === "lr") { 70 | // resize(edit.protyle); 71 | // } 72 | }; 73 | } -------------------------------------------------------------------------------- /src/components/setting/setting-notebook.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
47 | {#each Array.from(notebookMap.entries()) as [key, item] (key)} 48 | 70 | {/each} 71 |
72 | -------------------------------------------------------------------------------- /CHANGELOG_zh_CN.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## 0.10.8 4 | * 搜索页签支持上下键切换结果预览 5 | * 扁平化文档树支持拖拽到文档树移动文档。 6 | 7 | 8 | ## 0.10.7 9 | * 扁平化文档优化:增加扁平化文档树设置:支持全文搜索;搜索返回数量。 10 | * 搜索结果文档项优化:点击文档项时,可以在整个文档中定位到下一个匹配块(点击段落块还是局限在段落块中)。 11 | 12 | ## 0.10.6 13 | * 修复:修改配置文件导致i18n目录没有在打包文件中。 14 | 15 | ## 0.10.5 16 | * 优化:优化侧边栏快捷键,改用命令的方式点击 Dock。 17 | * 升级创建文件夹软连接脚本。 18 | 19 | ## 0.10.4 20 | * 修复:上一个修复引起的问题,搜索结果列表,多个关键字无法高亮。。 21 | 22 | 23 | ## 0.10.3 24 | * 修复:查询字符串存在js正则关键字的异常问题。比如(\ . *) 25 | 26 | 27 | ## 0.10.2 28 | * 优化:隐藏 文档搜索Dock 后消除高亮 29 | * 优化:中键点击搜索结果的文档项,支持 展开\折叠。 30 | 31 | ## 0.10.1 32 | * 修复:侧边栏同一篇文档,点击不同结果块可能无法定位。 33 | 34 | ## 0.10.0 35 | * 添加指定笔记本搜索。 36 | 37 | ## 0.9.11 38 | * 修复:修复文档项箭头失效。 39 | 40 | ## 0.9.10 41 | * 优化:添加 "交换文档项点击逻辑" 设置,禁用时,单击展开搜索结果,双击打开文档;启用时,单击打开文档,双击展开搜索结果。 42 | 43 | 44 | ## 0.9.9 45 | * 优化:添加 “单篇文档始终展开” 设置,开启后,如果搜索结果为单篇文档,展开所有搜索结果。 46 | 47 | 48 | ## 0.9.8 49 | * 适配:3.0.17版本,检查块是否折叠接口(`/api/block/checkBlockFold`) 响应值由 boolean 变成 object ,造成搜索结果查看时,聚焦异常(一直显示聚焦状态)。当前做了一下兼容,后续可能会移除兼容代码,建议大家使用最新版本的思源笔记。 50 | 51 | ## 0.9.7 52 | * 修复:手机端 基于文档搜索 Dock 不显示查询结果 53 | 54 | ## 0.9.6 55 | * 优化:文档悬浮显示标签信息;搜索界面的父文档,双击触发单击文档块逻辑(Dock 栏就是打开选中文档,页签搜索就是预览当前文档)。 56 | 57 | ## 0.9.5 58 | * 优化:文档搜索 dock 栏打开全选搜索扩的文本 59 | 60 | 61 | ## 0.9.4 62 | * 优化筛选类型弹窗,容器块进行标识。 63 | * 优化基于文档搜索,搜索结果中的文档块,悬浮展示更多的文档信息。 64 | 65 | ## 0.9.3 66 | * 优化 Dock 栏**加载中**提示的位置,防止遇到异常情况无法再次搜索,直接导致不可用。 67 | 68 | ## 0.9.2 69 | * 文档搜索和扁平化文档树添加 名称字母排序 方式。 70 | * 说明文档添加快捷键描述 71 | 72 | ## 0.9.1 73 | * Dock 栏支持在当前文档搜索 74 | 75 | ## 0.9.0 76 | * 支持英语环境,更新说明文档 77 | * 搜索类型过滤支持音频、视频、挂件和 IFrame 块。 78 | * 修复 文档搜索标签页,展开、折叠按钮失效问题 79 | * 优化点击同一个文档下的的搜索结果,特定情况下更流畅。 80 | * 特定情况:点击的搜索结果,跟前一次点击的搜索结果在同一个文档中,并且动态加载加载到了这个搜索结果。 81 | * 优化前:每次点击搜索结果,都会调用前端接口,预览文档会闪一下,视觉上不太友好。 82 | * 优化后:用原生自带的滚动定位,更直观的感受到结果位置。 83 | * 添加内容块排序方式:类型和原文内容顺序(个人比较喜欢)。 84 | 85 | ## 0.8.2 86 | * 修复扁平化文档树多关键字搜索错误。 87 | 88 | ## 0.8.1 89 | * 新增扁平化文档树 Dock,方便查看最近修改、创建的文档(目前默认展示前 30 个),支持文档名搜索。(不需要可以再设置中关闭) 90 | * 优化文档全文搜索 Dock 手机端布局。 91 | 92 | ## 0.7.0 93 | * 电脑端 Dock(侧边栏)搜索单击结果,支持打开文档后高亮并定位关键字。 94 | * 优化显示文档块,开启后文档块无论如何排序都在第一位。 95 | * 右击文档分组,支持对单篇文档的搜索结果进行排序。 96 | * 支持 原文内容排序。建议升级 v3.0.4 后使用,v3.0.4 之前的版本用此搜索会比较慢。 97 | 98 | ## 0.6.5 99 | * 支持点击相同搜索结果块,预览区定位到下一个关键字。 100 | 101 | ## 0.6.4 102 | * 优化查询速度:把查询匹配内容块 和 文档数量两个sql合并,降低sql并发,提升了查询速度(个人测试快了30%左右)。 103 | 104 | ## 0.6.3 105 | * 添加 刷新预览区延迟 设置,可用于代码块、数据库等需要时间渲染的块内容高亮。 106 | 107 | ## 0.6.2 108 | * 被折叠的块可以正常预览并打开定位。 109 | * 修改标签内的id,与官方进行区别,防止重复id的错误调用。 110 | * 添加双击时间阈值设置。 111 | 112 | ## 0.6.1 113 | * 支持分别设置文档、内容块的排序方式 114 | 115 | ## 0.6.0 116 | * 支持预览区高亮,支持表格定位首个匹配关键字位置 117 | 118 | ## 0.5.2 119 | * 优化设置“显示文档块”的逻辑,优化上下键选择搜索结果的样式。 120 | 121 | ## 0.5.1 122 | * 修复搜索预览超出页面,优化搜索页的结构。 123 | 124 | ## 0.5.0 125 | * 支持设置过滤块类型、笔记本、快属性(命名、别名等);支持配置每页文档数量、默认展开数量 126 | 127 | ## 0.4.0 128 | * 支持查询块的命名、别名、备注并显示 129 | 130 | ## 0.3.0 131 | * 支持分页查询 132 | 133 | ## 0.2.0 134 | * 支持手机端 135 | 136 | ## 0.1.0 137 | * 支持Dock和自定义页签查询 -------------------------------------------------------------------------------- /src/components/setting/setting-dock.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 50 | 51 | 74 | 75 | {EnvConfig.ins.i18n.dockModifyTips} 76 | 77 |
78 | -------------------------------------------------------------------------------- /src/components/setting/setting-hub.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 | 53 |
54 |
    55 | {#each groups as group} 56 |
  • { 61 | focusGroup = group; 62 | }} 63 | on:keydown={() => {}} 64 | > 65 | {group.title} 66 |
  • 67 | {/each} 68 |
69 |
70 |
71 | {#if focusGroup.type == "settingDock"} 72 | 73 | {/if} 74 | {#if focusGroup.type == "settingNotebook"} 75 | 76 | {/if} 77 | 78 | {#if focusGroup.type == "settingType"} 79 | 80 | {/if} 81 | 82 | {#if focusGroup.type == "settingAttr"} 83 | 84 | {/if} 85 | 86 | {#if focusGroup.type == "settingOther"} 87 | 88 | {/if} 89 | {#if focusGroup.type == "settingFlatDocTree"} 90 | 91 | {/if} 92 |
93 |
94 |
95 | 96 | 104 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | [英文](README.md) 2 | 3 | 4 | ## 基于文档搜索 5 | ### 特性 6 | * 以文档为单位进行搜索 7 | * 可设置搜索块的类型(与官方搜索类型隔离,互不影响。) 8 | * 支持过滤不常用笔记本 9 | * 支持手机端使用(需要开启Dock栏) 10 | 11 | 12 | ### 功能 13 | 14 | #### 单击搜索结果进行定位 15 | 单击搜索结果块,支持在文档中进行定位。在长表格或代码块中体验良好。 16 | 17 | ![Image](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/click-result-positioning.gif?raw=true) 18 | 19 | #### 标签页双击搜索结果打开文档页签 20 | 标签页双击搜索结果打开文档,并跳转到指定位置。如果双击后没有打开,可以在“其他”设置中,把“双击时间阈值”调大点。 21 | 22 | 23 | #### 单篇文档搜索结果排序 24 | * 触发方式: 25 | * 桌面端右键单击文档 26 | * 手机端长按文档 27 | 28 | ![Image](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/sorting-menu.png?raw=true) 29 | 30 | 31 | #### 支持方向键选择 32 | 光标在输入框中时,可以上下键选择搜索结果,回车键打开结果标签页。 33 | 34 | 35 | #### 代码块、数据库高亮 36 | 页签搜索预览区 或 dock栏单击打开文档后 支持 **代码块、数据库** 关键字高亮和定位!(官方搜索目前不支持) 37 | 如果 **代码块、数据库** 的高亮失效,可以再“其他”设置中,把“刷新预览区高亮延迟”调大点。 38 | 39 | 40 | #### Dock栏搜索 支持在当前文档搜索 41 | ![Alt text](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/search-in-document.png?raw=true) 42 | 43 | 关闭是全局搜索,打开后在当前文档搜索;打开后或强制使用原文排序,如果想要切换排序可以右击文档进行切换。 44 | 45 | 实际上是读取光标最后聚焦的文档,如果没有定位到想要的文档,可以切换页签或重新打开该文档。 46 | 47 | 48 | ## 扁平化文档树 49 | ### 特性 50 | * 官方文档树对文档的操作,这里都支持。 51 | * 注意:重命名、删除、新建文档 等操作需要手动刷新才会展示最新数据。 52 | * 支持手机端使用(需要开启Dock栏) 53 | ### 功能 54 | * 支持以修改时间、创建时间、引用次数进行排序。 55 | * 搜索框支持文档名搜索。 56 | 57 | ## 隐藏 Dock 58 | 可以在插件的设置 Dock设置 中对不需要的 Dock 进行隐藏。 59 | 注意:手机端会同步隐藏 60 | 61 | 62 | ## 查询语法 63 | `@`接特定字母使用查询语法,`@`或关键字前面加减号`-`就是排除这个条件的块;建议`-`排除语法最好在最后使用,否则会先查询出所有不符合条件的块,比较卡。 64 | 65 | ### 关键字查询 66 | 67 | #### 开启全文搜索 68 | * `工作 测试`:查询同时含有`工作`、`测试`的文档;然后显示含有 `工作`或`测试`的块 69 | * `-工作 测试`:查询不含`工作`,含有`测试`的文档;然后显示含有 `测试` 的块 70 | 71 | #### 关闭全文搜索 72 | * `工作 测试`:查询同时含有`工作`、`测试`的块。 73 | * `-工作 测试`:查询不含`工作`,含有`测试`的块。 74 | 75 | ### `@b` 笔记本过滤 76 | * `@b生活`:显示包含`生活`关键字的笔记本中的文档 77 | * `-@b生活`:显示不包含`生活`关键字的笔记本中的文档 78 | * 可以组合查询,列如 `@b生活 @b工作` 显示包含`生活` **和** `工作` 笔记本中的文档 79 | 80 | ### `@p` 路径过滤 81 | * `@p兴趣`:显示路径包含`兴趣`关键字的文档 82 | * `-@p兴趣`:显示路径不包含`兴趣`关键字的文档 83 | * 可组合查询。 84 | 85 | 如果想查看文档 `兴趣爱好` 下的所有子文档,但是不包含 `兴趣爱好`,可以使用 `@p兴趣%/`,在结尾添加 `%/` 表示子文档。 86 | 87 | ### `@c` 创建时间 `@u` 更新时间 88 | 时间或日期值采用 yyyyMMddhhmmss 格式,列如 2025年03月31日14:42:04 就是 `20250331144204`。 89 | 90 | * 查询2025年03月创建(更新)的笔记:`@c202503`、`@c=202503`、`@ceq202503` 91 | * 查询2025年03月15号之后创建(更新)的笔记:`@c>20250315`、`@cgt20250315` 92 | * 查询2025年之前创建的笔记:`@c<2025`、`@clt2025` 93 | * 查询2024年11月的笔记:`@c>202411 @c<202412` 94 | 95 | ### `@t` 按照块类型匹配关键字 96 | 块类型跟数据库中的 `blocks.type` 值一样: `d ` 文档 | `h ` 标题 | `l ` 列表 | `i ` 列表项 | `c ` 代码块 | `m ` 数学公式 | `t ` 表格 | `b ` 引述 | `av ` 属性视图(数据库) | `s ` 超级块 | `p ` 段落 | `tb ` 表格 | `html ` HTML | `video ` 视频 | `audio ` 音频 | `widget ` 挂件 | `iframe ` iframe | `query_embed ` 嵌入块 97 | 98 | * `@tav`:查询所有数据库块。 99 | * `@th紫罗兰`:查询含有`紫罗兰`的标题块。 100 | * `-@th紫罗兰`:查询不包含`紫罗兰`的标题块 此外的所有块;如果单单这个条件,基本上会返回许多块,所以建议先写包含的关键字,再根据搜索结果排除。 101 | * `@th -@th紫罗兰`:查询标题块,排除包含`紫罗兰`的标题块。 102 | * `@th @tp紫罗兰`:查询所有标题块,查询所有包含`紫罗兰`的段落块。 103 | 104 | 注意:开启全文搜索和不开全文搜索结果会不一样,全文搜索会以文档为单位来排除。每个块类型之间是(OR) 105 | * 列如:`@th紫罗兰 -@th花园` 查询含有 `紫罗兰` 的标题块,排除含有 `花园` 的标题块。 106 | * 如果开启全文搜索,那么含有 `紫罗兰` 的标题块,同时含有 `花园` 标题块的文档都会被过滤。 107 | * 关闭全文搜索,就会显示含有 `紫罗兰` 的标题块,同文档下含有 `花园` 标题块会被过滤,不会影响整个文档。 108 | 109 | ## 默认配置 110 | 111 | ### 基于文档搜索 112 | 113 | * 默认查询的块类型: 114 | * 文档, 标题, 代码块, 数学公式块, 表格块, 段落块, html块, 数据库,音频,视频 115 | * 默认会匹配块的 命名、别名、备注 116 | * 默认排序方式: 117 | * 文档排序:相关度降序 118 | * 内容块排序:类型 119 | * 每页文档数量:10 120 | * 默认最大展开数量:100 121 | 122 | 123 | ### 扁平化文档树 124 | 125 | * 默认修改时间降序 126 | * 展示30条 127 | 128 | 129 | ### 快捷键 130 | 131 | * 目前支持快捷键的操作:打开文档搜索页签、文档搜索 Dock、扁平化文档树 Dock 。 132 | * 均可在 思源笔记设置 -> 快捷键 -> 基于文档搜索 中找到并修改: 133 | 134 | # 更新记录 135 | > [更新记录](./CHANGELOG_zh_CN.md) -------------------------------------------------------------------------------- /src/lib/siyuan/protyle/util/compatibility.ts: -------------------------------------------------------------------------------- 1 | import { focusByRange } from "../../util/selection"; 2 | 3 | 4 | export const openByMobile = (uri: string) => { 5 | if (!uri) { 6 | return; 7 | } 8 | if (isInIOS()) { 9 | if (uri.startsWith("assets/")) { 10 | // iOS 16.7 之前的版本,uri 需要 encodeURIComponent 11 | window.webkit.messageHandlers.openLink.postMessage(location.origin + "/assets/" + encodeURIComponent(uri.replace("assets/", ""))); 12 | } else if (uri.startsWith("/")) { 13 | // 导出 zip 返回的是已经 encode 过的,因此不能再 encode 14 | window.webkit.messageHandlers.openLink.postMessage(location.origin + uri); 15 | } else { 16 | try { 17 | new URL(uri); 18 | window.webkit.messageHandlers.openLink.postMessage(uri); 19 | } catch (e) { 20 | window.webkit.messageHandlers.openLink.postMessage("https://" + uri); 21 | } 22 | } 23 | } else if (isInAndroid()) { 24 | window.JSAndroid.openExternal(uri); 25 | } else if (isInHarmony()) { 26 | window.JSHarmony.openExternal(uri); 27 | } else { 28 | window.open(uri); 29 | } 30 | }; 31 | 32 | 33 | export const exportByMobile = (uri: string) => { 34 | if (!uri) { 35 | return; 36 | } 37 | if (isInIOS()) { 38 | openByMobile(uri); 39 | } else if (isInAndroid()) { 40 | window.JSAndroid.exportByDefault(uri); 41 | } else if (isInHarmony()) { 42 | window.JSHarmony.exportByDefault(uri); 43 | } else { 44 | window.open(uri); 45 | } 46 | }; 47 | 48 | 49 | export const writeText = (text: string) => { 50 | let range: Range; 51 | if (getSelection().rangeCount > 0) { 52 | range = getSelection().getRangeAt(0).cloneRange(); 53 | } 54 | try { 55 | // navigator.clipboard.writeText 抛出异常不进入 catch,这里需要先处理移动端复制 56 | if (isInAndroid()) { 57 | window.JSAndroid.writeClipboard(text); 58 | return; 59 | } 60 | if (isInHarmony()) { 61 | window.JSHarmony.writeClipboard(text); 62 | return; 63 | } 64 | if (isInIOS()) { 65 | window.webkit.messageHandlers.setClipboard.postMessage(text); 66 | return; 67 | } 68 | navigator.clipboard.writeText(text); 69 | } catch (e) { 70 | if (isInIOS()) { 71 | window.webkit.messageHandlers.setClipboard.postMessage(text); 72 | } else if (isInAndroid()) { 73 | window.JSAndroid.writeClipboard(text); 74 | } else if (isInHarmony()) { 75 | window.JSHarmony.writeClipboard(text); 76 | } else { 77 | const textElement = document.createElement("textarea"); 78 | textElement.value = text; 79 | textElement.style.position = "fixed"; //avoid scrolling to bottom 80 | document.body.appendChild(textElement); 81 | textElement.focus(); 82 | textElement.select(); 83 | document.execCommand("copy"); 84 | document.body.removeChild(textElement); 85 | if (range) { 86 | focusByRange(range); 87 | } 88 | } 89 | } 90 | }; 91 | 92 | 93 | 94 | export const isWindows = () => { 95 | return navigator.platform.toUpperCase().indexOf("WIN") > -1; 96 | }; 97 | 98 | export const isInAndroid = () => { 99 | return window.siyuan.config.system.container === "android" && window.JSAndroid; 100 | }; 101 | 102 | export const isInIOS = () => { 103 | return window.siyuan.config.system.container === "ios" && window.webkit?.messageHandlers; 104 | }; 105 | 106 | export const isInHarmony = () => { 107 | return window.siyuan.config.system.container === "harmony" && window.JSHarmony; 108 | }; -------------------------------------------------------------------------------- /src/i18n/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "openDocumentSearchTab": "Open Document-based Search Tab", 3 | "table": "Table", 4 | "mathBlock": "Formula block", 5 | "quoteBlock": "Blockquote", 6 | "superBlock": "Super block", 7 | "paragraph": "Paragraph", 8 | "doc": "Doc", 9 | "headings": "Headings", 10 | "list": "List", 11 | "listItem": "List item", 12 | "codeBlock": "Code block", 13 | "htmlBlock": "HTML block", 14 | "embedBlock": "Embed block", 15 | "database": "Database", 16 | "video": "Video", 17 | "audio": "Audio", 18 | "IFrame": "IFrame", 19 | "widget": "Widget", 20 | "name": "Name", 21 | "alias": "Alias", 22 | "memo": "Memo", 23 | "allAttrs": "All attribute names and attribute values", 24 | "sortByRankASC": "Relevance ASC", 25 | "sortByRankDESC": "Relevance DESC", 26 | "modifiedASC": "Modified Time ASC", 27 | "modifiedDESC": "Modified Time DESC", 28 | "createdASC": "Created Time ASC", 29 | "createdDESC": "Created Time DESC", 30 | "type": "Type", 31 | "sortByContent": "Original content order", 32 | "sortByTypeAndContent": "Type And Original content order", 33 | "show": "Show", 34 | "hide": "Hide", 35 | "refCountASC": "Ref Count ASC", 36 | "refCountDESC": "Ref Count DESC", 37 | "fileNameASC": "Name Alphabet ASC", 38 | "fileNameDESC": "Name Alphabet DESC", 39 | "fileNameNatASC": "Name Natural ASC", 40 | "fileNameNatDESC": "Name Natural DESC", 41 | "notebook": "Notebook", 42 | "path": "Path", 43 | "sort": "Sort", 44 | "clear": "Clear", 45 | "refresh": "Refresh", 46 | "reference": "Ref", 47 | "documentBasedSearch": "Document-based Search", 48 | "flatDocumentTree": "Flat Document Tree", 49 | "documentBasedSearchDock": "Document-based Search Dock", 50 | "flatDocumentTreeDock": "Flat Document Tree Dock", 51 | "previousLabel": "Previous", 52 | "nextLabel": "Next", 53 | "findInDoc": "Found ${x} documents", 54 | "notebookFilter": "Notebook Filter", 55 | "attr": "Attribute", 56 | "other": "Other", 57 | "expand": "Expand", 58 | "collapse": "Collapse", 59 | "noContentBelow": "No content matching the criteria exists below", 60 | "dockModifyTips": "Note: Modifying Dock will refresh the interface.", 61 | "settingDock": "🌈 Dock Settings", 62 | "settingNotebookFilter": "🌈 Notebook Filter", 63 | "settingType": "🌈 Type", 64 | "settingAttr": "🌈 Attribute", 65 | "settingOther": "🌈 Other", 66 | "docSortMethod": "Document Sorting Method", 67 | "contentBlockSortMethod": "Content Block Sorting Method", 68 | "documentsPerPage": "Documents Per Page", 69 | "blockCountBehaviorTips": "If the number of blocks in the query result is less than the current value, all documents will be expanded by default; otherwise, all documents will be collapsed by default.", 70 | "defaultExpansionCount": "Default Expansion Count", 71 | "alwaysExpandSingleDoc": "Always Expand Single Document", 72 | "displayDocBlock": "Display Document Blocks", 73 | "doubleClickTimeThreshold": "Double Click Time Threshold (Milliseconds)", 74 | "previewRefreshHighlightDelayTips": "For code blocks, databases, and other blocks that require time to render, too short a delay may fail. Set to 0 if not needed.", 75 | "previewRefreshHighlightDelay": "Preview Refresh Highlight Delay (Milliseconds)", 76 | "settingHub": "Settings Hub", 77 | "switchCurrentDocumentSearchFailureMessage": "Document Search Plugin: Failed to retrieve the opened document. You can switch tabs or reopen the document.", 78 | "swapDocumentItemClickLogic": "Swap Document Item Click Logic", 79 | "swapDocumentItemClickLogicTips": "When disabled, click to expand search results and double-click to open the document; when enabled, click to open the document and double-click to expand search results.", 80 | "allNotebooks": "All Notebooks", 81 | "specifyNotebook": "Specify Notebook", 82 | "searchInTheCurrentDocument": "Search in the current document" 83 | } -------------------------------------------------------------------------------- /src/utils/html-util.ts: -------------------------------------------------------------------------------- 1 | import { findScrollingElement } from "@/components/search/search-util"; 2 | import { isStrEmpty, isStrNotEmpty, isStrNotBlank } from "./string-util"; 3 | import { isArrayEmpty } from "./array-util"; 4 | 5 | export const escapeAttr = (html: string) => { 6 | return html.replace(/"/g, """).replace(/'/g, "'"); 7 | }; 8 | 9 | export function highlightBlockContent(block: Block, keywords: string[]) { 10 | if (!block) { 11 | return; 12 | } 13 | let contentHtml = getHighlightedContent(block.content, keywords); 14 | let nameHml = getHighlightedContent(block.name, keywords); 15 | let aliasHtml = getHighlightedContent(block.alias, keywords); 16 | let memoHtml = getHighlightedContent(block.memo, keywords); 17 | let tagHtml = getHighlightedContent(block.tag, keywords); 18 | block.content = contentHtml; 19 | block.name = nameHml; 20 | block.alias = aliasHtml; 21 | block.memo = memoHtml; 22 | block.tag = tagHtml; 23 | } 24 | 25 | function getHighlightedContent( 26 | content: string, 27 | keywords: string[], 28 | ): string { 29 | if (!content) { 30 | return content; 31 | } 32 | let highlightedContent: string = escapeHtml(content); 33 | 34 | if (keywords) { 35 | highlightedContent = highlightMatches(highlightedContent, keywords); 36 | } 37 | return highlightedContent; 38 | } 39 | 40 | function highlightMatches(content: string, keywords: string[]): string { 41 | if (isArrayEmpty(keywords) || isStrEmpty(content)) { 42 | return content; // 返回原始字符串,因为没有需要匹配的内容 43 | } 44 | keywords = keywords.filter(isStrNotBlank); 45 | if (isArrayEmpty(keywords)) { 46 | return content; 47 | } 48 | let escapeKeywords = []; 49 | for (const str of keywords) { 50 | escapeKeywords.push(str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); 51 | } 52 | let escapedKeywordRegExp = escapeKeywords.join("|") 53 | // console.log("escapedKeywordRegExp ", escapedKeywordRegExp) 54 | const regexPattern = new RegExp(`(${escapedKeywordRegExp})`, "gi"); 55 | const highlightedString = content.replace( 56 | regexPattern, 57 | "$1", 58 | ); 59 | return highlightedString; 60 | } 61 | 62 | function escapeHtml(input: string): string { 63 | const escapeMap: Record = { 64 | "&": "&", 65 | "<": "<", 66 | ">": ">", 67 | '"': """, 68 | "'": "'", 69 | }; 70 | 71 | return input.replace(/[&<>"']/g, (match) => escapeMap[match]); 72 | } 73 | 74 | export function isElementHidden(element: Element) { 75 | if (!element) { 76 | return false; 77 | } 78 | 79 | if (element.classList.contains("fn__none")) { 80 | return true; 81 | } 82 | 83 | return isElementHidden(element.parentElement); 84 | } 85 | 86 | 87 | export function scrollByRange(matchRange: Range, position: ScrollLogicalPosition) { 88 | if (!matchRange) { 89 | return; 90 | } 91 | position = position ? position : "center"; 92 | 93 | const matchElement = 94 | matchRange.commonAncestorContainer.parentElement; 95 | if (!matchElement) { 96 | return; 97 | } 98 | 99 | if ( 100 | matchElement.clientHeight > 101 | document.documentElement.clientHeight 102 | ) { 103 | // 特殊情况:如果一个段落中软换行非常多,此时如果定位到匹配节点的首行, 104 | // 是看不到查询的文本的,需要通过 Range 的精确位置进行定位。 105 | const scrollingElement = findScrollingElement(matchElement); 106 | const contentRect = scrollingElement.getBoundingClientRect(); 107 | let scrollTop = 108 | scrollingElement.scrollTop + 109 | matchRange.getBoundingClientRect().top - 110 | contentRect.top - 111 | contentRect.height / 2; 112 | scrollingElement.scrollTo({ 113 | top: scrollTop, 114 | behavior: "smooth", 115 | }); 116 | } else { 117 | matchElement.scrollIntoView({ 118 | behavior: "smooth", 119 | block: position, 120 | inline: position, 121 | }); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Plugin, 3 | openTab, 4 | } from "siyuan"; 5 | // import "@/index.scss"; 6 | 7 | import SearchPreviewSvelte from "@/components/search/search-preview-view.svelte"; 8 | import { CUSTOM_ICON_MAP } from "@/config/icon-constant"; 9 | import { SettingConfig } from "@/services/setting-config"; 10 | import { EnvConfig } from "@/config/env-config"; 11 | import "./index.scss" 12 | import { initDock } from "@/components/dock/dock-util"; 13 | import { openSettingsDialog } from "@/components/setting/setting-util"; 14 | 15 | 16 | const SEARCH_TAB_TYPE = "search_home_tab"; 17 | 18 | 19 | export default class PluginSample extends Plugin { 20 | 21 | private documentSearchTab: SearchPreviewSvelte; 22 | 23 | async onload() { 24 | EnvConfig.ins.init(this); 25 | await SettingConfig.ins.load(this); 26 | initDock(); 27 | 28 | 29 | // 图标的制作参见帮助文档 30 | for (const key in CUSTOM_ICON_MAP) { 31 | if (Object.prototype.hasOwnProperty.call(CUSTOM_ICON_MAP, key)) { 32 | const item = CUSTOM_ICON_MAP[key]; 33 | this.addIcons(item.source); 34 | } 35 | } 36 | 37 | if (!EnvConfig.ins.isMobile) { 38 | this.addTopBar({ 39 | icon: CUSTOM_ICON_MAP.iconDocumentSearch.id, 40 | title: this.i18n.documentBasedSearch, 41 | position: "right", 42 | callback: () => { 43 | this.openDocumentSearchTab(); 44 | } 45 | }); 46 | } 47 | 48 | 49 | // this.openSetting.bind(this); 50 | 51 | this.addCommand({ 52 | langKey: EnvConfig.ins.i18n.openDocumentSearchTab, 53 | langText: EnvConfig.ins.i18n.openDocumentSearchTab, 54 | hotkey: "⇧⌘Q", 55 | callback: () => { 56 | this.openDocumentSearchTab(); 57 | }, 58 | }); 59 | 60 | // this.addIcons(CUSTOM_ICON_MAP.iconDocumentSearch.source); 61 | this.addDocumentSearchTab(); 62 | 63 | this.eventBus.on('switch-protyle', (e: any) => { 64 | // console.log("switch-protyle " + JSON.stringify(e.detail.protyle.block)); 65 | EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; 66 | // utils.setCurrentBoxId(e.detail.protyle.notebookId) 67 | }) 68 | 69 | EnvConfig.ins.plugin.eventBus.on("loaded-protyle-static", (e: any) => { 70 | // console.log("loaded-protyle-static e : ", e) 71 | if (!EnvConfig.ins.lastViewedDocId) { 72 | EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; 73 | } 74 | }); 75 | 76 | } 77 | 78 | openSetting(): void { 79 | openSettingsDialog("settingHub"); 80 | } 81 | 82 | onLayoutReady() { 83 | 84 | } 85 | 86 | async onunload() { 87 | 88 | } 89 | 90 | private addDocumentSearchTab() { 91 | let _this = this; 92 | this.addTab({ 93 | type: SEARCH_TAB_TYPE, 94 | init() { 95 | _this.documentSearchTab = new SearchPreviewSvelte({ 96 | target: this.element, 97 | props: { currentTab: this } 98 | }); 99 | }, 100 | beforeDestroy() { 101 | }, 102 | destroy() { 103 | }, 104 | resize() { 105 | if (_this.documentSearchTab) { 106 | _this.documentSearchTab.resize(); 107 | } 108 | }, 109 | }); 110 | } 111 | 112 | private openDocumentSearchTab() { 113 | let documentSearchTab: SearchPreviewSvelte = this.documentSearchTab; 114 | openTab({ 115 | app: this.app, 116 | custom: { 117 | id: this.name + SEARCH_TAB_TYPE, 118 | icon: CUSTOM_ICON_MAP.iconDocumentSearch.id, 119 | title: `Document Search`, 120 | }, 121 | afterOpen() { 122 | if (documentSearchTab) { 123 | documentSearchTab.resize(); 124 | } 125 | } 126 | }); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/lib/siyuan/menus/Menu.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/siyuan-note/siyuan/blob/171bfd62a70698879057742c2813f54f84164b66/app/src/menus/Menu.ts#L162 2 | export class MenuItem { 3 | public element: HTMLElement; 4 | 5 | constructor(options: IMenu) { 6 | if (options.ignore) { 7 | return; 8 | } 9 | if (options.type === "empty") { 10 | this.element = document.createElement("div"); 11 | this.element.innerHTML = options.label; 12 | if (options.bind) { 13 | options.bind(this.element); 14 | } 15 | return; 16 | } 17 | 18 | this.element = document.createElement("button"); 19 | if (options.disabled) { 20 | this.element.setAttribute("disabled", "disabled"); 21 | } 22 | if (options.id) { 23 | this.element.setAttribute("data-id", options.id); 24 | } 25 | if (options.type === "separator") { 26 | this.element.classList.add("b3-menu__separator"); 27 | return; 28 | } 29 | this.element.classList.add("b3-menu__item"); 30 | if (options.current) { 31 | this.element.classList.add("b3-menu__item--selected"); 32 | } 33 | if (options.click) { 34 | // 需使用 click,否则移动端无法滚动 35 | this.element.addEventListener("click", (event) => { 36 | if (this.element.getAttribute("disabled")) { 37 | return; 38 | } 39 | let keepOpen = options.click(this.element, event); 40 | if (keepOpen instanceof Promise) { 41 | keepOpen = false; 42 | } 43 | event.preventDefault(); 44 | event.stopImmediatePropagation(); 45 | event.stopPropagation(); 46 | if (this.element.parentElement && !keepOpen) { 47 | window.siyuan.menus.menu.remove(); 48 | } 49 | }); 50 | } 51 | if (options.type === "readonly") { 52 | this.element.classList.add("b3-menu__item--readonly"); 53 | } 54 | if (options.icon === "iconTrashcan" || options.warning) { 55 | this.element.classList.add("b3-menu__item--warning"); 56 | } 57 | 58 | if (options.element) { 59 | this.element.append(options.element); 60 | } else { 61 | let html = `${options.label || " "}`; 62 | if (typeof options.iconHTML === "string") { 63 | html = options.iconHTML + html; 64 | } else { 65 | html = `${html}`; 66 | } 67 | if (options.accelerator) { 68 | html += `${options.accelerator}`; 69 | } 70 | if (options.action) { 71 | html += ``; 72 | } 73 | if (options.checked) { 74 | html += ''; 75 | } 76 | this.element.innerHTML = html; 77 | } 78 | 79 | if (options.bind) { 80 | // 主题 rem craft 需要使用 b3-menu__item--custom 来区分自定义菜单 by 281261361 81 | this.element.classList.add("b3-menu__item--custom"); 82 | options.bind(this.element); 83 | } 84 | 85 | if (options.submenu) { 86 | const submenuElement = document.createElement("div"); 87 | submenuElement.classList.add("b3-menu__submenu"); 88 | submenuElement.innerHTML = '
'; 89 | options.submenu.forEach((item) => { 90 | submenuElement.firstElementChild.append(new MenuItem(item).element); 91 | }); 92 | this.element.insertAdjacentHTML("beforeend", ''); 93 | this.element.append(submenuElement); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig, loadEnv } from "vite" 3 | import minimist from "minimist" 4 | import { viteStaticCopy } from "vite-plugin-static-copy" 5 | import livereload from "rollup-plugin-livereload" 6 | import { svelte } from "@sveltejs/vite-plugin-svelte" 7 | import zipPack from "vite-plugin-zip-pack"; 8 | import fg from 'fast-glob'; 9 | 10 | const args = minimist(process.argv.slice(2)) 11 | const isWatch = args.watch || args.w || false 12 | const devDistDir = "dev" 13 | const distDir = isWatch ? devDistDir : "dist" 14 | 15 | console.log("isWatch=>", isWatch) 16 | console.log("distDir=>", distDir) 17 | 18 | export default defineConfig({ 19 | resolve: { 20 | alias: { 21 | "@": resolve(__dirname, "src"), 22 | } 23 | }, 24 | 25 | plugins: [ 26 | svelte(), 27 | 28 | viteStaticCopy({ 29 | targets: [ 30 | { 31 | src: "./README*.md", 32 | dest: "./", 33 | }, 34 | { 35 | src: "./plugin.json", 36 | dest: "./", 37 | }, 38 | { 39 | src: "./preview.png", 40 | dest: "./", 41 | }, 42 | { 43 | src: "./icon.png", 44 | dest: "./", 45 | }, 46 | { 47 | src: "./src/i18n/**", 48 | dest: "./i18n/", 49 | }, 50 | ], 51 | }), 52 | ], 53 | 54 | // https://github.com/vitejs/vite/issues/1930 55 | // https://vitejs.dev/guide/env-and-mode.html#env-files 56 | // https://github.com/vitejs/vite/discussions/3058#discussioncomment-2115319 57 | // 在这里自定义变量 58 | define: { 59 | "process.env.DEV_MODE": `"${isWatch}"`, 60 | "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) 61 | }, 62 | 63 | build: { 64 | // 输出路径 65 | outDir: distDir, 66 | emptyOutDir: false, 67 | 68 | // 构建后是否生成 source map 文件 69 | sourcemap: isWatch ? 'inline' : false, 70 | 71 | // 设置为 false 可以禁用最小化混淆 72 | // 或是用来指定是应用哪种混淆器 73 | // boolean | 'terser' | 'esbuild' 74 | // 不压缩,用于调试 75 | minify: !isWatch, 76 | 77 | lib: { 78 | // Could also be a dictionary or array of multiple entry points 79 | entry: resolve(__dirname, "src/index.ts"), 80 | // the proper extensions will be added 81 | fileName: "index", 82 | formats: ["cjs"], 83 | }, 84 | rollupOptions: { 85 | plugins: [ 86 | ...( 87 | isWatch ? [ 88 | livereload(devDistDir), 89 | { 90 | //监听静态资源文件 91 | name: 'watch-external', 92 | async buildStart() { 93 | const files = await fg([ 94 | 'src/i18n/*.json', 95 | './README*.md', 96 | './plugin.json' 97 | ]); 98 | for (let file of files) { 99 | this.addWatchFile(file); 100 | } 101 | } 102 | } 103 | ] : [ 104 | zipPack({ 105 | inDir: './dist', 106 | outDir: './', 107 | outFileName: 'package.zip' 108 | }) 109 | ] 110 | ) 111 | ], 112 | 113 | // make sure to externalize deps that shouldn't be bundled 114 | // into your library 115 | external: ["siyuan", "process"], 116 | 117 | output: { 118 | entryFileNames: "[name].js", 119 | assetFileNames: (assetInfo) => { 120 | if (assetInfo.name === "style.css") { 121 | return "index.css" 122 | } 123 | return assetInfo.name 124 | }, 125 | }, 126 | }, 127 | } 128 | }) -------------------------------------------------------------------------------- /src/components/dock/dock-util.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ICON_MAP } from "@/config/icon-constant"; 2 | import DocSearchDockSvelte from "@/components/dock/doc-search-dock.svelte"; 3 | import FlatDocTreeDockSvelte from "@/components/dock/flat-doc-tree-dock.svelte"; 4 | import { EnvConfig } from "@/config/env-config"; 5 | import { SettingConfig } from "@/services/setting-config"; 6 | 7 | const DOC_SEARCH_DOCK_TYPE = "doc_search_dock"; 8 | const FLAT_DOC_TREE_DOCK_TYPE = "flat_doc_tree_dock"; 9 | 10 | 11 | export function initDock() { 12 | addDocSearchDock(); 13 | addFlatDocTreeDock(); 14 | } 15 | 16 | function addDocSearchDock() { 17 | if (!EnvConfig.ins || !EnvConfig.ins.plugin) { 18 | console.log("添加搜索 dock 失败。") 19 | return; 20 | } 21 | let docSearchDockPoisition = SettingConfig.ins.docSearchDockPoisition; 22 | if (!docSearchDockPoisition || docSearchDockPoisition === "Hidden") { 23 | console.log("不添加搜索 dock") 24 | return; 25 | } 26 | let position: any = docSearchDockPoisition; 27 | 28 | let plugin = EnvConfig.ins.plugin; 29 | let docSearchSvelet: DocSearchDockSvelte; 30 | let dockRet = plugin.addDock({ 31 | config: { 32 | position: position, 33 | size: { width: 300, height: 0 }, 34 | icon: CUSTOM_ICON_MAP.iconDocumentSearch.id, 35 | title: EnvConfig.ins.i18n.documentBasedSearchDock, 36 | show: false, 37 | }, 38 | data: {}, 39 | type: DOC_SEARCH_DOCK_TYPE, 40 | resize() { 41 | if (docSearchSvelet) { 42 | docSearchSvelet.resize(this.element.clientWidth); 43 | } 44 | }, 45 | update() { 46 | }, 47 | init() { 48 | this.element.innerHTML = ""; 49 | docSearchSvelet = new DocSearchDockSvelte({ 50 | target: this.element, 51 | props: { 52 | } 53 | }); 54 | }, 55 | destroy() { 56 | } 57 | }); 58 | 59 | plugin.addCommand({ 60 | langKey: DOC_SEARCH_DOCK_TYPE + "_mapkey", 61 | langText: EnvConfig.ins.i18n.documentBasedSearchDock, 62 | hotkey: "⌥Q", 63 | callback: () => { 64 | const ele = document.querySelector( 65 | `span[data-type="${plugin.name + DOC_SEARCH_DOCK_TYPE}"]`, 66 | ) as HTMLElement; 67 | if (ele) { 68 | ele.click(); 69 | } 70 | if (docSearchSvelet) { 71 | docSearchSvelet.iconClick(); 72 | } 73 | }, 74 | }); 75 | 76 | EnvConfig.ins.docSearchDock = dockRet; 77 | } 78 | 79 | function addFlatDocTreeDock() { 80 | if (!EnvConfig.ins || !EnvConfig.ins.plugin) { 81 | console.log("添加扁平文档树 dock 失败。") 82 | return; 83 | } 84 | let flatDocTreeDockPoisition = SettingConfig.ins.flatDocTreeDockPoisition; 85 | if (!flatDocTreeDockPoisition || flatDocTreeDockPoisition === "Hidden") { 86 | console.log("不添加扁平化文档树 dock") 87 | return; 88 | } 89 | let position: any = flatDocTreeDockPoisition; 90 | 91 | let plugin = EnvConfig.ins.plugin; 92 | let flatDocTreeSvelte: FlatDocTreeDockSvelte; 93 | let dockRet = plugin.addDock({ 94 | config: { 95 | position: position, 96 | size: { width: 270, height: 0 }, 97 | icon: CUSTOM_ICON_MAP.iconFlatDocTree.id, 98 | title: EnvConfig.ins.i18n.flatDocumentTreeDock, 99 | }, 100 | data: { 101 | }, 102 | type: FLAT_DOC_TREE_DOCK_TYPE, 103 | resize() { 104 | if (flatDocTreeSvelte) { 105 | flatDocTreeSvelte.resize(this.element.clientWidth); 106 | } 107 | }, 108 | update() { 109 | }, 110 | init() { 111 | this.element.innerHTML = ""; 112 | this.element.classList.add("fn__flex-1", "fn__flex-column", "file-tree", "layout__tab--active"); 113 | flatDocTreeSvelte = new FlatDocTreeDockSvelte({ 114 | target: this.element, 115 | props: { 116 | } 117 | }); 118 | if (EnvConfig.ins.isMobile) { 119 | flatDocTreeSvelte.resize(1); 120 | } 121 | }, 122 | destroy() { 123 | } 124 | }); 125 | 126 | plugin.addCommand({ 127 | langKey: FLAT_DOC_TREE_DOCK_TYPE + "_mapkey", 128 | langText: EnvConfig.ins.i18n.documentBasedSearchDock, 129 | hotkey: "⌥E", 130 | callback: () => { 131 | const ele = document.querySelector( 132 | `span[data-type="${plugin.name + FLAT_DOC_TREE_DOCK_TYPE}"]`, 133 | ) as HTMLElement; 134 | if (ele) { 135 | ele.click(); 136 | } 137 | if (flatDocTreeSvelte) { 138 | flatDocTreeSvelte.iconClick(); 139 | } 140 | }, 141 | }); 142 | 143 | EnvConfig.ins.flatDocTreeDock = dockRet; 144 | 145 | } -------------------------------------------------------------------------------- /src/utils/icon-util.ts: -------------------------------------------------------------------------------- 1 | import { isStrEmpty } from "./string-util"; 2 | 3 | 4 | 5 | export function getNotebookIcon(iconStr: string): string { 6 | let icon: string = null; 7 | icon = convertIconInIal(iconStr); 8 | 9 | // 目前用的系统自带多选框,无法渲染 html。。笔记本图标转换成默认。 10 | if (isStrEmpty(icon) || icon.startsWith("${icon}`; 32 | } 33 | return icon; 34 | } 35 | 36 | export function convertIconInIal(icon: string): string { 37 | if (isStrEmpty(icon)) { 38 | return null; 39 | } 40 | 41 | if (icon.includes(".")) { 42 | // 如果包含 ".",则认为是图片,生成标签 43 | return ``; 44 | } else if (icon.startsWith("api/icon/")) { 45 | return ``; 46 | } else { 47 | // 如果是Emoji,转换为表情符号 48 | let emoji = ""; 49 | try { 50 | icon.split("-").forEach(item => { 51 | if (item.length < 5) { 52 | emoji += String.fromCodePoint(parseInt("0" + item, 16)); 53 | } else { 54 | emoji += String.fromCodePoint(parseInt(item, 16)); 55 | } 56 | }); 57 | 58 | } catch (e) { 59 | // 自定义表情搜索报错 https://github.com/siyuan-note/siyuan/issues/5883 60 | // 这里忽略错误不做处理 61 | } 62 | return emoji; 63 | } 64 | } 65 | 66 | export function convertIalStringToObject(ialStr: string): { [key: string]: string } { 67 | const obj: { [key: string]: string } = {}; 68 | 69 | // 去掉开头和结尾的大括号 70 | const trimmedInput = ialStr.slice(2, -1); 71 | 72 | // 使用正则表达式解析键值对 73 | const regex = /(\w+)="([^"]*)"/g; 74 | let match; 75 | 76 | while ((match = regex.exec(trimmedInput)) !== null) { 77 | const key = match[1]; 78 | const value = match[2]; 79 | obj[key] = value; 80 | } 81 | 82 | return obj; 83 | } 84 | 85 | 86 | 87 | export function getBlockTypeIconHref(type: string, subType: string): string { 88 | let iconHref = ""; 89 | if (type) { 90 | if (type === "d") { 91 | iconHref = "#iconFile"; 92 | } else if (type === "h") { 93 | if (subType === "h1") { 94 | iconHref = "#iconH1"; 95 | } else if (subType === "h2") { 96 | iconHref = "#iconH2"; 97 | } else if (subType === "h3") { 98 | iconHref = "#iconH3"; 99 | } else if (subType === "h4") { 100 | iconHref = "#iconH4"; 101 | } else if (subType === "h5") { 102 | iconHref = "#iconH5"; 103 | } else if (subType === "h6") { 104 | iconHref = "#iconH6"; 105 | } 106 | } else if (type === "c") { 107 | iconHref = "#iconCode"; 108 | } else if (type === "html") { 109 | iconHref = "#iconHTML5"; 110 | } else if (type === "p") { 111 | iconHref = "#iconParagraph"; 112 | } else if (type === "m") { 113 | iconHref = "#iconMath"; 114 | } else if (type === "t") { 115 | iconHref = "#iconTable"; 116 | } else if (type === "b") { 117 | iconHref = "#iconQuote"; 118 | } else if (type === "l") { 119 | if (subType === "o") { 120 | iconHref = "#iconOrderedList"; 121 | } else if (subType === "u") { 122 | iconHref = "#iconList"; 123 | } else if (subType === "t") { 124 | iconHref = "#iconCheck"; 125 | } 126 | } else if (type === "i") { 127 | iconHref = "#iconListItem"; 128 | } else if (type === "av") { 129 | iconHref = "#iconDatabase"; 130 | } else if (type === "s") { 131 | iconHref = "#iconSuper"; 132 | } else if (type === "audio") { 133 | iconHref = "#iconRecord"; 134 | } else if (type === "video") { 135 | iconHref = "#iconVideo"; 136 | } else if (type === "query_embed") { 137 | iconHref = "#iconSQL"; 138 | } else if (type === "tb") { 139 | iconHref = "#iconLine"; 140 | } else if (type === "widget") { 141 | iconHref = "#iconBoth"; 142 | } else if (type === "iframe") { 143 | iconHref = "#iconLanguage"; 144 | } 145 | } 146 | return iconHref; 147 | } -------------------------------------------------------------------------------- /scripts/update_version.js: -------------------------------------------------------------------------------- 1 | // const fs = require('fs'); 2 | // const path = require('path'); 3 | // const readline = require('readline'); 4 | import fs from 'node:fs'; 5 | import path from 'node:path'; 6 | import readline from 'node:readline'; 7 | 8 | // Utility to read JSON file 9 | function readJsonFile(filePath) { 10 | return new Promise((resolve, reject) => { 11 | fs.readFile(filePath, 'utf8', (err, data) => { 12 | if (err) return reject(err); 13 | try { 14 | const jsonData = JSON.parse(data); 15 | resolve(jsonData); 16 | } catch (e) { 17 | reject(e); 18 | } 19 | }); 20 | }); 21 | } 22 | 23 | // Utility to write JSON file 24 | function writeJsonFile(filePath, jsonData) { 25 | return new Promise((resolve, reject) => { 26 | fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), 'utf8', (err) => { 27 | if (err) return reject(err); 28 | resolve(); 29 | }); 30 | }); 31 | } 32 | 33 | // Utility to prompt the user for input 34 | function promptUser(query) { 35 | const rl = readline.createInterface({ 36 | input: process.stdin, 37 | output: process.stdout 38 | }); 39 | return new Promise((resolve) => rl.question(query, (answer) => { 40 | rl.close(); 41 | resolve(answer); 42 | })); 43 | } 44 | 45 | // Function to parse the version string 46 | function parseVersion(version) { 47 | const [major, minor, patch] = version.split('.').map(Number); 48 | return { major, minor, patch }; 49 | } 50 | 51 | // Function to auto-increment version parts 52 | function incrementVersion(version, type) { 53 | let { major, minor, patch } = parseVersion(version); 54 | 55 | switch (type) { 56 | case 'major': 57 | major++; 58 | minor = 0; 59 | patch = 0; 60 | break; 61 | case 'minor': 62 | minor++; 63 | patch = 0; 64 | break; 65 | case 'patch': 66 | patch++; 67 | break; 68 | default: 69 | break; 70 | } 71 | 72 | return `${major}.${minor}.${patch}`; 73 | } 74 | 75 | // Main script 76 | (async function () { 77 | try { 78 | const pluginJsonPath = path.join(process.cwd(), 'plugin.json'); 79 | const packageJsonPath = path.join(process.cwd(), 'package.json'); 80 | 81 | // Read both JSON files 82 | const pluginData = await readJsonFile(pluginJsonPath); 83 | const packageData = await readJsonFile(packageJsonPath); 84 | 85 | // Get the current version from both files (assuming both have the same version) 86 | const currentVersion = pluginData.version || packageData.version; 87 | console.log(`\n🌟 Current version: \x1b[36m${currentVersion}\x1b[0m\n`); 88 | 89 | // Calculate potential new versions for auto-update 90 | const newPatchVersion = incrementVersion(currentVersion, 'patch'); 91 | const newMinorVersion = incrementVersion(currentVersion, 'minor'); 92 | const newMajorVersion = incrementVersion(currentVersion, 'major'); 93 | 94 | // Prompt the user with formatted options 95 | console.log('🔄 How would you like to update the version?\n'); 96 | console.log(` 1️⃣ Auto update \x1b[33mpatch\x1b[0m version (new version: \x1b[32m${newPatchVersion}\x1b[0m)`); 97 | console.log(` 2️⃣ Auto update \x1b[33mminor\x1b[0m version (new version: \x1b[32m${newMinorVersion}\x1b[0m)`); 98 | console.log(` 3️⃣ Auto update \x1b[33mmajor\x1b[0m version (new version: \x1b[32m${newMajorVersion}\x1b[0m)`); 99 | console.log(` 4️⃣ Input version \x1b[33mmanually\x1b[0m`); 100 | // Press 0 to skip version update 101 | console.log(' 0️⃣ Quit without updating\n'); 102 | 103 | const updateChoice = await promptUser('👉 Please choose (1/2/3/4): '); 104 | 105 | let newVersion; 106 | 107 | switch (updateChoice.trim()) { 108 | case '1': 109 | newVersion = newPatchVersion; 110 | break; 111 | case '2': 112 | newVersion = newMinorVersion; 113 | break; 114 | case '3': 115 | newVersion = newMajorVersion; 116 | break; 117 | case '4': 118 | newVersion = await promptUser('✍️ Please enter the new version (in a.b.c format): '); 119 | break; 120 | case '0': 121 | console.log('\n🛑 Skipping version update.'); 122 | return; 123 | default: 124 | console.log('\n❌ Invalid option, no version update.'); 125 | return; 126 | } 127 | 128 | // Update the version in both plugin.json and package.json 129 | pluginData.version = newVersion; 130 | packageData.version = newVersion; 131 | 132 | // Write the updated JSON back to files 133 | await writeJsonFile(pluginJsonPath, pluginData); 134 | await writeJsonFile(packageJsonPath, packageData); 135 | 136 | console.log(`\n✅ Version successfully updated to: \x1b[32m${newVersion}\x1b[0m\n`); 137 | 138 | } catch (error) { 139 | console.error('❌ Error:', error); 140 | } 141 | })(); -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-09-06 17:42:57 5 | * @FilePath : /scripts/utils.js 6 | * @LastEditTime : 2024-09-06 19:23:12 7 | * @Description : 8 | */ 9 | // common.js 10 | import fs from 'fs'; 11 | import path from 'node:path'; 12 | import http from 'node:http'; 13 | import readline from 'node:readline'; 14 | 15 | // Logging functions 16 | export const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 17 | export const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 18 | 19 | // HTTP POST headers 20 | export const POST_HEADER = { 21 | "Content-Type": "application/json", 22 | }; 23 | 24 | // Fetch function compatible with older Node.js versions 25 | export async function myfetch(url, options) { 26 | return new Promise((resolve, reject) => { 27 | let req = http.request(url, options, (res) => { 28 | let data = ''; 29 | res.on('data', (chunk) => { 30 | data += chunk; 31 | }); 32 | res.on('end', () => { 33 | resolve({ 34 | ok: true, 35 | status: res.statusCode, 36 | json: () => JSON.parse(data) 37 | }); 38 | }); 39 | }); 40 | req.on('error', (e) => { 41 | reject(e); 42 | }); 43 | req.end(); 44 | }); 45 | } 46 | 47 | /** 48 | * Fetch SiYuan workspaces from port 6806 49 | * @returns {Promise} 50 | */ 51 | export async function getSiYuanDir() { 52 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 53 | let conf = {}; 54 | try { 55 | let response = await myfetch(url, { 56 | method: 'POST', 57 | headers: POST_HEADER 58 | }); 59 | if (response.ok) { 60 | conf = await response.json(); 61 | } else { 62 | error(`\tHTTP-Error: ${response.status}`); 63 | return null; 64 | } 65 | } catch (e) { 66 | error(`\tError: ${e}`); 67 | error("\tPlease make sure SiYuan is running!!!"); 68 | return null; 69 | } 70 | return conf?.data; // 保持原始返回值 71 | } 72 | 73 | /** 74 | * Choose target workspace 75 | * @param {{path: string}[]} workspaces 76 | * @returns {string} The path of the selected workspace 77 | */ 78 | export async function chooseTarget(workspaces) { 79 | let count = workspaces.length; 80 | log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`); 81 | workspaces.forEach((workspace, i) => { 82 | log(`\t[${i}] ${workspace.path}`); 83 | }); 84 | 85 | if (count === 1) { 86 | return `${workspaces[0].path}/data/plugins`; 87 | } else { 88 | const rl = readline.createInterface({ 89 | input: process.stdin, 90 | output: process.stdout 91 | }); 92 | let index = await new Promise((resolve) => { 93 | rl.question(`\tPlease select a workspace[0-${count - 1}]: `, (answer) => { 94 | resolve(answer); 95 | }); 96 | }); 97 | rl.close(); 98 | return `${workspaces[index].path}/data/plugins`; 99 | } 100 | } 101 | 102 | /** 103 | * Check if two paths are the same 104 | * @param {string} path1 105 | * @param {string} path2 106 | * @returns {boolean} 107 | */ 108 | export function cmpPath(path1, path2) { 109 | path1 = path1.replace(/\\/g, '/'); 110 | path2 = path2.replace(/\\/g, '/'); 111 | if (path1[path1.length - 1] !== '/') { 112 | path1 += '/'; 113 | } 114 | if (path2[path2.length - 1] !== '/') { 115 | path2 += '/'; 116 | } 117 | return path1 === path2; 118 | } 119 | 120 | export function getThisPluginName() { 121 | if (!fs.existsSync('./plugin.json')) { 122 | process.chdir('../'); 123 | if (!fs.existsSync('./plugin.json')) { 124 | error('Failed! plugin.json not found'); 125 | return null; 126 | } 127 | } 128 | 129 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 130 | const name = plugin?.name; 131 | if (!name) { 132 | error('Failed! Please set plugin name in plugin.json'); 133 | return null; 134 | } 135 | 136 | return name; 137 | } 138 | 139 | export function copyDirectory(srcDir, dstDir) { 140 | if (!fs.existsSync(dstDir)) { 141 | fs.mkdirSync(dstDir); 142 | log(`Created directory ${dstDir}`); 143 | } 144 | 145 | fs.readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { 146 | const src = path.join(srcDir, file.name); 147 | const dst = path.join(dstDir, file.name); 148 | 149 | if (file.isDirectory()) { 150 | copyDirectory(src, dst); 151 | } else { 152 | fs.copyFileSync(src, dst); 153 | log(`Copied file: ${src} --> ${dst}`); 154 | } 155 | }); 156 | log(`All files copied!`); 157 | } 158 | 159 | 160 | export function makeSymbolicLink(srcPath, targetPath) { 161 | if (!fs.existsSync(targetPath)) { 162 | // fs.symlinkSync(srcPath, targetPath, 'junction'); 163 | //Go 1.23 no longer supports junctions as symlinks 164 | //Please refer to https://github.com/siyuan-note/siyuan/issues/12399 165 | fs.symlinkSync(srcPath, targetPath, 'dir'); 166 | log(`Done! Created symlink ${targetPath}`); 167 | return; 168 | } 169 | 170 | //Check the existed target path 171 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 172 | if (!isSymbol) { 173 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 174 | return; 175 | } 176 | let existedPath = fs.readlinkSync(targetPath); 177 | if (cmpPath(existedPath, srcPath)) { 178 | log(`Good! ${targetPath} is already linked to ${srcPath}`); 179 | } else { 180 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${existedPath}`); 181 | } 182 | } -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2023 frostime. All rights reserved. 3 | */ 4 | 5 | /** 6 | * Frequently used data structures in SiYuan 7 | */ 8 | type DocumentId = string; 9 | type BlockId = string; 10 | type NotebookId = string; 11 | type PreviousID = BlockId; 12 | type ParentID = BlockId | DocumentId; 13 | 14 | type Notebook = { 15 | id: NotebookId; 16 | name: string; 17 | icon: string; 18 | sort: number; 19 | closed: boolean; 20 | } 21 | 22 | type NotebookConf = { 23 | name: string; 24 | closed: boolean; 25 | refCreateSavePath: string; 26 | createDocNameTemplate: string; 27 | dailyNoteSavePath: string; 28 | dailyNoteTemplatePath: string; 29 | } 30 | 31 | // type BlockType = "d" | "s" | "h" | "t" | "i" | "p" | "f" | "audio" | "video" | "other"; 32 | 33 | type BlockType = "d" // 文档 34 | | "h" // 标题 35 | | "l" // 列表 36 | | "i" // 列表项 37 | | "c" // 代码块 38 | | "m" // 数学公式 39 | | "t" // 表格 40 | | "b" // 引述 41 | | "av" // 属性视图(数据库) 42 | | "s" // 超级块 43 | | "p" // 段落 44 | | "tb" // 表格 45 | | "html" // HTML 46 | | "video" // 视频 47 | | "audio" // 音频 48 | | "widget" // 挂件 49 | | "iframe" // iframe 50 | | "query_embed" // 嵌入块 51 | ; 52 | 53 | type BlockSubType = "o" | "u" | "t" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 54 | 55 | type Block = { 56 | id: BlockId; 57 | parent_id?: BlockId; 58 | root_id: DocumentId; 59 | hash: string; 60 | box: string; 61 | path: string; 62 | hpath: string; 63 | name: string; 64 | alias: string; 65 | memo: string; 66 | tag: string; 67 | content: string; 68 | fcontent?: string; 69 | markdown: string; 70 | length: number; 71 | type: BlockType; 72 | subtype: BlockSubType; 73 | /** string of { [key: string]: string } 74 | * For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}" 75 | */ 76 | ial?: string; 77 | sort: number; 78 | created: string; 79 | updated: string; 80 | } 81 | 82 | type doOperation = { 83 | action: string; 84 | data: string; 85 | id: BlockId; 86 | parentID: BlockId | DocumentId; 87 | previousID: BlockId; 88 | retData: null; 89 | } 90 | 91 | interface Window { 92 | echarts: { 93 | init(element: HTMLElement, theme?: string, options?: { 94 | width: number 95 | }): { 96 | setOption(option: any): void; 97 | getZr(): any; 98 | on(name: string, event: (e: any) => void): any; 99 | containPixel(name: string, position: number[]): any; 100 | resize(): void; 101 | }; 102 | dispose(element: Element): void; 103 | getInstanceById(id: string): { 104 | resize: () => void 105 | clear: () => void 106 | getOption: () => { series: { type: string }[] } 107 | }; 108 | } 109 | ABCJS: { 110 | renderAbc(element: Element, text: string, options: { 111 | responsive: string 112 | }): void; 113 | } 114 | hljs: { 115 | listLanguages(): string[]; 116 | highlight(text: string, options: { 117 | language?: string, 118 | ignoreIllegals: boolean 119 | }): { 120 | value: string 121 | }; 122 | getLanguage(text: string): { 123 | name: string 124 | }; 125 | }; 126 | katex: { 127 | renderToString(math: string, option: { 128 | displayMode: boolean; 129 | output: string; 130 | macros: IObject; 131 | trust: boolean; 132 | strict: (errorCode: string) => "ignore" | "warn"; 133 | }): string; 134 | } 135 | mermaid: { 136 | initialize(options: any): void, 137 | render(id: string, text: string): { svg: string } 138 | }; 139 | plantumlEncoder: { 140 | encode(options: string): string, 141 | }; 142 | pdfjsLib: any 143 | 144 | dataLayer: any[] 145 | 146 | siyuan: ISiyuan 147 | webkit: { 148 | messageHandlers: { 149 | openLink: { postMessage: (url: string) => void } 150 | startKernelFast: { postMessage: (url: string) => void } 151 | changeStatusBar: { postMessage: (url: string) => void } 152 | setClipboard: { postMessage: (url: string) => void } 153 | purchase: { postMessage: (url: string) => void } 154 | } 155 | } 156 | htmlToImage: { 157 | toCanvas: (element: Element) => Promise 158 | toBlob: (element: Element) => Promise 159 | }; 160 | JSAndroid: { 161 | returnDesktop(): void 162 | openExternal(url: string): void 163 | exportByDefault(url: string): void 164 | changeStatusBarColor(color: string, mode: number): void 165 | writeClipboard(text: string): void 166 | writeHTMLClipboard(text: string, html: string): void 167 | writeImageClipboard(uri: string): void 168 | readClipboard(): string 169 | readHTMLClipboard(): string 170 | getBlockURL(): string 171 | } 172 | JSHarmony: { 173 | openExternal(url: string): void 174 | exportByDefault(url: string): void 175 | changeStatusBarColor(color: string, mode: number): void 176 | writeClipboard(text: string): void 177 | writeHTMLClipboard(text: string, html: string): void 178 | readClipboard(): string 179 | readHTMLClipboard(): string 180 | returnDesktop(): void 181 | } 182 | 183 | Protyle: import("../protyle/method").default 184 | 185 | goBack(): void 186 | 187 | showMessage(message: string, timeout: number, type: string, messageId?: string): void 188 | 189 | reconnectWebSocket(): void 190 | 191 | showKeyboardToolbar(height: number): void 192 | 193 | processIOSPurchaseResponse(code: number): void 194 | 195 | hideKeyboardToolbar(): void 196 | 197 | openFileByURL(URL: string): boolean 198 | 199 | destroyTheme(): Promise 200 | } 201 | 202 | 203 | interface IMenu { 204 | checked?: boolean, 205 | iconClass?: string, 206 | label?: string, 207 | click?: (element: HTMLElement, event: MouseEvent) => boolean | void | Promise 208 | type?: "separator" | "submenu" | "readonly" | "empty", 209 | accelerator?: string, 210 | action?: string, 211 | id?: string, 212 | submenu?: IMenu[] 213 | disabled?: boolean 214 | icon?: string 215 | iconHTML?: string 216 | current?: boolean 217 | bind?: (element: HTMLElement) => void 218 | index?: number 219 | element?: HTMLElement 220 | ignore?: boolean 221 | warning?: boolean 222 | } 223 | 224 | 225 | type DockPosition = "Hidden" | "LeftTop" | "LeftBottom" | "RightTop" | "RightBottom" | "BottomLeft" | "BottomRight"; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [中文](README_zh_CN.md) 2 | 3 | 4 | ## Document-based Search 5 | ### Features 6 | * Search based on documents 7 | * Ability to set the type of search blocks (isolated from official search types, without affecting each other.) 8 | * Support filtering out less frequently used notebooks 9 | * Support for mobile use (requires Dock bar to be enabled) 10 | 11 | 12 | ### Functions 13 | 14 | #### Click Result Positioning 15 | Click on a search result block to position within the document. Works well in long tables or code blocks. 16 | 17 | ![Image](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/click-result-positioning.gif?raw=true) 18 | 19 | 20 | #### Double-click Search Result to Open Document Tab 21 | Double-click a search result to open the document and jump to the specified location. If it does not open after double-clicking, you can adjust the "Double-click Time Threshold" in the "Other" settings. 22 | 23 | 24 | #### Single Document Search Result Sorting 25 | * Trigger method: 26 | * Right-click on document on desktop 27 | * Long press on document on mobile 28 | 29 | ![Image](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/sorting-menu-en.png?raw=true) 30 | 31 | 32 | #### Support for Arrow Key Selection 33 | When the cursor is in the input box, you can use the up and down arrow keys to select search results, and press Enter to open the result tab. 34 | 35 | 36 | #### Code Block, Database Highlighting 37 | After clicking to open a document in the tab search preview area or Dock bar, **code block, database** keyword highlighting and positioning are supported! (Official search currently does not support) 38 | If the highlighting for **code block, database** fails, you can adjust the "Preview Refresh Highlight Delay" in the "Other" settings. 39 | 40 | 41 | #### Dock Search Supports Searching in Current Document 42 | ![Alt text](https://github.com/Misuzu2027/syplugin-document-search/blob/main/src/assets/imgs/search-in-document.png?raw=true) 43 | 44 | Closing is for global search, opening is for searching in the current document; when opened, it forces the use of original order. If you want to switch sorting, you can right-click on the document to do so. 45 | 46 | In fact, it reads the document where the cursor is last focused. If the desired document is not located, you can switch tabs or reopen the document. 47 | 48 | 49 | ## Flat Document Tree 50 | ### Features 51 | * All operations on documents supported by the official document tree are also supported here. 52 | * Note: Operations such as renaming, deleting, and creating documents require manual refreshing to display the latest data. 53 | * Support for mobile use (requires Dock bar to be enabled) 54 | ### Functions 55 | * Supports sorting by modification time, creation time, and reference count. 56 | * Document name search supported in the search box. 57 | 58 | ## Hide Dock 59 | You can hide unused Docks in the plugin's Dock Settings. 60 | Note: The hiding will be synchronized on mobile. 61 | 62 | 63 | ## Query Syntax 64 | 65 | Use `@` followed by specific letters to apply advanced query filters. Prefixing `@` or a keyword with a minus sign `-` excludes blocks matching that condition. For best performance, it’s recommended to use exclusion filters at the end of the query; otherwise, the system first gathers all unmatched blocks, which can cause lag. 66 | 67 | ### Keyword Search 68 | 69 | #### Full-Text Search Enabled 70 | 71 | - `work test`: Finds documents that contain both `work` and `test`; then displays blocks that contain either `work` or `test`. 72 | 73 | - `-work test`: Finds documents that do **not** contain `work` but contain `test`; then displays blocks that contain `test`. 74 | 75 | 76 | #### Full-Text Search Disabled 77 | 78 | - `work test`: Finds blocks that contain both `work` and `test`. 79 | 80 | - `-work test`: Finds blocks that do **not** contain `work` but contain `test`. 81 | 82 | 83 | ### `@b` Notebook Filter 84 | 85 | - `@bLife`: Displays documents located in notebooks containing the keyword `Life`. 86 | 87 | - `-@bLife`: Displays documents located in notebooks **not** containing the keyword `Life`. 88 | 89 | - You can combine filters. For example, `@bLife @bWork` will display documents in notebooks that contain **both** `Life` and `Work`. 90 | 91 | 92 | ### `@p` Path Filter 93 | 94 | - `@pHobbies`: Displays documents whose path contains the keyword `Hobbies`. 95 | 96 | - `-@pHobbies`: Displays documents whose path does **not** contain the keyword `Hobbies`. 97 | 98 | - Combinable with other filters. 99 | 100 | 101 | To view all subdocuments under a document named `HobbiesAndInterests` but exclude the document itself, use `@pHobbies%/`. Adding `%/` at the end denotes subdocuments. 102 | 103 | ### `@c` Created Time / `@u` Updated Time 104 | 105 | Time or date values follow the `yyyyMMddhhmmss` format. For example, March 31, 2025 at 14:42:04 is written as `20250331144204`. 106 | 107 | - Notes created (or updated) in March 2025: `@c202503`, `@c=202503`, or `@ceq202503` 108 | 109 | - Notes created (or updated) after March 15, 2025: `@c>20250315` or `@cgt20250315` 110 | 111 | - Notes created before 2025: `@c<2025` or `@clt2025` 112 | 113 | - Notes created in November 2024: `@c>202411 @c<202412` 114 | 115 | 116 | ### `@t` Block Type Filter 117 | 118 | Block types correspond to `blocks.type` values in the database: 119 | 120 | `d` Document | `h` Heading | `l` List | `i` List Item | `c` Code | `m` Math | `t` Table | `b` Quote | `av` Property View (Database) | `s` Super Block | `p` Paragraph | `tb` Table Block | `html` HTML | `video` Video | `audio` Audio | `widget` Widget | `iframe` iFrame | `query_embed` Embedded Query 121 | 122 | - `@tav`: Finds all database blocks. 123 | 124 | - `@thViolet`: Finds heading blocks containing the keyword `Violet`. 125 | 126 | - `-@thViolet`: Finds all blocks except heading blocks that contain `Violet`. If used alone, this may return a large number of blocks, so it's best to combine it with inclusion keywords first and then filter out unwanted results. 127 | 128 | - `@th -@thViolet`: Finds all heading blocks, excluding those containing `Violet`. 129 | 130 | - `@th @tpViolet`: Finds all heading blocks and all paragraph blocks that contain `Violet`. 131 | 132 | 133 | **Note:** Results differ depending on whether full-text search is enabled. Full-text search filters at the document level, while disabled mode filters at the block level. Each block type is treated with an OR logic. 134 | 135 | Example: 136 | `@thViolet -@thGarden`: 137 | Finds heading blocks containing `Violet` but excludes those containing `Garden`. 138 | 139 | - With full-text search enabled: Any document with both `Violet` and `Garden` in heading blocks will be excluded entirely. 140 | 141 | - With full-text search disabled: Only the heading blocks with `Garden` are excluded; other heading blocks with `Violet` in the same document will still be shown. 142 | 143 | ## Default Configuration 144 | 145 | ### Document-based Search 146 | 147 | * Default block types for queries: 148 | * Document, Heading, Code Block, Math Block, Table Block, Paragraph Block, HTML Block, Database, Audio, Video 149 | * Default matching attributes for blocks: Name, Alias, Memo 150 | * Default sorting: 151 | * Document sorting: Descending by relevance 152 | * Content block sorting: Type 153 | * Documents per page: 10 154 | * Default maximum expansion count: 100 155 | 156 | 157 | ### Flat Document Tree 158 | 159 | * Default sorting: Descending by modification time 160 | * Display 30 items 161 | 162 | 163 | ### keyboard shortcuts 164 | 165 | * Currently supported keyboard shortcuts: Open document search tab, Document search Dock, Flat document tree Dock. 166 | * All can be found and modified in "Settings" -> "Keymap" -> "document dearch" in SiYuan. 167 | 168 | # Changelog 169 | > [Changelog(Chinese)](./CHANGELOG_zh_CN.md) -------------------------------------------------------------------------------- /src/services/setting-config.ts: -------------------------------------------------------------------------------- 1 | 2 | import Instance from '@/utils/Instance'; 3 | import { Plugin } from 'siyuan'; 4 | 5 | 6 | const SettingFile = 'search-setting.json'; 7 | 8 | export class SettingConfig { 9 | private defaultSettings = { 10 | defaultContentFields: ["content", "tag"], 11 | } 12 | private settings = { 13 | pageSize: 10 as number, // 每页的文档数 14 | includeTypes: ["d", "h", "c", "m", "t", "p", "html", "av", "video", "audio"] as BlockType[], // 查询的类型 15 | includeAttrFields: ["name", "alias", "memo"] as string[], // 查询的属性字段 16 | excludeNotebookIds: [] as string[], // 排除的笔记本ID 17 | maxExpandCount: 100 as number, // 最大展开数量,查询结果超过这个数量会自动折叠 18 | alwaysExpandSingleDoc: false as boolean, // 查询结果为单个文档时,始终展开所有结果。 19 | showChildDocument: true as boolean, // 是否在分组下面显示文档块,主要是方便复制文档块的id或引用块。 20 | swapDocItemClickLogic: false as boolean, 21 | documentSortMethod: "rankDesc" as DocumentSortMethod, // 文档排序方式,默认:相关度降序 22 | contentBlockSortMethod: "type" as ContentBlockSortMethod, // 内容块排序方式,默认:类型 23 | 24 | doubleClickTimeout: 190 as number, // 双击阈值 25 | refreshPreviewHighlightTimeout: 240 as number, // 刷新预览区高亮延迟,太短可能会高亮失败,不需要可以设置为0 26 | 27 | docSearchDockPoisition: "LeftTop" as DockPosition, 28 | flatDocTreeDockPoisition: "LeftTop" as DockPosition, 29 | // 扁平化文档列表设置 30 | flatDocFullTextSearch: false as boolean, // 全文搜索 31 | flatDocAllShowLimit: 30, // 显示全部文档时的大小限制 32 | }; 33 | 34 | 35 | 36 | public static get ins(): SettingConfig { 37 | return Instance.get(SettingConfig); 38 | } 39 | 40 | private plugin: Plugin; 41 | 42 | /** 43 | * 导入的时候,需要先加载设置;如果没有设置,则使用默认设置 44 | */ 45 | async load(plugin: Plugin) { 46 | if (plugin) { 47 | this.plugin = plugin; 48 | } 49 | if (!this.plugin) { 50 | console.info(`插件为空,无法加载数据。`) 51 | return; 52 | } 53 | let loaded = await this.plugin.loadData(SettingFile); 54 | if (loaded == null || loaded == undefined || loaded == '') { 55 | //如果没有配置文件,则使用默认配置,并保存 56 | console.info(`没有配置文件,使用默认配置`) 57 | this.save(); 58 | } else { 59 | //如果有配置文件,则使用配置文件 60 | console.info(`读入配置文件: ${SettingFile}`) 61 | //Docker 和 Windows 不知为何行为不一致, 一个读入字符串,一个读入对象 62 | //为了兼容,这里做一下判断 63 | if (typeof loaded === 'string') { 64 | loaded = JSON.parse(loaded); 65 | } 66 | try { 67 | for (let key in loaded) { 68 | this.set(key, loaded[key]); 69 | } 70 | } catch (error_msg) { 71 | console.log(`Setting load error: ${error_msg}`); 72 | } 73 | // this.save(); 74 | } 75 | } 76 | 77 | async save() { 78 | if (!this.plugin) { 79 | return; 80 | } 81 | let json = JSON.stringify(this.settings); 82 | console.log(`写入配置文件: ${json}`); 83 | this.plugin.saveData(SettingFile, json); 84 | } 85 | 86 | set(key: any, value: any) { 87 | if (!(key in this.settings)) { 88 | console.error(`"${key}" is not a setting`); 89 | return; 90 | } 91 | this.settings[key] = value; 92 | } 93 | 94 | get pageSize(): number { 95 | return this.settings.pageSize; 96 | } 97 | 98 | get includeTypes(): string[] { 99 | let includeTypes = [...this.settings.includeTypes]; 100 | return includeTypes; 101 | } 102 | 103 | get includeAttrFields(): string[] { 104 | return this.settings.includeAttrFields; 105 | } 106 | 107 | get includeQueryFields(): string[] { 108 | let queryFields = [...this.defaultSettings.defaultContentFields, ...this.settings.includeAttrFields]; 109 | return queryFields; 110 | } 111 | 112 | get excludeNotebookIds(): string[] { 113 | return this.settings.excludeNotebookIds; 114 | } 115 | 116 | get maxExpandCount(): number { 117 | return this.settings.maxExpandCount; 118 | } 119 | 120 | get alwaysExpandSingleDoc(): boolean { 121 | return this.settings.alwaysExpandSingleDoc; 122 | } 123 | 124 | 125 | get showChildDocument(): boolean { 126 | return this.settings.showChildDocument; 127 | } 128 | 129 | get documentSortMethod(): DocumentSortMethod { 130 | return this.settings.documentSortMethod; 131 | } 132 | 133 | get contentBlockSortMethod(): ContentBlockSortMethod { 134 | return this.settings.contentBlockSortMethod; 135 | } 136 | 137 | get swapDocItemClickLogic(): boolean { 138 | return this.settings.swapDocItemClickLogic; 139 | } 140 | 141 | get doubleClickTimeout(): number { 142 | return this.settings.doubleClickTimeout; 143 | } 144 | 145 | get refreshPreviewHighlightTimeout(): number { 146 | return this.settings.refreshPreviewHighlightTimeout; 147 | } 148 | 149 | get docSearchDockPoisition(): DockPosition { 150 | return this.settings.docSearchDockPoisition; 151 | } 152 | 153 | get flatDocTreeDockPoisition(): DockPosition { 154 | return this.settings.flatDocTreeDockPoisition; 155 | } 156 | 157 | 158 | get flatDocFullTextSearch(): boolean { 159 | return this.settings.flatDocFullTextSearch; 160 | } 161 | 162 | 163 | get flatDocAllShowLimit(): number { 164 | return this.settings.flatDocAllShowLimit; 165 | } 166 | 167 | 168 | updatePageSize(pageSize: number) { 169 | this.settings.pageSize = pageSize; 170 | this.save(); 171 | } 172 | 173 | updateIncludeTypes(types: BlockType[]) { 174 | this.settings.includeTypes = types; 175 | this.save(); 176 | } 177 | 178 | updateIncludeAttrFields(includeAttrFields: string[]) { 179 | this.settings.includeAttrFields = includeAttrFields; 180 | this.save(); 181 | } 182 | 183 | updateExcludeNotebookIds(notebookIds: string[]) { 184 | this.settings.excludeNotebookIds = notebookIds; 185 | this.save(); 186 | } 187 | 188 | updateMaxExpandCount(maxExpandCount: number) { 189 | this.settings.maxExpandCount = maxExpandCount; 190 | this.save(); 191 | } 192 | 193 | updateAlwaysExpandSingleDoc(alwaysExpandSingleDoc: boolean) { 194 | this.settings.alwaysExpandSingleDoc = alwaysExpandSingleDoc; 195 | this.save(); 196 | } 197 | 198 | updateShowChildDocument(showChildDocument: boolean) { 199 | this.settings.showChildDocument = showChildDocument; 200 | this.save(); 201 | } 202 | 203 | updateDocumentSortMethod(documentSortMethod: DocumentSortMethod) { 204 | this.settings.documentSortMethod = documentSortMethod; 205 | this.save(); 206 | } 207 | 208 | updateContentBlockSortMethod(contentBlockSortMethod: ContentBlockSortMethod) { 209 | this.settings.contentBlockSortMethod = contentBlockSortMethod; 210 | this.save(); 211 | } 212 | 213 | updateSwapDocItemClickLogic(swapDocItemClickLogic: boolean) { 214 | this.settings.swapDocItemClickLogic = swapDocItemClickLogic; 215 | this.save(); 216 | } 217 | 218 | updateDoubleClickTimeout(doubleClickTimeout: number) { 219 | this.settings.doubleClickTimeout = doubleClickTimeout; 220 | this.save(); 221 | } 222 | 223 | updateRefreshPreviewHighlightTimeout(refreshPreviewHighlightTimeout: number) { 224 | this.settings.refreshPreviewHighlightTimeout = refreshPreviewHighlightTimeout; 225 | this.save(); 226 | } 227 | 228 | 229 | updateDocSearchDockPoisition(docSearchDockPoisition: DockPosition) { 230 | this.settings.docSearchDockPoisition = docSearchDockPoisition; 231 | this.save(); 232 | } 233 | 234 | updateFlatDocTreeDockPoisition(flatDocTreeDockPoisition: DockPosition) { 235 | this.settings.flatDocTreeDockPoisition = flatDocTreeDockPoisition; 236 | this.save(); 237 | } 238 | 239 | updateflatDocFullTextSearch(flatDocFullTextSearch: boolean) { 240 | this.settings.flatDocFullTextSearch = flatDocFullTextSearch; 241 | this.save(); 242 | } 243 | 244 | updateflatDocAllShowLimit(flatDocAllShowLimit: number) { 245 | this.settings.flatDocAllShowLimit = flatDocAllShowLimit; 246 | this.save(); 247 | } 248 | } -------------------------------------------------------------------------------- /src/config/setting-constant.ts: -------------------------------------------------------------------------------- 1 | import { EnvConfig } from "@/config/env-config"; 2 | 3 | export function SETTING_BLOCK_TYPE_ELEMENT() { 4 | return [ 5 | { 6 | icon: "#iconMath", 7 | text: EnvConfig.ins.i18n.mathBlock, 8 | dataType: "mathBlock", 9 | value: "m", 10 | }, 11 | { 12 | icon: "#iconTable", 13 | text: EnvConfig.ins.i18n.table, 14 | dataType: "table", 15 | value: "t", 16 | }, 17 | { 18 | icon: "#iconParagraph", 19 | text: EnvConfig.ins.i18n.paragraph, 20 | dataType: "paragraph", 21 | value: "p", 22 | }, 23 | { 24 | icon: "#iconHeadings", 25 | text: EnvConfig.ins.i18n.headings, 26 | dataType: "heading", 27 | value: "h", 28 | }, 29 | { 30 | icon: "#iconCode", 31 | text: EnvConfig.ins.i18n.codeBlock, 32 | dataType: "codeBlock", 33 | value: "c", 34 | }, 35 | { 36 | icon: "#iconHTML5", 37 | text: EnvConfig.ins.i18n.htmlBlock, 38 | dataType: "htmlBlock", 39 | value: "html", 40 | }, 41 | { 42 | icon: "#iconDatabase", 43 | text: EnvConfig.ins.i18n.database, 44 | dataType: "databaseBlock", 45 | value: "av", 46 | }, 47 | { 48 | icon: "#iconSQL", 49 | text: EnvConfig.ins.i18n.embedBlock, 50 | dataType: "embedBlock", 51 | value: "query_embed", 52 | }, 53 | { 54 | icon: "#iconVideo", 55 | text: EnvConfig.ins.i18n.video, 56 | dataType: "videoBlock", 57 | value: "video", 58 | }, 59 | { 60 | icon: "#iconRecord", 61 | text: EnvConfig.ins.i18n.audio, 62 | dataType: "audioBlock", 63 | value: "audio", 64 | }, 65 | { 66 | icon: "#iconLanguage", 67 | text: EnvConfig.ins.i18n.IFrame, 68 | dataType: "iFrameBlock", 69 | value: "iframe", 70 | }, 71 | { 72 | icon: "#iconBoth", 73 | text: EnvConfig.ins.i18n.widget, 74 | dataType: "widgetBlock", 75 | value: "widget", 76 | }, 77 | { 78 | icon: "#iconFile", 79 | text: EnvConfig.ins.i18n.doc, 80 | dataType: "document", 81 | value: "d", 82 | containerBlockTip1: true, 83 | containerBlockTip2: true, 84 | }, 85 | { 86 | icon: "#iconQuote", 87 | text: EnvConfig.ins.i18n.quoteBlock, 88 | dataType: "quoteBlock", 89 | value: "b", 90 | containerBlockTip1: true, 91 | }, 92 | { 93 | icon: "#iconSuper", 94 | text: EnvConfig.ins.i18n.superBlock, 95 | dataType: "superBlock", 96 | value: "s", 97 | containerBlockTip1: true, 98 | }, 99 | { 100 | icon: "#iconList", 101 | text: EnvConfig.ins.i18n.list, 102 | dataType: "checkbox", 103 | value: "l", 104 | containerBlockTip1: true, 105 | }, 106 | { 107 | icon: "#iconListItem", 108 | text: EnvConfig.ins.i18n.listItem, 109 | dataType: "listItem", 110 | value: "i", 111 | containerBlockTip1: true, 112 | }, 113 | ]; 114 | } 115 | 116 | export function SETTING_BLOCK_ATTR_ELEMENT() { 117 | return [ 118 | { 119 | icon: "#iconN", 120 | text: EnvConfig.ins.i18n.name, 121 | value: "name", 122 | }, 123 | { 124 | icon: "#iconA", 125 | text: EnvConfig.ins.i18n.alias, 126 | value: "alias", 127 | }, 128 | { 129 | icon: "#iconM", 130 | text: EnvConfig.ins.i18n.memo, 131 | value: "memo", 132 | }, 133 | { 134 | icon: "#iconAttr", 135 | text: EnvConfig.ins.i18n.allAttrs, 136 | value: "ial", 137 | }, 138 | ]; 139 | } 140 | 141 | 142 | export function SETTING_DOCUMENT_SORT_METHOD_ELEMENT() { 143 | return [ 144 | { 145 | text: EnvConfig.ins.i18n.sortByRankASC, 146 | value: "rankAsc", 147 | }, 148 | { 149 | text: EnvConfig.ins.i18n.sortByRankDESC, 150 | value: "rankDesc", 151 | }, 152 | { 153 | text: EnvConfig.ins.i18n.modifiedASC, 154 | value: "modifiedAsc", 155 | }, 156 | { 157 | text: EnvConfig.ins.i18n.modifiedDESC, 158 | value: "modifiedDesc", 159 | }, 160 | { 161 | text: EnvConfig.ins.i18n.createdASC, 162 | value: "createdAsc", 163 | }, 164 | { 165 | text: EnvConfig.ins.i18n.createdDESC, 166 | value: "createdDesc", 167 | }, 168 | { 169 | text: EnvConfig.ins.i18n.fileNameASC, 170 | value: "alphabeticAsc", 171 | }, 172 | { 173 | text: EnvConfig.ins.i18n.fileNameDESC, 174 | value: "alphabeticDesc", 175 | }, 176 | ]; 177 | } 178 | 179 | export function SETTING_CONTENT_BLOCK_SORT_METHOD_ELEMENT() { 180 | return [ 181 | { 182 | text: EnvConfig.ins.i18n.type, 183 | value: "type", 184 | }, 185 | { 186 | text: EnvConfig.ins.i18n.sortByContent, 187 | value: "content", 188 | }, 189 | ...SETTING_DOCUMENT_SORT_METHOD_ELEMENT(), 190 | { 191 | text: EnvConfig.ins.i18n.sortByTypeAndContent, 192 | value: "typeAndContent", 193 | }, 194 | ]; 195 | } 196 | 197 | // "LeftTop" | "LeftBottom" | "RightTop" | "RightBottom" | "BottomLeft" | "BottomRight" 198 | export function SETTING_DOC_POISITION_ELEMENT() { 199 | return [ 200 | { 201 | text: EnvConfig.ins.i18n.show, 202 | value: "LeftTop", 203 | }, 204 | { 205 | text: EnvConfig.ins.i18n.hide, 206 | value: "Hidden", 207 | }, 208 | // { 209 | // text: "左侧上方", 210 | // value: "LeftTop", 211 | // }, 212 | // { 213 | // text: "左侧下方", 214 | // value: "LeftBottom", 215 | // }, 216 | // { 217 | // text: "右侧上方", 218 | // value: "RightTop", 219 | // }, 220 | // { 221 | // text: "右侧下方", 222 | // value: "RightBottom", 223 | // }, { 224 | // text: "下侧左方", 225 | // value: "BottomLeft", 226 | // }, 227 | // { 228 | // text: "下侧右方", 229 | // value: "BottomRight", 230 | // }, 231 | ]; 232 | } 233 | 234 | 235 | export function SETTING_FLAT_DOCUMENT_TREE_SORT_METHOD_ELEMENT() { 236 | return [ 237 | { 238 | text: EnvConfig.ins.i18n.modifiedASC, 239 | value: "modifiedAsc", 240 | }, 241 | { 242 | text: EnvConfig.ins.i18n.modifiedDESC, 243 | value: "modifiedDesc", 244 | }, 245 | { 246 | text: EnvConfig.ins.i18n.createdASC, 247 | value: "createdAsc", 248 | }, 249 | { 250 | text: EnvConfig.ins.i18n.createdDESC, 251 | value: "createdDesc", 252 | }, 253 | { 254 | text: EnvConfig.ins.i18n.refCountASC, 255 | value: "refCountAsc", 256 | }, 257 | { 258 | text: EnvConfig.ins.i18n.refCountDESC, 259 | value: "refCountDesc", 260 | }, 261 | { 262 | text: EnvConfig.ins.i18n.fileNameASC, 263 | value: "alphabeticAsc", 264 | }, 265 | { 266 | text: EnvConfig.ins.i18n.fileNameDESC, 267 | value: "alphabeticDesc", 268 | }, 269 | ]; 270 | } 271 | 272 | 273 | export function SEARCH_RESULT_EXPAND_COLLAPSE_MODE() { 274 | return [ 275 | { 276 | text: EnvConfig.ins.i18n.click, 277 | value: "click", 278 | }, 279 | { 280 | text: EnvConfig.ins.i18n.doubleClick, 281 | value: "doubleClick", 282 | }, 283 | ]; 284 | } -------------------------------------------------------------------------------- /src/components/setting/setting-other.svelte: -------------------------------------------------------------------------------- 1 | 92 | 93 |
94 | 117 | 118 | 141 | 142 | 159 | 180 | 181 | 198 | 214 | 233 | 250 | 251 | 271 |
272 | -------------------------------------------------------------------------------- /src/config/icon-constant.ts: -------------------------------------------------------------------------------- 1 | 2 | export const CUSTOM_ICON_MAP = 3 | { 4 | iconDocumentSearch: { 5 | id: "iconDocumentSearch", 6 | source: ` 7 | 8 | 9 | ` 10 | }, 11 | iconFlatDocTree: { 12 | id: "iconFlatDocTree", 13 | source: ` 14 | 15 | ` 16 | }, 17 | iconSearchSettingExcludeNotebook: { 18 | id: "iconSearchSettingExcludeNotebook", 19 | source: ` 20 | 23 | ` 24 | }, 25 | iconSearchSettingOther: { 26 | id: "iconSearchSettingOther", 27 | source: ` 28 | 30 | 31 | 33 | 34 | 36 | 37 | 39 | 40 | ` 41 | }, 42 | iconContentSort: { 43 | id: "iconContentSort", 44 | source: ` 45 | 47 | 48 | ` 49 | }, 50 | iconFullTextSearch: { 51 | id: "iconFullTextSearch", 52 | source: ` 53 | 54 | 55 | ` 56 | }, 57 | iconCurDocSearch: { 58 | id: "iconCurDocSearch", 59 | source: ` 60 | 61 | ` 62 | }, 63 | }; -------------------------------------------------------------------------------- /src/components/search/search-result-item.svelte: -------------------------------------------------------------------------------- 1 | 170 | 171 | 172 | 173 |
177 | {#each documentItemSearchResult as item} 178 |
clickDocItem(event, item)} 183 | on:mousedown={(event) => mousedownDocItem(event, item)} 184 | on:contextmenu|stopPropagation|preventDefault={(event) => 185 | documentItemContextmenuEvent(event, item)} 186 | on:keydown={handleKeyDownDefault} 187 | data-node-id={item.block.id} 188 | data-root-id={item.block.root_id} 189 | > 190 | 191 | 197 | toggleItemVisibility(item.block)} 198 | on:keydown={handleKeyDownDefault} 199 | > 200 | 207 | 208 | 209 | 210 | 211 | {@html item.icon} 212 | 213 | 219 | {@html item.block.content} 220 | 221 |
222 | {item.subItems.length} 223 |
224 |
225 |
226 | {#each item.subItems as subItem (subItem.block.id)} 227 |
itemClick(event, subItem)} 235 | on:keydown={handleKeyDownDefault} 236 | > 237 | 238 | 244 | 245 | {@html subItem.block.content} 247 | 248 | 249 | {#if subItem.block.name} 250 | 254 | 255 | 256 | 257 | {@html subItem.block.name} 258 | 259 | 260 | {/if} 261 | {#if subItem.block.alias} 262 | 266 | 267 | 268 | 269 | {@html subItem.block.alias} 270 | 271 | 272 | {/if} 273 | {#if subItem.block.memo} 274 | 278 | 281 | {@html subItem.block.memo} 282 | 283 | 284 | {/if} 285 |
286 | {/each} 287 |
288 | {/each} 289 | 290 |
293 | 294 | 295 |
296 |
297 | 298 | 305 | -------------------------------------------------------------------------------- /src/utils/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2023 frostime. All rights reserved. 3 | * https://github.com/frostime/sy-plugin-template-vite 4 | * 5 | * See API Document in [API.md](https://github.com/siyuan-note/siyuan/blob/master/API.md) 6 | * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md) 7 | */ 8 | 9 | import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan"; 10 | import { isBoolean } from "@/utils/object-util"; 11 | import { getNotebookIcon } from "./icon-util"; 12 | 13 | 14 | 15 | async function request(url: string, data: any) { 16 | let response: IWebSocketData = await fetchSyncPost(url, data); 17 | let res = response.code === 0 ? response.data : null; 18 | if (response.code != 0) { 19 | console.log(`基于文档搜索插件接口异常 url : ${url} , msg : ${response.msg}`) 20 | } 21 | return res; 22 | } 23 | 24 | 25 | // **************************************** Notebook **************************************** 26 | 27 | 28 | export async function lsNotebooks(): Promise { 29 | let url = '/api/notebook/lsNotebooks'; 30 | return request(url, ''); 31 | } 32 | 33 | export async function getNotebookMapByApi(showClosed: boolean): Promise> { 34 | let notebookMap: Map = new Map(); 35 | let notebooks: Notebook[] = (await lsNotebooks()).notebooks; 36 | for (const notebook of notebooks) { 37 | if (!showClosed && notebook.closed) { 38 | continue; 39 | } 40 | notebook.icon = getNotebookIcon(notebook.icon); 41 | notebookMap.set(notebook.id, notebook); 42 | } 43 | return notebookMap; 44 | } 45 | 46 | export function getNotebookMap(notebooks: Notebook[]): Map { 47 | let notebookMap: Map = new Map(); 48 | if (!notebooks) { 49 | return notebookMap; 50 | } 51 | for (const notebook of notebooks) { 52 | notebookMap.set(notebook.id, notebook); 53 | } 54 | return notebookMap; 55 | } 56 | 57 | 58 | 59 | export async function openNotebook(notebook: NotebookId) { 60 | let url = '/api/notebook/openNotebook'; 61 | return request(url, { notebook: notebook }); 62 | } 63 | 64 | 65 | export async function closeNotebook(notebook: NotebookId) { 66 | let url = '/api/notebook/closeNotebook'; 67 | return request(url, { notebook: notebook }); 68 | } 69 | 70 | 71 | export async function renameNotebook(notebook: NotebookId, name: string) { 72 | let url = '/api/notebook/renameNotebook'; 73 | return request(url, { notebook: notebook, name: name }); 74 | } 75 | 76 | 77 | export async function createNotebook(name: string): Promise { 78 | let url = '/api/notebook/createNotebook'; 79 | return request(url, { name: name }); 80 | } 81 | 82 | 83 | export async function removeNotebook(notebook: NotebookId) { 84 | let url = '/api/notebook/removeNotebook'; 85 | return request(url, { notebook: notebook }); 86 | } 87 | 88 | 89 | export async function getNotebookConf(notebook: NotebookId): Promise { 90 | let data = { notebook: notebook }; 91 | let url = '/api/notebook/getNotebookConf'; 92 | return request(url, data); 93 | } 94 | 95 | 96 | export async function setNotebookConf(notebook: NotebookId, conf: NotebookConf): Promise { 97 | let data = { notebook: notebook, conf: conf }; 98 | let url = '/api/notebook/setNotebookConf'; 99 | return request(url, data); 100 | } 101 | 102 | 103 | // **************************************** File Tree **************************************** 104 | export async function createDocWithMd(notebook: NotebookId, path: string, markdown: string): Promise { 105 | let data = { 106 | notebook: notebook, 107 | path: path, 108 | markdown: markdown, 109 | }; 110 | let url = '/api/filetree/createDocWithMd'; 111 | return request(url, data); 112 | } 113 | 114 | 115 | export async function renameDoc(notebook: NotebookId, path: string, title: string): Promise { 116 | let data = { 117 | doc: notebook, 118 | path: path, 119 | title: title 120 | }; 121 | let url = '/api/filetree/renameDoc'; 122 | return request(url, data); 123 | } 124 | 125 | 126 | export async function removeDoc(notebook: NotebookId, path: string) { 127 | let data = { 128 | notebook: notebook, 129 | path: path, 130 | }; 131 | let url = '/api/filetree/removeDoc'; 132 | return request(url, data); 133 | } 134 | 135 | 136 | export async function moveDocs(fromPaths: string[], toNotebook: NotebookId, toPath: string) { 137 | let data = { 138 | fromPaths: fromPaths, 139 | toNotebook: toNotebook, 140 | toPath: toPath 141 | }; 142 | let url = '/api/filetree/moveDocs'; 143 | return request(url, data); 144 | } 145 | 146 | 147 | export async function getHPathByPath(notebook: NotebookId, path: string): Promise { 148 | let data = { 149 | notebook: notebook, 150 | path: path 151 | }; 152 | let url = '/api/filetree/getHPathByPath'; 153 | return request(url, data); 154 | } 155 | 156 | 157 | export async function getHPathByID(id: BlockId): Promise { 158 | let data = { 159 | id: id 160 | }; 161 | let url = '/api/filetree/getHPathByID'; 162 | return request(url, data); 163 | } 164 | 165 | 166 | export async function getIDsByHPath(notebook: NotebookId, path: string): Promise { 167 | let data = { 168 | notebook: notebook, 169 | path: path 170 | }; 171 | let url = '/api/filetree/getIDsByHPath'; 172 | return request(url, data); 173 | } 174 | 175 | // **************************************** Asset Files **************************************** 176 | 177 | export async function upload(assetsDirPath: string, files: any[]): Promise { 178 | let form = new FormData(); 179 | form.append('assetsDirPath', assetsDirPath); 180 | for (let file of files) { 181 | form.append('file[]', file); 182 | } 183 | let url = '/api/asset/upload'; 184 | return request(url, form); 185 | } 186 | 187 | // **************************************** Block **************************************** 188 | type DataType = "markdown" | "dom"; 189 | export async function insertBlock( 190 | dataType: DataType, data: string, 191 | nextID?: BlockId, previousID?: BlockId, parentID?: BlockId 192 | ): Promise { 193 | let payload = { 194 | dataType: dataType, 195 | data: data, 196 | nextID: nextID, 197 | previousID: previousID, 198 | parentID: parentID 199 | } 200 | let url = '/api/block/insertBlock'; 201 | return request(url, payload); 202 | } 203 | 204 | 205 | export async function prependBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise { 206 | let payload = { 207 | dataType: dataType, 208 | data: data, 209 | parentID: parentID 210 | } 211 | let url = '/api/block/prependBlock'; 212 | return request(url, payload); 213 | } 214 | 215 | 216 | export async function appendBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise { 217 | let payload = { 218 | dataType: dataType, 219 | data: data, 220 | parentID: parentID 221 | } 222 | let url = '/api/block/appendBlock'; 223 | return request(url, payload); 224 | } 225 | 226 | 227 | export async function updateBlock(dataType: DataType, data: string, id: BlockId): Promise { 228 | let payload = { 229 | dataType: dataType, 230 | data: data, 231 | id: id 232 | } 233 | let url = '/api/block/updateBlock'; 234 | return request(url, payload); 235 | } 236 | 237 | 238 | export async function deleteBlock(id: BlockId): Promise { 239 | let data = { 240 | id: id 241 | } 242 | let url = '/api/block/deleteBlock'; 243 | return request(url, data); 244 | } 245 | 246 | 247 | export async function moveBlock(id: BlockId, previousID?: PreviousID, parentID?: ParentID): Promise { 248 | let data = { 249 | id: id, 250 | previousID: previousID, 251 | parentID: parentID 252 | } 253 | let url = '/api/block/moveBlock'; 254 | return request(url, data); 255 | } 256 | 257 | 258 | export async function getBlockKramdown(id: BlockId): Promise { 259 | let data = { 260 | id: id 261 | } 262 | let url = '/api/block/getBlockKramdown'; 263 | return request(url, data); 264 | } 265 | 266 | 267 | export async function getChildBlocks(id: BlockId): Promise { 268 | let data = { 269 | id: id 270 | } 271 | let url = '/api/block/getChildBlocks'; 272 | return request(url, data); 273 | } 274 | 275 | export async function transferBlockRef(fromID: BlockId, toID: BlockId, refIDs: BlockId[]) { 276 | let data = { 277 | fromID: fromID, 278 | toID: toID, 279 | refIDs: refIDs 280 | } 281 | let url = '/api/block/transferBlockRef'; 282 | return request(url, data); 283 | } 284 | 285 | export async function getBlockIndex(id: BlockId): Promise { 286 | let data = { 287 | id: id 288 | } 289 | let url = '/api/block/getBlockIndex'; 290 | 291 | return request(url, data); 292 | } 293 | 294 | export async function getBlocksIndexes(ids: BlockId[]): Promise { 295 | let data = { 296 | ids: ids 297 | } 298 | let url = '/api/block/getBlocksIndexes'; 299 | 300 | return request(url, data); 301 | } 302 | 303 | export async function getBlockIsFolded(id: string): Promise { 304 | 305 | let response = await checkBlockFold(id); 306 | let result: boolean; 307 | if (isBoolean(response)) { 308 | result = response as boolean; 309 | } else { 310 | result = response.isFolded; 311 | } 312 | // console.log(`getBlockIsFolded response : ${JSON.stringify(response)}, result : ${result} `) 313 | return result; 314 | }; 315 | 316 | export async function checkBlockFold(id: string): Promise { 317 | if (!id) { 318 | // 参数校验失败,返回拒绝 319 | return Promise.reject(new Error('参数错误')); 320 | } 321 | let data = { 322 | id: id 323 | } 324 | let url = '/api/block/checkBlockFold'; 325 | 326 | return request(url, data); 327 | }; 328 | 329 | // **************************************** Attributes **************************************** 330 | export async function setBlockAttrs(id: BlockId, attrs: { [key: string]: string }) { 331 | let data = { 332 | id: id, 333 | attrs: attrs 334 | } 335 | let url = '/api/attr/setBlockAttrs'; 336 | return request(url, data); 337 | } 338 | 339 | 340 | export async function getBlockAttrs(id: BlockId): Promise<{ [key: string]: string }> { 341 | let data = { 342 | id: id 343 | } 344 | let url = '/api/attr/getBlockAttrs'; 345 | return request(url, data); 346 | } 347 | 348 | // **************************************** SQL **************************************** 349 | 350 | export async function sql(sql: string): Promise { 351 | let sqldata = { 352 | stmt: sql, 353 | }; 354 | let url = '/api/query/sql'; 355 | return request(url, sqldata); 356 | } 357 | 358 | export async function getBlockByID(blockId: string): Promise { 359 | let sqlScript = `select * from blocks where id ='${blockId}'`; 360 | let data = await sql(sqlScript); 361 | return data[0]; 362 | } 363 | 364 | // **************************************** Template **************************************** 365 | 366 | export async function render(id: DocumentId, path: string): Promise { 367 | let data = { 368 | id: id, 369 | path: path 370 | } 371 | let url = '/api/template/render'; 372 | return request(url, data); 373 | } 374 | 375 | 376 | export async function renderSprig(template: string): Promise { 377 | let url = '/api/template/renderSprig'; 378 | return request(url, { template: template }); 379 | } 380 | 381 | // **************************************** File **************************************** 382 | 383 | export async function getFile(path: string): Promise { 384 | let data = { 385 | path: path 386 | } 387 | let url = '/api/file/getFile'; 388 | try { 389 | let file = await fetchSyncPost(url, data); 390 | return file; 391 | } catch (error_msg) { 392 | return null; 393 | } 394 | } 395 | 396 | export async function putFile(path: string, isDir: boolean, file: any) { 397 | let form = new FormData(); 398 | form.append('path', path); 399 | form.append('isDir', isDir.toString()); 400 | // Copyright (c) 2023, terwer. 401 | // https://github.com/terwer/siyuan-plugin-importer/blob/v1.4.1/src/api/kernel-api.ts 402 | form.append('modTime', Math.floor(Date.now() / 1000).toString()); 403 | form.append('file', file); 404 | let url = '/api/file/putFile'; 405 | return request(url, form); 406 | } 407 | 408 | export async function removeFile(path: string) { 409 | let data = { 410 | path: path 411 | } 412 | let url = '/api/file/removeFile'; 413 | return request(url, data); 414 | } 415 | 416 | 417 | 418 | export async function readDir(path: string): Promise { 419 | let data = { 420 | path: path 421 | } 422 | let url = '/api/file/readDir'; 423 | return request(url, data); 424 | } 425 | 426 | 427 | // **************************************** Export **************************************** 428 | 429 | export async function exportMdContent(id: DocumentId): Promise { 430 | let data = { 431 | id: id 432 | } 433 | let url = '/api/export/exportMdContent'; 434 | return request(url, data); 435 | } 436 | 437 | export async function exportResources(paths: string[], name: string): Promise { 438 | let data = { 439 | paths: paths, 440 | name: name 441 | } 442 | let url = '/api/export/exportResources'; 443 | return request(url, data); 444 | } 445 | 446 | // **************************************** Convert **************************************** 447 | 448 | export type PandocArgs = string; 449 | export async function pandoc(args: PandocArgs[]) { 450 | let data = { 451 | args: args 452 | } 453 | let url = '/api/convert/pandoc'; 454 | return request(url, data); 455 | } 456 | 457 | // **************************************** Notification **************************************** 458 | 459 | // /api/notification/pushMsg 460 | // { 461 | // "msg": "test", 462 | // "timeout": 7000 463 | // } 464 | export async function pushMsg(msg: string, timeout: number = 7000) { 465 | let payload = { 466 | msg: msg, 467 | timeout: timeout 468 | }; 469 | let url = "/api/notification/pushMsg"; 470 | return request(url, payload); 471 | } 472 | 473 | export async function pushErrMsg(msg: string, timeout: number = 7000) { 474 | let payload = { 475 | msg: msg, 476 | timeout: timeout 477 | }; 478 | let url = "/api/notification/pushErrMsg"; 479 | return request(url, payload); 480 | } 481 | 482 | // **************************************** Network **************************************** 483 | export async function forwardProxy( 484 | url: string, method: string = 'GET', payload: any = {}, 485 | headers: any[] = [], timeout: number = 7000, contentType: string = "text/html" 486 | ): Promise { 487 | let data = { 488 | url: url, 489 | method: method, 490 | timeout: timeout, 491 | contentType: contentType, 492 | headers: headers, 493 | payload: payload 494 | } 495 | let url1 = '/api/network/forwardProxy'; 496 | return request(url1, data); 497 | } 498 | 499 | 500 | // **************************************** System **************************************** 501 | 502 | export async function bootProgress(): Promise { 503 | return request('/api/system/bootProgress', {}); 504 | } 505 | 506 | 507 | export async function version(): Promise { 508 | return request('/api/system/version', {}); 509 | } 510 | 511 | 512 | export async function currentTime(): Promise { 513 | return request('/api/system/currentTime', {}); 514 | } 515 | --------------------------------------------------------------------------------