@real1ty-obsidian-plugins/utils 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/core/evaluator/base.d.ts +22 -0
  2. package/dist/core/evaluator/base.d.ts.map +1 -0
  3. package/dist/core/evaluator/base.js +52 -0
  4. package/dist/core/evaluator/base.js.map +1 -0
  5. package/dist/core/evaluator/color.d.ts +19 -0
  6. package/dist/core/evaluator/color.d.ts.map +1 -0
  7. package/dist/core/evaluator/color.js +25 -0
  8. package/dist/core/evaluator/color.js.map +1 -0
  9. package/dist/core/evaluator/excluded.d.ts +32 -0
  10. package/dist/core/evaluator/excluded.d.ts.map +1 -0
  11. package/dist/core/evaluator/excluded.js +41 -0
  12. package/dist/core/evaluator/excluded.js.map +1 -0
  13. package/dist/core/evaluator/filter.d.ts +15 -0
  14. package/dist/core/evaluator/filter.d.ts.map +1 -0
  15. package/dist/core/evaluator/filter.js +27 -0
  16. package/dist/core/evaluator/filter.js.map +1 -0
  17. package/dist/core/evaluator/included.d.ts +36 -0
  18. package/dist/core/evaluator/included.d.ts.map +1 -0
  19. package/dist/core/evaluator/included.js +51 -0
  20. package/dist/core/evaluator/included.js.map +1 -0
  21. package/dist/core/evaluator/index.d.ts +6 -0
  22. package/dist/core/evaluator/index.d.ts.map +1 -0
  23. package/dist/core/evaluator/index.js +6 -0
  24. package/dist/core/evaluator/index.js.map +1 -0
  25. package/dist/core/expression-utils.d.ts +17 -0
  26. package/dist/core/expression-utils.d.ts.map +1 -0
  27. package/dist/core/expression-utils.js +40 -0
  28. package/dist/core/expression-utils.js.map +1 -0
  29. package/dist/core/frontmatter-value.d.ts +143 -0
  30. package/dist/core/frontmatter-value.d.ts.map +1 -0
  31. package/dist/core/frontmatter-value.js +408 -0
  32. package/dist/core/frontmatter-value.js.map +1 -0
  33. package/dist/core/index.d.ts +3 -1
  34. package/dist/core/index.d.ts.map +1 -1
  35. package/dist/core/index.js +3 -1
  36. package/dist/core/index.js.map +1 -1
  37. package/dist/file/index.d.ts +1 -0
  38. package/dist/file/index.d.ts.map +1 -1
  39. package/dist/file/index.js +1 -0
  40. package/dist/file/index.js.map +1 -1
  41. package/dist/file/link-parser.d.ts +26 -0
  42. package/dist/file/link-parser.d.ts.map +1 -1
  43. package/dist/file/link-parser.js +59 -0
  44. package/dist/file/link-parser.js.map +1 -1
  45. package/dist/file/property-utils.d.ts +55 -0
  46. package/dist/file/property-utils.d.ts.map +1 -0
  47. package/dist/file/property-utils.js +90 -0
  48. package/dist/file/property-utils.js.map +1 -0
  49. package/package.json +2 -1
  50. package/src/core/evaluator/base.ts +71 -0
  51. package/src/core/evaluator/color.ts +37 -0
  52. package/src/core/evaluator/excluded.ts +63 -0
  53. package/src/core/evaluator/filter.ts +33 -0
  54. package/src/core/evaluator/included.ts +74 -0
  55. package/src/core/evaluator/index.ts +5 -0
  56. package/src/core/expression-utils.ts +53 -0
  57. package/src/core/frontmatter-value.ts +528 -0
  58. package/src/core/index.ts +3 -1
  59. package/src/file/index.ts +1 -0
  60. package/src/file/link-parser.ts +73 -0
  61. package/src/file/property-utils.ts +114 -0
  62. package/dist/core/evaluator-base.d.ts +0 -52
  63. package/dist/core/evaluator-base.d.ts.map +0 -1
  64. package/dist/core/evaluator-base.js +0 -84
  65. package/dist/core/evaluator-base.js.map +0 -1
  66. package/src/core/evaluator-base.ts +0 -118
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Adds a link to a property, avoiding duplicates using normalized path comparison.
3
+ * Prevents cycles and duplicate relationships by comparing normalized paths.
4
+ *
5
+ * **Important**: linkPath should be WITHOUT .md extension (wikilink format).
6
+ *
7
+ * @param currentValue - The current property value (can be string, string[], or undefined)
8
+ * @param linkPath - The file path to add (without .md extension, e.g., "folder/file")
9
+ * @returns New array with link added, or same array if link already exists
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * addLinkToProperty(undefined, "MyNote") // ["[[MyNote]]"]
14
+ * addLinkToProperty("[[Note1]]", "Note2") // ["[[Note1]]", "[[Note2]]"]
15
+ * addLinkToProperty(["[[Note1]]"], "Note2") // ["[[Note1]]", "[[Note2]]"]
16
+ * addLinkToProperty(["[[Note1]]"], "Note1") // ["[[Note1]]"] (no change - duplicate prevented)
17
+ * addLinkToProperty(["[[Folder/Note]]"], "folder/note") // ["[[Folder/Note]]", "[[folder/note|note]]"] (case-sensitive, different entry)
18
+ * ```
19
+ */
20
+ export declare function addLinkToProperty(currentValue: string | string[] | undefined, linkPath: string): string[];
21
+ /**
22
+ * Removes a link from a property using normalized path comparison.
23
+ *
24
+ * @param currentValue - The current property value (can be string, string[], or undefined)
25
+ * @param linkPath - The file path to remove (without .md extension)
26
+ * @returns New array with link removed (can be empty)
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * removeLinkFromProperty(["[[Note1]]", "[[Note2]]"], "Note1") // ["[[Note2]]"]
31
+ * removeLinkFromProperty(["[[Note1]]"], "Note1") // []
32
+ * removeLinkFromProperty("[[Note1]]", "Note1") // []
33
+ * removeLinkFromProperty(undefined, "Note1") // []
34
+ * removeLinkFromProperty(["[[Folder/Note]]"], "Folder/Note") // [] (case-sensitive removal)
35
+ * ```
36
+ */
37
+ export declare function removeLinkFromProperty(currentValue: string | string[] | undefined, linkPath: string): string[];
38
+ /**
39
+ * Checks if a link exists in a property using normalized path comparison.
40
+ *
41
+ * @param currentValue - The current property value (can be string, string[], or undefined)
42
+ * @param linkPath - The file path to check (without .md extension)
43
+ * @returns True if the link exists
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * hasLinkInProperty(["[[Note1]]", "[[Note2]]"], "Note1") // true
48
+ * hasLinkInProperty("[[Note1]]", "Note1") // true
49
+ * hasLinkInProperty([], "Note1") // false
50
+ * hasLinkInProperty(undefined, "Note1") // false
51
+ * hasLinkInProperty(["[[Folder/Note]]"], "Folder/Note") // true (case-sensitive match)
52
+ * ```
53
+ */
54
+ export declare function hasLinkInProperty(currentValue: string | string[] | undefined, linkPath: string): boolean;
55
+ //# sourceMappingURL=property-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-utils.d.ts","sourceRoot":"","sources":["../../src/file/property-utils.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,GACd,MAAM,EAAE,CAsBV;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CACrC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,GACd,MAAM,EAAE,CAiBV;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAChC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,EAC3C,QAAQ,EAAE,MAAM,GACd,OAAO,CAMT"}
@@ -0,0 +1,90 @@
1
+ import { normalizePath } from "obsidian";
2
+ import { formatWikiLink, parsePropertyLinks } from "./link-parser";
3
+ /**
4
+ * Adds a link to a property, avoiding duplicates using normalized path comparison.
5
+ * Prevents cycles and duplicate relationships by comparing normalized paths.
6
+ *
7
+ * **Important**: linkPath should be WITHOUT .md extension (wikilink format).
8
+ *
9
+ * @param currentValue - The current property value (can be string, string[], or undefined)
10
+ * @param linkPath - The file path to add (without .md extension, e.g., "folder/file")
11
+ * @returns New array with link added, or same array if link already exists
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * addLinkToProperty(undefined, "MyNote") // ["[[MyNote]]"]
16
+ * addLinkToProperty("[[Note1]]", "Note2") // ["[[Note1]]", "[[Note2]]"]
17
+ * addLinkToProperty(["[[Note1]]"], "Note2") // ["[[Note1]]", "[[Note2]]"]
18
+ * addLinkToProperty(["[[Note1]]"], "Note1") // ["[[Note1]]"] (no change - duplicate prevented)
19
+ * addLinkToProperty(["[[Folder/Note]]"], "folder/note") // ["[[Folder/Note]]", "[[folder/note|note]]"] (case-sensitive, different entry)
20
+ * ```
21
+ */
22
+ export function addLinkToProperty(currentValue, linkPath) {
23
+ // Handle undefined or null
24
+ if (currentValue === undefined || currentValue === null) {
25
+ return [formatWikiLink(linkPath)];
26
+ }
27
+ // Normalize to array
28
+ const currentArray = Array.isArray(currentValue) ? currentValue : [currentValue];
29
+ const existingPaths = parsePropertyLinks(currentArray);
30
+ // Normalize paths for comparison to prevent duplicates with different casing or separators
31
+ const normalizedLinkPath = normalizePath(linkPath);
32
+ const normalizedExistingPaths = existingPaths.map((p) => normalizePath(p));
33
+ // Only add if not already present (using normalized path comparison)
34
+ if (!normalizedExistingPaths.includes(normalizedLinkPath)) {
35
+ return [...currentArray, formatWikiLink(linkPath)];
36
+ }
37
+ return currentArray;
38
+ }
39
+ /**
40
+ * Removes a link from a property using normalized path comparison.
41
+ *
42
+ * @param currentValue - The current property value (can be string, string[], or undefined)
43
+ * @param linkPath - The file path to remove (without .md extension)
44
+ * @returns New array with link removed (can be empty)
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * removeLinkFromProperty(["[[Note1]]", "[[Note2]]"], "Note1") // ["[[Note2]]"]
49
+ * removeLinkFromProperty(["[[Note1]]"], "Note1") // []
50
+ * removeLinkFromProperty("[[Note1]]", "Note1") // []
51
+ * removeLinkFromProperty(undefined, "Note1") // []
52
+ * removeLinkFromProperty(["[[Folder/Note]]"], "Folder/Note") // [] (case-sensitive removal)
53
+ * ```
54
+ */
55
+ export function removeLinkFromProperty(currentValue, linkPath) {
56
+ if (currentValue === undefined || currentValue === null) {
57
+ return [];
58
+ }
59
+ // Normalize to array
60
+ const currentArray = Array.isArray(currentValue) ? currentValue : [currentValue];
61
+ const normalizedLinkPath = normalizePath(linkPath);
62
+ return currentArray.filter((item) => {
63
+ const parsed = parsePropertyLinks([item])[0];
64
+ if (!parsed)
65
+ return true; // Keep invalid entries
66
+ return normalizePath(parsed) !== normalizedLinkPath;
67
+ });
68
+ }
69
+ /**
70
+ * Checks if a link exists in a property using normalized path comparison.
71
+ *
72
+ * @param currentValue - The current property value (can be string, string[], or undefined)
73
+ * @param linkPath - The file path to check (without .md extension)
74
+ * @returns True if the link exists
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * hasLinkInProperty(["[[Note1]]", "[[Note2]]"], "Note1") // true
79
+ * hasLinkInProperty("[[Note1]]", "Note1") // true
80
+ * hasLinkInProperty([], "Note1") // false
81
+ * hasLinkInProperty(undefined, "Note1") // false
82
+ * hasLinkInProperty(["[[Folder/Note]]"], "Folder/Note") // true (case-sensitive match)
83
+ * ```
84
+ */
85
+ export function hasLinkInProperty(currentValue, linkPath) {
86
+ const existingPaths = parsePropertyLinks(currentValue);
87
+ const normalizedLinkPath = normalizePath(linkPath);
88
+ return existingPaths.some((path) => normalizePath(path) === normalizedLinkPath);
89
+ }
90
+ //# sourceMappingURL=property-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-utils.js","sourceRoot":"","sources":["../../src/file/property-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,iBAAiB,CAChC,YAA2C,EAC3C,QAAgB;IAEhB,2BAA2B;IAC3B,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QACzD,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnC,CAAC;IAED,qBAAqB;IACrB,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IAEjF,MAAM,aAAa,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAEvD,2FAA2F;IAC3F,MAAM,kBAAkB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEnD,MAAM,uBAAuB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAE3E,qEAAqE;IACrE,IAAI,CAAC,uBAAuB,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;QAC3D,OAAO,CAAC,GAAG,YAAY,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,YAAY,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CACrC,YAA2C,EAC3C,QAAgB;IAEhB,IAAI,YAAY,KAAK,SAAS,IAAI,YAAY,KAAK,IAAI,EAAE,CAAC;QACzD,OAAO,EAAE,CAAC;IACX,CAAC;IAED,qBAAqB;IACrB,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;IAEjF,MAAM,kBAAkB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEnD,OAAO,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,kBAAkB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAE7C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC,CAAC,uBAAuB;QAEjD,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,kBAAkB,CAAC;IACrD,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,iBAAiB,CAChC,YAA2C,EAC3C,QAAgB;IAEhB,MAAM,aAAa,GAAG,kBAAkB,CAAC,YAAY,CAAC,CAAC;IAEvD,MAAM,kBAAkB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEnD,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,KAAK,kBAAkB,CAAC,CAAC;AACjF,CAAC","sourcesContent":["import { normalizePath } from \"obsidian\";\n\nimport { formatWikiLink, parsePropertyLinks } from \"./link-parser\";\n\n/**\n * Adds a link to a property, avoiding duplicates using normalized path comparison.\n * Prevents cycles and duplicate relationships by comparing normalized paths.\n *\n * **Important**: linkPath should be WITHOUT .md extension (wikilink format).\n *\n * @param currentValue - The current property value (can be string, string[], or undefined)\n * @param linkPath - The file path to add (without .md extension, e.g., \"folder/file\")\n * @returns New array with link added, or same array if link already exists\n *\n * @example\n * ```ts\n * addLinkToProperty(undefined, \"MyNote\") // [\"[[MyNote]]\"]\n * addLinkToProperty(\"[[Note1]]\", \"Note2\") // [\"[[Note1]]\", \"[[Note2]]\"]\n * addLinkToProperty([\"[[Note1]]\"], \"Note2\") // [\"[[Note1]]\", \"[[Note2]]\"]\n * addLinkToProperty([\"[[Note1]]\"], \"Note1\") // [\"[[Note1]]\"] (no change - duplicate prevented)\n * addLinkToProperty([\"[[Folder/Note]]\"], \"folder/note\") // [\"[[Folder/Note]]\", \"[[folder/note|note]]\"] (case-sensitive, different entry)\n * ```\n */\nexport function addLinkToProperty(\n\tcurrentValue: string | string[] | undefined,\n\tlinkPath: string\n): string[] {\n\t// Handle undefined or null\n\tif (currentValue === undefined || currentValue === null) {\n\t\treturn [formatWikiLink(linkPath)];\n\t}\n\n\t// Normalize to array\n\tconst currentArray = Array.isArray(currentValue) ? currentValue : [currentValue];\n\n\tconst existingPaths = parsePropertyLinks(currentArray);\n\n\t// Normalize paths for comparison to prevent duplicates with different casing or separators\n\tconst normalizedLinkPath = normalizePath(linkPath);\n\n\tconst normalizedExistingPaths = existingPaths.map((p) => normalizePath(p));\n\n\t// Only add if not already present (using normalized path comparison)\n\tif (!normalizedExistingPaths.includes(normalizedLinkPath)) {\n\t\treturn [...currentArray, formatWikiLink(linkPath)];\n\t}\n\n\treturn currentArray;\n}\n\n/**\n * Removes a link from a property using normalized path comparison.\n *\n * @param currentValue - The current property value (can be string, string[], or undefined)\n * @param linkPath - The file path to remove (without .md extension)\n * @returns New array with link removed (can be empty)\n *\n * @example\n * ```ts\n * removeLinkFromProperty([\"[[Note1]]\", \"[[Note2]]\"], \"Note1\") // [\"[[Note2]]\"]\n * removeLinkFromProperty([\"[[Note1]]\"], \"Note1\") // []\n * removeLinkFromProperty(\"[[Note1]]\", \"Note1\") // []\n * removeLinkFromProperty(undefined, \"Note1\") // []\n * removeLinkFromProperty([\"[[Folder/Note]]\"], \"Folder/Note\") // [] (case-sensitive removal)\n * ```\n */\nexport function removeLinkFromProperty(\n\tcurrentValue: string | string[] | undefined,\n\tlinkPath: string\n): string[] {\n\tif (currentValue === undefined || currentValue === null) {\n\t\treturn [];\n\t}\n\n\t// Normalize to array\n\tconst currentArray = Array.isArray(currentValue) ? currentValue : [currentValue];\n\n\tconst normalizedLinkPath = normalizePath(linkPath);\n\n\treturn currentArray.filter((item) => {\n\t\tconst parsed = parsePropertyLinks([item])[0];\n\n\t\tif (!parsed) return true; // Keep invalid entries\n\n\t\treturn normalizePath(parsed) !== normalizedLinkPath;\n\t});\n}\n\n/**\n * Checks if a link exists in a property using normalized path comparison.\n *\n * @param currentValue - The current property value (can be string, string[], or undefined)\n * @param linkPath - The file path to check (without .md extension)\n * @returns True if the link exists\n *\n * @example\n * ```ts\n * hasLinkInProperty([\"[[Note1]]\", \"[[Note2]]\"], \"Note1\") // true\n * hasLinkInProperty(\"[[Note1]]\", \"Note1\") // true\n * hasLinkInProperty([], \"Note1\") // false\n * hasLinkInProperty(undefined, \"Note1\") // false\n * hasLinkInProperty([\"[[Folder/Note]]\"], \"Folder/Note\") // true (case-sensitive match)\n * ```\n */\nexport function hasLinkInProperty(\n\tcurrentValue: string | string[] | undefined,\n\tlinkPath: string\n): boolean {\n\tconst existingPaths = parsePropertyLinks(currentValue);\n\n\tconst normalizedLinkPath = normalizePath(linkPath);\n\n\treturn existingPaths.some((path) => normalizePath(path) === normalizedLinkPath);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real1ty-obsidian-plugins/utils",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Shared utilities for Obsidian plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -44,6 +44,7 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/luxon": "^3.7.1",
47
+ "fast-check": "^4.3.0",
47
48
  "typescript": "5.9.2",
48
49
  "vitest": "^2.0.5"
49
50
  },
@@ -0,0 +1,71 @@
1
+ import type { BehaviorSubject, Subscription } from "rxjs";
2
+
3
+ import { buildPropertyMapping, sanitizeExpression } from "../expression-utils";
4
+
5
+ export interface BaseRule {
6
+ id: string;
7
+ expression: string;
8
+ enabled: boolean;
9
+ }
10
+
11
+ /**
12
+ * Generic base class for evaluating JavaScript expressions against frontmatter objects.
13
+ * Provides reactive compilation of rules via RxJS subscription and safe evaluation.
14
+ */
15
+ export abstract class BaseEvaluator<TRule extends BaseRule, TSettings> {
16
+ protected rules: TRule[] = [];
17
+ private compiledFunctions = new Map<string, ((...args: any[]) => boolean) | null>();
18
+ private propertyMapping = new Map<string, string>();
19
+ private subscription: Subscription | null = null;
20
+
21
+ constructor(settingsStore: BehaviorSubject<TSettings>) {
22
+ this.subscription = settingsStore.subscribe((settings) => {
23
+ this.rules = this.extractRules(settings);
24
+ this.compiledFunctions.clear();
25
+ this.propertyMapping.clear();
26
+ });
27
+ }
28
+
29
+ protected abstract extractRules(settings: TSettings): TRule[];
30
+
31
+ destroy(): void {
32
+ this.subscription?.unsubscribe();
33
+ this.compiledFunctions.clear();
34
+ this.propertyMapping.clear();
35
+ }
36
+
37
+ protected evaluateRule(rule: TRule, frontmatter: Record<string, unknown>): boolean {
38
+ if (!rule.enabled || !rule.expression) {
39
+ return false;
40
+ }
41
+
42
+ try {
43
+ if (this.propertyMapping.size === 0) {
44
+ this.propertyMapping = buildPropertyMapping(Object.keys(frontmatter));
45
+ }
46
+
47
+ let compiledFunc = this.compiledFunctions.get(rule.id);
48
+
49
+ if (!compiledFunc) {
50
+ const sanitized = sanitizeExpression(rule.expression, this.propertyMapping);
51
+ const params = Array.from(this.propertyMapping.values());
52
+ compiledFunc = new Function(...params, `"use strict"; return ${sanitized};`) as (
53
+ ...args: any[]
54
+ ) => boolean;
55
+ this.compiledFunctions.set(rule.id, compiledFunc);
56
+ }
57
+
58
+ const values = Array.from(this.propertyMapping.keys()).map((key) => frontmatter[key]);
59
+ const result = compiledFunc(...values);
60
+
61
+ return result === true;
62
+ } catch (error) {
63
+ console.warn(`Invalid expression (${rule.id}):`, rule.expression, error);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ protected isTruthy(value: any): boolean {
69
+ return value === true;
70
+ }
71
+ }
@@ -0,0 +1,37 @@
1
+ import type { BehaviorSubject } from "rxjs";
2
+
3
+ import { BaseEvaluator, type BaseRule } from "./base";
4
+
5
+ export interface ColorRule extends BaseRule {
6
+ color: string;
7
+ }
8
+
9
+ /**
10
+ * Generic evaluator for determining colors based on frontmatter rules.
11
+ * Extends BaseEvaluator to evaluate color rules against frontmatter.
12
+ */
13
+ export class ColorEvaluator<
14
+ TSettings extends { defaultNodeColor: string; colorRules: ColorRule[] },
15
+ > extends BaseEvaluator<ColorRule, TSettings> {
16
+ private defaultColor: string;
17
+
18
+ constructor(settingsStore: BehaviorSubject<TSettings>) {
19
+ super(settingsStore);
20
+ this.defaultColor = settingsStore.value.defaultNodeColor;
21
+
22
+ settingsStore.subscribe((settings) => {
23
+ if (settings.defaultNodeColor) {
24
+ this.defaultColor = settings.defaultNodeColor;
25
+ }
26
+ });
27
+ }
28
+
29
+ protected extractRules(settings: TSettings): ColorRule[] {
30
+ return settings.colorRules;
31
+ }
32
+
33
+ evaluateColor(frontmatter: Record<string, unknown>): string {
34
+ const match = this.rules.find((rule) => this.isTruthy(this.evaluateRule(rule, frontmatter)));
35
+ return match?.color ?? this.defaultColor;
36
+ }
37
+ }
@@ -0,0 +1,63 @@
1
+ import type { BehaviorSubject } from "rxjs";
2
+
3
+ export interface PathExcludedProperties {
4
+ id: string;
5
+ path: string;
6
+ excludedProperties: string[];
7
+ enabled: boolean;
8
+ }
9
+
10
+ /**
11
+ * Generic evaluator for determining which frontmatter properties to exclude when creating new nodes.
12
+ *
13
+ * Logic:
14
+ * 1. ALWAYS includes the default excluded properties (e.g., Parent, Child, Related, _ZettelID)
15
+ * 2. Checks if the source file's path matches any path-based exclusion rules
16
+ * 3. First matching path rule's properties are ADDED to the default exclusion list
17
+ * 4. Returns the combined set of excluded properties
18
+ */
19
+ export class ExcludedPropertiesEvaluator<
20
+ TSettings extends {
21
+ defaultExcludedProperties: string[];
22
+ pathExcludedProperties: PathExcludedProperties[];
23
+ },
24
+ > {
25
+ private defaultExcludedProperties: string[];
26
+
27
+ private pathRules: PathExcludedProperties[];
28
+
29
+ constructor(settingsObservable: BehaviorSubject<TSettings>) {
30
+ const assignSettings = (settings: TSettings) => {
31
+ this.defaultExcludedProperties = settings.defaultExcludedProperties;
32
+ this.pathRules = settings.pathExcludedProperties.filter((rule) => rule.enabled);
33
+ };
34
+
35
+ assignSettings(settingsObservable.value);
36
+ settingsObservable.subscribe(assignSettings);
37
+ }
38
+
39
+ /**
40
+ * Evaluate which properties should be excluded for a given file path.
41
+ *
42
+ * @param filePath - The file path to match against path rules
43
+ * @returns Array of property names to exclude (always includes defaults + path rule matches)
44
+ */
45
+ evaluateExcludedProperties(filePath: string): string[] {
46
+ // Always start with default excluded properties
47
+ const excludedProperties = [...this.defaultExcludedProperties];
48
+
49
+ // Find first matching path rule and add its excluded properties
50
+ const match = this.pathRules.find((rule) => filePath.startsWith(rule.path));
51
+
52
+ if (match) {
53
+ // Add path-specific excluded properties to the defaults
54
+ for (const prop of match.excludedProperties) {
55
+ if (!excludedProperties.includes(prop)) {
56
+ excludedProperties.push(prop);
57
+ }
58
+ }
59
+ }
60
+
61
+ return excludedProperties;
62
+ }
63
+ }
@@ -0,0 +1,33 @@
1
+ import { BaseEvaluator, type BaseRule } from "./base";
2
+
3
+ export interface FilterRule extends BaseRule {}
4
+
5
+ /**
6
+ * Generic evaluator for filtering based on frontmatter expressions.
7
+ * Extends BaseEvaluator to evaluate filter rules against frontmatter.
8
+ * Returns true only if ALL rules evaluate to true.
9
+ */
10
+ export class FilterEvaluator<
11
+ TSettings extends { filterExpressions: string[] },
12
+ > extends BaseEvaluator<FilterRule, TSettings> {
13
+ protected extractRules(settings: TSettings): FilterRule[] {
14
+ return settings.filterExpressions.map((expression, index) => ({
15
+ id: `filter-${index}`,
16
+ expression: expression.trim(),
17
+ enabled: true,
18
+ }));
19
+ }
20
+
21
+ evaluateFilters(frontmatter: Record<string, unknown>): boolean {
22
+ if (this.rules.length === 0) {
23
+ return true;
24
+ }
25
+
26
+ return this.rules.every((rule) => {
27
+ if (!rule.enabled || !rule.expression) {
28
+ return true;
29
+ }
30
+ return this.evaluateRule(rule, frontmatter);
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,74 @@
1
+ import type { BehaviorSubject } from "rxjs";
2
+
3
+ export interface PathIncludedProperties {
4
+ id: string;
5
+ path: string;
6
+ includedProperties: string[];
7
+ enabled: boolean;
8
+ }
9
+
10
+ /**
11
+ * Generic evaluator for determining which properties to include in Bases view columns.
12
+ *
13
+ * Logic:
14
+ * 1. ALWAYS includes the default included properties
15
+ * 2. Checks if the file's path matches any path-based inclusion rules
16
+ * 3. First matching path rule's properties are ADDED to the default inclusion list
17
+ * 4. Returns the combined set of included properties in order:
18
+ * - file.name (always first)
19
+ * - default included properties (in specified order)
20
+ * - path-specific included properties (in specified order)
21
+ */
22
+ export class IncludedPropertiesEvaluator<
23
+ TSettings extends {
24
+ defaultBasesIncludedProperties: string[];
25
+ pathBasesIncludedProperties: PathIncludedProperties[];
26
+ },
27
+ > {
28
+ private defaultIncludedProperties: string[];
29
+
30
+ private pathRules: PathIncludedProperties[];
31
+
32
+ constructor(settingsObservable: BehaviorSubject<TSettings>) {
33
+ const assignSettings = (settings: TSettings) => {
34
+ this.defaultIncludedProperties = settings.defaultBasesIncludedProperties;
35
+ this.pathRules = settings.pathBasesIncludedProperties.filter((rule) => rule.enabled);
36
+ };
37
+
38
+ assignSettings(settingsObservable.value);
39
+ settingsObservable.subscribe(assignSettings);
40
+ }
41
+
42
+ /**
43
+ * Evaluate which properties should be included in the order array for a given file path.
44
+ * Returns an array with "file.name" as the first element, followed by default and path-specific properties.
45
+ *
46
+ * @param filePath - The file path to match against path rules
47
+ * @returns Array of property names to include in the order (file.name + defaults + path rule matches)
48
+ */
49
+ evaluateIncludedProperties(filePath: string): string[] {
50
+ // Always start with file.name
51
+ const includedProperties = ["file.name"];
52
+
53
+ // Add default included properties
54
+ for (const prop of this.defaultIncludedProperties) {
55
+ if (!includedProperties.includes(prop)) {
56
+ includedProperties.push(prop);
57
+ }
58
+ }
59
+
60
+ // Find first matching path rule and add its included properties
61
+ const match = this.pathRules.find((rule) => filePath.startsWith(rule.path));
62
+
63
+ if (match) {
64
+ // Add path-specific included properties to the defaults
65
+ for (const prop of match.includedProperties) {
66
+ if (!includedProperties.includes(prop)) {
67
+ includedProperties.push(prop);
68
+ }
69
+ }
70
+ }
71
+
72
+ return includedProperties;
73
+ }
74
+ }
@@ -0,0 +1,5 @@
1
+ export * from "./base";
2
+ export * from "./color";
3
+ export * from "./excluded";
4
+ export * from "./filter";
5
+ export * from "./included";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Sanitizes a property name for use as a JavaScript function parameter
3
+ * by replacing spaces and special characters with underscores.
4
+ * Adds a prefix to avoid conflicts with JavaScript reserved words.
5
+ */
6
+ export function sanitizePropertyName(name: string): string {
7
+ const sanitized = name.replace(/[^a-zA-Z0-9_]/g, "_");
8
+ return `prop_${sanitized}`;
9
+ }
10
+
11
+ /**
12
+ * Builds a mapping of original property names to sanitized versions
13
+ * suitable for use as JavaScript function parameters.
14
+ */
15
+ export function buildPropertyMapping(properties: string[]): Map<string, string> {
16
+ const mapping = new Map<string, string>();
17
+
18
+ for (const prop of properties) {
19
+ mapping.set(prop, sanitizePropertyName(prop));
20
+ }
21
+
22
+ return mapping;
23
+ }
24
+
25
+ /**
26
+ * Replaces property names in an expression with their sanitized versions.
27
+ * Sorts by length descending to replace longer property names first and avoid partial matches.
28
+ */
29
+ export function sanitizeExpression(
30
+ expression: string,
31
+ propertyMapping: Map<string, string>
32
+ ): string {
33
+ let sanitized = expression;
34
+
35
+ // Sort by length descending to replace longer property names first
36
+ const sortedEntries = Array.from(propertyMapping.entries()).sort(
37
+ ([a], [b]) => b.length - a.length
38
+ );
39
+
40
+ for (const [original, sanitizedName] of sortedEntries) {
41
+ if (original !== sanitizedName) {
42
+ const escaped = original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
43
+
44
+ // Use a regex that matches the property name not preceded or followed by word characters
45
+ // This allows matching properties with special characters like "My-Property"
46
+ const regex = new RegExp(`(?<!\\w)${escaped}(?!\\w)`, "g");
47
+
48
+ sanitized = sanitized.replace(regex, sanitizedName);
49
+ }
50
+ }
51
+
52
+ return sanitized;
53
+ }