├── .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 | - ![command](./assets/command.jpg) 13 | - Block menu item: `Save link assets to local` 14 | - ![block_menu_item](./assets/block_menu_item.jpg) 15 | - Page menu item: `Save all link assets to local` 16 | - ![page_menu_item](./assets/page_menu_item.jpg) 17 | - ## Demo 18 | - ![gif](./logseq-link-to-local.gif) 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 | - ![command](./assets/command.jpg) 12 | - 点击块级菜单项: `Save link assets to local` 13 | - ![block_menu_item](./assets/block_menu_item.jpg) 14 | - 点击页面菜单项: `Save all link assets to local` 15 | - ![page_menu_item](./assets/page_menu_item.jpg) 16 | - ## 演示 17 | - ![gif](./logseq-link-to-local.gif) 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 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | b-yp 24 | 25 | 26 | -------------------------------------------------------------------------------- /logo1.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 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 | --------------------------------------------------------------------------------