├── .eslintrc.json
├── .github
└── workflows
│ └── main.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── README_CN.md
├── assets
├── block_menu_item.jpg
├── command.jpg
└── page_menu_item.jpg
├── index.html
├── logo.svg
├── logo1.svg
├── logseq-link-to-local.gif
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── release.config.js
├── renovate.json
├── src
├── App.tsx
├── index.css
├── main.tsx
├── types.ts
└── utils.ts
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "eslint:recommended",
4 | "plugin:react/recommended",
5 | "plugin:@typescript-eslint/eslint-recommended",
6 | "plugin:@typescript-eslint/recommended"
7 | ],
8 | "plugins": ["@typescript-eslint", "react-hooks"],
9 | "parser": "@typescript-eslint/parser",
10 | "rules": {
11 | "react-hooks/rules-of-hooks": "error",
12 | "react-hooks/exhaustive-deps": "warn",
13 | "import/prefer-default-export": "off",
14 | "@typescript-eslint/ban-ts-comment": "off",
15 | "@typescript-eslint/no-non-null-assertion": "off",
16 | "@typescript-eslint/explicit-module-boundary-types": "off"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: Releases
4 |
5 | env:
6 | PLUGIN_NAME: logseq-link-to-local
7 |
8 | # Controls when the action will run.
9 | on:
10 | push:
11 | branches:
12 | - "master"
13 | # Allows you to run this workflow manually from the Actions tab
14 | workflow_dispatch:
15 |
16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
17 | jobs:
18 | release:
19 | # The type of runner that the job will run on
20 | runs-on: ubuntu-latest
21 |
22 | # Steps represent a sequence of tasks that will be executed as part of the job
23 | steps:
24 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
25 | - uses: actions/checkout@v3
26 | - uses: actions/setup-node@v3
27 | with:
28 | node-version: "18"
29 | - uses: pnpm/action-setup@v4
30 | with:
31 | version: 9
32 | - run: pnpm install
33 | - run: pnpm build
34 | - name: Install zip
35 | uses: montudor/action-zip@v1
36 | - name: Release
37 | run: npx semantic-release
38 | env:
39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [1.4.1](https://github.com/b-yp/logseq-link-to-local/compare/v1.4.0...v1.4.1) (2023-06-20)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * Date function usage error ([bb6d1c7](https://github.com/b-yp/logseq-link-to-local/commit/bb6d1c779bf735ca092b1855f79fc1f7b762d8fe))
7 |
8 | # [1.4.0](https://github.com/b-yp/logseq-link-to-local/compare/v1.3.0...v1.4.0) (2023-06-20)
9 |
10 |
11 | ### Bug Fixes
12 |
13 | * Fix the issue of images being replaced due to having the same name. & Matching juejin Images ([15e8939](https://github.com/b-yp/logseq-link-to-local/commit/15e893917ca7c35df635bf7d7e79af5215172df7))
14 |
15 |
16 | ### Features
17 |
18 | * update logo ([a5ae36f](https://github.com/b-yp/logseq-link-to-local/commit/a5ae36fe553713bd9fcbcd009192fbb3152f7283))
19 |
20 | # [1.3.0](https://github.com/b-yp/logseq-link-to-local/compare/v1.2.0...v1.3.0) (2023-06-19)
21 |
22 |
23 | ### Features
24 |
25 | * update readme ([321183c](https://github.com/b-yp/logseq-link-to-local/commit/321183c1acf2d1ed6e8a22d3dd6f4763b0b18670))
26 |
27 | # [1.2.0](https://github.com/b-yp/logseq-link-to-local/compare/v1.1.0...v1.2.0) (2023-06-19)
28 |
29 |
30 | ### Features
31 |
32 | * Add block menu item & Add all save menu ([7929bfb](https://github.com/b-yp/logseq-link-to-local/commit/7929bfb7fc88563400078c686b07cba00253df18))
33 | * Add webp image format ([ab49de9](https://github.com/b-yp/logseq-link-to-local/commit/ab49de9b5c886451cf70c8c73b3df82e710a74c5))
34 |
35 | # [1.1.0](https://github.com/b-yp/logseq-link-to-local/compare/v1.0.0...v1.1.0) (2023-06-17)
36 |
37 |
38 | ### Features
39 |
40 | * improve matching logic ([4b9d3e5](https://github.com/b-yp/logseq-link-to-local/commit/4b9d3e566312879a7f37eb2df906334c80b484d5))
41 |
42 | # 1.0.0 (2023-06-16)
43 |
44 |
45 | ### Bug Fixes
46 |
47 | * fix build error ([62d0560](https://github.com/b-yp/logseq-link-to-local/commit/62d0560f92445d649b8b506e0631c9738167cad6))
48 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 b-yp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English | [中文](./README_CN.md)
2 |
3 | - # logseq-link-to-local
4 | - Convert network assets into local assets.
5 | - ## Features
6 | - Image
7 | - Audio
8 | - Video
9 | - To be supplemented ...
10 | - ## Usage
11 | - Enter slash command: `/Save link assets to local`
12 | - 
13 | - Block menu item: `Save link assets to local`
14 | - 
15 | - Page menu item: `Save all link assets to local`
16 | - 
17 | - ## Demo
18 | - 
19 | - http://img.ypll.xyz/logseq/logseq-link-to-local.mp4
20 | - ## License
21 | - [MIT](https://choosealicense.com/licenses/mit/)
22 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | 中文 | [English](./README.md)
2 | - # logseq-link-to-local
3 | - 将网络资源转换成本地资源
4 | - ## 功能
5 | - Image
6 | - Audio
7 | - Video
8 | - 待补充...
9 | - ## 用法
10 | - 输入斜杠命令: `/Save link assets to local`
11 | - 
12 | - 点击块级菜单项: `Save link assets to local`
13 | - 
14 | - 点击页面菜单项: `Save all link assets to local`
15 | - 
16 | - ## 演示
17 | - 
18 | - http://img.ypll.xyz/logseq/logseq-link-to-local.mp4
19 | - ## 许可证
20 | - [MIT](https://choosealicense.com/licenses/mit/)
21 |
--------------------------------------------------------------------------------
/assets/block_menu_item.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/b-yp/logseq-link-to-local/cba9819208bf2eecb9296439b68c2773522c74b7/assets/block_menu_item.jpg
--------------------------------------------------------------------------------
/assets/command.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/b-yp/logseq-link-to-local/cba9819208bf2eecb9296439b68c2773522c74b7/assets/command.jpg
--------------------------------------------------------------------------------
/assets/page_menu_item.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/b-yp/logseq-link-to-local/cba9819208bf2eecb9296439b68c2773522c74b7/assets/page_menu_item.jpg
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Logseq Plugin
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
26 |
--------------------------------------------------------------------------------
/logo1.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/logseq-link-to-local.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/b-yp/logseq-link-to-local/cba9819208bf2eecb9296439b68c2773522c74b7/logseq-link-to-local.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logseq-link-to-local",
3 | "version": "1.4.1",
4 | "main": "dist/index.html",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "tsc && vite build",
8 | "preinstall": "npx only-allow pnpm"
9 | },
10 | "license": "MIT",
11 | "dependencies": {
12 | "@logseq/libs": "^0.0.14",
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0"
15 | },
16 | "devDependencies": {
17 | "@semantic-release/changelog": "6.0.3",
18 | "@semantic-release/exec": "6.0.3",
19 | "@semantic-release/git": "10.0.1",
20 | "@semantic-release/npm": "10.0.3",
21 | "@types/node": "18.16.5",
22 | "@types/react": "18.2.6",
23 | "@types/react-dom": "18.2.4",
24 | "@typescript-eslint/eslint-plugin": "5.59.2",
25 | "@typescript-eslint/parser": "5.59.2",
26 | "@vitejs/plugin-react": "3.1.0",
27 | "autoprefixer": "10.4.14",
28 | "conventional-changelog-conventionalcommits": "5.0.0",
29 | "eslint": "8.40.0",
30 | "eslint-plugin-react": "7.32.2",
31 | "eslint-plugin-react-hooks": "4.6.0",
32 | "postcss": "8.4.23",
33 | "semantic-release": "21.0.2",
34 | "tailwindcss": "3.3.2",
35 | "typescript": "4.9.5",
36 | "vite": "4.3.5",
37 | "vite-plugin-logseq": "1.1.2"
38 | },
39 | "logseq": {
40 | "id": "byp-logseq-link-to-local",
41 | "icon": "./logo.svg"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | plugins: {
4 | tailwindcss: {},
5 | autoprefixer: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: ["master"],
3 | plugins: [
4 | [
5 | "@semantic-release/commit-analyzer",
6 | {
7 | preset: "conventionalcommits",
8 | },
9 | ],
10 | "@semantic-release/release-notes-generator",
11 | "@semantic-release/changelog",
12 | [
13 | "@semantic-release/npm",
14 | {
15 | npmPublish: false,
16 | },
17 | ],
18 | "@semantic-release/git",
19 | [
20 | "@semantic-release/exec",
21 | {
22 | prepareCmd:
23 | "zip -qq -r logseq-link-to-local-${nextRelease.version}.zip dist readme.md logo.svg LICENSE package.json",
24 | },
25 | ],
26 | [
27 | "@semantic-release/github",
28 | {
29 | assets: "logseq-link-to-local-*.zip",
30 | },
31 | ],
32 | ],
33 | };
34 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["config:base"],
3 | "packageRules": [
4 | {
5 | "matchUpdateTypes": ["minor", "patch"],
6 | "automerge": true,
7 | "requiredStatusChecks": null
8 | },
9 | {
10 | "matchPackageNames": ["@logseq/libs"],
11 | "ignoreUnstable": false,
12 | "automerge": false,
13 | "requiredStatusChecks": null
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from "react";
2 | import { useAppVisible } from "./utils";
3 |
4 | function App() {
5 | const innerRef = useRef(null);
6 | const visible = useAppVisible();
7 | if (visible) {
8 | return (
9 | {
12 | if (!innerRef.current?.contains(e.target as any)) {
13 | window.logseq.hideMainUI();
14 | }
15 | }}
16 | >
17 |
18 | Welcome to [[Logseq]] Plugins!
19 |
20 |
21 | );
22 | }
23 | return null;
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import "@logseq/libs";
2 | import { BlockEntity } from "@logseq/libs/dist/LSPlugin";
3 |
4 | import React from "react";
5 | import * as ReactDOM from "react-dom/client";
6 |
7 | import App from "./App";
8 | import { deepFirstTraversal, findImageLinks } from "./utils";
9 | import { logseq as PL } from "../package.json";
10 |
11 | import "./index.css";
12 |
13 | const pluginId = PL.id;
14 |
15 | const saveBlockAssets = (currentBlock: BlockEntity) => {
16 | const storage = logseq.Assets.makeSandboxStorage();
17 | // 传递 block ID 用于保证名称的唯一性
18 | const options = findImageLinks(currentBlock.content, currentBlock.id);
19 | const localPaths: string[] = [];
20 |
21 | const saveImages = (item: string, index: number) => {
22 | return new Promise((resolve, reject) => {
23 | fetch(item)
24 | .then((res: any) => {
25 | if (res.status !== 200) {
26 | logseq.UI.showMsg(
27 | `链接: ${item} 请求失败,请检查链接或者去掉链接参数(问号及后面的部分)试试`,
28 | "error"
29 | );
30 | return reject(res);
31 | }
32 | return res.arrayBuffer();
33 | })
34 | .then((res) => {
35 | storage
36 | .setItem(
37 | decodeURIComponent(
38 | `${options[index].name}.${options[index].type}`
39 | ),
40 | res as any
41 | )
42 | .then((one) => {
43 | logseq.UI.showMsg(`Write DONE 🎉 - ${one}`, "success");
44 | resolve((one as unknown as string).match(/\/assets\/(.*)/gi));
45 | });
46 | })
47 | .catch((error) => {
48 | logseq.UI.showMsg(
49 | Object.keys(error).length !== 0
50 | ? JSON.stringify(error.message || error)
51 | : "请求失败",
52 | "error"
53 | );
54 | reject(error);
55 | });
56 | });
57 | };
58 |
59 | Promise.all(
60 | options.map((item, index) => {
61 | /**
62 | * wps 便签图片带参请求会报错,所以针对 wps 便签图片单独处理,使用无参 url
63 | * wps 便签图片使用 s3 对象存储, 前缀为 "moffice-note"
64 | */
65 | const url = item.url?.includes("moffice-note")
66 | ? item.url
67 | : item.originalUrl;
68 | return saveImages(url as string, index);
69 | })
70 | )
71 | .then((paths) => {
72 | paths.forEach((path) => localPaths.push(`..${(path as string)[0]}`));
73 |
74 | let currentContent = currentBlock?.content;
75 | options.forEach((item, index) => {
76 | /**
77 | * 这种分两种情况
78 | * 1: markdown 格式图片
79 | * 2: 网络链接图片
80 | * 通过 image 是否为 null 判断
81 | */
82 | currentContent = item.mdImage
83 | ? currentContent?.replace(
84 | item.originalUrl as string,
85 | localPaths[index]
86 | )
87 | : currentContent?.replace(
88 | item.originalUrl as string,
89 | `![${options[index].name}](${localPaths[index]})`
90 | );
91 | });
92 |
93 | logseq.Editor.updateBlock(currentBlock?.uuid as string, currentContent);
94 | })
95 | .catch((error) => {
96 | logseq.UI.showMsg(
97 | JSON.stringify(
98 | Object.keys(error).length !== 0 ? error.message || error : "请求失败"
99 | ),
100 | "error"
101 | );
102 | });
103 | };
104 |
105 | function main() {
106 | console.info(`#${pluginId}: MAIN`);
107 |
108 | const root = ReactDOM.createRoot(document.getElementById("app")!);
109 |
110 | root.render(
111 |
112 |
113 |
114 | );
115 |
116 | logseq.App.registerPageMenuItem(
117 | "Save all link assets to local",
118 | async (e) => {
119 | const pageBlocksTree = await logseq.Editor.getPageBlocksTree(e.page);
120 |
121 | // 深度优先遍历执行保存方法
122 | deepFirstTraversal(pageBlocksTree, saveBlockAssets);
123 | }
124 | );
125 |
126 | logseq.Editor.registerBlockContextMenuItem(
127 | "Save link assets to local",
128 | async ({ uuid }) => {
129 | if (!uuid) return;
130 | const currentBlock = await logseq.Editor.getBlock(uuid);
131 | if (currentBlock === null) return;
132 | saveBlockAssets(currentBlock);
133 | }
134 | );
135 |
136 | logseq.Editor.registerSlashCommand("Save link assets to local", async () => {
137 | const currentBlock = await logseq.Editor.getCurrentBlock();
138 | if (currentBlock === null) return;
139 | saveBlockAssets(currentBlock);
140 | });
141 | }
142 |
143 | logseq.ready(main).catch(console.error);
144 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | enum Type {
2 | Text = 'text',
3 | Mention = 'mention',
4 | Equation = 'equation',
5 | }
6 |
7 | export interface RichText {
8 | type?: Type
9 | [Type.Text]: {
10 | content: string
11 | link?: string | null
12 | }
13 | }
14 |
15 | export interface ImageLink {
16 | mdImage: string | null
17 | originalUrl: string | undefined
18 | url: string | undefined
19 | params: string | undefined
20 | fullName: string | undefined
21 | name: string
22 | type: string
23 | description: string
24 | }
25 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { BlockEntity, LSPluginUserEvents } from "@logseq/libs/dist/LSPlugin.user";
2 | import React from "react";
3 |
4 | import { ImageLink } from "./types";
5 |
6 | let _visible = logseq.isMainUIVisible;
7 |
8 | function subscribeLogseqEvent(
9 | eventName: T,
10 | handler: (...args: any) => void
11 | ) {
12 | logseq.on(eventName, handler);
13 | return () => {
14 | logseq.off(eventName, handler);
15 | };
16 | }
17 |
18 | const subscribeToUIVisible = (onChange: () => void) =>
19 | subscribeLogseqEvent("ui:visible:changed", ({ visible }) => {
20 | _visible = visible;
21 | onChange();
22 | });
23 |
24 | export const useAppVisible = () => {
25 | return React.useSyncExternalStore(subscribeToUIVisible, () => _visible);
26 | };
27 |
28 | // https://github.com/b-yp/logseq-link-to-local/issues/20
29 | const replaceSpecialCharacters = (inputString: string): string => {
30 | const regex = /[/\\?%*:|"<>.;,= ]/g;
31 | const result = inputString.replace(regex, '_');
32 | return result;
33 | }
34 |
35 | // 用于匹配 markdown 格式图片和你 直接链接图片, GPT4 给的匹配函数
36 | export const findImageLinks = (text: string | undefined = '', id: number | undefined = 0): ImageLink[] => {
37 | if (!text) return [];
38 | const markdownRegex = /!\[([^\]]*)\]\((https?:\/\/[^)]*)\)/gi;
39 | const urlRegex = /(https?:\/\/[^\s]*\.(png|jpg|jpeg|gif|bmp|webp|mp3|wav|ogg|mp4|mov|avi|wmv|flv|pdf))([^\s(){}]*)/gi;
40 | const matches = [];
41 | let match;
42 | let index = 1
43 | const markdownUrls = [];
44 |
45 | while ((match = markdownRegex.exec(text)) !== null) {
46 | const fullName = match[2].split('/').pop()?.split('?')[0]
47 | /**
48 | * 看来这里用索引加时间戳只能保证在同一个块里是唯一的,不同块之间任然不能保证唯一
49 | * 有 3 种解决方案:1、加 block 在当前 page 的索引,2、加 block uuid, 3、加 block id
50 | * 用 id 吧
51 | */
52 | const name = `${fullName?.split('.')[0] || 'image'}_${id}_${index}_${Date.now()}`;
53 | // const type = fullName?.split('.')[1] || 'png';
54 | const url = match[2].split('?')[0];
55 | const originalUrl = match[2];
56 | const params = originalUrl.includes('?') ? originalUrl.split('?')[1] : undefined;
57 | const description = match[1];
58 | index += 1
59 |
60 | const getType = () => {
61 | if (!fullName?.split('.')[1]) {
62 | return 'png'
63 | }
64 | // 这里判断 awebp 主要是针对掘金图片做处理,掘金图片后缀是 awebp,需要替换成 webp
65 | if (fullName?.split('.')[1] === 'awebp') {
66 | return 'webp'
67 | }
68 | // 判断,如果获取到的格式不属于常规格式,则返回 png
69 | const exts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'mp3', 'wav', 'ogg', 'mp4', 'mov', 'avi', 'wmv', 'flv', 'pdf']
70 | if (!exts.includes(fullName?.split('.')[1])) {
71 | return 'png'
72 | }
73 | return fullName?.split('.')[1]
74 | }
75 |
76 | matches.push({
77 | mdImage: match[0],
78 | originalUrl,
79 | url,
80 | params,
81 | fullName,
82 | name: replaceSpecialCharacters(name),
83 | type: getType(),
84 | description
85 | });
86 |
87 | // 记录markdown中的URL,用于后续排除这些URL
88 | markdownUrls.push(originalUrl);
89 | }
90 |
91 | while ((match = urlRegex.exec(text)) !== null) {
92 | // 如果此URL已经在Markdown链接中,跳过
93 | if (markdownUrls.includes(match[0])) {
94 | continue;
95 | }
96 |
97 | const fullName = match[0].split('/').pop()?.split('?')[0];
98 | const name = `${fullName?.split('.')[0] || 'image'}_${id}_${index}_${Date.now()}`;
99 | const type = fullName?.split('.')[1] || 'png';
100 | const url = match[1].split('?')[0];
101 | const originalUrl = match[0];
102 | const params = originalUrl.includes('?') ? originalUrl.split('?')[1] : undefined;
103 | index += 1
104 |
105 | matches.push({
106 | mdImage: null,
107 | originalUrl,
108 | url,
109 | params,
110 | fullName,
111 | name: replaceSpecialCharacters(name),
112 | type,
113 | description: ''
114 | });
115 | }
116 |
117 | return matches;
118 | }
119 |
120 | /**
121 | * 深度优先遍历,递归实现
122 | * @param arr BlockEntity[]
123 | * @param fn (block: BlockEntity) => void
124 | */
125 | export const deepFirstTraversal = (arr: BlockEntity[], fn: (block: BlockEntity) => void) => {
126 | arr.forEach(obj => {
127 | console.log(obj.id); // 输出当前节点的 id
128 | if (obj) {
129 | fn(obj)
130 | }
131 | if (obj.children && obj.children.length > 0) {
132 | deepFirstTraversal(obj.children as BlockEntity[], fn); // 递归遍历子节点
133 | }
134 | });
135 | }
136 |
137 | /**
138 | * 深度优先遍历 block, 迭代实现
139 | */
140 | // const deepFirstTraversal = (obj) => {
141 | // const stack = [obj];
142 |
143 | // while (stack.length > 0) {
144 | // const current = stack.pop();
145 | // console.log(current.id); // 输出当前节点的 id
146 |
147 | // if (current.children.length > 0) {
148 | // stack.push(...current.children.reverse());
149 | // }
150 | // }
151 | // }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | content: [
4 | './src/**/*.{js,ts,jsx,tsx}'
5 | ],
6 | theme: {
7 | extend: {},
8 | },
9 | plugins: [],
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
5 | "types": ["vite/client"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react"
18 | },
19 | "include": ["./src"]
20 | }
21 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import reactPlugin from "@vitejs/plugin-react";
2 | import { defineConfig } from "vite";
3 | import logseqDevPlugin from "vite-plugin-logseq";
4 |
5 | // https://vitejs.dev/config/
6 | export default defineConfig({
7 | plugins: [logseqDevPlugin(), reactPlugin()],
8 | // Makes HMR available for development
9 | build: {
10 | target: "esnext",
11 | minify: "esbuild",
12 | },
13 | });
14 |
--------------------------------------------------------------------------------