@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.
Files changed (69) 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/index.d.ts +2 -1
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +2 -1
  32. package/dist/core/index.js.map +1 -1
  33. package/package.json +3 -5
  34. package/src/async/async.ts +117 -0
  35. package/src/async/batch-operations.ts +53 -0
  36. package/src/async/index.ts +2 -0
  37. package/src/core/evaluator/base.ts +71 -0
  38. package/src/core/evaluator/color.ts +37 -0
  39. package/src/core/evaluator/excluded.ts +63 -0
  40. package/src/core/evaluator/filter.ts +35 -0
  41. package/src/core/evaluator/included.ts +74 -0
  42. package/src/core/evaluator/index.ts +5 -0
  43. package/src/core/expression-utils.ts +53 -0
  44. package/src/core/generate.ts +22 -0
  45. package/src/core/index.ts +3 -0
  46. package/src/date/date-recurrence.ts +244 -0
  47. package/src/date/date.ts +111 -0
  48. package/src/date/index.ts +2 -0
  49. package/src/file/child-reference.ts +76 -0
  50. package/src/file/file-operations.ts +197 -0
  51. package/src/file/file.ts +570 -0
  52. package/src/file/frontmatter.ts +80 -0
  53. package/src/file/index.ts +6 -0
  54. package/src/file/link-parser.ts +18 -0
  55. package/src/file/templater.ts +75 -0
  56. package/src/index.ts +14 -0
  57. package/src/settings/index.ts +2 -0
  58. package/src/settings/settings-store.ts +88 -0
  59. package/src/settings/settings-ui-builder.ts +507 -0
  60. package/src/string/index.ts +1 -0
  61. package/src/string/string.ts +26 -0
  62. package/src/testing/index.ts +23 -0
  63. package/src/testing/mocks/obsidian.ts +331 -0
  64. package/src/testing/mocks/utils.ts +113 -0
  65. package/src/testing/setup.ts +19 -0
  66. package/dist/core/evaluator-base.d.ts +0 -52
  67. package/dist/core/evaluator-base.d.ts.map +0 -1
  68. package/dist/core/evaluator-base.js +0 -84
  69. package/dist/core/evaluator-base.js.map +0 -1
@@ -0,0 +1,570 @@
1
+ import type { App, CachedMetadata } from "obsidian";
2
+ import { normalizePath, TFile } from "obsidian";
3
+
4
+ // ============================================================================
5
+ // File Path Operations
6
+ // ============================================================================
7
+
8
+ /**
9
+ * Retrieves a TFile object from the vault by its path.
10
+ * Handles path normalization using Obsidian's normalizePath utility.
11
+ *
12
+ * **Important**: Obsidian file paths ALWAYS include the `.md` extension.
13
+ * The TFile.path property returns paths like "folder/file.md", not "folder/file".
14
+ *
15
+ * @param app - The Obsidian App instance
16
+ * @param filePath - Path to the file (will be normalized, should include .md extension)
17
+ * @returns TFile if found, null otherwise
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * // Correct: Include .md extension
22
+ * const file = getFileByPath(app, "folder/note.md");
23
+ *
24
+ * // For wikilinks without extension, add .md
25
+ * const linkPath = "MyNote";
26
+ * const file = getFileByPath(app, `${linkPath}.md`);
27
+ * ```
28
+ */
29
+ export function getFileByPath(app: App, filePath: string): TFile | null {
30
+ // Normalize the path using Obsidian's utility
31
+ // This handles slashes, spaces, and platform-specific path issues
32
+ const normalizedPath = normalizePath(filePath);
33
+
34
+ // Use Vault's direct lookup method (most efficient)
35
+ // Prefer getFileByPath if available, otherwise use getAbstractFileByPath
36
+ if (typeof app.vault.getFileByPath === "function") {
37
+ return app.vault.getFileByPath(normalizedPath);
38
+ }
39
+
40
+ const abstractFile = app.vault.getAbstractFileByPath(normalizedPath);
41
+
42
+ return abstractFile instanceof TFile ? abstractFile : null;
43
+ }
44
+
45
+ /**
46
+ * Ensures a file path includes the .md extension.
47
+ * Use this when working with wikilinks or user input that may omit extensions.
48
+ *
49
+ * @param path - File path that may or may not include .md extension
50
+ * @returns Path guaranteed to end with .md
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * ensureMarkdownExtension("MyNote") // "MyNote.md"
55
+ * ensureMarkdownExtension("MyNote.md") // "MyNote.md"
56
+ * ensureMarkdownExtension("folder/note") // "folder/note.md"
57
+ * ```
58
+ */
59
+ export function ensureMarkdownExtension(path: string): string {
60
+ return path.endsWith(".md") ? path : `${path}.md`;
61
+ }
62
+
63
+ /**
64
+ * Removes the .md extension from a file path if present.
65
+ * Useful for displaying file names or creating wikilinks.
66
+ *
67
+ * @param path - File path that may include .md extension
68
+ * @returns Path without .md extension
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * removeMarkdownExtension("folder/note.md") // "folder/note"
73
+ * removeMarkdownExtension("folder/note") // "folder/note"
74
+ * ```
75
+ */
76
+ export function removeMarkdownExtension(path: string): string {
77
+ return path.endsWith(".md") ? path.slice(0, -3) : path;
78
+ }
79
+
80
+ // ============================================================================
81
+ // File Name Extraction
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Extracts the display name from a file path or wiki link.
86
+ *
87
+ * Handles various formats:
88
+ * - `[[path/to/file|Alias]]` -> returns "Alias"
89
+ * - `[[path/to/file]]` -> returns "file"
90
+ * - `path/to/file.md` -> returns "file"
91
+ * - `file.md` -> returns "file"
92
+ *
93
+ * @param input - File path or wiki link string
94
+ * @returns The display name to show in the UI
95
+ */
96
+ export function extractDisplayName(input: string): string {
97
+ if (!input) return "";
98
+
99
+ // Remove any surrounding whitespace
100
+ const trimmed = input.trim();
101
+
102
+ // Check if it's a wiki link format [[path|alias]] or [[path]]
103
+ const wikiLinkMatch = trimmed.match(/^\[\[([^\]]+)\]\]$/);
104
+
105
+ if (wikiLinkMatch) {
106
+ const innerContent = wikiLinkMatch[1];
107
+
108
+ // Check if there's an alias (pipe character)
109
+ const pipeIndex = innerContent.indexOf("|");
110
+
111
+ if (pipeIndex !== -1) {
112
+ // Return the alias (everything after the pipe)
113
+ return innerContent.substring(pipeIndex + 1).trim();
114
+ }
115
+
116
+ // No alias, extract filename from path
117
+ const path = innerContent.trim();
118
+
119
+ const lastSlashIndex = path.lastIndexOf("/");
120
+
121
+ const filename = lastSlashIndex !== -1 ? path.substring(lastSlashIndex + 1) : path;
122
+
123
+ return filename.replace(/\.md$/i, "");
124
+ }
125
+
126
+ // Not a wiki link, treat as regular path
127
+ const lastSlashIndex = trimmed.lastIndexOf("/");
128
+
129
+ const filename = lastSlashIndex !== -1 ? trimmed.substring(lastSlashIndex + 1) : trimmed;
130
+
131
+ return filename.replace(/\.md$/i, "");
132
+ }
133
+
134
+ /**
135
+ * Extracts the actual file path from a wiki link or returns the path as-is.
136
+ *
137
+ * Handles:
138
+ * - `[[path/to/file|Alias]]` -> returns "path/to/file.md"
139
+ * - `[[path/to/file]]` -> returns "path/to/file.md"
140
+ * - `path/to/file.md` -> returns "path/to/file.md"
141
+ *
142
+ * @param input - File path or wiki link string
143
+ * @returns The actual file path (with .md extension)
144
+ */
145
+ export function extractFilePath(input: string): string {
146
+ if (!input) return "";
147
+
148
+ const trimmed = input.trim();
149
+
150
+ // Check if it's a wiki link format [[path|alias]] or [[path]]
151
+ const wikiLinkMatch = trimmed.match(/^\[\[([^\]]+)\]\]$/);
152
+
153
+ if (wikiLinkMatch) {
154
+ const innerContent = wikiLinkMatch[1];
155
+
156
+ // Check if there's an alias (pipe character)
157
+ const pipeIndex = innerContent.indexOf("|");
158
+
159
+ const path =
160
+ pipeIndex !== -1 ? innerContent.substring(0, pipeIndex).trim() : innerContent.trim();
161
+
162
+ // Ensure .md extension
163
+ return path.endsWith(".md") ? path : `${path}.md`;
164
+ }
165
+
166
+ // Not a wiki link, ensure .md extension
167
+ return trimmed.endsWith(".md") ? trimmed : `${trimmed}.md`;
168
+ }
169
+
170
+ // ============================================================================
171
+ // File Context
172
+ // ============================================================================
173
+
174
+ export interface FileContext {
175
+ path: string;
176
+ pathWithExt: string;
177
+ baseName: string;
178
+ file: TFile | null;
179
+ frontmatter: Record<string, any> | undefined;
180
+ cache: CachedMetadata | null;
181
+ }
182
+
183
+ /**
184
+ * Creates a comprehensive file context object containing all relevant file information.
185
+ * Handles path normalization, file lookup, and metadata caching.
186
+ */
187
+ export function getFileContext(app: App, path: string): FileContext {
188
+ const pathWithExt = ensureMarkdownExtension(path);
189
+
190
+ const baseName = removeMarkdownExtension(path);
191
+
192
+ const file = getFileByPath(app, pathWithExt);
193
+
194
+ const cache = file ? app.metadataCache.getFileCache(file) : null;
195
+
196
+ const frontmatter = cache?.frontmatter;
197
+
198
+ return {
199
+ path,
200
+ pathWithExt,
201
+ baseName,
202
+ file,
203
+ frontmatter,
204
+ cache,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Helper function to work with file context that automatically handles file not found cases.
210
+ * Returns null if the file doesn't exist, otherwise executes the callback with the context.
211
+ */
212
+ export async function withFileContext<T>(
213
+ app: App,
214
+ path: string,
215
+ callback: (context: FileContext) => Promise<T> | T
216
+ ): Promise<T | null> {
217
+ const context = getFileContext(app, path);
218
+
219
+ if (!context.file) {
220
+ console.warn(`File not found: ${context.pathWithExt}`);
221
+ return null;
222
+ }
223
+
224
+ return await callback(context);
225
+ }
226
+
227
+ // ============================================================================
228
+ // File Path Generation
229
+ // ============================================================================
230
+
231
+ /**
232
+ * Generates a unique file path by appending a counter if the file already exists.
233
+ * Automatically adds .md extension if not present.
234
+ *
235
+ * @param app - The Obsidian App instance
236
+ * @param folder - Folder path (empty string for root, no trailing slash needed)
237
+ * @param baseName - Base file name without extension
238
+ * @returns Unique file path that doesn't exist in the vault
239
+ *
240
+ * @example
241
+ * ```ts
242
+ * // If "MyNote.md" exists, returns "MyNote 1.md"
243
+ * const path = getUniqueFilePath(app, "", "MyNote");
244
+ *
245
+ * // With folder: "Projects/Task.md" -> "Projects/Task 1.md"
246
+ * const path = getUniqueFilePath(app, "Projects", "Task");
247
+ *
248
+ * // Root folder handling
249
+ * const path = getUniqueFilePath(app, "/", "Note"); // -> "Note.md"
250
+ * ```
251
+ */
252
+ export function getUniqueFilePath(app: App, folder: string, baseName: string): string {
253
+ const normalizedFolder = folder && folder !== "/" ? folder : "";
254
+ const folderPath = normalizedFolder ? `${normalizedFolder}/` : "";
255
+
256
+ let fileName = `${baseName}.md`;
257
+ let fullPath = `${folderPath}${fileName}`;
258
+ let counter = 1;
259
+
260
+ while (app.vault.getAbstractFileByPath(fullPath)) {
261
+ fileName = `${baseName} ${counter}.md`;
262
+ fullPath = `${folderPath}${fileName}`;
263
+ counter++;
264
+ }
265
+
266
+ return fullPath;
267
+ }
268
+
269
+ /**
270
+ * Generates a unique file path by appending a counter if the file already exists.
271
+ * Supports custom file extensions.
272
+ *
273
+ * @param app - The Obsidian App instance
274
+ * @param folder - Folder path (empty string for root)
275
+ * @param baseName - Base file name without extension
276
+ * @param extension - File extension (defaults to "md")
277
+ * @returns Unique file path that doesn't exist in the vault
278
+ */
279
+ export const generateUniqueFilePath = (
280
+ app: App,
281
+ folder: string,
282
+ baseName: string,
283
+ extension: string = "md"
284
+ ): string => {
285
+ const folderPath = folder ? `${folder}/` : "";
286
+ let filePath = `${folderPath}${baseName}.${extension}`;
287
+ let counter = 1;
288
+
289
+ while (app.vault.getAbstractFileByPath(filePath)) {
290
+ filePath = `${folderPath}${baseName} ${counter++}.${extension}`;
291
+ }
292
+
293
+ return filePath;
294
+ };
295
+
296
+ // ============================================================================
297
+ // Folder Note Operations
298
+ // ============================================================================
299
+
300
+ /**
301
+ * Checks if a file is a folder note.
302
+ * A folder note is a file whose name matches its parent folder name.
303
+ *
304
+ * @param filePath - Path to the file (e.g., "tasks/tasks.md")
305
+ * @returns true if the file is a folder note, false otherwise
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * isFolderNote("tasks/tasks.md") // true
310
+ * isFolderNote("tasks/subtask.md") // false
311
+ * isFolderNote("note.md") // false (no parent folder)
312
+ * isFolderNote("projects/docs/docs.md") // true
313
+ * ```
314
+ */
315
+ export function isFolderNote(filePath: string): boolean {
316
+ if (!filePath) return false;
317
+
318
+ // Remove .md extension for comparison
319
+ const pathWithoutExt = removeMarkdownExtension(filePath);
320
+
321
+ // Split path into segments
322
+ const segments = pathWithoutExt.split("/");
323
+
324
+ // Need at least 2 segments (folder/file)
325
+ if (segments.length < 2) return false;
326
+
327
+ // Get the file name (last segment) and parent folder name (second to last)
328
+ const fileName = segments[segments.length - 1];
329
+ const parentFolderName = segments[segments.length - 2];
330
+
331
+ // File is a folder note if its name matches the parent folder
332
+ return fileName === parentFolderName;
333
+ }
334
+
335
+ /**
336
+ * Gets the folder path for a file.
337
+ *
338
+ * @param filePath - Path to the file (e.g., "tasks/subtask.md")
339
+ * @returns Folder path without trailing slash, or empty string if file is in root
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * getFolderPath("tasks/subtask.md") // "tasks"
344
+ * getFolderPath("projects/docs/notes.md") // "projects/docs"
345
+ * getFolderPath("note.md") // ""
346
+ * ```
347
+ */
348
+ export function getFolderPath(filePath: string): string {
349
+ if (!filePath) return "";
350
+
351
+ const lastSlashIndex = filePath.lastIndexOf("/");
352
+
353
+ if (lastSlashIndex === -1) return "";
354
+
355
+ return filePath.substring(0, lastSlashIndex);
356
+ }
357
+
358
+ /**
359
+ * Gets all markdown files in a specific folder (non-recursive).
360
+ *
361
+ * @param app - The Obsidian App instance
362
+ * @param folderPath - Path to the folder (e.g., "tasks")
363
+ * @returns Array of TFile objects in the folder
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const files = getFilesInFolder(app, "tasks");
368
+ * // Returns [task1.md, task2.md, tasks.md] but not tasks/subtasks/file.md
369
+ * ```
370
+ */
371
+ export function getFilesInFolder(app: App, folderPath: string): TFile[] {
372
+ const allFiles = app.vault.getMarkdownFiles();
373
+
374
+ return allFiles.filter((file) => {
375
+ const fileFolder = getFolderPath(file.path);
376
+
377
+ return fileFolder === folderPath;
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Gets all markdown files in a folder and its subfolders recursively.
383
+ *
384
+ * @param app - The Obsidian App instance
385
+ * @param folderPath - Path to the folder (e.g., "tasks")
386
+ * @returns Array of TFile objects in the folder tree
387
+ *
388
+ * @example
389
+ * ```ts
390
+ * const files = getAllFilesInFolderTree(app, "tasks");
391
+ * // Returns all .md files in tasks/ and all its subdirectories
392
+ * ```
393
+ */
394
+ export function getAllFilesInFolderTree(app: App, folderPath: string): TFile[] {
395
+ const allFiles = app.vault.getMarkdownFiles();
396
+
397
+ const normalizedFolder = folderPath ? `${folderPath}/` : "";
398
+
399
+ return allFiles.filter((file) => {
400
+ if (!normalizedFolder) return true; // Root folder includes all files
401
+
402
+ return file.path.startsWith(normalizedFolder);
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Gets the parent file path based on folder structure.
408
+ * For a file in a folder, the parent is the folder note if it exists.
409
+ *
410
+ * @param app - The Obsidian App instance
411
+ * @param filePath - Path to the file
412
+ * @returns Path to parent file, or null if no parent exists
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * // If tasks/tasks.md exists
417
+ * getParentByFolder(app, "tasks/subtask.md") // "tasks/tasks.md"
418
+ *
419
+ * // If parent folder note doesn't exist
420
+ * getParentByFolder(app, "tasks/subtask.md") // null
421
+ *
422
+ * // Root level file
423
+ * getParentByFolder(app, "note.md") // null
424
+ * ```
425
+ */
426
+ export function getParentByFolder(app: App, filePath: string): string | null {
427
+ const folderPath = getFolderPath(filePath);
428
+
429
+ if (!folderPath) return null; // File is at root level
430
+
431
+ // Check if folder note exists
432
+ const folderSegments = folderPath.split("/");
433
+
434
+ const parentFolderName = folderSegments[folderSegments.length - 1];
435
+
436
+ const potentialParentPath = `${folderPath}/${parentFolderName}.md`;
437
+
438
+ const parentFile = getFileByPath(app, potentialParentPath);
439
+
440
+ return parentFile ? potentialParentPath : null;
441
+ }
442
+
443
+ /**
444
+ * Gets all child file paths based on folder structure.
445
+ * Works for both folder notes and regular files.
446
+ *
447
+ * For folder notes (e.g., "tasks/tasks.md"):
448
+ * - Returns all files directly in the folder (excluding the folder note)
449
+ * - Includes subfolder notes one level down
450
+ *
451
+ * For regular files (e.g., "tasks/task1.md"):
452
+ * - Returns the folder note from matching subfolder if it exists (e.g., "tasks/task1/task1.md")
453
+ *
454
+ * @param app - The Obsidian App instance
455
+ * @param filePath - Path to the file
456
+ * @returns Array of child file paths
457
+ *
458
+ * @example
459
+ * ```ts
460
+ * // For tasks/tasks.md (folder note)
461
+ * getChildrenByFolder(app, "tasks/tasks.md")
462
+ * // Returns ["tasks/task1.md", "tasks/task2.md", "tasks/subtasks/subtasks.md"]
463
+ *
464
+ * // For tasks/task1.md (regular file with matching subfolder)
465
+ * getChildrenByFolder(app, "tasks/task1.md")
466
+ * // Returns ["tasks/task1/task1.md"] if it exists
467
+ * ```
468
+ */
469
+ export function getChildrenByFolder(app: App, filePath: string): string[] {
470
+ const allFiles = app.vault.getMarkdownFiles();
471
+
472
+ // Case 1: Folder note - get all files in the folder
473
+ if (isFolderNote(filePath)) {
474
+ const folderPath = getFolderPath(filePath);
475
+
476
+ const children: string[] = [];
477
+
478
+ allFiles.forEach((file) => {
479
+ // Skip the folder note itself
480
+ if (file.path === filePath) return;
481
+
482
+ const fileFolder = getFolderPath(file.path);
483
+
484
+ // Direct child: file is in the same folder as the folder note
485
+ if (fileFolder === folderPath) {
486
+ children.push(file.path);
487
+
488
+ return;
489
+ }
490
+
491
+ // Subfolder note: file is a folder note one level deeper
492
+ // e.g., for "tasks/tasks.md", include "tasks/subtasks/subtasks.md"
493
+ if (fileFolder.startsWith(`${folderPath}/`)) {
494
+ // Check if it's exactly one level deeper and is a folder note
495
+ const relativePath = fileFolder.substring(folderPath.length + 1);
496
+
497
+ const isOneLevel = !relativePath.includes("/");
498
+
499
+ if (isOneLevel && isFolderNote(file.path)) {
500
+ children.push(file.path);
501
+ }
502
+ }
503
+ });
504
+
505
+ return children;
506
+ }
507
+
508
+ // Case 2: Regular file - check for matching subfolder with folder note
509
+ const pathWithoutExt = removeMarkdownExtension(filePath);
510
+
511
+ const fileName = pathWithoutExt.split("/").pop() || "";
512
+
513
+ const potentialChildFolder = `${pathWithoutExt}`;
514
+
515
+ const potentialChildPath = `${potentialChildFolder}/${fileName}.md`;
516
+
517
+ // Check if the child folder note exists
518
+ const childFile = getFileByPath(app, potentialChildPath);
519
+
520
+ return childFile ? [potentialChildPath] : [];
521
+ }
522
+
523
+ /**
524
+ * Finds all root nodes in a folder tree.
525
+ * Root nodes are files at the top level of the folder (directly in the folder, not in subfolders).
526
+ *
527
+ * @param app - The Obsidian App instance
528
+ * @param folderPath - Path to the folder
529
+ * @returns Array of root file paths
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * // For folder structure:
534
+ * // tasks/
535
+ * // tasks.md (folder note)
536
+ * // task1.md
537
+ * // subtasks/
538
+ * // subtasks.md
539
+ * // subtask1.md
540
+ *
541
+ * findRootNodesInFolder(app, "tasks")
542
+ * // Returns ["tasks/tasks.md", "tasks/task1.md"]
543
+ * // Excludes subtasks/subtasks.md and subtasks/subtask1.md (they're in subfolder)
544
+ * ```
545
+ */
546
+ export function findRootNodesInFolder(app: App, folderPath: string): string[] {
547
+ return getFilesInFolder(app, folderPath).map((file) => file.path);
548
+ }
549
+
550
+ // ============================================================================
551
+ // Legacy Utility Functions (kept for backwards compatibility)
552
+ // ============================================================================
553
+
554
+ export const sanitizeForFilename = (input: string): string => {
555
+ return input
556
+ .replace(/[<>:"/\\|?*]/g, "") // Remove invalid filename characters
557
+ .replace(/\s+/g, "-") // Replace spaces with hyphens
558
+ .replace(/-+/g, "-") // Replace multiple hyphens with single
559
+ .replace(/^-|-$/g, "") // Remove leading/trailing hyphens
560
+ .toLowerCase();
561
+ };
562
+
563
+ export const getFilenameFromPath = (filePath: string): string => {
564
+ return filePath.split("/").pop() || "Unknown";
565
+ };
566
+
567
+ export const isFileInConfiguredDirectory = (filePath: string, directory: string): boolean => {
568
+ const normalizedDir = directory.endsWith("/") ? directory.slice(0, -1) : directory;
569
+ return filePath.startsWith(`${normalizedDir}/`) || filePath === normalizedDir;
570
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Serializes a frontmatter value to a string representation for display in input fields.
3
+ * Converts arrays, objects, booleans, and numbers to their string representations.
4
+ */
5
+ export function serializeFrontmatterValue(value: unknown): string {
6
+ if (value === null || value === undefined) {
7
+ return "";
8
+ }
9
+
10
+ if (typeof value === "string") {
11
+ return value;
12
+ }
13
+
14
+ if (typeof value === "number" || typeof value === "boolean") {
15
+ return String(value);
16
+ }
17
+
18
+ if (Array.isArray(value) || typeof value === "object") {
19
+ return JSON.stringify(value);
20
+ }
21
+
22
+ return String(value);
23
+ }
24
+
25
+ /**
26
+ * Parses a string value back to its original frontmatter type.
27
+ * Detects and converts to: arrays, objects, booleans, numbers, or keeps as string.
28
+ */
29
+ export function parseFrontmatterValue(stringValue: string): unknown {
30
+ // Empty string
31
+ if (stringValue === "") {
32
+ return "";
33
+ }
34
+
35
+ // Try to parse as JSON (arrays, objects, null)
36
+ if (
37
+ (stringValue.startsWith("[") && stringValue.endsWith("]")) ||
38
+ (stringValue.startsWith("{") && stringValue.endsWith("}")) ||
39
+ stringValue === "null"
40
+ ) {
41
+ try {
42
+ return JSON.parse(stringValue);
43
+ } catch {
44
+ // If JSON parse fails, return as string
45
+ return stringValue;
46
+ }
47
+ }
48
+
49
+ // Parse booleans
50
+ if (stringValue === "true") {
51
+ return true;
52
+ }
53
+ if (stringValue === "false") {
54
+ return false;
55
+ }
56
+
57
+ // Parse numbers (including integers and floats)
58
+ if (/^-?\d+(\.\d+)?$/.test(stringValue)) {
59
+ const num = Number(stringValue);
60
+ if (!Number.isNaN(num)) {
61
+ return num;
62
+ }
63
+ }
64
+
65
+ // Return as string if no other type matches
66
+ return stringValue;
67
+ }
68
+
69
+ /**
70
+ * Converts a record of string values back to their original frontmatter types.
71
+ */
72
+ export function parseFrontmatterRecord(record: Record<string, string>): Record<string, unknown> {
73
+ const parsed: Record<string, unknown> = {};
74
+
75
+ for (const [key, value] of Object.entries(record)) {
76
+ parsed[key] = parseFrontmatterValue(value);
77
+ }
78
+
79
+ return parsed;
80
+ }
@@ -0,0 +1,6 @@
1
+ export * from "./child-reference";
2
+ export * from "./file";
3
+ export * from "./file-operations";
4
+ export * from "./frontmatter";
5
+ export * from "./link-parser";
6
+ export * from "./templater";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Handles different link formats and edge cases:
3
+ * [[FileName]] -> FileName.md
4
+ * [[Folder/FileName]] -> Folder/FileName.md
5
+ * [[Folder/FileName|DisplayName]] -> Folder/FileName.md
6
+ * Normalizes paths and handles malformed links gracefully.
7
+ */
8
+ export const extractFilePathFromLink = (link: string): string | null => {
9
+ const match = link.match(/\[\[([^|\]]+?)(?:\|.*?)?\]\]/);
10
+ if (!match) return null;
11
+
12
+ const filePath = match[1].trim();
13
+ if (!filePath) return null;
14
+
15
+ if (filePath.includes("[[") || filePath.includes("]]")) return null;
16
+
17
+ return filePath.endsWith(".md") ? filePath : `${filePath}.md`;
18
+ };