diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8eb5daf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Create Release on Tag Push + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + # Checkout + - name: Checkout + uses: actions/checkout@v3 + + # Install Node.js + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org" + + # Install pnpm + - name: Install pnpm + uses: pnpm/action-setup@v2 + id: pnpm-install + with: + version: 8 + run_install: false + + # Get pnpm store directory + - name: Get pnpm store directory + id: pnpm-cache + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT + + # Setup pnpm cache + - name: Setup pnpm cache + uses: actions/cache@v3 + with: + path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store- + + # Install dependencies + - name: Install dependencies + run: pnpm install + + # Build for production, 这一步会生成一个 package.zip + - name: Build for production + run: pnpm build + + - name: Release + uses: ncipollo/release-action@v1 + with: + allowUpdates: true + artifactErrorsFailBuild: true + artifacts: "package.zip" + token: ${{ secrets.GITHUB_TOKEN }} + prerelease: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2a2064 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea +.vscode +.DS_Store +pnpm-lock.yaml +package-lock.json +package.zip +node_modules +dev +dist +build +tmp diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3cf562d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 SiYuan 思源笔记 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d61193a --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +个人使用,部分功能可能有数据或性能问题,支持反馈bug。 +有喜欢的功能,可以自行抽离该功能进行发布插件。 + +## 支持的功能 + +* 引用块样式 +* 代码块增强 + * 代码块快捷命名 + * 限制代码块高度 + * 调整代码块高度 + * 代码块常用语言 +* 文档树 + * 通过关键字过滤笔记本 + + \ No newline at end of file diff --git a/README_zh_CN.md b/README_zh_CN.md new file mode 100644 index 0000000..d61193a --- /dev/null +++ b/README_zh_CN.md @@ -0,0 +1,15 @@ +个人使用,部分功能可能有数据或性能问题,支持反馈bug。 +有喜欢的功能,可以自行抽离该功能进行发布插件。 + +## 支持的功能 + +* 引用块样式 +* 代码块增强 + * 代码块快捷命名 + * 限制代码块高度 + * 调整代码块高度 + * 代码块常用语言 +* 文档树 + * 通过关键字过滤笔记本 + + \ No newline at end of file diff --git a/asset/action.png b/asset/action.png new file mode 100644 index 0000000..a884045 Binary files /dev/null and b/asset/action.png differ diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..4cb743d Binary files /dev/null and b/icon.png differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f9b784 --- /dev/null +++ b/package.json @@ -0,0 +1,34 @@ +{ + "name": "syplugin-misuzu-custom", + "version": "0.0.1", + "type": "module", + "description": "SiYuan Note Plugin: Misuzu2027 Personal Use", + "repository": "https://github.com/Misuzu2027/syplugin-misuzu-custom", + "homepage": "https://github.com/Misuzu2027/syplugin-misuzu-custom", + "author": "Misuzu2027", + "license": "MIT", + "scripts": { + "make-link": "node --no-warnings ./scripts/make_dev_link.js", + "dev": "vite build --watch", + "build": "vite build", + "make-install": "vite build && node --no-warnings ./scripts/make_install.js" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@tsconfig/svelte": "^4.0.1", + "@types/node": "^20.3.0", + "fast-glob": "^3.2.12", + "glob": "^7.2.3", + "js-yaml": "^4.1.0", + "minimist": "^1.2.8", + "rollup-plugin-livereload": "^2.0.5", + "sass": "^1.63.3", + "siyuan": "0.9.9", + "svelte": "^4.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.1.3", + "vite": "^5.0.0", + "vite-plugin-static-copy": "^1.0.2", + "vite-plugin-zip-pack": "^1.0.5" + } +} \ No newline at end of file diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..b2e8c08 --- /dev/null +++ b/plugin.json @@ -0,0 +1,28 @@ +{ + "name": "syplugin-misuzu-custom", + "author": "Misuzu2027", + "url": "https://github.com/Misuzu2027/syplugin-misuzu-custom", + "version": "0.0.1", + "minAppVersion": "3.0.14", + "backends": [ + "all" + ], + "frontends": [ + "all" + ], + "displayName": { + "en_US": "Misuzu", + "zh_CN": "Misuzu" + }, + "description": { + "en_US": "个人使用插件", + "zh_CN": "个人使用插件" + }, + "readme": { + "en_US": "README.md", + "zh_CN": "README_zh_CN.md" + }, + "keywords": [ + + ] +} diff --git a/preview.png b/preview.png new file mode 100644 index 0000000..95b93c7 Binary files /dev/null and b/preview.png differ diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json new file mode 100644 index 0000000..8721caa --- /dev/null +++ b/public/i18n/en_US.json @@ -0,0 +1,83 @@ +{ + "openDocumentSearchTab": "Open Document-based Search Tab", + "table": "Table", + "mathBlock": "Formula block", + "quoteBlock": "Blockquote", + "superBlock": "Super block", + "paragraph": "Paragraph", + "doc": "Doc", + "headings": "Headings", + "list": "List", + "listItem": "List item", + "codeBlock": "Code block", + "htmlBlock": "HTML block", + "embedBlock": "Embed block", + "database": "Database", + "video": "Video", + "audio": "Audio", + "IFrame": "IFrame", + "widget": "Widget", + "name": "Name", + "alias": "Alias", + "memo": "Memo", + "allAttrs": "All attribute names and attribute values", + "sortByRankASC": "Relevance ASC", + "sortByRankDESC": "Relevance DESC", + "modifiedASC": "Modified Time ASC", + "modifiedDESC": "Modified Time DESC", + "createdASC": "Created Time ASC", + "createdDESC": "Created Time DESC", + "type": "Type", + "sortByContent": "Original content order", + "sortByTypeAndContent": "Type And Original content order", + "show": "Show", + "hide": "Hide", + "refCountASC": "Ref Count ASC", + "refCountDESC": "Ref Count DESC", + "fileNameASC": "Name Alphabet ASC", + "fileNameDESC": "Name Alphabet DESC", + "fileNameNatASC": "Name Natural ASC", + "fileNameNatDESC": "Name Natural DESC", + "notebook": "Notebook", + "path": "Path", + "sort": "Sort", + "clear": "Clear", + "refresh": "Refresh", + "reference": "Ref", + "documentBasedSearch": "Document-based Search", + "flatDocumentTree": "Flat Document Tree", + "documentBasedSearchDock": "Document-based Search Dock", + "flatDocumentTreeDock": "Flat Document Tree Dock", + "previousLabel": "Previous", + "nextLabel": "Next", + "findInBacklink": "Found ${x} backlink blocks", + "notebookFilter": "Notebook Filter", + "attr": "Attribute", + "other": "Other", + "expand": "Expand", + "collapse": "Collapse", + "noContentBelow": "No content matching the criteria exists below", + "dockModifyTips": "Note: Modifying Dock will refresh the interface.", + "settingDock": "🌈 Dock Settings", + "settingNotebookFilter": "🌈 Notebook Filter", + "settingType": "🌈 Type", + "settingAttr": "🌈 Attribute", + "settingOther": "🌈 Other", + "docSortMethod": "Document Sorting Method", + "contentBlockSortMethod": "Content Block Sorting Method", + "documentsPerPage": "Documents Per Page", + "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.", + "defaultExpansionCount": "Default Expansion Count", + "alwaysExpandSingleDoc": "Always Expand Single Document", + "displayDocBlock": "Display Document Blocks", + "doubleClickTimeThreshold": "Double Click Time Threshold (Milliseconds)", + "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.", + "previewRefreshHighlightDelay": "Preview Refresh Highlight Delay (Milliseconds)", + "settingHub": "Settings Hub", + "switchCurrentDocumentSearchFailureMessage": "Document Search Plugin: Failed to retrieve the opened document. You can switch tabs or reopen the document.", + "swapDocumentItemClickLogic": "Swap Document Item Click Logic", + "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.", + "allNotebooks": "All Notebooks", + "specifyNotebook": "Specify Notebook", + "searchInTheCurrentDocument": "Search in the current document" +} \ No newline at end of file diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json new file mode 100644 index 0000000..5d3d910 --- /dev/null +++ b/public/i18n/zh_CN.json @@ -0,0 +1,84 @@ +{ + "openDocumentSearchTab": "打开搜索页签", + "table": "表格", + "mathBlock": "公式块", + "quoteBlock": "引述块", + "superBlock": "超级块", + "paragraph": "段落", + "doc": "文档", + "headings": "标题", + "list": "列表", + "listItem": "列表项", + "codeBlock": "代码块", + "htmlBlock": "HTML 块", + "embedBlock": "嵌入块", + "database": "数据库", + "video": "视频", + "audio": "音频", + "IFrame": "IFrame", + "widget": "挂件", + "name": "name", + "alias": "别名", + "memo": "备注", + "allAttrs": "所有属性名和属性值", + "sortByRankASC": "按相关度升序", + "sortByRankDESC": "按相关度降序", + "modifiedASC": "修改时间升序", + "modifiedDESC": "修改时间降序", + "createdASC": "创建时间升序", + "createdDESC": "创建时间降序", + "type": "类型", + "sortByContent": "按原文内容顺序", + "sortByTypeAndContent": "类型和原文内容顺序", + "show": "显示", + "hide": "隐藏", + "refCountASC": "引用数升序", + "refCountDESC": "引用数降序", + "fileNameASC": "名称字母升序", + "fileNameDESC": "名称字母降序", + "fileNameNatASC": "名称自然升序", + "fileNameNatDESC": "名称自然降序", + "notebook": "笔记本", + "path": "路径", + "sort": "排序", + "clear": "清空", + "refresh": "刷新", + "reference": "引用", + "documentBasedSearch": "基于文档搜索", + "flatDocumentTree": "扁平化文档树", + "documentBasedSearchDock": "基于文档搜索 Dock", + "flatDocumentTreeDock": "扁平化文档树 Dock", + "previousLabel": "上一页", + "nextLabel": "下一页", + "findInBacklink": "共 ${x} 个反链块", + "notebookFilter": "笔记本过滤", + "attr": "属性", + "other": "其他", + "expand": "展开", + "collapse": "折叠", + "noContentBelow": "下不存在符合条件的内容", + "dockModifyTips": "注:修改 Dock 会刷新界面。", + "settingDock": "🌈 Dock 设置", + "settingNotebookFilter": "🌈 笔记本过滤", + "settingType": "🌈 类型", + "settingAttr": "🌈 属性", + "settingOther": "🌈 其他", + "docSortMethod": "文档排序方式", + "contentBlockSortMethod": "内容块排序方式", + "documentsPerPage": "每页文档数量", + "blockCountBehaviorTips": "如果查询结果的块数量小于当前值,默认展开全部文档;反之会默认折叠全部文档。", + "defaultExpansionCount": "默认展开数", + "alwaysExpandSingleDoc": "单篇文档始终展开", + "displayDocBlock": "显示文档块", + "doubleClickTimeThreshold": "双击时间阈值(毫秒)", + "previewRefreshHighlightDelayTips": "用于代码块、数据库这种需要时间渲染的块高亮,太短可能会失败,不需要可以设置为0", + "previewRefreshHighlightDelay": "刷新预览区高亮延迟(毫秒)", + "settingHub": "设置中心", + "switchCurrentDocumentSearchFailureMessage": "文档搜索插件: 没有获取到打开的文档,可切换页签或重新打开文档。", + "swapDocumentItemClickLogic": "交换文档项点击逻辑", + "swapDocumentItemClickLogicTips": "禁用时,单击展开搜索结果,双击打开文档;启用时,单击打开文档,双击展开搜索结果。", + "allNotebooks": "所有笔记本", + "specifyNotebook": "指定笔记本", + "searchInTheCurrentDocument": "在当前文档查询" + +} \ No newline at end of file diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..82fa0dc --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,5 @@ +.venv +build +dist +*.exe +*.spec diff --git a/scripts/make_dev_link.js b/scripts/make_dev_link.js new file mode 100644 index 0000000..ab567c4 --- /dev/null +++ b/scripts/make_dev_link.js @@ -0,0 +1,186 @@ +import fs from 'fs'; +import http from 'node:http'; +import readline from 'node:readline'; + + +//************************************ Write you dir here ************************************ + +//Please write the "workspace/data/plugins" directory here +//请在这里填写你的 "workspace/data/plugins" 目录 +let targetDir = 'E:/AppData/SiYuanData/testSpace/data/plugins'; +//Like this +// let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; +//******************************************************************************************** + +const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); +const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); + +let POST_HEADER = { + // "Authorization": `Token ${token}`, + "Content-Type": "application/json", +} + +async function myfetch(url, options) { + //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 + return new Promise((resolve, reject) => { + let req = http.request(url, options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ + ok: true, + status: res.statusCode, + json: () => JSON.parse(data) + }); + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} + +async function getSiYuanDir() { + let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; + let conf = {}; + try { + let response = await myfetch(url, { + method: 'POST', + headers: POST_HEADER + }); + if (response.ok) { + conf = await response.json(); + } else { + error(`\tHTTP-Error: ${response.status}`); + return null; + } + } catch (e) { + error(`\tError: ${e}`); + error("\tPlease make sure SiYuan is running!!!"); + return null; + } + return conf.data; +} + +async function chooseTarget(workspaces) { + let count = workspaces.length; + log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) + for (let i = 0; i < workspaces.length; i++) { + log(`\t[${i}] ${workspaces[i].path}`); + } + + if (count == 1) { + return `${workspaces[0].path}/data/plugins`; + } else { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + let index = await new Promise((resolve, reject) => { + rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { + resolve(answer); + }); + }); + rl.close(); + return `${workspaces[index].path}/data/plugins`; + } +} + +log('>>> Try to visit constant "targetDir" in make_dev_link.js...') + +if (targetDir === '') { + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') + let res = await getSiYuanDir(); + + if (res === null || res === undefined || res.length === 0) { + log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); + + // console.log(process.env) + let env = process.env?.SIYUAN_PLUGIN_DIR; + if (env !== undefined && env !== null && env !== '') { + targetDir = env; + log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); + } else { + error('\tCan not get SiYuan directory from environment variable "SIYUAN_PLUGIN_DIR", failed!'); + process.exit(1); + } + } else { + targetDir = await chooseTarget(res); + } + + + log(`>>> Successfully got target directory: ${targetDir}`); +} + +//Check +if (!fs.existsSync(targetDir)) { + error(`Failed! plugin directory not exists: "${targetDir}"`); + error(`Please set the plugin directory in scripts/make_dev_link.js`); + process.exit(1); +} + + +//check if plugin.json exists +if (!fs.existsSync('./plugin.json')) { + //change dir to parent + process.chdir('../'); + if (!fs.existsSync('./plugin.json')) { + error('Failed! plugin.json not found'); + process.exit(1); + } +} + +//load plugin.json +const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); +const name = plugin?.name; +if (!name || name === '') { + error('Failed! Please set plugin name in plugin.json'); + process.exit(1); +} + +//dev directory +const devDir = `${process.cwd()}/dev`; +//mkdir if not exists +if (!fs.existsSync(devDir)) { + fs.mkdirSync(devDir); +} + +function cmpPath(path1, path2) { + path1 = path1.replace(/\\/g, '/'); + path2 = path2.replace(/\\/g, '/'); + // sepertor at tail + if (path1[path1.length - 1] !== '/') { + path1 += '/'; + } + if (path2[path2.length - 1] !== '/') { + path2 += '/'; + } + return path1 === path2; +} + +const targetPath = `${targetDir}/${name}`; +//如果已经存在,就退出 +if (fs.existsSync(targetPath)) { + let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); + + if (isSymbol) { + let srcPath = fs.readlinkSync(targetPath); + + if (cmpPath(srcPath, devDir)) { + log(`Good! ${targetPath} is already linked to ${devDir}`); + } else { + error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); + } + } else { + error(`Failed! ${targetPath} already exists and is not a symbolic link`); + } + +} else { + //创建软链接 + fs.symlinkSync(devDir, targetPath, 'junction'); + log(`Done! Created symlink ${targetPath}`); +} + diff --git a/scripts/make_install.js b/scripts/make_install.js new file mode 100644 index 0000000..32605a4 --- /dev/null +++ b/scripts/make_install.js @@ -0,0 +1,191 @@ +import fs from 'fs'; +import path from 'path'; +import http from 'node:http'; +import readline from 'node:readline'; + + +//************************************ Write you dir here ************************************ + +let targetDir = ''; // the target directory of the plugin, '*/data/plugin' +//******************************************************************************************** + +const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); +const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); + +let POST_HEADER = { + "Authorization": ``, + "Content-Type": "application/json", +} + +async function myfetch(url, options) { + //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 + return new Promise((resolve, reject) => { + let req = http.request(url, options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ + ok: true, + status: res.statusCode, + json: () => JSON.parse(data) + }); + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} + +async function getSiYuanDir() { + let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; + let conf = {}; + try { + let response = await myfetch(url, { + method: 'POST', + headers: POST_HEADER + }); + if (response.ok) { + conf = await response.json(); + } else { + error(`\tHTTP-Error: ${response.status}`); + return null; + } + } catch (e) { + error(`\tError: ${e}`); + error("\tPlease make sure SiYuan is running!!!"); + return null; + } + return conf.data; +} + +async function chooseTarget(workspaces) { + let count = workspaces.length; + log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) + for (let i = 0; i < workspaces.length; i++) { + log(`\t[${i}] ${workspaces[i].path}`); + } + + if (count == 1) { + return `${workspaces[0].path}/data/plugins`; + } else { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + let index = await new Promise((resolve, reject) => { + rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { + resolve(answer); + }); + }); + rl.close(); + return `${workspaces[index].path}/data/plugins`; + } +} + +log('>>> Try to visit constant "targetDir" in make_install.js...') + +if (targetDir === '') { + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') + let res = await getSiYuanDir(); + + if (res === null || res === undefined || res.length === 0) { + error('>>> Can not get SiYuan directory automatically'); + + process.exit(1); + } else { + targetDir = await chooseTarget(res); + } + + log(`>>> Successfully got target directory: ${targetDir}`); +} + +//Check +if (!fs.existsSync(targetDir)) { + error(`Failed! plugin directory not exists: "${targetDir}"`); + error(`Please set the plugin directory in scripts/make_install.js`); + process.exit(1); +} + + +//check if plugin.json exists +if (!fs.existsSync('./plugin.json')) { + //change dir to parent + process.chdir('../'); + if (!fs.existsSync('./plugin.json')) { + error('Failed! plugin.json not found'); + process.exit(1); + } +} + +//load plugin.json +const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); +const name = plugin?.name; +if (!name || name === '') { + error('Failed! Please set plugin name in plugin.json'); + process.exit(1); +} + +const distDir = `${process.cwd()}/dist`; +//mkdir if not exists +if (!fs.existsSync(distDir)) { + fs.mkdirSync(distDir); +} + +function cmpPath(path1, path2) { + path1 = path1.replace(/\\/g, '/'); + path2 = path2.replace(/\\/g, '/'); + // sepertor at tail + if (path1[path1.length - 1] !== '/') { + path1 += '/'; + } + if (path2[path2.length - 1] !== '/') { + path2 += '/'; + } + return path1 === path2; +} + +const targetPath = `${targetDir}/${name}`; + + +function copyDirectory(srcDir, dstDir) { + if (!fs.existsSync(dstDir)) { + fs.mkdirSync(dstDir); + log(`Created directory ${dstDir}`); + } + //将 distDir 下的所有文件复制到 targetPath + fs.readdir(srcDir, { withFileTypes: true }, (err, files) => { + if (err) { + error('Error reading source directory:', err); + return; + } + + // 遍历源目录中的所有文件和子目录 + files.forEach((file) => { + const src = path.join(srcDir, file.name); + const dst = path.join(dstDir, file.name); + + // 判断当前项是文件还是目录 + if (file.isDirectory()) { + // 如果是目录,则递归调用复制函数复制子目录 + copyDirectory(src, dst); + } else { + // 如果是文件,则复制文件到目标目录 + fs.copyFile(src, dst, (err) => { + if (err) { + error('Error copying file:' + err); + } else { + log(`Copied file: ${src} --> ${dst}`); + } + }); + } + }); + log(`Copied ${distDir} to ${targetPath}`); + }); +} +copyDirectory(distDir, targetPath); + + diff --git a/src/components/code-block/CodeBlockService.ts b/src/components/code-block/CodeBlockService.ts new file mode 100644 index 0000000..a44d643 --- /dev/null +++ b/src/components/code-block/CodeBlockService.ts @@ -0,0 +1,376 @@ +import { EnvConfig } from "@/config/EnvConfig"; +import Instance from "@/utils/Instance"; +import CodeBlockNameInputSvelte from "@/components/code-block/code-block-name-input.svelte"; +import { findScrollableParent, stringToElement } from "@/utils/html-util"; +import { CssService } from "@/service/CssService"; +import { setBlockAttrs } from "@/utils/api"; +import { SettingService } from "@/service/SettingService"; +import { isValidStr } from "@/utils/string-util"; + +export class CodeBlockService { + + public static get ins(): CodeBlockService { + return Instance.get(CodeBlockService); + } + + public init() { + CssService.ins.initClass() + initBusEvent(); + initElementEvent(); + + } + + public destroy() { + EnvConfig.ins.plugin.eventBus.off("loaded-protyle-static", handleLoadedProtyle); + EnvConfig.ins.plugin.eventBus.off("loaded-protyle-dynamic", handleLoadedProtyle); + + EnvConfig.ins.plugin.eventBus.off("ws-main", handlewsMain); + + document.removeEventListener("mouseup", handleDocumentMouseup); + document.removeEventListener("mousemove", handleDocumentMousemove); + + let dragBarElementArray = document.querySelectorAll(".drag-bar.misuzu2027__copy"); + if (dragBarElementArray) { + for (const element of dragBarElementArray) { + element.remove(); + } + } + let nameingElementArray = document.querySelectorAll("span[misuzu2027-code-block-naming].misuzu2027__copy"); + if (nameingElementArray) { + for (const element of nameingElementArray) { + element.remove(); + } + } + } + +} + + + +function initBusEvent() { + let settingConfig = SettingService.ins.SettingConfig; + if (settingConfig.codeBlockTopLanguages + || settingConfig.codeBlockNaming + || settingConfig.codeBlockMaxHeight + || settingConfig.codeBlockAdjustHeight + ) { + + EnvConfig.ins.plugin.eventBus.off("loaded-protyle-static", handleLoadedProtyle); + EnvConfig.ins.plugin.eventBus.off("loaded-protyle-dynamic", handleLoadedProtyle); + + EnvConfig.ins.plugin.eventBus.on("loaded-protyle-static", handleLoadedProtyle); + EnvConfig.ins.plugin.eventBus.on("loaded-protyle-dynamic", handleLoadedProtyle); + + } + + if (settingConfig.codeBlockNaming + || settingConfig.codeBlockMaxHeight + || settingConfig.codeBlockAdjustHeight + ) { + EnvConfig.ins.plugin.eventBus.off("ws-main", handlewsMain); + + EnvConfig.ins.plugin.eventBus.on("ws-main", handlewsMain); + } +} + +function initElementEvent() { + + document.addEventListener("mouseup", handleDocumentMouseup); + + document.addEventListener("mousemove", handleDocumentMousemove); +} + + + +function handleLoadedProtyle(e) { + let protyleElement = e.detail.protyle.element; + addObserveCodeBlockLanguageElement(protyleElement); + let wysiwygElement = e.detail.protyle.wysiwyg.element; + initProtyleElement(wysiwygElement); + +} + +function handlewsMain(e) { + + if (e.detail.cmd != "transactions" + || !e.detail.data + ) { + return; + } + let existUpdateCodeBlock = false; + for (const dataObj of e.detail.data) { + if (!dataObj || !dataObj.doOperations) { + continue; + } + for (const doOperation of dataObj.doOperations) { + if (doOperation && (doOperation.action == "update" || doOperation.action == "insert")) { + let operationElement = stringToElement(doOperation.data); + if (operationElement && operationElement.getAttribute("data-type") == "NodeCodeBlock") { + existUpdateCodeBlock = true; + break; + } + } + } + } + + if (!existUpdateCodeBlock) { + return; + } + + let codeBlockElementList = document.querySelectorAll(`div[data-node-id][data-type*="NodeCodeBlock"]`); + initCodeBlockElementList(codeBlockElementList); + + // "savedoc" "transactions" + // detail.data[0].doOperations.id +} + +function handleDocumentMouseup() { + if (isDragging) { + if (draggingBarElement) { + draggingBarElement.style.removeProperty("background-color"); + } + isDragging = false; + + let attrs = {}; + attrs[ATTR_KEY] = draggingCodeBlockContainerElement.style.height; + setBlockAttrs(draggingCodeBlockId, attrs); + } +} + +function handleDocumentMousemove(e) { + e.stopPropagation(); + if (!isDragging) return; + let offsetY = draggingStartY - e.clientY; + if (offsetY == 0) { + return; + } + let newScrollTop = draggingScrollableParentElement.scrollTop - draggingScrollTop; + let newHeight = draggingStartHeight - offsetY + newScrollTop; + updateCodeBlockHeightElement(draggingCodeBlockContainerElement, newHeight); +}; + +function initProtyleElement(protyleContentElement: HTMLElement) { + let codeBlockElementArray = protyleContentElement.querySelectorAll(`div[data-node-id][data-type*="NodeCodeBlock"]`); + initCodeBlockElementList(codeBlockElementArray); +} + +function initCodeBlockElementList(codeBlockElementArray: NodeListOf) { + let settingConfig = SettingService.ins.SettingConfig; + if (!codeBlockElementArray || !settingConfig) { + return; + } + + for (const codeBlockElement of codeBlockElementArray) { + // 添加代码块命名 + if (settingConfig.codeBlockNaming) { + createCodeBlockNameInputSvelte(codeBlockElement); + } + + // 创建折叠按钮 ,按钮一直无法显示,放弃 + // if (settingConfig.codeBlockToggle) { + // console.log("codeBlockToggle") + // createCodeBlockToggleButton(codeBlockElement); + // } + + + // 设置代码块最大高度 + if (settingConfig.codeBlockMaxHeight && settingConfig.codeBlockMaxHeight > 50) { + let codeBlockContainerElement = codeBlockElement.querySelector("div.hljs") as HTMLElement; + updateCodeBlockMaxHeightElement(codeBlockContainerElement, settingConfig.codeBlockMaxHeight); + } + + // 创建拖动条 + if (settingConfig.codeBlockAdjustHeight) { + addCodeBlockDragBar(codeBlockElement); + } + + + } +} + + + +function createCodeBlockNameInputSvelte( + codeBlockElement: Element,) { + if (!codeBlockElement || codeBlockElement.querySelector("span[misuzu2027-code-block-naming]")) { + return; + } + let blockId = codeBlockElement.getAttribute("data-node-id"); + let blockName = codeBlockElement.getAttribute("name"); + let languageSelectElement = codeBlockElement.querySelector(`.protyle-action span.protyle-action__language`); + let codeBlockNameElement = document.createElement("span"); + codeBlockNameElement.classList.add("misuzu2027__copy", "misuzu2027__protyle-custom"); + codeBlockNameElement.setAttribute("contenteditable", "false"); + codeBlockNameElement.setAttribute("misuzu2027-code-block-naming", "1"); + + new CodeBlockNameInputSvelte({ + target: codeBlockNameElement, + props: { + codeBlockId: blockId, + codeBlockName: blockName, + } + }); + languageSelectElement.insertAdjacentElement('afterend', codeBlockNameElement); + + +} + + +let ATTR_KEY = "custom-misuzu2027-code-block-height"; + +let isDragging = false; +let draggingBarElement: HTMLElement; +let draggingCodeBlockId: string; +let draggingCodeBlockContainerElement: HTMLElement; +let draggingStartY: number; +let draggingScrollableParentElement: HTMLElement; +let draggingScrollTop: number; +let draggingStartHeight: number; +function addCodeBlockDragBar(codeBlockElement: Element) { + if (codeBlockElement.querySelector("div.drag-bar.misuzu2027__protyle-custom")) { + return; + } + // 父容器禁止输入。 + // codeBlockElement.setAttribute("contenteditable", "false"); + + let lastHeight = codeBlockElement.getAttribute(ATTR_KEY); + let hljsElement = codeBlockElement.querySelector("div.hljs") as HTMLElement; + // let linenumberElement = codeBlockElement.querySelector(".protyle-linenumber__rows") as HTMLElement; + + let dragBarElement = document.createElement("div"); + dragBarElement.classList.add("drag-bar", "misuzu2027__copy", "misuzu2027__protyle-custom"); + dragBarElement.setAttribute("contenteditable", "false") + // if (linenumberElement) { + // linenumberElement.insertAdjacentElement('afterend', dragBarElement); + // } else { + // hljsElement.insertAdjacentElement('afterend', dragBarElement); + // } + hljsElement.insertAdjacentElement('afterend', dragBarElement); + + // addObserveCodeBlockLinenumberElement(hljsElement); + // addCodeBlockContainerElementScrollEvent(hljsElement); + + updateCodeBlockHeightElement(hljsElement, parseFloat(lastHeight)); + + dragBarElement.addEventListener('click', (e) => { + e.stopPropagation(); + // hljsElement.focus(); + }); + + dragBarElement.addEventListener('mousedown', (e) => { + e.stopPropagation(); + // hljsElement.focus(); + isDragging = true; + draggingBarElement = dragBarElement; + draggingCodeBlockId = codeBlockElement.getAttribute("data-node-id"); + draggingCodeBlockContainerElement = hljsElement + draggingStartY = e.clientY; + draggingScrollableParentElement = findScrollableParent(codeBlockElement as HTMLElement); + draggingScrollTop = draggingScrollableParentElement.scrollTop; + draggingStartHeight = parseFloat(hljsElement.style.height) + if (!draggingStartHeight) { + draggingStartHeight = parseFloat(window.getComputedStyle(hljsElement).height); + } + }); + + +} + + +function updateCodeBlockHeightElement(codeBlockContainerElement: HTMLElement, newHeight: number) { + if (!codeBlockContainerElement || isNaN(newHeight) || newHeight < 20) { + if (newHeight < 20 && isDragging && draggingBarElement) { + draggingBarElement.style.backgroundColor = "red"; + } + return; + } + let maxHeight = parseFloat(codeBlockContainerElement.style.maxHeight); + if (!isNaN(maxHeight) && newHeight > maxHeight + 1) { + if (newHeight > maxHeight + 1 && isDragging && draggingBarElement) { + draggingBarElement.style.backgroundColor = "red"; + } + return; + } + if (draggingBarElement) { + draggingBarElement.style.removeProperty("background-color"); + } + + codeBlockContainerElement.style.height = `${newHeight}px`; + + // let linenumberElement = codeBlockContainerElement.parentElement.querySelector(".protyle-linenumber__rows") as HTMLElement; + // if (linenumberElement) { + // let codeBlockContainerElementRect = codeBlockContainerElement.getBoundingClientRect(); + // linenumberElement.style.height = codeBlockContainerElementRect.height + "px" + // } +} + + +function updateCodeBlockMaxHeightElement(codeBlockContainerElement: HTMLElement, newMaxHeight: number) { + if (!codeBlockContainerElement || isNaN(newMaxHeight) || newMaxHeight < 20) { + return; + } + + codeBlockContainerElement.style.maxHeight = `${newMaxHeight}px`; + // let linenumberElement = codeBlockContainerElement.parentElement.querySelector(".protyle-linenumber__rows") as HTMLElement; + // addCodeBlockContainerElementScrollEvent(codeBlockContainerElement); + // let offsetHeight = codeBlockContainerElement.getBoundingClientRect().height - parseFloat(window.getComputedStyle(codeBlockContainerElement).height); + // if (linenumberElement) { + // let linenumberHeight = newMaxHeight + offsetHeight; + // linenumberElement.style.maxHeight = linenumberHeight + "px" + // } + // addObserveCodeBlockLinenumberElement(codeBlockContainerElement); +} + + +function addObserveCodeBlockLanguageElement(protyleElement: HTMLElement) { + + let protyleUtilElement = protyleElement.querySelector("div.protyle-util"); + if (protyleUtilElement.getAttribute("data-misuzu2027-observed") == "1") { + return; + } + + // 创建一个 MutationObserver 实例,并传入回调函数 + const observer = new MutationObserver((mutationsList, observer) => { + let codeBlockTopLanguages = SettingService.ins.SettingConfig.codeBlockTopLanguages; + if (!isValidStr(codeBlockTopLanguages)) { + return; + } + + for (const mutation of mutationsList) { + if (mutation.type === 'childList' && mutation.addedNodes) { + for (let childNode of mutation.addedNodes.values()) { + const childElement = childNode as HTMLElement; + if (Node.ELEMENT_NODE != childNode.nodeType + || !childElement.matches("div.fn__flex-column") + || childElement.children.length != 2 + || childElement.children[0].tagName.toLowerCase() !== "input" + || !childElement.children[0].matches(".b3-text-field") + || !childElement.children[1].matches("div.b3-list.fn__flex-1.b3-list--background") + ) { + continue; + } + let languageSelectElement = childElement.lastElementChild; + let oldFirstItemElement = languageSelectElement.children[1]; + let topLanguageArray = codeBlockTopLanguages.replace(/,/g, ',').split(","); + for (const language of topLanguageArray) { + if (!isValidStr(language)) { + continue; + } + let newItemElement = document.createElement("div"); + newItemElement.classList.add("b3-list-item"); + newItemElement.textContent = language; + languageSelectElement.insertBefore(newItemElement, oldFirstItemElement); + } + languageSelectElement.insertBefore(document.createElement("hr"), languageSelectElement.children[1]); + languageSelectElement.insertBefore(document.createElement("hr"), oldFirstItemElement); + } + } + } + }); + + // 配置 MutationObserver 监听的类型 + const config = { childList: true, }; + protyleUtilElement.setAttribute("data-misuzu2027-observed", "1") + // 开始观察目标节点 + observer.observe(protyleUtilElement, config); +} \ No newline at end of file diff --git a/src/components/code-block/code-block-name-input.svelte b/src/components/code-block/code-block-name-input.svelte new file mode 100644 index 0000000..cba1d9b --- /dev/null +++ b/src/components/code-block/code-block-name-input.svelte @@ -0,0 +1,49 @@ + + + + + diff --git a/src/components/filetree/FileTreeService.ts b/src/components/filetree/FileTreeService.ts new file mode 100644 index 0000000..6e1657a --- /dev/null +++ b/src/components/filetree/FileTreeService.ts @@ -0,0 +1,141 @@ +import { SettingService } from "@/service/SettingService"; +import Instance from "@/utils/Instance"; +import { containsAllKeywords, splitKeywordStringToArray } from "@/utils/string-util"; + +export class FileTreeService { + private intervalId; + + + public static get ins(): FileTreeService { + return Instance.get(FileTreeService); + } + + public init() { + + let fileTreeKeywordFilter = SettingService.ins.SettingConfig.fileTreeKeywordFilter; + + if (fileTreeKeywordFilter) { + initFileTreeSearchInput(); + this.intervalId = setInterval(initFileTreeSearchInput, 1000) + } else { + this.destroy(); + } + } + + public destroy() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + let inputElement = document.querySelector("div.file-tree.sy__file div.misuzu2027__search-div"); + if (inputElement) { + inputElement.remove(); + } + } + +} + +function initFileTreeSearchInput() { + let fileTreeDocElement = document.querySelector("#layouts div.layout-tab-container div.file-tree.sy__file"); + if (!fileTreeDocElement) { + return; + } + if (fileTreeDocElement.querySelector("input.misuzu2027__search-input")) { + return + } + + let anchorElement = fileTreeDocElement.querySelector("div.block__icons "); + if (!anchorElement) { + return; + } + let searchDivElement = document.createElement("div"); + searchDivElement.style.margin = "1px 8px"; + searchDivElement.classList.add("misuzu2027__search-div", "misuzu2027__copy", "misuzu2027__protyle-custom"); + let searchInputElement = document.createElement("input"); + searchInputElement.classList.add("b3-text-field", "fn__block", "misuzu2027__search-input", "misuzu2027__copy", "misuzu2027__protyle-custom"); + + // let searchClearElement = document.createElement("svg"); + // searchClearElement.classList.add("b3-form__icon-clear"); + // searchClearElement.setAttribute("style", `right: 8px; height: 42px;`); + // searchClearElement.innerHTML = `` + + searchDivElement.append(searchInputElement); + // searchDivElement.append(searchClearElement); + + anchorElement.insertAdjacentElement('afterend', searchDivElement); + + searchInputElement.addEventListener("input", (event: InputEvent) => { + console.log("searchInputElement input") + if (event.isComposing) { + return; + } + let searchKeyword = searchInputElement.value; + let keywordArray = splitKeywordStringToArray(searchKeyword); + + let boxUlElementArray = fileTreeDocElement.querySelectorAll("ul[data-url].b3-list.b3-list--background"); + for (const ulElement of boxUlElementArray) { + let boxNameElement = ulElement.querySelector("li.b3-list-item > span.b3-list-item__text") + let boxName = boxNameElement.textContent; + if (containsAllKeywords(boxName, keywordArray)) { + ulElement.classList.remove("fn__none"); + } else { + ulElement.classList.add("fn__none"); + } + } + + + }) + +} + + + +// let lastNoteBookMap; +// function initFileTreeElement() { +// let fileTreeElement = document.querySelector("#layouts div.layout-tab-container div.file-tree.sy__file > div.fn__flex-1"); +// if (!fileTreeElement) { +// return +// } +// let notebookMap = getNotebookMap(); +// if (areMapsEqual(lastNoteBookMap, notebookMap)) { +// return; +// } +// lastNoteBookMap = notebookMap; +// console.log(`FileTreeService notebookMap `, notebookMap) +// let boxGroupElement = document.createElement("div"); +// boxGroupElement.classList.add("b3-list", "b3-list--background"); +// boxGroupElement.innerHTML = ` +//
+// +// +// +// 🗃 +// 测试文档分组 +//
+// `; + + +// let boxUlElement = fileTreeElement.querySelector(`ul[data-url="${notebookMap.keys().next().value}"].b3-list.b3-list--background`); + +// boxUlElement.before(boxGroupElement); +// boxGroupElement.append(boxUlElement); +// boxGroupElement.style.margin = "1px 0px" +// } + + +// function getNotebookMap(): Map { +// let fileTreeElement = document.querySelector("#layouts div.layout-tab-container div.file-tree.sy__file > div.fn__flex-1"); +// let idNameMap = new Map(); +// let boxUlElementArray = fileTreeElement.querySelectorAll("ul[data-url].b3-list.b3-list--background"); +// for (const ulElement of boxUlElementArray) { +// let boxId = ulElement.getAttribute("data-url"); +// let boxNameElement = ulElement.querySelector("li.b3-list-item > span.b3-list-item__text") +// let boxName = boxNameElement.textContent; +// if (isValidStr(boxId) && isValidStr(boxName)) { +// idNameMap.set(boxId, boxName); +// } +// } + +// return idNameMap; +// } diff --git a/src/components/setting/inputs/setting-input.svelte b/src/components/setting/inputs/setting-input.svelte new file mode 100644 index 0000000..825fb46 --- /dev/null +++ b/src/components/setting/inputs/setting-input.svelte @@ -0,0 +1,41 @@ + + +{#if itemProperty.type === "text"} + +{:else if itemProperty.type === "number"} + +{/if} diff --git a/src/components/setting/inputs/setting-select.svelte b/src/components/setting/inputs/setting-select.svelte new file mode 100644 index 0000000..f93703c --- /dev/null +++ b/src/components/setting/inputs/setting-select.svelte @@ -0,0 +1,21 @@ + + + diff --git a/src/components/setting/inputs/setting-switch.svelte b/src/components/setting/inputs/setting-switch.svelte new file mode 100644 index 0000000..5dc6fb8 --- /dev/null +++ b/src/components/setting/inputs/setting-switch.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/components/setting/setting-item.svelte b/src/components/setting/setting-item.svelte new file mode 100644 index 0000000..5c3fe40 --- /dev/null +++ b/src/components/setting/setting-item.svelte @@ -0,0 +1,16 @@ + + +
+
+ {itemProperty.name} +
+ {@html itemProperty.description} +
+
+
+ +
diff --git a/src/components/setting/setting-page.svelte b/src/components/setting/setting-page.svelte new file mode 100644 index 0000000..74441b2 --- /dev/null +++ b/src/components/setting/setting-page.svelte @@ -0,0 +1,67 @@ + + + + +
+
    + {#each tabArray as tab} +
  • { + activeTab = tab.key; + }} + on:keydown={handleKeyDownDefault} + > + + + + {tab.name} +
  • + {/each} +
+
+ {#each tabArray as tab} + {#if activeTab === tab.key} +
+ {#each tab.props as itemProperty} + + {#if itemProperty.type == "switch"} + + {:else if itemProperty.type == "select"} + + {:else if itemProperty.type == "number" || itemProperty.type == "text"} + + {:else} + 不能载入设置项,请检查设置代码实现。 Key: {itemProperty.key} +
+ can't load settings, check code please. Key: + {itemProperty.key} + {/if} +
+ {/each} +
+ {/if} + {/each} +
+
diff --git a/src/components/setting/setting-util.ts b/src/components/setting/setting-util.ts new file mode 100644 index 0000000..96d09b3 --- /dev/null +++ b/src/components/setting/setting-util.ts @@ -0,0 +1,27 @@ +import { EnvConfig } from "@/config/EnvConfig"; +import { Dialog } from "siyuan"; +import SettingPageSvelte from "@/components/setting/setting-page.svelte" + + + + +export function openSettingsDialog() { + let isMobile = EnvConfig.ins.isMobile; + // 生成Dialog内容 + const dialogId = "backlink-panel-setting-" + Date.now(); + // 创建dialog + const settingDialog = new Dialog({ + title: "反链面板插件设置", + content: ` +
+ `, + width: isMobile ? "92vw" : "1040px", + height: isMobile ? "50vw" : "80vh", + }); + + new SettingPageSvelte({ + target: settingDialog.element.querySelector(`#${dialogId}`), + }); + + +} \ No newline at end of file diff --git a/src/config/EnvConfig.ts b/src/config/EnvConfig.ts new file mode 100644 index 0000000..8976a16 --- /dev/null +++ b/src/config/EnvConfig.ts @@ -0,0 +1,65 @@ +import { lsNotebooks } from "@/utils/api"; +import { convertIconInIal } from "@/utils/icon-util"; +import Instance from "@/utils/Instance"; +import { App, I18N, IDockModel, IPluginDockTab, Plugin, getFrontend } from "siyuan"; + +export class EnvConfig { + + + public static get ins(): EnvConfig { + return Instance.get(EnvConfig); + } + + get isMobile(): boolean { + let frontEnd: string = getFrontend(); + let isMobile = frontEnd === "mobile" || frontEnd === "browser-mobile"; + return isMobile; + } + + private _plugin: Plugin; + get plugin(): Plugin { + return this._plugin; + } + + get app(): App { + return this._plugin.app; + } + + get i18n(): I18N { + if (this._plugin) { + return this._plugin.i18n; + } + const i18nObject: I18N = { + // 添加你需要的属性和方法 + }; + return i18nObject; + } + + public lastViewedDocId: string; + + + public init(plugin: Plugin) { + this._plugin = plugin; + } + + + // docSearchDock: { config: IPluginDockTab, model: IDockModel }; + // flatDocTreeDock: { config: IPluginDockTab, model: IDockModel }; + + private _notebookMap: Map = new Map(); + public async notebookMap(cache: boolean): Promise> { + if (cache && this._notebookMap && this._notebookMap.size > 0) { + return this._notebookMap + } else { + let notebooks: Notebook[] = (await lsNotebooks()).notebooks; + this._notebookMap.clear(); + for (const notebook of notebooks) { + notebook.icon = convertIconInIal(notebook.icon); + this._notebookMap.set(notebook.id, notebook); + } + } + return this._notebookMap; + } + + +} \ No newline at end of file diff --git a/src/i18n/en_US.json b/src/i18n/en_US.json new file mode 100644 index 0000000..42ab238 --- /dev/null +++ b/src/i18n/en_US.json @@ -0,0 +1,83 @@ +{ + "openDocumentSearchTab": "Open Document-based Search Tab", + "table": "Table", + "mathBlock": "Formula block", + "quoteBlock": "Blockquote", + "superBlock": "Super block", + "paragraph": "Paragraph", + "doc": "Doc", + "headings": "Headings", + "list": "List", + "listItem": "List item", + "codeBlock": "Code block", + "htmlBlock": "HTML block", + "embedBlock": "Embed block", + "database": "Database", + "video": "Video", + "audio": "Audio", + "IFrame": "IFrame", + "widget": "Widget", + "name": "Name", + "alias": "Alias", + "memo": "Memo", + "allAttrs": "All attribute names and attribute values", + "sortByRankASC": "Relevance ASC", + "sortByRankDESC": "Relevance DESC", + "modifiedASC": "Modified Time ASC", + "modifiedDESC": "Modified Time DESC", + "createdASC": "Created Time ASC", + "createdDESC": "Created Time DESC", + "type": "Type", + "sortByContent": "Original content order", + "sortByTypeAndContent": "Type And Original content order", + "show": "Show", + "hide": "Hide", + "refCountASC": "Ref Count ASC", + "refCountDESC": "Ref Count DESC", + "fileNameASC": "Name Alphabet ASC", + "fileNameDESC": "Name Alphabet DESC", + "fileNameNatASC": "Name Natural ASC", + "fileNameNatDESC": "Name Natural DESC", + "notebook": "Notebook", + "path": "Path", + "sort": "Sort", + "clear": "Clear", + "refresh": "Refresh", + "reference": "Ref", + "documentBasedSearch": "Document-based Search", + "flatDocumentTree": "Flat Document Tree", + "documentBasedSearchDock": "Document-based Search Dock", + "flatDocumentTreeDock": "Flat Document Tree Dock", + "previousLabel": "Previous", + "nextLabel": "Next", + "findInDoc": "Found ${x} documents", + "notebookFilter": "Notebook Filter", + "attr": "Attribute", + "other": "Other", + "expand": "Expand", + "collapse": "Collapse", + "noContentBelow": "No content matching the criteria exists below", + "dockModifyTips": "Note: Modifying Dock will refresh the interface.", + "settingDock": "🌈 Dock Settings", + "settingNotebookFilter": "🌈 Notebook Filter", + "settingType": "🌈 Type", + "settingAttr": "🌈 Attribute", + "settingOther": "🌈 Other", + "docSortMethod": "Document Sorting Method", + "contentBlockSortMethod": "Content Block Sorting Method", + "documentsPerPage": "Documents Per Page", + "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.", + "defaultExpansionCount": "Default Expansion Count", + "alwaysExpandSingleDoc": "Always Expand Single Document", + "displayDocBlock": "Display Document Blocks", + "doubleClickTimeThreshold": "Double Click Time Threshold (Milliseconds)", + "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.", + "previewRefreshHighlightDelay": "Preview Refresh Highlight Delay (Milliseconds)", + "settingHub": "Settings Hub", + "switchCurrentDocumentSearchFailureMessage": "Document Search Plugin: Failed to retrieve the opened document. You can switch tabs or reopen the document.", + "swapDocumentItemClickLogic": "Swap Document Item Click Logic", + "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.", + "allNotebooks": "All Notebooks", + "specifyNotebook": "Specify Notebook", + "searchInTheCurrentDocument": "Search in the current document" +} \ No newline at end of file diff --git a/src/i18n/zh_CN.json b/src/i18n/zh_CN.json new file mode 100644 index 0000000..4816714 --- /dev/null +++ b/src/i18n/zh_CN.json @@ -0,0 +1,84 @@ +{ + "openDocumentSearchTab": "打开搜索页签", + "table": "表格", + "mathBlock": "公式块", + "quoteBlock": "引述块", + "superBlock": "超级块", + "paragraph": "段落", + "doc": "文档", + "headings": "标题", + "list": "列表", + "listItem": "列表项", + "codeBlock": "代码块", + "htmlBlock": "HTML 块", + "embedBlock": "嵌入块", + "database": "数据库", + "video": "视频", + "audio": "音频", + "IFrame": "IFrame", + "widget": "挂件", + "name": "name", + "alias": "别名", + "memo": "备注", + "allAttrs": "所有属性名和属性值", + "sortByRankASC": "按相关度升序", + "sortByRankDESC": "按相关度降序", + "modifiedASC": "修改时间升序", + "modifiedDESC": "修改时间降序", + "createdASC": "创建时间升序", + "createdDESC": "创建时间降序", + "type": "类型", + "sortByContent": "按原文内容顺序", + "sortByTypeAndContent": "类型和原文内容顺序", + "show": "显示", + "hide": "隐藏", + "refCountASC": "引用数升序", + "refCountDESC": "引用数降序", + "fileNameASC": "名称字母升序", + "fileNameDESC": "名称字母降序", + "fileNameNatASC": "名称自然升序", + "fileNameNatDESC": "名称自然降序", + "notebook": "笔记本", + "path": "路径", + "sort": "排序", + "clear": "清空", + "refresh": "刷新", + "reference": "引用", + "documentBasedSearch": "基于文档搜索", + "flatDocumentTree": "扁平化文档树", + "documentBasedSearchDock": "基于文档搜索 Dock", + "flatDocumentTreeDock": "扁平化文档树 Dock", + "previousLabel": "上一页", + "nextLabel": "下一页", + "findInDoc": "匹配到 ${x} 个文档", + "notebookFilter": "笔记本过滤", + "attr": "属性", + "other": "其他", + "expand": "展开", + "collapse": "折叠", + "noContentBelow": "下不存在符合条件的内容", + "dockModifyTips": "注:修改 Dock 会刷新界面。", + "settingDock": "🌈 Dock 设置", + "settingNotebookFilter": "🌈 笔记本过滤", + "settingType": "🌈 类型", + "settingAttr": "🌈 属性", + "settingOther": "🌈 其他", + "docSortMethod": "文档排序方式", + "contentBlockSortMethod": "内容块排序方式", + "documentsPerPage": "每页文档数量", + "blockCountBehaviorTips": "如果查询结果的块数量小于当前值,默认展开全部文档;反之会默认折叠全部文档。", + "defaultExpansionCount": "默认展开数", + "alwaysExpandSingleDoc": "单篇文档始终展开", + "displayDocBlock": "显示文档块", + "doubleClickTimeThreshold": "双击时间阈值(毫秒)", + "previewRefreshHighlightDelayTips": "用于代码块、数据库这种需要时间渲染的块高亮,太短可能会失败,不需要可以设置为0", + "previewRefreshHighlightDelay": "刷新预览区高亮延迟(毫秒)", + "settingHub": "设置中心", + "switchCurrentDocumentSearchFailureMessage": "文档搜索插件: 没有获取到打开的文档,可切换页签或重新打开文档。", + "swapDocumentItemClickLogic": "交换文档项点击逻辑", + "swapDocumentItemClickLogicTips": "禁用时,单击展开搜索结果,双击打开文档;启用时,单击打开文档,双击展开搜索结果。", + "allNotebooks": "所有笔记本", + "specifyNotebook": "指定笔记本", + "searchInTheCurrentDocument": "在当前文档查询" + +} \ No newline at end of file diff --git a/src/index.scss b/src/index.scss new file mode 100644 index 0000000..63dce7c --- /dev/null +++ b/src/index.scss @@ -0,0 +1,87 @@ +/* 引用文本样式 */ +body.misuzu2027.ref-text__brackets .protyle-wysiwyg [data-node-id] span[data-type*="block-ref"]:not([data-type="virtual-block-ref"]):not([data-type*="sup"]):not([data-type*="sub"]):not(.av__celltext--ref) { + font-weight: inherit; + background-color: transparent; + border-bottom: none; + color: var(--b3-theme-primary) +} + +body.misuzu2027.ref-text__brackets .protyle-wysiwyg [data-node-id] span[data-type*="block-ref"]::before { + content: "[["; + color: var(--b3-theme-on-surface); + display: inline-block; + text-align: center; + margin: 0 0 0 0.125em; + padding: 0; + position: inherit; + font-size: 1em; +} + + +body.misuzu2027.ref-text__brackets .protyle-wysiwyg [data-node-id] span[data-type*="block-ref"]::after { + content: "]]"; + color: var(--b3-theme-on-surface); + display: inline-block; + text-align: center; + margin: 0 0.125em 0 0; + padding: 0; + position: inherit; + font-size: 1em; +} + +/* 块引用 静态*/ +body.misuzu2027.ref-text__brackets .protyle-wysiwyg [data-node-id] span[data-type*="block-ref"][data-subtype="s"]::before { + content: "["; +} + +body.misuzu2027.ref-text__brackets .protyle-wysiwyg [data-node-id] span[data-type*="block-ref"][data-subtype="s"]::after { + content: "]"; +} + + + +/* ---------------------------start 代码块样式 start------------------------------*/ +/* ------------ 代码块限制最大高度 ------------ */ +/* 限制代码块高度,设置超过高度,出现滚动条 */ +body.misuzu2027.code-block__drag-height .protyle-wysiwyg .code-block .drag-bar { + position: absolute; + /* 距离容器底部的距离,可以根据需要调整 */ + bottom: 0px; + left: 50%; + transform: translateX(-50%); + width: 60%; + /* 拖动条宽度,可以根据需要调整 */ + height: 5px; + /* 拖动条高度,可以根据需要调整 */ + background-color: var(--b3-theme-on-background); + /* 拖动条背景颜色 */ + cursor: row-resize; + /* 圆角,可以根据需要调整 */ + border-radius: 5px; + // caret-color: transparent; + user-select: none; +} + +// body.misuzu2027.code-block__drag-height .protyle-wysiwyg .code-block .protyle-linenumber__rows { +// overflow: hidden; +// } + +// body.misuzu2027.code-block__max-height .protyle-wysiwyg .code-block .protyle-linenumber__rows { +// overflow: hidden; +// } + +/* ------------ 代码块折叠样式 ------------ */ +// body.misuzu2027 .protyle-wysiwyg .code-block span[misuzu2027-code-block-toggle] svg { +// display: none; +// } + +// body.misuzu2027 .protyle-wysiwyg .code-block:not([fold="1"]) span[misuzu2027-code-block-toggle] svg.misuzu2027__svg_up { +// display: inline-block; +// } + +// body.misuzu2027 .protyle-wysiwyg .code-block[fold="1"] span[misuzu2027-code-block-toggle] svg.misuzu2027__svg_down { + +// display: inline-block; +// } + +/* ---------------------------end 代码块样式 end------------------------------*/ \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cf58cdd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,70 @@ +import { + Plugin, +} from "siyuan"; +import "@/index.scss"; + + +import { EnvConfig } from "./config/EnvConfig"; +import { CUSTOM_ICON_MAP } from "./models/icon-constant"; +import { SettingService } from "./service/SettingService"; +import { openSettingsDialog } from "./components/setting/setting-util"; +import { CssService } from "./service/CssService"; +import { CodeBlockService } from "./components/code-block/CodeBlockService"; +import { FileTreeService } from "./components/filetree/FileTreeService"; + + +export default class PluginSample extends Plugin { + + + + async onload() { + EnvConfig.ins.init(this); + await SettingService.ins.init() + CssService.ins.init(); + CodeBlockService.ins.init(); + FileTreeService.ins.init(); + + // 图标的制作参见帮助文档 + for (const key in CUSTOM_ICON_MAP) { + if (Object.prototype.hasOwnProperty.call(CUSTOM_ICON_MAP, key)) { + const item = CUSTOM_ICON_MAP[key]; + this.addIcons(item.source); + } + } + + this.eventBus.on('switch-protyle', (e: any) => { + EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; + }) + this.eventBus.on('loaded-protyle-static', (e: any) => { + // console.log("index loaded-protyle-static ") + if (EnvConfig.ins.isMobile && !EnvConfig.ins.lastViewedDocId) { + EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; + } + }) + } + + + + + onLayoutReady() { + + } + + async onunload() { + CodeBlockService.ins.destroy(); + FileTreeService.ins.destroy(); + } + + uninstall() { + CodeBlockService.ins.destroy(); + FileTreeService.ins.destroy(); + // console.log("uninstall"); + } + + + openSetting(): void { + openSettingsDialog(); + } + + +} diff --git a/src/models/backlink-constant.ts b/src/models/backlink-constant.ts new file mode 100644 index 0000000..1adfb49 --- /dev/null +++ b/src/models/backlink-constant.ts @@ -0,0 +1,161 @@ +import { EnvConfig } from "@/config/EnvConfig"; + +export const DefinitionBlockStatus = { + SELECTED: 'SELECTED', + EXCLUDED: 'EXCLUDED', + OPTIONAL: 'OPTIONAL', + NOT_OPTIONAL: 'NOT_OPTIONAL', +}; + + +export function BACKLINK_BLOCK_SORT_METHOD_ELEMENT(): { name: string, value: BlockSortMethod }[] { + return [ + { + name: EnvConfig.ins.i18n.modifiedASC, + value: "modifiedAsc", + }, + { + name: EnvConfig.ins.i18n.modifiedDESC, + value: "modifiedDesc", + }, + { + name: EnvConfig.ins.i18n.createdASC, + value: "createdAsc", + }, + { + name: EnvConfig.ins.i18n.createdDESC, + value: "createdDesc", + }, + { + name: EnvConfig.ins.i18n.fileNameASC, + value: "alphabeticAsc", + }, + { + name: EnvConfig.ins.i18n.fileNameDESC, + value: "alphabeticDesc", + }, + { + name: "文档名称升序", + value: "documentAlphabeticAsc", + }, + { + name: "文档名称降序", + value: "documentAlphabeticDesc", + }, + ]; +} + + + +export function CUR_DOC_DEF_BLOCK_SORT_METHOD_ELEMENT(): { name: string, value: BlockSortMethod }[] { + + return [ + { + name: EnvConfig.ins.i18n.type, + value: "typeAndContent", + }, + { + name: EnvConfig.ins.i18n.refCountASC, + value: "refCountAsc", + }, + { + name: EnvConfig.ins.i18n.refCountDESC, + value: "refCountDesc", + }, + ]; +} + +export function RELATED_DEF_BLOCK_TYPE_ELEMENT(): { name: string, value: string }[] { + + return [ + { + name: "所有", + value: "all", + }, + { + name: "动态锚文本", + value: "dynamicAnchorText", + }, + { + name: "静态锚文本", + value: "staticAnchorText", + }, + ]; +} + + +export function RELATED_DEF_BLOCK_SORT_METHOD_ELEMENT(): { name: string, value: BlockSortMethod }[] { + + return [ + { + name: EnvConfig.ins.i18n.refCountASC, + value: "refCountAsc", + }, + { + name: EnvConfig.ins.i18n.refCountDESC, + value: "refCountDesc", + }, { + name: EnvConfig.ins.i18n.modifiedASC, + value: "modifiedAsc", + }, + { + name: EnvConfig.ins.i18n.modifiedDESC, + value: "modifiedDesc", + }, + { + name: EnvConfig.ins.i18n.createdASC, + value: "createdAsc", + }, + { + name: EnvConfig.ins.i18n.createdDESC, + value: "createdDesc", + }, + { + name: EnvConfig.ins.i18n.fileNameASC, + value: "alphabeticAsc", + }, + { + name: EnvConfig.ins.i18n.fileNameDESC, + value: "alphabeticDesc", + }, + ]; +} + + + +export function RELATED_DOCMUMENT_SORT_METHOD_ELEMENT(): { name: string, value: BlockSortMethod }[] { + + return [ + { + name: EnvConfig.ins.i18n.refCountASC, + value: "refCountAsc", + }, + { + name: EnvConfig.ins.i18n.refCountDESC, + value: "refCountDesc", + }, { + name: EnvConfig.ins.i18n.modifiedASC, + value: "modifiedAsc", + }, + { + name: EnvConfig.ins.i18n.modifiedDESC, + value: "modifiedDesc", + }, + { + name: EnvConfig.ins.i18n.createdASC, + value: "createdAsc", + }, + { + name: EnvConfig.ins.i18n.createdDESC, + value: "createdDesc", + }, + { + name: EnvConfig.ins.i18n.fileNameASC, + value: "alphabeticAsc", + }, + { + name: EnvConfig.ins.i18n.fileNameDESC, + value: "alphabeticDesc", + }, + ]; +} diff --git a/src/models/backlink-model.ts b/src/models/backlink-model.ts new file mode 100644 index 0000000..d523825 --- /dev/null +++ b/src/models/backlink-model.ts @@ -0,0 +1,103 @@ + +export interface IBacklinkFilterPanelDataQueryParams { + rootId: string; + focusBlockId?: string; + queryParentDefBlock?: boolean; + querrChildDefBlockForListItem?: boolean; + queryChildDefBlockForHeadline?: boolean; +} + + +export interface IBacklinkBlockQueryParams { + queryParentDefBlock?: boolean; + querrChildDefBlockForListItem; + queryChildDefBlockForHeadline?: boolean; + defBlockIds: string[]; + backlinkBlockIds?: string[]; + backlinkBlocks?: BacklinkBlock[]; + backlinkParentBlockIds?: string[]; + // queryAllContentUnderHeadline?: boolean; + // includeTypes: string[]; + // relatedDefBlockIdArray?: string[]; +} + + +export interface IBacklinkBlockNode { + block: DefBlock; + documentBlock: DefBlock; + concatContent: string; + includeDirectDefBlockIds: Set; + includeRelatedDefBlockIds: Set; + dynamicAnchorMap: Map>; + staticAnchorMap: Map>; +} + + +export interface IBacklinkFilterPanelData { + rootId: string; + backlinkBlockNodeArray: IBacklinkBlockNode[]; + // 当前文档的定义块 + curDocDefBlockArray: DefBlock[]; + // 有关联的定义块 + relatedDefBlockArray: DefBlock[]; + // 反链块所属的文档 + backlinkDocumentArray: DefBlock[]; + + userCache?: boolean; + + // 关联块文档数据结构,不采用文档方式 + // documentNodeArray: DocumentNode[]; +} + +export interface IPanelRenderBacklinkQueryParams { + pageNum: number; + pageSize: number; + backlinkCurDocDefBlockType: string; + backlinkBlockSortMethod: BlockSortMethod; + backlinkKeywordStr: string; + includeRelatedDefBlockIds: Set; + excludeRelatedDefBlockIds: Set; + includeDocumentIds: Set; + excludeDocumentIds: Set; + +} + +export interface IPanelRednerFilterQueryParams extends IPanelRenderBacklinkQueryParams { + filterPanelCurDocDefBlockSortMethod: BlockSortMethod; + filterPanelCurDocDefBlockKeywords: string; + + filterPanelRelatedDefBlockType: string; + filterPanelRelatedDefBlockSortMethod: BlockSortMethod; + filterPanelRelatedDefBlockKeywords: string; + + filterPanelBacklinkDocumentSortMethod: BlockSortMethod; + filterPanelBacklinkDocumentKeywords: string; +} + + + +export interface IBacklinkPanelRenderData { + rootId: string; + + backlinkDataArray: IBacklinkData[]; + + backlinkBlockNodeArray: IBacklinkBlockNode[]; + // 当前文档的定义块 + curDocDefBlockArray: DefBlock[]; + // 有关联的定义块 + relatedDefBlockArray: DefBlock[]; + // 反链块所属的文档块信息 + backlinkDocumentArray: DefBlock[]; + + pageNum: number; + pageSize: number; + totalPage: number; + usedCache: boolean; +} + +export class BacklinkPanelFilterCriteria { + // backlinkPanelBaseDataQueryParams: BacklinkPanelBaseDataQueryParams; + queryParams: IPanelRednerFilterQueryParams; + backlinkPanelFilterViewExpand: boolean; + +} \ No newline at end of file diff --git a/src/models/css-constant.ts b/src/models/css-constant.ts new file mode 100644 index 0000000..93ecc27 --- /dev/null +++ b/src/models/css-constant.ts @@ -0,0 +1,24 @@ + + +export const CUSTOM_CSS_CLASS_MAP: { [key: string]: { name: string, describe: string } } = +{ + refTextClass: { + name: "ref-text__brackets", + describe: "引用文本中括号", + }, + codeBlockMaxHeightClass: { + name: "code-block__max-height", + describe: "代码块最大高度", + }, + codeBlockDragHeightClass: { + name: "code-block__drag-height", + describe: "代码块拖拽高度", + }, + + +} + ; + +export const refStyle = ` + +` \ No newline at end of file diff --git a/src/models/icon-constant.ts b/src/models/icon-constant.ts new file mode 100644 index 0000000..91f3112 --- /dev/null +++ b/src/models/icon-constant.ts @@ -0,0 +1,42 @@ + +export const CUSTOM_ICON_MAP = +{ + BacklinkPanelFilter: { + id: "BacklinkPanelFilter", + source: ` + + + + + ` + }, + LiElementExpand: { + id: "LiElementExpand", + source: ` + + + ` + }, + + LiElementCollapse: { + id: "LiElementCollapse", + source: ` + + ` + }, + ResetInitialization: { + id: "ResetInitialization", + source: ` + + ` + }, + iconContentSort: { + id: "iconContentSort", + source: ` + + + ` + }, +}; \ No newline at end of file diff --git a/src/models/setting-constant.ts b/src/models/setting-constant.ts new file mode 100644 index 0000000..1d27691 --- /dev/null +++ b/src/models/setting-constant.ts @@ -0,0 +1,103 @@ +import { BACKLINK_BLOCK_SORT_METHOD_ELEMENT, CUR_DOC_DEF_BLOCK_SORT_METHOD_ELEMENT, RELATED_DEF_BLOCK_SORT_METHOD_ELEMENT, RELATED_DOCMUMENT_SORT_METHOD_ELEMENT } from "./backlink-constant"; +import { ItemProperty, IOption, TabProperty, SettingConfig } from "./setting-model"; + +export function getDefaultSettingConfig() { + let defaultConfig = new SettingConfig(); + defaultConfig.useRefTextStyle = false; + + defaultConfig.codeBlockNaming = false; + defaultConfig.codeBlockMaxHeight = null; + defaultConfig.codeBlockToggle = false; + defaultConfig.codeBlockAdjustHeight = false; + defaultConfig.codeBlockTopLanguages = ""; + + defaultConfig.fileTreeKeywordFilter = false; + + + return defaultConfig; +} + + +export function getSettingTabArray(): TabProperty[] { + + let tabProperties: TabProperty[] = [ + + ]; + + tabProperties.push( + new TabProperty({ + key: "style-setting", name: "样式", iconKey: "iconFilter", props: [ + new ItemProperty({ key: "useRefTextStyle", type: "switch", name: "修改引用文本样式", description: "", tips: "" }), + ] + + }), + new TabProperty({ + key: "code-block-setting", name: "代码块", iconKey: "iconLink", props: [ + + new ItemProperty({ key: "codeBlockNaming", type: "switch", name: "代码块命名", description: "", tips: "" }), + new ItemProperty({ key: "codeBlockMaxHeight", type: "number", name: "代码块最大高度", description: "", tips: "", min: 50 }), + // new ItemProperty({ key: "codeBlockToggle", type: "switch", name: "代码块显示折叠展开按钮", description: "", tips: "" }), + new ItemProperty({ key: "codeBlockAdjustHeight", type: "switch", name: "代码块调整高度", description: "如果设置了最大高度,拖拽范围也不会超过最大高度", tips: "" }), + new ItemProperty({ key: "codeBlockTopLanguages", type: "text", name: "代码块常用语言", description: "选择语言时,会把这些语言置顶显示,英文逗号或中文逗号隔开多个语言", tips: "" }), + + // new ItemProperty({ key: "queryAllContentUnderHeadline", type: "switch", name: "反链区域关键字查询标题下的所有内容", description: "必须开启 查询标题下关联定义块 才可生效。", tips: "" }), + + ] + + }), + new TabProperty({ + key: "file-tree-setting", name: "文档树", iconKey: "iconPlugin", props: [ + new ItemProperty({ key: "fileTreeKeywordFilter", type: "switch", name: "关键字过滤笔记本", description: "", tips: "" }), + // new ItemProperty({ key: "documentBottomDisplay", type: "switch", name: "文档底部显示反链面板", description: "", tips: "" }), + // new ItemProperty({ key: "topBarDisplay", type: "switch", name: "桌面端顶栏创建反链页签 Icon", description: "", tips: "" }), + // new ItemProperty({ key: "cacheAfterResponseMs", type: "number", name: "启用缓存门槛(毫秒)", description: "当接口响应时间超过这个数,就会把这次查询结果存入缓存,-1 不开启缓存", tips: "", min: -1 }), + // new ItemProperty({ key: "cacheExpirationTime", type: "number", name: "缓存过期时间(秒)", description: "", tips: "缓存数据失效时间", min: -1, }), + // new ConfigProperty({ key: "usePraentIdIdx", type: "switch", name: "使用索引", description: "", tips: "" }), + + ] + }), + ); + + return tabProperties; +} + +function getBacklinkBlockSortMethodOptions(): IOption[] { + let backlinkBlockSortMethodElements = BACKLINK_BLOCK_SORT_METHOD_ELEMENT(); + let options: IOption[] = []; + for (const element of backlinkBlockSortMethodElements) { + options.push(element); + } + + return options; +} + + +function geturDocDefBlockSortMethodElement(): IOption[] { + let backlinkBlockSortMethodElements = CUR_DOC_DEF_BLOCK_SORT_METHOD_ELEMENT(); + let options: IOption[] = []; + for (const element of backlinkBlockSortMethodElements) { + options.push(element); + } + + return options; +} + +function getRelatedDefBlockSortMethodElement(): IOption[] { + let backlinkBlockSortMethodElements = RELATED_DEF_BLOCK_SORT_METHOD_ELEMENT(); + let options: IOption[] = []; + for (const element of backlinkBlockSortMethodElements) { + options.push(element); + } + + return options; +} + +function getRelatedDocmumentSortMethodElement(): IOption[] { + let backlinkBlockSortMethodElements = RELATED_DOCMUMENT_SORT_METHOD_ELEMENT(); + let options: IOption[] = []; + for (const element of backlinkBlockSortMethodElements) { + options.push(element); + } + + return options; +} \ No newline at end of file diff --git a/src/models/setting-model.ts b/src/models/setting-model.ts new file mode 100644 index 0000000..afd7b37 --- /dev/null +++ b/src/models/setting-model.ts @@ -0,0 +1,84 @@ +import { isValidStr } from "@/utils/string-util"; + + +export class SettingConfig { + // 引用文本样式 + useRefTextStyle: boolean; + + // 代码块 + codeBlockTopLanguages: string; + codeBlockNaming: boolean; + codeBlockToggle: boolean; + codeBlockMaxHeight: number; + codeBlockAdjustHeight: boolean; + + // 文档树 + fileTreeKeywordFilter: boolean; + + +} + + +interface ITabProperty { + key: string; + name: string; + props: Array; + iconKey?: string; +} + + +export class TabProperty { + key: string; + name: string; + iconKey: string; + props: ItemProperty[]; + + constructor({ key, name, iconKey, props }: ITabProperty) { + this.key = key; + this.name = name; + if (isValidStr(iconKey)) { + this.iconKey = iconKey; + } else { + this.iconKey = "setting"; + } + this.props = props; + + } + +} + +export interface IOption { + name: string; + desc?: string; + value: string; +} + + + + +export class ItemProperty { + key: string; + type: IItemPropertyType; + name: string; + description: string; + tips?: string; + + min?: number; + max?: number; + btndo?: () => void; + options?: IOption[]; + + + constructor({ key, type, name, description, tips, min, max, btndo, options }: ItemProperty) { + this.key = key; + this.type = type; + this.min = min; + this.max = max; + this.btndo = btndo; + this.options = options ?? []; + this.name = name; + this.description = description; + this.tips = tips; + } + +} diff --git a/src/service/CssService.ts b/src/service/CssService.ts new file mode 100644 index 0000000..ce03343 --- /dev/null +++ b/src/service/CssService.ts @@ -0,0 +1,59 @@ + + +import Instance from "@/utils/Instance"; +import { CUSTOM_CSS_CLASS_MAP } from "@/models/css-constant"; +import { SettingService } from "./SettingService"; + +export class CssService { + + public static get ins(): CssService { + return Instance.get(CssService); + } + + public init() { + let bodyElement = getBodyElement(); + if (!bodyElement) { + return; + } + bodyElement.classList.add("misuzu2027"); + + this.initClass(); + } + + + public initClass() { + let bodyElement = getBodyElement(); + if (!bodyElement) { + return; + } + for (const key of Object.keys(CUSTOM_CSS_CLASS_MAP)) { + bodyElement.classList.remove(CUSTOM_CSS_CLASS_MAP[key].name); + } + + let settingConfig = SettingService.ins.SettingConfig; + if (settingConfig.useRefTextStyle) { + bodyElement.classList.add(CUSTOM_CSS_CLASS_MAP.refTextClass.name); + } + if (settingConfig.codeBlockMaxHeight) { + bodyElement.classList.add(CUSTOM_CSS_CLASS_MAP.codeBlockMaxHeightClass.name); + } + if (settingConfig.codeBlockAdjustHeight) { + bodyElement.classList.add(CUSTOM_CSS_CLASS_MAP.codeBlockDragHeightClass.name); + } + + // bodyElement.classList.add("code-block__max-height"); + } + + + +} + + + +function getBodyElement() { + let tagNameArray = document.getElementsByTagName("body"); + if (!tagNameArray || !tagNameArray.item(0)) { + return; + } + return tagNameArray.item(0); +} \ No newline at end of file diff --git a/src/service/SettingService.ts b/src/service/SettingService.ts new file mode 100644 index 0000000..8db4ac9 --- /dev/null +++ b/src/service/SettingService.ts @@ -0,0 +1,118 @@ +import { EnvConfig } from "@/config/EnvConfig"; +import { getDefaultSettingConfig } from "@/models/setting-constant"; +import { SettingConfig } from "@/models/setting-model"; +import Instance from "@/utils/Instance"; +import { setReplacer } from "@/utils/json-util"; +import { mergeObjects } from "@/utils/object-util"; +import { CssService } from "./CssService"; +import { CodeBlockService } from "@/components/code-block/CodeBlockService"; +import { FileTreeService } from "@/components/filetree/FileTreeService"; + +const SettingFileName = 'misuzu2027-setting.json'; + +export class SettingService { + + public static get ins(): SettingService { + return Instance.get(SettingService); + } + + private _settingConfig: SettingConfig; + + public get SettingConfig() { + if (this._settingConfig) { + return this._settingConfig; + } + this.init() + return getDefaultSettingConfig() + + } + + public async init() { + let persistentConfig = await getPersistentConfig(); + this._settingConfig = mergeObjects(persistentConfig, getDefaultSettingConfig()); + // console.log("init this._settingConfig ", this._settingConfig) + } + + public async updateSettingCofnigValue(key: string, newValue: any) { + let oldValue = this._settingConfig[key]; + if (oldValue == newValue) { + return; + } + + this._settingConfig[key] = newValue; + let paramJson = JSON.stringify(this._settingConfig, setReplacer); + let plugin = EnvConfig.ins.plugin; + if (!plugin) { + return; + } + console.log(`Misuzu2027 更新设置配置文件: ${paramJson}`); + this.initFunction(); + plugin.saveData(SettingFileName, paramJson); + } + + public async updateSettingCofnig(settingConfigParam: SettingConfig) { + let plugin = EnvConfig.ins.plugin; + if (!plugin) { + return; + } + + let curSettingConfigJson = ""; + if (this._settingConfig) { + curSettingConfigJson = JSON.stringify(this._settingConfig, setReplacer); + } + let paramJson = JSON.stringify(settingConfigParam, setReplacer); + if (paramJson == curSettingConfigJson) { + return; + } + console.log(`Misuzu2027 更新设置配置文件: ${paramJson}`); + this._settingConfig = { ...settingConfigParam }; + this.initFunction(); + plugin.saveData(SettingFileName, paramJson); + + } + + public initFunction(){ + CssService.ins.init(); + CodeBlockService.ins.init(); + FileTreeService.ins.init(); + } + +} + + + +async function getPersistentConfig(): Promise { + let plugin = EnvConfig.ins.plugin; + let settingConfig = null; + if (!plugin) { + return settingConfig; + } + let loaded = await plugin.loadData(SettingFileName); + if (loaded == null || loaded == undefined || loaded == '') { + console.info(`Misuzu2027 没有配置文件,使用默认配置`) + } else { + //如果有配置文件,则使用配置文件 + // console.info(`读入配置文件: ${SettingFileName}`) + if (typeof loaded === 'string') { + loaded = JSON.parse(loaded); + } + try { + settingConfig = new SettingConfig(); + for (let key in loaded) { + setKeyValue(settingConfig, key, loaded[key]); + } + } catch (error_msg) { + console.log(`Setting load error: ${error_msg}`); + } + } + return settingConfig; +} + +function setKeyValue(settingConfig, key: any, value: any) { + if (!(key in settingConfig)) { + console.error(`"${key}" is not a setting`); + return; + } + settingConfig[key] = value; +} + diff --git a/src/types/api.d.ts b/src/types/api.d.ts new file mode 100644 index 0000000..cbb8466 --- /dev/null +++ b/src/types/api.d.ts @@ -0,0 +1,70 @@ +interface IResGetNotebookConf { + box: string; + conf: NotebookConf; + name: string; +} + +interface IReslsNotebooks { + notebooks: Notebook[]; +} + +interface IResUpload { + errFiles: string[]; + succMap: { [key: string]: string }; +} + +interface IResdoOperations { + doOperations: doOperation[]; + undoOperations: doOperation[] | null; +} + +interface IResGetBlockKramdown { + id: BlockId; + kramdown: string; +} + +interface IResGetChildBlock { + id: BlockId; + type: BlockType; + subtype?: BlockSubType; +} + +interface IResGetTemplates { + content: string; + path: string; +} + +interface IResReadDir { + isDir: boolean; + isSymlink: boolean; + name: string; +} + +interface IResExportMdContent { + hPath: string; + content: string; +} + +interface IResBootProgress { + progress: number; + details: string; +} + +interface IResForwardProxy { + body: string; + contentType: string; + elapsed: number; + headers: { [key: string]: string }; + status: number; + url: string; +} + +interface IResExportResources { + path: string; +} + + +interface IResCheckBlockFold{ + isFolded: boolean; + isRoot: boolean; +} diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts new file mode 100644 index 0000000..6137d79 --- /dev/null +++ b/src/types/custom.d.ts @@ -0,0 +1,61 @@ + +type BacklinkParentBlock = DefBlock & { + childIdPath: string; + subMarkdown: string; +}; + + +type BacklinkChildBlock = DefBlock & { + parentIdPath: string; +}; + + + +type BacklinkBlock = DefBlock & { + parentBlockType: string; + parentListItemMarkdown: string; +}; + + +type DefBlock = Block & { + refCount: number; + dynamicAnchor: string; + staticAnchor: string; + selectionStatus: string; + filterStatus: boolean; +}; + +type BlockSortMethod = + | "type" + | "content" + | "typeAndContent" + | "modifiedAsc" + | "modifiedDesc" + | "createdAsc" + | "createdDesc" + | "rankAsc" + | "rankDesc" + | "refCountAsc" + | "refCountDesc" + | "alphabeticAsc" + | "alphabeticDesc" + | "documentAlphabeticAsc" + | "documentAlphabeticDesc" + ; + + +interface IBacklinkCacheData { + backlinks: IBacklinkData[]; + usedCache: boolean; +} + + +type IItemPropertyType = + "select" | + "text" | + "number" | + "button" | + "textarea" | + "switch" | + "order" | + "tips"; \ No newline at end of file diff --git a/src/types/index.d.ts b/src/types/index.d.ts new file mode 100644 index 0000000..de58ab1 --- /dev/null +++ b/src/types/index.d.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-08-15 10:28:10 + * @FilePath : /src/types/index.d.ts + * @LastEditTime : 2024-06-08 20:50:53 + * @Description : Frequently used data structures in SiYuan + */ + + +type DocumentId = string; +type BlockId = string; +type NotebookId = string; +type PreviousID = BlockId; +type ParentID = BlockId | DocumentId; + +type Notebook = { + id: NotebookId; + name: string; + icon: string; + sort: number; + closed: boolean; +} + +type NotebookConf = { + name: string; + closed: boolean; + refCreateSavePath: string; + createDocNameTemplate: string; + dailyNoteSavePath: string; + dailyNoteTemplatePath: string; +} + +// type BlockType = "d" | "s" | "h" | "t" | "i" | "p" | "f" | "audio" | "video" | "other"; + +type BlockType = "d" // 文档 + | "h" // 标题 + | "l" // 列表 + | "i" // 列表项 + | "c" // 代码块 + | "m" // 数学公式 + | "t" // 表格 + | "b" // 引述 + | "av" // 属性视图(数据库) + | "s" // 超级块 + | "p" // 段落 + | "tb" // -- 分隔线 + | "html" // HTML + | "video" // 视频 + | "audio" // 音频 + | "widget" // 挂件 + | "iframe" // iframe + | "query_embed" // 嵌入块 + ; + +type BlockSubType = "o" | "u" | "t" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + +type Block = { + id: BlockId; + parent_id?: BlockId; + root_id: DocumentId; + hash: string; + box: string; + path: string; + hpath: string; + name: string; + alias: string; + memo: string; + tag: string; + content: string; + fcontent?: string; + markdown: string; + length: number; + type: BlockType; + subtype: BlockSubType; + /** string of { [key: string]: string } + * For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}" + */ + ial?: string; + sort: number; + created: string; + updated: string; +} + +type doOperation = { + action: string; + data: string; + id: BlockId; + parentID: BlockId | DocumentId; + previousID: BlockId; + retData: null; +} + +interface Window { + siyuan: { + config: any; + notebooks: any; + menus: any; + dialogs: any; + blockPanels: any; + storage: any; + user: any; + ws: any; + languages: any; + emojis: any; + }; +} + + +interface IMenu { + iconClass?: string, + label?: string, + click?: (element: HTMLElement, event: MouseEvent) => boolean | void | Promise + type?: "separator" | "submenu" | "readonly" | "empty", + accelerator?: string, + action?: string, + id?: string, + submenu?: IMenu[] + disabled?: boolean + icon?: string + iconHTML?: string + current?: boolean + bind?: (element: HTMLElement) => void + index?: number + element?: HTMLElement +} + + +type DockPosition = "Hidden" | "LeftTop" | "LeftBottom" | "RightTop" | "RightBottom" | "BottomLeft" | "BottomRight"; + + + + +/** + * 反链相关 + */ + + +interface IBreadcrumb { + id: string; + name: string; + type: string; + subType: string; + children: []; +} + +interface IBacklinkData { + blockPaths: IBreadcrumb[]; + dom: string; + expand: boolean; + backlinkBlock: Block; +} + + + +interface IProtyleOption { + backlinkData?: IBacklinkData[]; + action?: TProtyleAction[]; + mode?: TEditorMode; + toolbar?: Array; + blockId?: string; + key?: string; + scrollAttr?: { + rootId: string, + startId: string, + endId: string, + scrollTop: number, + focusId?: string, + focusStart?: number, + focusEnd?: number, + zoomInId?: string, + }; + defId?: string; + render?: { + background?: boolean, + title?: boolean, + gutter?: boolean, + scroll?: boolean, + breadcrumb?: boolean, + breadcrumbDocName?: boolean, + }; + typewriterMode?: boolean; + + after?(protyle: Protyle): void; +} \ No newline at end of file diff --git a/src/utils/Instance.ts b/src/utils/Instance.ts new file mode 100644 index 0000000..65d9ffc --- /dev/null +++ b/src/utils/Instance.ts @@ -0,0 +1,11 @@ +export type IClazz = new (...param: any[]) => T; + +export default class Instance { + + public static get(clazz: IClazz, ...param: any[]): T { + if (clazz["__Instance__"] == null) { + clazz["__Instance__"] = new clazz(...param); + } + return clazz["__Instance__"]; + } +} \ No newline at end of file diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..c4b1420 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,534 @@ +/** + * Copyright (c) 2023 frostime. All rights reserved. + * https://github.com/frostime/sy-plugin-template-vite + * + * See API Document in [API.md](https://github.com/siyuan-note/siyuan/blob/master/API.md) + * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md) + */ + +import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan"; +import { isBoolean } from "./object-util"; + + + +async function request(url: string, data: any) { + let response: IWebSocketData = await fetchSyncPost(url, data); + let res = response.code === 0 ? response.data : null; + if (response.code != 0) { + console.log(`反链面板插件接口异常 url : ${url} , msg : ${response.msg}`) + } + return res; +} + + +// **************************************** Noteboook **************************************** + + +export async function lsNotebooks(): Promise { + let url = '/api/notebook/lsNotebooks'; + return request(url, ''); +} + + +export async function openNotebook(notebook: NotebookId) { + let url = '/api/notebook/openNotebook'; + return request(url, { notebook: notebook }); +} + + +export async function closeNotebook(notebook: NotebookId) { + let url = '/api/notebook/closeNotebook'; + return request(url, { notebook: notebook }); +} + + +export async function renameNotebook(notebook: NotebookId, name: string) { + let url = '/api/notebook/renameNotebook'; + return request(url, { notebook: notebook, name: name }); +} + + +export async function createNotebook(name: string): Promise { + let url = '/api/notebook/createNotebook'; + return request(url, { name: name }); +} + + +export async function removeNotebook(notebook: NotebookId) { + let url = '/api/notebook/removeNotebook'; + return request(url, { notebook: notebook }); +} + + +export async function getNotebookConf(notebook: NotebookId): Promise { + let data = { notebook: notebook }; + let url = '/api/notebook/getNotebookConf'; + return request(url, data); +} + + +export async function setNotebookConf(notebook: NotebookId, conf: NotebookConf): Promise { + let data = { notebook: notebook, conf: conf }; + let url = '/api/notebook/setNotebookConf'; + return request(url, data); +} + + +// **************************************** File Tree **************************************** +export async function createDocWithMd(notebook: NotebookId, path: string, markdown: string): Promise { + let data = { + notebook: notebook, + path: path, + markdown: markdown, + }; + let url = '/api/filetree/createDocWithMd'; + return request(url, data); +} + + +export async function renameDoc(notebook: NotebookId, path: string, title: string): Promise { + let data = { + doc: notebook, + path: path, + title: title + }; + let url = '/api/filetree/renameDoc'; + return request(url, data); +} + + +export async function removeDoc(notebook: NotebookId, path: string) { + let data = { + notebook: notebook, + path: path, + }; + let url = '/api/filetree/removeDoc'; + return request(url, data); +} + + +export async function moveDocs(fromPaths: string[], toNotebook: NotebookId, toPath: string) { + let data = { + fromPaths: fromPaths, + toNotebook: toNotebook, + toPath: toPath + }; + let url = '/api/filetree/moveDocs'; + return request(url, data); +} + + +export async function getHPathByPath(notebook: NotebookId, path: string): Promise { + let data = { + notebook: notebook, + path: path + }; + let url = '/api/filetree/getHPathByPath'; + return request(url, data); +} + + +export async function getHPathByID(id: BlockId): Promise { + let data = { + id: id + }; + let url = '/api/filetree/getHPathByID'; + return request(url, data); +} + + +export async function getIDsByHPath(notebook: NotebookId, path: string): Promise { + let data = { + notebook: notebook, + path: path + }; + let url = '/api/filetree/getIDsByHPath'; + return request(url, data); +} + +// **************************************** Asset Files **************************************** + +export async function upload(assetsDirPath: string, files: any[]): Promise { + let form = new FormData(); + form.append('assetsDirPath', assetsDirPath); + for (let file of files) { + form.append('file[]', file); + } + let url = '/api/asset/upload'; + return request(url, form); +} + +// **************************************** Block **************************************** +type DataType = "markdown" | "dom"; +export async function insertBlock( + dataType: DataType, data: string, + nextID?: BlockId, previousID?: BlockId, parentID?: BlockId +): Promise { + let payload = { + dataType: dataType, + data: data, + nextID: nextID, + previousID: previousID, + parentID: parentID + } + let url = '/api/block/insertBlock'; + return request(url, payload); +} + + +export async function prependBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise { + let payload = { + dataType: dataType, + data: data, + parentID: parentID + } + let url = '/api/block/prependBlock'; + return request(url, payload); +} + + +export async function appendBlock(dataType: DataType, data: string, parentID: BlockId | DocumentId): Promise { + let payload = { + dataType: dataType, + data: data, + parentID: parentID + } + let url = '/api/block/appendBlock'; + return request(url, payload); +} + + +export async function updateBlock(dataType: DataType, data: string, id: BlockId): Promise { + let payload = { + dataType: dataType, + data: data, + id: id + } + let url = '/api/block/updateBlock'; + return request(url, payload); +} + + +export async function deleteBlock(id: BlockId): Promise { + let data = { + id: id + } + let url = '/api/block/deleteBlock'; + return request(url, data); +} + + +export async function moveBlock(id: BlockId, previousID?: PreviousID, parentID?: ParentID): Promise { + let data = { + id: id, + previousID: previousID, + parentID: parentID + } + let url = '/api/block/moveBlock'; + return request(url, data); +} + + +export async function getBlockKramdown(id: BlockId): Promise { + let data = { + id: id + } + let url = '/api/block/getBlockKramdown'; + return request(url, data); +} + + +export async function getChildBlocks(id: BlockId): Promise { + let data = { + id: id + } + let url = '/api/block/getChildBlocks'; + return request(url, data); +} + +export async function transferBlockRef(fromID: BlockId, toID: BlockId, refIDs: BlockId[]) { + let data = { + fromID: fromID, + toID: toID, + refIDs: refIDs + } + let url = '/api/block/transferBlockRef'; + return request(url, data); +} + +export async function getBlockIndex(id: BlockId): Promise { + let data = { + id: id + } + let url = '/api/block/getBlockIndex'; + + return request(url, data); +} + +export async function getBlocksIndexes(ids: BlockId[]): Promise { + let data = { + ids: ids + } + let url = '/api/block/getBlocksIndexes'; + + return request(url, data); +} + +export async function getBlockIsFolded(id: string): Promise { + + let response = await checkBlockFold(id); + let result: boolean; + if (isBoolean(response)) { + result = response as boolean; + } else { + result = response.isFolded; + } + // console.log(`getBlockIsFolded response : ${JSON.stringify(response)}, result : ${result} `) + return result; +}; + +export async function checkBlockFold(id: string): Promise { + if (!id) { + // 参数校验失败,返回拒绝 + return Promise.reject(new Error('参数错误')); + } + let data = { + id: id + } + let url = '/api/block/checkBlockFold'; + + return request(url, data); +}; + + +export async function getBatchBlockIdIndex(ids: string[]): Promise> { + let idMap: Map = new Map(); + let getSuccess = true; + try { + let idObject = await getBlocksIndexes(ids); + // 遍历对象的键值对,并将它们添加到 Map 中 + for (const key in idObject) { + if (Object.prototype.hasOwnProperty.call(idObject, key)) { + const value = idObject[key]; + idMap.set(key, value); + } + } + } catch (err) { + getSuccess = false; + console.error("批量获取块索引报错,可能是旧版本不支持批量接口 : ", err) + } + + if (!getSuccess) { + for (const id of ids) { + let index = 0 + try { + index = await getBlockIndex(id); + } catch (err) { + console.error("获取块索引报错 : ", err) + } + idMap.set(id, index) + } + } + + return idMap; +} + +// **************************************** Attributes **************************************** +export async function setBlockAttrs(id: BlockId, attrs: { [key: string]: string }) { + let data = { + id: id, + attrs: attrs + } + let url = '/api/attr/setBlockAttrs'; + return request(url, data); +} + + +export async function getBlockAttrs(id: BlockId): Promise<{ [key: string]: string }> { + let data = { + id: id + } + let url = '/api/attr/getBlockAttrs'; + return request(url, data); +} + +// **************************************** SQL **************************************** + +export async function sql(sql: string): Promise { + let sqldata = { + stmt: sql, + }; + let url = '/api/query/sql'; + return request(url, sqldata); +} + +export async function getBlockByID(blockId: string): Promise { + let sqlScript = `select * from blocks where id ='${blockId}'`; + let data = await sql(sqlScript); + return data[0]; +} + +// **************************************** Template **************************************** + +export async function render(id: DocumentId, path: string): Promise { + let data = { + id: id, + path: path + } + let url = '/api/template/render'; + return request(url, data); +} + + +export async function renderSprig(template: string): Promise { + let url = '/api/template/renderSprig'; + return request(url, { template: template }); +} + +// **************************************** File **************************************** + +export async function getFile(path: string): Promise { + let data = { + path: path + } + let url = '/api/file/getFile'; + try { + let file = await fetchSyncPost(url, data); + return file; + } catch (error_msg) { + return null; + } +} + +export async function putFile(path: string, isDir: boolean, file: any) { + let form = new FormData(); + form.append('path', path); + form.append('isDir', isDir.toString()); + // Copyright (c) 2023, terwer. + // https://github.com/terwer/siyuan-plugin-importer/blob/v1.4.1/src/api/kernel-api.ts + form.append('modTime', Math.floor(Date.now() / 1000).toString()); + form.append('file', file); + let url = '/api/file/putFile'; + return request(url, form); +} + +export async function removeFile(path: string) { + let data = { + path: path + } + let url = '/api/file/removeFile'; + return request(url, data); +} + + + +export async function readDir(path: string): Promise { + let data = { + path: path + } + let url = '/api/file/readDir'; + return request(url, data); +} + + +// **************************************** Export **************************************** + +export async function exportMdContent(id: DocumentId): Promise { + let data = { + id: id + } + let url = '/api/export/exportMdContent'; + return request(url, data); +} + +export async function exportResources(paths: string[], name: string): Promise { + let data = { + paths: paths, + name: name + } + let url = '/api/export/exportResources'; + return request(url, data); +} + +// **************************************** Convert **************************************** + +export type PandocArgs = string; +export async function pandoc(args: PandocArgs[]) { + let data = { + args: args + } + let url = '/api/convert/pandoc'; + return request(url, data); +} + +// **************************************** Notification **************************************** + +// /api/notification/pushMsg +// { +// "msg": "test", +// "timeout": 7000 +// } +export async function pushMsg(msg: string, timeout: number = 7000) { + let payload = { + msg: msg, + timeout: timeout + }; + let url = "/api/notification/pushMsg"; + return request(url, payload); +} + +export async function pushErrMsg(msg: string, timeout: number = 7000) { + let payload = { + msg: msg, + timeout: timeout + }; + let url = "/api/notification/pushErrMsg"; + return request(url, payload); +} + +// **************************************** Network **************************************** +export async function forwardProxy( + url: string, method: string = 'GET', payload: any = {}, + headers: any[] = [], timeout: number = 7000, contentType: string = "text/html" +): Promise { + let data = { + url: url, + method: method, + timeout: timeout, + contentType: contentType, + headers: headers, + payload: payload + } + let url1 = '/api/network/forwardProxy'; + return request(url1, data); +} + + +// **************************************** System **************************************** + +export async function bootProgress(): Promise { + return request('/api/system/bootProgress', {}); +} + + +export async function version(): Promise { + return request('/api/system/version', {}); +} + + +export async function currentTime(): Promise { + return request('/api/system/currentTime', {}); +} + + + +export async function getBacklinkDoc(defID: string, refTreeID: string, keyword: string): Promise<{ backlinks: IBacklinkData[] }> { + let data = { + defID: defID, + refTreeID: refTreeID, + keyword: keyword, + } + let url = '/api/ref/getBacklinkDoc'; + + return request(url, data); +} \ No newline at end of file diff --git a/src/utils/array-util.ts b/src/utils/array-util.ts new file mode 100644 index 0000000..b848c01 --- /dev/null +++ b/src/utils/array-util.ts @@ -0,0 +1,38 @@ +export function paginate(array: T[], pageNumber: number, pageSize: number): T[] { + // 计算起始索引 + const startIndex = (pageNumber - 1) * pageSize; + // 计算结束索引 + const endIndex = startIndex + pageSize; + // 返回对应的数组片段 + return array.slice(startIndex, endIndex); +} + +export function getLastItem(list: T[]): T | undefined { + return list.length > 0 ? list[list.length - 1] : undefined; +} + +export function isArrayEmpty(array: T[]): boolean { + return !array || array.length == 0; +} + + +export function isArrayNotEmpty(array: T[]): boolean { + return Array.isArray(array) && array.length > 0; +} + + +export function isSetNotEmpty(set: Set): boolean { + return set && set.size > 0; +} + +export function intersectionArray(array1: T[], array2: T[]): T[] { + if (isArrayEmpty(array1) || isArrayEmpty(array2)) { + return []; + } + // 使用 Set 来提高查找的效率 + // const set1 = new Set(array1); + const set2 = new Set(array2); + + // 过滤 array1 中的元素,只保留那些也在 set2 中的元素 + return array1.filter(item => set2.has(item)); +} diff --git a/src/utils/cache-util.ts b/src/utils/cache-util.ts new file mode 100644 index 0000000..8ca8145 --- /dev/null +++ b/src/utils/cache-util.ts @@ -0,0 +1,69 @@ + + +export class CacheUtil { + + + private cache: Map = new Map(); + + /** + * 设置缓存 + * @param key 缓存键 + * @param value 缓存值 + * @param ttl 缓存有效时间(毫秒) + */ + set(key: string, value: any, ttl: number): void { + const expiry = Date.now() + ttl; + this.cache.set(key, { value, expiry }); + } + + /** + * 获取缓存 + * @param key 缓存键 + * @returns 缓存值或 null + */ + get(key: string): any | null { + const cachedItem = this.cache.get(key); + if (cachedItem) { + if (cachedItem.expiry > Date.now()) { + return cachedItem.value; + } else { + this.cache.delete(key); + } + } + return null; + } + /** + * 主动丢弃缓存 + * @param key 缓存键 + */ + delete(key: string): void { + this.cache.delete(key); + } + + /** + * 清除所有过期的缓存项 + */ + cleanUp(): void { + const now = Date.now(); + for (const [key, { expiry }] of this.cache) { + if (expiry <= now) { + this.cache.delete(key); + } + } + } + + clearByPrefix(prefix: string): void { + for (const key of this.cache.keys()) { + if (key.startsWith(prefix)) { + this.cache.delete(key); + } + } + } +} + + +export function generateKey(...parts: string[]): string { + // 使用指定的分隔符连接所有字符串 + const separator = ':'; + return parts.join(separator); +} diff --git a/src/utils/datetime-util.ts b/src/utils/datetime-util.ts new file mode 100644 index 0000000..aec1634 --- /dev/null +++ b/src/utils/datetime-util.ts @@ -0,0 +1,64 @@ + +export function parseDateTimeInBlock(dateTimeString: string): Date | null { + if (dateTimeString.length !== 14) { + console.error("Invalid date time string format. It should be 'yyyyMMddhhmmss'."); + return null; + } + + const year = parseInt(dateTimeString.slice(0, 4), 10); + const month = parseInt(dateTimeString.slice(4, 6), 10) - 1; // 月份从 0 开始 + const day = parseInt(dateTimeString.slice(6, 8), 10); + const hour = parseInt(dateTimeString.slice(8, 10), 10); + const minute = parseInt(dateTimeString.slice(10, 12), 10); + const second = parseInt(dateTimeString.slice(12, 14), 10); + + return new Date(year, month, day, hour, minute, second); +} + + +export function convertDateTimeInBlock(dateTimeString: string): string { + if (dateTimeString.length !== 14) { + console.error("Invalid date time string format. It should be 'yyyyMMddhhmmss'."); + return null; + } + const year = dateTimeString.slice(0, 4); + const month = dateTimeString.slice(4, 6); + const day = dateTimeString.slice(6, 8); + const hour = dateTimeString.slice(8, 10); + const minute = dateTimeString.slice(10, 12); + const second = dateTimeString.slice(12, 14); + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; +} + + + +export function formatRelativeTimeInBlock(dateTimeString: string): string { + let timestamp = parseDateTimeInBlock(dateTimeString).getTime(); + return formatRelativeTime(timestamp); +} + + +export function formatRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + const minute = 60 * 1000; + const hour = 60 * minute; + const day = 24 * hour; + const month = 30 * day; + const year = 365 * day; + + if (diff < minute) { + return `${Math.floor(diff / 1000)}秒前`; + } else if (diff < hour) { + return `${Math.floor(diff / minute)}分钟前`; + } else if (diff < day) { + return `${Math.floor(diff / hour)}小时前`; + } else if (diff < month) { + return `${Math.floor(diff / day)}天前`; + } else if (diff < year) { + return `${Math.floor(diff / month)}个月前`; + } else { + return `${Math.floor(diff / year)}年前`; + } +} diff --git a/src/utils/html-util.ts b/src/utils/html-util.ts new file mode 100644 index 0000000..42163de --- /dev/null +++ b/src/utils/html-util.ts @@ -0,0 +1,269 @@ +import { isArrayEmpty } from "./array-util"; + +export const escapeAttr = (html: string) => { + return html.replace(/"/g, """).replace(/'/g, "'"); +}; + + + +export async function highlightElementTextByCss( + contentElement: HTMLElement, + keywordArray: string[], + +) { + if (!contentElement || isArrayEmpty(keywordArray)) { + return; + } + // If the CSS Custom Highlight API is not supported, + // display a message and bail-out. + if (!CSS.highlights) { + console.log("CSS Custom Highlight API not supported."); + return; + } + + // Find all text nodes in the article. We'll search within + // these text nodes. + const treeWalker = document.createTreeWalker( + contentElement, + NodeFilter.SHOW_TEXT, + ); + const allTextNodes: Node[] = []; + let currentNode = treeWalker.nextNode(); + while (currentNode) { + allTextNodes.push(currentNode); + currentNode = treeWalker.nextNode(); + } + + + // 默认不清除 + // clearCssHighlights(); + + // Clean-up the search query and bail-out if + // if it's empty. + + let allMatchRanges: Range[] = []; + + // Iterate over all text nodes and find matches. + allTextNodes + .map((el: Node) => { + return { el, text: el.textContent.toLowerCase() }; + }) + .map(({ el, text }) => { + const indices: { index: number; length: number }[] = []; + for (const queryStr of keywordArray) { + if (!queryStr) { + continue; + } + let startPos = 0; + while (startPos < text.length) { + const index = text.indexOf( + queryStr.toLowerCase(), + startPos, + ); + if (index === -1) break; + let length = queryStr.length; + indices.push({ index, length }); + startPos = index + length; + } + } + + indices + .sort((a, b) => a.index - b.index) + .map(({ index, length }) => { + const range = new Range(); + range.setStart(el, index); + range.setEnd(el, index + length); + allMatchRanges.push(range); + + }); + }); + + // Create a Highlight object for the ranges. + allMatchRanges = allMatchRanges.flat(); + if (!allMatchRanges || allMatchRanges.length <= 0) { + return; + } + + let searchResultsHighlight = CSS.highlights.get("search-result-mark"); + if (searchResultsHighlight) { + for (const range of allMatchRanges) { + searchResultsHighlight.add(range); + } + } else { + searchResultsHighlight = new Highlight(...allMatchRanges); + } + + // Register the Highlight object in the registry. + CSS.highlights.set("search-result-mark", searchResultsHighlight); + +} + +export function clearCssHighlights() { + CSS.highlights.delete("search-result-mark"); + CSS.highlights.delete("search-result-focus"); +} + + + +export function highlightContent(content: string, keywords: string[]): string { + if (!content) { + return content; + } + let contentHtml = getHighlightedContent(content, keywords); + return contentHtml; +} + +export function clearProtyleGutters(target: HTMLElement) { + if (!target) { + return; + } + target.querySelectorAll(".protyle-gutters").forEach((item) => { + item.classList.add("fn__none"); + item.innerHTML = ""; + }); +} + + +// 查找可滚动的父级元素 +export function findScrollableParent(element: HTMLElement) { + if (!element) { + return null; + } + + // const hasScrollableSpace = element.scrollHeight > element.clientHeight; + const hasVisibleOverflow = getComputedStyle(element).overflowY !== 'visible'; + + if (hasVisibleOverflow) { + return element; + } + + return findScrollableParent(element.parentElement); +} + + + + +function getHighlightedContent( + content: string, + keywords: string[], +): string { + if (!content) { + return content; + } + // let highlightedContent: string = escapeHtml(content); + let highlightedContent: string = content; + + if (keywords) { + highlightedContent = highlightMatches(highlightedContent, keywords); + } + return highlightedContent; +} + +function highlightMatches(content: string, keywords: string[]): string { + if (!keywords.length || !content) { + return content; // 返回原始字符串,因为没有需要匹配的内容 + } + + const regexPattern = new RegExp(`(${keywords.join("|")})`, "gi"); + const highlightedString = content.replace( + regexPattern, + "$1", + ); + return highlightedString; +} + +function escapeHtml(input: string): string { + const escapeMap: Record = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + + return input.replace(/[&<>"']/g, (match) => escapeMap[match]); +} + + + +export function getElementsBeforeDepth(rootElement: HTMLElement, selector: string, depth: number) { + const result = []; + + function recursiveSearch(node: Element, currentDepth: number) { + if (currentDepth > depth || syHasChildListNode(node)) { + return; + } + const targetElements = node.querySelectorAll(':scope > ' + selector); + for (const element of targetElements) { + result.push(element); + const childNodes = element.children; + for (let i = 0; i < childNodes.length; i++) { + recursiveSearch(childNodes[i], currentDepth + 1); + } + } + + } + + recursiveSearch(rootElement, 0) + + return result; +} + + +export function getElementsAtDepth(rootElement: Element, selector: string, depth: number) { + const result = []; + + function recursiveSearch(node: Element, currentDepth: number) { + const targetElements = node.querySelectorAll(':scope > ' + selector); + + if (currentDepth === depth) { + targetElements.forEach(element => result.push(element)); + return; + } + for (const element of targetElements) { + const childNodes = element.children; + for (let i = 0; i < childNodes.length; i++) { + recursiveSearch(childNodes[i], currentDepth + 1); + } + } + + } + + + recursiveSearch(rootElement, 0); + return result; +} + + +export function syHasChildListNode(root: Element): boolean { + if (!root) { + return false; + } + // 获取 root 的所有子节点 + const children = Array.from(root.children) as HTMLElement[]; + + // 确保有至少4个子节点 + if (children.length < 4) { + return false; + } + // let listNodeElement = root.querySelector(`:scope > [data-type="NodeList"].list`); + + // if ( + // listNodeElement + // ) { + // return true; + // } + + return true; +} + + +// 将字符串转换为 DOM 元素 +export function stringToElement(htmlString): Element { + // 使用 DOMParser 解析字符串 + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + + // 返回解析后的文档的根元素 + return doc.body.firstChild as Element; +} \ No newline at end of file diff --git a/src/utils/icon-util.ts b/src/utils/icon-util.ts new file mode 100644 index 0000000..6ac9793 --- /dev/null +++ b/src/utils/icon-util.ts @@ -0,0 +1,108 @@ + +export function convertIconInIal(icon: string): string { + if (icon) { + if (icon.includes(".")) { + // 如果包含 ".",则认为是图片,生成标签 + return ``; + } else { + // 如果是Emoji,转换为表情符号 + let emoji = ""; + try { + icon.split("-").forEach(item => { + if (item.length < 5) { + emoji += String.fromCodePoint(parseInt("0" + item, 16)); + } else { + emoji += String.fromCodePoint(parseInt(item, 16)); + } + }); + } catch (e) { + // 自定义表情搜索报错 https://github.com/siyuan-note/siyuan/issues/5883 + // 这里忽略错误不做处理 + } + return emoji; + } + } + // 既不是Emoji也不是图片,返回null + return null; +} + +export function convertIalStringToObject(ial: string): { [key: string]: string } { + const keyValuePairs = ial.match(/\w+="[^"]*"/g); + + if (!keyValuePairs) { + return {}; + } + + const resultObject: { [key: string]: string } = {}; + + keyValuePairs.forEach((pair) => { + const [key, value] = pair.split('='); + resultObject[key] = value.replace(/"/g, ''); // 去除值中的双引号 + }); + + return resultObject; +} + + + +export function getBlockTypeIconHref(type: string, subType: string): string { + let iconHref = ""; + if (type) { + if (type === "d") { + iconHref = "#iconFile"; + } else if (type === "h") { + if (subType === "h1") { + iconHref = "#iconH1"; + } else if (subType === "h2") { + iconHref = "#iconH2"; + } else if (subType === "h3") { + iconHref = "#iconH3"; + } else if (subType === "h4") { + iconHref = "#iconH4"; + } else if (subType === "h5") { + iconHref = "#iconH5"; + } else if (subType === "h6") { + iconHref = "#iconH6"; + } + } else if (type === "c") { + iconHref = "#iconCode"; + } else if (type === "html") { + iconHref = "#iconHTML5"; + } else if (type === "p") { + iconHref = "#iconParagraph"; + } else if (type === "m") { + iconHref = "#iconMath"; + } else if (type === "t") { + iconHref = "#iconTable"; + } else if (type === "b") { + iconHref = "#iconQuote"; + } else if (type === "l") { + if (subType === "o") { + iconHref = "#iconOrderedList"; + } else if (subType === "u") { + iconHref = "#iconList"; + } else if (subType === "t") { + iconHref = "#iconCheck"; + } + } else if (type === "i") { + iconHref = "#iconListItem"; + } else if (type === "av") { + iconHref = "#iconDatabase"; + } else if (type === "s") { + iconHref = "#iconSuper"; + } else if (type === "audio") { + iconHref = "#iconRecord"; + } else if (type === "video") { + iconHref = "#iconVideo"; + } else if (type === "query_embed") { + iconHref = "#iconSQL"; + } else if (type === "tb") { + iconHref = "#iconLine"; + } else if (type === "widget") { + iconHref = "#iconBoth"; + } else if (type === "iframe") { + iconHref = "#iconLanguage"; + } + } + return iconHref; +} \ No newline at end of file diff --git a/src/utils/json-util.ts b/src/utils/json-util.ts new file mode 100644 index 0000000..e18325b --- /dev/null +++ b/src/utils/json-util.ts @@ -0,0 +1,19 @@ +// 自定义 replacer 函数,在序列化时将 Set 对象转换为数组 +export function setReplacer(key, value) { + if (value instanceof Set) { + return { + _type: 'Set', + _value: [...value] + }; + } + return value; +} + +// 自定义 reviver 函数,在反序列化时将数组转换回 Set 对象 +export function setReviver(key, value) { + if (value && value._type === 'Set') { + return new Set(value._value); + } + return value; +} + diff --git a/src/utils/map-util.ts b/src/utils/map-util.ts new file mode 100644 index 0000000..6b9540d --- /dev/null +++ b/src/utils/map-util.ts @@ -0,0 +1,24 @@ +export function areMapsEqual(map1: Map, map2: Map): boolean { + if (map1 == map2) { + return true; + } + if (!map1 && map2) { + return false; + } + if (map1 && !map2) { + return false; + } + // 1. 比较两个 Map 的大小 + if (map1.size !== map2.size) { + return false; + } + + // 2. 逐项比较键值对 + for (let [key, value] of map1) { + if (!map2.has(key) || map2.get(key) !== value) { + return false; + } + } + + return true; +} diff --git a/src/utils/object-util.ts b/src/utils/object-util.ts new file mode 100644 index 0000000..1ebfc3a --- /dev/null +++ b/src/utils/object-util.ts @@ -0,0 +1,49 @@ +export function getObjectSizeInKB(obj: any): number { + try { + // 将 JSON 对象转换为字符串 + const jsonString = JSON.stringify(obj); + + // 计算字符串的字节数 + const bytes = new Blob([jsonString]).size; + + // 将字节数转换为 KB + const kilobytes = bytes / 1024; + return kilobytes; + } catch (err) { + console.log("计算对象大小报错") + } + return 0; +} + + +export function isBoolean(value: any): value is boolean { + return typeof value === 'boolean'; +} + +export function isObject(value: any): value is object { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + + +/** + * obj1 字段为空的值由 obj2 补上。 + * @param obj1 + * @param obj2 默认配置对象 + * @returns + */ +export function mergeObjects(obj1: T, obj2: U): T & U { + + const result = { ...obj1 } as T & U; + + for (const key in obj2) { + if (obj2.hasOwnProperty(key)) { + // 仅当 obj1[key] 为 null 或 undefined 时才覆盖 + if (result[key] === null || result[key] === undefined) { + (result as any)[key] = obj2[key]; + } + } + } + + return result; +} + diff --git a/src/utils/string-util.ts b/src/utils/string-util.ts new file mode 100644 index 0000000..9f6b48a --- /dev/null +++ b/src/utils/string-util.ts @@ -0,0 +1,109 @@ +import { isArrayEmpty } from "./array-util"; + +export function removePrefix(input: string, prefix: string): string { + if (input.startsWith(prefix)) { + return input.substring(prefix.length); + } else { + return input; + } +} + +export function removeSuffix(input: string, suffix: string): string { + if (input.endsWith(suffix)) { + return input.substring(0, input.length - suffix.length); + } else { + return input; + } +} + +export function removePrefixAndSuffix(input: string, prefix: string, suffix: string): string { + let result = input; + + if (result.startsWith(prefix)) { + result = result.substring(prefix.length); + } + + if (result.endsWith(suffix)) { + result = result.substring(0, result.length - suffix.length); + } + + return result; +} + +export function containsAllKeywords( + str: string, + keywords: string[], +): boolean { + return keywords.every(keyword => str.includes(keyword)); +} + + +export function longestCommonSubstring(s1: string, s2: string): string { + if (!s1 || !s2) { + return ""; + } + s1 = s1 ? s1 : ""; + s2 = s2 ? s2 : ""; + const len1 = s1.length; + const len2 = s2.length; + const dp: number[][] = Array.from({ length: len1 + 1 }, () => + Array(len2 + 1).fill(0), + ); + + let maxLength = 0; + let endIndex = 0; + + for (let i = 1; i <= len1; i++) { + for (let j = 1; j <= len2; j++) { + if (s1[i - 1] === s2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + if (dp[i][j] > maxLength) { + maxLength = dp[i][j]; + endIndex = i; + } + } + } + } + + return s1.substring(endIndex - maxLength, endIndex); +} + + +export function countOccurrences(str: string, subStr: string): number { + // 创建一个正则表达式,全局搜索指定的子字符串 + const regex = new RegExp(subStr, 'g'); + // 使用 match 方法匹配所有出现的子字符串,返回匹配结果数组 + const matches = str.match(regex); + // 返回匹配的次数,如果没有匹配到则返回 0 + return matches ? matches.length : 0; +} + +/** + * 判定字符串是否有效 + * @param s 需要检查的字符串(或其他类型的内容) + * @returns true / false 是否为有效的字符串 + */ +export function isValidStr(s: any): boolean { + if (s == undefined || s == null || s === '') { + return false; + } + return true; +} + +export function splitKeywordStringToArray(keywordStr: string): string[] { + let keywordArray = []; + if (!isValidStr(keywordStr)) { + return keywordArray; + } + // 分离空格 + keywordArray = keywordStr.trim().replace(/\s+/g, " ").split(" "); + if (isArrayEmpty(keywordArray)) { + return keywordArray; + } + // 去重 + keywordArray = Array.from(new Set( + keywordArray.filter((keyword) => keyword.length > 0), + )); + return keywordArray; + +} \ No newline at end of file diff --git a/src/utils/timing-util.ts b/src/utils/timing-util.ts new file mode 100644 index 0000000..421dda3 --- /dev/null +++ b/src/utils/timing-util.ts @@ -0,0 +1,24 @@ +export function delayedTwiceRefresh(executeFun: () => void, firstTimeout: number) { + if (!executeFun) { + return; + } + if (!firstTimeout) { + firstTimeout = 0; + } + let refreshPreviewHighlightTimeout = 140; + setTimeout(() => { + executeFun(); + + if ( + refreshPreviewHighlightTimeout && + refreshPreviewHighlightTimeout > 0 + ) { + setTimeout(() => { + executeFun(); + }, refreshPreviewHighlightTimeout); + } + + }, firstTimeout); +} + + diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..6d0422d --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-05-19 19:49:13 + * @FilePath : /svelte.config.js + * @LastEditTime : 2024-04-19 19:01:55 + * @Description : + */ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte" + +const NoWarns = new Set([ + "a11y-click-events-have-key-events", + "a11y-no-static-element-interactions", + "a11y-no-noninteractive-element-interactions" +]); + +export default { + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), + onwarn: (warning, handler) => { + if (warning.code.startsWith('A11y:')) { + return; + } + if (warning.code.startsWith('a11y-')) return + // suppress warnings on `vite dev` and `vite build`; but even without this, things still work + if (NoWarns.has(warning.code)) return; + handler(warning); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e196929 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,58 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "Node", + // "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + /* Linting */ + "strict": false, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + /* Svelte */ + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "types": [ + "node", + "vite/client", + "svelte" + ], + // "baseUrl": "./src", + "paths": { + "@/*": ["./src/*"], + "@/libs/*": ["./src/libs/*"], + } + }, + "include": [ + "tools/**/*.ts", + "src/**/*.ts", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.vue" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ], + "root": "." +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..1951553 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..809da57 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,131 @@ +import { resolve } from "path" +import { defineConfig, loadEnv } from "vite" +import minimist from "minimist" +import { viteStaticCopy } from "vite-plugin-static-copy" +import livereload from "rollup-plugin-livereload" +import { svelte } from "@sveltejs/vite-plugin-svelte" +import zipPack from "vite-plugin-zip-pack"; +import fg from 'fast-glob'; + +import vitePluginYamlI18n from './yaml-plugin'; + +const args = minimist(process.argv.slice(2)) +const isWatch = args.watch || args.w || false +const devDistDir = "dev" +const distDir = isWatch ? devDistDir : "dist" + +console.log("isWatch=>", isWatch) +console.log("distDir=>", distDir) + +export default defineConfig({ + resolve: { + alias: { + "@": resolve(__dirname, "src"), + } + }, + + plugins: [ + svelte(), + + vitePluginYamlI18n({ + inDir: 'public/i18n', + outDir: `${distDir}/i18n` + }), + + viteStaticCopy({ + targets: [ + { + src: "./README*.md", + dest: "./", + }, + { + src: "./plugin.json", + dest: "./", + }, + { + src: "./preview.png", + dest: "./", + }, + { + src: "./icon.png", + dest: "./", + } + ], + }), + ], + + // https://github.com/vitejs/vite/issues/1930 + // https://vitejs.dev/guide/env-and-mode.html#env-files + // https://github.com/vitejs/vite/discussions/3058#discussioncomment-2115319 + // 在这里自定义变量 + define: { + "process.env.DEV_MODE": `"${isWatch}"`, + "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV) + }, + + build: { + // 输出路径 + outDir: distDir, + emptyOutDir: false, + + // 构建后是否生成 source map 文件 + sourcemap: isWatch ? 'inline' : false, + + // 设置为 false 可以禁用最小化混淆 + // 或是用来指定是应用哪种混淆器 + // boolean | 'terser' | 'esbuild' + // 不压缩,用于调试 + minify: !isWatch, + + lib: { + // Could also be a dictionary or array of multiple entry points + entry: resolve(__dirname, "src/index.ts"), + // the proper extensions will be added + fileName: "index", + formats: ["cjs"], + }, + rollupOptions: { + plugins: [ + ...( + isWatch ? [ + livereload(devDistDir), + { + //监听静态资源文件 + name: 'watch-external', + async buildStart() { + const files = await fg([ + 'public/i18n/**', + './README*.md', + './plugin.json' + ]); + for (let file of files) { + this.addWatchFile(file); + } + } + } + ] : [ + zipPack({ + inDir: './dist', + outDir: './', + outFileName: 'package.zip' + }) + ] + ) + ], + + // make sure to externalize deps that shouldn't be bundled + // into your library + external: ["siyuan", "process"], + + output: { + entryFileNames: "[name].js", + assetFileNames: (assetInfo) => { + if (assetInfo.name === "style.css") { + return "index.css" + } + return assetInfo.name + }, + }, + }, + } +}) diff --git a/yaml-plugin.js b/yaml-plugin.js new file mode 100644 index 0000000..01c85e2 --- /dev/null +++ b/yaml-plugin.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-04-05 21:27:55 + * @FilePath : /yaml-plugin.js + * @LastEditTime : 2024-04-05 22:53:34 + * @Description : 去妮玛的 json 格式,我就是要用 yaml 写 i18n + */ +// plugins/vite-plugin-parse-yaml.js +import fs from 'fs'; +import yaml from 'js-yaml'; +import { resolve } from 'path'; + +export default function vitePluginYamlI18n(options = {}) { + // Default options with a fallback + const DefaultOptions = { + inDir: 'src/i18n', + outDir: 'dist/i18n', + }; + + const finalOptions = { ...DefaultOptions, ...options }; + + return { + name: 'vite-plugin-yaml-i18n', + buildStart() { + console.log('🌈 Parse I18n: YAML to JSON..'); + const inDir = finalOptions.inDir; + const outDir = finalOptions.outDir + + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + + //Parse yaml file, output to json + const files = fs.readdirSync(inDir); + for (const file of files) { + if (file.endsWith('.yaml') || file.endsWith('.yml')) { + console.log(`-- Parsing ${file}`) + //检查是否有同名的json文件 + const jsonFile = file.replace(/\.(yaml|yml)$/, '.json'); + if (files.includes(jsonFile)) { + console.log(`---- File ${jsonFile} already exists, skipping...`); + continue; + } + try { + const filePath = resolve(inDir, file); + const fileContents = fs.readFileSync(filePath, 'utf8'); + const parsed = yaml.load(fileContents); + const jsonContent = JSON.stringify(parsed, null, 2); + const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, '.json')); + console.log(`---- Writing to ${outputFilePath}`); + fs.writeFileSync(outputFilePath, jsonContent); + } catch (error) { + this.error(`---- Error parsing YAML file ${file}: ${error.message}`); + } + } + } + }, + }; +}