@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.
- package/dist/components/frontmatter-propagation-modal.d.ts +1 -0
- package/dist/components/frontmatter-propagation-modal.d.ts.map +1 -1
- package/dist/components/frontmatter-propagation-modal.js +12 -8
- package/dist/components/frontmatter-propagation-modal.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/indexer.d.ts +118 -0
- package/dist/core/indexer.d.ts.map +1 -0
- package/dist/core/indexer.js +205 -0
- package/dist/core/indexer.js.map +1 -0
- package/dist/file/frontmatter-propagation.d.ts +12 -0
- package/dist/file/frontmatter-propagation.d.ts.map +1 -0
- package/dist/file/frontmatter-propagation.js +42 -0
- package/dist/file/frontmatter-propagation.js.map +1 -0
- package/dist/file/index.d.ts +1 -0
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +1 -0
- package/dist/file/index.js.map +1 -1
- package/dist/file/templater.d.ts +11 -1
- package/dist/file/templater.d.ts.map +1 -1
- package/dist/file/templater.js +32 -1
- package/dist/file/templater.js.map +1 -1
- package/package.json +1 -1
- package/src/components/frontmatter-propagation-modal.ts +12 -8
- package/src/core/index.ts +1 -0
- package/src/core/indexer.ts +353 -0
- package/src/file/frontmatter-propagation.ts +59 -0
- package/src/file/index.ts +1 -0
- package/src/file/templater.ts +57 -1
|
@@ -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,
|
|
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"}
|
package/dist/file/templater.js
CHANGED
|
@@ -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,
|
|
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
|
@@ -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:
|
|
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({
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
73
|
+
cls: `${prefix}-change-deleted`,
|
|
70
74
|
});
|
|
71
75
|
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
|
-
const buttonContainer = contentEl.createDiv({ cls:
|
|
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
|
@@ -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";
|
package/src/file/templater.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type App, Notice, normalizePath,
|
|
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
|
+
}
|