@real1ty-obsidian-plugins/utils 2.15.0 → 2.16.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.
@@ -1 +1 @@
1
- {"version":3,"file":"templater.d.ts","sourceRoot":"","sources":["../../src/file/templater.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAyB,KAAK,KAAK,EAAE,MAAM,UAAU,CAAC;AAgCvE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAGtD;AAED,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,UAAQ,GACjB,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CA+BvB"}
1
+ {"version":3,"file":"templater.d.ts","sourceRoot":"","sources":["../../src/file/templater.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAyB,KAAK,EAAE,MAAM,UAAU,CAAC;AAelE,MAAM,WAAW,mBAAmB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAmBD,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAGtD;AAED,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,UAAQ,GACjB,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CA+BvB;AAED,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,mBAAmB,GAC1B,OAAO,CAAC,KAAK,CAAC,CAyChB"}
@@ -1,5 +1,5 @@
1
1
  import { __awaiter } from "tslib";
2
- import { Notice, normalizePath } from "obsidian";
2
+ import { Notice, normalizePath, TFile } from "obsidian";
3
3
  const TEMPLATER_ID = "templater-obsidian";
4
4
  function waitForTemplater(app_1) {
5
5
  return __awaiter(this, arguments, void 0, function* (app, timeoutMs = 8000) {
@@ -48,4 +48,35 @@ export function createFromTemplate(app_1, templatePath_1, targetFolder_1, filena
48
48
  }
49
49
  });
50
50
  }
51
+ export function createFileWithTemplate(app, options) {
52
+ return __awaiter(this, void 0, void 0, function* () {
53
+ const { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } = options;
54
+ const finalFilename = filename || title;
55
+ const baseName = finalFilename.replace(/\.md$/, "");
56
+ const filePath = normalizePath(`${targetDirectory}/${baseName}.md`);
57
+ const existingFile = app.vault.getAbstractFileByPath(filePath);
58
+ if (existingFile instanceof TFile) {
59
+ return existingFile;
60
+ }
61
+ if (useTemplater && templatePath && templatePath.trim() !== "" && isTemplaterAvailable(app)) {
62
+ const templateFile = yield createFromTemplate(app, templatePath, targetDirectory, finalFilename);
63
+ if (templateFile) {
64
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
65
+ yield app.fileManager.processFrontMatter(templateFile, (fm) => {
66
+ Object.assign(fm, frontmatter);
67
+ });
68
+ }
69
+ return templateFile;
70
+ }
71
+ }
72
+ const fileContent = content || "";
73
+ const file = yield app.vault.create(filePath, fileContent);
74
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
75
+ yield app.fileManager.processFrontMatter(file, (fm) => {
76
+ Object.assign(fm, frontmatter);
77
+ });
78
+ }
79
+ return file;
80
+ });
81
+ }
51
82
  //# sourceMappingURL=templater.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"templater.js","sourceRoot":"","sources":["../../src/file/templater.ts"],"names":[],"mappings":";AAAA,OAAO,EAAY,MAAM,EAAE,aAAa,EAAc,MAAM,UAAU,CAAC;AAEvE,MAAM,YAAY,GAAG,oBAAoB,CAAC;AAa1C,SAAe,gBAAgB;yDAAC,GAAQ,EAAE,SAAS,GAAG,IAAI;;QACzD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QAE3E,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,SAAS,EAAE,CAAC;YACzC,MAAM,IAAI,GAAQ,MAAA,MAAC,GAAW,CAAC,OAAO,0CAAE,SAAS,mDAAG,YAAY,CAAC,CAAC;YAClE,MAAM,GAAG,GAAG,MAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,SAAS,mCAAI,IAAI,CAAC;YAEpC,MAAM,QAAQ,GAAyB,MAAA,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,6BAA6B,0CAAE,IAAI,CAAC,GAAG,CAAC,CAAC;YACrF,IAAI,OAAO,QAAQ,KAAK,UAAU,EAAE,CAAC;gBACpC,OAAO,EAAE,6BAA6B,EAAE,QAAQ,EAAE,CAAC;YACpD,CAAC;YACD,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,IAAI,CAAC;IACb,CAAC;CAAA;AAED,MAAM,UAAU,oBAAoB,CAAC,GAAQ;;IAC5C,MAAM,QAAQ,GAAG,MAAA,MAAC,GAAW,CAAC,OAAO,0CAAE,SAAS,mDAAG,YAAY,CAAC,CAAC;IACjE,OAAO,CAAC,CAAC,QAAQ,CAAC;AACnB,CAAC;AAED,MAAM,UAAgB,kBAAkB;yDACvC,GAAQ,EACR,YAAoB,EACpB,YAAqB,EACrB,QAAiB,EACjB,WAAW,GAAG,KAAK;QAEnB,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC9C,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,OAAO,CAAC,IAAI,CAAC,uDAAuD,CAAC,CAAC;YACtE,IAAI,MAAM,CACT,0FAA0F,CAC1F,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;QAED,MAAM,YAAY,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC;QAC1E,IAAI,CAAC,YAAY,EAAE,CAAC;YACnB,OAAO,CAAC,KAAK,CAAC,uBAAuB,YAAY,EAAE,CAAC,CAAC;YACrD,IAAI,MAAM,CAAC,4BAA4B,YAAY,2CAA2C,CAAC,CAAC;YAChG,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,6BAA6B,CAC5D,YAAY,EACZ,YAAY,EACZ,QAAQ,EACR,WAAW,CACX,CAAC;YAEF,OAAO,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI,CAAC;QACxB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;YAC3D,IAAI,MAAM,CAAC,8EAA8E,CAAC,CAAC;YAC3F,OAAO,IAAI,CAAC;QACb,CAAC;IACF,CAAC;CAAA","sourcesContent":["import { type App, Notice, normalizePath, type TFile } from \"obsidian\";\n\nconst TEMPLATER_ID = \"templater-obsidian\";\n\ntype CreateFn = (\n\ttemplateFile: TFile,\n\tfolder?: string,\n\tfilename?: string,\n\topenNewNote?: boolean\n) => Promise<TFile | undefined>;\n\ninterface TemplaterLike {\n\tcreate_new_note_from_template: CreateFn;\n}\n\nasync function waitForTemplater(app: App, timeoutMs = 8000): Promise<TemplaterLike | null> {\n\tawait new Promise<void>((resolve) => app.workspace.onLayoutReady(resolve));\n\n\tconst started = Date.now();\n\twhile (Date.now() - started < timeoutMs) {\n\t\tconst plug: any = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);\n\t\tconst api = plug?.templater ?? null;\n\n\t\tconst createFn: CreateFn | undefined = api?.create_new_note_from_template?.bind(api);\n\t\tif (typeof createFn === \"function\") {\n\t\t\treturn { create_new_note_from_template: createFn };\n\t\t}\n\t\tawait new Promise((r) => setTimeout(r, 150));\n\t}\n\treturn null;\n}\n\nexport function isTemplaterAvailable(app: App): boolean {\n\tconst instance = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);\n\treturn !!instance;\n}\n\nexport async function createFromTemplate(\n\tapp: App,\n\ttemplatePath: string,\n\ttargetFolder?: string,\n\tfilename?: string,\n\topenNewNote = false\n): Promise<TFile | null> {\n\tconst templater = await waitForTemplater(app);\n\tif (!templater) {\n\t\tconsole.warn(\"Templater isn't ready yet (or not installed/enabled).\");\n\t\tnew Notice(\n\t\t\t\"Templater plugin is not available or enabled. Please ensure it is installed and enabled.\"\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst templateFile = app.vault.getFileByPath(normalizePath(templatePath));\n\tif (!templateFile) {\n\t\tconsole.error(`Template not found: ${templatePath}`);\n\t\tnew Notice(`Template file not found: ${templatePath}. Please ensure the template file exists.`);\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst newFile = await templater.create_new_note_from_template(\n\t\t\ttemplateFile,\n\t\t\ttargetFolder,\n\t\t\tfilename,\n\t\t\topenNewNote\n\t\t);\n\n\t\treturn newFile ?? null;\n\t} catch (error) {\n\t\tconsole.error(\"Error creating file from template:\", error);\n\t\tnew Notice(\"Error creating file from template. Please ensure the template file is valid.\");\n\t\treturn null;\n\t}\n}\n"]}
1
+ {"version":3,"file":"templater.js","sourceRoot":"","sources":["../../src/file/templater.ts"],"names":[],"mappings":"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","sourcesContent":["import { type App, Notice, normalizePath, TFile } from \"obsidian\";\n\nconst TEMPLATER_ID = \"templater-obsidian\";\n\ntype CreateFn = (\n\ttemplateFile: TFile,\n\tfolder?: string,\n\tfilename?: string,\n\topenNewNote?: boolean\n) => Promise<TFile | undefined>;\n\ninterface TemplaterLike {\n\tcreate_new_note_from_template: CreateFn;\n}\n\nexport interface FileCreationOptions {\n\ttitle: string;\n\ttargetDirectory: string;\n\tfilename?: string;\n\tcontent?: string;\n\tfrontmatter?: Record<string, unknown>;\n\ttemplatePath?: string;\n\tuseTemplater?: boolean;\n}\n\nasync function waitForTemplater(app: App, timeoutMs = 8000): Promise<TemplaterLike | null> {\n\tawait new Promise<void>((resolve) => app.workspace.onLayoutReady(resolve));\n\n\tconst started = Date.now();\n\twhile (Date.now() - started < timeoutMs) {\n\t\tconst plug: any = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);\n\t\tconst api = plug?.templater ?? null;\n\n\t\tconst createFn: CreateFn | undefined = api?.create_new_note_from_template?.bind(api);\n\t\tif (typeof createFn === \"function\") {\n\t\t\treturn { create_new_note_from_template: createFn };\n\t\t}\n\t\tawait new Promise((r) => setTimeout(r, 150));\n\t}\n\treturn null;\n}\n\nexport function isTemplaterAvailable(app: App): boolean {\n\tconst instance = (app as any).plugins?.getPlugin?.(TEMPLATER_ID);\n\treturn !!instance;\n}\n\nexport async function createFromTemplate(\n\tapp: App,\n\ttemplatePath: string,\n\ttargetFolder?: string,\n\tfilename?: string,\n\topenNewNote = false\n): Promise<TFile | null> {\n\tconst templater = await waitForTemplater(app);\n\tif (!templater) {\n\t\tconsole.warn(\"Templater isn't ready yet (or not installed/enabled).\");\n\t\tnew Notice(\n\t\t\t\"Templater plugin is not available or enabled. Please ensure it is installed and enabled.\"\n\t\t);\n\t\treturn null;\n\t}\n\n\tconst templateFile = app.vault.getFileByPath(normalizePath(templatePath));\n\tif (!templateFile) {\n\t\tconsole.error(`Template not found: ${templatePath}`);\n\t\tnew Notice(`Template file not found: ${templatePath}. Please ensure the template file exists.`);\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst newFile = await templater.create_new_note_from_template(\n\t\t\ttemplateFile,\n\t\t\ttargetFolder,\n\t\t\tfilename,\n\t\t\topenNewNote\n\t\t);\n\n\t\treturn newFile ?? null;\n\t} catch (error) {\n\t\tconsole.error(\"Error creating file from template:\", error);\n\t\tnew Notice(\"Error creating file from template. Please ensure the template file is valid.\");\n\t\treturn null;\n\t}\n}\n\nexport async function createFileWithTemplate(\n\tapp: App,\n\toptions: FileCreationOptions\n): Promise<TFile> {\n\tconst { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } =\n\t\toptions;\n\n\tconst finalFilename = filename || title;\n\tconst baseName = finalFilename.replace(/\\.md$/, \"\");\n\tconst filePath = normalizePath(`${targetDirectory}/${baseName}.md`);\n\n\tconst existingFile = app.vault.getAbstractFileByPath(filePath);\n\tif (existingFile instanceof TFile) {\n\t\treturn existingFile;\n\t}\n\n\tif (useTemplater && templatePath && templatePath.trim() !== \"\" && isTemplaterAvailable(app)) {\n\t\tconst templateFile = await createFromTemplate(\n\t\t\tapp,\n\t\t\ttemplatePath,\n\t\t\ttargetDirectory,\n\t\t\tfinalFilename\n\t\t);\n\n\t\tif (templateFile) {\n\t\t\tif (frontmatter && Object.keys(frontmatter).length > 0) {\n\t\t\t\tawait app.fileManager.processFrontMatter(templateFile, (fm) => {\n\t\t\t\t\tObject.assign(fm, frontmatter);\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn templateFile;\n\t\t}\n\t}\n\n\tconst fileContent = content || \"\";\n\tconst file = await app.vault.create(filePath, fileContent);\n\n\tif (frontmatter && Object.keys(frontmatter).length > 0) {\n\t\tawait app.fileManager.processFrontMatter(file, (fm) => {\n\t\t\tObject.assign(fm, frontmatter);\n\t\t});\n\t}\n\n\treturn file;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real1ty-obsidian-plugins/utils",
3
- "version": "2.15.0",
3
+ "version": "2.16.0",
4
4
  "description": "Shared utilities for Obsidian plugins",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,6 +9,7 @@ export interface FrontmatterPropagationModalOptions {
9
9
  instanceCount: number;
10
10
  onConfirm: () => void | Promise<void>;
11
11
  onCancel?: () => void | Promise<void>;
12
+ cssPrefix?: string;
12
13
  }
13
14
 
14
15
  export class FrontmatterPropagationModal extends Modal {
@@ -21,6 +22,7 @@ export class FrontmatterPropagationModal extends Modal {
21
22
 
22
23
  onOpen(): void {
23
24
  const { contentEl } = this;
25
+ const prefix = this.options.cssPrefix ?? "frontmatter-propagation";
24
26
 
25
27
  contentEl.empty();
26
28
 
@@ -30,48 +32,50 @@ export class FrontmatterPropagationModal extends Modal {
30
32
  text: `The recurring event "${this.options.eventTitle}" has frontmatter changes. Do you want to apply these changes to all ${this.options.instanceCount} physical instances?`,
31
33
  });
32
34
 
33
- const changesContainer = contentEl.createDiv({ cls: "prisma-frontmatter-changes" });
35
+ const changesContainer = contentEl.createDiv({ cls: `${prefix}-frontmatter-changes` });
34
36
 
35
37
  if (this.options.diff.added.length > 0) {
36
- const addedSection = changesContainer.createDiv({ cls: "prisma-changes-section" });
38
+ const addedSection = changesContainer.createDiv({
39
+ cls: `${prefix}-frontmatter-changes-section`,
40
+ });
37
41
  addedSection.createEl("h4", { text: "Added properties:" });
38
42
  const addedList = addedSection.createEl("ul");
39
43
 
40
44
  for (const change of this.options.diff.added) {
41
45
  addedList.createEl("li", {
42
46
  text: formatChangeForDisplay(change),
43
- cls: "prisma-change-added",
47
+ cls: `${prefix}-change-added`,
44
48
  });
45
49
  }
46
50
  }
47
51
 
48
52
  if (this.options.diff.modified.length > 0) {
49
- const modifiedSection = changesContainer.createDiv({ cls: "prisma-changes-section" });
53
+ const modifiedSection = changesContainer.createDiv({ cls: `${prefix}-changes-section` });
50
54
  modifiedSection.createEl("h4", { text: "Modified properties:" });
51
55
  const modifiedList = modifiedSection.createEl("ul");
52
56
 
53
57
  for (const change of this.options.diff.modified) {
54
58
  modifiedList.createEl("li", {
55
59
  text: formatChangeForDisplay(change),
56
- cls: "prisma-change-modified",
60
+ cls: `${prefix}-change-modified`,
57
61
  });
58
62
  }
59
63
  }
60
64
 
61
65
  if (this.options.diff.deleted.length > 0) {
62
- const deletedSection = changesContainer.createDiv({ cls: "prisma-changes-section" });
66
+ const deletedSection = changesContainer.createDiv({ cls: `${prefix}-changes-section` });
63
67
  deletedSection.createEl("h4", { text: "Deleted properties:" });
64
68
  const deletedList = deletedSection.createEl("ul");
65
69
 
66
70
  for (const change of this.options.diff.deleted) {
67
71
  deletedList.createEl("li", {
68
72
  text: formatChangeForDisplay(change),
69
- cls: "prisma-change-deleted",
73
+ cls: `${prefix}-change-deleted`,
70
74
  });
71
75
  }
72
76
  }
73
77
 
74
- const buttonContainer = contentEl.createDiv({ cls: "prisma-modal-buttons" });
78
+ const buttonContainer = contentEl.createDiv({ cls: `${prefix}-modal-buttons` });
75
79
 
76
80
  const yesButton = buttonContainer.createEl("button", {
77
81
  text: "Yes, propagate",
package/src/core/index.ts CHANGED
@@ -4,5 +4,6 @@ export * from "./evaluator";
4
4
  export * from "./expression-utils";
5
5
  export * from "./frontmatter-value";
6
6
  export * from "./generate";
7
+ export * from "./indexer";
7
8
  export * from "./property-renderer";
8
9
  export * from "./validation";
@@ -0,0 +1,353 @@
1
+ import { type App, type MetadataCache, type TAbstractFile, TFile, type Vault } from "obsidian";
2
+ import {
3
+ type BehaviorSubject,
4
+ from,
5
+ fromEventPattern,
6
+ lastValueFrom,
7
+ merge,
8
+ type Observable,
9
+ of,
10
+ BehaviorSubject as RxBehaviorSubject,
11
+ Subject,
12
+ type Subscription,
13
+ } from "rxjs";
14
+ import { debounceTime, filter, groupBy, map, mergeMap, switchMap, toArray } from "rxjs/operators";
15
+ import { isFileInConfiguredDirectory } from "../file/file";
16
+ import { compareFrontmatter, type FrontmatterDiff } from "../file/frontmatter-diff";
17
+
18
+ /**
19
+ * Generic frontmatter object type for indexer
20
+ */
21
+ export type IndexerFrontmatter = Record<string, unknown>;
22
+
23
+ /**
24
+ * Configuration for the generic indexer
25
+ */
26
+ export interface IndexerConfig {
27
+ /**
28
+ * Directory to scan for files (e.g., "Calendar", "Notes")
29
+ * If empty string or undefined, scans entire vault
30
+ */
31
+ directory?: string;
32
+
33
+ /**
34
+ * Properties to exclude when comparing frontmatter diffs
35
+ */
36
+ excludedDiffProps?: Set<string>;
37
+
38
+ /**
39
+ * Concurrency limit for file scanning operations
40
+ */
41
+ scanConcurrency?: number;
42
+
43
+ /**
44
+ * Debounce time in milliseconds for file change events
45
+ */
46
+ debounceMs?: number;
47
+ }
48
+
49
+ /**
50
+ * Raw file source with frontmatter and metadata
51
+ */
52
+ export interface FileSource {
53
+ filePath: string;
54
+ mtime: number;
55
+ frontmatter: IndexerFrontmatter;
56
+ folder: string;
57
+ }
58
+
59
+ /**
60
+ * Types of indexer events
61
+ */
62
+ export type IndexerEventType = "file-changed" | "file-deleted";
63
+
64
+ /**
65
+ * Generic indexer event
66
+ */
67
+ export interface IndexerEvent {
68
+ type: IndexerEventType;
69
+ filePath: string;
70
+ oldPath?: string;
71
+ source?: FileSource;
72
+ oldFrontmatter?: IndexerFrontmatter;
73
+ frontmatterDiff?: FrontmatterDiff;
74
+ }
75
+
76
+ type VaultEvent = "create" | "modify" | "delete" | "rename";
77
+
78
+ type FileIntent =
79
+ | { kind: "changed"; file: TFile; path: string; oldPath?: string }
80
+ | { kind: "deleted"; path: string };
81
+
82
+ /**
83
+ * Generic indexer that listens to Obsidian vault events and emits
84
+ * RxJS observables with frontmatter diffs and metadata.
85
+ *
86
+ * This indexer is framework-agnostic and can be used by any plugin
87
+ * that needs to track file changes with frontmatter.
88
+ */
89
+ export class Indexer {
90
+ private config: Required<IndexerConfig>;
91
+ private fileSub: Subscription | null = null;
92
+ private configSubscription: Subscription | null = null;
93
+ private vault: Vault;
94
+ private metadataCache: MetadataCache;
95
+ private scanEventsSubject = new Subject<IndexerEvent>();
96
+ private indexingCompleteSubject = new RxBehaviorSubject<boolean>(false);
97
+ private frontmatterCache: Map<string, IndexerFrontmatter> = new Map();
98
+
99
+ public readonly events$: Observable<IndexerEvent>;
100
+ public readonly indexingComplete$: Observable<boolean>;
101
+
102
+ constructor(_app: App, configStore: BehaviorSubject<IndexerConfig>) {
103
+ this.vault = _app.vault;
104
+ this.metadataCache = _app.metadataCache;
105
+
106
+ // Set defaults
107
+ this.config = {
108
+ directory: configStore.value.directory || "",
109
+ excludedDiffProps: configStore.value.excludedDiffProps || new Set(),
110
+ scanConcurrency: configStore.value.scanConcurrency || 10,
111
+ debounceMs: configStore.value.debounceMs || 100,
112
+ };
113
+
114
+ // Subscribe to config changes
115
+ this.configSubscription = configStore.subscribe((newConfig) => {
116
+ const directoryChanged = this.config.directory !== (newConfig.directory || "");
117
+
118
+ this.config = {
119
+ directory: newConfig.directory || "",
120
+ excludedDiffProps: newConfig.excludedDiffProps || new Set(),
121
+ scanConcurrency: newConfig.scanConcurrency || 10,
122
+ debounceMs: newConfig.debounceMs || 100,
123
+ };
124
+
125
+ // Rescan if directory changed
126
+ if (directoryChanged) {
127
+ this.indexingCompleteSubject.next(false);
128
+ void this.scanAllFiles();
129
+ }
130
+ });
131
+
132
+ this.events$ = this.scanEventsSubject.asObservable();
133
+ this.indexingComplete$ = this.indexingCompleteSubject.asObservable();
134
+ }
135
+
136
+ /**
137
+ * Start the indexer and perform initial scan
138
+ */
139
+ async start(): Promise<void> {
140
+ this.indexingCompleteSubject.next(false);
141
+
142
+ const fileSystemEvents$ = this.buildFileSystemEvents$();
143
+
144
+ this.fileSub = fileSystemEvents$.subscribe((event) => {
145
+ this.scanEventsSubject.next(event);
146
+ });
147
+
148
+ await this.scanAllFiles();
149
+ }
150
+
151
+ /**
152
+ * Stop the indexer and clean up subscriptions
153
+ */
154
+ stop(): void {
155
+ this.fileSub?.unsubscribe();
156
+ this.fileSub = null;
157
+ this.configSubscription?.unsubscribe();
158
+ this.configSubscription = null;
159
+ this.indexingCompleteSubject.complete();
160
+ }
161
+
162
+ /**
163
+ * Clear cache and rescan all files
164
+ */
165
+ resync(): void {
166
+ this.frontmatterCache.clear();
167
+ this.indexingCompleteSubject.next(false);
168
+ void this.scanAllFiles();
169
+ }
170
+
171
+ /**
172
+ * Scan all markdown files in the configured directory
173
+ */
174
+ private async scanAllFiles(): Promise<void> {
175
+ const allFiles = this.vault.getMarkdownFiles();
176
+ const relevantFiles = allFiles.filter((file) => this.isRelevantFile(file));
177
+
178
+ const events$ = from(relevantFiles).pipe(
179
+ mergeMap(async (file) => {
180
+ try {
181
+ return await this.buildEvent(file);
182
+ } catch (error) {
183
+ console.error(`Error processing file ${file.path}:`, error);
184
+ return null;
185
+ }
186
+ }, this.config.scanConcurrency),
187
+ filter((event): event is IndexerEvent => event !== null),
188
+ toArray()
189
+ );
190
+
191
+ try {
192
+ const allEvents = await lastValueFrom(events$);
193
+
194
+ for (const event of allEvents) {
195
+ this.scanEventsSubject.next(event);
196
+ }
197
+
198
+ this.indexingCompleteSubject.next(true);
199
+ } catch (error) {
200
+ console.error("❌ Error during file scanning:", error);
201
+ this.indexingCompleteSubject.next(true);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Create an observable from a vault event
207
+ */
208
+ private fromVaultEvent(eventName: VaultEvent): Observable<TAbstractFile> {
209
+ if (eventName === "create") {
210
+ return fromEventPattern<TAbstractFile>(
211
+ (handler) => this.vault.on("create", handler),
212
+ (handler) => this.vault.off("create", handler)
213
+ );
214
+ }
215
+
216
+ if (eventName === "modify") {
217
+ return fromEventPattern<TAbstractFile>(
218
+ (handler) => this.vault.on("modify", handler),
219
+ (handler) => this.vault.off("modify", handler)
220
+ );
221
+ }
222
+
223
+ if (eventName === "delete") {
224
+ return fromEventPattern<TAbstractFile>(
225
+ (handler) => this.vault.on("delete", handler),
226
+ (handler) => this.vault.off("delete", handler)
227
+ );
228
+ }
229
+
230
+ // eventName === "rename"
231
+ return fromEventPattern<[TAbstractFile, string]>(
232
+ (handler) => this.vault.on("rename", handler),
233
+ (handler) => this.vault.off("rename", handler)
234
+ ).pipe(map(([file]) => file));
235
+ }
236
+
237
+ /**
238
+ * Type guard to check if file is a markdown file
239
+ */
240
+ private static isMarkdownFile(f: TAbstractFile): f is TFile {
241
+ return f instanceof TFile && f.extension === "md";
242
+ }
243
+
244
+ /**
245
+ * Filter to only relevant markdown files in configured directory
246
+ */
247
+ private toRelevantFiles<T extends TAbstractFile>() {
248
+ return (source: Observable<T>) =>
249
+ source.pipe(
250
+ filter((f: TAbstractFile): f is TFile => Indexer.isMarkdownFile(f)),
251
+ filter((f) => this.isRelevantFile(f))
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Debounce events by file path
257
+ */
258
+ private debounceByPath<T>(ms: number, key: (x: T) => string) {
259
+ return (source: Observable<T>) =>
260
+ source.pipe(
261
+ groupBy(key),
262
+ mergeMap((g$) => g$.pipe(debounceTime(ms)))
263
+ );
264
+ }
265
+
266
+ /**
267
+ * Build the file system events observable stream
268
+ */
269
+ private buildFileSystemEvents$(): Observable<IndexerEvent> {
270
+ const created$ = this.fromVaultEvent("create").pipe(this.toRelevantFiles());
271
+ const modified$ = this.fromVaultEvent("modify").pipe(this.toRelevantFiles());
272
+ const deleted$ = this.fromVaultEvent("delete").pipe(this.toRelevantFiles());
273
+
274
+ const renamed$ = fromEventPattern<[TAbstractFile, string]>(
275
+ (handler) => this.vault.on("rename", handler),
276
+ (handler) => this.vault.off("rename", handler)
277
+ );
278
+
279
+ const changedIntents$ = merge(created$, modified$).pipe(
280
+ this.debounceByPath(this.config.debounceMs, (f) => f.path),
281
+ map((file): FileIntent => ({ kind: "changed", file, path: file.path }))
282
+ );
283
+
284
+ const deletedIntents$ = deleted$.pipe(
285
+ map((file): FileIntent => ({ kind: "deleted", path: file.path }))
286
+ );
287
+
288
+ const renamedIntents$ = renamed$.pipe(
289
+ map(([f, oldPath]) => [f, oldPath] as const),
290
+ filter(([f]) => Indexer.isMarkdownFile(f) && this.isRelevantFile(f)),
291
+ mergeMap(([f, oldPath]) => [
292
+ { kind: "deleted", path: oldPath } as FileIntent,
293
+ { kind: "changed", file: f, path: f.path, oldPath } as FileIntent,
294
+ ])
295
+ );
296
+
297
+ const intents$ = merge(changedIntents$, deletedIntents$, renamedIntents$);
298
+
299
+ return intents$.pipe(
300
+ switchMap((intent) => {
301
+ if (intent.kind === "deleted") {
302
+ this.frontmatterCache.delete(intent.path);
303
+ return of<IndexerEvent>({ type: "file-deleted", filePath: intent.path });
304
+ }
305
+
306
+ return from(this.buildEvent(intent.file, intent.oldPath)).pipe(
307
+ filter((e): e is IndexerEvent => e !== null)
308
+ );
309
+ })
310
+ );
311
+ }
312
+
313
+ /**
314
+ * Build an indexer event from a file
315
+ */
316
+ private async buildEvent(file: TFile, oldPath?: string): Promise<IndexerEvent | null> {
317
+ const cache = this.metadataCache.getFileCache(file);
318
+ if (!cache || !cache.frontmatter) return null;
319
+
320
+ const { frontmatter } = cache;
321
+ const oldFrontmatter = this.frontmatterCache.get(file.path);
322
+
323
+ const source: FileSource = {
324
+ filePath: file.path,
325
+ mtime: file.stat.mtime,
326
+ frontmatter,
327
+ folder: file.parent?.path || "",
328
+ };
329
+
330
+ const event: IndexerEvent = {
331
+ type: "file-changed",
332
+ filePath: file.path,
333
+ oldPath,
334
+ source,
335
+ oldFrontmatter,
336
+ frontmatterDiff: oldFrontmatter
337
+ ? compareFrontmatter(oldFrontmatter, frontmatter, this.config.excludedDiffProps)
338
+ : undefined,
339
+ };
340
+
341
+ // Update cache
342
+ this.frontmatterCache.set(file.path, { ...frontmatter });
343
+
344
+ return event;
345
+ }
346
+
347
+ /**
348
+ * Check if file is in the configured directory
349
+ */
350
+ private isRelevantFile(file: TFile): boolean {
351
+ return isFileInConfiguredDirectory(file.path, this.config.directory);
352
+ }
353
+ }
@@ -0,0 +1,59 @@
1
+ import type { App } from "obsidian";
2
+ import { TFile } from "obsidian";
3
+ import type { Frontmatter, FrontmatterDiff } from "./frontmatter-diff";
4
+
5
+ export interface NexusPropertiesSettings {
6
+ excludedPropagatedProps?: string;
7
+ parentProp: string;
8
+ childrenProp: string;
9
+ relatedProp: string;
10
+ zettelIdProp: string;
11
+ }
12
+
13
+ export function parseExcludedProps(settings: NexusPropertiesSettings): Set<string> {
14
+ const excludedPropsStr = settings.excludedPropagatedProps || "";
15
+ const userExcluded = excludedPropsStr
16
+ .split(",")
17
+ .map((prop) => prop.trim())
18
+ .filter((prop) => prop.length > 0);
19
+
20
+ const alwaysExcluded = [
21
+ settings.parentProp,
22
+ settings.childrenProp,
23
+ settings.relatedProp,
24
+ settings.zettelIdProp,
25
+ ];
26
+
27
+ return new Set([...alwaysExcluded, ...userExcluded]);
28
+ }
29
+
30
+ export async function applyFrontmatterChanges(
31
+ app: App,
32
+ targetPath: string,
33
+ sourceFrontmatter: Frontmatter,
34
+ diff: FrontmatterDiff
35
+ ): Promise<void> {
36
+ try {
37
+ const file = app.vault.getAbstractFileByPath(targetPath);
38
+ if (!(file instanceof TFile)) {
39
+ console.warn(`Target file not found: ${targetPath}`);
40
+ return;
41
+ }
42
+
43
+ await app.fileManager.processFrontMatter(file, (fm) => {
44
+ for (const change of diff.added) {
45
+ fm[change.key] = sourceFrontmatter[change.key];
46
+ }
47
+
48
+ for (const change of diff.modified) {
49
+ fm[change.key] = sourceFrontmatter[change.key];
50
+ }
51
+
52
+ for (const change of diff.deleted) {
53
+ delete fm[change.key];
54
+ }
55
+ });
56
+ } catch (error) {
57
+ console.error(`Error applying frontmatter changes to ${targetPath}:`, error);
58
+ }
59
+ }
package/src/file/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./file-operations";
4
4
  export * from "./file-utils";
5
5
  export * from "./frontmatter";
6
6
  export * from "./frontmatter-diff";
7
+ export * from "./frontmatter-propagation";
7
8
  export * from "./link-parser";
8
9
  export * from "./property-utils";
9
10
  export * from "./templater";
@@ -1,4 +1,4 @@
1
- import { type App, Notice, normalizePath, type TFile } from "obsidian";
1
+ import { type App, Notice, normalizePath, TFile } from "obsidian";
2
2
 
3
3
  const TEMPLATER_ID = "templater-obsidian";
4
4
 
@@ -13,6 +13,16 @@ interface TemplaterLike {
13
13
  create_new_note_from_template: CreateFn;
14
14
  }
15
15
 
16
+ export interface FileCreationOptions {
17
+ title: string;
18
+ targetDirectory: string;
19
+ filename?: string;
20
+ content?: string;
21
+ frontmatter?: Record<string, unknown>;
22
+ templatePath?: string;
23
+ useTemplater?: boolean;
24
+ }
25
+
16
26
  async function waitForTemplater(app: App, timeoutMs = 8000): Promise<TemplaterLike | null> {
17
27
  await new Promise<void>((resolve) => app.workspace.onLayoutReady(resolve));
18
28
 
@@ -73,3 +83,49 @@ export async function createFromTemplate(
73
83
  return null;
74
84
  }
75
85
  }
86
+
87
+ export async function createFileWithTemplate(
88
+ app: App,
89
+ options: FileCreationOptions
90
+ ): Promise<TFile> {
91
+ const { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } =
92
+ options;
93
+
94
+ const finalFilename = filename || title;
95
+ const baseName = finalFilename.replace(/\.md$/, "");
96
+ const filePath = normalizePath(`${targetDirectory}/${baseName}.md`);
97
+
98
+ const existingFile = app.vault.getAbstractFileByPath(filePath);
99
+ if (existingFile instanceof TFile) {
100
+ return existingFile;
101
+ }
102
+
103
+ if (useTemplater && templatePath && templatePath.trim() !== "" && isTemplaterAvailable(app)) {
104
+ const templateFile = await createFromTemplate(
105
+ app,
106
+ templatePath,
107
+ targetDirectory,
108
+ finalFilename
109
+ );
110
+
111
+ if (templateFile) {
112
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
113
+ await app.fileManager.processFrontMatter(templateFile, (fm) => {
114
+ Object.assign(fm, frontmatter);
115
+ });
116
+ }
117
+ return templateFile;
118
+ }
119
+ }
120
+
121
+ const fileContent = content || "";
122
+ const file = await app.vault.create(filePath, fileContent);
123
+
124
+ if (frontmatter && Object.keys(frontmatter).length > 0) {
125
+ await app.fileManager.processFrontMatter(file, (fm) => {
126
+ Object.assign(fm, frontmatter);
127
+ });
128
+ }
129
+
130
+ return file;
131
+ }