├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh_CN.md ├── asset └── action.png ├── assets ├── image-20230506013450-g2mkp8l.png ├── image-20241025221225-4ml02nc.png ├── image-20241025221628-8bslxks.png ├── image-20241025222516-lvb94rl.png ├── image-20241025223457-hi94ial.png ├── image-20241130145358-bqvwgmb.png ├── image-20241130151900-0n7ku7o.png ├── image-20241202164246-vla7mo8.png ├── image-20241202164442-588f7d7.png ├── image-20241204001321-csglpyu.png ├── image-20241204001504-jz4gbh1.png ├── image-20241204002444-9j30l5k.png ├── image-20241204002830-35q4qjh.png ├── image-20241204003312-d3040o5.png ├── image-20241204003849-8l19z7b.png ├── image-20241204004702-va0yg1n.png ├── image-20241204005817-mpdtp85.png ├── image-20241204112906-ih3lqzu.png ├── image-20241204123442-lceozz3.png ├── image-20241204123606-44328dv.png ├── image-20241204123811-vla1xke.png ├── image-20241204130225-vpgesgp.png ├── image-20241204130826-j6rwpyx.png ├── image-20241206182941-yzctkxu.png ├── image-20241206183442-ra4h7xl.png ├── image-20241206184455-4in6gct.png ├── image-20241206185311-ajowi8u.png ├── image-20241206190453-o0u8eb8.png ├── image-20241206190600-fu09ywo.png ├── image-20241206190618-bb58ls6.png ├── image-20241206190646-84tfh64.png ├── image-20241206191639-v6yiw7f.png ├── image-20241206192654-ycr25wv.png ├── image-20241206193640-g5h5jp9.png ├── image-20241206194739-md7he6w.png ├── image-20241206200537-udf4v6b.png ├── image-20241206201729-1bfn3md.png ├── image-20241206202124-3pu0qdw.png ├── image-20241206211503-q3b2uk5.png ├── image-20241207010811-8lh25x5.png ├── image-20241207010958-u6g07gl.png ├── image-20241207171409-l4z5ffo.png ├── image-20241207193310-9gpfbtk.png ├── image-20241207204410-a231unc.png ├── image-20241207210617-i5tmd5l.png ├── image-20241208222807-mvc3opc.png ├── image-20241208234136-s06cygn.png ├── image-20241209001506-1j38x18.png ├── image-20241209001549-kcurxon.png ├── image-20241209002057-jarcxsu.png ├── image-20241209005221-qtytbib.png ├── image-20241209210930-k9vnume.png ├── image-20241209212929-dlfxtip.png ├── image-20241209220101-oypr89p.png ├── image-20241210133627-mnp2zup.png ├── image-20241210171119-o72dyyd.png ├── image-20241210172133-ivjwzpc.png ├── image-20241210172746-kbxtfhr.png ├── image-20241210183914-5nm5w4r.png ├── image-20241211194155-oc0yj5l.png ├── image-20241211194348-sfzl8pc.png ├── image-20241211194447-8sa9hcx.png ├── image-20241211194757-74vrp7m.png ├── image-20241211213426-38ws4kk.png ├── image-20241211225413-fc962d4.png ├── image-20241213160419-62pwf7s.png ├── image-20241213161247-f6qm95q.png ├── image-20241213184747-0ma9dj4.png ├── image-20241213214406-rfj8yqh.png ├── image-20241213214945-r6p1je6.png ├── image-20241214152215-p163uhs.png ├── image-20241214183258-vdarhfx.png ├── image-20250308171816-crrru54.png ├── image-20250308172648-l0q3u5r.png ├── image-20250308172720-se43ute.png └── image-20250316162044-1l2i63f.png ├── auto-i18n.project.yaml ├── icon.png ├── package.json ├── plugin.json ├── preview.png ├── public ├── example │ ├── exp-avs-under-root-doc.js │ ├── exp-child-docs.js │ ├── exp-created-docs.js │ ├── exp-daily-sentence.js │ ├── exp-doc-backlinks-graph.js │ ├── exp-doc-backlinks-grouped.js │ ├── exp-doc-backlinks-table.js │ ├── exp-doc-tree.js │ ├── exp-gpt-chat.js │ ├── exp-gpt-translate.js │ ├── exp-latest-update-doc.js │ ├── exp-list-tags.js │ ├── exp-month-todo-kanban.js │ ├── exp-month-todo-timeline.js │ ├── exp-month-todo.js │ ├── exp-outline.js │ ├── exp-show-asset-images.js │ ├── exp-sql-executor.js │ └── exp-today-updated.js ├── i18n │ ├── README.md │ ├── en_US.yaml │ └── zh_CN.yaml └── types.d.ts ├── scripts ├── .gitignore ├── elevate.ps1 ├── export-types.js ├── git-tag.js └── utils.js ├── src ├── api.ts ├── core │ ├── components.ts │ ├── custom-view.ts │ ├── data-view.ts │ ├── editor.ts │ ├── finalize.ts │ ├── index.module.scss │ ├── index.ts │ ├── lute.ts │ ├── proxy.ts │ ├── query.ts │ ├── use-state.ts │ └── utils.ts ├── index.scss ├── index.ts ├── libs │ ├── const.ts │ ├── dialog.ts │ ├── index.d.ts │ ├── promise-pool.ts │ └── setting-utils.ts ├── setting │ └── index.ts ├── types │ ├── api.d.ts │ ├── data-view.d.ts │ ├── global.d.ts │ ├── i18n.d.ts │ └── index.d.ts ├── user-help │ ├── examples.ts │ ├── index.module.scss │ ├── index.ts │ └── sy-doc.ts └── utils │ ├── const.ts │ ├── index.ts │ ├── lute.ts │ ├── style.ts │ └── time.ts ├── translation.txt ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yaml-plugin.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release on Tag Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | # Install Node.js 17 | - name: Install Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | registry-url: "https://registry.npmjs.org" 22 | 23 | # Install pnpm 24 | - name: Install pnpm 25 | uses: pnpm/action-setup@v4 26 | id: pnpm-install 27 | with: 28 | version: 8 29 | run_install: false 30 | 31 | # Get pnpm store directory 32 | - name: Get pnpm store directory 33 | id: pnpm-cache 34 | shell: bash 35 | run: | 36 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 37 | 38 | # Setup pnpm cache 39 | - name: Setup pnpm cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 43 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 44 | restore-keys: | 45 | ${{ runner.os }}-pnpm-store- 46 | 47 | # Install dependencies 48 | - name: Install dependencies 49 | run: | 50 | pnpm install 51 | # 检查 typescript 是否正确安装 52 | pnpm list typescript 53 | # 打印当前目录 54 | pwd 55 | # 显示当前工作目录结构 56 | ls -la 57 | 58 | # Build for production 59 | - name: Build for production 60 | run: | 61 | # 显示 TypeScript 配置 62 | cat tsconfig.json 63 | # 执行构建并捕获所有输出 64 | pnpm run build || { echo "Build failed"; exit 1; } 65 | # 检查构建后的文件 66 | ls -la dist 67 | ls -la types || true 68 | 69 | - name: Release 70 | uses: ncipollo/release-action@v1 71 | with: 72 | allowUpdates: true 73 | artifactErrorsFailBuild: true 74 | artifacts: "package.zip" 75 | token: ${{ secrets.GITHUB_TOKEN }} 76 | prerelease: false 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | pnpm-lock.yaml 5 | package-lock.json 6 | package.zip 7 | node_modules 8 | dev 9 | dist 10 | build 11 | types/**/* 12 | .aider* 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### v1.2.3 4 | 5 | - ✨ feat: 新增 `Query.nearby` API,用于查询指定块的同级别的相邻块; 支持 `previous | next | both` 三种方向 6 | - 📝 doc: 修改 README 文档中的错别字 7 | 8 | ### v1.2.2 9 | 10 | - 🐛 fix: tag 匹配代码存在逻辑错误 11 | 12 | ### v1.2.1 适配 SiYuan 3.1.29 版本 13 | 14 | * `Query.task` API 适配 3.1.29 对列表符号的变更,自动按照思源版本适配 15 | * `Query.tag` API 新增 `match` 选项,支持 `=` 和 `like` 两种匹配模式 16 | * 改进 `Query.markdown` 函数的实现方案 17 | 18 | ### v1.1.0 ~ v1.2.0 的变化 19 | 20 | v1.1.0 版本中,由于存在和思源的不兼容性问题,插件暂时下架。 21 | 22 | v1.2.0 版本后,插件将不兼容思源的 3.1.24,25 版本。请选择其他的思源的版本来使用 Query View 插件。 23 | 24 | ✨ **新增功能** 25 | 26 | 1. DataView 中增加 `Card` 组件 27 | 2. DataView 的 `Markdown` 组件支持渲染数学公式 28 | 3. 优化了 DataView 中的 `Embed` 组件 29 | 4. 增加了 `Query.pruneBlocks` 函数,用于合并查询过程中具有父子关系的块,从而实现查询结果的去重 30 | 5. Example 中增加了 `list-tag` 的案例 31 | 32 | ⚠️ **API 变动** 33 | 34 | Query 中部分 API 的参数用法发生变动;旧的用法依然兼容,但是会提出警示,建议迁移到新的用法;具体情况请参考相关文档。 35 | 36 | 1. `Query.attr` 37 | 38 | ```javascript 39 | Query.attr("name", "value", "=", 10); // 弃用 40 | Query.attr("name", "value", { valMatch: "=", limit: 10 }); // 推荐 41 | ``` 42 | 2. `Query.tag` 43 | 44 | ```javascript 45 | Query.tag("tag1", "or", 10); // 弃用 46 | Query.tag("tag1", { join: "or", limit: 10 }); // 推荐 47 | ``` 48 | 3. `Query.task` 49 | 50 | ```javascript 51 | Query.task("2024101000", 32); // 弃用 52 | Query.task({ after: "2024101000", limit: 32 }); // 推荐 53 | ``` 54 | 4. `Query.keyword`/ `Query.keywordDoc` 55 | 56 | ``` 57 | Query.keyword("keyword", "or", 10); // 弃用 58 | Query.keyword("keyword", { join: "or", limit: 10 }); // 推荐 59 | ``` 60 | 5. `Query.dailynote` 61 | 62 | ```javascript 63 | Query.dailynote("20231224140619-bpyuay4", 32); // 弃用 64 | Query.dailynote({ notebook: "20231224140619-bpyuay4", limit: 32 }); // 推荐 65 | ``` -------------------------------------------------------------------------------- /asset/action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/asset/action.png -------------------------------------------------------------------------------- /assets/image-20230506013450-g2mkp8l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20230506013450-g2mkp8l.png -------------------------------------------------------------------------------- /assets/image-20241025221225-4ml02nc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241025221225-4ml02nc.png -------------------------------------------------------------------------------- /assets/image-20241025221628-8bslxks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241025221628-8bslxks.png -------------------------------------------------------------------------------- /assets/image-20241025222516-lvb94rl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241025222516-lvb94rl.png -------------------------------------------------------------------------------- /assets/image-20241025223457-hi94ial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241025223457-hi94ial.png -------------------------------------------------------------------------------- /assets/image-20241130145358-bqvwgmb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241130145358-bqvwgmb.png -------------------------------------------------------------------------------- /assets/image-20241130151900-0n7ku7o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241130151900-0n7ku7o.png -------------------------------------------------------------------------------- /assets/image-20241202164246-vla7mo8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241202164246-vla7mo8.png -------------------------------------------------------------------------------- /assets/image-20241202164442-588f7d7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241202164442-588f7d7.png -------------------------------------------------------------------------------- /assets/image-20241204001321-csglpyu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204001321-csglpyu.png -------------------------------------------------------------------------------- /assets/image-20241204001504-jz4gbh1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204001504-jz4gbh1.png -------------------------------------------------------------------------------- /assets/image-20241204002444-9j30l5k.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204002444-9j30l5k.png -------------------------------------------------------------------------------- /assets/image-20241204002830-35q4qjh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204002830-35q4qjh.png -------------------------------------------------------------------------------- /assets/image-20241204003312-d3040o5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204003312-d3040o5.png -------------------------------------------------------------------------------- /assets/image-20241204003849-8l19z7b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204003849-8l19z7b.png -------------------------------------------------------------------------------- /assets/image-20241204004702-va0yg1n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204004702-va0yg1n.png -------------------------------------------------------------------------------- /assets/image-20241204005817-mpdtp85.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204005817-mpdtp85.png -------------------------------------------------------------------------------- /assets/image-20241204112906-ih3lqzu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204112906-ih3lqzu.png -------------------------------------------------------------------------------- /assets/image-20241204123442-lceozz3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204123442-lceozz3.png -------------------------------------------------------------------------------- /assets/image-20241204123606-44328dv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204123606-44328dv.png -------------------------------------------------------------------------------- /assets/image-20241204123811-vla1xke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204123811-vla1xke.png -------------------------------------------------------------------------------- /assets/image-20241204130225-vpgesgp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204130225-vpgesgp.png -------------------------------------------------------------------------------- /assets/image-20241204130826-j6rwpyx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241204130826-j6rwpyx.png -------------------------------------------------------------------------------- /assets/image-20241206182941-yzctkxu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206182941-yzctkxu.png -------------------------------------------------------------------------------- /assets/image-20241206183442-ra4h7xl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206183442-ra4h7xl.png -------------------------------------------------------------------------------- /assets/image-20241206184455-4in6gct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206184455-4in6gct.png -------------------------------------------------------------------------------- /assets/image-20241206185311-ajowi8u.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206185311-ajowi8u.png -------------------------------------------------------------------------------- /assets/image-20241206190453-o0u8eb8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206190453-o0u8eb8.png -------------------------------------------------------------------------------- /assets/image-20241206190600-fu09ywo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206190600-fu09ywo.png -------------------------------------------------------------------------------- /assets/image-20241206190618-bb58ls6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206190618-bb58ls6.png -------------------------------------------------------------------------------- /assets/image-20241206190646-84tfh64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206190646-84tfh64.png -------------------------------------------------------------------------------- /assets/image-20241206191639-v6yiw7f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206191639-v6yiw7f.png -------------------------------------------------------------------------------- /assets/image-20241206192654-ycr25wv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206192654-ycr25wv.png -------------------------------------------------------------------------------- /assets/image-20241206193640-g5h5jp9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206193640-g5h5jp9.png -------------------------------------------------------------------------------- /assets/image-20241206194739-md7he6w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206194739-md7he6w.png -------------------------------------------------------------------------------- /assets/image-20241206200537-udf4v6b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206200537-udf4v6b.png -------------------------------------------------------------------------------- /assets/image-20241206201729-1bfn3md.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206201729-1bfn3md.png -------------------------------------------------------------------------------- /assets/image-20241206202124-3pu0qdw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206202124-3pu0qdw.png -------------------------------------------------------------------------------- /assets/image-20241206211503-q3b2uk5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241206211503-q3b2uk5.png -------------------------------------------------------------------------------- /assets/image-20241207010811-8lh25x5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207010811-8lh25x5.png -------------------------------------------------------------------------------- /assets/image-20241207010958-u6g07gl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207010958-u6g07gl.png -------------------------------------------------------------------------------- /assets/image-20241207171409-l4z5ffo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207171409-l4z5ffo.png -------------------------------------------------------------------------------- /assets/image-20241207193310-9gpfbtk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207193310-9gpfbtk.png -------------------------------------------------------------------------------- /assets/image-20241207204410-a231unc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207204410-a231unc.png -------------------------------------------------------------------------------- /assets/image-20241207210617-i5tmd5l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241207210617-i5tmd5l.png -------------------------------------------------------------------------------- /assets/image-20241208222807-mvc3opc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241208222807-mvc3opc.png -------------------------------------------------------------------------------- /assets/image-20241208234136-s06cygn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241208234136-s06cygn.png -------------------------------------------------------------------------------- /assets/image-20241209001506-1j38x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209001506-1j38x18.png -------------------------------------------------------------------------------- /assets/image-20241209001549-kcurxon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209001549-kcurxon.png -------------------------------------------------------------------------------- /assets/image-20241209002057-jarcxsu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209002057-jarcxsu.png -------------------------------------------------------------------------------- /assets/image-20241209005221-qtytbib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209005221-qtytbib.png -------------------------------------------------------------------------------- /assets/image-20241209210930-k9vnume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209210930-k9vnume.png -------------------------------------------------------------------------------- /assets/image-20241209212929-dlfxtip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209212929-dlfxtip.png -------------------------------------------------------------------------------- /assets/image-20241209220101-oypr89p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241209220101-oypr89p.png -------------------------------------------------------------------------------- /assets/image-20241210133627-mnp2zup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241210133627-mnp2zup.png -------------------------------------------------------------------------------- /assets/image-20241210171119-o72dyyd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241210171119-o72dyyd.png -------------------------------------------------------------------------------- /assets/image-20241210172133-ivjwzpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241210172133-ivjwzpc.png -------------------------------------------------------------------------------- /assets/image-20241210172746-kbxtfhr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241210172746-kbxtfhr.png -------------------------------------------------------------------------------- /assets/image-20241210183914-5nm5w4r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241210183914-5nm5w4r.png -------------------------------------------------------------------------------- /assets/image-20241211194155-oc0yj5l.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211194155-oc0yj5l.png -------------------------------------------------------------------------------- /assets/image-20241211194348-sfzl8pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211194348-sfzl8pc.png -------------------------------------------------------------------------------- /assets/image-20241211194447-8sa9hcx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211194447-8sa9hcx.png -------------------------------------------------------------------------------- /assets/image-20241211194757-74vrp7m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211194757-74vrp7m.png -------------------------------------------------------------------------------- /assets/image-20241211213426-38ws4kk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211213426-38ws4kk.png -------------------------------------------------------------------------------- /assets/image-20241211225413-fc962d4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241211225413-fc962d4.png -------------------------------------------------------------------------------- /assets/image-20241213160419-62pwf7s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241213160419-62pwf7s.png -------------------------------------------------------------------------------- /assets/image-20241213161247-f6qm95q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241213161247-f6qm95q.png -------------------------------------------------------------------------------- /assets/image-20241213184747-0ma9dj4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241213184747-0ma9dj4.png -------------------------------------------------------------------------------- /assets/image-20241213214406-rfj8yqh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241213214406-rfj8yqh.png -------------------------------------------------------------------------------- /assets/image-20241213214945-r6p1je6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241213214945-r6p1je6.png -------------------------------------------------------------------------------- /assets/image-20241214152215-p163uhs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241214152215-p163uhs.png -------------------------------------------------------------------------------- /assets/image-20241214183258-vdarhfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20241214183258-vdarhfx.png -------------------------------------------------------------------------------- /assets/image-20250308171816-crrru54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20250308171816-crrru54.png -------------------------------------------------------------------------------- /assets/image-20250308172648-l0q3u5r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20250308172648-l0q3u5r.png -------------------------------------------------------------------------------- /assets/image-20250308172720-se43ute.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20250308172720-se43ute.png -------------------------------------------------------------------------------- /assets/image-20250316162044-1l2i63f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/assets/image-20250316162044-1l2i63f.png -------------------------------------------------------------------------------- /auto-i18n.project.yaml: -------------------------------------------------------------------------------- 1 | # py-auto-i18n project. 2 | # * `i18n_dir`: The directory for storing translation files 3 | # * `main_file`: The translation file for the main language 4 | # * `code_files`: The types of code files to scan 5 | # * `i18n_pattern`: The pattern to mark text that needs to be translated in the code 6 | # * `dict`: A dictionary of specific terms for translation; you can place specific translations for your project here 7 | # * `strategy`: The translation strategy 8 | # * `"diff"` means only translating new content 9 | # * `"full"` means translating all content 10 | # * `i18n_var_prefix`: The prefix used for replacement variables in the code 11 | # * `export_dir`: The export directory. If set, it will be used as the output directory for the export command. 12 | # * `i18n_var_mid`: The strategy for generating the middle part of the i18n key. Options are: 13 | # * `"filename"`: Uses the full filename, e.g. `testts` 14 | # * `"filename_noext"`: Uses the filename without its extension 15 | # * `"pathname"`: Uses the relative path of the file, replacing '/' with '_' 16 | # 17 | code_files: 18 | - src/**/*.ts 19 | - src/**/*.svelte 20 | dict: 21 | - 思源翻译为 SiYuan 22 | - QueryView, DataView 这些专有名词, 不用翻译, 保持原名 23 | - 视图组件 翻译成 "View Component 24 | - 块 是思源笔记中的基本单位,翻译成 Block 25 | - 嵌入块 翻译成 Embed Block 26 | - 反向链接 或者 反链 翻译成 backlink 27 | export_dir: src/types 28 | i18n_dir: public/i18n 29 | i18n_pattern: \(\(`(.+?)`\)\) 30 | i18n_var_mid: pathname 31 | i18n_var_prefix: i18n 32 | main_file: zh_CN.yaml 33 | strategy: diff 34 | 35 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sy-query-view", 3 | "version": "1.2.3", 4 | "type": "module", 5 | "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan). Created with siyuan-plugin-cli v2.4.5.", 6 | "repository": "https://github.com/frostime/sy-query-view", 7 | "homepage": "", 8 | "author": "frostime", 9 | "license": "MIT", 10 | "scripts": { 11 | "dev": "npm run export-types && npm run vite:dev", 12 | "build": "npm run export-types && npm run vite:build", 13 | "vite:dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", 14 | "vite:build": "cross-env NODE_ENV=production vite build", 15 | "vite:build-srcmap": "cross-env NODE_ENV=production VITE_SOURCEMAP=inline vite build", 16 | "vite:build-no-minify": "cross-env NODE_ENV=production NO_MINIFY=true vite build", 17 | "make-link": "npx make-link-win", 18 | "update-version": "npx update-version", 19 | "make-install": "pnpm run vite:build-no-minify && npx make-install", 20 | "auto-i18n": "i18n extract && i18n translate && i18n export", 21 | "export-types": "node scripts/export-types.js", 22 | "git-tag": "node scripts/git-tag.js" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^20.3.0", 26 | "cross-env": "^7.0.3", 27 | "fast-glob": "^3.2.12", 28 | "glob": "^10.0.0", 29 | "js-yaml": "^4.1.0", 30 | "marked": "^15.0.3", 31 | "minimist": "^1.2.8", 32 | "rollup-plugin-livereload": "^2.0.5", 33 | "sass": "^1.63.3", 34 | "siyuan": "1.0.7", 35 | "ts-morph": "^24.0.0", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^5.1.3", 38 | "vite": "^5.2.9", 39 | "vite-plugin-static-copy": "^1.0.2", 40 | "vite-plugin-zip-pack": "^1.0.5" 41 | }, 42 | "dependencies": { 43 | "@frostime/siyuan-plugin-kits": "^1.5.9" 44 | } 45 | } -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sy-query-view", 3 | "author": "frostime", 4 | "url": "https://github.com/frostime/sy-query-view", 5 | "version": "1.2.3", 6 | "minAppVersion": "3.1.14", 7 | "backends": [ 8 | "windows", 9 | "linux", 10 | "darwin", 11 | "docker", 12 | "ios", 13 | "android", 14 | "harmony" 15 | ], 16 | "frontends": [ 17 | "desktop", 18 | "mobile", 19 | "browser-desktop", 20 | "browser-mobile", 21 | "desktop-window" 22 | ], 23 | "displayName": { 24 | "en_US": "Query & View", 25 | "zh_CN": "Query & View" 26 | }, 27 | "description": { 28 | "en_US": "Use Javascript to query and render as Dataview view", 29 | "zh_CN": "使用 Javascript 查询, 并自定义渲染为 Dataview 视图" 30 | }, 31 | "readme": { 32 | "en_US": "README.md", 33 | "zh_CN": "README_zh_CN.md" 34 | }, 35 | "funding": { 36 | "custom": [ 37 | "https://afdian.com/a/frostime" 38 | ] 39 | }, 40 | "keywords": [ 41 | "javascript", 42 | "query", 43 | "dataview" 44 | ] 45 | } -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/preview.png -------------------------------------------------------------------------------- /public/example/exp-avs-under-root-doc.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | const root_id = Query.root_id 4 | const sql = `select * from blocks where type='av' and path like '%${dv.root_id}%'`; 5 | const blocks = await Query.sql(sql); 6 | return blocks.pick('id') 7 | } 8 | 9 | return query(); 10 | -------------------------------------------------------------------------------- /public/example/exp-child-docs.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const row = (block) => ` 3 | {{{col 4 | 5 | ${block.icon} **${block.aslink}** 6 | 7 | **${block.createdDate} ~ ${block.updatedDate}** 8 | {: style="flex: none;" } 9 | 10 | }}} 11 | {: style="border-bottom: 1px dashed var(--b3-theme-on-surface-light); border-radius: 0px;" } 12 | `.trim(); 13 | 14 | const query = async () => { 15 | let dv = Query.DataView(protyle, item, top); 16 | 17 | let blocks = await Query.childDoc(dv.root_id); 18 | let icons = blocks.map(block => Query.Utils.docIcon(block)); 19 | blocks = blocks.addcols({ 'icon': icons }); 20 | 21 | dv.addmd(blocks.map(row).join('\n\n')); 22 | dv.render(); 23 | } 24 | 25 | return query(); -------------------------------------------------------------------------------- /public/example/exp-created-docs.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.DataView(protyle, item, top); 4 | const SQL = ` 5 | SELECT 6 | SUBSTR(created, 1, 6) AS month, 7 | COUNT(*) AS count 8 | FROM 9 | blocks 10 | WHERE 11 | type = 'd' 12 | GROUP BY 13 | SUBSTR(created, 1, 6) 14 | ORDER BY 15 | month; 16 | `; 17 | 18 | let blocks = await Query.sql(SQL); 19 | 20 | dv.addeline(blocks.pick('month'), blocks.pick('count'), { 21 | title: 'Monthly Created Documents', 22 | xlabel: 'Month', 23 | ylabel: 'Count' 24 | }); 25 | 26 | dv.render(); 27 | } 28 | 29 | return query(); -------------------------------------------------------------------------------- /public/example/exp-daily-sentence.js: -------------------------------------------------------------------------------- 1 | //!js 2 | let dv = Query.DataView(protyle, item, top); 3 | const today = Query.Utils.today(); 4 | const state = dv.useState(today); 5 | if (state()) { 6 | dv.addmd('今天的每日一句') 7 | dv.addmd(`> ${state()}`) 8 | } else { 9 | fetch('https://api.xygeng.cn/one').then(async ans => { 10 | console.log(ans) 11 | if (ans.ok) { 12 | let data = await ans.json(); 13 | console.log(data) 14 | state.value = `${data.data.content} —— ${data.data.origin}`; 15 | dv.addmd('今天的每日一句') 16 | dv.addmd(`> ${state.value}`) 17 | } 18 | }); 19 | } 20 | dv.render(); -------------------------------------------------------------------------------- /public/example/exp-doc-backlinks-graph.js: -------------------------------------------------------------------------------- 1 | //!js 2 | 3 | const clipStr = (text, cnt) => { 4 | if (text.length > cnt - 3) { 5 | return text.slice(0, cnt - 3) + '...'; 6 | } else { 7 | return text; 8 | } 9 | } 10 | 11 | const query = async () => { 12 | let dv = Query.DataView(protyle, item, top); 13 | let thisdoc = await Query.thisdoc(protyle); 14 | 15 | let backlinks = await Query.backlink(dv.root_id); 16 | let nodes = [thisdoc, ...backlinks]; //Merge to nodes 17 | let links = [ 18 | { source: thisdoc.id, target: backlinks.pick('id') }, //Create links 19 | ]; 20 | 21 | dv.addegraph(nodes, links, { 22 | height: '500px', 23 | roam: true, 24 | nodeRenderer: (block) => { 25 | //Only return name of the node is ok, other parts will use the default renderer. 26 | return { 27 | name: clipStr(block.name || block.content, 15), 28 | } 29 | } 30 | }); 31 | 32 | dv.render(); 33 | } 34 | 35 | return query(); 36 | -------------------------------------------------------------------------------- /public/example/exp-doc-backlinks-grouped.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.Dataview(protyle, item, top); 4 | let blocks = await Query.backlink(protyle.block.rootID); 5 | blocks = await Query.fb2p(blocks); 6 | blocks.groupby('type', (type, groups) => { 7 | dv.adddetails( 8 | Query.Utils.typename(type), 9 | dv.table(groups, { 10 | fullwidth: true, 11 | }) 12 | ); 13 | }) 14 | dv.render(); 15 | } 16 | return query(); 17 | -------------------------------------------------------------------------------- /public/example/exp-doc-backlinks-table.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.Dataview(protyle, item, top); 4 | let blocks = await Query.backlink(protyle.block.rootID); 5 | blocks = await Query.fb2p(blocks); 6 | dv.addtable(blocks, { 7 | fullwidth: true, 8 | }); 9 | dv.render(); 10 | } 11 | return query(); -------------------------------------------------------------------------------- /public/example/exp-doc-tree.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const MAX_DEPTH = 3; // Control the max depth of the tree, not recommend to set too large 3 | 4 | const buildTree = async (docId, depth = 1) => { 5 | if (depth > MAX_DEPTH) return []; 6 | const children = await Query.childdoc(docId); 7 | 8 | for (const child of children) { 9 | let docs = await buildTree(child.id, depth + 1); 10 | if (docs.length > 0) { 11 | child.children = Query.wrapBlocks(docs); 12 | } 13 | } 14 | 15 | return children; 16 | }; 17 | 18 | const query = async () => { 19 | let dv = Query.DataView(protyle, item, top); 20 | dv.render(); 21 | const tree = await buildTree(dv.root_id, 1); 22 | dv.addlist(tree, { type: 'o' }); 23 | }; 24 | 25 | return query(); 26 | -------------------------------------------------------------------------------- /public/example/exp-gpt-chat.js: -------------------------------------------------------------------------------- 1 | //!js 2 | 3 | const ui = () => { 4 | const textarea = document.createElement('textarea'); 5 | textarea.className = "fn__block b3-text-field"; 6 | textarea.rows = 3; 7 | textarea.placeholder = "Input Your Message..."; 8 | 9 | // 创建按钮容器,使用 flex 布局 10 | const buttonContainer = document.createElement('div'); 11 | buttonContainer.style.display = 'flex'; 12 | buttonContainer.style.justifyContent = 'flex-end'; 13 | buttonContainer.style.gap = '8px'; 14 | buttonContainer.style.marginTop = '8px'; 15 | 16 | const removeLastButton = document.createElement('button'); 17 | removeLastButton.className = "b3-button"; 18 | removeLastButton.textContent = "Remove Last"; 19 | buttonContainer.appendChild(removeLastButton); 20 | 21 | // Send 按钮 22 | const sendButton = document.createElement('button'); 23 | sendButton.className = "b3-button"; 24 | sendButton.textContent = "Send Input"; 25 | 26 | // 将按钮添加到容器 27 | buttonContainer.appendChild(removeLastButton); 28 | buttonContainer.appendChild(sendButton); 29 | 30 | return { textarea, buttonContainer, sendButton, removeLastButton }; 31 | 32 | } 33 | 34 | 35 | const chat = async () => { 36 | let dv = Query.DataView(protyle, item, top); 37 | const messages = dv.useState('messages', []); 38 | 39 | dv.addmd(`#### GPT Chat`); 40 | const msgIds = []; 41 | messages().forEach(msg => { 42 | let el = dv.addmd(`**${msg.role === 'user' ? 'You' : 'GPT'}**: ${msg.content}`); 43 | msgIds.push(el.dataset.id); 44 | }); 45 | 46 | dv.addmd('---'); 47 | 48 | const { textarea, buttonContainer, sendButton, removeLastButton } = ui(); 49 | 50 | dv.addele(textarea); 51 | 52 | dv.addele(buttonContainer); 53 | 54 | sendButton.onclick = async () => { 55 | const prompt = textarea.value.trim(); 56 | if (!prompt) return; 57 | 58 | messages([...messages(), { role: 'user', content: prompt }]); 59 | textarea.value = ''; 60 | sendButton.disabled = true; 61 | removeLastButton.disabled = true; 62 | let respond = dv.addmd(`**GPT**: `); 63 | let id = respond.dataset.id; 64 | 65 | try { 66 | const response = await Query.gpt(prompt, { 67 | stream: true, 68 | streamInterval: 3, 69 | streamMsg: (content) => { 70 | dv.replaceView(id, dv.md(`**GPT**: ${content}`)); 71 | } 72 | }); 73 | messages([...messages(), { role: 'assistant', content: response }]); 74 | } catch (error) { 75 | dv.addmd(`Error: ${error.message}`); 76 | } 77 | 78 | sendButton.disabled = false; 79 | removeLastButton.disabled = false; 80 | dv.repaint(); 81 | }; 82 | 83 | removeLastButton.onclick = () => { 84 | if (msgIds.length < 2) return; 85 | 86 | // 删除最后两条消息 87 | messages(messages().slice(0, -2)); 88 | 89 | // 删除最后两个消息的 DOM 元素 90 | dv.removeView(msgIds.pop()); 91 | dv.removeView(msgIds.pop()); 92 | }; 93 | 94 | dv.render(); 95 | } 96 | 97 | return chat(); -------------------------------------------------------------------------------- /public/example/exp-gpt-translate.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const prompt = (text) => ` 3 | Please translate the text after --- to English. 4 | Then output the translated text, no other text or comments. 5 | Maintain the markdown format of the original text. 6 | --- 7 | 8 | ${text} 9 | `; 10 | const query = async () => { 11 | let dv = Query.DataView(protyle, item, top); 12 | dv.render(); 13 | //任意选择一个不为空的段落 14 | let block = (await Query.random(10, 'p')).find(b => b.markdown !== ''); 15 | 16 | let md = block.markdown; 17 | dv.addmd(` 18 | ## Original Text 19 | 20 | ${md} 21 | 22 | ---- 23 | `); 24 | const div = document.createElement('div'); 25 | let tempid = (dv.addele(div)).dataset.id; 26 | const translated = await Query.gpt(prompt(md), { 27 | stream: true, 28 | streamMsg: (content) => { 29 | div.innerText = content; 30 | } 31 | }); 32 | dv.removeView(tempid); 33 | 34 | dv.addmd(` 35 | ## Translated Text 36 | 37 | ${translated} 38 | `.trim()); 39 | } 40 | 41 | return query(); -------------------------------------------------------------------------------- /public/example/exp-latest-update-doc.js: -------------------------------------------------------------------------------- 1 | //!js 2 | 3 | // SiYuan's super block syntax (md syntax extension) 4 | const columns = (block) => ` 5 | {{{col 6 | 7 | ${block.attr('hpath')} 8 | 9 | ${block.attr('box')} - ${block.attr('updated')} 10 | {: style="text-align: right; flex: none;" } 11 | 12 | }}} 13 | `.trim(); 14 | const query = async () => { 15 | let dv = Query.DataView(protyle, item, top); 16 | 17 | let blocks = await Query.sql(` 18 | select * from blocks where type='d' 19 | order by updated desc limit 32; 20 | `) 21 | dv.addlist(blocks, { 22 | renderer: columns 23 | }); 24 | 25 | dv.render(); 26 | } 27 | 28 | return query(); 29 | -------------------------------------------------------------------------------- /public/example/exp-list-tags.js: -------------------------------------------------------------------------------- 1 | //!js 2 | 3 | const useButton = (title, onclick) => { 4 | let button = document.createElement('button'); 5 | button.className = 'b3-button b3-button--text'; 6 | button.innerText = title; 7 | button.onclick = onclick; 8 | return button; 9 | } 10 | 11 | const query = async () => { 12 | let dv = Query.DataView(protyle, item, top); 13 | dv.render(); 14 | let tags = await Query.request('/api/tag/getTag', { 15 | sort: 4 16 | }); 17 | 18 | tags = tags.sort((a, b) => - a.count + b.count); 19 | 20 | const onclick = (tag) => { 21 | Query.tag(tag.label).then(async (blocks) => { 22 | if (blocks.length == 0) return; 23 | blocks = blocks.sorton('created'); 24 | blocks = await Query.prune(blocks, 'leaf'); 25 | blocks = await Query.fb2p(blocks); 26 | //const table = dv.table(blocks, {fullwidth: true} ); 27 | const table = dv.cards(blocks, { 28 | width: '275px', 29 | height: '150px' 30 | }); 31 | dv.replaceView(main.dataset.id, table); 32 | }); 33 | } 34 | 35 | const createTagButtons = (tags) => { 36 | const buttons = []; 37 | tags.forEach(tag => { 38 | const button = useButton(`#${tag.label} (${tag.count})`, () => { 39 | onclick(tag); 40 | }); 41 | button.style.margin = '5px'; 42 | buttons.push(button); 43 | 44 | // Recursively process children tags 45 | if (tag.children && tag.children.length > 0) { 46 | const childButtons = createTagButtons(tag.children); 47 | buttons.push(...childButtons); 48 | } 49 | }); 50 | return buttons; 51 | } 52 | 53 | const tagButtons = createTagButtons(tags); 54 | 55 | const allTagsList = document.createElement('div'); 56 | allTagsList.style.display = 'flex'; 57 | allTagsList.style.flexWrap = 'wrap'; 58 | tagButtons.forEach(tagElement => { 59 | allTagsList.appendChild(tagElement); 60 | }); 61 | dv.addele(allTagsList); 62 | dv.addmd('---'); 63 | 64 | let main = dv.addele('') 65 | } 66 | 67 | return query(); 68 | -------------------------------------------------------------------------------- /public/example/exp-month-todo-kanban.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.Dataview(protyle, item, top); 4 | // null: no `after` filter, query all task block 5 | // 128: max number of result 6 | let blocks = await Query.task(null, 128); 7 | let grouped = blocks.groupby((b) => { 8 | return b.createdDate.slice(0, -3) 9 | }); 10 | let N = Object.keys(grouped).length; 11 | // each group with a fixed witdh 200px 12 | dv.addmkanban(grouped, { 13 | width: `${N * 200}px` 14 | }); 15 | dv.render(); 16 | } 17 | return query(); 18 | -------------------------------------------------------------------------------- /public/example/exp-month-todo-timeline.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.Dataview(protyle, item, top); 4 | 5 | let blocks = await Query.task(null, 128); 6 | blocks = blocks.sorton('created', 'desc'); 7 | const blockKey = (b) => b.createdDate.slice(0, 7); 8 | 9 | let columns = []; 10 | blocks.groupby(blockKey, (groupname, group) => { 11 | let ele = dv.rows([ 12 | dv.md(`#### ${groupname}`), 13 | dv.list(group) 14 | ]) 15 | columns.push(ele); 16 | }); 17 | let ele = dv.addcols(columns, { 18 | minWidth: '400px' 19 | }); 20 | ele.style.border = '2px dashed var(--b3-theme-primary)'; 21 | ele.style.borderRadius = '10px'; 22 | ele.style.margin = '10px 20px'; 23 | 24 | dv.render(); 25 | } 26 | return query(); -------------------------------------------------------------------------------- /public/example/exp-month-todo.js: -------------------------------------------------------------------------------- 1 | //!js 2 | async function getIds() { 3 | let blocks = await Query.task(Query.utils.thisMonth(), 32); 4 | return blocks.pick('id'); 5 | } 6 | 7 | return getIds(); -------------------------------------------------------------------------------- /public/example/exp-outline.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.DataView(protyle, item, top); 4 | let ans = await Query.request('/api/outline/getDocOutline', { 5 | id: Query.root_id(protyle) 6 | }); 7 | ans = Query.wrapit(ans); 8 | const iterate = (data) => { 9 | for (let item of data) { 10 | if (item.count > 0) { 11 | let subtocs = iterate(item.blocks ?? item.children); 12 | item.children = Query.wrapBlocks(subtocs); 13 | } 14 | } 15 | return data; 16 | } 17 | let tocs = iterate(ans); 18 | dv.addlist(tocs, { 19 | renderer: b => `[${b.name || b.content}](${b.asurl})`, 20 | }); 21 | dv.render(); 22 | } 23 | 24 | return query(); -------------------------------------------------------------------------------- /public/example/exp-show-asset-images.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const assetFile = async () => { 3 | let response = await Query.request('/api/file/readDir', { 4 | path: '/data/assets' 5 | }); 6 | return response.map(file => file.name); 7 | } 8 | 9 | const ITEMS_PER_PAGE = 10; 10 | 11 | const useControl = (files) => { 12 | let page = 1; 13 | let leftBtn = document.createElement('button'); 14 | leftBtn.classList.add('b3-button'); 15 | let span = document.createElement('span'); 16 | let rightBtn = document.createElement('button'); 17 | rightBtn.classList.add('b3-button'); 18 | 19 | let slice = []; 20 | let total = files.length; 21 | let pages = Math.ceil(total / ITEMS_PER_PAGE); 22 | 23 | leftBtn.textContent = 'Previous'; 24 | span.textContent = `Page ${page} of ${pages}`; 25 | rightBtn.textContent = 'Next'; 26 | 27 | const panel = document.createElement('div'); 28 | Object.assign(panel.style, { 29 | display: 'flex', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | gap: '10px' 33 | }); 34 | panel.appendChild(leftBtn); 35 | panel.appendChild(span); 36 | panel.appendChild(rightBtn); 37 | 38 | const updateSlice = () => { 39 | const start = (page - 1) * ITEMS_PER_PAGE; 40 | const end = start + ITEMS_PER_PAGE; 41 | slice = files.slice(start, end); 42 | }; 43 | 44 | const left = () => { 45 | if (page > 1) { 46 | page--; 47 | span.textContent = `Page ${page} of ${pages}`; 48 | updateSlice(); 49 | } 50 | }; 51 | 52 | const right = () => { 53 | if (page < pages) { 54 | page++; 55 | span.textContent = `Page ${page} of ${pages}`; 56 | updateSlice(); 57 | } 58 | }; 59 | 60 | updateSlice(); 61 | 62 | return { 63 | panel, 64 | leftBtn, 65 | rightBtn, 66 | left, 67 | right, 68 | slice: () => slice 69 | }; 70 | } 71 | 72 | const query = async () => { 73 | let dv = Query.DataView(protyle, item, top); 74 | let files = await assetFile(); 75 | files = files.filter(file => { 76 | return file.endsWith('.jpg') || file.endsWith('.png') || file.endsWith('.jpeg'); 77 | }); 78 | 79 | let control = useControl(files); 80 | 81 | dv.addele(control.panel); 82 | 83 | let ele = dv.addele('Placeholder'); 84 | let id = ele.dataset.id; 85 | 86 | const createView = (slice) => { 87 | const data = slice.map(item => { 88 | return { 89 | name: item, 90 | img: `![](/assets/${item})` 91 | } 92 | }); 93 | return dv.table(data, { 94 | fullwidth: true, 95 | cols: null, 96 | }); 97 | } 98 | 99 | control.leftBtn.onclick = () => { 100 | control.left(); 101 | const slice = control.slice(); 102 | dv.replaceView(id, createView(slice)) 103 | }; 104 | control.rightBtn.onclick = () => { 105 | control.right(); 106 | const slice = control.slice(); 107 | dv.replaceView(id, createView(slice)) 108 | }; 109 | 110 | dv.replaceView(id, createView(control.slice())) 111 | 112 | dv.render(); 113 | }; 114 | 115 | return query(); 116 | -------------------------------------------------------------------------------- /public/example/exp-sql-executor.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const query = async () => { 3 | let dv = Query.DataView(protyle, item, top); 4 | const sql = dv.useState('sql', ''); 5 | const searchResult = dv.useState('search-result', []); 6 | 7 | dv.addmd(`#### SQL Executor`); 8 | const textarea = document.createElement('textarea'); 9 | textarea.className = "fn__block b3-text-field"; 10 | textarea.rows = 5; 11 | textarea.style.fontSize = '20px'; 12 | textarea.value = sql.value; 13 | dv.addele(textarea); 14 | 15 | const button = document.createElement('button'); 16 | button.className = "fn__block b3-button"; 17 | button.textContent = "Execute"; 18 | dv.addele(button); 19 | 20 | dv.addtable(searchResult(), { 21 | fullwidth: false, 22 | cols: null, 23 | renderer: (b, a) => b[a] 24 | }); 25 | 26 | button.onclick = async () => { 27 | const ans = await Query.sql(textarea.value); 28 | sql.value = textarea.value; 29 | searchResult(ans); 30 | dv.repaint(); 31 | } 32 | 33 | dv.render(); 34 | } 35 | 36 | return query(); -------------------------------------------------------------------------------- /public/example/exp-today-updated.js: -------------------------------------------------------------------------------- 1 | //!js 2 | const now = Query.Utils.today(false); 3 | const query = async () => { 4 | let dv = Query.DataView(protyle, item, top); 5 | const todayState = dv.useState('today', now); //Only update the state once. 6 | 7 | let updatedState = dv.useState('updated-docs', []); 8 | if (now === todayState.value) { 9 | dv.addmd('#### Today\'s Updated Documents'); 10 | let updatedDoc = await Query.sql(` 11 | select * from blocks where type='d' and updated like '${todayState.value}%' 12 | order by updated desc 13 | `); 14 | dv.addtable(updatedDoc, { 15 | fullwidth: true, 16 | cols: ['box', 'hpath', 'updated'], 17 | }); 18 | let state = updatedDoc.omit('ial', 'path', 'hash', 'fcontent'); 19 | updatedState(state); 20 | } else { 21 | dv.addmd(`#### Updated Documents on ${todayState.value}`) 22 | dv.addtable(updatedState(), { 23 | fullwidth: true, 24 | cols: ['box', 'hpath', 'updated'], 25 | }); 26 | } 27 | 28 | dv.render(); 29 | } 30 | 31 | return query(); -------------------------------------------------------------------------------- /public/i18n/README.md: -------------------------------------------------------------------------------- 1 | 思源支持的 i18n 文件范围,可以在控制台 `siyuan.config.langs` 中查看。以下是目前(2024-10-24)支持的语言方案: 2 | 3 | The range of i18n files supported by SiYuan can be viewed in the console under `siyuan.config.langs`. Below are the language schemes currently supported as of now (October 24, 2024) : 4 | 5 | ```js 6 | >>> siyuan.config.langs.map( lang => lang.name) 7 | ['de_DE', 'en_US', 'es_ES', 'fr_FR', 'he_IL', 'it_IT', 'ja_JP', 'pl_PL', 'ru_RU', 'zh_CHT', 'zh_CN'] 8 | ``` 9 | 10 | 在插件开发中,默认使用 JSON 格式作为国际化(i18n)的载体文件。如果您更喜欢使用 YAML 语法,可以将 JSON 文件替换为 YAML 文件(例如 `en_US.yaml`),并在其中编写 i18n 文本。本模板提供了相关的 Vite 插件,可以在编译时自动将 YAML 文件转换为 JSON 文件(请参见 `/yaml-plugin.js`)。本 MD 文件 和 YAML 文件会在 `npm run build` 时自动从 `dist` 目录下删除,仅保留必要的 JSON 文件共插件系统使用。 11 | 12 | In plugin development, JSON format is used by default as the carrier file for internationalization (i18n). If you prefer to use YAML syntax, you can replace the JSON file with a YAML file (e.g., `en_US.yaml`) and write the i18n text within it. This template provides a related Vite plugin that can automatically convert YAML files to JSON files during the compilation process (see `/yaml-plugin.js`). This markdown file and YAML files will be automatically removed from the `dist` directory during `npm run build`, leaving only the necessary JSON files for plugin system to use. 13 | -------------------------------------------------------------------------------- /public/i18n/en_US.yaml: -------------------------------------------------------------------------------- 1 | src_core_dataviewts: 2 | blank: Blank 3 | src_core_editorts: 4 | ext_code_editor_closed: The external code editor process {0} has been closed 5 | show_as_template_format: Display as template format 6 | show_embedded_block_code_in_siyuan_template_format: Display embedded block code 7 | in SiYuan template format 8 | unableto_use_ext_cmd: Unable to use the external editor command {0}, please check 9 | the correctness of the command 10 | unableto_use_external_editor_cmd: Unable to use the external editor command {0}, 11 | please check the correctness of the command 12 | unusableexteditorcmd: Unable to use the external editor command {0}, please check 13 | the correctness of the command 14 | src_core_indexts: 15 | custom_queryview_error: 'Note: There is a problem with the format of the custom 16 | QueryView script, and it cannot be imported normally!' 17 | reload_custom_comp: Reload Custom 18 | src_core_queryts: 19 | query_obsolete_params: The calling parameters of Query.{0} are deprecated, please 20 | change to the new usage scheme; for details, refer to the documentation after 21 | QueryView v1.2.0 22 | src_dataquery_componentsts: 23 | mermaid_render_failed: Mermaid rendering failed, please check the code 24 | src_dataquery_editorts: 25 | onlydesktop: Only supported to open in the code editor in the desktop environment 26 | src_indexts: 27 | incompatible_version: The current SiYuan version {0} is incompatible well with the 28 | QueryView plugin. It is recommended to downgrade or upgrade SiYuan to another 29 | version (other than v3.1.25 or v3.1.26). 30 | manual_release: Manual Release 31 | manual_release_desc: 'Manually release (dv.dispose) all DataView
Note: Unless 32 | under special circumstances, generally you don''t need to do this' 33 | plugin_not_working: QueryView cannot function properly 34 | src_setting_indexts: 35 | api_interface: 📃 API interface 36 | apitypedefinition: 'The type definition of the API interface, please refer to:' 37 | defaultcolumnsofdataviewtable: When calling DataView.table, the default columns 38 | to be displayed, separated by commas; leaving it blank means showing all columns 39 | echarts_renderer: Echarts renderer 40 | echarts_renderer_option: Echarts renderer, which can be "canvas" or "svg", default 41 | is "svg" 42 | import_failed: Import failed. Please check the console error for details. 43 | import_success: Import succeeded. There are a total of {cnt} custom views. 44 | local_command_desc: The local command used to open the code editor, by default it 45 | is "code -w {{filepath}}" which represents opening with VSCode, where {{filepath}} 46 | will be replaced with the actual code file at runtime 47 | open_local_editor: ✒️ Open Local Editor 48 | plugin_import_help_doc: The plugin allows the help documentation to be imported 49 | into SiYuan. By default, the complete README document and TypeScript Reference 50 | will be imported. If this option is enabled, only the Reference part will be imported 51 | during the import and no other parts. 52 | reload: Reload 53 | table_default_columns: 🔑 Default columns displayed in table view 54 | user_custom_view: User custom view 55 | user_doc_import_type_ref: User documentation only imports type references 56 | user_self_written_view: User self-written View component, stored under '/data/public/custom-query-view.js' 57 | src_userhelp_examplests: 58 | backlinks_table: Display backlinks of the current document in a table format 59 | daily_quote: Daily sentence, this example uses state, so only one quote will be 60 | displayed per day; note that this example uses an API found randomly online, which 61 | may not be stable, just consider it a case study 62 | doc_backlinks_grouping: View the backlinks of the current document grouped by the 63 | type of reference blocks, and display them in a collapsible list 64 | doc_outline_tree: Query the outline of the current document and display it in a 65 | tree structure 66 | docs_per_month: Query the number of documents created each month and display them 67 | using an echarts line chart 68 | echarts_graph_ref: Using Echarts Graph to Display Backlink Reference Blocks in the 69 | Current Document 70 | list_doc_subsections: List all the sub-documents of the current document. The effect 71 | is similar to software such as notion 72 | query_attr_views: Query all attribute views under the current document and summarize 73 | them in an embedded block 74 | query_doc_tree: Query the document tree structure under the current document and 75 | display it using a nested list; the maximum depth is controlled by the MAX_DEPTH 76 | variable. 77 | query_this_month_todo: Query all unfinished TODO lists of this month 78 | query_unfinished_tasks: Query all unfinished task blocks and arrange them horizontally 79 | grouped by monthly timelines 80 | random_text_translate: Randomly select a piece of text from SiYuan and translate 81 | it into English using GPT, with the GPT API set internally in SiYuan 82 | recent_docs: 'Displays the 32 most recently updated documents. This example uses 83 | SiYuan''s super block markdown dialect syntax to horizontally display multiple 84 | columns of information within the same list item. 85 | 86 | ' 87 | show_tags_card_view: Query and display all tags in card view 88 | simple_chatgpt: A very simple ChatGPT dialog box, using the GPT API set internally 89 | in SiYuan 90 | sql_exec_result: Enter an SQL statement in the input box, click the execute button, 91 | and display the execution result in a table format 92 | unfinished_task_monthly: Query unfinished Tasks for each month and display them 93 | in summary on the board 94 | updated_docs_today: 'Query all documents updated today and display them in a list 95 | format; this example uses state, so after today, the table will no longer update 96 | and will only display cached records. In the actual use process, in fact, it is 97 | more recommended to use it with a template. When creating, directly configure 98 | "now" as the date of the current day instead of maintaining the date status through 99 | "state". 100 | 101 | ' 102 | view_assets_images: View all images in the assets directory by pagination 103 | src_userhelp_indexts: 104 | create_notebook: Please create at least one notebook to store the help documents 105 | download: Download 106 | edit_custom_view: Edit Code File 107 | help_doc: Help Document 108 | help_doc_2: Help Document 109 | open_custom_view_dir: Open Directory 110 | open_locally: Open local 111 | queryview: Query View Basic Template 112 | unable_open_custom_view: Unable to open custom view JS file 113 | unable_open_custom_view_dir: Unable to open custom view directory 114 | unable_open_d_ts: Unable to open d.ts file 115 | useview: If you want to use DataView, please uncomment the following line 116 | useview2: If you want to use DataView, please comment out the above return and uncomment 117 | the following two lines 118 | src_userhelp_sydocts: 119 | create_user_doc: 'Create user documentation:' 120 | plugin_setting_doc: '> 🖊️ Due to your settings in the plugin, this help document 121 | only contains the type definitions of the API.' 122 | plugin_update_doc: Detected plugin version update, updating documentation, please 123 | wait... 124 | user_help: 125 | ahead_hint: | 126 | > 💡 Note: This document is automatically generated by the SiYuan Query&View plugin **{{version}}** for the plugin's README documentation. 127 | > You can place this document anywhere in SiYuan. If the plugin fails to find this document when it needs to be opened, it will automatically create a new one. 128 | > Please do not record anything valuable to you in this document, as the information recorded here is likely to be lost when the user document is recreated! 129 | -------------------------------------------------------------------------------- /public/i18n/zh_CN.yaml: -------------------------------------------------------------------------------- 1 | src_core_dataviewts: 2 | blank: 空白 3 | src_core_editorts: 4 | ext_code_editor_closed: 外部代码编辑器进程 {0} 已关闭 5 | show_as_template_format: 显示为模板格式 6 | show_embedded_block_code_in_siyuan_template_format: 以思源模板的格式显示嵌入块代码 7 | unableto_use_ext_cmd: 无法使用外部编辑器命令 {0}, 请检查命令的正确性 8 | unableto_use_external_editor_cmd: 无法使用外部编辑器命令 {0}, 请检查命令的正确性 9 | unusableexteditorcmd: 无法使用外部编辑器命令 {0}, 请检查命令的正确性 10 | src_core_indexts: 11 | custom_queryview_error: '注意: 自定义的 QueryView 脚本格式存在问题,无法正常导入!' 12 | reload_custom_comp: 重载自定义组件 13 | src_core_queryts: 14 | query_obsolete_params: Query.{0} 的调用参数已经过时, 请变更为新的使用方案; 具体请参考 QueryView v1.2.0 之后的文档 15 | src_dataquery_componentsts: 16 | mermaid_render_failed: Mermaid 渲染失败, 请检查代码 17 | src_dataquery_editorts: 18 | onlydesktop: 仅在桌面环境下支持在代码编辑器中打开 19 | src_indexts: 20 | incompatible_version: 当前思源版本{0}和 QueryView 插件不兼容,建议降级或升级思源到(v3.1.25 v3.1.26 之外的)另一个版本 21 | manual_release: 手动释放 22 | manual_release_desc: '手动释放 (dv.dispose) 所有 DataView
注意: 除非特殊情况, 一般来说你不需要这么做' 23 | plugin_not_working: QueryView 插件无法正常运行 24 | src_setting_indexts: 25 | api_interface: 📃 API 接口 26 | apitypedefinition: 'API 接口的类型定义,请参考:' 27 | defaultcolumnsofdataviewtable: 当调用 DataView.table 时, 默认显示的列, 以逗号分隔; 留空则表示显示所有列 28 | echarts_renderer: Echarts 渲染器 29 | echarts_renderer_option: Echarts 渲染器,可以为 "canvas" 或 "svg", 默认 "svg" 30 | import_failed: 导入失败, 详细情况请检查控制台报错 31 | import_success: 导入成功, 共 {cnt} 个自定义视图 32 | local_command_desc: 本地用于打开代码编辑器的命令, 默认为 "code -w {{filepath}}" 代表使用 VSCode 打开, 其中 33 | {{filepath}} 在运行时会被替换为真实的代码文件 34 | open_local_editor: ✒️ 打开本地编辑器 35 | plugin_import_help_doc: 插件允许将帮助文档导入到思源笔记中,默认情况下会导入完整的 README 文档以及 Tyepscript Reference;如果开启了这个选项,那么在导入的时候将只导入 36 | Reference 部分而不会导入其余部分 37 | reload: 重新加载 38 | table_default_columns: 🔑 表格视图默认显示列 39 | user_custom_view: 用户自定义视图 40 | user_doc_import_type_ref: 用户文档只导入类型参考 41 | user_self_written_view: 用户自行编写的 View 组件, 存放在 '/data/public/custom-query-view.js' 42 | 下 43 | src_userhelp_examplests: 44 | backlinks_table: 以表格的形式显示当前文档的回链 45 | daily_quote: 每日一句,这个案例中用到了 state,所以每天只会显示一条句子;不过注意这个例子中用到了随便从网上找到 API,不一定稳定,当个案例看看就行 46 | doc_backlinks_grouping: 按照引用块的类型,分组查看当前文档的反向链接,并放入折叠列表中展示 47 | doc_outline_tree: 查询当前文档的大纲,并以树状结构展示 48 | docs_per_month: 查询每个月创建的文档的数量,并使用 echarts 折线图展示出来 49 | echarts_graph_ref: 使用 Echarts Graph 展示当前文档的反链引用块 50 | list_doc_subsections: 列出当前文档的所有子文裆,效果类似 notion 等软件 51 | query_attr_views: 查询所在文档下,所有的属性视图 (Attribute View) 然后汇总显示在嵌入块中 52 | query_doc_tree: 查询当前文档下属的文档树结构,并使用嵌套列表展示; 最大深度由 MAX_DEPTH 变量控制 53 | query_this_month_todo: 查询本月所有未完成的 TODO 列表 54 | query_unfinished_tasks: 查询所有未完成的任务块,以月份时间线分组横向排列 55 | random_text_translate: 随机从思源中选取一段文字,然后使用 GPT 翻译成英文,使用思源内部设置的 GPT API 56 | recent_docs: 展示最近更新的 32 篇文档,这个案例中使用了思源的超级块的 markdown 方言语法,用于在同一个列表项中横向展示多列的信息 57 | show_tags_card_view: 查询并以卡片视图的形式展示所有的标签 (tags) 58 | simple_chatgpt: 一个非常简单的 ChatGPT 对话框,使用思源内部设置的 GPT API 59 | sql_exec_result: 在输入框中输入 SQL 语句,点击执行按钮,将执行结果以表格的形式展示 60 | unfinished_task_monthly: 查询每个月尚未完成的 Task,汇总显示在看板上 61 | updated_docs_today: '查询今天更新的所有文档,并以列表的形式展示;这个案例中使用了 state,今天之后将不会再更新表格而只会显示缓存的记录。 62 | 实际使用过程中,其实更加建议配合模板使用,在创建的时候直接配置 now 为当天的日期,而非通过 state 来维护日期状态。 63 | 64 | ' 65 | view_assets_images: 分页查看 assets 目录下所有的图片 66 | src_userhelp_indexts: 67 | create_notebook: 请至少先创建一个笔记本用于存放帮助文档 68 | download: 下载 69 | edit_custom_view: 编辑代码 70 | help_doc: 帮助文档 71 | help_doc_2: 帮助文档 72 | open_custom_view_dir: 打开目录 73 | open_locally: 在本地打开 74 | queryview: Query View 基本模板 75 | unable_open_custom_view: 无法打开自定义视图 JS 文件 76 | unable_open_custom_view_dir: 无法打开自定义视图目录 77 | unable_open_d_ts: 无法打开 d.ts 文件 78 | useview: 如果要使用 DataView 请取消下面这行的注释 79 | useview2: 如果要使用 DataView 请注释上面的 return, 并取消下方两行注释 80 | src_userhelp_sydocts: 81 | create_user_doc: '创建用户文档:' 82 | plugin_setting_doc: '> 🖊️ 由于你在插件中的设置,此帮助文档只包含了 API 的类型定义。' 83 | plugin_update_doc: 检查到插件版本已更新,正在更新文档,请稍等... 84 | user_help: 85 | ahead_hint: | 86 | > 💡 注: 本文档由 SiYuan Query&View 插件 **{{version}}** 版自动生成,为插件的 README 文档。 87 | > 你可以将本文档放在思源的任何地方,如果插件在需要打开的时候未能找到本文档,将会自动创建一个新的文档。 88 | > 请不要在这个文档中记录任何对你有价值的东西,这些记录在本文档中的信息很有可能随着重新创建用户文档而丢失! 89 | -------------------------------------------------------------------------------- /scripts/.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | build 3 | dist 4 | *.exe 5 | *.spec 6 | -------------------------------------------------------------------------------- /scripts/elevate.ps1: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024 by frostime. All Rights Reserved. 2 | # @Author : frostime 3 | # @Date : 2024-09-06 19:15:53 4 | # @FilePath : /scripts/elevate.ps1 5 | # @LastEditTime : 2024-09-06 19:39:13 6 | # @Description : Force to elevate the script to admin privilege. 7 | 8 | param ( 9 | [string]$scriptPath 10 | ) 11 | 12 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition 13 | $projectDir = Split-Path -Parent $scriptDir 14 | 15 | if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { 16 | $args = "-NoProfile -ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`" -scriptPath `"" + $scriptPath + "`"" 17 | Start-Process powershell.exe -Verb RunAs -ArgumentList $args -WorkingDirectory $projectDir 18 | exit 19 | } 20 | 21 | Set-Location -Path $projectDir 22 | & node $scriptPath 23 | 24 | pause 25 | -------------------------------------------------------------------------------- /scripts/export-types.js: -------------------------------------------------------------------------------- 1 | // import { glob } from 'glob'; 2 | import { execSync } from 'child_process'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { fileURLToPath } from 'url'; 6 | import process from 'process'; 7 | 8 | // First define __filename and __dirname 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | // Then use process 13 | process.chdir(path.join(__dirname, '..')); 14 | console.log(process.cwd()); 15 | const dirname = process.cwd(); 16 | 17 | // 读取 plugin.json 18 | const pluginJson = fs.readFileSync('./plugin.json', 'utf8'); 19 | const plugin = JSON.parse(pluginJson); 20 | 21 | const removeTypesDir = () => { 22 | if (fs.existsSync('./types')) { 23 | fs.rmSync('./types', { recursive: true, force: true }); 24 | console.debug('remove ./types'); 25 | } 26 | } 27 | 28 | removeTypesDir(); 29 | 30 | let outputDir = './public'; 31 | 32 | const tsc = `tsc --declaration --emitDeclarationOnly --skipLibCheck --target ESNext --project tsconfig.json --outDir ./types --noEmitOnError false --stripInternal`; 33 | 34 | console.log(tsc); 35 | execSync(tsc); 36 | 37 | 38 | const fileWriter = (filepath) => { 39 | let fd = fs.openSync(filepath, 'w'); 40 | 41 | return { 42 | append: (content) => { 43 | fs.writeSync(fd, content); 44 | }, 45 | close: () => { 46 | fs.closeSync(fd); 47 | } 48 | } 49 | } 50 | 51 | const readFile = (filepath) => { 52 | filepath = path.join(__dirname, '..', filepath); 53 | return fs.readFileSync(filepath, 'utf8'); 54 | } 55 | 56 | const removeLine = (content, line) => { 57 | const lines = content.split('\n'); 58 | lines.splice(lines.indexOf(line), 1); 59 | return lines.join('\n'); 60 | } 61 | 62 | const writer = fileWriter(path.join(outputDir, 'types.d.ts')); 63 | const replaceSomething = (content, lines = []) => { 64 | if (lines.length > 0) { 65 | lines.forEach(line => { 66 | content = content.replaceAll(line, ''); 67 | }); 68 | } 69 | content = content.replaceAll(`import("./proxy").`, ''); 70 | content = content.replaceAll(`import("@/core/query").default;`, 'Query'); 71 | return content; 72 | } 73 | 74 | writer.append(` 75 | /** 76 | * @name ${plugin.name} 77 | * @author ${plugin.author} 78 | * @version ${plugin.version} 79 | * @updated ${new Date().toISOString()} 80 | */ 81 | 82 | declare module 'siyuan' { 83 | interface IProtyle { 84 | [key: string]: any; 85 | } 86 | } 87 | 88 | /** 89 | * Send siyuan kernel request 90 | */ 91 | declare function request(url: string, data: any): Promise; 92 | 93 | `.trimStart()); 94 | 95 | writer.append('///@query.d.ts\n'); 96 | let query = readFile('./types/core/query.d.ts'); 97 | query = replaceSomething(query, [ 98 | 'import { request } from "@/api";', 99 | 'import { IWrappedBlock, IWrappedList } from "./proxy";' 100 | ]); 101 | writer.append(query); 102 | writer.append('\n'); 103 | 104 | writer.append('///@data-view-types.d.ts\n'); 105 | let dataviewdts = readFile('./src/types/data-view.d.ts'); 106 | dataviewdts = replaceSomething(dataviewdts); 107 | writer.append(dataviewdts); 108 | writer.append('\n'); 109 | 110 | writer.append('///@data-view.d.ts\n'); 111 | let dataview = readFile('./types/core/data-view.d.ts'); 112 | dataview = removeLine(dataview, 'import { IProtyle } from "siyuan";'); 113 | dataview = replaceSomething(dataview); 114 | dataview = dataview.replaceAll('DataView extends UseStateMixin', 'DataView'); 115 | writer.append(dataview); 116 | writer.append('\n'); 117 | 118 | // writer.append('// ================== use-state.d.ts ==================\n'); 119 | // let useState = readFile('./types/core/use-state.d.ts'); 120 | // useState = removeLine(useState, 'import { IProtyle } from "siyuan";'); 121 | // useState = replaceSomething(useState); 122 | // writer.append(useState); 123 | // writer.append('\n'); 124 | 125 | writer.append('///@proxy.d.ts\n'); 126 | let proxy = readFile('./types/core/proxy.d.ts'); 127 | proxy = replaceSomething(proxy); 128 | writer.append(proxy); 129 | writer.append('\n'); 130 | 131 | writer.append('///@index.d.ts\n'); 132 | let indexdts = readFile('./src/types/index.d.ts'); 133 | indexdts = replaceSomething(indexdts); 134 | writer.append(indexdts); 135 | writer.append('\n'); 136 | 137 | writer.close(); 138 | 139 | const cache = { 140 | '{{Query}}': query, 141 | '{{DataView}}': dataview + '\n\n' + dataviewdts, 142 | '{{Proxy}}': proxy, 143 | } 144 | // 写入 json 145 | fs.writeFileSync('./types/types.d.ts.json', JSON.stringify(cache, null, 2)); 146 | 147 | //format 148 | let content = fs.readFileSync(path.join(outputDir, 'types.d.ts'), 'utf8'); 149 | const ERASED_LINES = [ 150 | 'import { DataView } from "./data-view";', 151 | 'export default Query;', 152 | 'import UseStateMixin from "./use-state";', 153 | ]; 154 | 155 | for (const line of ERASED_LINES) { 156 | content = removeLine(content, line); 157 | } 158 | 159 | const lines = content.split('\n'); 160 | 161 | for (let i = 0; i < lines.length - 1; i++) { 162 | if (lines[i].trim() === '}') { 163 | if (lines[i + 1].trim() !== '') { 164 | lines.splice(i + 1, 0, ''); 165 | } 166 | } 167 | } 168 | 169 | // removeTypesDir(); 170 | fs.writeFileSync(path.join(outputDir, 'types.d.ts'), lines.join('\n')); 171 | console.log(`${outputDir}/types.d.ts generated`); 172 | -------------------------------------------------------------------------------- /scripts/git-tag.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-11 14:35:13 5 | * @FilePath : /scripts/git-tag.js 6 | * @LastEditTime : 2024-12-11 14:41:11 7 | * @Description : 8 | */ 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | import { fileURLToPath } from 'url'; 12 | import process from 'process'; 13 | import readline from'readline'; 14 | import child_process from 'child_process'; 15 | 16 | 17 | // First define __filename and __dirname 18 | const __filename = fileURLToPath(import.meta.url); 19 | const __dirname = path.dirname(__filename); 20 | 21 | 22 | const confirm = async (question) => { 23 | const userConfirm = readline.createInterface({ 24 | input: process.stdin, 25 | output: process.stdout 26 | }); 27 | return new Promise((resolve) => { 28 | userConfirm.question(question, (answer) => { 29 | userConfirm.close(); 30 | resolve(answer.toLowerCase() === 'y'); 31 | }); 32 | }); 33 | } 34 | 35 | 36 | // Then use process 37 | process.chdir(path.join(__dirname, '..')); 38 | console.log(process.cwd()); 39 | const dirname = process.cwd(); 40 | 41 | const pluginJson = JSON.parse(fs.readFileSync(path.join(dirname, 'plugin.json'), 'utf-8')); 42 | const tagName = `v${pluginJson.version}`; 43 | 44 | // 检查 tag 是否存在, 如果存在就删掉 45 | const gitTagListCommand = `git tag -l ${tagName}`; 46 | console.log(gitTagListCommand); 47 | const gitTagListResult = child_process.execSync(gitTagListCommand, { encoding: 'utf-8' }); 48 | console.log(gitTagListResult); 49 | if (gitTagListResult.trim() === tagName) { 50 | console.log(`Tag ${tagName} already exists, it first.`); 51 | let flag = await confirm(`Do you want to delete the tag ${tagName}? (y/n) `); 52 | if (flag) { 53 | const gitTagDeleteCommand = `git tag -d ${tagName}`; 54 | console.log(gitTagDeleteCommand); 55 | const gitTagDeleteResult = child_process.execSync(gitTagDeleteCommand, { encoding: 'utf-8' }); 56 | console.log(gitTagDeleteResult); 57 | } else { 58 | console.log('Aborted.'); 59 | process.exit(0); 60 | } 61 | } 62 | 63 | // git tag 64 | const gitTagCommand = `git tag ${tagName}`; 65 | console.log(gitTagCommand); 66 | const gitTagResult = child_process.execSync(gitTagCommand, { encoding: 'utf-8' }); 67 | console.log(gitTagResult); 68 | 69 | 70 | let upload = await confirm(`Do you want to upload the tag ${tagName} to the remote repository? (y/n) `); 71 | if (upload) { 72 | const gitPushCommand = `git push origin ${tagName}`; 73 | console.log(gitPushCommand); 74 | const gitPushResult = child_process.execSync(gitPushCommand, { encoding: 'utf-8' }); 75 | console.log(gitPushResult); 76 | } 77 | -------------------------------------------------------------------------------- /scripts/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-09-06 17:42:57 5 | * @FilePath : /scripts/utils.js 6 | * @LastEditTime : 2024-09-06 19:23:12 7 | * @Description : 8 | */ 9 | // common.js 10 | import fs from 'fs'; 11 | import path from 'node:path'; 12 | import http from 'node:http'; 13 | import readline from 'node:readline'; 14 | 15 | // Logging functions 16 | export const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); 17 | export const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); 18 | 19 | // HTTP POST headers 20 | export const POST_HEADER = { 21 | "Content-Type": "application/json", 22 | }; 23 | 24 | // Fetch function compatible with older Node.js versions 25 | export async function myfetch(url, options) { 26 | return new Promise((resolve, reject) => { 27 | let req = http.request(url, options, (res) => { 28 | let data = ''; 29 | res.on('data', (chunk) => { 30 | data += chunk; 31 | }); 32 | res.on('end', () => { 33 | resolve({ 34 | ok: true, 35 | status: res.statusCode, 36 | json: () => JSON.parse(data) 37 | }); 38 | }); 39 | }); 40 | req.on('error', (e) => { 41 | reject(e); 42 | }); 43 | req.end(); 44 | }); 45 | } 46 | 47 | /** 48 | * Fetch SiYuan workspaces from port 6806 49 | * @returns {Promise} 50 | */ 51 | export async function getSiYuanDir() { 52 | let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; 53 | let conf = {}; 54 | try { 55 | let response = await myfetch(url, { 56 | method: 'POST', 57 | headers: POST_HEADER 58 | }); 59 | if (response.ok) { 60 | conf = await response.json(); 61 | } else { 62 | error(`\tHTTP-Error: ${response.status}`); 63 | return null; 64 | } 65 | } catch (e) { 66 | error(`\tError: ${e}`); 67 | error("\tPlease make sure SiYuan is running!!!"); 68 | return null; 69 | } 70 | return conf?.data; // 保持原始返回值 71 | } 72 | 73 | /** 74 | * Choose target workspace 75 | * @param {{path: string}[]} workspaces 76 | * @returns {string} The path of the selected workspace 77 | */ 78 | export async function chooseTarget(workspaces) { 79 | let count = workspaces.length; 80 | log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`); 81 | workspaces.forEach((workspace, i) => { 82 | log(`\t[${i}] ${workspace.path}`); 83 | }); 84 | 85 | if (count === 1) { 86 | return `${workspaces[0].path}/data/plugins`; 87 | } else { 88 | const rl = readline.createInterface({ 89 | input: process.stdin, 90 | output: process.stdout 91 | }); 92 | let index = await new Promise((resolve) => { 93 | rl.question(`\tPlease select a workspace[0-${count - 1}]: `, (answer) => { 94 | resolve(answer); 95 | }); 96 | }); 97 | rl.close(); 98 | return `${workspaces[index].path}/data/plugins`; 99 | } 100 | } 101 | 102 | /** 103 | * Check if two paths are the same 104 | * @param {string} path1 105 | * @param {string} path2 106 | * @returns {boolean} 107 | */ 108 | export function cmpPath(path1, path2) { 109 | path1 = path1.replace(/\\/g, '/'); 110 | path2 = path2.replace(/\\/g, '/'); 111 | if (path1[path1.length - 1] !== '/') { 112 | path1 += '/'; 113 | } 114 | if (path2[path2.length - 1] !== '/') { 115 | path2 += '/'; 116 | } 117 | return path1 === path2; 118 | } 119 | 120 | export function getThisPluginName() { 121 | if (!fs.existsSync('./plugin.json')) { 122 | process.chdir('../'); 123 | if (!fs.existsSync('./plugin.json')) { 124 | error('Failed! plugin.json not found'); 125 | return null; 126 | } 127 | } 128 | 129 | const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); 130 | const name = plugin?.name; 131 | if (!name) { 132 | error('Failed! Please set plugin name in plugin.json'); 133 | return null; 134 | } 135 | 136 | return name; 137 | } 138 | 139 | export function copyDirectory(srcDir, dstDir) { 140 | if (!fs.existsSync(dstDir)) { 141 | fs.mkdirSync(dstDir); 142 | log(`Created directory ${dstDir}`); 143 | } 144 | 145 | fs.readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { 146 | const src = path.join(srcDir, file.name); 147 | const dst = path.join(dstDir, file.name); 148 | 149 | if (file.isDirectory()) { 150 | copyDirectory(src, dst); 151 | } else { 152 | fs.copyFileSync(src, dst); 153 | log(`Copied file: ${src} --> ${dst}`); 154 | } 155 | }); 156 | log(`All files copied!`); 157 | } 158 | 159 | 160 | export function makeSymbolicLink(srcPath, targetPath) { 161 | if (!fs.existsSync(targetPath)) { 162 | // fs.symlinkSync(srcPath, targetPath, 'junction'); 163 | //Go 1.23 no longer supports junctions as symlinks 164 | //Please refer to https://github.com/siyuan-note/siyuan/issues/12399 165 | fs.symlinkSync(srcPath, targetPath, 'dir'); 166 | log(`Done! Created symlink ${targetPath}`); 167 | return; 168 | } 169 | 170 | //Check the existed target path 171 | let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); 172 | if (!isSymbol) { 173 | error(`Failed! ${targetPath} already exists and is not a symbolic link`); 174 | return; 175 | } 176 | let existedPath = fs.readlinkSync(targetPath); 177 | if (cmpPath(existedPath, srcPath)) { 178 | log(`Good! ${targetPath} is already linked to ${srcPath}`); 179 | } else { 180 | error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${existedPath}`); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/core/custom-view.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-02 22:54:07 5 | * @FilePath : /src/core/custom-view.ts 6 | * @LastEditTime : 2024-12-03 14:43:59 7 | * @Description : 8 | */ 9 | 10 | import { putFile } from "@/api"; 11 | import { PROHIBIT_METHOD_NAMES } from "@/core/data-view"; 12 | 13 | const blankContent = ` 14 | /** 15 | This script is used for sy-query-view plugin to define user's customized view components. Type declarations as follows: 16 | 17 | interface ICustomView { 18 | use: (dv: IDataView) => { 19 | render: (container: HTMLElement, ...args: any[]) => HTMLElement; //Create the user custom view. 20 | dispose?: () => void; // Unmount hook for the user custom view. 21 | }, 22 | alias?: string[]; // Alias name for the custom view 23 | } 24 | 25 | Once correctly registerd, you can use the custom view like this: 26 | 27 | dv.addexample(1); 28 | dv.addcols([dv.table(childs), dv.Example(2)]); //use alias 29 | */ 30 | 31 | const custom = { 32 | example: { 33 | use: () => { 34 | let state; 35 | return { 36 | render: (element, id) => { 37 | console.log('init example custom view with id:', id); 38 | state = id; 39 | element.innerHTML = 'This is a example custom view ' + id; 40 | }, 41 | dispose: () => { 42 | console.log('dispose example custom view ' + state); 43 | } 44 | }; 45 | }, 46 | alias: ['Example', 'ExampleView'] 47 | } 48 | } 49 | 50 | export default custom; 51 | `.trimStart(); 52 | 53 | let customView: IUserCustom; 54 | 55 | const filename = 'query-view.custom.js'; 56 | export const filepath = `/data/public/${filename}`; 57 | 58 | const createCustomModuleFile = async () => { 59 | const file = new File([blankContent], filename, { type: 'text/javascript' }); 60 | const res = await putFile( 61 | filepath, 62 | false, 63 | file 64 | ); 65 | return res; 66 | } 67 | 68 | const validateCustomView = (module: IUserCustom) => { 69 | if (typeof module !== 'object') { 70 | console.warn('Invalid custom query-view module format, should be an object'); 71 | return false; 72 | } 73 | 74 | for (const [key, value] of Object.entries(module)) { 75 | if (PROHIBIT_METHOD_NAMES.includes(key)) { 76 | console.warn(`Invalid custom query-view module format, ${key} is a prohibited method name`); 77 | return false; 78 | } 79 | const view = value as ICustomView; 80 | if (!view.use || typeof view.use !== 'function') { 81 | console.warn(`Invalid custom query-view module format, ${key} should have a use method`); 82 | return false; 83 | } 84 | const { render, dispose } = view.use(null); 85 | if (typeof render !== 'function') { 86 | console.warn(`Invalid custom query-view module format, ${key} render should be a function`); 87 | return false; 88 | } 89 | if (dispose && typeof dispose !== 'function') { 90 | console.warn(`Invalid custom query-view module format, ${key} dispose should be a function`); 91 | return false; 92 | } 93 | } 94 | return true; 95 | } 96 | 97 | export const loadUserCustomView = async () => { 98 | const result = { 99 | ok: false, 100 | exists: false, 101 | valid: false, 102 | custom: null 103 | } 104 | 105 | let url: string | undefined; 106 | try { 107 | const response = await fetch(filepath.replace('/data', ''), { 108 | cache: 'no-cache' 109 | }); // /data 是路径, 路由则不需要这个前缀 110 | if (!response.ok) { 111 | result.exists = false; 112 | throw new Error(`Failed to fetch custom JS file: ${response.statusText}`); 113 | } 114 | result.exists = true; 115 | const jsContent = await response.text(); 116 | 117 | const blob = new Blob([jsContent], { type: 'text/javascript' }); 118 | url = URL.createObjectURL(blob); 119 | 120 | const module = await import(url); 121 | const custom = module.default; 122 | const validateResult = validateCustomView(custom); 123 | if (validateResult) { 124 | customView = custom; 125 | result.valid = true; 126 | result.custom = custom; 127 | } else { 128 | result.valid = false; 129 | } 130 | } catch (error) { 131 | console.warn('Failed to load custom JS file:', error); 132 | if (!result.exists) { 133 | try { 134 | await createCustomModuleFile(); 135 | } catch (createError) { 136 | console.error('Failed to create custom module file:', createError); 137 | } 138 | } 139 | } finally { 140 | if (url) { 141 | URL.revokeObjectURL(url); 142 | } 143 | } 144 | result.ok = result.exists && result.valid; 145 | return result; 146 | } 147 | 148 | export const getCustomView = () => customView; 149 | -------------------------------------------------------------------------------- /src/core/editor.ts: -------------------------------------------------------------------------------- 1 | // import { getBlockByID } from "@/api"; 2 | import { showMessage } from "siyuan"; 3 | 4 | import { updateBlock } from "@/api"; 5 | import { debounce } from "@/utils"; 6 | 7 | import { setting } from "@/setting"; 8 | 9 | import { i18n } from "@/index"; 10 | import { inputDialog, simpleDialog } from "@/libs/dialog"; 11 | 12 | const child_process = require("child_process"); 13 | 14 | declare global { 15 | interface Window { 16 | monaco: any; 17 | } 18 | } 19 | 20 | const editJsCode = async (blockId: BlockId, code: string) => { 21 | // const block = await getBlockByID(blockId); 22 | 23 | // let code = element.dataset.content; 24 | code = window.Lute.UnEscapeHTMLStr(code); 25 | 26 | // const elementRef = new WeakRef(element); 27 | 28 | /** 29 | * 更新内核的块数据 30 | * @param code 31 | */ 32 | const updateBlockData = async (code: string) => { 33 | const embedBlock = '{{' + code.replaceAll('\n', '_esc_newline_') + '}}'; 34 | updateBlock('markdown', embedBlock, blockId); 35 | } 36 | const updateBlockDataDebounced = debounce(updateBlockData, 1500); 37 | 38 | //桌面环境, 可以访问 node 环境 39 | if (child_process) { 40 | const os = require('os'); 41 | const path = require('path'); 42 | const fs = require('fs'); 43 | const ext = code.startsWith('//!js') ? 'js' : 'sql'; 44 | const filePath = path.join(os.tmpdir(), `siyuan-${blockId}-${Date.now()}.${ext}`); 45 | 46 | // 写入文件 47 | fs.writeFileSync(filePath, code); 48 | let editor: any; 49 | let watcher: any; 50 | const cleanUp = () => { 51 | watcher?.close(); 52 | try { 53 | fs.unlinkSync(filePath); // 删除临时文件 54 | } catch (e) { 55 | console.error('清理临时文件失败:', e); 56 | } 57 | } 58 | 59 | const codeEditor = setting.codeEditor; 60 | //codeEditor 为一个命令行, 其中 {{filepath}} 会被替换为真实的文件路径 61 | const input = codeEditor.replace('{{filepath}}', filePath); 62 | const commandArr = input.split(' ').map(item => item.trim()).filter(item => item !== ''); 63 | 64 | // 添加调试信息 65 | console.debug('About to execute command:', { 66 | command: input, 67 | commandArr, 68 | codeEditor: setting.codeEditor 69 | }); 70 | let command = commandArr[0]; 71 | 72 | try { 73 | editor = child_process.spawn(command, commandArr.slice(1), { 74 | shell: true 75 | }); 76 | } catch (e) { 77 | console.error('启动代码编辑器失败:', e); 78 | showMessage(i18n.src_core_editorts.unusableexteditorcmd.replace('{0}', input), 5000, 'error'); 79 | cleanUp(); 80 | return; 81 | } 82 | 83 | editor.on('error', (err: any) => { 84 | console.error('代码编辑器错误:', err); 85 | showMessage(i18n.src_core_editorts.unusableexteditorcmd.replace('{0}', input)); 86 | cleanUp(); 87 | }); 88 | 89 | editor.on('exit', () => { 90 | showMessage(i18n.src_core_editorts.ext_code_editor_closed.replace('{0}', commandArr[0])); 91 | console.log('代码编辑器已关闭'); 92 | try { 93 | const updatedContent = fs.readFileSync(filePath, 'utf-8'); 94 | updateBlockDataDebounced(updatedContent); 95 | } catch (e) { 96 | console.error('读取文件失败:', e); 97 | } 98 | cleanUp(); 99 | }); 100 | 101 | // 监听文件变化 102 | watcher = fs.watch(filePath, (eventType, _filename) => { 103 | if (eventType === 'change') { 104 | try { 105 | const updatedContent = fs.readFileSync(filePath, 'utf-8'); 106 | updateBlockDataDebounced(updatedContent); 107 | } catch (e) { 108 | console.error('读取文件失败:', e); 109 | } 110 | } 111 | }); 112 | } else { 113 | showMessage(i18n.src_dataquery_editorts.onlydesktop, 3000, 'error'); 114 | } 115 | } 116 | 117 | export async function embedBlockEvent({ detail }: any) { 118 | if (detail.blockElements.length > 1) { 119 | return; 120 | } 121 | let ele: HTMLDivElement = detail.blockElements[0]; 122 | // const protyleRef = new WeakRef(detail.protyle); 123 | let type = ele.getAttribute("data-type"); 124 | if (type !== "NodeBlockQueryEmbed") { 125 | return; 126 | } 127 | 128 | let id = ele.getAttribute("data-node-id"); 129 | let menu = detail.menu; 130 | menu.addItem({ 131 | icon: "iconGit", 132 | label: "Edit Code", 133 | click: () => { 134 | editJsCode(id, ele.dataset.content); 135 | } 136 | }); 137 | menu.addItem({ 138 | icon: "iconMarkdown", 139 | label: i18n.src_core_editorts.show_as_template_format, 140 | click: () => { 141 | let code = ele.dataset.content; 142 | code = window.Lute.UnEscapeHTMLStr(code); 143 | //换行符全部替换为 `_esc_newline_` 144 | code = code.replace(/\n/g, '_esc_newline_'); 145 | const textarea = document.createElement('textarea'); 146 | textarea.value = `{{${code}}}`; 147 | textarea.style.flex = "1"; 148 | textarea.style.fontSize = "16px"; 149 | textarea.style.margin = "15px 10px"; 150 | textarea.style.borderRadius = "5px"; 151 | textarea.style.resize = "none"; 152 | // 不可编辑 153 | textarea.setAttribute("readonly", "true"); 154 | 155 | simpleDialog({ 156 | title: i18n.src_core_editorts.show_embedded_block_code_in_siyuan_template_format, 157 | ele: textarea, 158 | width: "700px", 159 | height: "300px" 160 | }); 161 | } 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /src/core/finalize.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-01 11:21:44 5 | * @FilePath : /src/core/finalize.ts 6 | * @LastEditTime : 2025-03-13 22:14:04 7 | * @Description : 8 | */ 9 | import { DataView } from "./data-view"; 10 | 11 | // 其实用 WeakRef 也没啥必要的。。 12 | const dataviews = new Map[]>(); 13 | 14 | // const sessionStorageKeys = new Map(); 15 | 16 | // const registry = new FinalizationRegistry((docId: string) => { 17 | // const views = dataviews.get(docId); 18 | // if (views) { 19 | // views.forEach(view => { 20 | // const dataView = view.deref(); 21 | // if (dataView) { 22 | // console.debug(`FinalizationRegistry dispose dataView@${dataView.embed_id} under doc@${dataView.root_id}`); 23 | // dataView.dispose(); 24 | // } 25 | // }); 26 | // dataviews.delete(docId); 27 | // } 28 | // }); 29 | 30 | /** 31 | * 获取 sessionStorage 占用的大小 32 | * @returns 33 | */ 34 | export function getSessionStorageSize() { 35 | let totalSize = 0; 36 | for (let key in sessionStorage) { 37 | if (sessionStorage.hasOwnProperty(key)) { 38 | let value = sessionStorage.getItem(key); 39 | totalSize += value.length * 2; // 每个字符占用 2 字节(UTF-16) 40 | } 41 | } 42 | 43 | function formatSize(size) { 44 | if (size < 1024) { 45 | return size + ' bytes'; 46 | } else if (size < 1024 * 1024) { 47 | return (size / 1024).toFixed(2) + ' KB'; 48 | } else { 49 | return (size / (1024 * 1024)).toFixed(2) + ' MB'; 50 | } 51 | } 52 | return formatSize(totalSize); 53 | } 54 | 55 | 56 | 57 | /** 58 | * Register DataView for Garbage Collection on Document Closed 59 | * @param docId 60 | * @param dataView 61 | */ 62 | export const registerProtyleGC = (docId: DocumentId, dataView: DataView) => { 63 | if (!dataviews.has(docId)) { 64 | dataviews.set(docId, []); 65 | } 66 | const views = dataviews.get(docId); 67 | 68 | // 检查是否已经存在相同的 embed_id 69 | const existingIndex = views.findIndex(ref => { 70 | const view = ref.deref(); 71 | return view && view.embed_id === dataView.embed_id; 72 | }); 73 | 74 | // 如果存在,替换旧的引用 75 | if (existingIndex !== -1) { 76 | views[existingIndex] = new WeakRef(dataView); 77 | } else { 78 | // 如果不存在,添加新的引用 79 | views.push(new WeakRef(dataView)); 80 | } 81 | } 82 | 83 | const finalizeDataView = async (dataView: DataView, rootID?: string) => { 84 | console.debug(`[finalize.ts] Finalize dataView@${dataView.embed_id} under doc@${rootID}`); 85 | await dataView.flushStateIntoBlockAttr(); 86 | dataView.dispose(); 87 | //保证之后如果有其他设备的数据同步给当前设备,则新打开的时候会从 element 而非 session 中读取 88 | dataView.removeFromSessionStorage(); 89 | } 90 | 91 | export const onProtyleDestroyed = ({ detail }) => { 92 | const rootID = detail.protyle.block.rootID; 93 | 94 | if (!dataviews.has(rootID)) return; 95 | 96 | const views = dataviews.get(rootID); 97 | console.group(`[finalize.ts] onProtyleDestroyed for doc@${rootID} (${views.length} views)`); 98 | views.forEach(view => { 99 | const dataView = view.deref(); 100 | if (dataView) { 101 | finalizeDataView(dataView, rootID); 102 | } 103 | }); 104 | console.groupEnd(); 105 | dataviews.delete(rootID); 106 | } 107 | 108 | export const onProtyleSwitch = ({ detail }) => { 109 | //TODO 在 switch 的时候, 同样 finalize 110 | } 111 | 112 | export const finalizeAllDataviews = () => { 113 | console.group(`[finalize.ts] finalizeAllDataviews (${dataviews.size} views)`); 114 | dataviews.forEach((views, docId) => { 115 | views.forEach(async view => { 116 | const dataView = view.deref(); 117 | if (dataView) { 118 | await finalizeDataView(dataView, docId); 119 | } 120 | }); 121 | }); 122 | console.groupEnd(); 123 | } 124 | -------------------------------------------------------------------------------- /src/core/index.module.scss: -------------------------------------------------------------------------------- 1 | .data-query-embed { 2 | cursor: default; 3 | } 4 | 5 | .data-view-component { 6 | margin-top: 5px; 7 | margin-bottom: 5px; 8 | padding-left: 0.5em; 9 | padding-right: 0.5em; 10 | // overflow-x: hidden; 11 | } 12 | 13 | .markdown-component { 14 | :global(.protyle-action) { 15 | // display: none !important; 16 | cursor: default; 17 | pointer-events: none !important; 18 | } 19 | } 20 | 21 | .embed-container[data-node-id] { 22 | border: 1px solid var(--b3-border-color); 23 | padding: 0.5em; 24 | border-radius: 0; 25 | margin-top: 0px; 26 | margin-bottom: 0px; 27 | box-sizing: border-box; 28 | /* gap: 0px; */ 29 | 30 | & a.embed-jump-icon { 31 | opacity: 0; 32 | position: absolute; 33 | top: 0.5em; 34 | right: 0.5em; 35 | border-radius: 50%; 36 | cursor: pointer; 37 | font-size: 0.85rem; 38 | text-decoration: none; 39 | 40 | height: 2em; 41 | width: 2em; 42 | 43 | &>svg { 44 | width: 100%; 45 | height: 100%; 46 | } 47 | 48 | &:hover { 49 | opacity: 0.9; 50 | background-color: var(--b3-theme-primary-lightest); 51 | text-decoration: none; 52 | } 53 | } 54 | 55 | & .embed-more-svg { 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | margin-top: 0; 60 | height: 1em; 61 | 62 | &>svg { 63 | height: 100%; 64 | transform: rotate(90deg); 65 | opacity: 0.6; 66 | } 67 | 68 | &:hover { 69 | background-color: var(--b3-theme-primary-lightest); 70 | } 71 | } 72 | } 73 | 74 | .remove-mermaid { 75 | /* 缓慢删除, 向下移动退场 */ 76 | transition: opacity 1s ease-in-out, transform 1s ease-in-out; 77 | } 78 | 79 | .mindmap-node-siyuan { 80 | cursor: pointer; 81 | } 82 | 83 | .columns { 84 | 85 | --flex-grow: 1; 86 | --col-min-width: 350px; 87 | --col-gap: 5px; 88 | 89 | gap: var(--col-gap); 90 | 91 | display: flex; 92 | flex-wrap: nowrap; 93 | min-width: min-content; 94 | 95 | // 所有直接子元素作为列 96 | >.column { 97 | flex-grow: var(--flex-grow); 98 | min-width: var(--col-min-width); // 最小列宽 99 | word-break: break-word; 100 | } 101 | } 102 | 103 | .rows { 104 | // --flex-grow: initial; 105 | --row-gap: 5px; 106 | 107 | gap: var(--row-gap); 108 | 109 | display: flex; 110 | flex-direction: column; 111 | flex-wrap: nowrap; 112 | 113 | > .row { 114 | flex-grow: var(--flex-grow); 115 | word-break: break-word; 116 | } 117 | } 118 | 119 | .katex-center-display { 120 | display: flex; 121 | justify-content: center; 122 | margin: 1em 0; 123 | } 124 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by zxhd863943427, frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-05-08 15:00:37 5 | * @FilePath : /src/core/index.ts 6 | * @LastEditTime : 2025-03-25 16:57:20 7 | * @Description : 8 | * - Fork from project https://github.com/zxhd863943427/siyuan-plugin-data-query 9 | * - 基于该项目的 v0.0.7 版本进行修改 10 | */ 11 | // import type FMiscPlugin from "@/index"; 12 | import { 13 | IMenu, 14 | showMessage, 15 | } from "siyuan"; 16 | 17 | import { embedBlockEvent } from "./editor"; 18 | import Query from "./query"; 19 | import { finalizeAllDataviews, onProtyleDestroyed } from "./finalize"; 20 | import { loadUserCustomView, filepath as customViewFilePath } from "./custom-view"; 21 | import { setting } from "../setting"; 22 | import type QueryViewPlugin from ".."; 23 | import { i18n } from ".."; 24 | 25 | const child_process = window?.require?.('child_process'); 26 | 27 | 28 | const openCustomViewDirectory = (open: 'file' | 'dir') => { 29 | if (!child_process) return; 30 | const workspaceDir = window.siyuan.config.system.workspaceDir; 31 | 32 | const path = window?.require('path'); 33 | const jsPath = path.join(workspaceDir, customViewFilePath); 34 | const dirPath = path.dirname(jsPath); 35 | 36 | const electron = window?.require?.('electron'); 37 | if (!electron) { 38 | let msg = open === 'file' ? i18n.src_userhelp_indexts.unable_open_custom_view : i18n.src_userhelp_indexts.unable_open_custom_view_dir; 39 | showMessage(msg, 3000, 'error'); 40 | return; 41 | } 42 | if (open === 'file') { 43 | // electron?.shell.openPath(jsPath); 44 | const command = setting.codeEditor.replace('{{filepath}}', jsPath); 45 | child_process.exec(command, (err, stdout, stderr) => { 46 | if (err) { 47 | console.warn('Error executing command:', err); 48 | } else { 49 | // console.debug('Command executed successfully:', stdout); 50 | } 51 | }); 52 | } else { 53 | electron?.shell.openPath(dirPath); 54 | } 55 | } 56 | 57 | const load = (plugin: QueryViewPlugin) => { 58 | 59 | globalThis.Query = Query; 60 | 61 | plugin.eventBus.on("click-blockicon", embedBlockEvent); 62 | //关闭页签的时候,finailize 内部的 dataview 63 | plugin.eventBus.on("destroy-protyle", onProtyleDestroyed); 64 | 65 | loadUserCustomView().then(status => { 66 | if (status.exists && !status.valid) { 67 | //@ts-ignore 68 | showMessage(plugin.i18n.src_core_indexts.custom_queryview_error, 5000, 'error'); 69 | return; 70 | } 71 | if (status.ok) { 72 | console.debug(`Load custom query-view: ${Object.keys(status.custom)}`); 73 | } 74 | }); 75 | 76 | const submenus: IMenu[] = [ 77 | { 78 | //@ts-ignore 79 | label: i18n.src_core_indexts.reload_custom_comp, 80 | icon: 'iconAccount', 81 | click: async () => { 82 | const result = await loadUserCustomView(); 83 | if (result.ok) { 84 | let cnt = Object.keys(result.custom).length; 85 | showMessage(i18n.src_setting_indexts.import_success.replace('{cnt}', `${cnt}`), 3000, 'info'); 86 | } else { 87 | showMessage(i18n.src_setting_indexts.import_failed, 3000, 'error'); 88 | } 89 | } 90 | } 91 | ]; 92 | 93 | 94 | if (child_process) { 95 | submenus.push({ 96 | label: i18n.src_userhelp_indexts.edit_custom_view, 97 | icon: 'iconCode', 98 | click: () => { 99 | try { 100 | openCustomViewDirectory('file'); 101 | } catch (error) { 102 | console.error(error); 103 | showMessage(i18n.src_userhelp_indexts.unable_open_custom_view, 3000, 'error'); 104 | } 105 | } 106 | }); 107 | submenus.push({ 108 | label: i18n.src_userhelp_indexts.open_custom_view_dir, 109 | icon: 'iconFolder', 110 | click: () => { 111 | try { 112 | openCustomViewDirectory('dir'); 113 | } catch (error) { 114 | console.error(error); 115 | showMessage(i18n.src_userhelp_indexts.unable_open_custom_view_dir, 3000, 'error'); 116 | } 117 | } 118 | }) 119 | } 120 | 121 | plugin.registerMenuItem({ 122 | label: 'CustomView', 123 | icon: 'iconCode', 124 | submenu: submenus 125 | }); 126 | } 127 | 128 | const unload = (plugin: QueryViewPlugin) => { 129 | 130 | finalizeAllDataviews(); 131 | 132 | delete globalThis.Query; 133 | 134 | plugin.eventBus.off("click-blockicon", embedBlockEvent); 135 | plugin.eventBus.off("destroy-protyle", onProtyleDestroyed); 136 | } 137 | 138 | export { 139 | load, unload, 140 | finalizeAllDataviews 141 | }; 142 | -------------------------------------------------------------------------------- /src/core/lute.ts: -------------------------------------------------------------------------------- 1 | import { ILute, setLute } from "@/utils/lute"; 2 | 3 | let lute: ILute = null; 4 | 5 | export const initLute = () => { 6 | if (lute) return; 7 | lute = setLute({}); 8 | } 9 | 10 | export const getLute = () => { 11 | if (!lute) { 12 | initLute(); 13 | } 14 | return lute; 15 | } 16 | -------------------------------------------------------------------------------- /src/core/use-state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-03 19:49:52 5 | * @FilePath : /src/core/use-state.ts 6 | * @LastEditTime : 2024-12-08 19:38:54 7 | * @Description : 8 | */ 9 | import { setBlockAttrs } from "@/api"; 10 | import { debounce } from "@/utils"; 11 | // import { debounce } from "@/utils"; 12 | 13 | export const purgeSessionStorage = () => { 14 | const keys = new Array(sessionStorage.length).fill(0).map((_, i) => sessionStorage.key(i)); 15 | keys.forEach(key => { 16 | if (key.startsWith('dv-state@')) { 17 | sessionStorage.removeItem(key); 18 | } 19 | }); 20 | } 21 | 22 | class UseStateMixin { 23 | /** @internal */ 24 | protected stateMap: Map = new Map(); 25 | /** @internal */ 26 | private blockId: BlockId; 27 | /** @internal */ 28 | private node: WeakRef; 29 | 30 | constructor(node: HTMLElement) { 31 | this.blockId = node.dataset.nodeId; 32 | this.node = new WeakRef(node); 33 | this.restoreState(); // 从块属性中恢复 state 34 | } 35 | 36 | 37 | /** @internal */ 38 | private getState(key: string) { 39 | return this.stateMap.get(key); 40 | } 41 | 42 | /** @internal */ 43 | private setState(key: string, value: any) { 44 | this.stateMap.set(key, value); 45 | } 46 | 47 | protected sessionStorageKey = () => `dv-state@${this.blockId}`; 48 | static customStateKey = (key: string) => `custom-dv-state-${key}`; 49 | 50 | /** @internal 51 | * 首先尝试从 sessionStorage 中恢复 state, 如果 sessionStorage 中没有数据, 则从 element 的属性中查找恢复 52 | */ 53 | protected restoreState() { 54 | //先尝试从 sessionStorage 中恢复 55 | const storage = sessionStorage.getItem(this.sessionStorageKey()); 56 | if (storage) { 57 | this.stateMap = new Map(Object.entries(JSON.parse(storage))); 58 | } else { 59 | //如果没有数据, 就从 element 的属性中查找恢复 60 | const customStateAttrs = Array.from( 61 | this.node.deref()?.attributes ?? [] 62 | ).filter(attr => attr.name.startsWith(UseStateMixin.customStateKey(''))); 63 | customStateAttrs.forEach(attr => { 64 | this.stateMap.set(attr.name.replace(UseStateMixin.customStateKey(''), ''), JSON.parse(attr.value)); 65 | }); 66 | } 67 | } 68 | 69 | protected async saveToBlockAttrs() { 70 | const stateObj: Record = {}; 71 | this.stateMap.forEach((value, key) => { 72 | stateObj[UseStateMixin.customStateKey(key)] = JSON.stringify(value); 73 | }); 74 | console.debug('saveToBlockAttrs', this.blockId, stateObj); 75 | await setBlockAttrs(this.blockId, stateObj); 76 | } 77 | 78 | // protected saveToBlockDebounced = debounce(this.saveToBlockAttrs.bind(this), 1000); 79 | // private saveToBlockThrottled = throttle(this.saveToBlockAttrs.bind(this), 1000); 80 | 81 | public get hasState() { 82 | return this.stateMap.size > 0; 83 | } 84 | 85 | 86 | protected saveToSessionStorage() { 87 | if (!this.hasState) { 88 | return; 89 | } 90 | const storageObj: Record = {}; 91 | this.stateMap.forEach((value, key) => { 92 | storageObj[key] = value; 93 | }); 94 | sessionStorage.setItem(this.sessionStorageKey(), JSON.stringify(storageObj)); 95 | } 96 | 97 | public removeFromSessionStorage() { 98 | sessionStorage.removeItem(this.sessionStorageKey()); 99 | } 100 | 101 | useState(key: string, initialValue?: T): IState { 102 | if (!this.stateMap.has(key)) { 103 | this.setState(key, initialValue); 104 | } 105 | 106 | const registeredEffects: ((newValue: T, oldValue: T) => void)[] = []; 107 | 108 | const getter = () => this.getState(key); 109 | const setter = (value: any) => { 110 | this.setState(key, value); 111 | this.saveToSessionStorage(); 112 | registeredEffects.forEach(effect => effect(value, this.getState(key))); 113 | } 114 | 115 | const state = (value?: any) => { 116 | if (value !== undefined) { 117 | setter(value); 118 | } 119 | return getter(); 120 | }; 121 | Object.defineProperty(state, 'value', { 122 | get: getter, 123 | set: setter, 124 | }); 125 | 126 | Object.defineProperty(state, 'effect', { 127 | value: (effect: (newValue: T, oldValue: T) => void) => { 128 | registeredEffects.push(effect); 129 | }, 130 | }); 131 | Object.defineProperty(state, 'derived', { 132 | value: (derive: (value: T) => T) => { 133 | return () => derive(state()); 134 | }, 135 | }); 136 | 137 | 138 | return state as IState; 139 | } 140 | } 141 | 142 | export default UseStateMixin; 143 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-11-28 21:29:40 5 | * @FilePath : /src/core/utils.ts 6 | * @LastEditTime : 2025-03-13 19:36:01 7 | * @Description : 8 | */ 9 | 10 | import { Constants } from "siyuan"; 11 | import styles from './index.module.scss'; 12 | 13 | //https://github.com/siyuan-note/siyuan/blob/master/app/src/protyle/util/addScript.ts 14 | export const addScript = (path: string, id: string) => { 15 | return new Promise((resolve) => { 16 | if (document.getElementById(id)) { 17 | // 脚本加载后再次调用直接返回 18 | resolve(false); 19 | return false; 20 | } 21 | const scriptElement = document.createElement("script"); 22 | scriptElement.src = path; 23 | scriptElement.async = true; 24 | // 循环调用时 Chrome 不会重复请求 js 25 | document.head.appendChild(scriptElement); 26 | scriptElement.onload = () => { 27 | if (document.getElementById(id)) { 28 | // 循环调用需清除 DOM 中的 script 标签 29 | scriptElement.remove(); 30 | resolve(false); 31 | return false; 32 | } 33 | scriptElement.id = id; 34 | resolve(true); 35 | }; 36 | }); 37 | }; 38 | 39 | 40 | // https://github.com/siyuan-note/siyuan/blob/master/app/src/protyle/util/addStyle.ts 41 | export const addStyle = (url: string, id: string) => { 42 | if (!document.getElementById(id)) { 43 | const styleElement = document.createElement("link"); 44 | styleElement.id = id; 45 | styleElement.rel = "stylesheet"; 46 | styleElement.type = "text/css"; 47 | styleElement.href = url; 48 | const pluginsStyle = document.querySelector("#pluginsStyle"); 49 | if (pluginsStyle) { 50 | pluginsStyle.before(styleElement); 51 | } else { 52 | document.getElementsByTagName("head")[0].appendChild(styleElement); 53 | } 54 | } 55 | }; 56 | 57 | export const matchIDFormat = (id: string) => { 58 | let match = id.match(/^\d{14}-[a-z0-9]{7}$/); 59 | if (match) { 60 | return true; 61 | } else { 62 | return false; 63 | } 64 | } 65 | 66 | /** 67 | * Deep merges two objects, handling arrays and nested objects 68 | * @param source The original object 69 | * @param target The object to merge in 70 | * @returns The merged object 71 | */ 72 | export function deepMerge(source: T, target: Partial | any): T { 73 | // Handle null/undefined cases 74 | if (!source) return target as T; 75 | if (!target) return source; 76 | 77 | const result = { ...source }; 78 | 79 | Object.keys(target).forEach(key => { 80 | const sourceValue = source[key]; 81 | const targetValue = target[key]; 82 | 83 | // Skip undefined values 84 | if (targetValue === undefined) return; 85 | 86 | // Handle null values 87 | if (targetValue === null) { 88 | result[key] = null; 89 | return; 90 | } 91 | 92 | // Handle arrays 93 | if (Array.isArray(targetValue)) { 94 | result[key] = Array.isArray(sourceValue) 95 | ? sourceValue.map((item, index) => { 96 | return targetValue[index] !== undefined 97 | ? (typeof item === 'object' && typeof targetValue[index] === 'object') 98 | ? deepMerge(item, targetValue[index]) 99 | : targetValue[index] 100 | : item; 101 | }).concat(targetValue.slice(sourceValue?.length || 0)) 102 | : [...targetValue]; 103 | return; 104 | } 105 | 106 | // Handle objects 107 | if (typeof targetValue === 'object' && Object.keys(targetValue).length > 0) { 108 | result[key] = typeof sourceValue === 'object' 109 | ? deepMerge(sourceValue, targetValue) 110 | : targetValue; 111 | return; 112 | } 113 | 114 | // Handle primitive values 115 | result[key] = targetValue; 116 | }); 117 | 118 | return result; 119 | } 120 | 121 | 122 | export const initKatex = async () => { 123 | if (window.katex) return; 124 | // https://github.com/siyuan-note/siyuan/blob/master/app/src/protyle/render/mathRender.ts 125 | const cdn = Constants.PROTYLE_CDN; 126 | addStyle(`${cdn}/js/katex/katex.min.css`, "protyleKatexStyle"); 127 | await addScript(`${cdn}/js/katex/katex.min.js`, "protyleKatexScript"); 128 | return window.katex !== undefined && window.katex !== null; 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frostime/sy-query-view/944f561744d64d1f67f6599b2ffdd1ecaaa8e766/src/index.scss -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-01 15:57:28 5 | * @FilePath : /src/index.ts 6 | * @LastEditTime : 2025-04-07 20:24:04 7 | * @Description : 8 | */ 9 | import { 10 | confirm, 11 | IMenu, 12 | Menu, 13 | Plugin, 14 | showMessage, 15 | type App, 16 | } from "siyuan"; 17 | import "@/index.scss"; 18 | import * as DataQuery from "./core"; 19 | import * as Setting from "./setting"; 20 | import * as UserHelp from "./user-help"; 21 | import { confirmDialog, siyuanVersion } from "@frostime/siyuan-plugin-kits"; 22 | import { simpleDialog } from "./libs/dialog"; 23 | 24 | let i18n: I18n; 25 | let app: App; 26 | 27 | const onClose = () => { 28 | console.debug('Close Window') 29 | DataQuery.finalizeAllDataviews(); 30 | } 31 | 32 | export default class QueryViewPlugin extends Plugin { 33 | 34 | disposeCb = []; 35 | 36 | private topBarMenuItems: IMenu[] = []; 37 | 38 | declare version: string; 39 | 40 | private init() { 41 | this.protyleSlash = []; 42 | this.addIcons(ICON); 43 | const showMenu = () => { 44 | let menu = new Menu("query-view"); 45 | this.topBarMenuItems.forEach(item => { 46 | menu.addItem(item); 47 | }); 48 | 49 | menu.addSeparator(); 50 | 51 | // Register the dispose all views menu item 52 | menu.addItem({ 53 | label: 'Manual Dispose', 54 | icon: 'iconTrashcan', 55 | click: () => { 56 | confirmDialog({ 57 | title: i18n.src_indexts.manual_release, 58 | content: i18n.src_indexts.manual_release_desc, 59 | confirm: () => { 60 | DataQuery.finalizeAllDataviews(); 61 | showMessage('All views have been disposed', 3000, 'info'); 62 | } 63 | }) 64 | } 65 | }) 66 | 67 | // setting 68 | menu.addItem({ 69 | icon: 'iconSettings', 70 | label: 'Setting', 71 | click: () => { 72 | this.openSetting(); 73 | } 74 | }); 75 | 76 | const rect = topbar.getBoundingClientRect(); 77 | menu.open({ 78 | x: rect.left, 79 | y: rect.bottom, 80 | isLeft: false 81 | }); 82 | } 83 | 84 | const topbar = this.addTopBar({ 85 | icon: 'iconQueryView', 86 | title: 'Query&View', 87 | position: 'left', 88 | callback: showMenu 89 | }); 90 | } 91 | 92 | registerMenuItem(item: IMenu) { 93 | this.topBarMenuItems.push(item); 94 | } 95 | 96 | async onload() { 97 | i18n = this.i18n as unknown as I18n; 98 | //@ts-ignore 99 | const version = siyuanVersion(); 100 | // if (version.version === '3.1.25' || version.version === '3.1.26') { 101 | if (version.version === '3.1.25' || version.version === '3.1.26') { 102 | const text = '⚠️' + i18n.src_indexts.incompatible_version.replace('{0}', version.version); 103 | if (version.version === '3.1.25') { 104 | simpleDialog({ 105 | title: i18n.src_indexts.plugin_not_working, 106 | ele: `
${text}
` 107 | }); 108 | return; 109 | } else { 110 | showMessage(text, -1, 'error'); 111 | } 112 | } 113 | app = this.app; 114 | this.init(); 115 | 116 | Setting.load(this); 117 | DataQuery.load(this); 118 | UserHelp.load(this); 119 | } 120 | 121 | async onunload() { 122 | DataQuery.unload(this); 123 | this.disposeCb.forEach(f => f()); 124 | } 125 | 126 | onLayoutReady(): void { 127 | //Ctrl + F5 刷新的时候起作用 128 | window.addEventListener('beforeunload', onClose); 129 | this.disposeCb.push(() => { 130 | window.removeEventListener('beforeunload', onClose); 131 | }) 132 | 133 | 134 | const closeEle = document.querySelector('div#windowControls > div#closeWindow'); 135 | if (closeEle) { 136 | closeEle.addEventListener('click', onClose, true); 137 | this.disposeCb.push(() => { 138 | closeEle.removeEventListener('click', onClose, true); 139 | }) 140 | } 141 | } 142 | } 143 | 144 | const ICON = ` 145 | 146 | `; 147 | 148 | export { i18n, app }; 149 | -------------------------------------------------------------------------------- /src/libs/const.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-06-08 20:36:30 5 | * @FilePath : /src/libs/const.ts 6 | * @LastEditTime : 2025-03-09 21:55:03 7 | * @Description : 8 | */ 9 | 10 | 11 | export const BlockType2NodeType: { [key in BlockType]: string } = { 12 | d: 'NodeDocument', 13 | p: 'NodeParagraph', 14 | query_embed: 'NodeBlockQueryEmbed', 15 | l: 'NodeList', 16 | i: 'NodeListItem', 17 | h: 'NodeHeading', 18 | iframe: 'NodeIFrame', 19 | tb: 'NodeThematicBreak', 20 | b: 'NodeBlockquote', 21 | s: 'NodeSuperBlock', 22 | c: 'NodeCodeBlock', 23 | widget: 'NodeWidget', 24 | t: 'NodeTable', 25 | html: 'NodeHTMLBlock', 26 | m: 'NodeMathBlock', 27 | av: 'NodeAttributeView', 28 | audio: 'NodeAudio' 29 | } 30 | 31 | 32 | export const NodeIcons = { 33 | NodeAttributeView: { 34 | icon: "iconDatabase" 35 | }, 36 | NodeAudio: { 37 | icon: "iconRecord" 38 | }, 39 | NodeBlockQueryEmbed: { 40 | icon: "iconSQL" 41 | }, 42 | NodeBlockquote: { 43 | icon: "iconQuote" 44 | }, 45 | NodeCodeBlock: { 46 | icon: "iconCode" 47 | }, 48 | NodeDocument: { 49 | icon: "iconFile" 50 | }, 51 | NodeHTMLBlock: { 52 | icon: "iconHTML5" 53 | }, 54 | NodeHeading: { 55 | icon: "iconHeadings", 56 | subtypes: { 57 | h1: { icon: "iconH1" }, 58 | h2: { icon: "iconH2" }, 59 | h3: { icon: "iconH3" }, 60 | h4: { icon: "iconH4" }, 61 | h5: { icon: "iconH5" }, 62 | h6: { icon: "iconH6" } 63 | } 64 | }, 65 | NodeIFrame: { 66 | icon: "iconLanguage" 67 | }, 68 | NodeList: { 69 | subtypes: { 70 | o: { icon: "iconOrderedList" }, 71 | t: { icon: "iconCheck" }, 72 | u: { icon: "iconList" } 73 | } 74 | }, 75 | NodeListItem: { 76 | icon: "iconListItem" 77 | }, 78 | NodeMathBlock: { 79 | icon: "iconMath" 80 | }, 81 | NodeParagraph: { 82 | icon: "iconParagraph" 83 | }, 84 | NodeSuperBlock: { 85 | icon: "iconSuper" 86 | }, 87 | NodeTable: { 88 | icon: "iconTable" 89 | }, 90 | NodeThematicBreak: { 91 | icon: "iconLine" 92 | }, 93 | NodeVideo: { 94 | icon: "iconVideo" 95 | }, 96 | NodeWidget: { 97 | icon: "iconBoth" 98 | } 99 | }; 100 | 101 | 102 | export const getBlockTypeIcon = (block: Block) => { 103 | const nodeType = BlockType2NodeType[block.type]; 104 | const icon = NodeIcons[nodeType]; 105 | if (!icon) { 106 | return ''; 107 | } 108 | 109 | if (block.subtype) { 110 | const subtype = icon.subtypes?.[block.subtype]; 111 | if (subtype) { 112 | return subtype.icon; 113 | } 114 | } 115 | 116 | return icon.icon; 117 | } 118 | -------------------------------------------------------------------------------- /src/libs/dialog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-03-23 21:37:33 5 | * @FilePath : /src/libs/dialog.ts 6 | * @LastEditTime : 2024-12-03 15:35:47 7 | * @Description : Kits about dialogs 8 | */ 9 | import { Dialog } from "siyuan"; 10 | 11 | export const inputDialog = (args: { 12 | title: string, placeholder?: string, defaultText?: string, 13 | confirm?: (text: string) => void, cancel?: () => void, 14 | width?: string, height?: string 15 | }) => { 16 | const dialog = new Dialog({ 17 | title: args.title, 18 | content: `
19 |
20 |
21 |
22 |
23 | 24 |
`, 25 | width: args.width ?? "520px", 26 | height: args.height 27 | }); 28 | const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea"); 29 | const btnsElement = dialog.element.querySelectorAll(".b3-button"); 30 | btnsElement[0].addEventListener("click", () => { 31 | if (args?.cancel) { 32 | args.cancel(); 33 | } 34 | dialog.destroy(); 35 | }); 36 | btnsElement[1].addEventListener("click", () => { 37 | if (args?.confirm) { 38 | args.confirm(target.value); 39 | } 40 | dialog.destroy(); 41 | }); 42 | }; 43 | 44 | export const inputDialogSync = async (args: { 45 | title: string, placeholder?: string, defaultText?: string, 46 | width?: string, height?: string 47 | }) => { 48 | return new Promise((resolve) => { 49 | let newargs = { 50 | ...args, confirm: (text) => { 51 | resolve(text); 52 | }, cancel: () => { 53 | resolve(null); 54 | } 55 | }; 56 | inputDialog(newargs); 57 | }); 58 | } 59 | 60 | 61 | interface IConfirmDialogArgs { 62 | title: string; 63 | content: string | HTMLElement; 64 | confirm?: (ele?: HTMLElement) => void; 65 | cancel?: (ele?: HTMLElement) => void; 66 | width?: string; 67 | height?: string; 68 | } 69 | 70 | export const confirmDialog = (args: IConfirmDialogArgs) => { 71 | const { title, content, confirm, cancel, width, height } = args; 72 | 73 | const dialog = new Dialog({ 74 | title, 75 | content: `
76 |
77 |
78 |
79 |
80 |
81 | 82 |
`, 83 | width: width, 84 | height: height 85 | }); 86 | 87 | const target: HTMLElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword"); 88 | if (typeof content === "string") { 89 | target.innerHTML = content; 90 | } else { 91 | target.appendChild(content); 92 | } 93 | 94 | const btnsElement = dialog.element.querySelectorAll(".b3-button"); 95 | btnsElement[0].addEventListener("click", () => { 96 | if (cancel) { 97 | cancel(target); 98 | } 99 | dialog.destroy(); 100 | }); 101 | btnsElement[1].addEventListener("click", () => { 102 | if (confirm) { 103 | confirm(target); 104 | } 105 | dialog.destroy(); 106 | }); 107 | }; 108 | 109 | 110 | export const confirmDialogSync = async (args: IConfirmDialogArgs) => { 111 | return new Promise((resolve) => { 112 | let newargs = { 113 | ...args, confirm: (ele: HTMLElement) => { 114 | resolve(ele); 115 | }, cancel: (ele: HTMLElement) => { 116 | resolve(ele); 117 | } 118 | }; 119 | confirmDialog(newargs); 120 | }); 121 | }; 122 | 123 | 124 | export const simpleDialog = (args: { 125 | title: string, ele: HTMLElement | DocumentFragment | string, 126 | width?: string, height?: string, 127 | callback?: () => void; 128 | }) => { 129 | const dialog = new Dialog({ 130 | title: args.title, 131 | content: `
`, 132 | width: args.width, 133 | height: args.height, 134 | destroyCallback: args.callback 135 | }); 136 | // dialog.element.querySelector(".dialog-content").appendChild(args.ele); 137 | if (typeof args.ele === "string") { 138 | dialog.element.querySelector(".dialog-content").innerHTML = args.ele; 139 | } else { 140 | dialog.element.querySelector(".dialog-content").appendChild(args.ele); 141 | } 142 | return { 143 | dialog, 144 | close: dialog.destroy.bind(dialog) 145 | }; 146 | } 147 | -------------------------------------------------------------------------------- /src/libs/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-04-19 18:30:12 5 | * @FilePath : /src/libs/index.d.ts 6 | * @LastEditTime : 2024-04-30 16:39:54 7 | * @Description : 8 | */ 9 | type TSettingItemType = "checkbox" | "select" | "textinput" | "textarea" | "number" | "slider" | "button" | "hint" | "custom"; 10 | 11 | interface ISettingItemCore { 12 | type: TSettingItemType; 13 | key: string; 14 | value: any; 15 | placeholder?: string; 16 | slider?: { 17 | min: number; 18 | max: number; 19 | step: number; 20 | }; 21 | options?: { [key: string | number]: string }; 22 | button?: { 23 | label: string; 24 | callback: () => void; 25 | } 26 | } 27 | 28 | interface ISettingItem extends ISettingItemCore { 29 | title: string; 30 | description: string; 31 | direction?: "row" | "column"; 32 | } 33 | 34 | 35 | //Interface for setting-utils 36 | interface ISettingUtilsItem extends ISettingItem { 37 | action?: { 38 | callback: () => void; 39 | } 40 | createElement?: (currentVal: any) => HTMLElement; 41 | getEleVal?: (ele: HTMLElement) => any; 42 | setEleVal?: (ele: HTMLElement, val: any) => void; 43 | } 44 | -------------------------------------------------------------------------------- /src/libs/promise-pool.ts: -------------------------------------------------------------------------------- 1 | export default class PromiseLimitPool { 2 | private maxConcurrent: number; 3 | private currentRunning = 0; 4 | private queue: (() => void)[] = []; 5 | private promises: Promise[] = []; 6 | 7 | constructor(maxConcurrent: number) { 8 | this.maxConcurrent = maxConcurrent; 9 | } 10 | 11 | add(fn: () => Promise): void { 12 | const promise = new Promise((resolve, reject) => { 13 | const run = async () => { 14 | try { 15 | this.currentRunning++; 16 | const result = await fn(); 17 | resolve(result); 18 | } catch (error) { 19 | reject(error); 20 | } finally { 21 | this.currentRunning--; 22 | this.next(); 23 | } 24 | }; 25 | 26 | if (this.currentRunning < this.maxConcurrent) { 27 | run(); 28 | } else { 29 | this.queue.push(run); 30 | } 31 | }); 32 | this.promises.push(promise); 33 | } 34 | 35 | async awaitAll(): Promise { 36 | return Promise.all(this.promises); 37 | } 38 | 39 | /** 40 | * Handles the next task in the queue. 41 | */ 42 | private next(): void { 43 | if (this.queue.length > 0 && this.currentRunning < this.maxConcurrent) { 44 | const nextRun = this.queue.shift()!; 45 | nextRun(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/libs/setting-utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2023-12-17 18:28:19 5 | * @FilePath : /src/libs/setting-utils.ts 6 | * @LastEditTime : 2024-12-01 17:02:07 7 | * @Description : 8 | */ 9 | 10 | import { Plugin, Setting } from 'siyuan'; 11 | 12 | 13 | /** 14 | * The default function to get the value of the element 15 | * @param type 16 | * @returns 17 | */ 18 | const createDefaultGetter = (type: TSettingItemType) => { 19 | let getter: (ele: HTMLElement) => any; 20 | switch (type) { 21 | case 'checkbox': 22 | getter = (ele: HTMLInputElement) => { 23 | return ele.checked; 24 | }; 25 | break; 26 | case 'select': 27 | case 'slider': 28 | case 'textinput': 29 | case 'textarea': 30 | getter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => { 31 | return ele.value; 32 | }; 33 | break; 34 | case 'number': 35 | getter = (ele: HTMLInputElement) => { 36 | return parseInt(ele.value); 37 | } 38 | break; 39 | default: 40 | getter = () => null; 41 | break; 42 | } 43 | return getter; 44 | } 45 | 46 | 47 | /** 48 | * The default function to set the value of the element 49 | * @param type 50 | * @returns 51 | */ 52 | const createDefaultSetter = (type: TSettingItemType) => { 53 | let setter: (ele: HTMLElement, value: any) => void; 54 | switch (type) { 55 | case 'checkbox': 56 | setter = (ele: HTMLInputElement, value: any) => { 57 | ele.checked = value; 58 | }; 59 | break; 60 | case 'select': 61 | case 'slider': 62 | case 'textinput': 63 | case 'textarea': 64 | case 'number': 65 | setter = (ele: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: any) => { 66 | ele.value = value; 67 | }; 68 | break; 69 | default: 70 | setter = () => {}; 71 | break; 72 | } 73 | return setter; 74 | 75 | } 76 | 77 | 78 | export class SettingUtils { 79 | plugin: Plugin; 80 | name: string; 81 | file: string; 82 | 83 | settings: Map = new Map(); 84 | elements: Map = new Map(); 85 | 86 | constructor(args: { 87 | plugin: Plugin, 88 | name?: string, 89 | callback?: (data: any) => void, 90 | width?: string, 91 | height?: string 92 | }) { 93 | this.name = args.name ?? 'settings'; 94 | this.plugin = args.plugin; 95 | this.file = this.name.endsWith('.json') ? this.name : `${this.name}.json`; 96 | this.plugin.setting = new Setting({ 97 | width: args.width, 98 | height: args.height, 99 | confirmCallback: () => { 100 | for (let key of this.settings.keys()) { 101 | this.updateValueFromElement(key); 102 | } 103 | let data = this.dump(); 104 | if (args.callback !== undefined) { 105 | args.callback(data); 106 | } 107 | this.plugin.data[this.name] = data; 108 | this.save(data); 109 | }, 110 | destroyCallback: () => { 111 | //Restore the original value 112 | for (let key of this.settings.keys()) { 113 | this.updateElementFromValue(key); 114 | } 115 | } 116 | }); 117 | } 118 | 119 | async load() { 120 | let data = await this.plugin.loadData(this.file); 121 | console.debug('Load config:', data); 122 | if (data) { 123 | for (let [key, item] of this.settings) { 124 | item.value = data?.[key] ?? item.value; 125 | } 126 | } 127 | this.plugin.data[this.name] = this.dump(); 128 | return data; 129 | } 130 | 131 | async save(data?: any) { 132 | data = data ?? this.dump(); 133 | await this.plugin.saveData(this.file, this.dump()); 134 | console.debug('Save config:', data); 135 | return data; 136 | } 137 | 138 | /** 139 | * read the data after saving 140 | * @param key key name 141 | * @returns setting item value 142 | */ 143 | get(key: string) { 144 | return this.settings.get(key)?.value; 145 | } 146 | 147 | /** 148 | * Set data to this.settings, 149 | * but do not save it to the configuration file 150 | * @param key key name 151 | * @param value value 152 | */ 153 | set(key: string, value: any) { 154 | let item = this.settings.get(key); 155 | if (item) { 156 | item.value = value; 157 | this.updateElementFromValue(key); 158 | } 159 | } 160 | 161 | /** 162 | * Set and save setting item value 163 | * If you want to set and save immediately you can use this method 164 | * @param key key name 165 | * @param value value 166 | */ 167 | async setAndSave(key: string, value: any) { 168 | let item = this.settings.get(key); 169 | if (item) { 170 | item.value = value; 171 | this.updateElementFromValue(key); 172 | await this.save(); 173 | } 174 | } 175 | 176 | /** 177 | * Read in the value of element instead of setting obj in real time 178 | * @param key key name 179 | * @param apply whether to apply the value to the setting object 180 | * if true, the value will be applied to the setting object 181 | * @returns value in html 182 | */ 183 | take(key: string, apply: boolean = false) { 184 | let item = this.settings.get(key); 185 | let element = this.elements.get(key) as any; 186 | if (!element) { 187 | return 188 | } 189 | if (apply) { 190 | this.updateValueFromElement(key); 191 | } 192 | return item.getEleVal(element); 193 | } 194 | 195 | /** 196 | * Read data from html and save it 197 | * @param key key name 198 | * @param value value 199 | * @return value in html 200 | */ 201 | async takeAndSave(key: string) { 202 | let value = this.take(key, true); 203 | await this.save(); 204 | return value; 205 | } 206 | 207 | /** 208 | * Disable setting item 209 | * @param key key name 210 | */ 211 | disable(key: string) { 212 | let element = this.elements.get(key) as any; 213 | if (element) { 214 | element.disabled = true; 215 | } 216 | } 217 | 218 | /** 219 | * Enable setting item 220 | * @param key key name 221 | */ 222 | enable(key: string) { 223 | let element = this.elements.get(key) as any; 224 | if (element) { 225 | element.disabled = false; 226 | } 227 | } 228 | 229 | /** 230 | * 将设置项目导出为 JSON 对象 231 | * @returns object 232 | */ 233 | dump(): Object { 234 | let data: any = {}; 235 | for (let [key, item] of this.settings) { 236 | if (item.type === 'button') continue; 237 | data[key] = item.value; 238 | } 239 | return data; 240 | } 241 | 242 | addItem(item: ISettingUtilsItem) { 243 | this.settings.set(item.key, item); 244 | const IsCustom = item.type === 'custom'; 245 | let error = IsCustom && (item.createElement === undefined || item.getEleVal === undefined || item.setEleVal === undefined); 246 | if (error) { 247 | console.error('The custom setting item must have createElement, getEleVal and setEleVal methods'); 248 | return; 249 | } 250 | 251 | if (item.getEleVal === undefined) { 252 | item.getEleVal = createDefaultGetter(item.type); 253 | } 254 | if (item.setEleVal === undefined) { 255 | item.setEleVal = createDefaultSetter(item.type); 256 | } 257 | 258 | if (item.createElement === undefined) { 259 | let itemElement = this.createDefaultElement(item); 260 | this.elements.set(item.key, itemElement); 261 | this.plugin.setting.addItem({ 262 | title: item.title, 263 | description: item?.description, 264 | direction: item?.direction, 265 | createActionElement: () => { 266 | this.updateElementFromValue(item.key); 267 | let element = this.getElement(item.key); 268 | return element; 269 | } 270 | }); 271 | } else { 272 | this.plugin.setting.addItem({ 273 | title: item.title, 274 | description: item?.description, 275 | direction: item?.direction, 276 | createActionElement: () => { 277 | let val = this.get(item.key); 278 | let element = item.createElement(val); 279 | this.elements.set(item.key, element); 280 | return element; 281 | } 282 | }); 283 | } 284 | } 285 | 286 | createDefaultElement(item: ISettingUtilsItem) { 287 | let itemElement: HTMLElement; 288 | //阻止思源内置的回车键确认 289 | const preventEnterConfirm = (e) => { 290 | if (e.key === 'Enter') { 291 | e.preventDefault(); 292 | e.stopImmediatePropagation(); 293 | } 294 | } 295 | switch (item.type) { 296 | case 'checkbox': 297 | let element: HTMLInputElement = document.createElement('input'); 298 | element.type = 'checkbox'; 299 | element.checked = item.value; 300 | element.className = "b3-switch fn__flex-center"; 301 | itemElement = element; 302 | element.onchange = item.action?.callback ?? (() => { }); 303 | break; 304 | case 'select': 305 | let selectElement: HTMLSelectElement = document.createElement('select'); 306 | selectElement.className = "b3-select fn__flex-center fn__size200"; 307 | let options = item?.options ?? {}; 308 | for (let val in options) { 309 | let optionElement = document.createElement('option'); 310 | let text = options[val]; 311 | optionElement.value = val; 312 | optionElement.text = text; 313 | selectElement.appendChild(optionElement); 314 | } 315 | selectElement.value = item.value; 316 | selectElement.onchange = item.action?.callback ?? (() => { }); 317 | itemElement = selectElement; 318 | break; 319 | case 'slider': 320 | let sliderElement: HTMLInputElement = document.createElement('input'); 321 | sliderElement.type = 'range'; 322 | sliderElement.className = 'b3-slider fn__size200 b3-tooltips b3-tooltips__n'; 323 | sliderElement.ariaLabel = item.value; 324 | sliderElement.min = item.slider?.min.toString() ?? '0'; 325 | sliderElement.max = item.slider?.max.toString() ?? '100'; 326 | sliderElement.step = item.slider?.step.toString() ?? '1'; 327 | sliderElement.value = item.value; 328 | sliderElement.onchange = () => { 329 | sliderElement.ariaLabel = sliderElement.value; 330 | item.action?.callback(); 331 | } 332 | itemElement = sliderElement; 333 | break; 334 | case 'textinput': 335 | let textInputElement: HTMLInputElement = document.createElement('input'); 336 | textInputElement.className = 'b3-text-field fn__flex-center fn__size200'; 337 | textInputElement.value = item.value; 338 | textInputElement.onchange = item.action?.callback ?? (() => { }); 339 | itemElement = textInputElement; 340 | textInputElement.addEventListener('keydown', preventEnterConfirm); 341 | break; 342 | case 'textarea': 343 | let textareaElement: HTMLTextAreaElement = document.createElement('textarea'); 344 | textareaElement.className = "b3-text-field fn__block"; 345 | textareaElement.value = item.value; 346 | textareaElement.onchange = item.action?.callback ?? (() => { }); 347 | itemElement = textareaElement; 348 | break; 349 | case 'number': 350 | let numberElement: HTMLInputElement = document.createElement('input'); 351 | numberElement.type = 'number'; 352 | numberElement.className = 'b3-text-field fn__flex-center fn__size200'; 353 | numberElement.value = item.value; 354 | itemElement = numberElement; 355 | numberElement.addEventListener('keydown', preventEnterConfirm); 356 | break; 357 | case 'button': 358 | let buttonElement: HTMLButtonElement = document.createElement('button'); 359 | buttonElement.className = "b3-button b3-button--outline fn__flex-center fn__size200"; 360 | buttonElement.innerText = item.button?.label ?? 'Button'; 361 | buttonElement.onclick = item.button?.callback ?? (() => { }); 362 | itemElement = buttonElement; 363 | break; 364 | case 'hint': 365 | let hintElement: HTMLElement = document.createElement('div'); 366 | hintElement.className = 'b3-label fn__flex-center'; 367 | itemElement = hintElement; 368 | break; 369 | } 370 | if (item.direction === 'row') { 371 | itemElement.classList.remove('fn__size200'); 372 | } 373 | return itemElement; 374 | } 375 | 376 | /** 377 | * return the setting element 378 | * @param key key name 379 | * @returns element 380 | */ 381 | getElement(key: string) { 382 | // let item = this.settings.get(key); 383 | let element = this.elements.get(key) as any; 384 | return element; 385 | } 386 | 387 | private updateValueFromElement(key: string) { 388 | let item = this.settings.get(key); 389 | if (item.type === 'button') return; 390 | let element = this.elements.get(key) as any; 391 | item.value = item.getEleVal(element); 392 | } 393 | 394 | private updateElementFromValue(key: string) { 395 | let item = this.settings.get(key); 396 | if (item.type === 'button') return; 397 | let element = this.elements.get(key) as any; 398 | item.setEleVal(element, item.value); 399 | } 400 | } -------------------------------------------------------------------------------- /src/setting/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-01 16:25:57 5 | * @FilePath : /src/setting/index.ts 6 | * @LastEditTime : 2024-12-20 21:58:58 7 | * @Description : 8 | */ 9 | 10 | import { i18n } from "@/index"; 11 | 12 | import { Plugin, showMessage } from "siyuan"; 13 | import { SettingUtils } from "@/libs/setting-utils"; 14 | import { loadUserCustomView } from "@/core/custom-view"; 15 | 16 | 17 | let defaultSetting = { 18 | codeEditor: 'code -w {{filepath}}', 19 | defaultTableColumns: ['type', 'content', 'hpath', 'box'].join(','), 20 | echartsRenderer: 'svg', 21 | onlyImportDtsInUserDoc: true 22 | }; 23 | 24 | let settingUtils: SettingUtils; 25 | 26 | const useDeviceStorage = async (plugin: Plugin) => { 27 | const device = window.siyuan.config.system; 28 | const fname = `Device@${device.id}.json`; 29 | let config = await plugin.loadData(fname); 30 | config = config || {}; 31 | return { 32 | get: (key: string | number) => { 33 | return config[key]; 34 | }, 35 | set: async (key: string | number, value: any) => { 36 | config[key] = value; 37 | await plugin.saveData(fname, config); 38 | }, 39 | } 40 | } 41 | 42 | const storageName = 'setting'; 43 | export const load = async (plugin: Plugin) => { 44 | const localStorage = await useDeviceStorage(plugin); 45 | 46 | settingUtils = new SettingUtils({ 47 | plugin, 48 | name: storageName, 49 | callback: (data: typeof defaultSetting) => { 50 | localStorage.set('codeEditor', data.codeEditor); 51 | }, 52 | width: '1000px', 53 | height: '500px' 54 | }); 55 | settingUtils.addItem({ 56 | type: 'hint', 57 | title: i18n.src_setting_indexts.api_interface, 58 | description: i18n.src_setting_indexts.apitypedefinition + `frostime/sy-query-view/public/types.d.ts`, 59 | key: 'apiDoc', 60 | value: '', 61 | }); 62 | settingUtils.addItem({ 63 | type: 'checkbox', 64 | title: i18n.src_setting_indexts.user_doc_import_type_ref, 65 | description: i18n.src_setting_indexts.plugin_import_help_doc, 66 | key: 'onlyImportDtsInUserDoc', 67 | value: defaultSetting.onlyImportDtsInUserDoc, 68 | }); 69 | settingUtils.addItem({ 70 | type: 'textinput', 71 | title: i18n.src_setting_indexts.open_local_editor, 72 | description: i18n.src_setting_indexts.local_command_desc, 73 | key: 'codeEditor', 74 | value: defaultSetting.codeEditor, 75 | direction: 'row' 76 | }); 77 | settingUtils.addItem({ 78 | type: 'textinput', 79 | title: i18n.src_setting_indexts.table_default_columns, 80 | description: i18n.src_setting_indexts.defaultcolumnsofdataviewtable, 81 | key: 'defaultTableColumns', 82 | value: defaultSetting.defaultTableColumns, 83 | direction: 'row' 84 | }); 85 | settingUtils.addItem({ 86 | type: 'button', 87 | title: i18n.src_setting_indexts.user_custom_view, 88 | description: i18n.src_setting_indexts.user_self_written_view, 89 | key: 'userCustomView', 90 | value: '', 91 | button: { 92 | label: i18n.src_setting_indexts.reload, 93 | callback: async () => { 94 | const result = await loadUserCustomView(); 95 | if (result.ok) { 96 | let cnt = Object.keys(result.custom).length; 97 | showMessage(i18n.src_setting_indexts.import_success.replace('{cnt}', `${cnt}`), 3000, 'info'); 98 | } else { 99 | showMessage(i18n.src_setting_indexts.import_failed, 3000, 'error'); 100 | } 101 | } 102 | } 103 | }); 104 | settingUtils.addItem({ 105 | type: 'select', 106 | title: i18n.src_setting_indexts.echarts_renderer, 107 | key: 'echartsRenderer', 108 | description: i18n.src_setting_indexts.echarts_renderer_option, 109 | value: defaultSetting.echartsRenderer, 110 | options: { 111 | canvas: 'canvas', 112 | svg: 'svg' 113 | } 114 | }); 115 | 116 | const configs = await settingUtils.load(); 117 | // codeEditor config is sotred in localstorage 118 | let codeEditor = localStorage.get('codeEditor') ?? configs.codeEditor; 119 | settingUtils.set('codeEditor', codeEditor); 120 | }; 121 | 122 | export const setting = new Proxy({} as typeof defaultSetting, { 123 | get: (target, key: string) => { 124 | // return target[key]; 125 | return settingUtils.get(key); 126 | }, 127 | set: (target, key, value) => { 128 | console.warn('禁止外部修改插件配置变量'); 129 | return false; 130 | } 131 | }); 132 | 133 | -------------------------------------------------------------------------------- /src/types/api.d.ts: -------------------------------------------------------------------------------- 1 | interface IResGetNotebookConf { 2 | box: string; 3 | conf: NotebookConf; 4 | name: string; 5 | } 6 | 7 | interface IReslsNotebooks { 8 | notebooks: Notebook[]; 9 | } 10 | 11 | interface IResUpload { 12 | errFiles: string[]; 13 | succMap: { [key: string]: string }; 14 | } 15 | 16 | interface IResdoOperations { 17 | doOperations: doOperation[]; 18 | undoOperations: doOperation[] | null; 19 | } 20 | 21 | interface IResGetBlockKramdown { 22 | id: BlockId; 23 | kramdown: string; 24 | } 25 | 26 | interface IResGetChildBlock { 27 | id: BlockId; 28 | type: BlockType; 29 | subtype?: BlockSubType; 30 | } 31 | 32 | interface IResGetTemplates { 33 | content: string; 34 | path: string; 35 | } 36 | 37 | interface IResReadDir { 38 | isDir: boolean; 39 | isSymlink: boolean; 40 | name: string; 41 | } 42 | 43 | interface IResExportMdContent { 44 | hPath: string; 45 | content: string; 46 | } 47 | 48 | interface IResBootProgress { 49 | progress: number; 50 | details: string; 51 | } 52 | 53 | interface IResForwardProxy { 54 | body: string; 55 | contentType: string; 56 | elapsed: number; 57 | headers: { [key: string]: string }; 58 | status: number; 59 | url: string; 60 | } 61 | 62 | interface IResExportResources { 63 | path: string; 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/types/data-view.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * List Options 3 | * @interface IListOptions 4 | * @property {string} type - List type: 'u' for unordered, 'o' for ordered 5 | * @property {number} columns - Number of columns to display 6 | * @property {(b: T, defaultRenderer?: (b: T) => string) => string | number | undefined | null} renderer - Custom function to render each list item; if not provided or return null, the default renderer will be used; The second parameter is the default renderer, you can call it to get the default rendering result 7 | */ 8 | interface IListOptions { 9 | type?: 'u' | 'o'; 10 | columns?: number; 11 | renderer?: (b: T, defaultRenderer?: (b: T) => string) => string | number | undefined | null; 12 | } 13 | 14 | interface IHasChildren { 15 | children?: IHasChildren[]; 16 | } 17 | 18 | interface ITreeNode extends IHasChildren { 19 | name: string; 20 | children?: ITreeNode[]; 21 | [key: string]: any; 22 | } 23 | 24 | /** 25 | * Extends the block, enable children property 26 | * Block has id, name and content properties, so it is also a tree node 27 | * @interface IBlockWithChilds 28 | * @extends Block 29 | * @extends IHasChildren 30 | * @extends ITreeNode 31 | */ 32 | interface IBlockWithChilds extends Block, IHasChildren, ITreeNode { 33 | id: string; 34 | name: string; 35 | content: string; 36 | children?: IBlockWithChilds[]; 37 | } 38 | 39 | /** 40 | * Is actually the nodes type of Echart { type: 'graph' } 41 | * @link https://echarts.apache.org/zh/option.html#series-graph.data 42 | */ 43 | interface IGraphNode { 44 | id: string; 45 | name?: string; 46 | value?: string; 47 | category?: number; 48 | [key: string]: any; 49 | } 50 | 51 | /** 52 | * Minimum link data structure for Echarts 53 | * @link https://echarts.apache.org/zh/option.html#series-graph.links 54 | * @property {string} source - Source node ID 55 | * @property {string | string[]} target - Target node ID 56 | * NOT THAT, you can pass an array, which is more flexible than the original Echarts option 57 | * @property {[key: string]: any} [key: string] - Allow other custom properties in link 58 | */ 59 | interface IGraphLink { 60 | source: string; 61 | target: string | string[]; 62 | [key: string]: any; 63 | } 64 | 65 | interface IEchartsSeriesOption { 66 | [key: string]: any; 67 | } 68 | 69 | interface IEchartsOption { 70 | [key: string]: any; 71 | series?: IEchartsSeriesOption[]; 72 | } 73 | 74 | /** 75 | * Implemented by class DataView 76 | */ 77 | interface IDataView { 78 | render: () => void; 79 | } 80 | 81 | /** 82 | * User customized view. If registered, you can use it inside DataView by `dv.xxx()` or `dv.addxxx()` 83 | */ 84 | interface ICustomView { 85 | /** 86 | * Use the custom view 87 | * @param dv - DataView instance, might be empty while validating process 88 | */ 89 | use: (dv?: IDataView) => { 90 | render: (container: HTMLElement, ...args: any[]) => void | string | HTMLElement; //Create the user custom view. 91 | dispose?: () => void; // Unmount hook for the user custom view. 92 | }, 93 | alias?: string[]; // Alias name for the custom view 94 | } 95 | 96 | interface IUserCustom { 97 | [key: string]: ICustomView; 98 | } 99 | 100 | 101 | /** 102 | * State object 103 | * @interface IState 104 | * @template T 105 | * @property {() => T} - Get the state value 106 | * @property {(value: T) => T} - Set the state value 107 | * @property {T} - The state value, can be set or get 108 | * @property {(effect: (newValue: T, oldValue: T) => void) => void} - Register an effect to the state 109 | * @property {(derive: (value: T) => T) => () => T} - Create a derived state 110 | */ 111 | interface IState { 112 | (): T; 113 | (value: T): T; 114 | 115 | value: T; 116 | 117 | /** 118 | * @warn 119 | * The effect function is not supposed to return anything! 120 | * It is merely a callback function when setter is called, don't treat it powerful as in React or etc. 121 | */ 122 | effect: (effect: (newValue: T, oldValue: T) => void) => void; 123 | derived: (derive: (value: T) => T) => () => T; 124 | } 125 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.scss' { 2 | const classes: { readonly [key: string]: string }; 3 | export default classes; 4 | } 5 | 6 | declare module '*.module.css' { 7 | const classes: { readonly [key: string]: string }; 8 | export default classes; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/i18n.d.ts: -------------------------------------------------------------------------------- 1 | interface I18n { 2 | src_core_dataviewts: { 3 | blank: string; 4 | }; 5 | src_core_editorts: { 6 | ext_code_editor_closed: string; 7 | show_as_template_format: string; 8 | show_embedded_block_code_in_siyuan_template_format: string; 9 | unableto_use_ext_cmd: string; 10 | unableto_use_external_editor_cmd: string; 11 | unusableexteditorcmd: string; 12 | }; 13 | src_core_indexts: { 14 | custom_queryview_error: string; 15 | reload_custom_comp: string; 16 | }; 17 | src_core_queryts: { 18 | query_obsolete_params: string; 19 | }; 20 | src_dataquery_componentsts: { 21 | mermaid_render_failed: string; 22 | }; 23 | src_dataquery_editorts: { 24 | onlydesktop: string; 25 | }; 26 | src_indexts: { 27 | incompatible_version: string; 28 | manual_release: string; 29 | manual_release_desc: string; 30 | plugin_not_working: string; 31 | }; 32 | src_setting_indexts: { 33 | api_interface: string; 34 | apitypedefinition: string; 35 | defaultcolumnsofdataviewtable: string; 36 | echarts_renderer: string; 37 | echarts_renderer_option: string; 38 | import_failed: string; 39 | import_success: string; 40 | local_command_desc: string; 41 | open_local_editor: string; 42 | plugin_import_help_doc: string; 43 | reload: string; 44 | table_default_columns: string; 45 | user_custom_view: string; 46 | user_doc_import_type_ref: string; 47 | user_self_written_view: string; 48 | }; 49 | src_userhelp_examplests: { 50 | backlinks_table: string; 51 | daily_quote: string; 52 | doc_backlinks_grouping: string; 53 | doc_outline_tree: string; 54 | docs_per_month: string; 55 | echarts_graph_ref: string; 56 | list_doc_subsections: string; 57 | query_attr_views: string; 58 | query_doc_tree: string; 59 | query_this_month_todo: string; 60 | query_unfinished_tasks: string; 61 | random_text_translate: string; 62 | recent_docs: string; 63 | show_tags_card_view: string; 64 | simple_chatgpt: string; 65 | sql_exec_result: string; 66 | unfinished_task_monthly: string; 67 | updated_docs_today: string; 68 | view_assets_images: string; 69 | }; 70 | src_userhelp_indexts: { 71 | create_notebook: string; 72 | download: string; 73 | edit_custom_view: string; 74 | help_doc: string; 75 | help_doc_2: string; 76 | open_custom_view_dir: string; 77 | open_locally: string; 78 | queryview: string; 79 | unable_open_custom_view: string; 80 | unable_open_custom_view_dir: string; 81 | unable_open_d_ts: string; 82 | useview: string; 83 | useview2: string; 84 | }; 85 | src_userhelp_sydocts: { 86 | create_user_doc: string; 87 | plugin_setting_doc: string; 88 | plugin_update_doc: string; 89 | }; 90 | user_help: { 91 | ahead_hint: string; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2023-08-15 10:28:10 5 | * @FilePath : /src/types/index.d.ts 6 | * @LastEditTime : 2024-12-01 22:54:38 7 | * @Description : Frequently used data structures in SiYuan 8 | */ 9 | 10 | type ScalarValue = string | number | boolean; 11 | 12 | type DocumentId = string; 13 | type BlockId = string; 14 | type NotebookId = string; 15 | type PreviousID = BlockId; 16 | type ParentID = BlockId | DocumentId; 17 | 18 | type Notebook = { 19 | id: NotebookId; 20 | name: string; 21 | icon: string; 22 | sort: number; 23 | closed: boolean; 24 | } 25 | 26 | type NotebookConf = { 27 | name: string; 28 | closed: boolean; 29 | refCreateSavePath: string; 30 | createDocNameTemplate: string; 31 | dailyNoteSavePath: string; 32 | dailyNoteTemplatePath: string; 33 | } 34 | 35 | type BlockType = 36 | | 'd' 37 | | 'p' 38 | | 'query_embed' 39 | | 'l' 40 | | 'i' 41 | | 'h' 42 | | 'iframe' 43 | | 'tb' 44 | | 'b' 45 | | 's' 46 | | 'c' 47 | | 'widget' 48 | | 't' 49 | | 'html' 50 | | 'm' 51 | | 'av' 52 | | 'audio'; 53 | 54 | 55 | type BlockSubType = 56 | | 'h1' 57 | | 'h2' 58 | | 'h3' 59 | | 'h4' 60 | | 'h5' 61 | | 'h6' 62 | | 'o' 63 | | 'u' 64 | | 't'; 65 | 66 | 67 | type Block = { 68 | id: BlockId; // ID of the block 69 | parent_id?: BlockId; // ID of the parent block 70 | root_id: DocumentId; // ID of the document 71 | hash: string; 72 | box: string; // ID of the notebook 73 | path: string; // Path of the .sy file, like /20241201224713-q858585/20241201224713-1234567.sy 74 | hpath: string; // Human-readable path of the document, like /Test/Document 75 | name: string; 76 | alias: string; 77 | memo: string; 78 | tag: string; 79 | content: string; // Content of the block, no md modifier 80 | fcontent?: string; // Content of the first block, when the block is a container block like list item 81 | markdown: string; // Markdown content of the block 82 | length: number; 83 | type: BlockType; 84 | subtype: BlockSubType; 85 | /** string of { [key: string]: string } 86 | * For instance: "{: custom-type=\"query-code\" id=\"20230613234017-zkw3pr0\" updated=\"20230613234509\"}" 87 | */ 88 | ial?: string; 89 | sort: number; 90 | created: string; // Time of creation, with format like 20241201224713 91 | updated: string; // Time of last update, with format like 20241201224713 92 | } 93 | 94 | type PartialBlock = Partial; 95 | 96 | type doOperation = { 97 | action: string; 98 | data: string; 99 | id: BlockId; 100 | parentID: BlockId | DocumentId; 101 | previousID: BlockId; 102 | retData: null; 103 | } 104 | 105 | declare interface Window { 106 | siyuan: { 107 | config: any; 108 | notebooks: any; 109 | menus: any; 110 | dialogs: any; 111 | blockPanels: any; 112 | storage: any; 113 | user: any; 114 | ws: any; 115 | languages: any; 116 | emojis: any; 117 | }; 118 | Lute: any; 119 | Query: typeof import("@/core/query").default; 120 | mermaid: any; 121 | echarts: any; 122 | katex: any; 123 | } 124 | 125 | // globalThis 126 | declare interface GlobalThis { 127 | Query: typeof import("@/core/query").default; 128 | } 129 | -------------------------------------------------------------------------------- /src/user-help/examples.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-11 18:24:00 5 | * @FilePath : /src/user-help/examples.ts 6 | * @LastEditTime : 2025-03-16 16:35:18 7 | * @Description : 8 | */ 9 | import { getFileBlob, readDir } from "@/api"; 10 | import type QueryViewPlugin from ".." 11 | import { openTab } from "siyuan"; 12 | 13 | import styles from './index.module.scss'; 14 | 15 | import { i18n } from ".."; 16 | 17 | let exampleHTML = ` 18 |
19 | 20 | 21 | 22 |
23 | `; 24 | 25 | const Description = { 26 | "exp-month-todo.js": () => i18n.src_userhelp_examplests.query_this_month_todo, 27 | "exp-child-docs.js": () => i18n.src_userhelp_examplests.list_doc_subsections, 28 | "exp-avs-under-root-doc.js": () => i18n.src_userhelp_examplests.query_attr_views, 29 | "exp-doc-backlinks-table.js": () => i18n.src_userhelp_examplests.backlinks_table, 30 | "exp-doc-backlinks-grouped.js": () => i18n.src_userhelp_examplests.doc_backlinks_grouping, 31 | "exp-outline.js": () => i18n.src_userhelp_examplests.doc_outline_tree, 32 | "exp-list-tags.js": () => i18n.src_userhelp_examplests.show_tags_card_view, 33 | "exp-latest-update-doc.js": () => i18n.src_userhelp_examplests.recent_docs, 34 | "exp-today-updated.js": () => i18n.src_userhelp_examplests.updated_docs_today, 35 | "exp-created-docs.js": () => i18n.src_userhelp_examplests.docs_per_month, 36 | "exp-sql-executor.js": () => i18n.src_userhelp_examplests.sql_exec_result, 37 | "exp-gpt-chat.js": () => i18n.src_userhelp_examplests.simple_chatgpt, 38 | "exp-doc-backlinks-graph.js": () => i18n.src_userhelp_examplests.echarts_graph_ref, 39 | "exp-show-asset-images.js": () => i18n.src_userhelp_examplests.view_assets_images, 40 | "exp-daily-sentence.js": () => i18n.src_userhelp_examplests.daily_quote, 41 | "exp-gpt-translate.js": () => i18n.src_userhelp_examplests.random_text_translate, 42 | "exp-doc-tree.js": () => i18n.src_userhelp_examplests.query_doc_tree, 43 | "exp-month-todo-kanban.js": () => i18n.src_userhelp_examplests.unfinished_task_monthly, 44 | "exp-month-todo-timeline.js": () => i18n.src_userhelp_examplests.query_unfinished_tasks 45 | }; 46 | 47 | const addSection = (title: string, content: string) => { 48 | let desc = Description[title]?.() ?? ''; 49 | let newpart = ` 50 |

${title}

51 | 52 | ${desc ? `
${desc}
` : ''} 53 | 54 | 61 | 62 |
63 | 64 | `; 65 | exampleHTML += newpart; 66 | } 67 | 68 | const loadJsContent = async (path: string) => { 69 | const blob = await getFileBlob(path); 70 | const content = await blob.text(); 71 | return content; 72 | } 73 | 74 | export const useExamples = async (plugin: QueryViewPlugin) => { 75 | const examplePath = `/data/plugins/${plugin.name}/example`; 76 | let files = await readDir(examplePath); 77 | let filenames = files.map(f => f.name); 78 | const keys = Object.keys(Description); 79 | filenames.sort((a: string, b: string) => { 80 | if (Description[a] === undefined) return 1; 81 | if (Description[b] === undefined) return -1; 82 | 83 | const indexA = keys.indexOf(a); 84 | const indexB = keys.indexOf(b); 85 | return indexA - indexB; 86 | }); 87 | 88 | // const routePrefix = `/plugins/${plugin.name}/example`; 89 | 90 | const tocli = filenames.map(title => 91 | `
  • 92 | 93 | ${title} 94 | 95 |
  • ` 96 | ); 97 | 98 | const toc = ` 99 |

    TOC

    100 |
      ${tocli.join('')}
    101 | 102 |

    Examples

    103 | `; 104 | exampleHTML += toc; 105 | 106 | for (const file of filenames) { 107 | let name = file; 108 | const filePath = `${examplePath}/${name}`; 109 | const content = await loadJsContent(filePath); 110 | addSection(name, content); 111 | } 112 | 113 | const onClick = (event: MouseEvent) => { 114 | // Get the target element 115 | const target = event.target as HTMLElement; 116 | 117 | // Check if the clicked element is an
  • with a data-target attribute 118 | if (target.tagName === 'SPAN' && target.hasAttribute('data-target')) { 119 | // Get the value of the data-target attribute 120 | const targetId = target.getAttribute('data-target'); 121 | 122 | // Find the element with the corresponding id 123 | const elementToScrollTo = document.getElementById(targetId); 124 | 125 | // Scroll to the element if it exists 126 | if (elementToScrollTo) { 127 | elementToScrollTo.scrollIntoView({ behavior: 'smooth' }); 128 | } 129 | } else if (target.closest(`.${styles['to-top']}`)) { 130 | // Scroll to the top of the page 131 | const container = target.closest(`.item__readme`); 132 | container?.querySelector('h3.TOC')?.scrollIntoView({ behavior: 'smooth' }); 133 | } 134 | }; 135 | 136 | plugin.addTab({ 137 | type: 'js-example', 138 | init() { 139 | const readme = document.createElement('div'); 140 | readme.classList.add('item__readme'); 141 | readme.classList.add('b3-typography'); 142 | readme.classList.add('b3-typography--default'); 143 | readme.innerHTML = exampleHTML; 144 | readme.style.padding = '10px 15%'; 145 | this.element.appendChild(readme); 146 | this.element.addEventListener('click', onClick); 147 | }, 148 | destroy() { 149 | this.element.removeEventListener('click', onClick); 150 | } 151 | }); 152 | 153 | return { 154 | open() { 155 | openTab({ 156 | app: plugin.app, 157 | custom: { 158 | id: `${plugin.name}js-example`, 159 | title: 'Query&View Examples', 160 | icon: 'iconCode' 161 | } 162 | }) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/user-help/index.module.scss: -------------------------------------------------------------------------------- 1 | .link { 2 | color: var(--b3-protyle-inline-link-color); 3 | cursor: pointer; 4 | transition: var(--b3-transition); 5 | } 6 | 7 | .to-top { 8 | position: absolute; 9 | bottom: 50px; 10 | right: 50px; 11 | width: 50px; 12 | height: 50px; 13 | background-color: var(--b3-theme-primary-lighter); 14 | border-radius: 50%; 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | cursor: pointer; 19 | 20 | &:hover { 21 | background-color: var(--b3-theme-primary); 22 | } 23 | 24 | svg { 25 | fill: var(--b3-theme-on-primary); 26 | width: 24px; 27 | height: 24px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/user-help/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-10 18:46:12 5 | * @FilePath : /src/user-help/index.ts 6 | * @LastEditTime : 2025-03-13 22:01:01 7 | * @Description : 8 | */ 9 | import { i18n } from "@/index"; 10 | import type QueryViewPlugin from "@/index"; 11 | import { useUserReadme } from "./sy-doc"; 12 | import { useExamples } from "./examples"; 13 | import { insertBlock } from "@/api"; 14 | import { setting } from "@/setting"; 15 | import { showMessage } from "siyuan"; 16 | 17 | const child_process = require("child_process"); 18 | 19 | const BASIC_TEMPLATE = () => ` 20 | //!js 21 | const query = async () => { 22 | //${i18n.src_userhelp_indexts.useview} 23 | //let dv = Query.DataView(protyle, item, top); 24 | 25 | const SQL = \` 26 | select * from blocks 27 | order by random() 28 | limit 5; 29 | \`; 30 | let blocks = await Query.sql(SQL); 31 | 32 | return blocks.pick('id'); 33 | //${i18n.src_userhelp_indexts.useview2} 34 | //dv.addlist(blocks); 35 | //dv.render(); 36 | } 37 | 38 | return query(); 39 | `.trim(); 40 | 41 | const toEmbed = (code: string) => { 42 | code = code.trim(); 43 | return '{{' + code.replaceAll('\n', '_esc_newline_') + '}}\n{: breadcrumb="true" }'; 44 | } 45 | 46 | const openLocalDTSFile = () => { 47 | if (!child_process) return; 48 | const endpoint = '/plugins/sy-query-view/types.d.ts' 49 | const dataDir = window.siyuan.config.system.dataDir; 50 | 51 | const path = window?.require('path'); 52 | const filepath = path.join(dataDir, endpoint); 53 | 54 | const codeEditor = setting.codeEditor; 55 | const command = codeEditor.replace('{{filepath}}', filepath); 56 | child_process.exec(command, (err, stdout, stderr) => { 57 | if (err) { 58 | console.warn('Error executing command:', err); 59 | } else { 60 | // console.debug('Command executed successfully:', stdout); 61 | } 62 | }); 63 | } 64 | 65 | 66 | export const load = async (plugin: QueryViewPlugin) => { 67 | const pluignUrl = '/plugins/sy-query-view/plugin.json'; 68 | const pluginJson = await fetch(pluignUrl).then(res => res.json()); 69 | const pluginName = pluginJson.name; 70 | const pluginVersion = pluginJson.version; 71 | 72 | plugin.protyleSlash.push({ 73 | filter: ['qv-basic', 'queryview'], 74 | html: i18n.src_userhelp_indexts.queryview, 75 | id: pluginName, 76 | callback: (protyle) => { 77 | protyle.insert(window.Lute.Caret, false, false); 78 | const selection = document.getSelection(); 79 | if (selection.rangeCount === 0) { 80 | return; 81 | } 82 | const range = selection.getRangeAt(0); 83 | const element = range.startContainer.parentElement; 84 | const node = element.closest('div[data-node-id]'); 85 | if (!node) { 86 | return; 87 | } 88 | const id = node.getAttribute('data-node-id'); 89 | setTimeout(async () => { 90 | await insertBlock('markdown', toEmbed(BASIC_TEMPLATE()), null, id, null); 91 | }, 500); 92 | } 93 | }); 94 | 95 | if (child_process) { 96 | 97 | plugin.registerMenuItem({ 98 | label: i18n.src_userhelp_indexts.open_locally + ' d.ts', 99 | icon: 'iconEdit', 100 | click: () => { 101 | try { 102 | openLocalDTSFile(); 103 | } catch (error) { 104 | console.error(error); 105 | showMessage(i18n.src_userhelp_indexts.unable_open_d_ts, 3000, 'error'); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | plugin.registerMenuItem({ 112 | label: i18n.src_userhelp_indexts.download + ' d.ts', 113 | icon: 'iconDownload', 114 | click: () => { 115 | const url = '/plugins/sy-query-view/types.d.ts'; 116 | const a = document.createElement('a'); 117 | a.href = url; 118 | a.download = `${pluginName}@${pluginVersion}.types.d.ts`; 119 | a.click(); 120 | } 121 | }); 122 | 123 | plugin.version = pluginVersion; 124 | 125 | plugin.registerMenuItem({ 126 | label: i18n.src_userhelp_indexts.help_doc, 127 | icon: 'iconHelp', 128 | click: () => { 129 | useUserReadme(plugin); 130 | } 131 | }); 132 | const { open } = await useExamples(plugin); 133 | plugin.registerMenuItem({ 134 | label: 'Examples', 135 | icon: 'iconCode', 136 | click: open 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /src/user-help/sy-doc.ts: -------------------------------------------------------------------------------- 1 | import { createDocWithMd, removeDoc, renameDoc, request, setBlockAttrs, sql, updateBlock } from "@/api"; 2 | import type QueryViewPlugin from "@/index"; 3 | import { openBlock } from "@/utils"; 4 | import { confirm, showMessage } from "siyuan"; 5 | import { i18n } from "@/index"; 6 | import { setting } from "@/setting"; 7 | 8 | 9 | const CUSTOM_USER_README_ATTR = 'custom-query-view-user-readme'; 10 | 11 | const compareVersion = (v1: string, v2: string) => { 12 | const onlyNum = (str: string) => str.replace(/\D/g, ''); 13 | const toNum = (str: string) => { 14 | str = onlyNum(str); 15 | return str.length > 0 ? Number(str) : 0; 16 | } 17 | 18 | const v1Arr = v1.split('.').map(toNum); 19 | const v2Arr = v2.split('.').map(toNum); 20 | for (let i = 0; i < v1Arr.length; i++) { 21 | if (v1Arr[i] > v2Arr[i]) { 22 | return 1; 23 | } else if (v1Arr[i] < v2Arr[i]) { 24 | return -1; 25 | } 26 | } 27 | return 0; 28 | } 29 | 30 | const OutlineCode = ` 31 | > {{//!js_esc_newline_const query = async () => {_esc_newline_ let dv = Query.DataView(protyle, item, top);_esc_newline_ let ans = await Query.request('/api/outline/getDocOutline', {_esc_newline_ id: Query.root_id(protyle)_esc_newline_ });_esc_newline_ ans = Query.wrapit(ans);_esc_newline_ const iterate = (data) => {_esc_newline_ for (let item of data) {_esc_newline_ if (item.count > 0) {_esc_newline_ let subtocs = iterate(item.blocks ?? item.children);_esc_newline_ item.children = Query.wrapBlocks(subtocs);_esc_newline_ }_esc_newline_ }_esc_newline_ return data;_esc_newline_ }_esc_newline_ let tocs = iterate(ans);_esc_newline_ dv.addlist(tocs, {_esc_newline_ renderer: b => \`[\${b.name || b.content}](\${b.asurl})\`,_esc_newline_ columns: 2_esc_newline_ });_esc_newline_ dv.render();_esc_newline_}_esc_newline__esc_newline_return query();}} 32 | `.trim(); 33 | 34 | 35 | const createReadmeText = async (plugin: QueryViewPlugin) => { 36 | const lang = window.siyuan.config.lang; 37 | const fname = lang.startsWith('zh') ? 'README_zh_CN.md' : 'README.md'; 38 | 39 | const response = await fetch(`/plugins/sy-query-view/${fname}`); 40 | let readme = await response.text(); 41 | 42 | if (setting.onlyImportDtsInUserDoc) { 43 | // 找到 ​``​ 和 ​``​ 之间的内容 44 | const start = '``'; 45 | const end = '``'; 46 | const startIndex = readme.indexOf(start); 47 | const endIndex = readme.indexOf(end); 48 | 49 | const hint = i18n.src_userhelp_sydocts.plugin_setting_doc; 50 | 51 | if (startIndex !== -1 && endIndex !== -1) { 52 | readme = readme.substring(startIndex + start.length, endIndex).trim(); 53 | } 54 | 55 | readme = hint + '\n\n' + readme; 56 | } 57 | 58 | const AheadHint = i18n.user_help.ahead_hint.trim(); 59 | let ahead = AheadHint.replace('{{version}}', plugin.version); 60 | readme = ahead + '\n' + OutlineCode + '\n\n' + readme; 61 | return readme; 62 | } 63 | 64 | const createReadme = async (plugin: QueryViewPlugin, title: string) => { 65 | const notebooks = window.siyuan.notebooks.filter(n => n.closed === false); 66 | if (notebooks.length === 0) { 67 | showMessage(i18n.src_userhelp_indexts.create_notebook); 68 | return null; 69 | } 70 | const notebook = notebooks[0]; 71 | let readme = await createReadmeText(plugin); 72 | let docId = await createDocWithMd(notebook.id, `/${title}`, readme); 73 | const attr = { 74 | [CUSTOM_USER_README_ATTR]: plugin.version, 75 | 'custom-sy-readonly': 'true' 76 | }; 77 | await setBlockAttrs(docId, attr); 78 | return docId; 79 | } 80 | 81 | const useUserReadme = async (plugin: QueryViewPlugin) => { 82 | const docs: (Block & { version: string })[] = await sql(` 83 | SELECT B.*, A.value as version 84 | FROM blocks AS B 85 | JOIN attributes AS A ON A.block_id = B.id 86 | WHERE A.name = '${CUSTOM_USER_README_ATTR}' 87 | ORDER BY B.UPDATED DESC; 88 | `); 89 | 90 | let targetDocId: DocumentId = null; 91 | 92 | if (!docs || docs.length === 0) { 93 | const title = `${plugin.displayName}@${plugin.version} ` + i18n.src_userhelp_indexts.help_doc; 94 | targetDocId = await createReadme(plugin, title); 95 | showMessage(i18n.src_userhelp_sydocts.create_user_doc + ' ' + title); 96 | } else if (docs.length === 1) { 97 | targetDocId = docs[0].id; 98 | } else { 99 | //找到版本号最大的文档 100 | docs.sort((a, b) => compareVersion(b.version, a.version)); 101 | targetDocId = docs[0].id; 102 | const others = docs.slice(1); 103 | for (let doc of others) { 104 | await removeDoc(doc.box, doc.path); 105 | } 106 | } 107 | 108 | // validate version 109 | if (!targetDocId) return; 110 | const attrVer = await sql(` 111 | SELECT A.value 112 | FROM attributes AS A 113 | WHERE A.name = '${CUSTOM_USER_README_ATTR}' 114 | AND A.block_id = '${targetDocId}' 115 | `); 116 | if (attrVer.length === 0) return; 117 | const attrVerStr = attrVer[0].value; 118 | 119 | const updateDoc = async () => { 120 | showMessage(i18n.src_userhelp_sydocts.plugin_update_doc, 5000) 121 | let newText = await createReadmeText(plugin); 122 | const title = `${plugin.displayName}@${plugin.version} ` + i18n.src_userhelp_indexts.help_doc; 123 | const doc = docs[0]; 124 | await renameDoc(doc.box, doc.path, title) 125 | await updateBlock('markdown', newText, targetDocId); 126 | await setBlockAttrs(targetDocId, { 127 | [CUSTOM_USER_README_ATTR]: plugin.version, 128 | 'custom-sy-readonly': 'true' 129 | }); 130 | } 131 | 132 | if (attrVerStr.trim() !== plugin.version.trim()) { 133 | await updateDoc(); 134 | } 135 | setTimeout(() => { 136 | openBlock(targetDocId); 137 | }, 0); 138 | } 139 | 140 | export { 141 | useUserReadme 142 | } 143 | -------------------------------------------------------------------------------- /src/utils/const.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-04-02 22:43:02 5 | * @FilePath : /src/utils/const.ts 6 | * @LastEditTime : 2025-02-10 17:43:09 7 | * @Description : 8 | */ 9 | export const Href = { 10 | Style_Vertical_Tabbar: '/plugins/sy-f-misc/style/tab-bar-vertical.css', 11 | Style: { 12 | Font_Color: '/plugins/sy-f-misc/style/font-and-color.css', 13 | Link_Icon: '/plugins/sy-f-misc/style/link-icon.css', 14 | List_Mindmap: '/plugins/sy-f-misc/style/list-mindmap.css' 15 | } 16 | }; 17 | 18 | export const Svg = { 19 | Toolbox: ``, 20 | Vertical: ``, 21 | Theme: '', 22 | Transfer: ``, 23 | Top: ` ` 24 | }; 25 | 26 | const BlockTypeShort_ZH = { 27 | "d": "文档", 28 | "h": "标题", 29 | "l": "列表", 30 | "o": "有序列表", 31 | "u": "无序列表", 32 | "i": "列表项", 33 | "c": "代码", 34 | "m": "数学公式", 35 | "t": "表格", 36 | "b": "引述", 37 | "sb": "超级块", 38 | "p": "段落", 39 | "html": "HTML块", 40 | "query_embed": "嵌入块", 41 | 'av': '属性视图', 42 | 'widget': '挂件', 43 | 'tb': '分割线', 44 | 'audio': '音频', 45 | 'video': '视频', 46 | 'iframe': 'IFrame', 47 | // h1 ~ h6 48 | 'h1': '一级标题', 49 | 'h2': '二级标题', 50 | 'h3': '三级标题', 51 | 'h4': '四级标题', 52 | 'h5': '五级标题', 53 | 'h6': '六级标题', 54 | } 55 | 56 | const BlockTypeShort_EN = { 57 | "d": "Document", 58 | "h": "Heading", 59 | "l": "List", 60 | "o": "Ordered List", 61 | "u": "Unordered List", 62 | "i": "ListItem", 63 | "c": "Code", 64 | "m": "Math", 65 | "t": "Table", 66 | "b": "Blockquote", 67 | "sb": "SuperBlock", 68 | "p": "Paragraph", 69 | "html": "HTML", 70 | "query_embed": "Embed", 71 | 'av': 'Attribute View', 72 | 'widget': 'Widget', 73 | 'tb': 'Thematic Break', 74 | 'audio': 'Audio', 75 | 'video': 'Video', 76 | 'iframe': 'IFrame', 77 | // h1 ~ h6 78 | 'h1': 'Heading 1', 79 | 'h2': 'Heading 2', 80 | 'h3': 'Heading 3', 81 | 'h4': 'Heading 4', 82 | 'h5': 'Heading 5', 83 | 'h6': 'Heading 6', 84 | } 85 | 86 | export const BlockTypeShort = window.siyuan.config.lang.startsWith('zh') ? BlockTypeShort_ZH : BlockTypeShort_EN; 87 | 88 | export const BlockType2NodeType = { 89 | av: 'NodeAttributeView', 90 | c: 'NodeCodeBlock', 91 | d: 'NodeDocument', 92 | sb: 'NodeSuperBlock', 93 | h: 'NodeHeading', 94 | t: 'NodeTable', 95 | i: 'NodeListItem', 96 | p: 'NodeParagraph', 97 | l: 'NodeList', 98 | m: 'NodeMathBlock', 99 | b: 'NodeBlockquote', 100 | html: 'NodeHTMLBlock', 101 | query_embed: 'NodeBlockQueryEmbed' 102 | } 103 | 104 | 105 | export const NodeIcons = { 106 | NodeAttributeView: { 107 | icon: "iconDatabase" 108 | }, 109 | NodeAudio: { 110 | icon: "iconRecord" 111 | }, 112 | NodeBlockQueryEmbed: { 113 | icon: "iconSQL" 114 | }, 115 | NodeBlockquote: { 116 | icon: "iconQuote" 117 | }, 118 | NodeCodeBlock: { 119 | icon: "iconCode" 120 | }, 121 | NodeDocument: { 122 | icon: "iconFile" 123 | }, 124 | NodeHTMLBlock: { 125 | icon: "iconHTML5" 126 | }, 127 | NodeHeading: { 128 | icon: "iconHeadings", 129 | subtypes: { 130 | h1: { icon: "iconH1" }, 131 | h2: { icon: "iconH2" }, 132 | h3: { icon: "iconH3" }, 133 | h4: { icon: "iconH4" }, 134 | h5: { icon: "iconH5" }, 135 | h6: { icon: "iconH6" } 136 | } 137 | }, 138 | NodeIFrame: { 139 | icon: "iconLanguage" 140 | }, 141 | NodeList: { 142 | subtypes: { 143 | o: { icon: "iconOrderedList" }, 144 | t: { icon: "iconCheck" }, 145 | u: { icon: "iconList" } 146 | } 147 | }, 148 | NodeListItem: { 149 | icon: "iconListItem" 150 | }, 151 | NodeMathBlock: { 152 | icon: "iconMath" 153 | }, 154 | NodeParagraph: { 155 | icon: "iconParagraph" 156 | }, 157 | NodeSuperBlock: { 158 | icon: "iconSuper" 159 | }, 160 | NodeTable: { 161 | icon: "iconTable" 162 | }, 163 | NodeThematicBreak: { 164 | icon: "iconLine" 165 | }, 166 | NodeVideo: { 167 | icon: "iconVideo" 168 | }, 169 | NodeWidget: { 170 | icon: "iconBoth" 171 | } 172 | }; 173 | 174 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-04-18 21:05:32 5 | * @FilePath : /src/utils/index.ts 6 | * @LastEditTime : 2025-01-14 14:28:41 7 | * @Description : 8 | */ 9 | import * as api from '../api'; 10 | import { getFrontend, openMobileFileById, openTab, TProtyleAction, type Plugin } from 'siyuan'; 11 | import { app } from '@/index'; 12 | 13 | 14 | export function debounce) => ReturnType>(func: F, wait: number) { 15 | let timeout: ReturnType | undefined; 16 | return function(...args: Parameters) { 17 | clearTimeout(timeout); 18 | timeout = setTimeout(() => func(...args), wait); 19 | }; 20 | } 21 | 22 | export const isMobile = () => { 23 | return getFrontend().endsWith('mobile'); 24 | } 25 | 26 | export const openBlock = (id: BlockId, options?: { 27 | zoomIn?: boolean; 28 | action?: TProtyleAction[]; 29 | position?: Parameters[0]['position']; 30 | keepCursor?: boolean; 31 | }) => { 32 | if (isMobile()) { 33 | openMobileFileById(app, id); 34 | } else { 35 | openTab({ 36 | app: app, 37 | doc: { 38 | id: id, 39 | zoomIn: options?.zoomIn ?? false, 40 | action: options?.action ?? [], 41 | }, 42 | position: options?.position, 43 | keepCursor: options?.keepCursor, 44 | }); 45 | } 46 | }; 47 | 48 | 49 | export const getNotebook = (boxId: string): Notebook => { 50 | let notebooks: Notebook[] = window.siyuan.notebooks; 51 | for (let notebook of notebooks) { 52 | if (notebook.id === boxId) { 53 | return notebook; 54 | } 55 | } 56 | } 57 | 58 | export function getActiveDoc() { 59 | let tab = document.querySelector("div.layout__wnd--active ul.layout-tab-bar>li.item--focus"); 60 | let dataId: string = tab?.getAttribute("data-id"); 61 | if (!dataId) { 62 | return null; 63 | } 64 | const activeTab: HTMLDivElement = document.querySelector( 65 | `.layout-tab-container.fn__flex-1>div.protyle[data-id="${dataId}"]` 66 | ) as HTMLDivElement; 67 | if (!activeTab) { 68 | return; 69 | } 70 | const eleTitle = activeTab.querySelector(".protyle-title"); 71 | let docId = eleTitle?.getAttribute("data-node-id"); 72 | return docId; 73 | } 74 | 75 | export function isnot(value: any) { 76 | if (value === undefined || value === null) { 77 | return true; 78 | } else if (value === false) { 79 | return true; 80 | } else if (typeof value === 'string' && value.trim() === '') { 81 | return true; 82 | } else if (value?.length === 0) { 83 | return true; 84 | } 85 | return false; 86 | } 87 | 88 | export async function getChildDocs(block: BlockId, limit=64) { 89 | let sqlCode = `select * from blocks where path regexp '.*/${block}/[0-9a-z\-]+\.sy' and type='d' 90 | order by hpath desc limit ${limit};`; 91 | let childDocs = await api.sql(sqlCode); 92 | return childDocs; 93 | } 94 | 95 | 96 | export const html2ele = (html: string): DocumentFragment => { 97 | let template = document.createElement('template'); 98 | template.innerHTML = html.trim(); 99 | let ele = document.importNode(template.content, true); 100 | return ele; 101 | } 102 | 103 | export function throttle any>(func: T, wait: number = 500){ 104 | let previous = 0; 105 | return function(...args: Parameters){ 106 | let now = Date.now(), context = this; 107 | if(now - previous > wait){ 108 | func.apply(context, args); 109 | previous = now; 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/lute.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-12-01 16:16:53 5 | * @FilePath : /src/utils/lute.ts 6 | * @LastEditTime : 2025-03-13 19:04:33 7 | * @Description : 8 | */ 9 | //from https://github.com/zxhd863943427/siyuan-plugin-data-query/blob/main/src/libs/utils.ts 10 | 11 | import { Lute } from "siyuan"; 12 | 13 | export interface ILute extends Lute { 14 | SetHTMLTag2TextMark: (enable: boolean) => void; 15 | InlineMd2BlockDOM: (md: string) => string; 16 | // HTML2Markdown: (html: string) => string; 17 | HTML2Md: (html: string) => string; 18 | SetGitConflict: (args: boolean) => void; 19 | } 20 | 21 | export const setLute = (options) => { 22 | let lute: ILute = globalThis.Lute.New(); 23 | lute.SetSpellcheck(window.siyuan.config.editor.spellcheck); 24 | lute.SetProtyleMarkNetImg(window.siyuan.config.editor.displayNetImgMark); 25 | lute.SetFileAnnotationRef(true); 26 | lute.SetTextMark(true); 27 | lute.SetHeadingID(false); 28 | lute.SetYamlFrontMatter(false); 29 | // lute.PutEmojis(options.emojis); 30 | // lute.SetEmojiSite(options.emojiSite); 31 | // lute.SetHeadingAnchor(options.headingAnchor); 32 | lute.SetInlineMathAllowDigitAfterOpenMarker(true); 33 | lute.SetToC(false); 34 | lute.SetIndentCodeBlock(false); 35 | lute.SetParagraphBeginningSpace(true); 36 | lute.SetSetext(false); 37 | lute.SetFootnotes(false); 38 | lute.SetLinkRef(false); 39 | // lute.SetSanitize(options.sanitize); 40 | // lute.SetChineseParagraphBeginningSpace(options.paragraphBeginningSpace); 41 | // lute.SetRenderListStyle(options.listStyle); 42 | lute.SetImgPathAllowSpace(true); 43 | lute.SetKramdownIAL(true); 44 | lute.SetTag(true); 45 | lute.SetSuperBlock(true); 46 | // lute.SetGitConflict(true); 47 | lute.SetMark(true); 48 | lute.SetInlineAsterisk(window.siyuan.config.editor.markdown.inlineAsterisk); 49 | lute.SetInlineUnderscore(window.siyuan.config.editor.markdown.inlineUnderscore); 50 | lute.SetSup(window.siyuan.config.editor.markdown.inlineSup); 51 | lute.SetSub(window.siyuan.config.editor.markdown.inlineSub); 52 | lute.SetTag(window.siyuan.config.editor.markdown.inlineTag); 53 | lute.SetInlineMath(window.siyuan.config.editor.markdown.inlineMath); 54 | lute.SetGFMStrikethrough1(false); 55 | lute.SetGFMStrikethrough(window.siyuan.config.editor.markdown.inlineStrikethrough); 56 | lute.SetMark(window.siyuan.config.editor.markdown.inlineMark); 57 | lute.SetProtyleWYSIWYG(true); 58 | // if (options.lazyLoadImage) { 59 | // lute.SetImageLazyLoading(options.lazyLoadImage); 60 | // } 61 | lute.SetBlockRef(true); 62 | lute.SetHTMLTag2TextMark(true) 63 | if (window.siyuan.emojis[0].items.length > 0) { 64 | const emojis = {}; 65 | window.siyuan.emojis[0].items.forEach(item => { 66 | emojis[item.keywords] = options.emojiSite + "/" + item.unicode; 67 | }); 68 | lute.PutEmojis(emojis); 69 | } 70 | return lute; 71 | }; 72 | -------------------------------------------------------------------------------- /src/utils/style.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-06-23 17:52:18 5 | * @FilePath : /src/utils/style.ts 6 | * @LastEditTime : 2024-06-23 17:54:03 7 | * @Description : 8 | */ 9 | export const updateStyleDom = (domId: string, css: string) => { 10 | let style: HTMLStyleElement = document.getElementById(domId) as HTMLStyleElement; 11 | if (!style) { 12 | style = document.createElement('style'); 13 | style.id = domId; 14 | document.head.appendChild(style); 15 | } 16 | style.innerHTML = css; 17 | } 18 | 19 | export const removeStyleDom = (domId: string) => { 20 | const style = document.querySelector(`style#${domId}`); 21 | style?.remove(); 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/utils/time.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-07-11 14:21:11 5 | * @FilePath : /src/utils/time.ts 6 | * @LastEditTime : 2024-07-14 21:52:43 7 | * @Description : 8 | */ 9 | 10 | /** 11 | * 将 SiYuan block 中的时间格式转换为 Date 12 | * @param fmt 13 | * @returns 14 | */ 15 | export const sy2Date = (fmt: string): Date => { 16 | // Extract year, month, day, hour, minute, and second from the fmt string 17 | const year = parseInt(fmt.slice(0, 4), 10); 18 | const month = parseInt(fmt.slice(4, 6), 10) - 1; // Month is 0-indexed in JavaScript Date 19 | const day = parseInt(fmt.slice(6, 8), 10); 20 | const hour = parseInt(fmt.slice(8, 10), 10); 21 | const minute = parseInt(fmt.slice(10, 12), 10); 22 | const second = parseInt(fmt.slice(12, 14), 10); 23 | 24 | // Create a new Date object using the extracted values 25 | return new Date(year, month, day, hour, minute, second); 26 | } 27 | 28 | export function formatDate(date?: Date, sep=''): string { 29 | date = date === undefined ? new Date() : date; 30 | let year = date.getFullYear(); 31 | let month = date.getMonth() + 1; 32 | let day = date.getDate(); 33 | return `${year}${sep}${month < 10 ? '0' + month : month}${sep}${day < 10 ? '0' + day : day}`; 34 | } 35 | 36 | const renderString = (template: string, data: { [key: string]: string }) => { 37 | for (let key in data) { 38 | template = template.replace(key, data[key]); 39 | } 40 | return template; 41 | } 42 | 43 | /** 44 | * yyyy-MM-dd HH:mm:ss 45 | * @param template 46 | * @param now 47 | * @returns 48 | */ 49 | export const formatDateTime = (template: string, now?: Date) => { 50 | now = now || new Date(); 51 | let year = now.getFullYear(); 52 | let month = now.getMonth() + 1; 53 | let day = now.getDate(); 54 | let hour = now.getHours(); 55 | let minute = now.getMinutes(); 56 | let second = now.getSeconds(); 57 | return renderString(template, { 58 | 'yyyy': year.toString(), 59 | 'MM': month.toString().padStart(2, '0'), 60 | 'dd': day.toString().padStart(2, '0'), 61 | 'HH': hour.toString().padStart(2, '0'), 62 | 'mm': minute.toString().padStart(2, '0'), 63 | 'ss': second.toString().padStart(2, '0'), 64 | 'yy': year.toString().slice(-2), 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /translation.txt: -------------------------------------------------------------------------------- 1 | - **Task**: Translate the given readme document into English. 2 | - **Requirements**: 3 | - Retain the original Markdown formatting of the text 4 | - No need to output intermediate text. After you finished the translation, offer me the translated markdown file to download. 5 | - The file is quite long, please follow the procedure. 6 | - **Procedure**: 7 | 1. Esimate the length of whole document. 8 | 2. Split the total document to several chunks (BUT DO NOT BREAK THE NATRUAL LANGUAGE's SYNTAX), each of which you are capable of handling once. 9 | 3. Translate every chunk 10 | 4. Merge the translation result, check and validate the result 11 | 5. Output them into one file, and offer the result file to me. 12 | - **Vocabulary**: 13 | - 思源翻译为 SiYuan 14 | - QueryView, DataView 这些专有名词, 不用翻译, 保持原名 15 | - "视图组件" 翻译成 "View Component" 16 | - "块" 是思源笔记中的基本单位,翻译成 Block 17 | - 嵌入块 翻译成 Embed Block 18 | - "反向链接" 或者 "反链" 翻译成 "backlink" 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": [ 7 | "DOM", 8 | "DOM.Iterable", 9 | "ESNext", 10 | "ESNext.WeakRef", 11 | "ES2023", 12 | "ES2023.Array" 13 | ], 14 | "skipLibCheck": true, 15 | "noImplicitAny": false, 16 | /* Bundler mode */ 17 | "moduleResolution": "Node", 18 | // "allowImportingTsExtensions": true, 19 | "allowSyntheticDefaultImports": true, 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | // "noEmit": true, 23 | "jsx": "preserve", 24 | /* Linting */ 25 | "strict": false, 26 | "noUnusedLocals": false, 27 | "noUnusedParameters": false, 28 | "noFallthroughCasesInSwitch": true, 29 | /* Svelte */ 30 | /** 31 | * Typecheck JS in `.svelte` and `.js` files by default. 32 | * Disable checkJs if you'd like to use dynamic types in JS. 33 | * Note that setting allowJs false does not prevent the use 34 | * of JS in `.svelte` files. 35 | */ 36 | "allowJs": true, 37 | // "checkJs": true, 38 | "types": [ 39 | "node", 40 | // "vite/client", 41 | // "svelte" 42 | ], 43 | // "baseUrl": "./src", 44 | "typeRoots": [ 45 | "./src/types", 46 | "./node_modules/@types" 47 | ], 48 | "paths": { 49 | "@/*": ["./src/*"], 50 | "@/libs/*": ["./src/libs/*"], 51 | }, 52 | "noEmitOnError": false, // 强制生成文件 53 | }, 54 | "include": [ 55 | "src/**/*", 56 | "src/types/**/*" 57 | ], 58 | "references": [ 59 | { 60 | "path": "./tsconfig.node.json" 61 | } 62 | ], 63 | "root": "." 64 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": [ 10 | "vite.config.ts" 11 | ] 12 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { defineConfig, loadEnv } from "vite" 3 | import { viteStaticCopy } from "vite-plugin-static-copy" 4 | import livereload from "rollup-plugin-livereload" 5 | import zipPack from "vite-plugin-zip-pack"; 6 | import fg from 'fast-glob'; 7 | 8 | import vitePluginYamlI18n from './yaml-plugin'; 9 | 10 | const env = process.env; 11 | const isSrcmap = env.VITE_SOURCEMAP === 'inline'; 12 | const isDev = env.NODE_ENV === 'development'; 13 | const minify = env.NO_MINIFY ? false : true; 14 | 15 | const outputDir = isDev ? "dev" : "dist"; 16 | 17 | console.log("isDev=>", isDev); 18 | console.log("isSrcmap=>", isSrcmap); 19 | console.log("outputDir=>", outputDir); 20 | 21 | export default defineConfig({ 22 | resolve: { 23 | alias: { 24 | "@": resolve(__dirname, "src"), 25 | } 26 | }, 27 | 28 | plugins: [ 29 | vitePluginYamlI18n({ 30 | inDir: 'public/i18n', 31 | outDir: `${outputDir}/i18n` 32 | }), 33 | 34 | viteStaticCopy({ 35 | targets: [ 36 | { src: "./README*.md", dest: "./" }, 37 | { src: "./CHANGELOG.md", dest: "./" }, 38 | { src: "./plugin.json", dest: "./" }, 39 | { src: "./preview.png", dest: "./" }, 40 | { src: "./icon.png", dest: "./" } 41 | ], 42 | }), 43 | 44 | ], 45 | 46 | define: { 47 | "process.env.DEV_MODE": JSON.stringify(isDev), 48 | "process.env.NODE_ENV": JSON.stringify(env.NODE_ENV) 49 | }, 50 | 51 | build: { 52 | outDir: outputDir, 53 | emptyOutDir: false, 54 | minify: minify ?? true, 55 | sourcemap: isSrcmap ? 'inline' : false, 56 | 57 | lib: { 58 | entry: resolve(__dirname, "src/index.ts"), 59 | fileName: "index", 60 | formats: ["cjs"], 61 | }, 62 | rollupOptions: { 63 | plugins: [ 64 | ...(isDev ? [ 65 | livereload(outputDir), 66 | { 67 | name: 'watch-external', 68 | async buildStart() { 69 | const files = await fg([ 70 | 'public/i18n/**', 71 | './README*.md', 72 | './plugin.json' 73 | ]); 74 | for (let file of files) { 75 | this.addWatchFile(file); 76 | } 77 | } 78 | }, 79 | replaceMDVars(outputDir), 80 | replaceMDImgUrl(outputDir) 81 | ] : [ 82 | // Clean up unnecessary files under dist dir 83 | cleanupDistFiles({ 84 | patterns: ['i18n/*.yaml', 'i18n/*.md'], 85 | distDir: outputDir 86 | }), 87 | replaceMDVars(outputDir), 88 | replaceMDImgUrl(outputDir), 89 | zipPack({ 90 | inDir: './dist', 91 | outDir: './', 92 | outFileName: 'package.zip' 93 | }) 94 | ]) 95 | ], 96 | 97 | external: ["siyuan", "process"], 98 | 99 | output: { 100 | entryFileNames: "[name].js", 101 | assetFileNames: (assetInfo) => { 102 | if (assetInfo.name === "style.css") { 103 | return "index.css" 104 | } 105 | return assetInfo.name 106 | }, 107 | }, 108 | }, 109 | } 110 | }); 111 | 112 | 113 | /** 114 | * Clean up some dist files after compiled 115 | * @author frostime 116 | * @param options: 117 | * @returns 118 | */ 119 | function cleanupDistFiles(options: { patterns: string[], distDir: string }) { 120 | const { 121 | patterns, 122 | distDir 123 | } = options; 124 | 125 | return { 126 | name: 'rollup-plugin-cleanup', 127 | enforce: 'post', 128 | writeBundle: { 129 | sequential: true, 130 | order: 'post' as 'post', 131 | async handler() { 132 | const fg = await import('fast-glob'); 133 | const fs = await import('fs'); 134 | // const path = await import('path'); 135 | 136 | // 使用 glob 语法,确保能匹配到文件 137 | const distPatterns = patterns.map(pat => `${distDir}/${pat}`); 138 | console.debug('Cleanup searching patterns:', distPatterns); 139 | 140 | const files = await fg.default(distPatterns, { 141 | dot: true, 142 | absolute: true, 143 | onlyFiles: false 144 | }); 145 | 146 | // console.info('Files to be cleaned up:', files); 147 | 148 | for (const file of files) { 149 | try { 150 | if (fs.default.existsSync(file)) { 151 | const stat = fs.default.statSync(file); 152 | if (stat.isDirectory()) { 153 | fs.default.rmSync(file, { recursive: true }); 154 | } else { 155 | fs.default.unlinkSync(file); 156 | } 157 | console.log(`Cleaned up: ${file}`); 158 | } 159 | } catch (error) { 160 | console.error(`Failed to clean up ${file}:`, error); 161 | } 162 | } 163 | } 164 | } 165 | }; 166 | } 167 | 168 | 169 | function replaceMDVars(dirname: string) { 170 | 171 | return { 172 | name: 'rollup-plugin-replace-md-vars', 173 | enforce: 'post', 174 | writeBundle: { 175 | sequential: true, 176 | order: 'post' as 'post', 177 | async handler() { 178 | const path = await import('path'); 179 | const fs = await import('fs'); 180 | 181 | const readFile = (filepath: string) => { 182 | return fs.readFileSync(filepath, 'utf8'); 183 | } 184 | 185 | const replaceMDFileVar = (dirname: string, varVal: Record) => { 186 | const replace = (filepath: string) => { 187 | let md = readFile(filepath); 188 | for (const [key, value] of Object.entries(varVal)) { 189 | //@ts-ignore 190 | md = md.replaceAll(key, value); 191 | } 192 | fs.writeFileSync(filepath, md); 193 | } 194 | 195 | // 遍历所有 README*.md 文件 196 | const files = fs.readdirSync(dirname).filter(file => file.startsWith('README') && file.endsWith('.md')); 197 | for (const file of files) { 198 | replace(path.join(dirname, file)); 199 | } 200 | } 201 | console.log('Replace MD vars under:', dirname); 202 | const jsonfile = './types/types.d.ts.json'; 203 | const cache = JSON.parse(fs.readFileSync(jsonfile, 'utf8')); 204 | replaceMDFileVar(dirname, cache); 205 | } 206 | } 207 | }; 208 | 209 | } 210 | 211 | 212 | function replaceMDImgUrl(dirname: string) { 213 | return { 214 | name: 'rollup-plugin-replace-md-img-url', 215 | enforce: 'post', 216 | writeBundle: { 217 | sequential: true, 218 | order: 'post' as 'post', 219 | async handler() { 220 | const fs = await import('fs'); 221 | const { resolve } = await import('path'); 222 | 223 | console.log('Replace MD image URLs under:', dirname); 224 | 225 | const replace = async (readmePath: string, prefix: string) => { 226 | if (prefix.endsWith('/')) { 227 | prefix = prefix.slice(0, -1); 228 | } 229 | function replaceImageUrl(url: string) { 230 | // Replace with your desired image hosting URL 231 | if (url.startsWith('assets/')) { 232 | return `${prefix}/${url}`; 233 | } 234 | return url; 235 | } 236 | 237 | try { 238 | let readmeContent = fs.readFileSync(readmePath, 'utf-8'); 239 | 240 | // Regular expression to match Markdown image syntax 241 | // Matches both with and without title/alt text 242 | const imageRegex = /!\[([^\]]*)\]\(([^)\s"]+)(?:\s+"([^"]*)")?\)/g; 243 | 244 | // Replace all image URLs in the content 245 | const updatedReadmeContent = readmeContent.replace( 246 | imageRegex, 247 | (match, alt, url, title) => { 248 | const newUrl = replaceImageUrl(url); 249 | // If there was a title, include it in the new markdown 250 | if (title) { 251 | return `![${alt}](${newUrl} "${title}")`; 252 | } 253 | // Otherwise just return the image with alt text 254 | return `![${alt}](${newUrl})`; 255 | } 256 | ); 257 | 258 | // Write the updated content back to the file 259 | fs.writeFileSync(readmePath, updatedReadmeContent, 'utf-8'); 260 | console.log(`Successfully updated ${readmePath}`); 261 | 262 | } catch (error) { 263 | console.error(`Error processing ${readmePath}:`, error); 264 | } 265 | } 266 | 267 | const prefix_github = 'https://github.com/frostime/sy-query-view/raw/main'; 268 | const prefix_cdn = 'https://cdn.jsdelivr.net/gh/frostime/sy-query-view@main'; 269 | // const prefix_cdn = 'https://ghgo.xyz/https://github.com/frostime/sy-query-view/raw/main'; 270 | 271 | let readmePath = resolve(dirname, 'README.md'); 272 | await replace(readmePath, prefix_github); 273 | readmePath = resolve(dirname, 'README_zh_CN.md'); 274 | await replace(readmePath, prefix_cdn); 275 | } 276 | } 277 | }; 278 | } 279 | -------------------------------------------------------------------------------- /yaml-plugin.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 by frostime. All Rights Reserved. 3 | * @Author : frostime 4 | * @Date : 2024-04-05 21:27:55 5 | * @FilePath : /yaml-plugin.js 6 | * @LastEditTime : 2024-04-05 22:53:34 7 | * @Description : 去妮玛的 json 格式,我就是要用 yaml 写 i18n 8 | */ 9 | // plugins/vite-plugin-parse-yaml.js 10 | import fs from 'fs'; 11 | import yaml from 'js-yaml'; 12 | import { resolve } from 'path'; 13 | 14 | export default function vitePluginYamlI18n(options = {}) { 15 | // Default options with a fallback 16 | const DefaultOptions = { 17 | inDir: 'src/i18n', 18 | outDir: 'dist/i18n', 19 | }; 20 | 21 | const finalOptions = { ...DefaultOptions, ...options }; 22 | 23 | return { 24 | name: 'vite-plugin-yaml-i18n', 25 | buildStart() { 26 | console.log('🌈 Parse I18n: YAML to JSON..'); 27 | const inDir = finalOptions.inDir; 28 | const outDir = finalOptions.outDir 29 | 30 | if (!fs.existsSync(outDir)) { 31 | fs.mkdirSync(outDir, { recursive: true }); 32 | } 33 | 34 | //Parse yaml file, output to json 35 | const files = fs.readdirSync(inDir); 36 | for (const file of files) { 37 | if (file.endsWith('.yaml') || file.endsWith('.yml')) { 38 | console.log(`-- Parsing ${file}`) 39 | //检查是否有同名的json文件 40 | const jsonFile = file.replace(/\.(yaml|yml)$/, '.json'); 41 | if (files.includes(jsonFile)) { 42 | console.log(`---- File ${jsonFile} already exists, skipping...`); 43 | continue; 44 | } 45 | try { 46 | const filePath = resolve(inDir, file); 47 | const fileContents = fs.readFileSync(filePath, 'utf8'); 48 | const parsed = yaml.load(fileContents); 49 | const jsonContent = JSON.stringify(parsed, null, 2); 50 | const outputFilePath = resolve(outDir, file.replace(/\.(yaml|yml)$/, '.json')); 51 | console.log(`---- Writing to ${outputFilePath}`); 52 | fs.writeFileSync(outputFilePath, jsonContent); 53 | } catch (error) { 54 | this.error(`---- Error parsing YAML file ${file}: ${error.message}`); 55 | } 56 | } 57 | } 58 | }, 59 | }; 60 | } 61 | --------------------------------------------------------------------------------