├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .npmrc
├── .prettierrc.js
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── ColorfulNoteBordersDemo800.gif
├── ColorfulNoteBordersDemov0.3.gif
└── PopupWindow.png
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package-lock.json
├── package.json
├── release.sh
├── src
├── main.ts
└── settingsTab.ts
├── styles.css
├── tests
├── __mocks__
│ └── obsidian.ts
├── global-setup.js
└── main.test.ts
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # See http://EditorConfig.org for more information about .editorconfig files.
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | indent_style = space
10 | indent_size = 4
11 | tab_width = 4
12 |
13 | [*.xml]
14 | indent_size = 2
15 |
16 | [*.json]
17 | indent_size = 4
18 | # Stop IDEs adding newlines to end of Obsidian .json config files:
19 | insert_final_newline = false
20 |
21 | [*.{yml,yaml}]
22 | indent_size = 2
23 |
24 | [*.{md,mdx}]
25 | trim_trailing_whitespace = true
26 |
27 | [*.{htm,html,js,jsm,ts,tsx,mjs}]
28 | indent_size = 4
29 |
30 | [*.{cmd,bat,ps1}]
31 | end_of_line = crlf
32 |
33 | [*.sh]
34 | end_of_line = lf
35 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | src/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 | }
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | trailingComma: 'all', // "es5"
3 | printWidth: 120,
4 | tabWidth: 4,
5 | useTabs: false,
6 | singleQuote: true,
7 | bracketSpacing: true,
8 | semi: triggerAsyncId,
9 | };
10 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## NEXT (NEXT)
4 |
5 | ### Bugfixes
6 |
7 | - Fixed issue #4: Folder string that is part of a filename will be selected
8 |
9 | ## 0.2.4 (2023-03-30)
10 |
11 | ### Bugfixes
12 |
13 | - Improved responsiveness; color rules are now applied to all views.
14 |
15 | ## 0.2.3 (2023-03-28)
16 |
17 | - code cleanup
18 |
19 | ## 0.2.1 (2023-03-27)
20 |
21 | ### Bugfixes
22 |
23 | - Multiple matching rules are now applied correctly based on their order - first rule takes precedence.
24 |
25 | ## 0.2.0 (2023-03-27)
26 |
27 | ### Features
28 |
29 | - Added ability to re-order the rules to prioritize which rule takes precedence when multiple rules match. The last rule takes precedence.
30 |
31 | ### Bugfixes
32 |
33 | - updated the settings tab layout
34 | - fixed color input and color picker so that they update properly when the other is changed
35 |
36 | ## 0.1.1 (2023-03-27)
37 |
38 | Initial Release
39 |
40 | ### Features
41 |
42 | - Apply colorful borders to notes based on customizable rules
43 | - Supports two types of rules:
44 | - Folder location
45 | - Frontmatter metadata
46 | - Add and remove rules as needed
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | # Released under MIT License
2 |
3 | Copyright (c) 2013 Mark Otto.
4 |
5 | Copyright (c) 2017 Andrew Fong.
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy of
8 | this software and associated documentation files (the "Software"), to deal in
9 | the Software without restriction, including without limitation the rights to
10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
11 | the Software, and to permit persons to whom the Software is furnished to do so,
12 | subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Colorful Note Borders Plugin for Obsidian
2 |
3 | The Colorful Note Borders plugin for Obsidian is designed to help you visually distinguish your notes based on custom rules. By applying colored borders around your notes, you can easily recognize and categorize them based on their folder location or specific frontmatter metadata.
4 |
5 | This plugin supports two types of rules:
6 |
7 | 1. **Folder-based rules**: Apply a colorful border to notes based on their folder location. For example, you can configure a green border to be displayed around notes located in the "Inbox" folder.
8 | 2. **Frontmatter metadata-based rules**: Apply a colorful border to notes based on their frontmatter metadata. For example, you can configure a red border to be displayed around notes that have "private: true" property in the frontmatter metadata.
9 |
10 | By using the Colorful Note Borders plugin, you can create a more organized and visually appealing workspace in Obsidian. Customize your note appearance with an easy-to-configure settings page that allows you to define your color rules dynamically.
11 |
12 | ## Demo
13 |
14 |
15 |
16 |
17 |
18 | ## Features
19 |
20 | The Colorful Note Borders plugin for Obsidian offers the following features:
21 |
22 | - Apply colorful borders to notes based on customizable rules.
23 | - Supports two types of rules:
24 | - Folder location
25 | - Frontmatter metadata
26 | - Users can add, edit, and remove rules from the settings page.
27 | - Users can re-order the rules to prioritize which rule takes precedence when multiple rules match. The first rule takes precedence.
28 | - Compatible with Obsidian's light and dark modes.
29 |
30 | ## Installation
31 |
32 | To install the Colorful Borders plugin, follow these steps:
33 |
34 | 1. Open your Obsidian vault
35 | 2. Go to the Settings page (click the gear icon in the left sidebar)
36 | 3. Navigate to Third-party plugins and make sure the "Safe mode" toggle is off
37 | 4. Click "Browse" and search for "Colorful Note Borders"
38 | 5. Click "Install" on the Colorful Note Borders plugin
39 | 6. After the installation is complete, click "Enable" to activate the plugin
40 |
41 | ## Manual Installation using BRAT
42 |
43 | BRAT (Beta Reviewers Auto-update Tester) is a plugin for Obsidian that allows you to install and manage plugins that are not yet approved and included in the Obsidian Plugin Directory. You can use BRAT to install the Colorful Borders plugin manually.
44 |
45 | ### Prerequisites
46 |
47 | - Obsidian 0.9.7 or later
48 |
49 | ### Installation Steps
50 |
51 | 1. Open your Obsidian vault.
52 | 2. Go to the Settings page (click the gear icon in the left sidebar).
53 | 3. Navigate to Third-party plugins and make sure the "Safe mode" toggle is off.
54 | 4. Click "Browse" and search for "BRAT".
55 | 5. Click "Install" on the BRAT plugin.
56 | 6. After the installation is complete, click "Enable" to activate the BRAT plugin.
57 | 7. Navigate to Plugin Options and click on "BRAT".
58 | 8. In the "Plugin Repository URL" field, enter the GitHub repository URL for the Colorful Borders plugin (`https://github.com/rusi/obsidian-colorful-note-borders`).
59 | 9. Click "Add plugin".
60 | 10. Click "Update plugins" to download and install the Colorful Note Borders plugin.
61 | 11. Navigate to Third-party plugins in the Obsidian settings.
62 | 12. Find the Colorful Note Borders plugin in the "Installed plugins" list and click "Enable" to activate it.
63 |
64 | Now the Colorful Note Borders plugin should be installed and activated. Follow the usage instructions in the previous sections to configure the plugin.
65 |
66 | ## Usage
67 |
68 | To configure the Colorful Note Borders plugin, follow these steps:
69 |
70 | 1. Go to the Settings page in your Obsidian vault
71 | 2. Navigate to Plugin Options and click on "Colorful Note Borders"
72 | 3. In the settings page, you can add or remove rules by clicking the "Add new rule" button or the "Remove" button next to each rule
73 | 4. Configure each rule by providing:
74 | - A name for the rule
75 | - A value to match (e.g., folder name or frontmatter metadata value)
76 | - The rule type (either "Path" for folder location or "Frontmatter" for frontmatter metadata)
77 | - A color for the border (use the color picker or enter a color hex code)
78 | 5. Save your settings
79 |
80 | The plugin will automatically apply the colorful borders to your notes based on the rules you've configured. If a note matches a rule, the border will be displayed around the note's content.
81 |
82 | ## Support
83 |
84 | If you encounter any issues or have feature requests, please create an issue on the plugin's GitHub repository.
85 |
86 | ## License
87 |
88 | This plugin is licensed under the MIT License. For more information, see the LICENSE file in the plugin's GitHub repository.
89 |
--------------------------------------------------------------------------------
/assets/ColorfulNoteBordersDemo800.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/ColorfulNoteBordersDemo800.gif
--------------------------------------------------------------------------------
/assets/ColorfulNoteBordersDemov0.3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/ColorfulNoteBordersDemov0.3.gif
--------------------------------------------------------------------------------
/assets/PopupWindow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rusi/obsidian-colorful-note-borders/d804202b1bb9ee634a9c7b5b3c1ced77adab46b9/assets/PopupWindow.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | verbose: true,
3 | preset: 'ts-jest',
4 | // testEnvironment: 'node',
5 | // roots: ['/tests'],
6 | transform: {
7 | '^.+\\.ts$': 'ts-jest'
8 | },
9 | moduleFileExtensions: ['js', 'ts', 'svelte'],
10 | collectCoverage: true, // Enable coverage collection
11 | coverageDirectory: 'coverage', // Directory where coverage reports will be stored
12 | collectCoverageFrom: [
13 | 'src/**/*.{js,ts}', // Include all JavaScript and TypeScript files in src/
14 | '!src/**/*.d.ts' // Exclude TypeScript declaration files
15 | ],
16 | coverageReporters: ['json', 'lcov', 'text', 'clover'], // Specify coverage reporters
17 |
18 | globalSetup: './tests/global-setup.js',
19 | };
20 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "colorful-note-borders",
3 | "name": "Colorful Note Borders",
4 | "version": "0.2.4",
5 | "minAppVersion": "0.15.0",
6 | "description": "Add customizable colorful borders to notes based on folder location or frontmatter metadata, enhancing visual organization in Obsidian.",
7 | "author": "rusi",
8 | "isDesktopOnly": false
9 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "colorful-note-borders",
3 | "version": "0.2.4",
4 | "description": "Add customizable colorful borders to notes based on folder location or frontmatter metadata, enhancing visual organization in Obsidian.",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "node esbuild.config.mjs",
8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
9 | "lint": "eslint . --ext .ts",
10 | "test": "jest --ci",
11 | "test:dev": "jest --watch",
12 | "test:coverage": "jest --coverage",
13 | "version": "node version-bump.mjs && git add manifest.json versions.json",
14 | "release": "standard-version -t ''"
15 | },
16 | "keywords": [
17 | "obsidian",
18 | "note-colors"
19 | ],
20 | "author": "rusi",
21 | "license": "MIT",
22 | "devDependencies": {
23 | "@types/jest": "^29.5.11",
24 | "@types/node": "^16.11.6",
25 | "@typescript-eslint/eslint-plugin": "5.29.0",
26 | "@typescript-eslint/parser": "5.29.0",
27 | "builtin-modules": "3.3.0",
28 | "esbuild": "0.17.3",
29 | "eslint-config-prettier": "^9.1.0",
30 | "eslint-plugin-prettier": "^5.1.3",
31 | "jest": "^29.7.0",
32 | "jest-environment-jsdom": "^29.7.0",
33 | "obsidian": "latest",
34 | "prettier": "^3.2.2",
35 | "standard-version": "^9.1.1",
36 | "ts-jest": "^29.1.1",
37 | "tslib": "2.4.0",
38 | "typescript": "4.7.4"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 | set -o pipefail
4 | npm run build
5 | npm version patch
6 | # npm run release
7 | git push origin master --tags
8 | version=$(cat manifest.json | jq -r ".version")
9 | gh release create ${version} -F CHANGELOG.md manifest.json main.js styles.css
10 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { Plugin, TFile, MarkdownView, WorkspaceLeaf } from 'obsidian';
2 |
3 | import { SettingsTab, ColorBorderSettings, DEFAULT_SETTINGS, ColorRule, RuleType } from './settingsTab';
4 |
5 | export const checkPath = (currentPath: string, folder: string): boolean => {
6 | // return currentPath.includes(folder);
7 | const parts = currentPath.split(/[/\\]/);
8 | return parts.includes(folder);
9 | }
10 |
11 | export default class ColorfulNoteBordersPlugin extends Plugin {
12 | settings: ColorBorderSettings;
13 |
14 | async onload() {
15 | await this.loadSettings();
16 |
17 | // This adds a settings tab so the user can configure various aspects of the plugin
18 | this.addSettingTab(new SettingsTab(this.app, this));
19 |
20 | this.registerEvent(
21 | this.app.workspace.on("active-leaf-change", this.onActiveLeafChange.bind(this))
22 | );
23 | this.registerEvent(
24 | this.app.metadataCache.on("changed", this.onMetadataChange.bind(this))
25 | );
26 | this.registerEvent(
27 | this.app.vault.on("rename", this.onFileRename.bind(this))
28 | );
29 | }
30 |
31 | async onunload() {
32 | // cleanup all custom styles
33 | this.settings.colorRules.forEach((rule) => {
34 | this.removeStyle(rule);
35 | });
36 | }
37 |
38 | async removeStyle(rule: ColorRule) {
39 | const style = this.makeStyleName(rule);
40 | const styleElement = document.getElementById(style);
41 | if (styleElement) {
42 | styleElement.remove();
43 | }
44 | }
45 |
46 | async onActiveLeafChange(activeLeaf: WorkspaceLeaf) {
47 | // console.log("+ active leaf change: ", activeLeaf);
48 | this.applyRules();
49 | }
50 |
51 | async onMetadataChange(file: TFile) {
52 | // console.log("+ metadata change");
53 | this.applyRules(file);
54 | }
55 |
56 | async onFileRename(file: TFile) {
57 | // console.log("+ filename change");
58 | this.applyRules();
59 | }
60 |
61 | async loadSettings() {
62 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
63 | this.updateStyles();
64 | }
65 |
66 | async saveSettings() {
67 | await this.saveData(this.settings);
68 | this.updateStyles();
69 | const activeFile = this.app.workspace.getActiveFile();
70 | if (activeFile) {
71 | this.onFileRename(activeFile);
72 | }
73 | }
74 |
75 | async updateStyles() {
76 | this.settings.colorRules.forEach((rule) => this.updateStyle(rule));
77 | }
78 | async updateStyle(rule: ColorRule) {
79 | const styleName = this.makeStyleName(rule);
80 | this.updateCustomCSS(styleName, `
81 | .${styleName} {
82 | border: 5px solid ${rule.color} !important;
83 | }
84 | `);
85 | }
86 |
87 | addCustomCSS(cssstylename: string, css: string) {
88 | const styleElement = document.createElement('style');
89 | styleElement.id = cssstylename;
90 | styleElement.innerText = css;
91 | document.head.appendChild(styleElement);
92 | }
93 | updateCustomCSS(cssstylename: string, css: string) {
94 | const styleElement = document.getElementById(cssstylename);
95 | if (styleElement) {
96 | styleElement.innerText = css;
97 | } else {
98 | this.addCustomCSS(cssstylename, css);
99 | }
100 | }
101 |
102 | async applyRules(file: TFile | null = null) {
103 | this.app.workspace.getLeavesOfType("markdown").forEach((value: WorkspaceLeaf) => {
104 | if (!(value.view instanceof MarkdownView)) return;
105 | const activeView = value.view as MarkdownView;
106 | const viewFile = activeView.file;
107 | if (file && file !== viewFile) return;
108 | const contentView = activeView.containerEl.querySelector(".view-content");
109 | if (!contentView) return;
110 |
111 | this.unhighlightNote(contentView);
112 | this.settings.colorRules.some((rule) => {
113 | return this.applyRule(viewFile, rule, contentView);
114 | });
115 | });
116 | }
117 |
118 | applyRule(file: TFile, rule: ColorRule, contentView: Element): boolean {
119 | switch (rule.type) {
120 | case RuleType.Folder: {
121 | if (checkPath(file.path, rule.value)) {
122 | // console.log("- folder -", file);
123 | // console.log(file.path);
124 | // console.log(rule.value);
125 | this.highlightNote(contentView, rule);
126 | return true;
127 | }
128 | break;
129 | }
130 | case RuleType.Frontmatter: {
131 | // console.log("- front-matter -", file);
132 | // console.log(rule.value);
133 | const [key, value] = rule.value.split(":", 2);
134 | const frontMatterValue = this.app.metadataCache.getFileCache(file)?.frontmatter?.[key];
135 | const normalizedFrontMatterValue = frontMatterValue?.toString().toLowerCase().trim();
136 | const normalizedValueToHighlight = value?.toString().toLowerCase().trim();
137 | // console.log(`++ front matter: ${key}, ${value} :: ${normalizedFrontMatterValue} === ${normalizedValueToHighlight}`);
138 | if (normalizedFrontMatterValue === normalizedValueToHighlight) {
139 | this.highlightNote(contentView, rule);
140 | return true;
141 | }
142 | break
143 | }
144 | }
145 | return false;
146 | }
147 |
148 | highlightNote(element: Element, rule: ColorRule) {
149 | element.classList.add(this.makeStyleName(rule));
150 | }
151 |
152 | unhighlightNote(element: Element) {
153 | this.settings.colorRules.forEach((rule) => {
154 | element.classList.remove(this.makeStyleName(rule));
155 | });
156 | }
157 |
158 | makeStyleName(rule: ColorRule): string {
159 | return `cnb-${rule.id}-style`;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/settingsTab.ts:
--------------------------------------------------------------------------------
1 | import { App, PluginSettingTab, Setting, TextComponent, ButtonComponent, DropdownComponent, ColorComponent } from 'obsidian';
2 | import ColorfulNoteBordersPlugin from './main';
3 |
4 | export enum RuleType {
5 | Folder = "folder",
6 | Frontmatter = "frontmatter"
7 | }
8 |
9 | export interface ColorRule {
10 | id: string;
11 | value: string;
12 | type: RuleType;
13 | color: string;
14 | }
15 |
16 | export class ColorBorderSettings {
17 | colorRules: ColorRule[] = [];
18 | }
19 |
20 | export const DEFAULT_SETTINGS: ColorBorderSettings = {
21 | colorRules: [
22 | {
23 | id: "inbox-ffb300",
24 | value: "Inbox",
25 | type: RuleType.Folder,
26 | color: "#ffb300"
27 | },
28 | {
29 | id: "frontmatter-public-499749",
30 | value: "category: public",
31 | type: RuleType.Frontmatter,
32 | color: "#499749"
33 | },
34 | {
35 | id: "frontmatter-private-c44545",
36 | value: "category: private",
37 | type: RuleType.Frontmatter,
38 | color: "#c44545"
39 | }
40 | ],
41 | };
42 |
43 | export class SettingsTab extends PluginSettingTab {
44 | plugin: ColorfulNoteBordersPlugin;
45 |
46 | constructor(app: App, plugin: ColorfulNoteBordersPlugin) {
47 | super(app, plugin);
48 | this.plugin = plugin;
49 | }
50 |
51 | display(): void {
52 | let { containerEl } = this;
53 | containerEl.empty();
54 | containerEl.createEl('h1', { text: 'Colorful Note Borders Settings' });
55 |
56 | // Create a header row
57 | const headerRow = containerEl.createEl('div', { cls: 'cnb-rule-settings-header-row' });
58 |
59 | // Add labels for each column
60 | headerRow.createEl('span', { text: 'Rule Type', cls: 'cnb-rule-settings-column-rule-type' });
61 | headerRow.createEl('span', { text: 'Value', cls: 'cnb-rule-settings-column-rule-value' });
62 | headerRow.createEl('span', { text: 'Color', cls: 'cnb-rule-settings-column-rule-color' });
63 | headerRow.createEl('span', { text: '', cls: 'cnb-rule-settings-column-rule-button' });
64 |
65 | const rulesContainer = containerEl.createEl('div', { cls: 'cnb-rules-container' });
66 |
67 | // Display existing rules
68 | this.plugin.settings.colorRules.forEach((rule, index) => this.addRuleSetting(rulesContainer, rule, index));
69 |
70 | // Add new rule button
71 | new ButtonComponent(containerEl)
72 | .setButtonText('Add new rule')
73 | .onClick(() => {
74 | const newRule: ColorRule = {
75 | id: Date.now().toString(),
76 | value: '',
77 | type: RuleType.Folder,
78 | color: '#000000',
79 | };
80 | this.plugin.settings.colorRules.push(newRule);
81 | this.addRuleSetting(rulesContainer, newRule);
82 | this.plugin.saveSettings();
83 | });
84 | }
85 |
86 | addRuleSetting(
87 | containerEl: HTMLElement,
88 | rule: ColorRule,
89 | index: number = this.plugin.settings.colorRules.length - 1,
90 | ): void {
91 | const ruleSettingDiv = containerEl.createEl('div', { cls: 'cnb-rule-settings-row' });
92 |
93 | new Setting(ruleSettingDiv)
94 | // .setName('Type')
95 | .setClass('cnb-rule-setting-item')
96 | .addDropdown((dropdown: DropdownComponent) => {
97 | dropdown.addOption(RuleType.Folder, 'Folder');
98 | dropdown.addOption(RuleType.Frontmatter, 'Frontmatter');
99 | dropdown.setValue(rule.type);
100 | dropdown.onChange((value) => {
101 | rule.type = value as RuleType;
102 | this.plugin.saveSettings();
103 | });
104 | dropdown.selectEl.classList.add('cnb-rule-type-dropdown');
105 | });
106 |
107 | new Setting(ruleSettingDiv)
108 | // .setName('Value')
109 | .setClass('cnb-rule-setting-item')
110 | .addText((text) => {
111 | text.setPlaceholder('Enter rule value');
112 | text.setValue(rule.value);
113 | text.onChange((value) => {
114 | rule.value = value;
115 | this.plugin.saveSettings();
116 | });
117 | text.inputEl.classList.add('cnb-rule-value-input');
118 | });
119 |
120 | const colorSetting = new Setting(ruleSettingDiv)
121 | .setClass('cnb-rule-setting-item');
122 | // .setName('Color');
123 | // colorSetting.settingEl.style.gridColumn = '3';
124 |
125 | const colorInput = new TextComponent(colorSetting.controlEl)
126 | .setPlaceholder('Enter color hex code')
127 | .setValue(rule.color);
128 | colorInput.inputEl.classList.add('cnb-rule-setting-item-text-input');
129 |
130 | const picker = new ColorComponent(colorSetting.controlEl)
131 | .setValue(rule.color)
132 | .onChange((color) => {
133 | rule.color = color;
134 | colorInput.setValue(color);
135 | this.plugin.saveSettings();
136 | });
137 |
138 | colorInput.onChange((value: string) => {
139 | if (/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value)) {
140 | rule.color = value;
141 | picker.setValue(value);
142 | this.plugin.saveSettings();
143 | }
144 | });
145 |
146 | new ButtonComponent(ruleSettingDiv)
147 | .setButtonText('▲')
148 | // .setIcon("up-arrow")
149 | .setTooltip("Move Up")
150 | .setClass('cnb-rule-setting-item-up-button')
151 | .setDisabled(index == 0)
152 | .onClick(() => {
153 | if (index > 0) {
154 | this.plugin.settings.colorRules.splice(index, 1);
155 | this.plugin.settings.colorRules.splice(index - 1, 0, rule);
156 | this.plugin.saveSettings();
157 | this.display();
158 | }
159 | });
160 |
161 | new ButtonComponent(ruleSettingDiv)
162 | .setButtonText('▼')
163 | // .setIcon("down-arrow")
164 | .setTooltip("Move Down")
165 | .setClass('cnb-rule-setting-item-down-button')
166 | .setDisabled(index == this.plugin.settings.colorRules.length - 1)
167 | .onClick(() => {
168 | if (index < this.plugin.settings.colorRules.length - 1) {
169 | this.plugin.settings.colorRules.splice(index, 1);
170 | this.plugin.settings.colorRules.splice(index + 1, 0, rule);
171 | this.plugin.saveSettings();
172 | this.display();
173 | }
174 | });
175 |
176 | new ButtonComponent(ruleSettingDiv)
177 | .setButtonText('Remove')
178 | // .setIcon('remove')
179 | // .setTooltip("Remove")
180 | .setClass('cnb-rule-setting-item-remove-button')
181 | .setCta().onClick(() => {
182 | this.plugin.settings.colorRules = this.plugin.settings.colorRules.filter((r) => r.id !== rule.id);
183 | this.plugin.saveSettings();
184 | this.plugin.removeStyle(rule);
185 | ruleSettingDiv.remove();
186 | });
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 |
2 | .cnb-rules-container {
3 | margin-bottom: 1em;
4 | border-top: 1px solid var(--background-modifier-border);
5 | }
6 |
7 | .cnb-rule-setting-item,
8 | .cnb-rule-setting-item:first-child {
9 | padding: 0.35em 0;
10 | justify-content: center;
11 | border-top: 1px solid var(--background-modifier-border);
12 | }
13 |
14 | .cnb-rule-setting-item .setting-item-info {
15 | display: none;
16 | }
17 | .cnb-rule-setting-item .setting-item-control {
18 | flex: none;
19 | }
20 |
21 | .cnb-rule-settings-header-row {
22 | display: grid;
23 | grid-template-columns: 2fr 2fr 2fr 0.5fr 0.5fr 0.5fr;
24 | /* gap: 10px; */
25 | font-weight: bold;
26 | align-items: center;
27 | margin-bottom: 10px;
28 | }
29 |
30 | .cnb-rule-settings-row {
31 | display: grid;
32 | grid-template-columns: 2fr 2fr 2fr 0.5fr 0.5fr 0.5fr;
33 | /* gap: 10px; */
34 | align-items: center;
35 | /* margin-bottom: 5px; */
36 | }
37 |
38 | .cnb-rule-type-dropdown {
39 | width: 10em;
40 | }
41 | .cnb-rule-value-input {
42 | width: 10em;
43 | }
44 | .cnb-rule-setting-item-text-input {
45 | width: 6em;
46 | }
47 |
48 | /*
49 | button.cnd-rule-setting-item-up-button,
50 | button.cnd-rule-setting-item-down-button,
51 | button.cnd-rule-setting-item-delete-button
52 | {
53 | padding: 0;
54 | } */
55 |
56 | /* .cnb-rule-settings-column-rule-type {
57 | width: 10em;
58 | }
59 | .cnb-rule-settings-column-rule-value {
60 | width: 10em;
61 | }
62 | .cnb-rule-settings-column-rule-color {
63 | width: 6em;
64 | }
65 | .cnb-rule-settings-column-rule-button {
66 | width: 6em;
67 | } */
68 |
--------------------------------------------------------------------------------
/tests/__mocks__/obsidian.ts:
--------------------------------------------------------------------------------
1 | export { };
2 |
--------------------------------------------------------------------------------
/tests/global-setup.js:
--------------------------------------------------------------------------------
1 | module.exports = async () => {
2 | process.env.TZ = 'UTC';
3 | };
4 |
--------------------------------------------------------------------------------
/tests/main.test.ts:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * @jest-environment jsdom
4 | */
5 | import { describe, expect, test } from '@jest/globals';
6 | import { checkPath } from '../src/main';
7 |
8 |
9 | jest.mock('obsidian', () => {
10 | class MockPlugin {
11 | app: any;
12 | constructor(app: any) {
13 | this.app = app;
14 | }
15 | // Add any mock methods or properties as needed
16 | }
17 |
18 | return {
19 | Plugin: MockPlugin,
20 | PluginSettingTab: class {
21 | constructor(app: any, plugin: any) {
22 | // Mock properties and methods as needed
23 | }
24 | },
25 | // ... other Obsidian exports
26 | };
27 | });
28 |
29 |
30 | describe('utility functions', () => {
31 | it('ensure checkPath matches full folders', () => {
32 | expect(checkPath("Other/Two Rules.md", "Other")).toBe(true);
33 | });
34 | it('should not match filenames', () => {
35 | expect(checkPath("Obsidian/readme.md", "Obsidian")).toBe(true);
36 | // Issue #4 - https://github.com/rusi/obsidian-colorful-note-borders/issues/4
37 | expect(checkPath("Index/300-Obsidian-index.md", "Obsidian")).toBe(false);
38 | });
39 | it('should match Windows style paths', () => {
40 | expect(checkPath("Obsidian\\readme.md", "Obsidian")).toBe(true);
41 | });
42 | it('should match paths with spaces', () => {
43 | expect(checkPath("Inbox/Test Note/test note with spaces.md", "Test Note")).toBe(true);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | // "module": "CommonJS",
9 | // "target": "ES2019",
10 | // "strict": true,
11 | // "allowJs": true,
12 | // "skipLibCheck": true,
13 | // "forceConsistentCasingInFileNames": true,
14 | "esModuleInterop": true,
15 | "noImplicitAny": true,
16 | "moduleResolution": "node",
17 | "importHelpers": true,
18 | "isolatedModules": true,
19 | "strictNullChecks": true,
20 | // "outDir": "./",
21 | "lib": [
22 | "DOM",
23 | "ES5",
24 | "ES6",
25 | "ES7"
26 | ]
27 | },
28 | "include": [
29 | "**/*.ts"
30 | ],
31 | "exclude": ["node_modules"]
32 | }
33 |
--------------------------------------------------------------------------------
/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 | "0.1.1": "0.15.0",
3 | "0.2.0": "0.15.0",
4 | "0.2.1": "0.15.0",
5 | "0.2.2": "0.15.0",
6 | "0.2.3": "0.15.0",
7 | "0.2.4": "0.15.0"
8 | }
--------------------------------------------------------------------------------