@real1ty-obsidian-plugins/utils 2.3.0 → 2.5.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.
- package/dist/core/evaluator/base.d.ts +22 -0
- package/dist/core/evaluator/base.d.ts.map +1 -0
- package/dist/core/evaluator/base.js +52 -0
- package/dist/core/evaluator/base.js.map +1 -0
- package/dist/core/evaluator/color.d.ts +19 -0
- package/dist/core/evaluator/color.d.ts.map +1 -0
- package/dist/core/evaluator/color.js +25 -0
- package/dist/core/evaluator/color.js.map +1 -0
- package/dist/core/evaluator/excluded.d.ts +32 -0
- package/dist/core/evaluator/excluded.d.ts.map +1 -0
- package/dist/core/evaluator/excluded.js +41 -0
- package/dist/core/evaluator/excluded.js.map +1 -0
- package/dist/core/evaluator/filter.d.ts +15 -0
- package/dist/core/evaluator/filter.d.ts.map +1 -0
- package/dist/core/evaluator/filter.js +27 -0
- package/dist/core/evaluator/filter.js.map +1 -0
- package/dist/core/evaluator/included.d.ts +36 -0
- package/dist/core/evaluator/included.d.ts.map +1 -0
- package/dist/core/evaluator/included.js +51 -0
- package/dist/core/evaluator/included.js.map +1 -0
- package/dist/core/evaluator/index.d.ts +6 -0
- package/dist/core/evaluator/index.d.ts.map +1 -0
- package/dist/core/evaluator/index.js +6 -0
- package/dist/core/evaluator/index.js.map +1 -0
- package/dist/core/expression-utils.d.ts +17 -0
- package/dist/core/expression-utils.d.ts.map +1 -0
- package/dist/core/expression-utils.js +40 -0
- package/dist/core/expression-utils.js.map +1 -0
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/package.json +3 -5
- package/src/async/async.ts +117 -0
- package/src/async/batch-operations.ts +53 -0
- package/src/async/index.ts +2 -0
- package/src/core/evaluator/base.ts +71 -0
- package/src/core/evaluator/color.ts +37 -0
- package/src/core/evaluator/excluded.ts +63 -0
- package/src/core/evaluator/filter.ts +35 -0
- package/src/core/evaluator/included.ts +74 -0
- package/src/core/evaluator/index.ts +5 -0
- package/src/core/expression-utils.ts +53 -0
- package/src/core/generate.ts +22 -0
- package/src/core/index.ts +3 -0
- package/src/date/date-recurrence.ts +244 -0
- package/src/date/date.ts +111 -0
- package/src/date/index.ts +2 -0
- package/src/file/child-reference.ts +76 -0
- package/src/file/file-operations.ts +197 -0
- package/src/file/file.ts +570 -0
- package/src/file/frontmatter.ts +80 -0
- package/src/file/index.ts +6 -0
- package/src/file/link-parser.ts +18 -0
- package/src/file/templater.ts +75 -0
- package/src/index.ts +14 -0
- package/src/settings/index.ts +2 -0
- package/src/settings/settings-store.ts +88 -0
- package/src/settings/settings-ui-builder.ts +507 -0
- package/src/string/index.ts +1 -0
- package/src/string/string.ts +26 -0
- package/src/testing/index.ts +23 -0
- package/src/testing/mocks/obsidian.ts +331 -0
- package/src/testing/mocks/utils.ts +113 -0
- package/src/testing/setup.ts +19 -0
- package/dist/core/evaluator-base.d.ts +0 -52
- package/dist/core/evaluator-base.d.ts.map +0 -1
- package/dist/core/evaluator-base.js +0 -84
- package/dist/core/evaluator-base.js.map +0 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const capitalize = (str: string): string => {
|
|
2
|
+
if (!str) return str;
|
|
3
|
+
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const generateDuplicatedTitle = (originalTitle: string): string => {
|
|
7
|
+
// Check if title already has a counter pattern like " (2)", " - Copy", etc.
|
|
8
|
+
const counterMatch = originalTitle.match(/^(.*?)(?:\s*\((\d+)\)|\s*-?\s*Copy(?:\s*(\d+))?)$/);
|
|
9
|
+
|
|
10
|
+
if (counterMatch) {
|
|
11
|
+
const baseName = counterMatch[1];
|
|
12
|
+
const existingCounter = counterMatch[2] || counterMatch[3];
|
|
13
|
+
const nextCounter = existingCounter ? parseInt(existingCounter, 10) + 1 : 2;
|
|
14
|
+
return `${baseName} (${nextCounter})`;
|
|
15
|
+
} else {
|
|
16
|
+
return `${originalTitle} (2)`;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const pluralize = (count: number): string => {
|
|
21
|
+
return count === 1 ? "" : "s";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const getWeekDirection = (weeks: number): "next" | "previous" => {
|
|
25
|
+
return weeks > 0 ? "next" : "previous";
|
|
26
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from "./mocks/obsidian";
|
|
2
|
+
// Re-export commonly used combinations
|
|
3
|
+
export {
|
|
4
|
+
createMockApp,
|
|
5
|
+
createMockFile,
|
|
6
|
+
createMockFileCache,
|
|
7
|
+
Modal,
|
|
8
|
+
Notice,
|
|
9
|
+
Plugin,
|
|
10
|
+
PluginSettingTab,
|
|
11
|
+
Setting,
|
|
12
|
+
TFile,
|
|
13
|
+
} from "./mocks/obsidian";
|
|
14
|
+
export * from "./mocks/utils";
|
|
15
|
+
export {
|
|
16
|
+
mockFileOperations,
|
|
17
|
+
mockLinkParser,
|
|
18
|
+
resetAllMocks,
|
|
19
|
+
setupDefaultMockImplementations,
|
|
20
|
+
setupMockImplementation,
|
|
21
|
+
setupMockReturnValue,
|
|
22
|
+
verifyMockCalls,
|
|
23
|
+
} from "./mocks/utils";
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Base Plugin class mock
|
|
4
|
+
export class Plugin {
|
|
5
|
+
app: any;
|
|
6
|
+
manifest: any;
|
|
7
|
+
settings: any;
|
|
8
|
+
|
|
9
|
+
constructor(app: any, manifest: any) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.manifest = manifest;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Core plugin methods
|
|
15
|
+
addSettingTab = vi.fn();
|
|
16
|
+
registerEvent = vi.fn();
|
|
17
|
+
loadData = vi.fn().mockResolvedValue({});
|
|
18
|
+
saveData = vi.fn().mockResolvedValue(undefined);
|
|
19
|
+
onload = vi.fn();
|
|
20
|
+
onunload = vi.fn();
|
|
21
|
+
|
|
22
|
+
// UI methods
|
|
23
|
+
addRibbonIcon = vi.fn();
|
|
24
|
+
addStatusBarItem = vi.fn();
|
|
25
|
+
addCommand = vi.fn();
|
|
26
|
+
removeCommand = vi.fn();
|
|
27
|
+
|
|
28
|
+
// Event methods
|
|
29
|
+
registerDomEvent = vi.fn();
|
|
30
|
+
registerCodeMirror = vi.fn();
|
|
31
|
+
registerEditorExtension = vi.fn();
|
|
32
|
+
registerMarkdownPostProcessor = vi.fn();
|
|
33
|
+
registerMarkdownCodeBlockProcessor = vi.fn();
|
|
34
|
+
registerObsidianProtocolHandler = vi.fn();
|
|
35
|
+
registerEditorSuggest = vi.fn();
|
|
36
|
+
registerHoverLinkSource = vi.fn();
|
|
37
|
+
|
|
38
|
+
// Interval methods
|
|
39
|
+
registerInterval = vi.fn();
|
|
40
|
+
|
|
41
|
+
// View and extension methods
|
|
42
|
+
registerView = vi.fn();
|
|
43
|
+
registerExtensions = vi.fn();
|
|
44
|
+
|
|
45
|
+
// Lifecycle methods
|
|
46
|
+
onUserEnable = vi.fn();
|
|
47
|
+
load = vi.fn();
|
|
48
|
+
unload = vi.fn();
|
|
49
|
+
|
|
50
|
+
// Other methods
|
|
51
|
+
addChild = vi.fn();
|
|
52
|
+
removeChild = vi.fn();
|
|
53
|
+
register = vi.fn();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// PluginSettingTab mock
|
|
57
|
+
export class PluginSettingTab {
|
|
58
|
+
app: any;
|
|
59
|
+
plugin: any;
|
|
60
|
+
containerEl: HTMLElement;
|
|
61
|
+
|
|
62
|
+
constructor(app: any, plugin: any) {
|
|
63
|
+
this.app = app;
|
|
64
|
+
this.plugin = plugin;
|
|
65
|
+
this.containerEl = document.createElement("div");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
display = vi.fn();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ItemView mock
|
|
72
|
+
export class ItemView {
|
|
73
|
+
app: any;
|
|
74
|
+
leaf: any;
|
|
75
|
+
containerEl: HTMLElement;
|
|
76
|
+
|
|
77
|
+
constructor(leaf: any) {
|
|
78
|
+
this.leaf = leaf;
|
|
79
|
+
this.app = leaf?.app;
|
|
80
|
+
this.containerEl = document.createElement("div");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Don't override onOpen/onClose - let subclasses implement them
|
|
84
|
+
// These methods are implemented by MountableView mixin
|
|
85
|
+
|
|
86
|
+
// Don't provide default implementations for these methods
|
|
87
|
+
// Let subclasses implement them
|
|
88
|
+
getViewType(): string {
|
|
89
|
+
return "mock-view";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
getDisplayText(): string {
|
|
93
|
+
return "Mock View";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getIcon(): string {
|
|
97
|
+
return "mock-icon";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getState = vi.fn().mockReturnValue({});
|
|
101
|
+
setState = vi.fn().mockResolvedValue(undefined);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Setting component mock
|
|
105
|
+
export class Setting {
|
|
106
|
+
settingEl: HTMLElement;
|
|
107
|
+
nameEl: HTMLElement;
|
|
108
|
+
descEl: HTMLElement;
|
|
109
|
+
controlEl: HTMLElement;
|
|
110
|
+
|
|
111
|
+
constructor(_containerEl: HTMLElement) {
|
|
112
|
+
this.settingEl = document.createElement("div");
|
|
113
|
+
this.nameEl = document.createElement("div");
|
|
114
|
+
this.descEl = document.createElement("div");
|
|
115
|
+
this.controlEl = document.createElement("div");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setName = vi.fn().mockReturnThis();
|
|
119
|
+
setDesc = vi.fn().mockReturnThis();
|
|
120
|
+
addText = vi.fn().mockReturnThis();
|
|
121
|
+
addTextArea = vi.fn().mockReturnThis();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// TFolder mock
|
|
125
|
+
export class TFolder {
|
|
126
|
+
path: string;
|
|
127
|
+
name: string;
|
|
128
|
+
children: any[];
|
|
129
|
+
vault: any;
|
|
130
|
+
parent: TFolder | null;
|
|
131
|
+
|
|
132
|
+
constructor(path: string) {
|
|
133
|
+
this.path = path;
|
|
134
|
+
this.name = path.split("/").pop() || "";
|
|
135
|
+
this.children = [];
|
|
136
|
+
this.vault = {};
|
|
137
|
+
this.parent = null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isRoot(): boolean {
|
|
141
|
+
return this.path === "" || this.path === "/";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// TFile mock with full interface
|
|
146
|
+
export class TFile {
|
|
147
|
+
path: string;
|
|
148
|
+
name: string;
|
|
149
|
+
basename: string;
|
|
150
|
+
extension: string;
|
|
151
|
+
stat: any;
|
|
152
|
+
vault: any;
|
|
153
|
+
parent: TFolder | null;
|
|
154
|
+
|
|
155
|
+
constructor(path: string, parentPath?: string) {
|
|
156
|
+
this.path = path;
|
|
157
|
+
this.name = path.split("/").pop() || "";
|
|
158
|
+
this.basename = this.name.replace(/\.[^/.]+$/, ""); // Remove extension
|
|
159
|
+
this.extension = path.split(".").pop() || "md";
|
|
160
|
+
this.stat = {};
|
|
161
|
+
this.vault = {};
|
|
162
|
+
|
|
163
|
+
// Set parent based on path or explicit parentPath
|
|
164
|
+
if (parentPath !== undefined) {
|
|
165
|
+
this.parent = parentPath ? new TFolder(parentPath) : null;
|
|
166
|
+
} else {
|
|
167
|
+
// Derive parent from path
|
|
168
|
+
const lastSlash = path.lastIndexOf("/");
|
|
169
|
+
if (lastSlash > 0) {
|
|
170
|
+
this.parent = new TFolder(path.substring(0, lastSlash));
|
|
171
|
+
} else {
|
|
172
|
+
this.parent = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Modal mock
|
|
179
|
+
export class Modal {
|
|
180
|
+
app: any;
|
|
181
|
+
containerEl: HTMLElement;
|
|
182
|
+
titleEl: HTMLElement;
|
|
183
|
+
contentEl: HTMLElement;
|
|
184
|
+
|
|
185
|
+
constructor(app: any) {
|
|
186
|
+
this.app = app;
|
|
187
|
+
this.containerEl = document.createElement("div");
|
|
188
|
+
this.titleEl = document.createElement("div");
|
|
189
|
+
this.contentEl = document.createElement("div");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
open = vi.fn();
|
|
193
|
+
close = vi.fn();
|
|
194
|
+
onOpen = vi.fn();
|
|
195
|
+
onClose = vi.fn();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Notice mock
|
|
199
|
+
export class Notice {
|
|
200
|
+
constructor(message: string) {
|
|
201
|
+
console.log(`Notice: ${message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MarkdownRenderer mock
|
|
206
|
+
export const MarkdownRenderer = {
|
|
207
|
+
render: vi.fn().mockResolvedValue(undefined),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Debounce function mock
|
|
211
|
+
export function debounce<T extends (...args: any[]) => any>(
|
|
212
|
+
func: T,
|
|
213
|
+
wait: number,
|
|
214
|
+
immediate?: boolean
|
|
215
|
+
): T {
|
|
216
|
+
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
217
|
+
|
|
218
|
+
return ((...args: Parameters<T>) => {
|
|
219
|
+
const later = () => {
|
|
220
|
+
timeout = null;
|
|
221
|
+
if (!immediate) func(...args);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const callNow = immediate && !timeout;
|
|
225
|
+
|
|
226
|
+
if (timeout !== null) {
|
|
227
|
+
clearTimeout(timeout);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
timeout = setTimeout(later, wait);
|
|
231
|
+
|
|
232
|
+
if (callNow) func(...args);
|
|
233
|
+
}) as T;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// normalizePath mock - simple path normalization for tests
|
|
237
|
+
export function normalizePath(path: string): string {
|
|
238
|
+
// Basic normalization: replace backslashes with forward slashes
|
|
239
|
+
// and remove duplicate slashes
|
|
240
|
+
return path.replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// App mock
|
|
244
|
+
export const App = vi.fn();
|
|
245
|
+
|
|
246
|
+
// Mock interfaces for TypeScript
|
|
247
|
+
export interface MockApp {
|
|
248
|
+
fileManager: {
|
|
249
|
+
processFrontMatter: ReturnType<typeof vi.fn>;
|
|
250
|
+
};
|
|
251
|
+
metadataCache: {
|
|
252
|
+
getFileCache: ReturnType<typeof vi.fn>;
|
|
253
|
+
};
|
|
254
|
+
vault: {
|
|
255
|
+
getAbstractFileByPath: ReturnType<typeof vi.fn>;
|
|
256
|
+
on: ReturnType<typeof vi.fn>;
|
|
257
|
+
read: ReturnType<typeof vi.fn>;
|
|
258
|
+
modify: ReturnType<typeof vi.fn>;
|
|
259
|
+
create: ReturnType<typeof vi.fn>;
|
|
260
|
+
delete: ReturnType<typeof vi.fn>;
|
|
261
|
+
rename: ReturnType<typeof vi.fn>;
|
|
262
|
+
getFiles: ReturnType<typeof vi.fn>;
|
|
263
|
+
getMarkdownFiles: ReturnType<typeof vi.fn>;
|
|
264
|
+
getFolderByPath: ReturnType<typeof vi.fn>;
|
|
265
|
+
};
|
|
266
|
+
workspace: {
|
|
267
|
+
getActiveFile: ReturnType<typeof vi.fn>;
|
|
268
|
+
on: ReturnType<typeof vi.fn>;
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Helper function to create a fully mocked app
|
|
273
|
+
export function createMockApp(): MockApp {
|
|
274
|
+
return {
|
|
275
|
+
fileManager: {
|
|
276
|
+
processFrontMatter: vi.fn(),
|
|
277
|
+
},
|
|
278
|
+
metadataCache: {
|
|
279
|
+
getFileCache: vi.fn(),
|
|
280
|
+
},
|
|
281
|
+
vault: {
|
|
282
|
+
getAbstractFileByPath: vi.fn(),
|
|
283
|
+
on: vi.fn(),
|
|
284
|
+
read: vi.fn(),
|
|
285
|
+
modify: vi.fn(),
|
|
286
|
+
create: vi.fn(),
|
|
287
|
+
delete: vi.fn(),
|
|
288
|
+
rename: vi.fn(),
|
|
289
|
+
getFiles: vi.fn().mockReturnValue([]),
|
|
290
|
+
getMarkdownFiles: vi.fn().mockReturnValue([]),
|
|
291
|
+
getFolderByPath: vi.fn(),
|
|
292
|
+
},
|
|
293
|
+
workspace: {
|
|
294
|
+
getActiveFile: vi.fn(),
|
|
295
|
+
on: vi.fn(),
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Helper to create mock TFile instances
|
|
301
|
+
export function createMockFile(
|
|
302
|
+
path: string,
|
|
303
|
+
options?: {
|
|
304
|
+
basename?: string;
|
|
305
|
+
parentPath?: string;
|
|
306
|
+
extension?: string;
|
|
307
|
+
}
|
|
308
|
+
): TFile {
|
|
309
|
+
const file = new TFile(path, options?.parentPath);
|
|
310
|
+
if (options?.basename) {
|
|
311
|
+
file.basename = options.basename;
|
|
312
|
+
}
|
|
313
|
+
if (options?.extension) {
|
|
314
|
+
file.extension = options.extension;
|
|
315
|
+
}
|
|
316
|
+
return file;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Helper to create mock file cache
|
|
320
|
+
export function createMockFileCache(frontmatter?: Record<string, any>) {
|
|
321
|
+
return {
|
|
322
|
+
frontmatter: frontmatter || {},
|
|
323
|
+
frontmatterPosition: frontmatter ? { start: { line: 0 }, end: { line: 3 } } : null,
|
|
324
|
+
sections: [],
|
|
325
|
+
headings: [],
|
|
326
|
+
links: [],
|
|
327
|
+
embeds: [],
|
|
328
|
+
tags: [],
|
|
329
|
+
listItems: [],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// File operations mocks
|
|
4
|
+
export const mockFileOperations = {
|
|
5
|
+
arraysEqual: vi.fn(),
|
|
6
|
+
normalizeArray: vi.fn(),
|
|
7
|
+
createFileLink: vi.fn(),
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Link parser mocks
|
|
11
|
+
export const mockLinkParser = {
|
|
12
|
+
extractFilePathFromLink: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Default mock implementations that match the actual behavior
|
|
16
|
+
export function setupDefaultMockImplementations() {
|
|
17
|
+
// Set up file operations mocks
|
|
18
|
+
mockFileOperations.normalizeArray.mockImplementation((arr) =>
|
|
19
|
+
Array.isArray(arr) ? arr : arr ? [arr] : []
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
mockFileOperations.arraysEqual.mockImplementation(
|
|
23
|
+
(a, b) => JSON.stringify(a) === JSON.stringify(b)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
mockFileOperations.createFileLink.mockImplementation((file) => {
|
|
27
|
+
if (!file) return "[[Unknown File]]";
|
|
28
|
+
|
|
29
|
+
const basename =
|
|
30
|
+
file.basename ||
|
|
31
|
+
file.path
|
|
32
|
+
?.split("/")
|
|
33
|
+
.pop()
|
|
34
|
+
?.replace(/\.[^/.]+$/, "") ||
|
|
35
|
+
"";
|
|
36
|
+
const parentPath = file.parent?.path;
|
|
37
|
+
|
|
38
|
+
if (!parentPath || parentPath === "/" || parentPath === "") {
|
|
39
|
+
return `[[${basename}]]`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `[[${parentPath}/${basename}|${basename}]]`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Set up link parser mocks
|
|
46
|
+
mockLinkParser.extractFilePathFromLink.mockImplementation((link) => {
|
|
47
|
+
if (!link || typeof link !== "string") return null;
|
|
48
|
+
|
|
49
|
+
// Handle text that contains a link
|
|
50
|
+
const linkMatch = link.match(/\[\[([^\]]+)\]\]/);
|
|
51
|
+
if (linkMatch) {
|
|
52
|
+
const content = linkMatch[1];
|
|
53
|
+
// Remove display name if present
|
|
54
|
+
const filePart = content.split("|")[0].trim();
|
|
55
|
+
if (!filePart) return null;
|
|
56
|
+
|
|
57
|
+
// Add .md extension if not present
|
|
58
|
+
return filePart.endsWith(".md") ? filePart : `${filePart}.md`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Reset all mocks
|
|
66
|
+
export function resetAllMocks() {
|
|
67
|
+
Object.values(mockFileOperations).forEach((mock) => {
|
|
68
|
+
mock.mockReset();
|
|
69
|
+
});
|
|
70
|
+
Object.values(mockLinkParser).forEach((mock) => {
|
|
71
|
+
mock.mockReset();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Helper to setup mocks with specific implementations
|
|
76
|
+
export function setupMockImplementation(
|
|
77
|
+
mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
|
|
78
|
+
implementation: (...args: any[]) => any
|
|
79
|
+
) {
|
|
80
|
+
if (mockName in mockFileOperations) {
|
|
81
|
+
(mockFileOperations as any)[mockName].mockImplementation(implementation);
|
|
82
|
+
} else if (mockName in mockLinkParser) {
|
|
83
|
+
(mockLinkParser as any)[mockName].mockImplementation(implementation);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Helper to setup mock return values
|
|
88
|
+
export function setupMockReturnValue(
|
|
89
|
+
mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
|
|
90
|
+
value: any
|
|
91
|
+
) {
|
|
92
|
+
if (mockName in mockFileOperations) {
|
|
93
|
+
(mockFileOperations as any)[mockName].mockReturnValue(value);
|
|
94
|
+
} else if (mockName in mockLinkParser) {
|
|
95
|
+
(mockLinkParser as any)[mockName].mockReturnValue(value);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Helper to verify mock calls
|
|
100
|
+
export function verifyMockCalls(
|
|
101
|
+
mockName: keyof typeof mockFileOperations | keyof typeof mockLinkParser,
|
|
102
|
+
expectedCalls: any[][]
|
|
103
|
+
) {
|
|
104
|
+
const mock =
|
|
105
|
+
mockName in mockFileOperations
|
|
106
|
+
? (mockFileOperations as any)[mockName]
|
|
107
|
+
: (mockLinkParser as any)[mockName];
|
|
108
|
+
|
|
109
|
+
expect(mock).toHaveBeenCalledTimes(expectedCalls.length);
|
|
110
|
+
expectedCalls.forEach((args, index) => {
|
|
111
|
+
expect(mock).toHaveBeenNthCalledWith(index + 1, ...args);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import { mockFileOperations, mockLinkParser } from "./mocks/utils";
|
|
3
|
+
|
|
4
|
+
// Global test setup that can be imported once per test file
|
|
5
|
+
export function setupTestEnvironment() {
|
|
6
|
+
// Mock the utils modules
|
|
7
|
+
vi.mock("@obsidian-plugins/utils/file-operations", () => mockFileOperations);
|
|
8
|
+
vi.mock("@obsidian-plugins/utils/link-parser", () => mockLinkParser);
|
|
9
|
+
|
|
10
|
+
// Mock any plugin-specific components
|
|
11
|
+
vi.mock("../src/components/settings-tab", () => ({
|
|
12
|
+
TreePropertiesManagerSettingTab: class MockSettingTab {},
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Return cleanup function
|
|
16
|
+
return () => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { BehaviorSubject } from "rxjs";
|
|
2
|
-
export interface BaseRule {
|
|
3
|
-
id: string;
|
|
4
|
-
expression: string;
|
|
5
|
-
enabled: boolean;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Generic base class for evaluating JavaScript expressions against frontmatter objects.
|
|
9
|
-
* Provides reactive compilation of rules via RxJS subscription and safe evaluation.
|
|
10
|
-
*/
|
|
11
|
-
export declare abstract class BaseEvaluator<TRule extends BaseRule, TSettings> {
|
|
12
|
-
protected compiledRules: Array<TRule & {
|
|
13
|
-
fn: (frontmatter: Record<string, unknown>) => boolean;
|
|
14
|
-
}>;
|
|
15
|
-
private settingsSubscription;
|
|
16
|
-
constructor(settingsStore: BehaviorSubject<TSettings>);
|
|
17
|
-
/**
|
|
18
|
-
* Extract rules from settings object. Must be implemented by subclasses.
|
|
19
|
-
*/
|
|
20
|
-
protected abstract extractRules(settings: TSettings): TRule[];
|
|
21
|
-
/**
|
|
22
|
-
* Compile rules into executable functions with error handling.
|
|
23
|
-
*/
|
|
24
|
-
private compileRules;
|
|
25
|
-
/**
|
|
26
|
-
* Evaluate a single rule against frontmatter. Returns the result or undefined if error.
|
|
27
|
-
*/
|
|
28
|
-
protected evaluateRule(rule: TRule & {
|
|
29
|
-
fn: (frontmatter: Record<string, unknown>) => boolean;
|
|
30
|
-
}, frontmatter: Record<string, unknown>): boolean | undefined;
|
|
31
|
-
/**
|
|
32
|
-
* Convert evaluation result to boolean - only explicit true is considered truthy.
|
|
33
|
-
*/
|
|
34
|
-
protected isTruthy(result: boolean | undefined): boolean;
|
|
35
|
-
/**
|
|
36
|
-
* Clean up subscriptions and compiled rules.
|
|
37
|
-
*/
|
|
38
|
-
destroy(): void;
|
|
39
|
-
/**
|
|
40
|
-
* Get the number of active (compiled) rules.
|
|
41
|
-
*/
|
|
42
|
-
getActiveRuleCount(): number;
|
|
43
|
-
/**
|
|
44
|
-
* Get information about all rules including their validity.
|
|
45
|
-
*/
|
|
46
|
-
getRuleInfo(): Array<{
|
|
47
|
-
expression: string;
|
|
48
|
-
isValid: boolean;
|
|
49
|
-
enabled: boolean;
|
|
50
|
-
}>;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=evaluator-base.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"evaluator-base.d.ts","sourceRoot":"","sources":["../../src/core/evaluator-base.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAgB,MAAM,MAAM,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED;;;GAGG;AACH,8BAAsB,aAAa,CAAC,KAAK,SAAS,QAAQ,EAAE,SAAS;IACpE,SAAS,CAAC,aAAa,EAAE,KAAK,CAC7B,KAAK,GAAG;QAAE,EAAE,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;KAAE,CACjE,CAAM;IACP,OAAO,CAAC,oBAAoB,CAA6B;gBAE7C,aAAa,EAAE,eAAe,CAAC,SAAS,CAAC;IAUrD;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,CAAC,QAAQ,EAAE,SAAS,GAAG,KAAK,EAAE;IAE7D;;OAEG;IACH,OAAO,CAAC,YAAY;IA6BpB;;OAEG;IACH,SAAS,CAAC,YAAY,CACrB,IAAI,EAAE,KAAK,GAAG;QAAE,EAAE,EAAE,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAA;KAAE,EACvE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAClC,OAAO,GAAG,SAAS;IAStB;;OAEG;IACH,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO;IAIxD;;OAEG;IACH,OAAO,IAAI,IAAI;IAQf;;OAEG;IACH,kBAAkB,IAAI,MAAM;IAI5B;;OAEG;IACH,WAAW,IAAI,KAAK,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAShF"}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic base class for evaluating JavaScript expressions against frontmatter objects.
|
|
3
|
-
* Provides reactive compilation of rules via RxJS subscription and safe evaluation.
|
|
4
|
-
*/
|
|
5
|
-
export class BaseEvaluator {
|
|
6
|
-
constructor(settingsStore) {
|
|
7
|
-
this.compiledRules = [];
|
|
8
|
-
this.settingsSubscription = null;
|
|
9
|
-
const initialRules = this.extractRules(settingsStore.value);
|
|
10
|
-
this.compileRules(initialRules);
|
|
11
|
-
this.settingsSubscription = settingsStore.subscribe((settings) => {
|
|
12
|
-
const newRules = this.extractRules(settings);
|
|
13
|
-
this.compileRules(newRules);
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Compile rules into executable functions with error handling.
|
|
18
|
-
*/
|
|
19
|
-
compileRules(rules) {
|
|
20
|
-
this.compiledRules = [];
|
|
21
|
-
for (const rule of rules) {
|
|
22
|
-
if (!rule.enabled || !rule.expression.trim())
|
|
23
|
-
continue;
|
|
24
|
-
try {
|
|
25
|
-
const cleanExpression = rule.expression.trim();
|
|
26
|
-
// Create a function that takes 'fm' (frontmatter) as parameter
|
|
27
|
-
// and evaluates the expression in that context
|
|
28
|
-
const fn = new Function("fm", `return (${cleanExpression});`);
|
|
29
|
-
// Test the function with a dummy object to catch syntax errors early
|
|
30
|
-
fn({});
|
|
31
|
-
this.compiledRules.push(Object.assign(Object.assign({}, rule), { expression: cleanExpression, fn }));
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
console.warn(`Invalid rule expression "${rule.expression}":`, error);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Evaluate a single rule against frontmatter. Returns the result or undefined if error.
|
|
40
|
-
*/
|
|
41
|
-
evaluateRule(rule, frontmatter) {
|
|
42
|
-
try {
|
|
43
|
-
return rule.fn(frontmatter);
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
console.warn(`Error evaluating rule "${rule.expression}":`, error);
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Convert evaluation result to boolean - only explicit true is considered truthy.
|
|
52
|
-
*/
|
|
53
|
-
isTruthy(result) {
|
|
54
|
-
return result === true;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Clean up subscriptions and compiled rules.
|
|
58
|
-
*/
|
|
59
|
-
destroy() {
|
|
60
|
-
if (this.settingsSubscription) {
|
|
61
|
-
this.settingsSubscription.unsubscribe();
|
|
62
|
-
this.settingsSubscription = null;
|
|
63
|
-
}
|
|
64
|
-
this.compiledRules = [];
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Get the number of active (compiled) rules.
|
|
68
|
-
*/
|
|
69
|
-
getActiveRuleCount() {
|
|
70
|
-
return this.compiledRules.length;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Get information about all rules including their validity.
|
|
74
|
-
*/
|
|
75
|
-
getRuleInfo() {
|
|
76
|
-
const validExpressions = new Set(this.compiledRules.map((r) => r.expression));
|
|
77
|
-
return this.compiledRules.map((rule) => ({
|
|
78
|
-
expression: rule.expression,
|
|
79
|
-
isValid: validExpressions.has(rule.expression),
|
|
80
|
-
enabled: rule.enabled,
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
//# sourceMappingURL=evaluator-base.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"evaluator-base.js","sourceRoot":"","sources":["../../src/core/evaluator-base.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BehaviorSubject, Subscription } from \"rxjs\";\n\nexport interface BaseRule {\n\tid: string;\n\texpression: string;\n\tenabled: boolean;\n}\n\n/**\n * Generic base class for evaluating JavaScript expressions against frontmatter objects.\n * Provides reactive compilation of rules via RxJS subscription and safe evaluation.\n */\nexport abstract class BaseEvaluator<TRule extends BaseRule, TSettings> {\n\tprotected compiledRules: Array<\n\t\tTRule & { fn: (frontmatter: Record<string, unknown>) => boolean }\n\t> = [];\n\tprivate settingsSubscription: Subscription | null = null;\n\n\tconstructor(settingsStore: BehaviorSubject<TSettings>) {\n\t\tconst initialRules = this.extractRules(settingsStore.value);\n\t\tthis.compileRules(initialRules);\n\n\t\tthis.settingsSubscription = settingsStore.subscribe((settings) => {\n\t\t\tconst newRules = this.extractRules(settings);\n\t\t\tthis.compileRules(newRules);\n\t\t});\n\t}\n\n\t/**\n\t * Extract rules from settings object. Must be implemented by subclasses.\n\t */\n\tprotected abstract extractRules(settings: TSettings): TRule[];\n\n\t/**\n\t * Compile rules into executable functions with error handling.\n\t */\n\tprivate compileRules(rules: TRule[]): void {\n\t\tthis.compiledRules = [];\n\n\t\tfor (const rule of rules) {\n\t\t\tif (!rule.enabled || !rule.expression.trim()) continue;\n\n\t\t\ttry {\n\t\t\t\tconst cleanExpression = rule.expression.trim();\n\n\t\t\t\t// Create a function that takes 'fm' (frontmatter) as parameter\n\t\t\t\t// and evaluates the expression in that context\n\t\t\t\tconst fn = new Function(\"fm\", `return (${cleanExpression});`) as (\n\t\t\t\t\tfrontmatter: Record<string, unknown>\n\t\t\t\t) => boolean;\n\n\t\t\t\t// Test the function with a dummy object to catch syntax errors early\n\t\t\t\tfn({});\n\n\t\t\t\tthis.compiledRules.push({\n\t\t\t\t\t...rule,\n\t\t\t\t\texpression: cleanExpression,\n\t\t\t\t\tfn,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconsole.warn(`Invalid rule expression \"${rule.expression}\":`, error);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Evaluate a single rule against frontmatter. Returns the result or undefined if error.\n\t */\n\tprotected evaluateRule(\n\t\trule: TRule & { fn: (frontmatter: Record<string, unknown>) => boolean },\n\t\tfrontmatter: Record<string, unknown>\n\t): boolean | undefined {\n\t\ttry {\n\t\t\treturn rule.fn(frontmatter);\n\t\t} catch (error) {\n\t\t\tconsole.warn(`Error evaluating rule \"${rule.expression}\":`, error);\n\t\t\treturn undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Convert evaluation result to boolean - only explicit true is considered truthy.\n\t */\n\tprotected isTruthy(result: boolean | undefined): boolean {\n\t\treturn result === true;\n\t}\n\n\t/**\n\t * Clean up subscriptions and compiled rules.\n\t */\n\tdestroy(): void {\n\t\tif (this.settingsSubscription) {\n\t\t\tthis.settingsSubscription.unsubscribe();\n\t\t\tthis.settingsSubscription = null;\n\t\t}\n\t\tthis.compiledRules = [];\n\t}\n\n\t/**\n\t * Get the number of active (compiled) rules.\n\t */\n\tgetActiveRuleCount(): number {\n\t\treturn this.compiledRules.length;\n\t}\n\n\t/**\n\t * Get information about all rules including their validity.\n\t */\n\tgetRuleInfo(): Array<{ expression: string; isValid: boolean; enabled: boolean }> {\n\t\tconst validExpressions = new Set(this.compiledRules.map((r) => r.expression));\n\n\t\treturn this.compiledRules.map((rule) => ({\n\t\t\texpression: rule.expression,\n\t\t\tisValid: validExpressions.has(rule.expression),\n\t\t\tenabled: rule.enabled,\n\t\t}));\n\t}\n}\n"]}
|