├── .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 |

4 |

5 |
6 |
7 | This is a plugin for obsidian, to visualize files and tags as nodes in 3D graphic.
8 | 
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 |
--------------------------------------------------------------------------------