├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md └── workflows │ └── release.yml ├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── esbuild.config.mjs ├── manifest.json ├── package.json ├── post-commit.sh ├── src ├── main.ts ├── util │ ├── CodeBlockProcessor.ts │ ├── d3-force-3d.d.ts │ ├── query.ts │ └── util.ts └── views │ ├── TagsRoutes.ts │ ├── settings.ts │ └── tag-report.js.txt ├── styles.css ├── tsconfig.json ├── usage ├── node-highlight.gif ├── setup-color-v109.gif ├── setup-color.gif ├── switch-settings.gif ├── v1.1.0-defaultLightTheme.gif ├── v1.1.0-usage.gif ├── v1.1.1-feature.gif ├── v1.1.3-feature.gif ├── v1.1.8-feature.gif ├── v1.2.1-lockScene.gif ├── v1.2.1-setFocus.gif ├── v1.2.3-feature1.png ├── v1.2.3-feature2.png ├── v109-update.gif └── v120-feature.gif ├── version-bump.mjs └── versions.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | 29 | **Additional context** 30 | Add any other context about the problem here. 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Use Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: "18.x" 19 | 20 | - name: Build plugin 21 | run: | 22 | npm install 23 | npm run build 24 | 25 | - name: Create release 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | run: | 29 | tag="${GITHUB_REF#refs/tags/}" 30 | 31 | gh release create "$tag" \ 32 | --title="$tag" \ 33 | --draft \ 34 | main.js manifest.json styles.css 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | /version_info.txt 24 | /package-lock.json 25 | /.tgitconfig 26 | /src/version_info.txt 27 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 kctekn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian plugin: TagsRoutes 2 |
3 | GitHub Release 4 | GitHub Release 5 |
6 | 7 | This is a plugin for obsidian, to visualize files and tags as nodes in 3D graphic. 8 | ![title](https://github.com/user-attachments/assets/4033af48-3dd7-4cbf-948f-02a03cf29189) 9 | 10 | 11 | 12 | 13 | Wiki: 14 | 15 | [Organize Tags by Timestamp Using the Obsidian Plugin: "Tags Routes"](https://github.com/kctekn/obsidian-TagsRoutes/wiki/Organize-Tags-by-Timestamp-Using-the-Obsidian-Plugin:-%22Tags-Routes%22) 16 | 17 | [Organize Tags with Hierarchy Using the Obsidian Plugin "Tags Routes"](https://github.com/kctekn/obsidian-TagsRoutes/wiki/Organize-Tags-with-Hierarchy-Using-the-Obsidian-Plugin-%22Tags-Routes%22) 18 | 19 | And you can show up your beautiful vault pictures here: [Share & showcase](https://github.com/kctekn/obsidian-TagsRoutes/discussions/17) 20 | ## Version 1.2.3 Release Notes: 21 | 22 | 23 | ### New Features: 24 | 25 | 1. **Path Filter**: 26 | - You can now use both positive and negative filters to customize the display of your vault content. 27 | - This allows for more precise control over what is shown, as illustrated below. 28 | 29 | 30 | 31 | 32 | 33 | https://github.com/user-attachments/assets/62c32f11-977b-4d38-b33f-835963125712 34 | 35 | 36 | 37 | 2. **Open Scene in Sidebar as a Navigator**: 38 | - A new button has been added to the "Display Control" section, enabling you to open a scene in the sidebar for easier navigation. 39 | 40 | 41 | 42 | 43 | 44 | https://github.com/user-attachments/assets/6c734a67-1325-4466-b2cb-ec66445d0e71 45 | 46 | 47 | 48 | ## Version 1.2.2 Bug Fix Release: 49 | 50 | - **Bug fix:** LMB+Drag and RMB+Drag has no effect 51 | 52 | Refer to https://github.com/kctekn/obsidian-TagsRoutes/issues/34 for detail. 53 | 54 | This appears to be an issue introduced in Obsidian v1.7.x, now been fixed. 55 | 56 | ## Version 1.2.1 Release Notes: 57 | 58 | ### New Features: 59 | 60 | - **Lock Scene**: 61 | - When enabled, nodes within the visual will no longer change or update automatically, and the view angle will remain fixed unless manually adjusted by dragging. 62 | - This is useful when you want to focus on a specific sub-network within a vault without distractions. 63 | 64 | 65 | 66 | - **View Distance Tuning**: 67 | - You can now set the current view distance relative to the focused node as a global setting. This ensures that the view distance for all other nodes is automatically adjusted based on their size. 68 | - This provides an optimal way to adjust the view distance for a more comfortable and consistent viewing experience across the scene. 69 | 70 | 71 | 72 | ## Version 1.2.0 Release Notes: 73 | 74 | - **Time Lapse Animation Feature:** 75 | - **Vault Growth Visualization:** You can now initiate an animation that showcases how the vault has grown over time. 76 | - **Display Settings:** Ensure these are adjusted appropriately before starting the animation for optimal viewing. 77 | - **Interactive Controls:** 78 | - Press the **Animate** button to begin the visualization. 79 | - The animation can be **paused** or **stopped** as needed. 80 | 81 | Have fun exploring the progression of your vault with this engaging feature. 82 | 83 | 84 | ## Version 1.1.8 Release Notes: 85 | 86 | ### 1. New feature: 87 | - **Selection Box for Enhanced Clarity:** A selection box has been added to clearly highlight the currently selected node. This visual cue makes it easier to track your position within the graph and navigate your tags effectively. (See GIF for a demonstration) 88 | 89 | 90 | 91 | ### 2. Bug fix: 92 | - **Global Preference for Tag Click Actions Now Functional:** Previously, disabling tag click actions in the global plugin preferences did not work as intended. This issue has been resolved, and you can now reliably control this behavior through the plugin settings. (See issue https://github.com/kctekn/obsidian-TagsRoutes/issues/28 for more details) 93 | 94 | ## Version 1.1.5/1.1.6 Release Notes: 95 | 96 | ### 1. Tag Query Enhancements: 97 | - **Time-based tag queries:** 98 | You can now query content associated with time-sensitive tags like `#1day`, `#30day`, `#60day`, etc. 99 | - **How to use it:** 100 | 1. Add time-based tags (e.g., `#1day`, `#30day`) anywhere in your notes. 101 | 1. Note the time of the tags will be timestamp you added or the file's creation time. 102 | 2. Refer to [Organize Tags by Timestamp Using the Obsidian Plugin: "Tags Routes"](https://github.com/kctekn/obsidian-TagsRoutes/wiki/Organize-Tags-by-Timestamp-Using-the-Obsidian-Plugin:-%22Tags-Routes%22) for the detail. 103 | 3. Restart Obsidian to allow the plugin to re-index the tags. 104 | 4. Navigate to the plugin's interface, find the tag, and click on the corresponding node. 105 | 5. You'll receive a report summarizing the content tagged within the specified time period. 106 | - **Direct linking to paragraphs:** 107 | Instead of linking only to entire notes, the plugin now creates direct links to specific paragraphs, improving navigation accuracy. 108 | At the same time you can turn off this in the main settings tab. 109 | - **Performance optimization:** 110 | Query performance has been significantly improved for faster and more efficient results. 111 | 112 | ### 2. New Option: "Auto-Focus File in Explorer" 113 | - **Toggle Auto-Focus:** 114 | A new option has been added to toggle the auto-focus feature in the file explorer. This was the default behavior in previous versions, but now you can turn it off if it causes issues or if you don’t always need the file explorer to focus on the current file. 115 | - **Enhanced Functionality:** 116 | When auto-focus is enabled, deleting items in the canvas will no longer trigger unintended reactions in the file explorer. 117 | 118 | For the details, please check https://github.com/kctekn/obsidian-TagsRoutes/issues/24 . Thanks to @scriptingtest for the detailed feedback! 119 | 120 | ### Version 1.1.6 Bug Fix Release 121 | 122 | 1. The issue when multiple notes has the same name in different directories. 123 | 2. The issue of identical queries to be executed multiple times unnecessarily. 124 | 125 | 126 | ## Version 1.1.3/1.1.4 Release Notes 127 | 128 | This release introduces major updates to screenshots, enhancements to graph display control, and provides more choice in how you interact with tags nodes. 129 | 130 | **1. Major Updates:** 131 | 132 | * **Screenshot Functionality:** Capture your current graph as an image! You can now save the graph as a picture and insert it directly into your note or save it to a designated snapshot folder. 133 | * **New "Screenshot" Node Type:** A new node type dedicated to screenshots has been added. Customize its color and use it to easily manage and check for orphan files. 134 | 135 | **2. Graph Display Enhancements:** 136 | 137 | * **Adjustable Node Label Color:** Easily adjust the color of node labels using a slider. This allows you to automatically find a clear and readable text color against the background. 138 | * **Tunable Bloom in Dark Mode:** Fine-tune the bloom strength in dark mode for optimal visual appeal. 139 | * **Lock Node Positions:** Lock the position of nodes to improve performance, especially on lower-end devices. 140 | * **Disable Link Particles:** Set the link particle number to 0 to disable the particle effect and boost performance. 141 | * **New "Track" Highlight Mode:** Introducing a new highlight mode called "Track." When enabled, clicking a node will highlight all directly and indirectly connected nodes, allowing you to easily visualize sub-networks within your larger graph. 142 | 143 | **3. Choice of Tags Node Interaction:** 144 | 145 | * **Enable/Disable Tags Query:** Choose whether to enable the tag query function when clicking on tag nodes. Disable this feature if you don't rely on tags for note management. 146 | * **"TagsRoutes" Folder Removal:** If you disable the following three options: 147 | * 'Log node/link Count' 148 | * 'Show log file on Startup' 149 | * 'Enable tag click action ' 150 | 151 | You can safely delete the 'TagsRoutes' folder in your vault, as the plugin will no longer utilize it. 152 | 153 | ### Version 1.1.4 Bug Fix Release 154 | 155 | 1. Fix a bug that the setting of 'highlight track mode' lost update when switch slot. 156 | 157 | 158 | _*The usage demo:*_ 159 | 160 | 161 | 162 | ## Version 1.1.1/1.1.2 Release Notes 163 | 164 | 1. **Improved Orphan File Detection** 165 | - Enhanced algorithm to identify unlinked files: 166 | - Files are now flagged as orphaned if they are not referenced by any other markdown file. 167 | - New feature: Easily select and link orphaned files within the application. 168 | 169 | 2. **Added Support for PDF Files**: 170 | - The application now supports linking and managing PDF file types. 171 | 172 | 3. **Bug Fixes**: 173 | - Resolved various issues related to linking files within the scene. 174 | 175 | 4. **New Quick Focus Function**: 176 | - Right-clicking on a node in the scene now triggers a "quick focus" behavior for easier navigation. 177 | 178 | ### Version 1.1.2 Bug Fix Release 179 | 180 | 1. Color Map Source Update Issue: The color map source now correctly updates when the color is reset. 181 | 2. Label Text Display Issue: The label text now displays correctly when toggled off. 182 | 3. Unwanted Border Issue: An issue causing an unwanted border to appear in certain scenarios has been resolved. 183 | 184 | _*The usage demo:*_ 185 | 186 | 187 | 188 | _*Settings of the demo:*_ 189 | 1. Obsidian theme: "80s Neon" - dark mode 190 | 2. Plugin theme: default settings - dark mode 191 | 3. Toggle global map: off 192 | 4. Toggle label display: on 193 | 194 | ## Version 1.1.0 Release Notes 195 | 196 | I'm excited to announce the release of Version 1.1.0, which includes several new features and improvements to enhance your experience: 197 | 198 | ### Major Updates: 199 | 200 | 1. **Light Theme Added**: 201 | 202 | - Introduced a new light theme with a bright background and distinct visual elements, offering an alternative to the dark theme. 203 | 204 | 205 | 2. **Node Color Synchronization with Obsidian**: 206 | 207 | - You can now import node colors directly from Obsidian: 208 | 1. Node colors will sync with Obsidian's graph view. 209 | 2. You can switch between different Obsidian themes and: 210 | - **Apply Theme Colors**: Import the color scheme of the selected theme. 211 | - **Save Slot**: Save the imported color scheme into a slot for future use. 212 | 3. The saved color schemes can be reused across different modes (light/dark) and themes, as long as the corresponding slot is loaded. 213 | 214 | ### New Features: 215 | 216 | 3. **Enhanced Node Interaction**: 217 | 218 | - Clicking on frontmatter tags within a note will now focus on the corresponding node in the scene, consistent with other clickable elements. 219 | 4. **User-Friendly Tooltip Bar**: 220 | 221 | - A new tooltip bar has been added to guide new users on how to navigate and operate the interface. Special thanks to @RealSourceOfficial for his support in this addition. 222 | 5. **Node Label Display Toggle**: 223 | 224 | - A new toggle in the settings allows you to turn off node label displays. This is particularly useful if there are too many labels cluttering the view or if you don't need to see note labels constantly. 225 | 6. **Improved Node Label Interaction**: 226 | 227 | - Node labels will no longer respond to mouse clicks, making it easier to interact directly with the nodes. 228 | 7. **Settings Box Style Update**: 229 | 230 | - The settings box style has been updated to match the current Obsidian theme, ensuring a more cohesive visual experience. 231 | 232 | These updates significantly enhance customization options, improve user experience, and provide better integration with Obsidian's theming system. I hope you enjoy the new features and improvements! 233 | 234 | _**You can check the simple usage demo here:**_ 235 | 236 | 237 | 238 | 239 | 240 | # More 241 | **Full version history please refer to [What's-new-history](https://github.com/kctekn/obsidian-TagsRoutes/wiki/What's-new-history)** 242 | 243 | # How to operate: 244 | https://github.com/kctekn/obsidian-TagsRoutes/assets/32674595/2c37676c-f307-4a74-9dae-0679067cbae7 245 | 246 | 247 | 248 | https://github.com/kctekn/obsidian-TagsRoutes/assets/32674595/759e9cba-c729-4b3e-a0c4-bb4c4f1b5dd1 249 | 250 | 251 | 252 | 253 | 254 | 255 | This plugin provides a comprehensive graph view to visualize the relationships between files, file-tag connections, and inter-tag connections within Obsidian. **It is particularly useful for users who manage extensive thoughts and ideas with numerous tags in Obsidian.** 256 | 257 | # Features: 258 | 259 | - **Node and Link Visualization** : 260 | - Display all files and their links. 261 | 262 | - Display all tags and their connections, including: 263 | - Tag-to-tag links 264 | 265 | - Tag-to-file links 266 | 267 | - File-to-file links 268 | 269 | - **Dynamic Node Sizing** : 270 | - Adjust the size of file nodes based on the number of links. 271 | 272 | - Adjust the size of tag nodes based on their frequency of appearance. 273 | 274 | This approach helps you identify the most significant parts of your vault at a glance. 275 | 276 | # Additional Functionalities: 277 | 278 | - **Orphan File Linking** : 279 | - Connect all orphan files, making them easier to review. Note that orphan files are not necessarily useless but are: 280 | - Non-markdown files with no links to other files. 281 | 282 | - For example, they could be isolated images from copy/paste operations or various collected items. 283 | 284 | - **Orphan Excalidraw File Linking** : 285 | - Connect all orphan Excalidraw files that are not linked by any markdown files, simplifying their review. 286 | 287 | # Interactive Features: 288 | 289 | - **Node Interaction** : 290 | - Click on a file node to open it in the editor, regardless of its file type. 291 | 292 | - Click on a tag node to generate a query result for that tag, displayed in a file in the editor. 293 | - Provides a clear view of the tag's content by capturing the surrounding lines until a blank line is encountered, showing the entire paragraph containing the tag. 294 | 295 | - **Graph Focus** : 296 | - Clicking on any file to open it in the editor will automatically focus the graph on its node. 297 | 298 | - Clicking on a tag in Obsidian's "Reading Mode" will focus on the tag node in the graph. 299 | 300 | This allows you to clearly understand the status of files and tags through: 301 | 302 | - The file’s link status 303 | 304 | - The tags contained within the file 305 | 306 | # Adjustable Settings: 307 | 308 | - Focus distance on a node 309 | 310 | - Toggle tag query result page 311 | 312 | - Toggle log page 313 | 314 | - Display styles: 315 | - Link distance and width 316 | - Link particle size, number, and color 317 | - Node size and repulsion 318 | 319 | 320 | # Install 321 | - Search for "Tags routes" in Obsidian's community plugins browser, or you can find it [HERE](https://obsidian.md/plugins?search=tags%20routes). 322 | - Choose to intall it. 323 | - You can also install it manually: 324 | - Download the release file, and extract to your obsidian's: valut/.obsidian/plugin/tags-routes. 325 | - Enable it in obsidian settings tab. 326 | 327 | # More 328 | 329 | **For more information,please refer to [What's-new-history](https://github.com/kctekn/obsidian-TagsRoutes/wiki/What's-new-history) and [Discussions](https://github.com/kctekn/obsidian-TagsRoutes/discussions)** 330 | 331 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "tags-routes", 3 | "name": "Tags Routes", 4 | "version": "1.2.3", 5 | "minAppVersion": "0.15.0", 6 | "description": "A powerful 3D graph visualization tool offers dynamic time-lapse, intelligent orphan file management, tag-based queries, and a range of displaying customization options for a great insightful experience.", 7 | "author": "Ken", 8 | "authorUrl": "https://github.com/kctekn", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-sample-plugin", 3 | "version": "1.0.0", 4 | "description": "This is a sample plugin for Obsidian (https://obsidian.md)", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/node": "^16.11.6", 16 | "@types/three": "^0.166.0", 17 | "@typescript-eslint/eslint-plugin": "5.29.0", 18 | "@typescript-eslint/parser": "5.29.0", 19 | "builtin-modules": "3.3.0", 20 | "esbuild": "0.17.3", 21 | "obsidian": "latest", 22 | "tslib": "2.4.0", 23 | "typescript": "4.7.4" 24 | }, 25 | "dependencies": { 26 | "3d-force-graph": "^1.74.1", 27 | "three": "^0.169.0", 28 | "three-spritetext": "^1.8.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /post-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 获取最新的提交哈希值 4 | commit_hash=$(git rev-parse --short HEAD) 5 | 6 | # 获取当前分支名称 7 | branch_name=$(git rev-parse --abbrev-ref HEAD) 8 | 9 | # 获取最新修改时间 10 | modification_time=$(date) 11 | 12 | # 将这些信息写入文件 13 | echo "Branch: $branch_name, Date: $modification_time, Commit: $commit_hash" > src/version_info.txt 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App, WorkspaceLeaf, Notice, Plugin, PluginSettingTab, Setting, ToggleComponent, TextComponent, ColorComponent, ExtraButtonComponent, TextAreaComponent } from 'obsidian'; 2 | import { TagRoutesView, VIEW_TYPE_TAGS_ROUTES } from "./views/TagsRoutes" 3 | import { PathFilter, createFolderIfNotExists,DebugLevel,DebugMsg,namedColor } from "./util/util" 4 | import { codeBlockProcessor } from './util/CodeBlockProcessor'; 5 | import * as path from 'path'; 6 | //const versionInfo = require('./version_info.txt'); 7 | 8 | export const globalProgramControl = { 9 | useDiv : false, 10 | debugLevel: DebugLevel.INFO, 11 | useGroup: true, 12 | allowDuplicated: false, 13 | aimBeforeLink: true, 14 | useTrackHighlight: true, 15 | snapshotDirectory: "graph-screenshot", 16 | generateLinker: true, 17 | } 18 | export const currentVersion = '1.2.3'; //Used to show in debug console 19 | export const currentSaveSpecVer = 10203; //Indicate current version of saved config file: data.json 20 | export const minSaveSpecVer = 10101; //Data will be loaded if the loaded version of data.json >= minSaveSpecVer, and will be completely overrided to default if version < minSaveSpecVer 21 | 22 | const programDirectory = "TagsRoutes" 23 | const logDirectory = `${programDirectory}/logs` 24 | const reportDirectory = `${programDirectory}/reports` 25 | const logFilePath = `${programDirectory}/logMessage.md` 26 | 27 | export const globalDirectory = { 28 | programDirectory, 29 | logDirectory, 30 | reportDirectory, 31 | logFilePath, 32 | } 33 | 34 | export interface colorSpec { 35 | name?: string; 36 | value: string; 37 | } 38 | export interface colorMap { 39 | markdown: colorSpec; 40 | attachment: colorSpec; 41 | broken: colorSpec; 42 | excalidraw: colorSpec; 43 | pdf: colorSpec; 44 | tag: colorSpec; 45 | frontmatter_tag: colorSpec; 46 | screenshot: colorSpec; 47 | nodeHighlightColor: colorSpec; 48 | nodeFocusColor: colorSpec; 49 | linkHighlightColor: colorSpec; 50 | linkNormalColor: colorSpec; 51 | linkParticleColor: colorSpec; 52 | linkParticleHighlightColor: colorSpec; 53 | selectionBoxColor: colorSpec; 54 | backgroundColor: colorSpec; 55 | } 56 | export const defaultolorMapDark: colorMap = { 57 | markdown: {name:"default", value: "#00ff00"}, 58 | attachment: {name:"default", value: "#ffff00" }, 59 | broken: {name:"default", value: "#ff0000"}, 60 | excalidraw: {name:"default", value: "#00ffff"}, 61 | pdf: {name:"default", value: "#0000ff"}, 62 | tag: { name: "default", value: "#ff00ff" }, 63 | frontmatter_tag:{name: "default", value:"#fa8072"}, 64 | screenshot:{name: "default", value:"#7f00ff"}, 65 | nodeHighlightColor: {name:"default", value: "#3333ff"}, 66 | nodeFocusColor: {name:"default", value: "#ff3333"}, 67 | linkHighlightColor: {name:"default", value: "#ffffff"}, 68 | linkNormalColor: {name:"default", value: "#ffffff"}, 69 | linkParticleColor: {name:"default", value: "#ffffff"}, 70 | linkParticleHighlightColor: { name: "default", value: "#ff00ff" }, 71 | selectionBoxColor:{name:"default", value:"#ffff00"}, 72 | backgroundColor:{name:"default",value:"#000003"} 73 | } 74 | export const defaultolorMapLight: colorMap = { 75 | markdown: {name:"default", value: "#00ff00"}, 76 | attachment: {name:"default", value: "#ffff00" }, 77 | broken: {name:"default", value: "#ff0000"}, 78 | excalidraw: {name:"default", value: "#00ffff"}, 79 | pdf: {name:"default", value: "#0000ff"}, 80 | tag: {name:"default", value: "#ff00ff"}, 81 | frontmatter_tag:{name: "default", value:"#fa8072"}, 82 | screenshot:{name: "default", value:"#7f00ff"}, 83 | nodeHighlightColor: {name:"default", value: "#3333ff"}, 84 | nodeFocusColor: {name:"default", value: "#ff3333"}, 85 | linkHighlightColor: {name:"default", value: "#ffffff"}, 86 | linkNormalColor: {name:"default", value: "#ffffff"}, 87 | linkParticleColor: {name:"default", value: "#ffffff"}, 88 | linkParticleHighlightColor: { name: "default", value: "#ff00ff" }, 89 | selectionBoxColor: {name:"default", value:"#e0ac00"}, 90 | backgroundColor:{name:"default",value:"#ffffff"} 91 | } 92 | export const defaltColorMap = { 93 | dark: defaultolorMapDark, 94 | light: defaultolorMapLight 95 | } 96 | export interface TagRoutesSettings { 97 | node_size: number; 98 | node_repulsion: number; 99 | link_distance: number; 100 | link_width: number; 101 | link_particle_size: number; 102 | link_particle_number: number; 103 | text_color_angle: number; 104 | bloom_strength: number; 105 | toggle_global_map: boolean; 106 | toggle_label_display: boolean; 107 | toggle_highlight_track_mode: boolean; 108 | toggle_selection_box: boolean; 109 | colorMapSource: string; 110 | colorMap: colorMap; 111 | cameraDistance: number; 112 | } 113 | type ThemeSlots = [TagRoutesSettings, TagRoutesSettings, TagRoutesSettings, TagRoutesSettings, TagRoutesSettings, TagRoutesSettings]; 114 | interface Settings { 115 | saveSpecVer: number; 116 | enableSave: boolean; 117 | enableShow: boolean; 118 | currentSlotNum: number; 119 | themeSlotNum: { 120 | dark: number; 121 | light: number; 122 | } 123 | openInCurrentTab: boolean; 124 | enableTagsReaction: boolean; 125 | enableAutoFocus: boolean; 126 | enableParagraphLinker: boolean; 127 | snapShotFolder: string; 128 | currentTheme: "dark"|"light"; 129 | customSlot: ThemeSlots | null ; 130 | dark: ThemeSlots; 131 | light: ThemeSlots; 132 | showingFilter: string; 133 | hidingFilter: string; 134 | } 135 | 136 | export const DEFAULT_DISPLAY_SETTINGS_DARK: TagRoutesSettings = { 137 | node_size: 5, 138 | node_repulsion: 0, 139 | // where is min an max set? 140 | link_distance: 17, 141 | link_width: 1, 142 | link_particle_size: 2, 143 | link_particle_number: 2, 144 | text_color_angle: 0, 145 | bloom_strength:2.0, 146 | toggle_global_map: true, 147 | toggle_label_display: false, 148 | toggle_highlight_track_mode: false, 149 | toggle_selection_box: true, 150 | colorMapSource:"Default dark", 151 | colorMap: defaultolorMapDark, 152 | cameraDistance: 0, 153 | } 154 | export const DEFAULT_DISPLAY_SETTINGS_LIGHT: TagRoutesSettings = { 155 | node_size: 5, 156 | node_repulsion: 0, 157 | link_distance: 5, 158 | link_width: 1, 159 | link_particle_size: 2, 160 | link_particle_number: 2, 161 | text_color_angle:0, 162 | bloom_strength: 2.0, 163 | toggle_global_map: true, 164 | toggle_label_display: false, 165 | toggle_highlight_track_mode: false, 166 | toggle_selection_box: true, 167 | colorMapSource:"Defalt light", 168 | colorMap: defaultolorMapLight, 169 | cameraDistance:0, 170 | } 171 | export const DEFAULT_DISPLAY_SETTINGS = { 172 | dark: DEFAULT_DISPLAY_SETTINGS_DARK, 173 | light: DEFAULT_DISPLAY_SETTINGS_LIGHT 174 | } 175 | 176 | const DEFAULT_SETTINGS: Settings = { 177 | saveSpecVer: currentSaveSpecVer, 178 | enableSave: true, 179 | enableShow: true, 180 | currentSlotNum: 1, 181 | themeSlotNum: { 182 | dark: 1, 183 | light: 1, 184 | }, 185 | openInCurrentTab: false, 186 | enableTagsReaction: true, 187 | enableAutoFocus: true, 188 | enableParagraphLinker: true, 189 | snapShotFolder: "graph-screenshot", 190 | currentTheme: "dark", 191 | customSlot: null, 192 | showingFilter: PathFilter.encode("*"), 193 | hidingFilter: PathFilter.encode(""), 194 | dark: [ 195 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 196 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 197 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 198 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 199 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 200 | structuredClone(DEFAULT_DISPLAY_SETTINGS_DARK), 201 | ], 202 | light: [ 203 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 204 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 205 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 206 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 207 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 208 | structuredClone(DEFAULT_DISPLAY_SETTINGS_LIGHT), 209 | ] 210 | } 211 | 212 | 213 | 214 | 215 | // plugin 主体 216 | export default class TagsRoutes extends Plugin { 217 | public settings: Settings; 218 | public view: TagRoutesView; 219 | public skipSave: boolean = true; 220 | onFileClick(filePath: string) { 221 | // 传递文件路径给 Graph 并聚焦到相应的节点 222 | for (let leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_TAGS_ROUTES)) { 223 | if (leaf.view instanceof TagRoutesView) { 224 | leaf.view.focusGraphNodeById(filePath) 225 | // leaf.view.Graph.refresh(); 226 | // leaf.view.updateHighlight(); 227 | } 228 | } 229 | } 230 | async onDoubleWait() { 231 | if (this.app.metadataCache.resolvedLinks !== undefined) { 232 | await this.initializePlugin(); 233 | } else { 234 | this.app.metadataCache.on("resolved", async () => { 235 | await this.initializePlugin(); 236 | }); 237 | } 238 | } 239 | async onload() { 240 | this.app.workspace.onLayoutReady(() => { 241 | DebugMsg(DebugLevel.INFO,"Loading Tags Routes v",currentVersion) 242 | this.initializePlugin(); 243 | }); 244 | } 245 | async onLayoutReady(): Promise { 246 | return new Promise((resolve) => { 247 | // 检查 layout 是否已经 ready 248 | if (this.app.workspace.layoutReady) { 249 | resolve(); 250 | } else { 251 | // 等待 layout ready 事件 252 | this.app.workspace.onLayoutReady(() => resolve()); 253 | } 254 | }); 255 | } 256 | async initializePlugin() { 257 | //DebugMsg(DebugLevel.DEBUG,versionInfo); 258 | //new Notice(versionInfo, 0) 259 | /* createFolderIfNotExists('.TagsRoutes') 260 | createFolderIfNotExists('.TagsRoutes/logs') 261 | createFolderIfNotExists('.TagsRoutes/reports') */ 262 | await this.loadSettings(); 263 | this.registerView( 264 | VIEW_TYPE_TAGS_ROUTES, 265 | (leaf) => this.view = new TagRoutesView(leaf, this) 266 | ); 267 | const codeProcess = new codeBlockProcessor(this); 268 | this.registerMarkdownCodeBlockProcessor("tagsroutes", codeProcess.codeBlockProcessor); 269 | this.registerEvent( 270 | this.app.workspace.on('file-open', (file) => { 271 | if (file) 272 | this.onFileClick(file.path); 273 | }) 274 | ); 275 | this.addRibbonIcon("waypoints", "Open tags routes", () => { 276 | this.activateView(); 277 | }); 278 | // This adds a settings tab so the user can configure various aspects of the plugin 279 | this.addSettingTab(new TagsroutesSettingsTab(this.app, this)); 280 | // 在 Obsidian 插件的 onload 方法中注册事件 281 | this.registerDomEvent(document, 'click', (e: MouseEvent) => { 282 | const target = e.target as HTMLElement; 283 | if (target) { 284 | // 检查是否点击在标签内容上 285 | let tag = ""; 286 | if (target.hasClass('tag')) { 287 | tag = target.innerText; // 获取标签内容 288 | } 289 | if (target.hasClass('cm-hashtag')) { 290 | tag = '#' + target.innerText; // 获取标签内容 291 | } 292 | if (tag) { 293 | // 传递文件路径给 Graph 并聚焦到相应的节点 294 | for (let leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_TAGS_ROUTES)) { 295 | if (leaf.view instanceof TagRoutesView) { 296 | leaf.view.focusGraphTag(tag) 297 | } 298 | } 299 | return; 300 | } 301 | if (target && target.closest('.multi-select-pill-content')) { 302 | // 查找父容器,确保是包含frontmatter的标签 303 | const parent = target.closest('[data-property-key="tags"]'); 304 | 305 | if (parent) { 306 | // 获取点击的标签内容 307 | const tagContent = target.textContent || target.innerText; 308 | // 传递文件路径给 Graph 并聚焦到相应的节点 309 | for (let leaf of this.app.workspace.getLeavesOfType(VIEW_TYPE_TAGS_ROUTES)) { 310 | if (leaf.view instanceof TagRoutesView) { 311 | leaf.view.focusGraphTag(tagContent) 312 | } 313 | } 314 | } 315 | return 316 | } 317 | 318 | } 319 | }); 320 | } 321 | onunload() { 322 | } 323 | 324 | mergeDeep(target: any, ...sources: any[]): any { 325 | if (!sources.length) return target; 326 | const source = sources.shift(); 327 | 328 | if (this.isObject(target) && this.isObject(source)) { 329 | for (const key in target) { 330 | if (key in source) { 331 | if (this.isObject(target[key]) && this.isObject(source[key])) { 332 | // 递归合并嵌套对象 333 | this.mergeDeep(target[key], source[key]); 334 | } else if (typeof target[key] === typeof source[key] && typeof target[key] !== 'object') { 335 | // 只在类型匹配时更新值 336 | target[key] = source[key]; 337 | } else if (Array.isArray(target[key]) && Array.isArray(source[key])) { 338 | for (let i: number = 0; i < target[key].length; i++) { 339 | //only deal with an array which have only objects 340 | if (this.isObject(target[key][i]) && this.isObject(source[key][i])) { 341 | this.mergeDeep(target[key][i], source[key][i]) 342 | } 343 | 344 | } 345 | 346 | 347 | } 348 | // 如果类型不匹配,保留 target 的值 349 | } 350 | // 如果 source 中没有这个键,保留 target 的值 351 | } 352 | } 353 | 354 | // 继续合并剩余的 sources 355 | return this.mergeDeep(target, ...sources); 356 | } 357 | 358 | // 辅助函数:检查是否为对象 359 | isObject(item: any): boolean { 360 | return (item && typeof item === 'object' && !Array.isArray(item)); 361 | } 362 | async loadSettings() { 363 | this.settings = structuredClone(DEFAULT_SETTINGS); 364 | const loadedSettings = await this.loadData() //as Settings; 365 | if (loadedSettings?.saveSpecVer && loadedSettings.saveSpecVer >= minSaveSpecVer) { 366 | this.mergeDeep(this.settings, loadedSettings) 367 | if (loadedSettings.saveSpecVer != currentSaveSpecVer) { 368 | DebugMsg(DebugLevel.INFO, `Save spec version ${loadedSettings.saveSpecVer} merged.`) 369 | } 370 | } else { 371 | if (loadedSettings && loadedSettings.saveSpecVer) { 372 | DebugMsg(DebugLevel.INFO,`Override save spec version ${loadedSettings.saveSpecVer}.`) 373 | } else if (loadedSettings && !loadedSettings.saveSpecVer) { 374 | DebugMsg(DebugLevel.INFO,`Override no version spec.`) 375 | } 376 | else if (!loadedSettings) { 377 | DebugMsg(DebugLevel.INFO,`New installation: Using default settings.`) 378 | } 379 | } 380 | this.settings.customSlot = this.settings[this.settings.currentTheme]; 381 | this.settings.currentSlotNum = this.settings.themeSlotNum[this.settings.currentTheme]; 382 | this.settings.customSlot[0] = structuredClone( 383 | this.settings.customSlot[this.settings.currentSlotNum]); 384 | this.settings.saveSpecVer = DEFAULT_SETTINGS.saveSpecVer; 385 | } 386 | async saveSettings() { 387 | if (this.skipSave) return; 388 | DebugMsg(DebugLevel.DEBUG,"[TagsRoutes: Save settings]") 389 | this.settings.customSlot = null; //don't save the duplicated object 390 | this.saveData(this.settings); //maybe need await here 391 | this.settings.customSlot = this.settings[this.settings.currentTheme]; 392 | } 393 | async activateView() { 394 | const { workspace } = this.app; 395 | let leaf: WorkspaceLeaf | null = null; 396 | const leaves = workspace.getLeavesOfType(VIEW_TYPE_TAGS_ROUTES); 397 | if (leaves.length > 0) { 398 | // A leaf with our view already exists, use that 399 | leaf = leaves[0]; 400 | } else { 401 | // Our view could not be found in the workspace, create a new leaf 402 | // in the right sidebar for it 403 | if (!this.settings.openInCurrentTab) { 404 | leaf = workspace.getLeaf('split') 405 | } else { 406 | leaf = workspace.getLeaf() 407 | } 408 | if (leaf) { 409 | await workspace.revealLeaf(leaf); 410 | await leaf.setViewState({ type: VIEW_TYPE_TAGS_ROUTES, active: true }); 411 | } 412 | } 413 | // "Reveal" the leaf in case it is in a collapsed sidebar 414 | if (leaf) { 415 | workspace.revealLeaf(leaf); 416 | } 417 | } 418 | } 419 | class colorPickerGroup { 420 | private plugin: TagsRoutes; 421 | private text: Setting; 422 | private keyname: keyof colorMap 423 | private colorPicker: Setting; 424 | private textC: TextComponent; 425 | private colorC: ColorComponent; 426 | private isProgrammaticChange: boolean = false; 427 | private skipSave = false; 428 | 429 | constructor(plugin: TagsRoutes, container: HTMLElement, name: string, keyname: keyof colorMap) { 430 | this.plugin = plugin; 431 | this.keyname = keyname; 432 | const holder = container.createEl("div", "inline-settings") 433 | 434 | this.text = new Setting(holder.createEl("span")).addText( 435 | (text) => { 436 | this.textC = text 437 | .setValue("") 438 | .onChange((v) => { 439 | if (!this.plugin.settings.customSlot) return; 440 | if (v === "") return; 441 | const colorHex = this.namedColorToHex(v) 442 | if (colorHex !== "N/A") { 443 | this.isProgrammaticChange = true; 444 | this.plugin.settings.customSlot[0].colorMap[keyname].name = v; 445 | this.colorC.setValue(colorHex) 446 | this.text.setDesc(`${v} - ${colorHex}`) 447 | this.isProgrammaticChange = false; 448 | } 449 | }) 450 | 451 | 452 | } 453 | ).setName(name) 454 | .setDesc(this.plugin.settings.customSlot?.[0].colorMap[keyname].name||this.plugin.settings.customSlot?.[0].colorMap[keyname].value||"") 455 | this.colorPicker = new Setting(holder.createEl("span")).addColorPicker( 456 | (c) => { 457 | this.colorC = c 458 | .setValue(this.plugin.settings.customSlot?.[0].colorMap[keyname].value||"") 459 | .onChange((v) => { 460 | if (!this.plugin.settings.customSlot) return; 461 | this.textC.setValue("") 462 | if (this.isProgrammaticChange == false) { 463 | this.text.setDesc(v) 464 | this.plugin.settings.customSlot[0].colorMap[keyname].value = v; 465 | this.plugin.settings.customSlot[0].colorMap[keyname].name = ""; 466 | } else { 467 | this.plugin.settings.customSlot[0].colorMap[keyname].value = v; 468 | } 469 | if (!this.skipSave) { 470 | this.plugin.view.onSettingsSave(); 471 | } 472 | this.plugin.view.updateColor(); 473 | // setTimeout(() => this.colorPicker.setDesc(v), 0); 474 | }) 475 | } 476 | ) 477 | //this.text. 478 | return this; 479 | } 480 | namedColorToHex(color: string): string { 481 | const ret = namedColor.get(color); 482 | if (ret) { 483 | return ret; 484 | } 485 | 486 | return 'N/A'; 487 | } 488 | resetColor(skipSave: boolean) { 489 | if (!this.plugin.settings.customSlot) return; 490 | this.skipSave = skipSave; 491 | this.isProgrammaticChange = true; 492 | this.colorC.setValue(this.plugin.settings.customSlot[0].colorMap[this.keyname].value) 493 | this.text.setDesc(this.plugin.settings.customSlot[0].colorMap[this.keyname].name || this.plugin.settings.customSlot[0].colorMap[this.keyname].value) 494 | this.skipSave = false; 495 | this.isProgrammaticChange = false; 496 | } 497 | } 498 | class TagsroutesSettingsTab extends PluginSettingTab { 499 | plugin: TagsRoutes; 500 | toggleEnableSave: ToggleComponent; 501 | toggleEnableShow: ToggleComponent; 502 | colors: colorPickerGroup[] = []; 503 | colorMapSourceElement: HTMLElement; 504 | showFilter: TextAreaComponent; 505 | hideFilter: TextAreaComponent; 506 | constructor(app: App, plugin: TagsRoutes) { 507 | super(app, plugin); 508 | this.plugin = plugin; 509 | this.loadColor = this.loadColor.bind(this) 510 | } 511 | loadColor(value: string) { 512 | this.plugin.view.updateColor(); 513 | } 514 | 515 | display(): void { 516 | this.plugin.skipSave = true; 517 | const { containerEl } = this; 518 | containerEl.empty(); 519 | // containerEl.addClass("tags-routes") 520 | containerEl.createEl("h1", { text: "General" }); 521 | 522 | new Setting(containerEl) 523 | .setName('Log node/link count') 524 | .setDesc('Enable or disable logging the number of nodes and links when the graph loads.') 525 | .addToggle((toggle: ToggleComponent) => { 526 | toggle 527 | .setValue(this.plugin.settings.enableSave) 528 | .onChange(async (value) => { 529 | if (!value) { 530 | this.toggleEnableShow.setValue(value); 531 | } 532 | this.plugin.settings.enableSave = value; 533 | await this.plugin.saveSettings(); 534 | }) 535 | this.toggleEnableSave = toggle; 536 | } 537 | ) 538 | new Setting(containerEl) 539 | .setName('Show log file on startup') 540 | .setDesc('Automatically display the log file after the graph loads.') 541 | .addToggle((toggle: ToggleComponent) => { 542 | toggle 543 | .onChange(async (value) => { 544 | if (value) { 545 | this.toggleEnableSave.setValue(value); 546 | } 547 | this.plugin.settings.enableShow = value; 548 | await this.plugin.saveSettings(); 549 | }) 550 | .setValue(this.plugin.settings.enableShow) 551 | this.toggleEnableShow = toggle; 552 | } 553 | ) 554 | new Setting(containerEl) 555 | .setName('Enable tag click actions') 556 | .setDesc('When enabled, clicking on a tag will generate a content report based on that tag.') 557 | .addToggle((toggle: ToggleComponent) => { 558 | toggle 559 | .onChange(async (value) => { 560 | this.plugin.settings.enableTagsReaction = value; 561 | await this.plugin.saveSettings(); 562 | }) 563 | .setValue(this.plugin.settings.enableTagsReaction) 564 | } 565 | ) 566 | new Setting(containerEl) 567 | .setName('Auto-focus file in explorer') 568 | .setDesc('Automatically navigate and highlight the current file in the file explorer when opened.') 569 | .addToggle((toggle: ToggleComponent) => { 570 | toggle 571 | .onChange(async (value) => { 572 | this.plugin.settings.enableAutoFocus = value; 573 | await this.plugin.saveSettings(); 574 | }) 575 | .setValue(this.plugin.settings.enableAutoFocus) 576 | } 577 | ) 578 | new Setting(containerEl) 579 | .setName('Create paragraph anchor') 580 | .setDesc('Adds an anchor at the end of paragraphs in your notes matching your tag query. This modification enables precise backlinking to specific paragraphs.') 581 | .addToggle((toggle: ToggleComponent) => { 582 | toggle 583 | .onChange(async (value) => { 584 | this.plugin.settings.enableParagraphLinker = value; 585 | await this.plugin.saveSettings(); 586 | }) 587 | .setValue(this.plugin.settings.enableParagraphLinker) 588 | } 589 | ) 590 | new Setting(containerEl) 591 | .setName('Open graph in current tab') 592 | .setDesc('Toggle to open graph within current tab.') 593 | .addToggle((toggle: ToggleComponent) => { 594 | toggle 595 | .onChange(async (value) => { 596 | this.plugin.settings.openInCurrentTab = value; 597 | await this.plugin.saveSettings(); 598 | }) 599 | .setValue(this.plugin.settings.openInCurrentTab) 600 | 601 | } 602 | ) 603 | new Setting(containerEl) 604 | .setName('Save location for graph snapshots') 605 | .setDesc('Choose the folder where you want to save screenshots of the graph.') 606 | .addText((text: TextComponent) => { 607 | text 608 | .setValue(this.plugin.settings.snapShotFolder) 609 | .onChange(async (v) => { 610 | this.plugin.settings.snapShotFolder = v; 611 | // console.log("on text change: ", v) 612 | await this.plugin.saveSettings(); 613 | }) 614 | }) 615 | 616 | const themeTitle = containerEl.createEl("div", {cls: 'tags-routes-settings-title'}); 617 | themeTitle.createEl("h1", { text: "Theme" }); 618 | 619 | new Setting(containerEl) 620 | .setName('Theme selection') 621 | .setDesc('Toggel to use light or dark theme, the default and recommended is dark.') 622 | .addToggle((toggle: ToggleComponent) => { 623 | toggle 624 | .onChange(async (value) => { 625 | if (value === true) { 626 | this.plugin.settings.currentTheme = 'light' 627 | } else { 628 | this.plugin.settings.currentTheme = 'dark'; 629 | } 630 | if (!this.plugin.view) return; 631 | if (this.plugin?.view?.currentVisualString !== undefined && this.plugin.view.currentVisualString === this.plugin.settings.currentTheme) 632 | return; 633 | // switch save slot 634 | this.plugin.settings.customSlot = this.plugin.settings[this.plugin.settings.currentTheme]; 635 | this.plugin.settings.currentSlotNum = this.plugin.settings.themeSlotNum[this.plugin.settings.currentTheme]; 636 | this.plugin.view.currentSlotNum = this.plugin.settings.currentSlotNum; 637 | this.plugin.settings.customSlot[0] = structuredClone( 638 | this.plugin.settings.customSlot[this.plugin.settings.currentSlotNum]); 639 | 640 | this.plugin.view.switchTheme(this.plugin.settings.currentTheme).then((result) => { 641 | if (result) { 642 | const entry = this.plugin.view._controls.find(v => v.id === "Slot #"); 643 | if (entry) { 644 | entry.control.setValue(this.plugin.settings.currentSlotNum) 645 | } 646 | const lockScene = this.plugin.view._controls.find(v => v.id === 'Lock scene'); 647 | if (lockScene) { 648 | lockScene.control.setValue(false); 649 | } 650 | this.plugin.saveSettings(); 651 | } 652 | }); 653 | this.colors.forEach(v => v.resetColor(true)) 654 | this.colorMapSourceElement.innerText = this.plugin.settings.customSlot[this.plugin.settings.currentSlotNum].colorMapSource; 655 | 656 | }) 657 | .setValue(this.plugin.settings.currentTheme === 'dark' ? false : true) 658 | 659 | } 660 | ) 661 | 662 | const colorTitle = containerEl.createEl("div", { cls: 'tags-routes-settings-title' }); 663 | colorTitle.createEl("h1", { text: "Color" }); 664 | 665 | 666 | new ExtraButtonComponent(colorTitle.createEl('span', { cls: 'group-bar-button' })) 667 | .setIcon("reset") 668 | .setTooltip("Reset color of current slot ") 669 | .onClick(() => { 670 | if (!this.plugin.settings.customSlot) return; 671 | this.plugin.settings.customSlot[0].colorMap = structuredClone(defaltColorMap[this.plugin.settings.currentTheme]); 672 | this.plugin.settings.customSlot[0].colorMapSource = DEFAULT_DISPLAY_SETTINGS[this.plugin.settings.currentTheme].colorMapSource; 673 | this.plugin.view.onSettingsSave(); 674 | this.plugin.view.updateColor(); 675 | this.colors.forEach(v => v.resetColor(true)) 676 | this.colorMapSourceElement.innerText = this.plugin.settings.customSlot[this.plugin.settings.currentSlotNum].colorMapSource; 677 | 678 | new Notice(`Color reset on slot ${this.plugin.settings.currentSlotNum}`); 679 | }); 680 | 681 | const desc = containerEl.createEl("div", { text: "You can enter css named colors here, like 'blue', 'lightblue' etc." }); 682 | desc.createEl("br") 683 | desc.appendText("For the supported css named colors, please refer to: ") 684 | desc.createEl("a", { href: "https://www.w3.org/wiki/CSS/Properties/color/keywords", text: "Css color keywords" }) 685 | desc.createEl("br") 686 | desc.createEl("br") 687 | this.colorMapSourceElement = 688 | desc.createEl("div").createEl("span", { text: "Current color map source: " }) 689 | .createEl("span", { text: this.plugin.settings.customSlot?.[0]?.colorMapSource || "Defalt" }) 690 | this.colorMapSourceElement.addClass("tags-routes-need-save") 691 | //desc.addClass("tags-routes"); 692 | desc.addClass("setting-item-description"); 693 | 694 | const colorSettingsGroup = containerEl.createEl("div", { cls: "tags-routes" }) 695 | 696 | new Setting(colorSettingsGroup).setName("Node type").setHeading().settingEl.addClass("tg-settingtab-heading") 697 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Markdown", "markdown")) 698 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Tag","tag")); 699 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Excalidraw","excalidraw")); 700 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Pdf","pdf")); 701 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Attachment","attachment")); 702 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Frontmatter tag","frontmatter_tag")); 703 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Screenshot","screenshot")); 704 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Broken", "broken")); 705 | 706 | new Setting(colorSettingsGroup).setName("Node state").setHeading().setDesc("Effects in global map mode.").settingEl.addClass("tg-settingtab-heading") 707 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Highlight", "nodeHighlightColor")); 708 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Focus", "nodeFocusColor")); 709 | 710 | new Setting(colorSettingsGroup).setName("Link state").setHeading().settingEl.addClass("tg-settingtab-heading") 711 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Normal", "linkNormalColor")); 712 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Highlight", "linkHighlightColor")); 713 | 714 | new Setting(colorSettingsGroup).setName("Particle state").setHeading().settingEl.addClass("tg-settingtab-heading") 715 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Normal", "linkParticleColor")); 716 | this.colors.push(new colorPickerGroup(this.plugin, colorSettingsGroup, "Highlight", "linkParticleHighlightColor")); 717 | 718 | const colorTitle1 = containerEl.createEl("div", { cls: 'tags-routes-settings-title' }); 719 | colorTitle1.createEl("h1", { text: "Filter" }); 720 | const button = colorTitle1.createEl('div').createEl('button', { text: "Apply Filter" /*, cls: buttonClass*/ }); 721 | button.addClass("mod-cta") 722 | button.addEventListener('click', ()=>this.plugin.view.onResetGraph(false)); 723 | 724 | 725 | const desc1 = containerEl.createEl("div", { text: "Use the fields below to define path filters. Enter one filter per line, case sensitive." }); 726 | desc1.createEl("br") 727 | desc1.appendText("Glob patterns (e.g., * for wildcard) are supported.") 728 | //desc1.createEl("a", { href: "https://www.w3.org/wiki/CSS/Properties/color/keywords", text: "Css color keywords" }) 729 | //desc1.addClass("setting-item-description"); 730 | desc1.createEl("hr") 731 | const textAreaDiv = containerEl.createEl("div") 732 | textAreaDiv.addClass("tags-routes") 733 | let textAreaTmp= new Setting(textAreaDiv).addTextArea( 734 | (text) => { 735 | text.setValue(PathFilter.decode(this.plugin.settings.showingFilter)) 736 | .onChange((value) => { 737 | try { 738 | // 处理每一行 739 | const patterns = value.split('\n') 740 | .map(line => line.trim()) 741 | .filter(line => line) // 过滤空行 742 | .map(line => PathFilter.validatePattern(line)); 743 | 744 | // 保存设置 745 | this.plugin.settings.showingFilter = PathFilter.encode(value); 746 | this.plugin.saveSettings(); 747 | } catch (e) { 748 | console.error('Invalid pattern:', e); 749 | // 可以添加错误提示 750 | new Notice('Invalid pattern found in filters'); 751 | } 752 | }) 753 | this.showFilter = text; 754 | //console.log("the string: ",this.plugin.settings.showingFilter ) 755 | 756 | 757 | } 758 | ).setName("Positive filter") 759 | .setDesc("Only paths matching the specified filters will be shown.") 760 | .setClass("setting-item-filter") 761 | 762 | textAreaTmp.descEl.createEl("br") 763 | textAreaTmp.descEl.createEl("br") 764 | textAreaTmp.descEl.appendText("For example:") 765 | textAreaTmp.descEl.createEl("br") 766 | textAreaTmp.descEl.appendText("1. \"*\" : Default value to display all paths.") 767 | textAreaTmp.descEl.createEl("br") 768 | textAreaTmp.descEl.appendText("2. \"A-directory-name/\" : Displays all content within the specified directory. You can enter multiple directories, one per line.") 769 | textAreaTmp.descEl.createEl("br") 770 | textAreaTmp.descEl.appendText("3. \"A-name-keyword\" :Displays all content containing the specified keyword. You can enter multiple keywords, one per line.") 771 | 772 | textAreaTmp= new Setting(textAreaDiv).addTextArea( 773 | (text) => { 774 | text.setValue(PathFilter.decode(this.plugin.settings.hidingFilter)) 775 | .onChange((value) => { 776 | try { 777 | // 处理每一行 778 | const patterns = value.split('\n') 779 | .map(line => line.trim()) 780 | .filter(line => line) // 过滤空行 781 | .map(line => PathFilter.validatePattern(line)); 782 | 783 | // 保存设置 784 | this.plugin.settings.hidingFilter = PathFilter.encode(value); 785 | this.plugin.saveSettings(); 786 | } catch (e) { 787 | console.error('Invalid pattern:', e); 788 | // 可以添加错误提示 789 | new Notice('Invalid pattern found in filters'); 790 | } 791 | }); 792 | this.hideFilter = text; 793 | //console.log("the string: ",this.plugin.settings.showingFilter ) 794 | 795 | 796 | } 797 | ).setName("Negative filter") 798 | .setDesc("Paths matching the specified filters will be hidden.") 799 | .setClass("setting-item-filter") 800 | textAreaTmp.descEl.createEl("br") 801 | textAreaTmp.descEl.createEl("br") 802 | textAreaTmp.descEl.appendText("For example:") 803 | textAreaTmp.descEl.createEl("br") 804 | textAreaTmp.descEl.appendText("1. \"path/name-key-word\" : Hides all nodes containing \"path/name-keyword\". ") 805 | textAreaTmp.descEl.createEl("br") 806 | textAreaTmp.descEl.appendText("2. \"#\" : Hides all tag type nodes.") 807 | textAreaTmp.descEl.createEl("br") 808 | textAreaTmp.descEl.appendText("3. \"#hide-tag\" : Hides tags with the name: \"#hide-tag\".") 809 | textAreaTmp.descEl.createEl("br") 810 | textAreaTmp.descEl.appendText("4. \"#hide-tag/\" : Hides all sub-tags of the root tag: \"#hide-tag/\", but not for root tag itself.") 811 | //textAreaTmp.descEl. 812 | this.plugin.skipSave = false; 813 | } 814 | } 815 | -------------------------------------------------------------------------------- /src/util/CodeBlockProcessor.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownPostProcessorContext, moment, TFile, MarkdownRenderer, MarkdownView, HeadingCache, getFrontMatterInfo, parseFrontMatterTags } from "obsidian" 2 | import TagsRoutes, { globalProgramControl } from '../main'; 3 | import { getLineTime, DebugLevel, DebugMsg } from "./util"; 4 | 5 | //Include: number, English chars, Chinese chars, and: /, - 6 | const pattern_tags_char = '#[0-9a-zA-Z\\u4e00-\\u9fa5/_-]' 7 | const pattern_timeStamp = '\\d{4}-\\d{2}-\\d{2} *\\d{2}:\\d{2}:\\d{2}' 8 | //Links format: 'tr-' + number or English chars 9 | //const pattern_link = '\\^tr-[a-z0-9]+$' 10 | const tagRegEx = /\^tr-[a-z0-9]+$/ 11 | const regex_TagsWithTimeStamp = new RegExp(`(?:(?<=\\s)|(?<=^))((?:${pattern_tags_char}+ *)+)(${pattern_timeStamp})?`, 'gm'); 12 | const timeDurationRegex=/#\d+day/ 13 | interface queryKey{ 14 | type: string; 15 | value: string; 16 | result: string[]; 17 | } 18 | class performanceCount { 19 | start:number 20 | end:number 21 | startDatetime:string 22 | endDatetime:string 23 | constructor() { 24 | this.start = performance.now(); 25 | this.startDatetime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss') 26 | } 27 | getTimeCost() { 28 | this.end = performance.now(); 29 | this.endDatetime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss') 30 | const retStr = `Start at: ${this.startDatetime} - ${this.endDatetime}, execution: ${this.end - this.start} ms` 31 | this.start = performance.now(); 32 | this.startDatetime = moment(new Date()).format('YYYY-MM-DD HH:mm:ss') 33 | return retStr; 34 | } 35 | } 36 | 37 | export class codeBlockProcessor { 38 | plugin: TagsRoutes; 39 | constructor(plugin: TagsRoutes) { 40 | this.plugin = plugin; 41 | this.codeBlockProcessor = this.codeBlockProcessor.bind(this); 42 | } 43 | 44 | getTimeDiffHour(start: string, end: string): number { 45 | return ((new Date(end)).getTime() - (new Date(start)).getTime()) / (1000 * 60 * 60); 46 | } 47 | private async frontmatterTagProcessor(query:queryKey) { 48 | 49 | //const tag = source.replace(/frontmatter_tag:/, '').trim(); 50 | const tag = query.value; 51 | const files = this.plugin.app.vault.getMarkdownFiles().filter(f=>this.plugin.view.testPathFilter(f.path)); 52 | 53 | const matchingFiles = await Promise.all(files.map(async (file) => { 54 | const cache = this.plugin.app.metadataCache.getCache(file.path); 55 | if (cache?.frontmatter?.tags) { 56 | let tags = Array.isArray(cache.frontmatter.tags) 57 | ? cache.frontmatter.tags 58 | : [cache.frontmatter.tags]; 59 | 60 | if (tags.includes("tag-report")) { 61 | return null; // Exclude tag-report files 62 | } 63 | 64 | if (tags.some(t => t.includes(tag))) { 65 | // console.log(">>find the file have this tag: ", file.path); 66 | return file.path; 67 | } 68 | } 69 | return null; 70 | })); 71 | 72 | const result = matchingFiles.filter(path => path !== null) as string[]; 73 | const writeContent = ` 74 | # Total \`${result.length}\` notes with tag \`${tag}\` : 75 | ${result.map(v => "- [[" + v.replace(/.md$/, "") + "]]").join("\n")} 76 | `; 77 | // this.writeMarkdown("frontmatter_tag: " + tag, writeContent, el, ctx); 78 | //query.result = writeContent; 79 | return [writeContent]; 80 | } 81 | private async tagProcessor(query: queryKey): Promise[]>{ 82 | const term = query.value; 83 | const files = this.plugin.app.vault.getMarkdownFiles().filter(f=>this.plugin.view.testPathFilter(f.path)); 84 | const arr = files.map( 85 | async (file) => { 86 | const content = await this.plugin.app.vault.cachedRead(file); 87 | const fmi = getFrontMatterInfo(content); 88 | if (fmi.exists && fmi.frontmatter.contains("tag-report")) { 89 | return []; 90 | } 91 | 92 | const paragraphs = content 93 | .split(/\n[\ ]*\n/) 94 | .filter(line => line.contains(term)); 95 | 96 | if (paragraphs.length != 0) { 97 | var mmtime; 98 | //'[\/_# \u4e00-\u9fa5]*' 99 | var regstr = term + '[#a-zA-Z0-9\\-\/_\u4e00-\u9fa5 ]* +(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})'; 100 | var regex = new RegExp(regstr, 'g'); 101 | 102 | let updatedContent = content; 103 | let isUpdated = false; // 用于跟踪是否进行了任何更新 104 | const retArr = paragraphs.map( 105 | (paragraph) => { 106 | const stripedParagraph = paragraph.replace(/<.*>/gm, "").replace(/```.*```/gm, "").replace(/#\d+day/gm,"") 107 | if (paragraph.length != stripedParagraph.length) { 108 | // console.log("original lenght: ", line.length) 109 | // console.log("stripped lenght: ", stripedLine.length) 110 | } 111 | regex.lastIndex = 0; 112 | let match = regex.exec(stripedParagraph); 113 | if (match) { 114 | mmtime = " Tag Time: " + match[1]; 115 | } else { 116 | mmtime = " Created Time: " + moment(file.stat.ctime).format('YYYY-MM-DD HH:mm:ss'); 117 | } 118 | 119 | let randomLinker="" 120 | if (this.plugin.settings.enableParagraphLinker) { 121 | // 生成一个随机的段落链接标记 122 | const tagMatch = paragraph.trimEnd().match(tagRegEx); 123 | if (tagMatch) { 124 | 125 | randomLinker = tagMatch[0].substring(1); // 获取已有的链接标记 126 | } else { 127 | randomLinker = 'tr-' + Math.random().toString(36).substr(2, 9); 128 | let updatedLine = "" 129 | if (paragraph.trimEnd().match(/\`\`\`/)) { 130 | updatedLine = paragraph.trimEnd() + `\n^${randomLinker}\n`; 131 | } else { 132 | updatedLine = paragraph.trimEnd() + ` ^${randomLinker}\n`; 133 | } 134 | updatedContent = updatedContent.replace(paragraph, updatedLine.trimEnd()); 135 | isUpdated = true; // 标记为更新 136 | } 137 | randomLinker = `#^${randomLinker}` 138 | } 139 | // return paragraph.trimEnd() + "\n\n \[*From* [[" + `${file.name.split(".")[0]}#^${randomLinker}|${file.name.split(".")[0]}]], *` + mmtime + "*\]\n"; 140 | const regexp_local = new RegExp(regex_TagsWithTimeStamp.source, regex_TagsWithTimeStamp.flags); 141 | let matched_Tags_Timestamp_Group; 142 | let contentTimeString = mmtime; 143 | let retParagraph = ""; 144 | while ((matched_Tags_Timestamp_Group = regexp_local.exec(stripedParagraph)) !== null) { 145 | let matched_Tags = matched_Tags_Timestamp_Group[1]; 146 | const regexB = new RegExp(`${pattern_tags_char}+`,'gm') 147 | const matches = matched_Tags.match(regexB) 148 | retParagraph = paragraph.trimEnd() + "\n\n----\n " + 149 | "\[ *Tags:* " + matches?.join(' ') + " \]\n" + 150 | "\[ *" + contentTimeString + "* \]\n" + 151 | (this.plugin.settings.enableParagraphLinker? 152 | "\[ *From:* [[" + `${file.path}${randomLinker}|${file.name.split(".")[0]}]] \]\n` : 153 | "\[ *From:* [[" + `${file.path}|${file.name.split(".")[0]}]] \]\n` ) 154 | } 155 | return retParagraph; 156 | } 157 | ); 158 | 159 | // 如果有任何更新,才将所有更新的行内容写回文件 160 | if (isUpdated) { 161 | await this.plugin.app.vault.modify(file, updatedContent); 162 | DebugMsg(DebugLevel.DEBUG,"file modified: ", file) 163 | } 164 | return retArr; 165 | } else { 166 | return []; 167 | } 168 | } 169 | ); 170 | return arr; 171 | } 172 | /*** 173 | * the all tag content within a time period 174 | */ 175 | private async timeDurationProcessor(query: queryKey): Promise[]> { 176 | const queryDuration = Number(query.value.replace('#','').replace('day','')); 177 | 178 | const files = this.plugin.app.vault.getMarkdownFiles().filter(f=>this.plugin.view.testPathFilter(f.path)); 179 | const arr = files.map( 180 | async (file) => { 181 | const content = await this.plugin.app.vault.cachedRead(file); 182 | const fmi = getFrontMatterInfo(content); 183 | if (fmi.exists && fmi.frontmatter.contains("tag-report")) { 184 | return []; 185 | } 186 | // console.log("process file: ", file.path) 187 | const paragraphs = content.split(/\n[\ ]*\n/) 188 | 189 | 190 | 191 | if (paragraphs.length != 0) { 192 | 193 | let updatedContent = content; 194 | let isUpdated = false; // 用于跟踪是否进行了任何更新 195 | const retArr = paragraphs.map( 196 | //return the paragraph with information: "tags, tag/create time, from" appended. 197 | (paragraph) => { 198 | // const regex_TagsWithTimeStamp = /(?:(?<=\s)|(?<=^))((?:#[0-9a-zA-Z\u4e00-\u9fa5/-]+ +)+)(\d{4}-\d{2}-\d{2} *\d{2}:\d{2}:\d{2})?/gm; 199 | const stripedParagraph = paragraph.replace(/<.*>/gm, "").replace(/```.*```/gm, "").replace(/#\d+day/gm,"") 200 | if (paragraph.length != stripedParagraph.length) { 201 | // console.log("original lenght: ", paragraph.length) 202 | // console.log("stripped lenght: ", stripedParagraph.length) 203 | } 204 | // get the timestamp 205 | let matched_Tags_Timestamp_Group; 206 | let contentTimeString; 207 | let retParagraph = ""; 208 | const regexp_local = new RegExp(regex_TagsWithTimeStamp.source, regex_TagsWithTimeStamp.flags); 209 | // regex_TagsWithTimeStamp.lastIndex = 0; 210 | while ((matched_Tags_Timestamp_Group = regexp_local.exec(stripedParagraph)) !== null) { 211 | let matched_Timestamp = matched_Tags_Timestamp_Group[2] || ""; 212 | let matched_Tags = matched_Tags_Timestamp_Group[1]; 213 | let lineTime = "" 214 | if (matched_Timestamp !== "") { 215 | contentTimeString = " Tag Time: " + matched_Tags_Timestamp_Group[2]; 216 | lineTime = matched_Tags_Timestamp_Group[2]; 217 | } else { 218 | lineTime = moment(file.stat.ctime).format('YYYY-MM-DD HH:mm:ss') 219 | contentTimeString = " Created Time: " + lineTime; 220 | } 221 | let duration = this.getTimeDiffHour(lineTime, moment(new Date()).format('YYYY-MM-DD HH:mm:ss')) 222 | if (duration > 24 * queryDuration) { 223 | //not applicated, bypass 224 | continue; 225 | } 226 | 227 | let randomLinker="" 228 | if (this.plugin.settings.enableParagraphLinker) { 229 | // 生成一个随机的段落链接标记 230 | const tagMatch = paragraph.trimEnd().match(tagRegEx); 231 | if (tagMatch) { 232 | randomLinker = tagMatch[0].substring(1); // 获取已有的链接标记 233 | } else { 234 | //create link, and change the original paragraph in the file content 235 | randomLinker = 'tr-' + Math.random().toString(36).substr(2, 9); 236 | let updatedLine = "" 237 | if (paragraph.trimEnd().match(/\`\`\`/)) { 238 | updatedLine = paragraph.trimEnd() + `\n^${randomLinker}\n`; 239 | } else { 240 | updatedLine = paragraph.trimEnd() + ` ^${randomLinker}\n`; 241 | } 242 | updatedContent = updatedContent.replace(paragraph, updatedLine.trimEnd()); 243 | isUpdated = true; // 标记为更新 244 | } 245 | randomLinker = `#^${randomLinker}` 246 | } 247 | const regexB = new RegExp(`${pattern_tags_char}+`,'gm') 248 | const matches = matched_Tags.match(regexB) 249 | retParagraph = paragraph.trimEnd() + "\n\n----\n " + 250 | "\[ *Tags:* " + matches?.join(' ') + " \]\n" + 251 | "\[ *" + contentTimeString + "* \]\n" + 252 | (this.plugin.settings.enableParagraphLinker? 253 | "\[ *From:* [[" + `${file.path}${randomLinker}|${file.name.split(".")[0]}]] \]\n` : 254 | "\[ *From:* [[" + `${file.path}|${file.name.split(".")[0]}]] \]\n` ) 255 | } 256 | return retParagraph; 257 | } 258 | ); 259 | 260 | // 如果有任何更新,才将所有更新的行内容写回文件 261 | if (isUpdated) { 262 | await this.plugin.app.vault.modify(file, updatedContent); 263 | DebugMsg(DebugLevel.DEBUG,"file modified: ", file) 264 | } 265 | return retArr //.flat(); 266 | } else { 267 | return []; 268 | } 269 | } 270 | ); 271 | return arr; 272 | } 273 | 274 | writeMarkdownWrap(query: queryKey, source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { 275 | if (query.type == 'frontmatter_tag:') { 276 | this.writeMarkdown(query.type+query.value,source, el,ctx) 277 | } else { 278 | this.writeMarkdown(query.value,source, el,ctx) 279 | } 280 | 281 | } 282 | 283 | async writeMarkdown(term: string, source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { 284 | const markDownSource = source; 285 | 286 | if (globalProgramControl.useDiv) { 287 | //el.createEl('pre', {text: markDownSource}) 288 | MarkdownRenderer.render(this.plugin.app, 289 | markDownSource, 290 | el.createEl('div'), ctx.sourcePath, this.plugin.app.workspace.getActiveViewOfType(MarkdownView) as MarkdownView 291 | ) 292 | } else { 293 | const fileContent = `---\ntags:\n - tag-report\n---\n 294 | \`\`\`tagsroutes 295 | ${term} 296 | \`\`\` 297 | *This file is automatically generated and will be overwritten.* 298 | *Please do not edit this file to avoid losing your changes.* 299 | ` 300 | const { vault } = this.plugin.app; 301 | 302 | //need to add only process file under report directory 303 | const file = vault.getAbstractFileByPath(ctx.sourcePath); 304 | if (file instanceof TFile) { 305 | vault.modify(file, fileContent + markDownSource) 306 | } 307 | } 308 | } 309 | 310 | extractQueryKey(source: string):queryKey { 311 | let queryKey: queryKey = {type:"",value:"",result:[]}; 312 | if (source.contains("frontmatter_tag:")) { 313 | queryKey.type = "frontmatter_tag:" 314 | queryKey.value= source.replace(/frontmatter_tag:/, '').trim(); 315 | } else { 316 | const regstr = `(${pattern_tags_char}*)` 317 | const regex = new RegExp(regstr, 'g') 318 | const match = source.match(regex) 319 | const term = match?.[0] || "#empty" 320 | const timeRegex = new RegExp(timeDurationRegex.source,timeDurationRegex.flags) 321 | const timeMatch = term.match(timeRegex) 322 | if (timeMatch) { 323 | queryKey.type = "time_duration:" 324 | queryKey.value = term; //'#'+ timeMatch[0].substring(1); 325 | } else { 326 | queryKey.type = "tag:" 327 | queryKey.value = term 328 | } 329 | } 330 | return queryKey; 331 | } 332 | getMarkdownContent(query:queryKey) 333 | { 334 | if (query.type == 'frontmatter_tag:') return query.result; 335 | const noteArr = query.result; 336 | const term = query.value; 337 | const markdownText: string[] = []; 338 | if (globalProgramControl.useGroup) { 339 | const tagMap: Map = new Map(); 340 | //Get tags 341 | // const regex1 = /(?<= )#([a-zA-Z0-9\u4e00-\u9fa5\/\-]+)/g; 342 | // const pattern_tags_char = '#[0-9a-zA-Z\\u4e00-\\u9fa5/_-]' 343 | const regex1 = new RegExp(`(?<= )${pattern_tags_char}+`,'g') 344 | noteArr.sort((b, a) => getLineTime(a) - getLineTime(b)) 345 | 346 | 347 | for (let i = 0; i < noteArr.length; i++) { 348 | 349 | //Get every tag in multiple tags 350 | const matches = noteArr[i].replace(/[^]*Tags:/, "").replace(/<.*>/gm, "").replace(/```.*```/gm, "").match(regex1); 351 | // console.log("replaced: ", noteArr[i].replace(/[^]*Tags:/, "")) 352 | // console.log("replaced original: ", noteArr[i]) 353 | // console.log("matches: ", matches) 354 | 355 | //For every tag, do push the content to this tag even it will be duplicated 356 | try { 357 | matches?.forEach(m => { 358 | if (!tagMap.has(m)) { 359 | tagMap.set(m, new Array()) 360 | 361 | } 362 | tagMap.get(m)?.push(noteArr[i]) 363 | if (!globalProgramControl.allowDuplicated) { 364 | throw ("pushed") 365 | } 366 | }); 367 | } catch (error) { 368 | 369 | } 370 | } 371 | 372 | //Generate the content block 373 | markdownText.push("# Tag\ [" + term + "\] total: `" + noteArr.length + "` records.") 374 | tagMap.forEach((content, tag) => { 375 | content.sort((a, b) => getLineTime(a) - getLineTime(b)) 376 | markdownText.push(`# \\${tag} (${content.length})`) 377 | for (let i = 0; i < content.length; i++) { 378 | content[content.length - 1 - i] = content[content.length - 1 - i].replace(/^#/g, "###").replace(/\n#/g, "\n###") 379 | content[content.length - 1 - i] = "> [!info]+ " + (i + 1) + "\n> " + content[content.length - 1 - i].replace(/\n/g, "\n> ") 380 | markdownText.push("## " + (i + 1) + "\n" + `${content[content.length - 1 - i]}`) 381 | } 382 | 383 | }) 384 | 385 | } else { //no group 386 | 387 | noteArr.sort((a, b) => getLineTime(a) - getLineTime(b)) 388 | 389 | //Generate the content block 390 | markdownText.push("# Tag\ [" + term + "\] total: `" + noteArr.length + "` records.") 391 | for (let i = 0; i < noteArr.length; i++) { 392 | noteArr[noteArr.length - 1 - i] = noteArr[noteArr.length - 1 - i].replace(/^#/g, "###").replace(/\n#/g, "\n###") 393 | noteArr[noteArr.length - 1 - i] = "> [!info]+ " + (i + 1) + "\n> " + noteArr[noteArr.length - 1 - i].replace(/\n/g, "\n> ") 394 | markdownText.push("## " + (i + 1) + "\n" + `${noteArr[noteArr.length - 1 - i]}`) 395 | } 396 | } 397 | return markdownText; 398 | } 399 | async codeBlockProcessor(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { 400 | //Bypass the none-first pass 401 | if ((ctx.frontmatter as any).tags !== undefined) { 402 | return; 403 | } 404 | 405 | //Get the key type, and value 406 | const query = this.extractQueryKey(source) 407 | const perf = new performanceCount() 408 | this.writeMarkdownWrap(query, "
PROCESSING...
The first time will be slow depending on vault size.
", el, ctx); 409 | //Process to get the content 410 | switch (query.type) { 411 | case 'frontmatter_tag:': 412 | query.result = await this.frontmatterTagProcessor(query); 413 | break; 414 | case 'time_duration:': 415 | query.result = (await Promise.all(await this.timeDurationProcessor(query))).flat().filter(v => v != ""); 416 | break; 417 | case 'tag:': 418 | query.result = (await Promise.all(await this.tagProcessor(query))).flat().filter(v => v != ""); 419 | break; 420 | } 421 | //Render it 422 | let executionTimeString 423 | if (globalProgramControl.debugLevel == DebugLevel.DEBUG) { 424 | executionTimeString = perf.getTimeCost(); 425 | } else { 426 | executionTimeString = `Report refreshed at ${moment(new Date()).format('YYYY-MM-DD HH:mm:ss')} ` 427 | } 428 | const mc = "*" + executionTimeString + "*\n\n" + this.getMarkdownContent(query).filter(line => line.trim() !== "").join("\n") 429 | 430 | this.writeMarkdownWrap(query, mc, el, ctx); 431 | return; 432 | 433 | } 434 | } -------------------------------------------------------------------------------- /src/util/d3-force-3d.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'd3-force-3d'; 2 | -------------------------------------------------------------------------------- /src/util/query.ts: -------------------------------------------------------------------------------- 1 | export const fileContent = 2 | ` 3 | 4 | var term = "#empty" 5 | function getarg(...args) { 6 | term = args[0] 7 | } 8 | getarg(input) 9 | const files = app.vault.getMarkdownFiles() 10 | const arr = files.map( 11 | async (file) => { 12 | const content = await app.vault.cachedRead(file) 13 | if (content.contains("tag-report")) { 14 | return [] 15 | } 16 | 17 | const lines = content 18 | .split(/\\n[\\ ]*\\n/) 19 | .filter(line => line.contains(term)) 20 | 21 | if (lines.length != 0) { 22 | var mmtime 23 | var regstr = term + '[\\/_# \\u4e00-\\u9fa5]* +(\\\\d{4}-\\\\d{2}-\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2})' 24 | var regex = new RegExp(regstr, 'g') 25 | const retArr = lines.map( 26 | 27 | (line) => { 28 | regex.lastIndex = 0 29 | let match = regex.exec(line) 30 | if (match) { 31 | mmtime = " Tag Time: " + match[1] 32 | } else { 33 | mmtime = " Created Time: " + moment(file.stat.ctime).format('YYYY-MM-DD HH:mm:ss') 34 | } 35 | return [line + "\\n\\n \\[*From* " + "[[" + 36 | file.name.split(".")[0] + "]], *" + mmtime + "*\\]\\n"] 37 | } 38 | ) 39 | return retArr 40 | } else { 41 | return [] 42 | } 43 | } 44 | ) 45 | function getLineTime(line) { 46 | let regstr = 'Time: +(\\\\d{4}-\\\\d{2}-\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2})' 47 | let regex = new RegExp(regstr, 'g') 48 | let match = regex.exec(line) 49 | if (match) { 50 | return (new Date(match[1])).getTime() 51 | } else 52 | return 0 53 | } 54 | Promise.all(arr).then(values => { 55 | let noteArr = values.flat() 56 | noteArr.sort((a, b) => getLineTime(a) - getLineTime(b)) 57 | dv.header(3, "Tag\\ [" + term + "\\] total: \`" + noteArr.length + "\` records.") 58 | for (let i = 0; i < noteArr.length; i++) { 59 | dv.paragraph("## " + (i + 1) + "\\n" + \`\${noteArr[noteArr.length - 1 - i]}\`) 60 | } 61 | }) 62 | ` 63 | -------------------------------------------------------------------------------- /src/util/util.ts: -------------------------------------------------------------------------------- 1 | import { CachedMetadata, MarkdownView, TagCache, View, WorkspaceLeaf } from 'obsidian'; 2 | import { TFile } from "obsidian"; 3 | import { globalProgramControl } from 'src/main'; 4 | import { nodeTypes } from 'src/views/TagsRoutes'; 5 | 6 | // 定义调试级别 7 | export enum DebugLevel { 8 | NONE = 0, // 不输出 9 | ERROR = 1, // 仅输出错误 10 | WARN = 2, // 输出警告和错误 11 | INFO = 3, // 输出信息、警告和错误 12 | DEBUG = 4 // 输出所有信息 13 | } 14 | 15 | // DebugMsg 16 | export function DebugMsg(level: DebugLevel, ...args: any[]): void { 17 | if (level <= globalProgramControl.debugLevel) { 18 | switch (level) { 19 | case DebugLevel.ERROR: 20 | console.error('%c [ERROR] %c', 'color: white;background-color: red;', 'color: black;',...args); 21 | break; 22 | case DebugLevel.WARN: 23 | console.warn('%c [WARN] %c', 'color: white;background-color: orange;', 'color: black;',...args); 24 | break; 25 | case DebugLevel.INFO: 26 | console.info('%c [INFO] %c', 'color: white;background-color: blue;', 'color: black;',...args); 27 | break; 28 | case DebugLevel.DEBUG: 29 | console.debug('%c [DEBUG] %c', 'color: black;background-color: lime;', 'color: black;',...args); 30 | break; 31 | default: 32 | break; 33 | } 34 | } 35 | } 36 | 37 | export const setViewType = (view: View, mode: "source" | "preview" | "live") => { 38 | if (view && view.getViewType() === 'markdown') { 39 | switch (mode) { 40 | case "source": 41 | view.setState({ mode: mode,source:true }, { history: false }) 42 | break; 43 | case "preview": 44 | view.setState({ mode: mode }, { history: false }) 45 | break; 46 | case "live": 47 | view.setState({ mode: "source", source: false }, { history: false }) 48 | break; 49 | } 50 | } 51 | } 52 | export function createFolderIfNotExists(folderPath: string) { 53 | const folder = this.app.vault.getAbstractFileByPath(folderPath); 54 | if (!folder) { 55 | this.app.vault.createFolder(folderPath); 56 | // DebugMsg(DebugLevel.DEBUG,`Folder created: ${folderPath}`); 57 | } else { 58 | // DebugMsg(DebugLevel.DEBUG,`Folder already exists: ${folderPath}`); 59 | } 60 | } 61 | // 函数:获取所有标签 62 | export const getTags = (cache: CachedMetadata | null): TagCache[] => { 63 | if (!cache || !cache.tags) return []; 64 | return cache.tags; 65 | }; 66 | // 函数:判断文件类型 67 | export const getFileType = (filePath: string): nodeTypes => { 68 | const parts = filePath.split('.'); 69 | const extension = parts[parts.length - 1]; 70 | const middlePart = parts[parts.length - 2]; 71 | switch (extension) { 72 | case 'md': 73 | if (middlePart === 'excalidraw') { 74 | return 'excalidraw'; 75 | } else { 76 | return 'markdown' 77 | } 78 | case 'pdf': 79 | return 'pdf' 80 | } 81 | if (filePath.contains("attachments")) return 'attachment' 82 | if (middlePart?.contains("graph-screenshot-")) return 'screenshot' 83 | 84 | return 'other' 85 | }; 86 | export const getAllLinks = (cache: CachedMetadata | null): string[] => { 87 | if (!cache || !cache.links) return []; 88 | return cache.links.map(link => { 89 | const linkPath = link.link; 90 | return linkPath.contains('.') ? linkPath : `${linkPath}.md`; 91 | }); 92 | }; 93 | // 函数:解析标签层级结构 94 | export const parseTagHierarchy = (tag: string): string[] => { 95 | const parts = tag.split('/'); 96 | return parts.map((_, index) => parts.slice(0, index + 1).join('/')); 97 | }; 98 | export const parseTagHierarchy1 = (tag: string): string[] => { 99 | return tag.split('/'); 100 | } 101 | export const filterStrings = ['TagsRoutes', 'AnotherString']; // 需要过滤的字符串列表 102 | // 过滤条件函数:检查路径中是否包含字符串列表中的任何一个字符串 103 | export const shouldRemove = (path: string, filterList: string[]) => { 104 | return filterList.some(filterStr => path.includes(filterStr)); 105 | }; 106 | export async function showFile(filePath: string) { 107 | const { vault } = this.app; 108 | let file = vault.getAbstractFileByPath(filePath) 109 | let waitFlag = true; 110 | const timeout = setTimeout(() => { 111 | waitFlag = false; 112 | }, 3000); 113 | while (!(file && file instanceof TFile) && waitFlag) { 114 | await sleep(100) 115 | // DebugMsg(DebugLevel.DEBUG,"wait for file ready") 116 | file = vault.getAbstractFileByPath(filePath) 117 | } 118 | clearTimeout(timeout); 119 | if (file && file instanceof TFile) { 120 | 121 | const leaves = this.app.workspace.getLeavesOfType("markdown"); 122 | const existingLeaf = leaves.find((leaf: WorkspaceLeaf) => (leaf.view as MarkdownView).file?.path === filePath); 123 | 124 | if (existingLeaf) { 125 | 126 | this.app.workspace.setActiveLeaf(existingLeaf); 127 | await existingLeaf.openFile(file); 128 | setViewType(existingLeaf.view, "preview"); 129 | } else { 130 | const leaf = this.app.workspace.getLeaf(false); 131 | await leaf.openFile(file); 132 | setViewType(leaf.view, "preview"); 133 | } 134 | } 135 | } 136 | export function getLineTime(line:string) { 137 | let regstr = 'Time: +(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})' 138 | let regex = new RegExp(regstr, 'g') 139 | let match = regex.exec(line) 140 | if (match) { 141 | return (new Date(match[1])).getTime() 142 | } else 143 | return 0 144 | } 145 | export class PathFilter { 146 | static readonly DEFAULT_VALUE = "put one filter per line"; 147 | 148 | static encode(value: string): string { 149 | return btoa(encodeURIComponent(value)); 150 | } 151 | 152 | static decode(encoded: string | null | undefined): string { 153 | if (!encoded) return this.DEFAULT_VALUE; 154 | try { 155 | return decodeURIComponent(atob(encoded)); 156 | } catch { 157 | return this.DEFAULT_VALUE; 158 | } 159 | } 160 | 161 | static validatePattern(pattern: string): string { 162 | pattern = pattern.trim(); 163 | if (!pattern) return ''; 164 | 165 | // 处理通配符 166 | if (this.isGlobPattern(pattern)) { 167 | return this.globToRegex(pattern); 168 | } 169 | 170 | // 验证正则表达式 171 | try { 172 | new RegExp(pattern); 173 | return pattern; 174 | } catch { 175 | throw new Error(`Invalid pattern: ${pattern}`); 176 | } 177 | } 178 | 179 | static processFilters(encoded: string | null | undefined): { 180 | patterns: string[]; 181 | regexPatterns: RegExp[]; 182 | } { 183 | const decoded = this.decode(encoded); 184 | const patterns = decoded.split('\n') 185 | .map(line => line.trim()) 186 | .filter(line => line); 187 | 188 | const validPatterns = patterns.map(p => this.validatePattern(p)); 189 | const regexPatterns = validPatterns.map(p => new RegExp(p)); 190 | 191 | return { 192 | patterns: validPatterns, 193 | regexPatterns: regexPatterns 194 | }; 195 | } 196 | 197 | private static isGlobPattern(pattern: string): boolean { 198 | return pattern.includes('*') || pattern.includes('?')|| pattern.includes('.'); 199 | } 200 | 201 | private static globToRegex(glob: string): string { 202 | return glob 203 | .replace(/\*/g, '.*') 204 | .replace(/\./g, '\\.') 205 | .replace(/\?/g, '.') 206 | // .replace(/\\/g, '\\\\'); 207 | } 208 | } 209 | export const namedColor = new Map([ 210 | ["aliceblue", "#f0f8ff"], 211 | ["antiquewhite", "#faebd7"], 212 | ["aqua", "#00ffff"], 213 | ["aquamarine", "#7fffd4"], 214 | ["azure", "#f0ffff"], 215 | ["beige", "#f5f5dc"], 216 | ["bisque", "#ffe4c4"], 217 | ["black", "#000000"], 218 | ["blanchedalmond", "#ffebcd"], 219 | ["blue", "#0000ff"], 220 | ["blueviolet", "#8a2be2"], 221 | ["brown", "#a52a2a"], 222 | ["burlywood", "#deb887"], 223 | ["cadetblue", "#5f9ea0"], 224 | ["chartreuse", "#7fff00"], 225 | ["chocolate", "#d2691e"], 226 | ["coral", "#ff7f50"], 227 | ["cornflowerblue", "#6495ed"], 228 | ["cornsilk", "#fff8dc"], 229 | ["crimson", "#dc143c"], 230 | ["cyan", "#00ffff"], 231 | ["darkblue", "#00008b"], 232 | ["darkcyan", "#008b8b"], 233 | ["darkgoldenrod", "#b8860b"], 234 | ["darkgray", "#a9a9a9"], 235 | ["darkgreen", "#006400"], 236 | ["darkkhaki", "#bdb76b"], 237 | ["darkmagenta", "#8b008b"], 238 | ["darkolivegreen", "#556b2f"], 239 | ["darkorange", "#ff8c00"], 240 | ["darkorchid", "#9932cc"], 241 | ["darkred", "#8b0000"], 242 | ["darksalmon", "#e9967a"], 243 | ["darkseagreen", "#8fbc8f"], 244 | ["darkslateblue", "#483d8b"], 245 | ["darkslategray", "#2f4f4f"], 246 | ["darkturquoise", "#00ced1"], 247 | ["darkviolet", "#9400d3"], 248 | ["deeppink", "#ff1493"], 249 | ["deepskyblue", "#00bfff"], 250 | ["dimgray", "#696969"], 251 | ["dodgerblue", "#1e90ff"], 252 | ["firebrick", "#b22222"], 253 | ["floralwhite", "#fffaf0"], 254 | ["forestgreen", "#228b22"], 255 | ["fuchsia", "#ff00ff"], 256 | ["gainsboro", "#dcdcdc"], 257 | ["ghostwhite", "#f8f8ff"], 258 | ["gold", "#ffd700"], 259 | ["goldenrod", "#daa520"], 260 | ["gray", "#808080"], 261 | ["green", "#008000"], 262 | ["greenyellow", "#adff2f"], 263 | ["honeydew", "#f0fff0"], 264 | ["hotpink", "#ff69b4"], 265 | ["indianred ", "#cd5c5c"], 266 | ["indigo", "#4b0082"], 267 | ["ivory", "#fffff0"], 268 | ["khaki", "#f0e68c"], 269 | ["lavender", "#e6e6fa"], 270 | ["lavenderblush", "#fff0f5"], 271 | ["lawngreen", "#7cfc00"], 272 | ["lemonchiffon", "#fffacd"], 273 | ["lightblue", "#add8e6"], 274 | ["lightcoral", "#f08080"], 275 | ["lightcyan", "#e0ffff"], 276 | ["lightgoldenrodyellow", "#fafad2"], 277 | ["lightgrey", "#d3d3d3"], 278 | ["lightgreen", "#90ee90"], 279 | ["lightpink", "#ffb6c1"], 280 | ["lightsalmon", "#ffa07a"], 281 | ["lightseagreen", "#20b2aa"], 282 | ["lightskyblue", "#87cefa"], 283 | ["lightslategray", "#778899"], 284 | ["lightsteelblue", "#b0c4de"], 285 | ["lightyellow", "#ffffe0"], 286 | ["lime", "#00ff00"], 287 | ["limegreen", "#32cd32"], 288 | ["linen", "#faf0e6"], 289 | ["magenta", "#ff00ff"], 290 | ["maroon", "#800000"], 291 | ["mediumaquamarine", "#66cdaa"], 292 | ["mediumblue", "#0000cd"], 293 | ["mediumorchid", "#ba55d3"], 294 | ["mediumpurple", "#9370d8"], 295 | ["mediumseagreen", "#3cb371"], 296 | ["mediumslateblue", "#7b68ee"], 297 | ["mediumspringgreen", "#00fa9a"], 298 | ["mediumturquoise", "#48d1cc"], 299 | ["mediumvioletred", "#c71585"], 300 | ["midnightblue", "#191970"], 301 | ["mintcream", "#f5fffa"], 302 | ["mistyrose", "#ffe4e1"], 303 | ["moccasin", "#ffe4b5"], 304 | ["navajowhite", "#ffdead"], 305 | ["navy", "#000080"], 306 | ["oldlace", "#fdf5e6"], 307 | ["olive", "#808000"], 308 | ["olivedrab", "#6b8e23"], 309 | ["orange", "#ffa500"], 310 | ["orangered", "#ff4500"], 311 | ["orchid", "#da70d6"], 312 | ["palegoldenrod", "#eee8aa"], 313 | ["palegreen", "#98fb98"], 314 | ["paleturquoise", "#afeeee"], 315 | ["palevioletred", "#d87093"], 316 | ["papayawhip", "#ffefd5"], 317 | ["peachpuff", "#ffdab9"], 318 | ["peru", "#cd853f"], 319 | ["pink", "#ffc0cb"], 320 | ["plum", "#dda0dd"], 321 | ["powderblue", "#b0e0e6"], 322 | ["purple", "#800080"], 323 | ["rebeccapurple", "#663399"], 324 | ["red", "#ff0000"], 325 | ["rosybrown", "#bc8f8f"], 326 | ["royalblue", "#4169e1"], 327 | ["saddlebrown", "#8b4513"], 328 | ["salmon", "#fa8072"], 329 | ["sandybrown", "#f4a460"], 330 | ["seagreen", "#2e8b57"], 331 | ["seashell", "#fff5ee"], 332 | ["sienna", "#a0522d"], 333 | ["silver", "#c0c0c0"], 334 | ["skyblue", "#87ceeb"], 335 | ["slateblue", "#6a5acd"], 336 | ["slategray", "#708090"], 337 | ["snow", "#fffafa"], 338 | ["springgreen", "#00ff7f"], 339 | ["steelblue", "#4682b4"], 340 | ["tan", "#d2b48c"], 341 | ["teal", "#008080"], 342 | ["thistle", "#d8bfd8"], 343 | ["tomato", "#ff6347"], 344 | ["turquoise", "#40e0d0"], 345 | ["violet", "#ee82ee"], 346 | ["wheat", "#f5deb3"], 347 | ["white", "#ffffff"], 348 | ["whitesmoke", "#f5f5f5"], 349 | ["yellow", "#ffff00"], 350 | ["yellowgreen", "#9acd32"], 351 | ]); -------------------------------------------------------------------------------- /src/views/settings.ts: -------------------------------------------------------------------------------- 1 | import { Setting, ExtraButtonComponent, SliderComponent, ToggleComponent, DropdownComponent } from 'obsidian'; 2 | import TagsRoutes from '../main'; 3 | export class settingGroup { 4 | public readonly id: string; 5 | public readonly rootContainer: HTMLElement; 6 | private headContainer: HTMLElement; 7 | private holdContainer: HTMLElement; 8 | private handleButton: ExtraButtonComponent; 9 | private _baseContainer: HTMLElement; 10 | private _goAction: boolean = true; 11 | plugin: TagsRoutes; 12 | /* 13 | This constructor will create: 14 | - a root container: 15 | - with a head container 16 | - and a hold container ready to add sub components 17 | - with in the given comtainer 18 | */ 19 | constructor(plugin: TagsRoutes, id: string, name: string, type: "root" | "group" | "flex-box"|"normal-box" = "group") {//isRoot: boolean = false) { 20 | this.plugin = plugin 21 | this.rootContainer = document.createElement('div') 22 | this.rootContainer.id = id; 23 | if (type === "group") { 24 | this.rootContainer.addClass("tree-item") 25 | this.rootContainer.addClass("graph-control-section") 26 | } 27 | if (type === "flex-box") { 28 | this.holdContainer = this.rootContainer.createDiv('div') 29 | this.holdContainer.addClass('setting-flex-box') 30 | return this; 31 | } 32 | if (type === "normal-box") { 33 | this.holdContainer = this.rootContainer.createDiv('div') 34 | //this.holdContainer.addClass('setting-flex-box') 35 | return this; 36 | } 37 | this.headContainer = this.rootContainer.createEl('div')//, { cls: 'title-bar' }) 38 | this.holdContainer = this.rootContainer.createDiv('div') 39 | if (type === "group") { 40 | this.handleButton = new ExtraButtonComponent(this.headContainer.createEl('div', { cls: 'tree-item-icon collapse-icon' })) 41 | .setIcon("chevron-down") 42 | .setTooltip("Close " + name) 43 | /* .onClick(() => { 44 | if (this.holdContainer.style.display === 'none') { 45 | this.holdContainer.style.display = 'inline'; 46 | this.handleButton.setTooltip("Close " + name); 47 | this.handleButton.setIcon("x") 48 | } else { 49 | this.holdContainer.style.display = 'none'; 50 | this.handleButton.setTooltip("Open " + name); 51 | this.handleButton.setIcon("chevron-down") 52 | } 53 | }); */ 54 | this.headContainer.createEl('div', { cls: 'tree-item-inner graph-control-section-header' }).textContent = name; 55 | this.headContainer.addClass("tree-item-self") 56 | 57 | this.headContainer.onclick = () => { 58 | if (this.holdContainer.style.display === 'none') { 59 | this.holdContainer.style.display = 'inline'; 60 | this.handleButton.setTooltip("Close " + name); 61 | this.handleButton.setIcon("x") 62 | } else { 63 | this.holdContainer.style.display = 'none'; 64 | this.handleButton.setTooltip("Open " + name); 65 | this.handleButton.setIcon("chevron-down") 66 | } 67 | }; 68 | this.headContainer.addClass("mod-collapsible") 69 | this.holdContainer.addClass("tree-item-children") 70 | } else if (type === "root") { 71 | 72 | this.handleButton = new ExtraButtonComponent(this.headContainer) 73 | .setTooltip("Open " + name) 74 | .onClick(() => { 75 | if (!this._goAction && this.holdContainer.style.display == 'none' && 76 | this._baseContainer.style.opacity == '100') { 77 | this._baseContainer.style.opacity = '0' 78 | this._baseContainer.addClass("is-close") 79 | this.handleButton.setTooltip("Show settings button"); 80 | return; 81 | } 82 | if (this.holdContainer.style.display == 'none' && 83 | this._baseContainer.style.opacity == '0') { 84 | this._baseContainer.style.opacity = '100' 85 | this.handleButton.setTooltip("Open " + name); 86 | this._goAction = true; 87 | return; 88 | } 89 | 90 | if (this.holdContainer.style.display === 'none') { 91 | this.holdContainer.style.display = 'block'; 92 | this.handleButton.setTooltip("Close " + name); 93 | this.handleButton.setIcon("x") 94 | this._baseContainer.removeClass("is-close") 95 | this.headContainer.addClasses(["graph-controls-button", "mod-close"]) 96 | this.handleButton.extraSettingsEl.addClass("narrow-icon-style") 97 | this._goAction = false 98 | } else { 99 | this.holdContainer.style.display = 'none'; 100 | this.handleButton.setTooltip("Open " + name); 101 | this._baseContainer.addClass("is-close") 102 | if (this._goAction == false) { 103 | this.handleButton.setTooltip("Hide this button"); 104 | this.headContainer.removeClasses(["graph-controls-button", "mod-close"]) 105 | this.handleButton.extraSettingsEl.removeClass("narrow-icon-style") 106 | } 107 | this.handleButton.setIcon("settings") 108 | } 109 | }); 110 | if (this.holdContainer.style.display === 'none') { 111 | this.handleButton.setIcon("x") 112 | } else { 113 | this.handleButton.setIcon("settings") 114 | } 115 | this.handleButton.extraSettingsEl.style.justifyContent = 'flex-end'; 116 | } 117 | return this 118 | } 119 | /* 120 | it add a htmlelement , or a settinggroup's root container 121 | to current hold container 122 | */ 123 | public add({ arg = null }: { arg?: HTMLElement | settingGroup | null } = {}): this { 124 | if (arg instanceof HTMLElement) { 125 | this.holdContainer.appendChild(arg); 126 | } else if (arg instanceof settingGroup) { 127 | this.holdContainer.appendChild(arg.rootContainer) 128 | } 129 | return this 130 | } 131 | public hide() { 132 | this.holdContainer.style.display = 'none' 133 | return this 134 | } 135 | public hideAll() { 136 | const subholders = Array.from(this.rootContainer.getElementsByClassName('tree-item-children')); 137 | subholders.forEach(element => { 138 | if (element instanceof HTMLElement) { 139 | (element as HTMLElement).style.display = 'none'; 140 | } 141 | }); 142 | this._baseContainer.addClass("is-close") 143 | this._baseContainer.style.padding = '0px' 144 | 145 | } 146 | public show() { 147 | this.holdContainer.style.display = 'block' 148 | return this 149 | } 150 | public addExButton(buttonIcon:string,buttonDesc:string,buttonCallback:()=>void) { 151 | new ExtraButtonComponent(this.holdContainer.createEl('div', { cls: 'group-link-button' })) 152 | .setIcon(buttonIcon) 153 | .setTooltip(buttonDesc) 154 | .onClick(buttonCallback) 155 | return this; 156 | } 157 | public addButton(buttonText: string, buttonClass: string, buttonCallback: () => void) { 158 | const button = this.holdContainer.createEl('div').createEl('button', { text: buttonText, cls: buttonClass }); 159 | button.addClass("mod-cta") 160 | button.addEventListener('click', buttonCallback); 161 | return this; 162 | } 163 | public getLastElement(ref: { value: HTMLElement | null }) { 164 | ref.value = this.holdContainer.lastChild as HTMLElement 165 | return this 166 | } 167 | addDropdown(name: string, options:Record, defaultValue: string, cb: (v: string) => void, cls: string = ".setting-item-inline") { 168 | let _dropdwon: DropdownComponent | undefined; 169 | const dropdwon = new Setting(this.holdContainer) 170 | .setName(name) 171 | .addDropdown(dropDown => _dropdwon = dropDown 172 | .addOptions(options 173 | /* { 174 | broken: "broken", 175 | md: "md", 176 | pdf:"pdf" 177 | } */ 178 | ) 179 | .setValue("broken") 180 | .onChange(async value => cb(value)) 181 | ) 182 | dropdwon.setClass(cls) 183 | if (_dropdwon !== undefined) { 184 | _dropdwon.setValue("broken") 185 | this.plugin.view._controls.push({ id: name, control: _dropdwon }); 186 | } 187 | return this; 188 | } 189 | addSlider(name: string, min: number, max: number, step: number, defaultNum: number, cb: (v: number) => void, cls: string = "setting-item-block") { 190 | let _slider: SliderComponent | undefined; 191 | const slider = new Setting(this.holdContainer) 192 | .setName(name) 193 | // .setClass("mod-slider") 194 | .addSlider(slider => 195 | _slider = slider 196 | .setLimits(min, max, step) 197 | .setValue(defaultNum) 198 | .setDynamicTooltip() 199 | .onChange(async value => { 200 | cb(value) 201 | // if (!this.plugin.skipSave) { 202 | // this.plugin.view.setSaveButton(true); 203 | // } 204 | })) 205 | slider.setClass(cls) 206 | if (_slider !== undefined) { 207 | _slider.setValue(defaultNum) 208 | this.plugin.view._controls.push({ id: name, control: _slider }); 209 | } 210 | return this; 211 | } 212 | addColorPicker(name: string, defaultColor: string, cb: (v: string) => void) { 213 | const colorpicker = new Setting(this.holdContainer) 214 | .setName(name) 215 | .setDesc(defaultColor || "#000000") 216 | .addColorPicker(picker => { 217 | picker 218 | .onChange(async (value) => { 219 | cb(value) 220 | setTimeout(() => colorpicker.setDesc(value), 0); 221 | }) 222 | .setValue(defaultColor) 223 | this.plugin.view._controls.push({ id: name, control: picker }) 224 | }) 225 | colorpicker.setClass("setting-item-inline") 226 | return this; 227 | } 228 | addToggle(name: string, defaultState: boolean, cb: (v: boolean) => void, needSave: boolean = true) { 229 | const toggler = new Setting(this.holdContainer) 230 | .setName(name) 231 | //.setDesc('Enable or disable logging the number of nodes and links when the graph loads') 232 | .addToggle((toggle: ToggleComponent) => { 233 | toggle 234 | .onChange(async value => { 235 | cb(value) 236 | // if (needSave && !this.plugin.skipSave) { 237 | // this.plugin.view.setSaveButton(true) 238 | // } 239 | } 240 | /* async (value) => { 241 | if (!value) { 242 | this.toggleEnableShow.setValue(value); 243 | } 244 | this.plugin.settings.enableSave = value; 245 | await this.plugin.saveSettings(); 246 | }*/ 247 | ) 248 | .setValue(defaultState) 249 | //this.toggleEnableSave = toggle; 250 | this.plugin.view._controls.push({ id: name, control: toggle}) 251 | } 252 | ) 253 | toggler.setClass("setting-item-inline") 254 | return this; 255 | } 256 | addText(name: string, cb: (v: string) => void) { 257 | const texter = new Setting(this.holdContainer) 258 | .setName(name) 259 | .addText(picker => picker 260 | .setPlaceholder("file path") 261 | .onChange(async (value) => { 262 | cb(value) 263 | }) 264 | ) 265 | texter.setClass("setting-item-block") 266 | return this; 267 | } 268 | attachEl(container: HTMLElement) { 269 | container.append(this.rootContainer) 270 | this._baseContainer = container; 271 | this._baseContainer.style.opacity = '100' 272 | return this; 273 | } 274 | } -------------------------------------------------------------------------------- /src/views/tag-report.js.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | var term = "#empty" 4 | function getarg(...args) { 5 | term = args[0] 6 | } 7 | getarg(input) 8 | const files = app.vault.getMarkdownFiles() 9 | const arr = files.map( 10 | async (file) => { 11 | const content = await app.vault.cachedRead(file) 12 | if (content.contains("tag-report")) { 13 | return [] 14 | } 15 | 16 | const lines = content 17 | .split(/\n[\ ]*\n/) 18 | .filter(line => line.contains(term)) 19 | 20 | if (lines.length != 0) { 21 | var mmtime 22 | var regstr = term + '[\/_# \u4e00-\u9fa5]* +(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})' 23 | var regex = new RegExp(regstr, 'g') 24 | const retArr = lines.map( 25 | 26 | (line) => { 27 | regex.lastIndex = 0 28 | let match = regex.exec(line) 29 | if (match) { 30 | mmtime = " Tag Time: " + match[1] 31 | } else { 32 | mmtime = " Created Time: " + moment(file.stat.ctime).format('YYYY-MM-DD HH:mm:ss') 33 | } 34 | return [line + "\n\n \[*From* " + "[[" + 35 | file.name.split(".")[0] + "]], *" + mmtime + "*\]\n"] 36 | } 37 | ) 38 | return retArr 39 | } else { 40 | return [] 41 | } 42 | } 43 | ) 44 | function getLineTime(line) { 45 | let regstr = 'Time: +(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})' 46 | let regex = new RegExp(regstr, 'g') 47 | let match = regex.exec(line) 48 | if (match) { 49 | return (new Date(match[1])).getTime() 50 | } else 51 | return 0 52 | } 53 | Promise.all(arr).then(values => { 54 | let noteArr = values.flat() 55 | noteArr.sort((a, b) => getLineTime(a) - getLineTime(b)) 56 | dv.header(3, "Tag\ [" + term + "\] total: `" + noteArr.length + "` records.") 57 | for (let i = 0; i < noteArr.length; i++) { 58 | dv.paragraph("## " + (i + 1) + "\n" + `${noteArr[noteArr.length - 1 - i]}`) 59 | } 60 | }) 61 | 62 | async function fun001(a, b){ 63 | return a + b; 64 | } 65 | 66 | function fun002() 67 | { 68 | a = 3; 69 | b = 4; 70 | fun001(a,b).then(value=>DebugMsg(DebugLevel.DEBUG,"the result is:", value)) 71 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | This CSS file will be included with your plugin, and 3 | available in the app when your plugin is enabled. 4 | If your plugin does not need CSS, delete this file. 5 | */ 6 | 7 | .tags-routes { 8 | padding: 0px !important; 9 | position: relative; 10 | overflow: hidden !important; 11 | } 12 | 13 | /* .tags-routes .graph-container { 14 | overflow: hidden !important; 15 | } */ 16 | 17 | .tags-routes .tooltip-wrapper { 18 | position: absolute; 19 | margin-left: 0px; 20 | transition: 1s ease-in-out; 21 | } 22 | 23 | .tags-routes .tooltip-wrapper { 24 | overflow: hidden; 25 | display: flex; 26 | background-color: var(--background-secondary); 27 | position: absolute !important; 28 | left: 0px; 29 | bottom: 0px; 30 | border-top-right-radius: 5px; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .tags-routes .tooltip-flexbox-container { 36 | display: flex; 37 | font: var(--text-normal); 38 | top: 2.5%; 39 | height: 95%; 40 | width: 100%; 41 | margin-left: 0%; 42 | } 43 | 44 | .tags-routes .hidden { 45 | position: relative; 46 | margin-left: -34.1em !important; 47 | transition: 1s ease-in-out; 48 | } 49 | 50 | .tags-routes .tooltip-flexbox-container-hide { 51 | font: var(--text-normal); 52 | font-size: 10px; 53 | margin-bottom: 0px; 54 | border-radius: 0px; 55 | border-top-right-radius: 5px; 56 | } 57 | 58 | .tags-routes .tooltip-flexbox { 59 | margin: 5px; 60 | font-size: 10px; 61 | } 62 | 63 | .tags-routes .tooltip-divider { 64 | border: 0.5px solid var(--background-secondary-alt); 65 | border-radius: 5px; 66 | margin-top: 2px; 67 | margin-bottom: 2px; 68 | } 69 | 70 | 71 | /* .tags-routes .settings-container { 72 | position: absolute; 73 | top: 5px; 74 | right: 5px; 75 | background-color: var(--background-primary); 76 | } */ 77 | 78 | .tags-routes .narrow-icon-style { 79 | padding: 0px; 80 | border-radius: 2px; 81 | } 82 | 83 | .tags-routes .mod-close { 84 | position: absolute; 85 | top: var(--size-4-1); 86 | inset-inline-end: var(--size-4-2); 87 | padding: var(--size-2-2); 88 | } 89 | 90 | 91 | /* 92 | .tags-routes .slider-container { 93 | display: block; 94 | margin-left: 5px; 95 | margin-right: 5px; 96 | } 97 | 98 | 99 | .tags-routes .group-holder { 100 | text-align: center; 101 | } 102 | */ 103 | 104 | .tags-routes .graph-button { 105 | top: 15px; 106 | right: 10px; 107 | z-index: 10; 108 | padding: 1px 10px; 109 | /* margin: 4px;*/ 110 | font-size: 14px; 111 | border: none; 112 | border-radius: 5px; 113 | cursor: pointer; 114 | height: 25px; 115 | text-align: center; 116 | } 117 | 118 | 119 | /* .tags-routes .title-bar { 120 | display: block; 121 | text-align: left; 122 | } */ 123 | 124 | .tags-routes .inline-settings { 125 | display: grid; 126 | justify-content: end; 127 | grid-auto-flow: column; 128 | align-items: end; 129 | /* border-top: 1px solid var(--background-modifier-border);*/ 130 | text-align: right; 131 | } 132 | 133 | .tags-routes .tg-settingtab-heading { 134 | border-bottom: 1px solid var(--background-modifier-border); 135 | } 136 | 137 | .tags-routes .group-bar-button { 138 | display: inline-block; 139 | margin-top: 5px; 140 | margin-bottom: 5px; 141 | margin-left: 5px; 142 | margin-right: 5px; 143 | } 144 | 145 | .tags-routes .group-bar-text { 146 | display: inline-block; 147 | margin-top: 5px; 148 | margin-bottom: 5px; 149 | padding-right: 45px; 150 | margin-right: 5px; 151 | } 152 | 153 | .tags-routes .setting-item-block { 154 | display: block; 155 | text-align: left; 156 | margin-bottom: 5px; 157 | margin-left: 5px; 158 | margin-right: 5px; 159 | padding: 0px; 160 | border-top: 0px; 161 | } 162 | 163 | .tags-routes .setting-flex-box { 164 | display: flex; 165 | justify-content: space-between; 166 | } 167 | 168 | .tags-routes .setting-item-inline { 169 | display: flex; 170 | justify-content: space-between; 171 | border-top: 0px; 172 | } 173 | 174 | .tags-routes .setting-item-inline .setting-item-info { 175 | flex: 0 1 auto; 176 | padding-left: 5px; 177 | justify-content: space-between; 178 | } 179 | 180 | .tags-routes .setting-item-name { 181 | margin-left: 2px; 182 | } 183 | 184 | .tags-routes .setting-item-control input[type="range"] { 185 | width: 100%; 186 | height: 1px; 187 | margin-top: 0.6em; 188 | margin-bottom: 0.6em; 189 | /* 确保滑动条的input元素也占据100%宽度 */ 190 | } 191 | 192 | .tags-routes .setting-item-control input[type="text"] { 193 | width: 100%; 194 | margin-top: 0.5em; 195 | padding: 0px; 196 | } 197 | .tags-routes .setting-item-filter { 198 | display: flex; 199 | align-items: flex-start ; 200 | } 201 | .tags-routes .setting-item-filter .setting-item-control textarea{ 202 | width: 20em; 203 | height: 15em; 204 | } 205 | .tags-routes .setting-item-filter .setting-item-description { 206 | padding-left: 0.2em; 207 | } 208 | .notice-container { 209 | /* display: none;*/ 210 | } 211 | 212 | .tags-routes .scene-nav-info { 213 | display: none; 214 | visibility: hidden; 215 | } 216 | 217 | .tags-routes-settings-title { 218 | display: flex; 219 | align-items: center; 220 | justify-content: space-between; 221 | } 222 | 223 | .tags-routes .group-link-button .clickable-icon { 224 | /* color: var(--interactive-accent);*/ 225 | } 226 | .tags-routes .graph-animate-play-button .clickable-icon { 227 | /* color: var(--interactive-accent);*/ 228 | } 229 | .tags-routes .graph-controls{ 230 | background-color: rgb(from var(--background-primary) r g b / 0.7) 231 | } 232 | .tags-routes-need-save { 233 | border-bottom: 5px solid var(--interactive-accent-hover); 234 | } 235 | .monitor-screen { 236 | position: absolute; 237 | top: 10px; 238 | left: 10px; 239 | background-color: rgb(from var(--background-primary) r g b / 0.7); 240 | /*border: 1px solid #ccc;*/ 241 | /*border: 1px solid;*/ 242 | padding: 2px; 243 | border-radius: 4px; 244 | /*box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);*/ 245 | border: 1px solid var(--background-modifier-border); 246 | box-shadow: var(--shadow-s) 247 | } 248 | 249 | /* 250 | .tags-routes .graph-control-section{ 251 | padding: var(--size-2-3) var(--size-4-3) 252 | }*/ 253 | 254 | .container-fluid { 255 | /*height: 100px;*/ 256 | /*background: linear-gradient(45deg, #2d2a6c 1%,#5b81ea 83%,#9393f2 100%);*/ 257 | background: linear-gradient(45deg, var(--background-primary) 1%, var(--background-secondary) 83%, var(--background-secondary-alt) 100%); 258 | text-align: center; 259 | padding-top: 10px; 260 | overflow: hidden !important; 261 | position: relative; 262 | } 263 | 264 | .tg-alert { 265 | color: var(--text-accent); 266 | font-size: 2em; 267 | animation: interference 3s infinite; 268 | } 269 | 270 | @keyframes interference { 271 | 0% { 272 | transform: skewX(0) 273 | } 274 | 31% { 275 | transform: skewX(-2deg) 276 | } 277 | 31.5% { 278 | transform: skewX(89deg) 279 | } 280 | 32% { 281 | transform: skewX(89deg) 282 | } 283 | 32.1% { 284 | transform: skewX(0) 285 | } 286 | 33% { 287 | transform: skewX(2deg) 288 | } 289 | 54% { 290 | transform: skewX(0deg) 291 | } 292 | 94% { 293 | transform: skewX(2deg) 294 | } 295 | 95.1% { 296 | transform: skewX(-3deg) 297 | } 298 | 95.2% { 299 | transform: skewX(-89deg) 300 | } 301 | 95.3% { 302 | transform: skewX(2deg) 303 | } 304 | 100% { 305 | transform: skewX(0) 306 | } 307 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /usage/node-highlight.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/node-highlight.gif -------------------------------------------------------------------------------- /usage/setup-color-v109.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/setup-color-v109.gif -------------------------------------------------------------------------------- /usage/setup-color.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/setup-color.gif -------------------------------------------------------------------------------- /usage/switch-settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/switch-settings.gif -------------------------------------------------------------------------------- /usage/v1.1.0-defaultLightTheme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.1.0-defaultLightTheme.gif -------------------------------------------------------------------------------- /usage/v1.1.0-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.1.0-usage.gif -------------------------------------------------------------------------------- /usage/v1.1.1-feature.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.1.1-feature.gif -------------------------------------------------------------------------------- /usage/v1.1.3-feature.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.1.3-feature.gif -------------------------------------------------------------------------------- /usage/v1.1.8-feature.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.1.8-feature.gif -------------------------------------------------------------------------------- /usage/v1.2.1-lockScene.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.2.1-lockScene.gif -------------------------------------------------------------------------------- /usage/v1.2.1-setFocus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.2.1-setFocus.gif -------------------------------------------------------------------------------- /usage/v1.2.3-feature1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.2.3-feature1.png -------------------------------------------------------------------------------- /usage/v1.2.3-feature2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v1.2.3-feature2.png -------------------------------------------------------------------------------- /usage/v109-update.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v109-update.gif -------------------------------------------------------------------------------- /usage/v120-feature.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kctekn/obsidian-TagsRoutes/89b4be5d60a760089352aefe3cc54c1f2f031df0/usage/v120-feature.gif -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0" 3 | } 4 | --------------------------------------------------------------------------------