@real1ty-obsidian-plugins/utils 2.21.0 → 2.23.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/file/file-utils.d.ts +5 -0
- package/dist/file/file-utils.d.ts.map +1 -1
- package/dist/file/file-utils.js +21 -0
- package/dist/file/file-utils.js.map +1 -1
- package/dist/file/file.d.ts +19 -0
- package/dist/file/file.d.ts.map +1 -1
- package/dist/file/file.js +40 -6
- package/dist/file/file.js.map +1 -1
- package/dist/file/frontmatter-serialization.d.ts +21 -0
- package/dist/file/frontmatter-serialization.d.ts.map +1 -0
- package/dist/file/frontmatter-serialization.js +57 -0
- package/dist/file/frontmatter-serialization.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-service.d.ts +1 -3
- package/dist/file/templater-service.d.ts.map +1 -1
- package/dist/file/templater-service.js +5 -38
- package/dist/file/templater-service.js.map +1 -1
- package/dist/file/templater.d.ts +15 -1
- package/dist/file/templater.d.ts.map +1 -1
- package/dist/file/templater.js +66 -22
- package/dist/file/templater.js.map +1 -1
- package/package.json +2 -1
- package/src/file/file-utils.ts +26 -0
- package/src/file/file.ts +46 -8
- package/src/file/frontmatter-serialization.ts +72 -0
- package/src/file/index.ts +1 -0
- package/src/file/templater-service.ts +14 -51
- package/src/file/templater.ts +88 -25
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { App } from "obsidian";
|
|
2
2
|
import { TFile } from "obsidian";
|
|
3
|
+
/**
|
|
4
|
+
* Waits for a file to be accessible and readable by Obsidian's cache.
|
|
5
|
+
* This is necessary because Templater creates files asynchronously.
|
|
6
|
+
*/
|
|
7
|
+
export declare function waitForFileReady(app: App, filePath: string, timeoutMs?: number): Promise<TFile | null>;
|
|
3
8
|
/**
|
|
4
9
|
* Gets a TFile by path or throws an error if not found.
|
|
5
10
|
* Useful when you need to ensure a file exists before proceeding.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/file/file-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,EAAE,MAAM,MAAM,KAAG,KAIxD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,GAAG,EACR,MAAM,KAAK,EACX,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,kBACO,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,KAAK,GAAG,EAAE,MAAM,KAAK,qCAM5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC9B,KAAK,GAAG,EACR,MAAM,KAAK,EACX,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAO/B,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,8BAA8B,GAAI,aAAa,MAAM,KAAG,MAWpE,CAAC"}
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../../src/file/file-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AACpC,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,SAAS,SAAO,GACd,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAgBvB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,GAAG,EAAE,MAAM,MAAM,KAAG,KAIxD,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC3B,KAAK,GAAG,EACR,MAAM,KAAK,EACX,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,kBACO,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAU,KAAK,GAAG,EAAE,MAAM,KAAK,qCAM5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC9B,KAAK,GAAG,EACR,MAAM,KAAK,EACX,UAAU,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,kBAO/B,CAAC;AAEJ;;;GAGG;AACH,eAAO,MAAM,8BAA8B,GAAI,aAAa,MAAM,KAAG,MAWpE,CAAC"}
|
package/dist/file/file-utils.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
2
|
import { TFile } from "obsidian";
|
|
3
|
+
/**
|
|
4
|
+
* Waits for a file to be accessible and readable by Obsidian's cache.
|
|
5
|
+
* This is necessary because Templater creates files asynchronously.
|
|
6
|
+
*/
|
|
7
|
+
export function waitForFileReady(app_1, filePath_1) {
|
|
8
|
+
return __awaiter(this, arguments, void 0, function* (app, filePath, timeoutMs = 5000) {
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
while (Date.now() - started < timeoutMs) {
|
|
11
|
+
const file = app.vault.getAbstractFileByPath(filePath);
|
|
12
|
+
if (file instanceof TFile) {
|
|
13
|
+
// Check if file is readable by trying to get its metadata
|
|
14
|
+
const metadata = app.metadataCache.getFileCache(file);
|
|
15
|
+
if (metadata !== null && metadata !== undefined) {
|
|
16
|
+
return file;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
yield new Promise((r) => setTimeout(r, 50));
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
3
24
|
/**
|
|
4
25
|
* Gets a TFile by path or throws an error if not found.
|
|
5
26
|
* Useful when you need to ensure a file exists before proceeding.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-utils.js","sourceRoot":"","sources":["../../src/file/file-utils.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEjC;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,GAAQ,EAAE,IAAY,EAAS,EAAE;IAChE,MAAM,CAAC,GAAG,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAChD,IAAI,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,CAAC;AACV,CAAC,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAC9B,GAAQ,EACR,IAAW,EACX,MAA6C,EAC5C,EAAE,kDAAC,OAAA,GAAG,CAAC,WAAW,CAAC,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA,GAAA,CAAC;AAEtD;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAO,GAAQ,EAAE,IAAW,EAAE,EAAE;IAChE,IAAI,IAAI,GAA4B,EAAE,CAAC;IACvC,MAAM,eAAe,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE;QACvC,IAAI,qBAAQ,EAAE,CAAE,CAAC;IAClB,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACb,CAAC,CAAA,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CACjC,GAAQ,EACR,IAAW,EACX,QAAiC,EAChC,EAAE;IACH,OAAA,eAAe,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE;QACjC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACd,CAAC;QACD,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAA;EAAA,CAAC;AAEJ;;;GAGG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,WAAmB,EAAU,EAAE;IAC7E,MAAM,gBAAgB,GAAG,+BAA+B,CAAC;IACzD,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAElD,IAAI,KAAK,EAAE,CAAC;QACX,mCAAmC;QACnC,OAAO,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,KAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;IAC9D,CAAC;IAED,qDAAqD;IACrD,OAAO,WAAW,CAAC;AACpB,CAAC,CAAC","sourcesContent":["import type { App } from \"obsidian\";\nimport { TFile } from \"obsidian\";\n\n/**\n * Gets a TFile by path or throws an error if not found.\n * Useful when you need to ensure a file exists before proceeding.\n */\nexport const getTFileOrThrow = (app: App, path: string): TFile => {\n\tconst f = app.vault.getAbstractFileByPath(path);\n\tif (!(f instanceof TFile)) throw new Error(`File not found: ${path}`);\n\treturn f;\n};\n\n/**\n * Executes an operation on a file's frontmatter.\n * Wrapper around Obsidian's processFrontMatter for more concise usage.\n */\nexport const withFrontmatter = async (\n\tapp: App,\n\tfile: TFile,\n\tupdate: (fm: Record<string, unknown>) => void\n) => app.fileManager.processFrontMatter(file, update);\n\n/**\n * Creates a backup copy of a file's frontmatter.\n * Useful for undo/redo operations or temporary modifications.\n */\nexport const backupFrontmatter = async (app: App, file: TFile) => {\n\tlet copy: Record<string, unknown> = {};\n\tawait withFrontmatter(app, file, (fm) => {\n\t\tcopy = { ...fm };\n\t});\n\treturn copy;\n};\n\n/**\n * Restores a file's frontmatter from a backup.\n * Clears existing frontmatter and replaces with the backup.\n */\nexport const restoreFrontmatter = async (\n\tapp: App,\n\tfile: TFile,\n\toriginal: Record<string, unknown>\n) =>\n\twithFrontmatter(app, file, (fm) => {\n\t\tfor (const k of Object.keys(fm)) {\n\t\t\tdelete fm[k];\n\t\t}\n\t\tObject.assign(fm, original);\n\t});\n\n/**\n * Extracts the content that appears after the frontmatter section.\n * Returns the entire content if no frontmatter is found.\n */\nexport const extractContentAfterFrontmatter = (fullContent: string): string => {\n\tconst frontmatterRegex = /^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/;\n\tconst match = fullContent.match(frontmatterRegex);\n\n\tif (match) {\n\t\t// Return content after frontmatter\n\t\treturn fullContent.substring(match.index! + match[0].length);\n\t}\n\n\t// If no frontmatter found, return the entire content\n\treturn fullContent;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"file-utils.js","sourceRoot":"","sources":["../../src/file/file-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { App } from \"obsidian\";\nimport { TFile } from \"obsidian\";\n\n/**\n * Waits for a file to be accessible and readable by Obsidian's cache.\n * This is necessary because Templater creates files asynchronously.\n */\nexport async function waitForFileReady(\n\tapp: App,\n\tfilePath: string,\n\ttimeoutMs = 5000\n): Promise<TFile | null> {\n\tconst started = Date.now();\n\n\twhile (Date.now() - started < timeoutMs) {\n\t\tconst file = app.vault.getAbstractFileByPath(filePath);\n\t\tif (file instanceof TFile) {\n\t\t\t// Check if file is readable by trying to get its metadata\n\t\t\tconst metadata = app.metadataCache.getFileCache(file);\n\t\t\tif (metadata !== null && metadata !== undefined) {\n\t\t\t\treturn file;\n\t\t\t}\n\t\t}\n\t\tawait new Promise((r) => setTimeout(r, 50));\n\t}\n\n\treturn null;\n}\n\n/**\n * Gets a TFile by path or throws an error if not found.\n * Useful when you need to ensure a file exists before proceeding.\n */\nexport const getTFileOrThrow = (app: App, path: string): TFile => {\n\tconst f = app.vault.getAbstractFileByPath(path);\n\tif (!(f instanceof TFile)) throw new Error(`File not found: ${path}`);\n\treturn f;\n};\n\n/**\n * Executes an operation on a file's frontmatter.\n * Wrapper around Obsidian's processFrontMatter for more concise usage.\n */\nexport const withFrontmatter = async (\n\tapp: App,\n\tfile: TFile,\n\tupdate: (fm: Record<string, unknown>) => void\n) => app.fileManager.processFrontMatter(file, update);\n\n/**\n * Creates a backup copy of a file's frontmatter.\n * Useful for undo/redo operations or temporary modifications.\n */\nexport const backupFrontmatter = async (app: App, file: TFile) => {\n\tlet copy: Record<string, unknown> = {};\n\tawait withFrontmatter(app, file, (fm) => {\n\t\tcopy = { ...fm };\n\t});\n\treturn copy;\n};\n\n/**\n * Restores a file's frontmatter from a backup.\n * Clears existing frontmatter and replaces with the backup.\n */\nexport const restoreFrontmatter = async (\n\tapp: App,\n\tfile: TFile,\n\toriginal: Record<string, unknown>\n) =>\n\twithFrontmatter(app, file, (fm) => {\n\t\tfor (const k of Object.keys(fm)) {\n\t\t\tdelete fm[k];\n\t\t}\n\t\tObject.assign(fm, original);\n\t});\n\n/**\n * Extracts the content that appears after the frontmatter section.\n * Returns the entire content if no frontmatter is found.\n */\nexport const extractContentAfterFrontmatter = (fullContent: string): string => {\n\tconst frontmatterRegex = /^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/;\n\tconst match = fullContent.match(frontmatterRegex);\n\n\tif (match) {\n\t\t// Return content after frontmatter\n\t\treturn fullContent.substring(match.index! + match[0].length);\n\t}\n\n\t// If no frontmatter found, return the entire content\n\treturn fullContent;\n};\n"]}
|
package/dist/file/file.d.ts
CHANGED
|
@@ -116,6 +116,25 @@ export declare function withFileContext<T>(app: App, path: string, callback: (co
|
|
|
116
116
|
* ```
|
|
117
117
|
*/
|
|
118
118
|
export declare function getUniqueFilePath(app: App, folder: string, baseName: string): string;
|
|
119
|
+
/**
|
|
120
|
+
* Generates a unique file path by appending a counter if the file already exists.
|
|
121
|
+
* Accepts a complete file path and iterates with a counter suffix.
|
|
122
|
+
*
|
|
123
|
+
* @param app - The Obsidian App instance
|
|
124
|
+
* @param filePath - Complete file path including folder, name, and extension
|
|
125
|
+
* @returns Unique file path that doesn't exist in the vault
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* // If "folder/note.md" exists, returns "folder/note 1.md"
|
|
130
|
+
* const path = getUniqueFilePathFromFull(app, "folder/note.md");
|
|
131
|
+
*
|
|
132
|
+
* // Works with any extension
|
|
133
|
+
* const path = getUniqueFilePathFromFull(app, "assets/image.png");
|
|
134
|
+
* // -> "assets/image 1.png" if image.png exists
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export declare const getUniqueFilePathFromFull: (app: App, filePath: string) => string;
|
|
119
138
|
/**
|
|
120
139
|
* Generates a unique file path by appending a counter if the file already exists.
|
|
121
140
|
* Supports custom file extensions.
|
package/dist/file/file.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/file/file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAiB,KAAK,EAAE,MAAM,UAAU,CAAC;AAMhD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CActE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE5D;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAoCxD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAuBrD;AAMD,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,SAAS,CAAC;IAC7C,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAmBlE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,CAAC,EACtC,GAAG,EAAE,GAAG,EACR,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAChD,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CASnB;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAepF;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GAClC,KAAK,GAAG,EACR,QAAQ,MAAM,EACd,UAAU,MAAM,EAChB,YAAW,MAAa,KACtB,
|
|
1
|
+
{"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/file/file.ts"],"names":[],"mappings":"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"}
|
package/dist/file/file.js
CHANGED
|
@@ -208,6 +208,44 @@ export function getUniqueFilePath(app, folder, baseName) {
|
|
|
208
208
|
}
|
|
209
209
|
return fullPath;
|
|
210
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* Generates a unique file path by appending a counter if the file already exists.
|
|
213
|
+
* Accepts a complete file path and iterates with a counter suffix.
|
|
214
|
+
*
|
|
215
|
+
* @param app - The Obsidian App instance
|
|
216
|
+
* @param filePath - Complete file path including folder, name, and extension
|
|
217
|
+
* @returns Unique file path that doesn't exist in the vault
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* ```ts
|
|
221
|
+
* // If "folder/note.md" exists, returns "folder/note 1.md"
|
|
222
|
+
* const path = getUniqueFilePathFromFull(app, "folder/note.md");
|
|
223
|
+
*
|
|
224
|
+
* // Works with any extension
|
|
225
|
+
* const path = getUniqueFilePathFromFull(app, "assets/image.png");
|
|
226
|
+
* // -> "assets/image 1.png" if image.png exists
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
export const getUniqueFilePathFromFull = (app, filePath) => {
|
|
230
|
+
// If file doesn't exist, return as-is
|
|
231
|
+
if (!app.vault.getAbstractFileByPath(filePath)) {
|
|
232
|
+
return filePath;
|
|
233
|
+
}
|
|
234
|
+
// Extract folder, base name, and extension
|
|
235
|
+
const lastSlashIndex = filePath.lastIndexOf("/");
|
|
236
|
+
const folderPath = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex + 1) : "";
|
|
237
|
+
const fileName = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;
|
|
238
|
+
const lastDotIndex = fileName.lastIndexOf(".");
|
|
239
|
+
const baseName = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
|
|
240
|
+
const extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : "";
|
|
241
|
+
let counter = 1;
|
|
242
|
+
let uniquePath = `${folderPath}${baseName} ${counter}${extension}`;
|
|
243
|
+
while (app.vault.getAbstractFileByPath(uniquePath)) {
|
|
244
|
+
counter++;
|
|
245
|
+
uniquePath = `${folderPath}${baseName} ${counter}${extension}`;
|
|
246
|
+
}
|
|
247
|
+
return uniquePath;
|
|
248
|
+
};
|
|
211
249
|
/**
|
|
212
250
|
* Generates a unique file path by appending a counter if the file already exists.
|
|
213
251
|
* Supports custom file extensions.
|
|
@@ -220,12 +258,8 @@ export function getUniqueFilePath(app, folder, baseName) {
|
|
|
220
258
|
*/
|
|
221
259
|
export const generateUniqueFilePath = (app, folder, baseName, extension = "md") => {
|
|
222
260
|
const folderPath = folder ? `${folder}/` : "";
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
while (app.vault.getAbstractFileByPath(filePath)) {
|
|
226
|
-
filePath = `${folderPath}${baseName} ${counter++}.${extension}`;
|
|
227
|
-
}
|
|
228
|
-
return filePath;
|
|
261
|
+
const fullPath = `${folderPath}${baseName}.${extension}`;
|
|
262
|
+
return getUniqueFilePathFromFull(app, fullPath);
|
|
229
263
|
};
|
|
230
264
|
// ============================================================================
|
|
231
265
|
// Folder Note Operations
|
package/dist/file/file.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/file/file.ts"],"names":[],"mappings":"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","sourcesContent":["import type { App, CachedMetadata } from \"obsidian\";\nimport { normalizePath, TFile } from \"obsidian\";\n\n// ============================================================================\n// File Path Operations\n// ============================================================================\n\n/**\n * Retrieves a TFile object from the vault by its path.\n * Handles path normalization using Obsidian's normalizePath utility.\n *\n * **Important**: Obsidian file paths ALWAYS include the `.md` extension.\n * The TFile.path property returns paths like \"folder/file.md\", not \"folder/file\".\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file (will be normalized, should include .md extension)\n * @returns TFile if found, null otherwise\n *\n * @example\n * ```ts\n * // Correct: Include .md extension\n * const file = getFileByPath(app, \"folder/note.md\");\n *\n * // For wikilinks without extension, add .md\n * const linkPath = \"MyNote\";\n * const file = getFileByPath(app, `${linkPath}.md`);\n * ```\n */\nexport function getFileByPath(app: App, filePath: string): TFile | null {\n\t// Normalize the path using Obsidian's utility\n\t// This handles slashes, spaces, and platform-specific path issues\n\tconst normalizedPath = normalizePath(filePath);\n\n\t// Use Vault's direct lookup method (most efficient)\n\t// Prefer getFileByPath if available, otherwise use getAbstractFileByPath\n\tif (typeof app.vault.getFileByPath === \"function\") {\n\t\treturn app.vault.getFileByPath(normalizedPath);\n\t}\n\n\tconst abstractFile = app.vault.getAbstractFileByPath(normalizedPath);\n\n\treturn abstractFile instanceof TFile ? abstractFile : null;\n}\n\n/**\n * Ensures a file path includes the .md extension.\n * Use this when working with wikilinks or user input that may omit extensions.\n *\n * @param path - File path that may or may not include .md extension\n * @returns Path guaranteed to end with .md\n *\n * @example\n * ```ts\n * ensureMarkdownExtension(\"MyNote\") // \"MyNote.md\"\n * ensureMarkdownExtension(\"MyNote.md\") // \"MyNote.md\"\n * ensureMarkdownExtension(\"folder/note\") // \"folder/note.md\"\n * ```\n */\nexport function ensureMarkdownExtension(path: string): string {\n\treturn path.endsWith(\".md\") ? path : `${path}.md`;\n}\n\n/**\n * Removes the .md extension from a file path if present.\n * Useful for displaying file names or creating wikilinks.\n *\n * @param path - File path that may include .md extension\n * @returns Path without .md extension\n *\n * @example\n * ```ts\n * removeMarkdownExtension(\"folder/note.md\") // \"folder/note\"\n * removeMarkdownExtension(\"folder/note\") // \"folder/note\"\n * ```\n */\nexport function removeMarkdownExtension(path: string): string {\n\treturn path.endsWith(\".md\") ? path.slice(0, -3) : path;\n}\n\n// ============================================================================\n// File Name Extraction\n// ============================================================================\n\n/**\n * Extracts the display name from a file path or wiki link.\n *\n * Handles various formats:\n * - `[[path/to/file|Alias]]` -> returns \"Alias\"\n * - `[[path/to/file]]` -> returns \"file\"\n * - `path/to/file.md` -> returns \"file\"\n * - `file.md` -> returns \"file\"\n *\n * @param input - File path or wiki link string\n * @returns The display name to show in the UI\n */\nexport function extractDisplayName(input: string): string {\n\tif (!input) return \"\";\n\n\t// Remove any surrounding whitespace\n\tconst trimmed = input.trim();\n\n\t// Check if it's a wiki link format [[path|alias]] or [[path]]\n\tconst wikiLinkMatch = trimmed.match(/^\\[\\[([^\\]]+)\\]\\]$/);\n\n\tif (wikiLinkMatch) {\n\t\tconst innerContent = wikiLinkMatch[1];\n\n\t\t// Check if there's an alias (pipe character)\n\t\tconst pipeIndex = innerContent.indexOf(\"|\");\n\n\t\tif (pipeIndex !== -1) {\n\t\t\t// Return the alias (everything after the pipe)\n\t\t\treturn innerContent.substring(pipeIndex + 1).trim();\n\t\t}\n\n\t\t// No alias, extract filename from path\n\t\tconst path = innerContent.trim();\n\n\t\tconst lastSlashIndex = path.lastIndexOf(\"/\");\n\n\t\tconst filename = lastSlashIndex !== -1 ? path.substring(lastSlashIndex + 1) : path;\n\n\t\treturn filename.replace(/\\.md$/i, \"\");\n\t}\n\n\t// Not a wiki link, treat as regular path\n\tconst lastSlashIndex = trimmed.lastIndexOf(\"/\");\n\n\tconst filename = lastSlashIndex !== -1 ? trimmed.substring(lastSlashIndex + 1) : trimmed;\n\n\treturn filename.replace(/\\.md$/i, \"\");\n}\n\n/**\n * Extracts the actual file path from a wiki link or returns the path as-is.\n *\n * Handles:\n * - `[[path/to/file|Alias]]` -> returns \"path/to/file.md\"\n * - `[[path/to/file]]` -> returns \"path/to/file.md\"\n * - `path/to/file.md` -> returns \"path/to/file.md\"\n *\n * @param input - File path or wiki link string\n * @returns The actual file path (with .md extension)\n */\nexport function extractFilePath(input: string): string {\n\tif (!input) return \"\";\n\n\tconst trimmed = input.trim();\n\n\t// Check if it's a wiki link format [[path|alias]] or [[path]]\n\tconst wikiLinkMatch = trimmed.match(/^\\[\\[([^\\]]+)\\]\\]$/);\n\n\tif (wikiLinkMatch) {\n\t\tconst innerContent = wikiLinkMatch[1];\n\n\t\t// Check if there's an alias (pipe character)\n\t\tconst pipeIndex = innerContent.indexOf(\"|\");\n\n\t\tconst path =\n\t\t\tpipeIndex !== -1 ? innerContent.substring(0, pipeIndex).trim() : innerContent.trim();\n\n\t\t// Ensure .md extension\n\t\treturn path.endsWith(\".md\") ? path : `${path}.md`;\n\t}\n\n\t// Not a wiki link, ensure .md extension\n\treturn trimmed.endsWith(\".md\") ? trimmed : `${trimmed}.md`;\n}\n\n// ============================================================================\n// File Context\n// ============================================================================\n\nexport interface FileContext {\n\tpath: string;\n\tpathWithExt: string;\n\tbaseName: string;\n\tfile: TFile | null;\n\tfrontmatter: Record<string, any> | undefined;\n\tcache: CachedMetadata | null;\n}\n\n/**\n * Creates a comprehensive file context object containing all relevant file information.\n * Handles path normalization, file lookup, and metadata caching.\n */\nexport function getFileContext(app: App, path: string): FileContext {\n\tconst pathWithExt = ensureMarkdownExtension(path);\n\n\tconst baseName = removeMarkdownExtension(path);\n\n\tconst file = getFileByPath(app, pathWithExt);\n\n\tconst cache = file ? app.metadataCache.getFileCache(file) : null;\n\n\tconst frontmatter = cache?.frontmatter;\n\n\treturn {\n\t\tpath,\n\t\tpathWithExt,\n\t\tbaseName,\n\t\tfile,\n\t\tfrontmatter,\n\t\tcache,\n\t};\n}\n\n/**\n * Helper function to work with file context that automatically handles file not found cases.\n * Returns null if the file doesn't exist, otherwise executes the callback with the context.\n */\nexport async function withFileContext<T>(\n\tapp: App,\n\tpath: string,\n\tcallback: (context: FileContext) => Promise<T> | T\n): Promise<T | null> {\n\tconst context = getFileContext(app, path);\n\n\tif (!context.file) {\n\t\tconsole.warn(`File not found: ${context.pathWithExt}`);\n\t\treturn null;\n\t}\n\n\treturn await callback(context);\n}\n\n// ============================================================================\n// File Path Generation\n// ============================================================================\n\n/**\n * Generates a unique file path by appending a counter if the file already exists.\n * Automatically adds .md extension if not present.\n *\n * @param app - The Obsidian App instance\n * @param folder - Folder path (empty string for root, no trailing slash needed)\n * @param baseName - Base file name without extension\n * @returns Unique file path that doesn't exist in the vault\n *\n * @example\n * ```ts\n * // If \"MyNote.md\" exists, returns \"MyNote 1.md\"\n * const path = getUniqueFilePath(app, \"\", \"MyNote\");\n *\n * // With folder: \"Projects/Task.md\" -> \"Projects/Task 1.md\"\n * const path = getUniqueFilePath(app, \"Projects\", \"Task\");\n *\n * // Root folder handling\n * const path = getUniqueFilePath(app, \"/\", \"Note\"); // -> \"Note.md\"\n * ```\n */\nexport function getUniqueFilePath(app: App, folder: string, baseName: string): string {\n\tconst normalizedFolder = folder && folder !== \"/\" ? folder : \"\";\n\tconst folderPath = normalizedFolder ? `${normalizedFolder}/` : \"\";\n\n\tlet fileName = `${baseName}.md`;\n\tlet fullPath = `${folderPath}${fileName}`;\n\tlet counter = 1;\n\n\twhile (app.vault.getAbstractFileByPath(fullPath)) {\n\t\tfileName = `${baseName} ${counter}.md`;\n\t\tfullPath = `${folderPath}${fileName}`;\n\t\tcounter++;\n\t}\n\n\treturn fullPath;\n}\n\n/**\n * Generates a unique file path by appending a counter if the file already exists.\n * Supports custom file extensions.\n *\n * @param app - The Obsidian App instance\n * @param folder - Folder path (empty string for root)\n * @param baseName - Base file name without extension\n * @param extension - File extension (defaults to \"md\")\n * @returns Unique file path that doesn't exist in the vault\n */\nexport const generateUniqueFilePath = (\n\tapp: App,\n\tfolder: string,\n\tbaseName: string,\n\textension: string = \"md\"\n): string => {\n\tconst folderPath = folder ? `${folder}/` : \"\";\n\tlet filePath = `${folderPath}${baseName}.${extension}`;\n\tlet counter = 1;\n\n\twhile (app.vault.getAbstractFileByPath(filePath)) {\n\t\tfilePath = `${folderPath}${baseName} ${counter++}.${extension}`;\n\t}\n\n\treturn filePath;\n};\n\n// ============================================================================\n// Folder Note Operations\n// ============================================================================\n\n/**\n * Checks if a file is a folder note.\n * A folder note is a file whose name matches its parent folder name.\n *\n * @param filePath - Path to the file (e.g., \"tasks/tasks.md\")\n * @returns true if the file is a folder note, false otherwise\n *\n * @example\n * ```ts\n * isFolderNote(\"tasks/tasks.md\") // true\n * isFolderNote(\"tasks/subtask.md\") // false\n * isFolderNote(\"note.md\") // false (no parent folder)\n * isFolderNote(\"projects/docs/docs.md\") // true\n * ```\n */\nexport function isFolderNote(filePath: string): boolean {\n\tif (!filePath) return false;\n\n\t// Remove .md extension for comparison\n\tconst pathWithoutExt = removeMarkdownExtension(filePath);\n\n\t// Split path into segments\n\tconst segments = pathWithoutExt.split(\"/\");\n\n\t// Need at least 2 segments (folder/file)\n\tif (segments.length < 2) return false;\n\n\t// Get the file name (last segment) and parent folder name (second to last)\n\tconst fileName = segments[segments.length - 1];\n\tconst parentFolderName = segments[segments.length - 2];\n\n\t// File is a folder note if its name matches the parent folder\n\treturn fileName === parentFolderName;\n}\n\n/**\n * Gets the folder path for a file.\n *\n * @param filePath - Path to the file (e.g., \"tasks/subtask.md\")\n * @returns Folder path without trailing slash, or empty string if file is in root\n *\n * @example\n * ```ts\n * getFolderPath(\"tasks/subtask.md\") // \"tasks\"\n * getFolderPath(\"projects/docs/notes.md\") // \"projects/docs\"\n * getFolderPath(\"note.md\") // \"\"\n * ```\n */\nexport function getFolderPath(filePath: string): string {\n\tif (!filePath) return \"\";\n\n\tconst lastSlashIndex = filePath.lastIndexOf(\"/\");\n\n\tif (lastSlashIndex === -1) return \"\";\n\n\treturn filePath.substring(0, lastSlashIndex);\n}\n\n/**\n * Gets all markdown files in a specific folder (non-recursive).\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder (e.g., \"tasks\")\n * @returns Array of TFile objects in the folder\n *\n * @example\n * ```ts\n * const files = getFilesInFolder(app, \"tasks\");\n * // Returns [task1.md, task2.md, tasks.md] but not tasks/subtasks/file.md\n * ```\n */\nexport function getFilesInFolder(app: App, folderPath: string): TFile[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\treturn allFiles.filter((file) => {\n\t\tconst fileFolder = getFolderPath(file.path);\n\n\t\treturn fileFolder === folderPath;\n\t});\n}\n\n/**\n * Gets all markdown files in a folder and its subfolders recursively.\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder (e.g., \"tasks\")\n * @returns Array of TFile objects in the folder tree\n *\n * @example\n * ```ts\n * const files = getAllFilesInFolderTree(app, \"tasks\");\n * // Returns all .md files in tasks/ and all its subdirectories\n * ```\n */\nexport function getAllFilesInFolderTree(app: App, folderPath: string): TFile[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\tconst normalizedFolder = folderPath ? `${folderPath}/` : \"\";\n\n\treturn allFiles.filter((file) => {\n\t\tif (!normalizedFolder) return true; // Root folder includes all files\n\n\t\treturn file.path.startsWith(normalizedFolder);\n\t});\n}\n\n/**\n * Gets the parent file path based on folder structure.\n * For a file in a folder, the parent is the folder note if it exists.\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file\n * @returns Path to parent file, or null if no parent exists\n *\n * @example\n * ```ts\n * // If tasks/tasks.md exists\n * getParentByFolder(app, \"tasks/subtask.md\") // \"tasks/tasks.md\"\n *\n * // If parent folder note doesn't exist\n * getParentByFolder(app, \"tasks/subtask.md\") // null\n *\n * // Root level file\n * getParentByFolder(app, \"note.md\") // null\n * ```\n */\nexport function getParentByFolder(app: App, filePath: string): string | null {\n\tconst folderPath = getFolderPath(filePath);\n\n\tif (!folderPath) return null; // File is at root level\n\n\t// Check if folder note exists\n\tconst folderSegments = folderPath.split(\"/\");\n\n\tconst parentFolderName = folderSegments[folderSegments.length - 1];\n\n\tconst potentialParentPath = `${folderPath}/${parentFolderName}.md`;\n\n\tconst parentFile = getFileByPath(app, potentialParentPath);\n\n\treturn parentFile ? potentialParentPath : null;\n}\n\n/**\n * Gets all child file paths based on folder structure.\n * Works for both folder notes and regular files.\n *\n * For folder notes (e.g., \"tasks/tasks.md\"):\n * - Returns all files directly in the folder (excluding the folder note)\n * - Includes subfolder notes one level down\n *\n * For regular files (e.g., \"tasks/task1.md\"):\n * - Returns the folder note from matching subfolder if it exists (e.g., \"tasks/task1/task1.md\")\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file\n * @returns Array of child file paths\n *\n * @example\n * ```ts\n * // For tasks/tasks.md (folder note)\n * getChildrenByFolder(app, \"tasks/tasks.md\")\n * // Returns [\"tasks/task1.md\", \"tasks/task2.md\", \"tasks/subtasks/subtasks.md\"]\n *\n * // For tasks/task1.md (regular file with matching subfolder)\n * getChildrenByFolder(app, \"tasks/task1.md\")\n * // Returns [\"tasks/task1/task1.md\"] if it exists\n * ```\n */\nexport function getChildrenByFolder(app: App, filePath: string): string[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\t// Case 1: Folder note - get all files in the folder\n\tif (isFolderNote(filePath)) {\n\t\tconst folderPath = getFolderPath(filePath);\n\n\t\tconst children: string[] = [];\n\n\t\tallFiles.forEach((file) => {\n\t\t\t// Skip the folder note itself\n\t\t\tif (file.path === filePath) return;\n\n\t\t\tconst fileFolder = getFolderPath(file.path);\n\n\t\t\t// Direct child: file is in the same folder as the folder note\n\t\t\tif (fileFolder === folderPath) {\n\t\t\t\tchildren.push(file.path);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Subfolder note: file is a folder note one level deeper\n\t\t\t// e.g., for \"tasks/tasks.md\", include \"tasks/subtasks/subtasks.md\"\n\t\t\tif (fileFolder.startsWith(`${folderPath}/`)) {\n\t\t\t\t// Check if it's exactly one level deeper and is a folder note\n\t\t\t\tconst relativePath = fileFolder.substring(folderPath.length + 1);\n\n\t\t\t\tconst isOneLevel = !relativePath.includes(\"/\");\n\n\t\t\t\tif (isOneLevel && isFolderNote(file.path)) {\n\t\t\t\t\tchildren.push(file.path);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\treturn children;\n\t}\n\n\t// Case 2: Regular file - check for matching subfolder with folder note\n\tconst pathWithoutExt = removeMarkdownExtension(filePath);\n\n\tconst fileName = pathWithoutExt.split(\"/\").pop() || \"\";\n\n\tconst potentialChildFolder = `${pathWithoutExt}`;\n\n\tconst potentialChildPath = `${potentialChildFolder}/${fileName}.md`;\n\n\t// Check if the child folder note exists\n\tconst childFile = getFileByPath(app, potentialChildPath);\n\n\treturn childFile ? [potentialChildPath] : [];\n}\n\n/**\n * Finds all root nodes in a folder tree.\n * Root nodes are files at the top level of the folder (directly in the folder, not in subfolders).\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder\n * @returns Array of root file paths\n *\n * @example\n * ```ts\n * // For folder structure:\n * // tasks/\n * // tasks.md (folder note)\n * // task1.md\n * // subtasks/\n * // subtasks.md\n * // subtask1.md\n *\n * findRootNodesInFolder(app, \"tasks\")\n * // Returns [\"tasks/tasks.md\", \"tasks/task1.md\"]\n * // Excludes subtasks/subtasks.md and subtasks/subtask1.md (they're in subfolder)\n * ```\n */\nexport function findRootNodesInFolder(app: App, folderPath: string): string[] {\n\treturn getFilesInFolder(app, folderPath).map((file) => file.path);\n}\n\n// ============================================================================\n// Filename Sanitization\n// ============================================================================\n\nexport interface SanitizeFilenameOptions {\n\t/**\n\t * Style of sanitization to apply.\n\t * - \"kebab\": Convert to lowercase, replace spaces with hyphens (default, backwards compatible)\n\t * - \"preserve\": Preserve spaces and case, only remove invalid characters\n\t */\n\tstyle?: \"kebab\" | \"preserve\";\n}\n\n/**\n * Sanitizes a string for use as a filename.\n * Defaults to kebab-case style for backwards compatibility.\n *\n * @param input - String to sanitize\n * @param options - Sanitization options\n * @returns Sanitized filename string\n *\n * @example\n * // Default kebab-case style (backwards compatible)\n * sanitizeForFilename(\"My File Name\") // \"my-file-name\"\n *\n * // Preserve spaces and case\n * sanitizeForFilename(\"My File Name\", { style: \"preserve\" }) // \"My File Name\"\n */\nexport const sanitizeForFilename = (\n\tinput: string,\n\toptions: SanitizeFilenameOptions = {}\n): string => {\n\tconst { style = \"kebab\" } = options;\n\n\tif (style === \"preserve\") {\n\t\treturn sanitizeFilenamePreserveSpaces(input);\n\t}\n\n\t// Default: kebab-case style (legacy behavior)\n\treturn sanitizeFilenameKebabCase(input);\n};\n\n/**\n * Sanitizes filename using kebab-case style.\n * - Removes invalid characters\n * - Converts to lowercase\n * - Replaces spaces with hyphens\n *\n * Best for: CLI tools, URLs, slugs, technical files\n *\n * @example\n * sanitizeFilenameKebabCase(\"My File Name\") // \"my-file-name\"\n * sanitizeFilenameKebabCase(\"Travel Around The World\") // \"travel-around-the-world\"\n */\nexport const sanitizeFilenameKebabCase = (input: string): string => {\n\treturn (\n\t\tinput\n\t\t\t// Remove invalid filename characters\n\t\t\t.replace(/[<>:\"/\\\\|?*]/g, \"\")\n\t\t\t// Replace spaces with hyphens\n\t\t\t.replace(/\\s+/g, \"-\")\n\t\t\t// Replace multiple hyphens with single\n\t\t\t.replace(/-+/g, \"-\")\n\t\t\t// Remove leading/trailing hyphens\n\t\t\t.replace(/^-|-$/g, \"\")\n\t\t\t// Convert to lowercase\n\t\t\t.toLowerCase()\n\t);\n};\n\n/**\n * Sanitizes filename while preserving spaces and case.\n * - Removes invalid characters only\n * - Preserves spaces and original casing\n * - Removes trailing dots (Windows compatibility)\n *\n * Best for: Note titles, human-readable filenames, Obsidian notes\n *\n * @example\n * sanitizeFilenamePreserveSpaces(\"My File Name\") // \"My File Name\"\n * sanitizeFilenamePreserveSpaces(\"Travel Around The World\") // \"Travel Around The World\"\n * sanitizeFilenamePreserveSpaces(\"File<Invalid>Chars\") // \"FileInvalidChars\"\n */\nexport const sanitizeFilenamePreserveSpaces = (input: string): string => {\n\treturn (\n\t\tinput\n\t\t\t// Remove invalid filename characters (cross-platform compatibility)\n\t\t\t.replace(/[<>:\"/\\\\|?*]/g, \"\")\n\t\t\t// Remove trailing dots (invalid on Windows)\n\t\t\t.replace(/\\.+$/g, \"\")\n\t\t\t// Remove leading/trailing whitespace\n\t\t\t.trim()\n\t);\n};\n\nexport const getFilenameFromPath = (filePath: string): string => {\n\treturn filePath.split(\"/\").pop() || \"Unknown\";\n};\n\nexport const isFileInConfiguredDirectory = (filePath: string, directory: string): boolean => {\n\tconst normalizedDir = directory.endsWith(\"/\") ? directory.slice(0, -1) : directory;\n\treturn filePath.startsWith(`${normalizedDir}/`) || filePath === normalizedDir;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"file.js","sourceRoot":"","sources":["../../src/file/file.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEhD,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,aAAa,CAAC,GAAQ,EAAE,QAAgB;IACvD,8CAA8C;IAC9C,kEAAkE;IAClE,MAAM,cAAc,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAE/C,oDAAoD;IACpD,yEAAyE;IACzE,IAAI,OAAO,GAAG,CAAC,KAAK,CAAC,aAAa,KAAK,UAAU,EAAE,CAAC;QACnD,OAAO,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC;IAChD,CAAC;IAED,MAAM,YAAY,GAAG,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,cAAc,CAAC,CAAC;IAErE,OAAO,YAAY,YAAY,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5D,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY;IACnD,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY;IACnD,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa;IAC/C,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAEtB,oCAAoC;IACpC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,8DAA8D;IAC9D,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAE1D,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAEtC,6CAA6C;QAC7C,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE5C,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;YACtB,+CAA+C;YAC/C,OAAO,YAAY,CAAC,SAAS,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACrD,CAAC;QAED,uCAAuC;QACvC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC;QAEjC,MAAM,cAAc,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAE7C,MAAM,QAAQ,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAEnF,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,yCAAyC;IACzC,MAAM,cAAc,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAEhD,MAAM,QAAQ,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IAEzF,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,eAAe,CAAC,KAAa;IAC5C,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IAEtB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,8DAA8D;IAC9D,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAE1D,IAAI,aAAa,EAAE,CAAC;QACnB,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;QAEtC,6CAA6C;QAC7C,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAE5C,MAAM,IAAI,GACT,SAAS,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAEtF,uBAAuB;QACvB,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,CAAC;IACnD,CAAC;IAED,wCAAwC;IACxC,OAAO,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,KAAK,CAAC;AAC5D,CAAC;AAeD;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,GAAQ,EAAE,IAAY;IACpD,MAAM,WAAW,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAElD,MAAM,QAAQ,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAE/C,MAAM,IAAI,GAAG,aAAa,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAE7C,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjE,MAAM,WAAW,GAAG,KAAK,aAAL,KAAK,uBAAL,KAAK,CAAE,WAAW,CAAC;IAEvC,OAAO;QACN,IAAI;QACJ,WAAW;QACX,QAAQ;QACR,IAAI;QACJ,WAAW;QACX,KAAK;KACL,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAgB,eAAe,CACpC,GAAQ,EACR,IAAY,EACZ,QAAkD;;QAElD,MAAM,OAAO,GAAG,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAE1C,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,OAAO,CAAC,IAAI,CAAC,mBAAmB,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACb,CAAC;QAED,OAAO,MAAM,QAAQ,CAAC,OAAO,CAAC,CAAC;IAChC,CAAC;CAAA;AAED,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAQ,EAAE,MAAc,EAAE,QAAgB;IAC3E,MAAM,gBAAgB,GAAG,MAAM,IAAI,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,MAAM,UAAU,GAAG,gBAAgB,CAAC,CAAC,CAAC,GAAG,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAElE,IAAI,QAAQ,GAAG,GAAG,QAAQ,KAAK,CAAC;IAChC,IAAI,QAAQ,GAAG,GAAG,UAAU,GAAG,QAAQ,EAAE,CAAC;IAC1C,IAAI,OAAO,GAAG,CAAC,CAAC;IAEhB,OAAO,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAClD,QAAQ,GAAG,GAAG,QAAQ,IAAI,OAAO,KAAK,CAAC;QACvC,QAAQ,GAAG,GAAG,UAAU,GAAG,QAAQ,EAAE,CAAC;QACtC,OAAO,EAAE,CAAC;IACX,CAAC;IAED,OAAO,QAAQ,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAQ,EAAE,QAAgB,EAAU,EAAE;IAC/E,sCAAsC;IACtC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChD,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,2CAA2C;IAC3C,MAAM,cAAc,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1F,MAAM,QAAQ,GAAG,cAAc,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IAE3F,MAAM,YAAY,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAC/C,MAAM,QAAQ,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;IACtF,MAAM,SAAS,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9E,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,UAAU,GAAG,GAAG,UAAU,GAAG,QAAQ,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;IAEnE,OAAO,GAAG,CAAC,KAAK,CAAC,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,CAAC;QACV,UAAU,GAAG,GAAG,UAAU,GAAG,QAAQ,IAAI,OAAO,GAAG,SAAS,EAAE,CAAC;IAChE,CAAC;IAED,OAAO,UAAU,CAAC;AACnB,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CACrC,GAAQ,EACR,MAAc,EACd,QAAgB,EAChB,YAAoB,IAAI,EACf,EAAE;IACX,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAAG,GAAG,UAAU,GAAG,QAAQ,IAAI,SAAS,EAAE,CAAC;IACzD,OAAO,yBAAyB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AACjD,CAAC,CAAC;AAEF,+EAA+E;AAC/E,yBAAyB;AACzB,+EAA+E;AAE/E;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC5C,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAE5B,sCAAsC;IACtC,MAAM,cAAc,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAEzD,2BAA2B;IAC3B,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE3C,yCAAyC;IACzC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtC,2EAA2E;IAC3E,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC/C,MAAM,gBAAgB,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEvD,8DAA8D;IAC9D,OAAO,QAAQ,KAAK,gBAAgB,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,QAAgB;IAC7C,IAAI,CAAC,QAAQ;QAAE,OAAO,EAAE,CAAC;IAEzB,MAAM,cAAc,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAEjD,IAAI,cAAc,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAErC,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAQ,EAAE,UAAkB;IAC5D,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;IAE9C,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAE5C,OAAO,UAAU,KAAK,UAAU,CAAC;IAClC,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAQ,EAAE,UAAkB;IACnE,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;IAE9C,MAAM,gBAAgB,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IAE5D,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE;QAC/B,IAAI,CAAC,gBAAgB;YAAE,OAAO,IAAI,CAAC,CAAC,iCAAiC;QAErE,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAQ,EAAE,QAAgB;IAC3D,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAE3C,IAAI,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC,CAAC,wBAAwB;IAEtD,8BAA8B;IAC9B,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE7C,MAAM,gBAAgB,GAAG,cAAc,CAAC,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAEnE,MAAM,mBAAmB,GAAG,GAAG,UAAU,IAAI,gBAAgB,KAAK,CAAC;IAEnE,MAAM,UAAU,GAAG,aAAa,CAAC,GAAG,EAAE,mBAAmB,CAAC,CAAC;IAE3D,OAAO,UAAU,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,IAAI,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAQ,EAAE,QAAgB;IAC7D,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,gBAAgB,EAAE,CAAC;IAE9C,oDAAoD;IACpD,IAAI,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5B,MAAM,UAAU,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;QAE3C,MAAM,QAAQ,GAAa,EAAE,CAAC;QAE9B,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE;YACzB,8BAA8B;YAC9B,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ;gBAAE,OAAO;YAEnC,MAAM,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAE5C,8DAA8D;YAC9D,IAAI,UAAU,KAAK,UAAU,EAAE,CAAC;gBAC/B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAEzB,OAAO;YACR,CAAC;YAED,yDAAyD;YACzD,mEAAmE;YACnE,IAAI,UAAU,CAAC,UAAU,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC;gBAC7C,8DAA8D;gBAC9D,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;gBAEjE,MAAM,UAAU,GAAG,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAE/C,IAAI,UAAU,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC3C,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC1B,CAAC;YACF,CAAC;QACF,CAAC,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC;IACjB,CAAC;IAED,uEAAuE;IACvE,MAAM,cAAc,GAAG,uBAAuB,CAAC,QAAQ,CAAC,CAAC;IAEzD,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;IAEvD,MAAM,oBAAoB,GAAG,GAAG,cAAc,EAAE,CAAC;IAEjD,MAAM,kBAAkB,GAAG,GAAG,oBAAoB,IAAI,QAAQ,KAAK,CAAC;IAEpE,wCAAwC;IACxC,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAEzD,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAQ,EAAE,UAAkB;IACjE,OAAO,gBAAgB,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAeD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAClC,KAAa,EACb,UAAmC,EAAE,EAC5B,EAAE;IACX,MAAM,EAAE,KAAK,GAAG,OAAO,EAAE,GAAG,OAAO,CAAC;IAEpC,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;QAC1B,OAAO,8BAA8B,CAAC,KAAK,CAAC,CAAC;IAC9C,CAAC;IAED,8CAA8C;IAC9C,OAAO,yBAAyB,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,KAAa,EAAU,EAAE;IAClE,OAAO,CACN,KAAK;QACJ,qCAAqC;SACpC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;QAC7B,8BAA8B;SAC7B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;QACrB,uCAAuC;SACtC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;QACpB,kCAAkC;SACjC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC;QACtB,uBAAuB;SACtB,WAAW,EAAE,CACf,CAAC;AACH,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,KAAa,EAAU,EAAE;IACvE,OAAO,CACN,KAAK;QACJ,oEAAoE;SACnE,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;QAC7B,4CAA4C;SAC3C,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACrB,qCAAqC;SACpC,IAAI,EAAE,CACR,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,QAAgB,EAAU,EAAE;IAC/D,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,SAAS,CAAC;AAC/C,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,QAAgB,EAAE,SAAiB,EAAW,EAAE;IAC3F,MAAM,aAAa,GAAG,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACnF,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,GAAG,CAAC,IAAI,QAAQ,KAAK,aAAa,CAAC;AAC/E,CAAC,CAAC","sourcesContent":["import type { App, CachedMetadata } from \"obsidian\";\nimport { normalizePath, TFile } from \"obsidian\";\n\n// ============================================================================\n// File Path Operations\n// ============================================================================\n\n/**\n * Retrieves a TFile object from the vault by its path.\n * Handles path normalization using Obsidian's normalizePath utility.\n *\n * **Important**: Obsidian file paths ALWAYS include the `.md` extension.\n * The TFile.path property returns paths like \"folder/file.md\", not \"folder/file\".\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file (will be normalized, should include .md extension)\n * @returns TFile if found, null otherwise\n *\n * @example\n * ```ts\n * // Correct: Include .md extension\n * const file = getFileByPath(app, \"folder/note.md\");\n *\n * // For wikilinks without extension, add .md\n * const linkPath = \"MyNote\";\n * const file = getFileByPath(app, `${linkPath}.md`);\n * ```\n */\nexport function getFileByPath(app: App, filePath: string): TFile | null {\n\t// Normalize the path using Obsidian's utility\n\t// This handles slashes, spaces, and platform-specific path issues\n\tconst normalizedPath = normalizePath(filePath);\n\n\t// Use Vault's direct lookup method (most efficient)\n\t// Prefer getFileByPath if available, otherwise use getAbstractFileByPath\n\tif (typeof app.vault.getFileByPath === \"function\") {\n\t\treturn app.vault.getFileByPath(normalizedPath);\n\t}\n\n\tconst abstractFile = app.vault.getAbstractFileByPath(normalizedPath);\n\n\treturn abstractFile instanceof TFile ? abstractFile : null;\n}\n\n/**\n * Ensures a file path includes the .md extension.\n * Use this when working with wikilinks or user input that may omit extensions.\n *\n * @param path - File path that may or may not include .md extension\n * @returns Path guaranteed to end with .md\n *\n * @example\n * ```ts\n * ensureMarkdownExtension(\"MyNote\") // \"MyNote.md\"\n * ensureMarkdownExtension(\"MyNote.md\") // \"MyNote.md\"\n * ensureMarkdownExtension(\"folder/note\") // \"folder/note.md\"\n * ```\n */\nexport function ensureMarkdownExtension(path: string): string {\n\treturn path.endsWith(\".md\") ? path : `${path}.md`;\n}\n\n/**\n * Removes the .md extension from a file path if present.\n * Useful for displaying file names or creating wikilinks.\n *\n * @param path - File path that may include .md extension\n * @returns Path without .md extension\n *\n * @example\n * ```ts\n * removeMarkdownExtension(\"folder/note.md\") // \"folder/note\"\n * removeMarkdownExtension(\"folder/note\") // \"folder/note\"\n * ```\n */\nexport function removeMarkdownExtension(path: string): string {\n\treturn path.endsWith(\".md\") ? path.slice(0, -3) : path;\n}\n\n// ============================================================================\n// File Name Extraction\n// ============================================================================\n\n/**\n * Extracts the display name from a file path or wiki link.\n *\n * Handles various formats:\n * - `[[path/to/file|Alias]]` -> returns \"Alias\"\n * - `[[path/to/file]]` -> returns \"file\"\n * - `path/to/file.md` -> returns \"file\"\n * - `file.md` -> returns \"file\"\n *\n * @param input - File path or wiki link string\n * @returns The display name to show in the UI\n */\nexport function extractDisplayName(input: string): string {\n\tif (!input) return \"\";\n\n\t// Remove any surrounding whitespace\n\tconst trimmed = input.trim();\n\n\t// Check if it's a wiki link format [[path|alias]] or [[path]]\n\tconst wikiLinkMatch = trimmed.match(/^\\[\\[([^\\]]+)\\]\\]$/);\n\n\tif (wikiLinkMatch) {\n\t\tconst innerContent = wikiLinkMatch[1];\n\n\t\t// Check if there's an alias (pipe character)\n\t\tconst pipeIndex = innerContent.indexOf(\"|\");\n\n\t\tif (pipeIndex !== -1) {\n\t\t\t// Return the alias (everything after the pipe)\n\t\t\treturn innerContent.substring(pipeIndex + 1).trim();\n\t\t}\n\n\t\t// No alias, extract filename from path\n\t\tconst path = innerContent.trim();\n\n\t\tconst lastSlashIndex = path.lastIndexOf(\"/\");\n\n\t\tconst filename = lastSlashIndex !== -1 ? path.substring(lastSlashIndex + 1) : path;\n\n\t\treturn filename.replace(/\\.md$/i, \"\");\n\t}\n\n\t// Not a wiki link, treat as regular path\n\tconst lastSlashIndex = trimmed.lastIndexOf(\"/\");\n\n\tconst filename = lastSlashIndex !== -1 ? trimmed.substring(lastSlashIndex + 1) : trimmed;\n\n\treturn filename.replace(/\\.md$/i, \"\");\n}\n\n/**\n * Extracts the actual file path from a wiki link or returns the path as-is.\n *\n * Handles:\n * - `[[path/to/file|Alias]]` -> returns \"path/to/file.md\"\n * - `[[path/to/file]]` -> returns \"path/to/file.md\"\n * - `path/to/file.md` -> returns \"path/to/file.md\"\n *\n * @param input - File path or wiki link string\n * @returns The actual file path (with .md extension)\n */\nexport function extractFilePath(input: string): string {\n\tif (!input) return \"\";\n\n\tconst trimmed = input.trim();\n\n\t// Check if it's a wiki link format [[path|alias]] or [[path]]\n\tconst wikiLinkMatch = trimmed.match(/^\\[\\[([^\\]]+)\\]\\]$/);\n\n\tif (wikiLinkMatch) {\n\t\tconst innerContent = wikiLinkMatch[1];\n\n\t\t// Check if there's an alias (pipe character)\n\t\tconst pipeIndex = innerContent.indexOf(\"|\");\n\n\t\tconst path =\n\t\t\tpipeIndex !== -1 ? innerContent.substring(0, pipeIndex).trim() : innerContent.trim();\n\n\t\t// Ensure .md extension\n\t\treturn path.endsWith(\".md\") ? path : `${path}.md`;\n\t}\n\n\t// Not a wiki link, ensure .md extension\n\treturn trimmed.endsWith(\".md\") ? trimmed : `${trimmed}.md`;\n}\n\n// ============================================================================\n// File Context\n// ============================================================================\n\nexport interface FileContext {\n\tpath: string;\n\tpathWithExt: string;\n\tbaseName: string;\n\tfile: TFile | null;\n\tfrontmatter: Record<string, any> | undefined;\n\tcache: CachedMetadata | null;\n}\n\n/**\n * Creates a comprehensive file context object containing all relevant file information.\n * Handles path normalization, file lookup, and metadata caching.\n */\nexport function getFileContext(app: App, path: string): FileContext {\n\tconst pathWithExt = ensureMarkdownExtension(path);\n\n\tconst baseName = removeMarkdownExtension(path);\n\n\tconst file = getFileByPath(app, pathWithExt);\n\n\tconst cache = file ? app.metadataCache.getFileCache(file) : null;\n\n\tconst frontmatter = cache?.frontmatter;\n\n\treturn {\n\t\tpath,\n\t\tpathWithExt,\n\t\tbaseName,\n\t\tfile,\n\t\tfrontmatter,\n\t\tcache,\n\t};\n}\n\n/**\n * Helper function to work with file context that automatically handles file not found cases.\n * Returns null if the file doesn't exist, otherwise executes the callback with the context.\n */\nexport async function withFileContext<T>(\n\tapp: App,\n\tpath: string,\n\tcallback: (context: FileContext) => Promise<T> | T\n): Promise<T | null> {\n\tconst context = getFileContext(app, path);\n\n\tif (!context.file) {\n\t\tconsole.warn(`File not found: ${context.pathWithExt}`);\n\t\treturn null;\n\t}\n\n\treturn await callback(context);\n}\n\n// ============================================================================\n// File Path Generation\n// ============================================================================\n\n/**\n * Generates a unique file path by appending a counter if the file already exists.\n * Automatically adds .md extension if not present.\n *\n * @param app - The Obsidian App instance\n * @param folder - Folder path (empty string for root, no trailing slash needed)\n * @param baseName - Base file name without extension\n * @returns Unique file path that doesn't exist in the vault\n *\n * @example\n * ```ts\n * // If \"MyNote.md\" exists, returns \"MyNote 1.md\"\n * const path = getUniqueFilePath(app, \"\", \"MyNote\");\n *\n * // With folder: \"Projects/Task.md\" -> \"Projects/Task 1.md\"\n * const path = getUniqueFilePath(app, \"Projects\", \"Task\");\n *\n * // Root folder handling\n * const path = getUniqueFilePath(app, \"/\", \"Note\"); // -> \"Note.md\"\n * ```\n */\nexport function getUniqueFilePath(app: App, folder: string, baseName: string): string {\n\tconst normalizedFolder = folder && folder !== \"/\" ? folder : \"\";\n\tconst folderPath = normalizedFolder ? `${normalizedFolder}/` : \"\";\n\n\tlet fileName = `${baseName}.md`;\n\tlet fullPath = `${folderPath}${fileName}`;\n\tlet counter = 1;\n\n\twhile (app.vault.getAbstractFileByPath(fullPath)) {\n\t\tfileName = `${baseName} ${counter}.md`;\n\t\tfullPath = `${folderPath}${fileName}`;\n\t\tcounter++;\n\t}\n\n\treturn fullPath;\n}\n\n/**\n * Generates a unique file path by appending a counter if the file already exists.\n * Accepts a complete file path and iterates with a counter suffix.\n *\n * @param app - The Obsidian App instance\n * @param filePath - Complete file path including folder, name, and extension\n * @returns Unique file path that doesn't exist in the vault\n *\n * @example\n * ```ts\n * // If \"folder/note.md\" exists, returns \"folder/note 1.md\"\n * const path = getUniqueFilePathFromFull(app, \"folder/note.md\");\n *\n * // Works with any extension\n * const path = getUniqueFilePathFromFull(app, \"assets/image.png\");\n * // -> \"assets/image 1.png\" if image.png exists\n * ```\n */\nexport const getUniqueFilePathFromFull = (app: App, filePath: string): string => {\n\t// If file doesn't exist, return as-is\n\tif (!app.vault.getAbstractFileByPath(filePath)) {\n\t\treturn filePath;\n\t}\n\n\t// Extract folder, base name, and extension\n\tconst lastSlashIndex = filePath.lastIndexOf(\"/\");\n\tconst folderPath = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex + 1) : \"\";\n\tconst fileName = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;\n\n\tconst lastDotIndex = fileName.lastIndexOf(\".\");\n\tconst baseName = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;\n\tconst extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : \"\";\n\n\tlet counter = 1;\n\tlet uniquePath = `${folderPath}${baseName} ${counter}${extension}`;\n\n\twhile (app.vault.getAbstractFileByPath(uniquePath)) {\n\t\tcounter++;\n\t\tuniquePath = `${folderPath}${baseName} ${counter}${extension}`;\n\t}\n\n\treturn uniquePath;\n};\n\n/**\n * Generates a unique file path by appending a counter if the file already exists.\n * Supports custom file extensions.\n *\n * @param app - The Obsidian App instance\n * @param folder - Folder path (empty string for root)\n * @param baseName - Base file name without extension\n * @param extension - File extension (defaults to \"md\")\n * @returns Unique file path that doesn't exist in the vault\n */\nexport const generateUniqueFilePath = (\n\tapp: App,\n\tfolder: string,\n\tbaseName: string,\n\textension: string = \"md\"\n): string => {\n\tconst folderPath = folder ? `${folder}/` : \"\";\n\tconst fullPath = `${folderPath}${baseName}.${extension}`;\n\treturn getUniqueFilePathFromFull(app, fullPath);\n};\n\n// ============================================================================\n// Folder Note Operations\n// ============================================================================\n\n/**\n * Checks if a file is a folder note.\n * A folder note is a file whose name matches its parent folder name.\n *\n * @param filePath - Path to the file (e.g., \"tasks/tasks.md\")\n * @returns true if the file is a folder note, false otherwise\n *\n * @example\n * ```ts\n * isFolderNote(\"tasks/tasks.md\") // true\n * isFolderNote(\"tasks/subtask.md\") // false\n * isFolderNote(\"note.md\") // false (no parent folder)\n * isFolderNote(\"projects/docs/docs.md\") // true\n * ```\n */\nexport function isFolderNote(filePath: string): boolean {\n\tif (!filePath) return false;\n\n\t// Remove .md extension for comparison\n\tconst pathWithoutExt = removeMarkdownExtension(filePath);\n\n\t// Split path into segments\n\tconst segments = pathWithoutExt.split(\"/\");\n\n\t// Need at least 2 segments (folder/file)\n\tif (segments.length < 2) return false;\n\n\t// Get the file name (last segment) and parent folder name (second to last)\n\tconst fileName = segments[segments.length - 1];\n\tconst parentFolderName = segments[segments.length - 2];\n\n\t// File is a folder note if its name matches the parent folder\n\treturn fileName === parentFolderName;\n}\n\n/**\n * Gets the folder path for a file.\n *\n * @param filePath - Path to the file (e.g., \"tasks/subtask.md\")\n * @returns Folder path without trailing slash, or empty string if file is in root\n *\n * @example\n * ```ts\n * getFolderPath(\"tasks/subtask.md\") // \"tasks\"\n * getFolderPath(\"projects/docs/notes.md\") // \"projects/docs\"\n * getFolderPath(\"note.md\") // \"\"\n * ```\n */\nexport function getFolderPath(filePath: string): string {\n\tif (!filePath) return \"\";\n\n\tconst lastSlashIndex = filePath.lastIndexOf(\"/\");\n\n\tif (lastSlashIndex === -1) return \"\";\n\n\treturn filePath.substring(0, lastSlashIndex);\n}\n\n/**\n * Gets all markdown files in a specific folder (non-recursive).\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder (e.g., \"tasks\")\n * @returns Array of TFile objects in the folder\n *\n * @example\n * ```ts\n * const files = getFilesInFolder(app, \"tasks\");\n * // Returns [task1.md, task2.md, tasks.md] but not tasks/subtasks/file.md\n * ```\n */\nexport function getFilesInFolder(app: App, folderPath: string): TFile[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\treturn allFiles.filter((file) => {\n\t\tconst fileFolder = getFolderPath(file.path);\n\n\t\treturn fileFolder === folderPath;\n\t});\n}\n\n/**\n * Gets all markdown files in a folder and its subfolders recursively.\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder (e.g., \"tasks\")\n * @returns Array of TFile objects in the folder tree\n *\n * @example\n * ```ts\n * const files = getAllFilesInFolderTree(app, \"tasks\");\n * // Returns all .md files in tasks/ and all its subdirectories\n * ```\n */\nexport function getAllFilesInFolderTree(app: App, folderPath: string): TFile[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\tconst normalizedFolder = folderPath ? `${folderPath}/` : \"\";\n\n\treturn allFiles.filter((file) => {\n\t\tif (!normalizedFolder) return true; // Root folder includes all files\n\n\t\treturn file.path.startsWith(normalizedFolder);\n\t});\n}\n\n/**\n * Gets the parent file path based on folder structure.\n * For a file in a folder, the parent is the folder note if it exists.\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file\n * @returns Path to parent file, or null if no parent exists\n *\n * @example\n * ```ts\n * // If tasks/tasks.md exists\n * getParentByFolder(app, \"tasks/subtask.md\") // \"tasks/tasks.md\"\n *\n * // If parent folder note doesn't exist\n * getParentByFolder(app, \"tasks/subtask.md\") // null\n *\n * // Root level file\n * getParentByFolder(app, \"note.md\") // null\n * ```\n */\nexport function getParentByFolder(app: App, filePath: string): string | null {\n\tconst folderPath = getFolderPath(filePath);\n\n\tif (!folderPath) return null; // File is at root level\n\n\t// Check if folder note exists\n\tconst folderSegments = folderPath.split(\"/\");\n\n\tconst parentFolderName = folderSegments[folderSegments.length - 1];\n\n\tconst potentialParentPath = `${folderPath}/${parentFolderName}.md`;\n\n\tconst parentFile = getFileByPath(app, potentialParentPath);\n\n\treturn parentFile ? potentialParentPath : null;\n}\n\n/**\n * Gets all child file paths based on folder structure.\n * Works for both folder notes and regular files.\n *\n * For folder notes (e.g., \"tasks/tasks.md\"):\n * - Returns all files directly in the folder (excluding the folder note)\n * - Includes subfolder notes one level down\n *\n * For regular files (e.g., \"tasks/task1.md\"):\n * - Returns the folder note from matching subfolder if it exists (e.g., \"tasks/task1/task1.md\")\n *\n * @param app - The Obsidian App instance\n * @param filePath - Path to the file\n * @returns Array of child file paths\n *\n * @example\n * ```ts\n * // For tasks/tasks.md (folder note)\n * getChildrenByFolder(app, \"tasks/tasks.md\")\n * // Returns [\"tasks/task1.md\", \"tasks/task2.md\", \"tasks/subtasks/subtasks.md\"]\n *\n * // For tasks/task1.md (regular file with matching subfolder)\n * getChildrenByFolder(app, \"tasks/task1.md\")\n * // Returns [\"tasks/task1/task1.md\"] if it exists\n * ```\n */\nexport function getChildrenByFolder(app: App, filePath: string): string[] {\n\tconst allFiles = app.vault.getMarkdownFiles();\n\n\t// Case 1: Folder note - get all files in the folder\n\tif (isFolderNote(filePath)) {\n\t\tconst folderPath = getFolderPath(filePath);\n\n\t\tconst children: string[] = [];\n\n\t\tallFiles.forEach((file) => {\n\t\t\t// Skip the folder note itself\n\t\t\tif (file.path === filePath) return;\n\n\t\t\tconst fileFolder = getFolderPath(file.path);\n\n\t\t\t// Direct child: file is in the same folder as the folder note\n\t\t\tif (fileFolder === folderPath) {\n\t\t\t\tchildren.push(file.path);\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Subfolder note: file is a folder note one level deeper\n\t\t\t// e.g., for \"tasks/tasks.md\", include \"tasks/subtasks/subtasks.md\"\n\t\t\tif (fileFolder.startsWith(`${folderPath}/`)) {\n\t\t\t\t// Check if it's exactly one level deeper and is a folder note\n\t\t\t\tconst relativePath = fileFolder.substring(folderPath.length + 1);\n\n\t\t\t\tconst isOneLevel = !relativePath.includes(\"/\");\n\n\t\t\t\tif (isOneLevel && isFolderNote(file.path)) {\n\t\t\t\t\tchildren.push(file.path);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\treturn children;\n\t}\n\n\t// Case 2: Regular file - check for matching subfolder with folder note\n\tconst pathWithoutExt = removeMarkdownExtension(filePath);\n\n\tconst fileName = pathWithoutExt.split(\"/\").pop() || \"\";\n\n\tconst potentialChildFolder = `${pathWithoutExt}`;\n\n\tconst potentialChildPath = `${potentialChildFolder}/${fileName}.md`;\n\n\t// Check if the child folder note exists\n\tconst childFile = getFileByPath(app, potentialChildPath);\n\n\treturn childFile ? [potentialChildPath] : [];\n}\n\n/**\n * Finds all root nodes in a folder tree.\n * Root nodes are files at the top level of the folder (directly in the folder, not in subfolders).\n *\n * @param app - The Obsidian App instance\n * @param folderPath - Path to the folder\n * @returns Array of root file paths\n *\n * @example\n * ```ts\n * // For folder structure:\n * // tasks/\n * // tasks.md (folder note)\n * // task1.md\n * // subtasks/\n * // subtasks.md\n * // subtask1.md\n *\n * findRootNodesInFolder(app, \"tasks\")\n * // Returns [\"tasks/tasks.md\", \"tasks/task1.md\"]\n * // Excludes subtasks/subtasks.md and subtasks/subtask1.md (they're in subfolder)\n * ```\n */\nexport function findRootNodesInFolder(app: App, folderPath: string): string[] {\n\treturn getFilesInFolder(app, folderPath).map((file) => file.path);\n}\n\n// ============================================================================\n// Filename Sanitization\n// ============================================================================\n\nexport interface SanitizeFilenameOptions {\n\t/**\n\t * Style of sanitization to apply.\n\t * - \"kebab\": Convert to lowercase, replace spaces with hyphens (default, backwards compatible)\n\t * - \"preserve\": Preserve spaces and case, only remove invalid characters\n\t */\n\tstyle?: \"kebab\" | \"preserve\";\n}\n\n/**\n * Sanitizes a string for use as a filename.\n * Defaults to kebab-case style for backwards compatibility.\n *\n * @param input - String to sanitize\n * @param options - Sanitization options\n * @returns Sanitized filename string\n *\n * @example\n * // Default kebab-case style (backwards compatible)\n * sanitizeForFilename(\"My File Name\") // \"my-file-name\"\n *\n * // Preserve spaces and case\n * sanitizeForFilename(\"My File Name\", { style: \"preserve\" }) // \"My File Name\"\n */\nexport const sanitizeForFilename = (\n\tinput: string,\n\toptions: SanitizeFilenameOptions = {}\n): string => {\n\tconst { style = \"kebab\" } = options;\n\n\tif (style === \"preserve\") {\n\t\treturn sanitizeFilenamePreserveSpaces(input);\n\t}\n\n\t// Default: kebab-case style (legacy behavior)\n\treturn sanitizeFilenameKebabCase(input);\n};\n\n/**\n * Sanitizes filename using kebab-case style.\n * - Removes invalid characters\n * - Converts to lowercase\n * - Replaces spaces with hyphens\n *\n * Best for: CLI tools, URLs, slugs, technical files\n *\n * @example\n * sanitizeFilenameKebabCase(\"My File Name\") // \"my-file-name\"\n * sanitizeFilenameKebabCase(\"Travel Around The World\") // \"travel-around-the-world\"\n */\nexport const sanitizeFilenameKebabCase = (input: string): string => {\n\treturn (\n\t\tinput\n\t\t\t// Remove invalid filename characters\n\t\t\t.replace(/[<>:\"/\\\\|?*]/g, \"\")\n\t\t\t// Replace spaces with hyphens\n\t\t\t.replace(/\\s+/g, \"-\")\n\t\t\t// Replace multiple hyphens with single\n\t\t\t.replace(/-+/g, \"-\")\n\t\t\t// Remove leading/trailing hyphens\n\t\t\t.replace(/^-|-$/g, \"\")\n\t\t\t// Convert to lowercase\n\t\t\t.toLowerCase()\n\t);\n};\n\n/**\n * Sanitizes filename while preserving spaces and case.\n * - Removes invalid characters only\n * - Preserves spaces and original casing\n * - Removes trailing dots (Windows compatibility)\n *\n * Best for: Note titles, human-readable filenames, Obsidian notes\n *\n * @example\n * sanitizeFilenamePreserveSpaces(\"My File Name\") // \"My File Name\"\n * sanitizeFilenamePreserveSpaces(\"Travel Around The World\") // \"Travel Around The World\"\n * sanitizeFilenamePreserveSpaces(\"File<Invalid>Chars\") // \"FileInvalidChars\"\n */\nexport const sanitizeFilenamePreserveSpaces = (input: string): string => {\n\treturn (\n\t\tinput\n\t\t\t// Remove invalid filename characters (cross-platform compatibility)\n\t\t\t.replace(/[<>:\"/\\\\|?*]/g, \"\")\n\t\t\t// Remove trailing dots (invalid on Windows)\n\t\t\t.replace(/\\.+$/g, \"\")\n\t\t\t// Remove leading/trailing whitespace\n\t\t\t.trim()\n\t);\n};\n\nexport const getFilenameFromPath = (filePath: string): string => {\n\treturn filePath.split(\"/\").pop() || \"Unknown\";\n};\n\nexport const isFileInConfiguredDirectory = (filePath: string, directory: string): boolean => {\n\tconst normalizedDir = directory.endsWith(\"/\") ? directory.slice(0, -1) : directory;\n\treturn filePath.startsWith(`${normalizedDir}/`) || filePath === normalizedDir;\n};\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates file content with YAML frontmatter and body content.
|
|
3
|
+
* This is the atomic file creation format that prevents race conditions.
|
|
4
|
+
*
|
|
5
|
+
* @param frontmatter - Frontmatter as JSON object
|
|
6
|
+
* @param content - Body content (markdown text after frontmatter)
|
|
7
|
+
* @returns Complete file content with YAML frontmatter
|
|
8
|
+
*/
|
|
9
|
+
export declare function createFileContentWithFrontmatter(frontmatter: Record<string, unknown>, content?: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Parses file content to extract frontmatter and body separately.
|
|
12
|
+
* Returns both the frontmatter object and the body content.
|
|
13
|
+
*
|
|
14
|
+
* @param fileContent - Complete file content including frontmatter
|
|
15
|
+
* @returns Object with frontmatter and body
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseFileContent(fileContent: string): {
|
|
18
|
+
frontmatter: Record<string, unknown>;
|
|
19
|
+
body: string;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=frontmatter-serialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter-serialization.d.ts","sourceRoot":"","sources":["../../src/file/frontmatter-serialization.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,gCAAgC,CAC/C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,OAAO,SAAK,GACV,MAAM,CAgCR;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG;IACtD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;CACb,CAcA"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { stringify as stringifyYAML } from "yaml";
|
|
2
|
+
/**
|
|
3
|
+
* Creates file content with YAML frontmatter and body content.
|
|
4
|
+
* This is the atomic file creation format that prevents race conditions.
|
|
5
|
+
*
|
|
6
|
+
* @param frontmatter - Frontmatter as JSON object
|
|
7
|
+
* @param content - Body content (markdown text after frontmatter)
|
|
8
|
+
* @returns Complete file content with YAML frontmatter
|
|
9
|
+
*/
|
|
10
|
+
export function createFileContentWithFrontmatter(frontmatter, content = "") {
|
|
11
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
12
|
+
// No frontmatter, just return content
|
|
13
|
+
return content;
|
|
14
|
+
}
|
|
15
|
+
// Use the yaml library to stringify frontmatter
|
|
16
|
+
// This handles all edge cases: special characters, multiline strings, arrays, nested objects, etc.
|
|
17
|
+
const yaml = stringifyYAML(frontmatter, {
|
|
18
|
+
// Use 2-space indentation (Obsidian standard)
|
|
19
|
+
indent: 2,
|
|
20
|
+
// Don't add document markers (--- at the end)
|
|
21
|
+
lineWidth: 0,
|
|
22
|
+
// Minimize unnecessary quotes
|
|
23
|
+
defaultStringType: "PLAIN",
|
|
24
|
+
// Handle null values explicitly
|
|
25
|
+
nullStr: "",
|
|
26
|
+
}).trim();
|
|
27
|
+
if (!yaml) {
|
|
28
|
+
// Empty frontmatter after stringification
|
|
29
|
+
return content;
|
|
30
|
+
}
|
|
31
|
+
// Ensure content doesn't start with extra newlines
|
|
32
|
+
const trimmedContent = content.replace(/^\n+/, "");
|
|
33
|
+
if (trimmedContent) {
|
|
34
|
+
return `---\n${yaml}\n---\n\n${trimmedContent}`;
|
|
35
|
+
}
|
|
36
|
+
return `---\n${yaml}\n---\n`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parses file content to extract frontmatter and body separately.
|
|
40
|
+
* Returns both the frontmatter object and the body content.
|
|
41
|
+
*
|
|
42
|
+
* @param fileContent - Complete file content including frontmatter
|
|
43
|
+
* @returns Object with frontmatter and body
|
|
44
|
+
*/
|
|
45
|
+
export function parseFileContent(fileContent) {
|
|
46
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
47
|
+
const match = fileContent.match(frontmatterRegex);
|
|
48
|
+
if (!match) {
|
|
49
|
+
// No frontmatter found
|
|
50
|
+
return { frontmatter: {}, body: fileContent };
|
|
51
|
+
}
|
|
52
|
+
const [, , body] = match;
|
|
53
|
+
// For this use case, we just need to split the file
|
|
54
|
+
// The frontmatter will be properly parsed when we read it back with Obsidian's APIs
|
|
55
|
+
return { frontmatter: {}, body: body.trim() };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=frontmatter-serialization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter-serialization.js","sourceRoot":"","sources":["../../src/file/frontmatter-serialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,IAAI,aAAa,EAAE,MAAM,MAAM,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,UAAU,gCAAgC,CAC/C,WAAoC,EACpC,OAAO,GAAG,EAAE;IAEZ,IAAI,CAAC,WAAW,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3D,sCAAsC;QACtC,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,gDAAgD;IAChD,mGAAmG;IACnG,MAAM,IAAI,GAAG,aAAa,CAAC,WAAW,EAAE;QACvC,8CAA8C;QAC9C,MAAM,EAAE,CAAC;QACT,8CAA8C;QAC9C,SAAS,EAAE,CAAC;QACZ,8BAA8B;QAC9B,iBAAiB,EAAE,OAAO;QAC1B,gCAAgC;QAChC,OAAO,EAAE,EAAE;KACX,CAAC,CAAC,IAAI,EAAE,CAAC;IAEV,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,0CAA0C;QAC1C,OAAO,OAAO,CAAC;IAChB,CAAC;IAED,mDAAmD;IACnD,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEnD,IAAI,cAAc,EAAE,CAAC;QACpB,OAAO,QAAQ,IAAI,YAAY,cAAc,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,QAAQ,IAAI,SAAS,CAAC;AAC9B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB;IAInD,MAAM,gBAAgB,GAAG,oCAAoC,CAAC;IAC9D,MAAM,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;IAElD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,uBAAuB;QACvB,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC/C,CAAC;IAED,MAAM,CAAC,EAAE,AAAD,EAAG,IAAI,CAAC,GAAG,KAAK,CAAC;IAEzB,oDAAoD;IACpD,oFAAoF;IACpF,OAAO,EAAE,WAAW,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;AAC/C,CAAC","sourcesContent":["import { stringify as stringifyYAML } from \"yaml\";\n\n/**\n * Creates file content with YAML frontmatter and body content.\n * This is the atomic file creation format that prevents race conditions.\n *\n * @param frontmatter - Frontmatter as JSON object\n * @param content - Body content (markdown text after frontmatter)\n * @returns Complete file content with YAML frontmatter\n */\nexport function createFileContentWithFrontmatter(\n\tfrontmatter: Record<string, unknown>,\n\tcontent = \"\"\n): string {\n\tif (!frontmatter || Object.keys(frontmatter).length === 0) {\n\t\t// No frontmatter, just return content\n\t\treturn content;\n\t}\n\n\t// Use the yaml library to stringify frontmatter\n\t// This handles all edge cases: special characters, multiline strings, arrays, nested objects, etc.\n\tconst yaml = stringifyYAML(frontmatter, {\n\t\t// Use 2-space indentation (Obsidian standard)\n\t\tindent: 2,\n\t\t// Don't add document markers (--- at the end)\n\t\tlineWidth: 0,\n\t\t// Minimize unnecessary quotes\n\t\tdefaultStringType: \"PLAIN\",\n\t\t// Handle null values explicitly\n\t\tnullStr: \"\",\n\t}).trim();\n\n\tif (!yaml) {\n\t\t// Empty frontmatter after stringification\n\t\treturn content;\n\t}\n\n\t// Ensure content doesn't start with extra newlines\n\tconst trimmedContent = content.replace(/^\\n+/, \"\");\n\n\tif (trimmedContent) {\n\t\treturn `---\\n${yaml}\\n---\\n\\n${trimmedContent}`;\n\t}\n\n\treturn `---\\n${yaml}\\n---\\n`;\n}\n\n/**\n * Parses file content to extract frontmatter and body separately.\n * Returns both the frontmatter object and the body content.\n *\n * @param fileContent - Complete file content including frontmatter\n * @returns Object with frontmatter and body\n */\nexport function parseFileContent(fileContent: string): {\n\tfrontmatter: Record<string, unknown>;\n\tbody: string;\n} {\n\tconst frontmatterRegex = /^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/;\n\tconst match = fileContent.match(frontmatterRegex);\n\n\tif (!match) {\n\t\t// No frontmatter found\n\t\treturn { frontmatter: {}, body: fileContent };\n\t}\n\n\tconst [, , body] = match;\n\n\t// For this use case, we just need to split the file\n\t// The frontmatter will be properly parsed when we read it back with Obsidian's APIs\n\treturn { frontmatter: {}, body: body.trim() };\n}\n"]}
|
package/dist/file/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./file-utils";
|
|
|
5
5
|
export * from "./frontmatter";
|
|
6
6
|
export * from "./frontmatter-diff";
|
|
7
7
|
export * from "./frontmatter-propagation";
|
|
8
|
+
export * from "./frontmatter-serialization";
|
|
8
9
|
export * from "./link-parser";
|
|
9
10
|
export * from "./property-utils";
|
|
10
11
|
export * from "./templater";
|
package/dist/file/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
|
package/dist/file/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./file-utils";
|
|
|
5
5
|
export * from "./frontmatter";
|
|
6
6
|
export * from "./frontmatter-diff";
|
|
7
7
|
export * from "./frontmatter-propagation";
|
|
8
|
+
export * from "./frontmatter-serialization";
|
|
8
9
|
export * from "./link-parser";
|
|
9
10
|
export * from "./property-utils";
|
|
10
11
|
export * from "./templater";
|
package/dist/file/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC","sourcesContent":["export * from \"./child-reference\";\nexport * from \"./file\";\nexport * from \"./file-operations\";\nexport * from \"./file-utils\";\nexport * from \"./frontmatter\";\nexport * from \"./frontmatter-diff\";\nexport * from \"./frontmatter-propagation\";\nexport * from \"./link-parser\";\nexport * from \"./property-utils\";\nexport * from \"./templater\";\nexport * from \"./templater-service\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/file/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,QAAQ,CAAC;AACvB,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC","sourcesContent":["export * from \"./child-reference\";\nexport * from \"./file\";\nexport * from \"./file-operations\";\nexport * from \"./file-utils\";\nexport * from \"./frontmatter\";\nexport * from \"./frontmatter-diff\";\nexport * from \"./frontmatter-propagation\";\nexport * from \"./frontmatter-serialization\";\nexport * from \"./link-parser\";\nexport * from \"./property-utils\";\nexport * from \"./templater\";\nexport * from \"./templater-service\";\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { App, TFile } from "obsidian";
|
|
2
2
|
import type { FileCreationOptions } from "./templater";
|
|
3
3
|
export type { FileCreationOptions };
|
|
4
4
|
export declare class TemplaterService {
|
|
@@ -12,7 +12,5 @@ export declare class TemplaterService {
|
|
|
12
12
|
* Creates a file using Templater or falls back to manual creation.
|
|
13
13
|
*/
|
|
14
14
|
createFile(options: FileCreationOptions): Promise<TFile>;
|
|
15
|
-
private shouldUseTemplate;
|
|
16
|
-
private createManually;
|
|
17
15
|
}
|
|
18
16
|
//# sourceMappingURL=templater-service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templater-service.d.ts","sourceRoot":"","sources":["../../src/file/templater-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"templater-service.d.ts","sourceRoot":"","sources":["../../src/file/templater-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAQvD,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAMpC,qBAAa,gBAAgB;IAChB,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,GAAG;IAE5B;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,KAAK,CAAC;CA2B9D"}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
|
-
import {
|
|
3
|
-
import { createFromTemplate, isTemplaterAvailable } from "./templater";
|
|
2
|
+
import { createFileManually, createFromTemplate, isTemplaterAvailable, shouldUseTemplate, } from "./templater";
|
|
4
3
|
// ============================================================================
|
|
5
4
|
// Templater Service (Class-based wrapper)
|
|
6
5
|
// ============================================================================
|
|
@@ -21,49 +20,17 @@ export class TemplaterService {
|
|
|
21
20
|
return __awaiter(this, void 0, void 0, function* () {
|
|
22
21
|
const { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } = options;
|
|
23
22
|
const finalFilename = filename || title;
|
|
24
|
-
// If content is provided, use manual creation to preserve the content
|
|
25
23
|
if (content) {
|
|
26
|
-
return this.
|
|
24
|
+
return createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);
|
|
27
25
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const templateFile = yield createFromTemplate(this.app, templatePath, targetDirectory, finalFilename);
|
|
26
|
+
if (useTemplater && shouldUseTemplate(this.app, templatePath)) {
|
|
27
|
+
const templateFile = yield createFromTemplate(this.app, templatePath, targetDirectory, finalFilename, false, frontmatter);
|
|
31
28
|
if (templateFile) {
|
|
32
|
-
// Apply frontmatter if provided
|
|
33
|
-
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
34
|
-
yield this.app.fileManager.processFrontMatter(templateFile, (fm) => {
|
|
35
|
-
Object.assign(fm, frontmatter);
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
29
|
return templateFile;
|
|
39
30
|
}
|
|
40
31
|
}
|
|
41
32
|
// Fallback to manual creation
|
|
42
|
-
return this.
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
shouldUseTemplate(templatePath) {
|
|
46
|
-
return !!(templatePath && this.isAvailable() && this.app.vault.getFileByPath(templatePath));
|
|
47
|
-
}
|
|
48
|
-
createManually(title, targetDirectory, filename, customContent, frontmatter) {
|
|
49
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
50
|
-
const baseName = filename.replace(/\.md$/, "");
|
|
51
|
-
const filePath = `${targetDirectory}/${baseName}.md`;
|
|
52
|
-
// Check if file already exists
|
|
53
|
-
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
54
|
-
if (existingFile instanceof TFile) {
|
|
55
|
-
return existingFile;
|
|
56
|
-
}
|
|
57
|
-
// Use custom content or default
|
|
58
|
-
const content = customContent || `# ${title}\n\n`;
|
|
59
|
-
const file = yield this.app.vault.create(filePath, content);
|
|
60
|
-
// Apply frontmatter if provided
|
|
61
|
-
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
62
|
-
yield this.app.fileManager.processFrontMatter(file, (fm) => {
|
|
63
|
-
Object.assign(fm, frontmatter);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
return file;
|
|
33
|
+
return createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);
|
|
67
34
|
});
|
|
68
35
|
}
|
|
69
36
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templater-service.js","sourceRoot":"","sources":["../../src/file/templater-service.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"templater-service.js","sourceRoot":"","sources":["../../src/file/templater-service.ts"],"names":[],"mappings":";AAEA,OAAO,EACN,kBAAkB,EAClB,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,GACjB,MAAM,aAAa,CAAC;AAIrB,+EAA+E;AAC/E,0CAA0C;AAC1C,+EAA+E;AAE/E,MAAM,OAAO,gBAAgB;IAC5B,YAAoB,GAAQ;QAAR,QAAG,GAAH,GAAG,CAAK;IAAG,CAAC;IAEhC;;OAEG;IACH,WAAW;QACV,OAAO,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACvC,CAAC;IAED;;OAEG;IACG,UAAU,CAAC,OAA4B;;YAC5C,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,GAC3F,OAAO,CAAC;YAET,MAAM,aAAa,GAAG,QAAQ,IAAI,KAAK,CAAC;YAExC,IAAI,OAAO,EAAE,CAAC;gBACb,OAAO,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;YAC3F,CAAC;YAED,IAAI,YAAY,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,EAAE,CAAC;gBAC/D,MAAM,YAAY,GAAG,MAAM,kBAAkB,CAC5C,IAAI,CAAC,GAAG,EACR,YAAa,EACb,eAAe,EACf,aAAa,EACb,KAAK,EACL,WAAW,CACX,CAAC;gBAEF,IAAI,YAAY,EAAE,CAAC;oBAClB,OAAO,YAAY,CAAC;gBACrB,CAAC;YACF,CAAC;YACD,8BAA8B;YAC9B,OAAO,kBAAkB,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC;QAC3F,CAAC;KAAA;CACD","sourcesContent":["import type { App, TFile } from \"obsidian\";\nimport type { FileCreationOptions } from \"./templater\";\nimport {\n\tcreateFileManually,\n\tcreateFromTemplate,\n\tisTemplaterAvailable,\n\tshouldUseTemplate,\n} from \"./templater\";\n\nexport type { FileCreationOptions };\n\n// ============================================================================\n// Templater Service (Class-based wrapper)\n// ============================================================================\n\nexport class TemplaterService {\n\tconstructor(private app: App) {}\n\n\t/**\n\t * Checks if Templater plugin is installed and enabled.\n\t */\n\tisAvailable(): boolean {\n\t\treturn isTemplaterAvailable(this.app);\n\t}\n\n\t/**\n\t * Creates a file using Templater or falls back to manual creation.\n\t */\n\tasync createFile(options: FileCreationOptions): Promise<TFile> {\n\t\tconst { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } =\n\t\t\toptions;\n\n\t\tconst finalFilename = filename || title;\n\n\t\tif (content) {\n\t\t\treturn createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);\n\t\t}\n\n\t\tif (useTemplater && shouldUseTemplate(this.app, templatePath)) {\n\t\t\tconst templateFile = await createFromTemplate(\n\t\t\t\tthis.app,\n\t\t\t\ttemplatePath!,\n\t\t\t\ttargetDirectory,\n\t\t\t\tfinalFilename,\n\t\t\t\tfalse,\n\t\t\t\tfrontmatter\n\t\t\t);\n\n\t\t\tif (templateFile) {\n\t\t\t\treturn templateFile;\n\t\t\t}\n\t\t}\n\t\t// Fallback to manual creation\n\t\treturn createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);\n\t}\n}\n"]}
|
package/dist/file/templater.d.ts
CHANGED
|
@@ -9,6 +9,20 @@ export interface FileCreationOptions {
|
|
|
9
9
|
useTemplater?: boolean;
|
|
10
10
|
}
|
|
11
11
|
export declare function isTemplaterAvailable(app: App): boolean;
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Checks if a template should be used based on availability and file existence.
|
|
14
|
+
*/
|
|
15
|
+
export declare function shouldUseTemplate(app: App, templatePath: string | undefined): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a file at the specified full path with optional frontmatter and content.
|
|
18
|
+
* Returns existing file if it already exists.
|
|
19
|
+
*/
|
|
20
|
+
export declare function createFileAtPath(app: App, filePath: string, content?: string, frontmatter?: Record<string, unknown>): Promise<TFile>;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a file manually with optional frontmatter and content.
|
|
23
|
+
* Returns existing file if it already exists.
|
|
24
|
+
*/
|
|
25
|
+
export declare function createFileManually(app: App, targetDirectory: string, filename: string, content?: string, frontmatter?: Record<string, unknown>): Promise<TFile>;
|
|
26
|
+
export declare function createFromTemplate(app: App, templatePath: string, targetFolder?: string, filename?: string, openNewNote?: boolean, frontmatter?: Record<string, unknown>): Promise<TFile | null>;
|
|
13
27
|
export declare function createFileWithTemplate(app: App, options: FileCreationOptions): Promise<TFile>;
|
|
14
28
|
//# sourceMappingURL=templater.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAiBlE,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;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAOrF;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACrC,GAAG,EAAE,GAAG,EACR,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,KAAK,CAAC,CAkBhB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,GAAG,EACR,eAAe,EAAE,MAAM,EACvB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,KAAK,CAAC,CAKhB;AAED,wBAAsB,kBAAkB,CACvC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,CAAC,EAAE,MAAM,EACjB,WAAW,UAAQ,EACnB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CA8CvB;AAED,wBAAsB,sBAAsB,CAC3C,GAAG,EAAE,GAAG,EACR,OAAO,EAAE,mBAAmB,GAC1B,OAAO,CAAC,KAAK,CAAC,CA4BhB"}
|
package/dist/file/templater.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
2
|
import { Notice, normalizePath, TFile } from "obsidian";
|
|
3
|
+
import { waitForFileReady } from "./file-utils";
|
|
4
|
+
import { createFileContentWithFrontmatter } from "./frontmatter-serialization";
|
|
3
5
|
const TEMPLATER_ID = "templater-obsidian";
|
|
4
6
|
function waitForTemplater(app_1) {
|
|
5
7
|
return __awaiter(this, arguments, void 0, function* (app, timeoutMs = 8000) {
|
|
@@ -23,8 +25,51 @@ export function isTemplaterAvailable(app) {
|
|
|
23
25
|
const instance = (_b = (_a = app.plugins) === null || _a === void 0 ? void 0 : _a.getPlugin) === null || _b === void 0 ? void 0 : _b.call(_a, TEMPLATER_ID);
|
|
24
26
|
return !!instance;
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Checks if a template should be used based on availability and file existence.
|
|
30
|
+
*/
|
|
31
|
+
export function shouldUseTemplate(app, templatePath) {
|
|
32
|
+
return !!(templatePath &&
|
|
33
|
+
templatePath.trim() !== "" &&
|
|
34
|
+
isTemplaterAvailable(app) &&
|
|
35
|
+
app.vault.getFileByPath(templatePath));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a file at the specified full path with optional frontmatter and content.
|
|
39
|
+
* Returns existing file if it already exists.
|
|
40
|
+
*/
|
|
41
|
+
export function createFileAtPath(app, filePath, content, frontmatter) {
|
|
42
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
43
|
+
// Check if file already exists
|
|
44
|
+
const existingFile = app.vault.getAbstractFileByPath(filePath);
|
|
45
|
+
if (existingFile instanceof TFile) {
|
|
46
|
+
return existingFile;
|
|
47
|
+
}
|
|
48
|
+
const bodyContent = content || "";
|
|
49
|
+
let fileContent;
|
|
50
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
51
|
+
fileContent = createFileContentWithFrontmatter(frontmatter, bodyContent);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
fileContent = bodyContent;
|
|
55
|
+
}
|
|
56
|
+
const file = yield app.vault.create(filePath, fileContent);
|
|
57
|
+
return file;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a file manually with optional frontmatter and content.
|
|
62
|
+
* Returns existing file if it already exists.
|
|
63
|
+
*/
|
|
64
|
+
export function createFileManually(app, targetDirectory, filename, content, frontmatter) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
const baseName = filename.replace(/\.md$/, "");
|
|
67
|
+
const filePath = `${targetDirectory}/${baseName}.md`;
|
|
68
|
+
return createFileAtPath(app, filePath, content, frontmatter);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
26
71
|
export function createFromTemplate(app_1, templatePath_1, targetFolder_1, filename_1) {
|
|
27
|
-
return __awaiter(this, arguments, void 0, function* (app, templatePath, targetFolder, filename, openNewNote = false) {
|
|
72
|
+
return __awaiter(this, arguments, void 0, function* (app, templatePath, targetFolder, filename, openNewNote = false, frontmatter) {
|
|
28
73
|
const templater = yield waitForTemplater(app);
|
|
29
74
|
if (!templater) {
|
|
30
75
|
console.warn("Templater isn't ready yet (or not installed/enabled).");
|
|
@@ -39,7 +84,19 @@ export function createFromTemplate(app_1, templatePath_1, targetFolder_1, filena
|
|
|
39
84
|
}
|
|
40
85
|
try {
|
|
41
86
|
const newFile = yield templater.create_new_note_from_template(templateFile, targetFolder, filename, openNewNote);
|
|
42
|
-
|
|
87
|
+
if (!newFile) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
91
|
+
const readyFile = yield waitForFileReady(app, newFile.path);
|
|
92
|
+
if (readyFile) {
|
|
93
|
+
yield app.fileManager.processFrontMatter(readyFile, (fm) => {
|
|
94
|
+
Object.assign(fm, frontmatter);
|
|
95
|
+
});
|
|
96
|
+
return readyFile;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return newFile;
|
|
43
100
|
}
|
|
44
101
|
catch (error) {
|
|
45
102
|
console.error("Error creating file from template:", error);
|
|
@@ -52,31 +109,18 @@ export function createFileWithTemplate(app, options) {
|
|
|
52
109
|
return __awaiter(this, void 0, void 0, function* () {
|
|
53
110
|
const { title, targetDirectory, filename, content, frontmatter, templatePath, useTemplater } = options;
|
|
54
111
|
const finalFilename = filename || title;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (existingFile instanceof TFile) {
|
|
59
|
-
return existingFile;
|
|
112
|
+
// If content is provided, use manual creation to preserve the content
|
|
113
|
+
if (content) {
|
|
114
|
+
return createFileManually(app, targetDirectory, finalFilename, content, frontmatter);
|
|
60
115
|
}
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
// Try to use template if requested and available
|
|
117
|
+
if (useTemplater && shouldUseTemplate(app, templatePath)) {
|
|
118
|
+
const templateFile = yield createFromTemplate(app, templatePath, targetDirectory, finalFilename, false, frontmatter);
|
|
63
119
|
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
120
|
return templateFile;
|
|
70
121
|
}
|
|
71
122
|
}
|
|
72
|
-
|
|
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;
|
|
123
|
+
return createFileManually(app, targetDirectory, finalFilename, content, frontmatter);
|
|
80
124
|
});
|
|
81
125
|
}
|
|
82
126
|
//# 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,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;
|
|
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\";\nimport { waitForFileReady } from \"./file-utils\";\nimport { createFileContentWithFrontmatter } from \"./frontmatter-serialization\";\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\n/**\n * Checks if a template should be used based on availability and file existence.\n */\nexport function shouldUseTemplate(app: App, templatePath: string | undefined): boolean {\n\treturn !!(\n\t\ttemplatePath &&\n\t\ttemplatePath.trim() !== \"\" &&\n\t\tisTemplaterAvailable(app) &&\n\t\tapp.vault.getFileByPath(templatePath)\n\t);\n}\n\n/**\n * Creates a file at the specified full path with optional frontmatter and content.\n * Returns existing file if it already exists.\n */\nexport async function createFileAtPath(\n\tapp: App,\n\tfilePath: string,\n\tcontent?: string,\n\tfrontmatter?: Record<string, unknown>\n): Promise<TFile> {\n\t// Check if file already exists\n\tconst existingFile = app.vault.getAbstractFileByPath(filePath);\n\tif (existingFile instanceof TFile) {\n\t\treturn existingFile;\n\t}\n\n\tconst bodyContent = content || \"\";\n\n\tlet fileContent: string;\n\tif (frontmatter && Object.keys(frontmatter).length > 0) {\n\t\tfileContent = createFileContentWithFrontmatter(frontmatter, bodyContent);\n\t} else {\n\t\tfileContent = bodyContent;\n\t}\n\n\tconst file = await app.vault.create(filePath, fileContent);\n\treturn file;\n}\n\n/**\n * Creates a file manually with optional frontmatter and content.\n * Returns existing file if it already exists.\n */\nexport async function createFileManually(\n\tapp: App,\n\ttargetDirectory: string,\n\tfilename: string,\n\tcontent?: string,\n\tfrontmatter?: Record<string, unknown>\n): Promise<TFile> {\n\tconst baseName = filename.replace(/\\.md$/, \"\");\n\tconst filePath = `${targetDirectory}/${baseName}.md`;\n\n\treturn createFileAtPath(app, filePath, content, frontmatter);\n}\n\nexport async function createFromTemplate(\n\tapp: App,\n\ttemplatePath: string,\n\ttargetFolder?: string,\n\tfilename?: string,\n\topenNewNote = false,\n\tfrontmatter?: Record<string, unknown>\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\tif (!newFile) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (frontmatter && Object.keys(frontmatter).length > 0) {\n\t\t\tconst readyFile = await waitForFileReady(app, newFile.path);\n\n\t\t\tif (readyFile) {\n\t\t\t\tawait app.fileManager.processFrontMatter(readyFile, (fm) => {\n\t\t\t\t\tObject.assign(fm, frontmatter);\n\t\t\t\t});\n\t\t\t\treturn readyFile;\n\t\t\t}\n\t\t}\n\n\t\treturn newFile;\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\n\t// If content is provided, use manual creation to preserve the content\n\tif (content) {\n\t\treturn createFileManually(app, targetDirectory, finalFilename, content, frontmatter);\n\t}\n\n\t// Try to use template if requested and available\n\tif (useTemplater && shouldUseTemplate(app, templatePath)) {\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\tfalse,\n\t\t\tfrontmatter\n\t\t);\n\n\t\tif (templateFile) {\n\t\t\treturn templateFile;\n\t\t}\n\t}\n\n\treturn createFileManually(app, targetDirectory, finalFilename, content, frontmatter);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real1ty-obsidian-plugins/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "Shared utilities for Obsidian plugins",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"obsidian": "^1.8.7",
|
|
56
56
|
"rxjs": "^7.8.2",
|
|
57
57
|
"tslib": "2.8.1",
|
|
58
|
+
"yaml": "^2.8.2",
|
|
58
59
|
"zod": "^4.1.5"
|
|
59
60
|
},
|
|
60
61
|
"nx": {
|
package/src/file/file-utils.ts
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import type { App } from "obsidian";
|
|
2
2
|
import { TFile } from "obsidian";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Waits for a file to be accessible and readable by Obsidian's cache.
|
|
6
|
+
* This is necessary because Templater creates files asynchronously.
|
|
7
|
+
*/
|
|
8
|
+
export async function waitForFileReady(
|
|
9
|
+
app: App,
|
|
10
|
+
filePath: string,
|
|
11
|
+
timeoutMs = 5000
|
|
12
|
+
): Promise<TFile | null> {
|
|
13
|
+
const started = Date.now();
|
|
14
|
+
|
|
15
|
+
while (Date.now() - started < timeoutMs) {
|
|
16
|
+
const file = app.vault.getAbstractFileByPath(filePath);
|
|
17
|
+
if (file instanceof TFile) {
|
|
18
|
+
// Check if file is readable by trying to get its metadata
|
|
19
|
+
const metadata = app.metadataCache.getFileCache(file);
|
|
20
|
+
if (metadata !== null && metadata !== undefined) {
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
4
30
|
/**
|
|
5
31
|
* Gets a TFile by path or throws an error if not found.
|
|
6
32
|
* Useful when you need to ensure a file exists before proceeding.
|
package/src/file/file.ts
CHANGED
|
@@ -266,6 +266,50 @@ export function getUniqueFilePath(app: App, folder: string, baseName: string): s
|
|
|
266
266
|
return fullPath;
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Generates a unique file path by appending a counter if the file already exists.
|
|
271
|
+
* Accepts a complete file path and iterates with a counter suffix.
|
|
272
|
+
*
|
|
273
|
+
* @param app - The Obsidian App instance
|
|
274
|
+
* @param filePath - Complete file path including folder, name, and extension
|
|
275
|
+
* @returns Unique file path that doesn't exist in the vault
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```ts
|
|
279
|
+
* // If "folder/note.md" exists, returns "folder/note 1.md"
|
|
280
|
+
* const path = getUniqueFilePathFromFull(app, "folder/note.md");
|
|
281
|
+
*
|
|
282
|
+
* // Works with any extension
|
|
283
|
+
* const path = getUniqueFilePathFromFull(app, "assets/image.png");
|
|
284
|
+
* // -> "assets/image 1.png" if image.png exists
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export const getUniqueFilePathFromFull = (app: App, filePath: string): string => {
|
|
288
|
+
// If file doesn't exist, return as-is
|
|
289
|
+
if (!app.vault.getAbstractFileByPath(filePath)) {
|
|
290
|
+
return filePath;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Extract folder, base name, and extension
|
|
294
|
+
const lastSlashIndex = filePath.lastIndexOf("/");
|
|
295
|
+
const folderPath = lastSlashIndex !== -1 ? filePath.substring(0, lastSlashIndex + 1) : "";
|
|
296
|
+
const fileName = lastSlashIndex !== -1 ? filePath.substring(lastSlashIndex + 1) : filePath;
|
|
297
|
+
|
|
298
|
+
const lastDotIndex = fileName.lastIndexOf(".");
|
|
299
|
+
const baseName = lastDotIndex !== -1 ? fileName.substring(0, lastDotIndex) : fileName;
|
|
300
|
+
const extension = lastDotIndex !== -1 ? fileName.substring(lastDotIndex) : "";
|
|
301
|
+
|
|
302
|
+
let counter = 1;
|
|
303
|
+
let uniquePath = `${folderPath}${baseName} ${counter}${extension}`;
|
|
304
|
+
|
|
305
|
+
while (app.vault.getAbstractFileByPath(uniquePath)) {
|
|
306
|
+
counter++;
|
|
307
|
+
uniquePath = `${folderPath}${baseName} ${counter}${extension}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return uniquePath;
|
|
311
|
+
};
|
|
312
|
+
|
|
269
313
|
/**
|
|
270
314
|
* Generates a unique file path by appending a counter if the file already exists.
|
|
271
315
|
* Supports custom file extensions.
|
|
@@ -283,14 +327,8 @@ export const generateUniqueFilePath = (
|
|
|
283
327
|
extension: string = "md"
|
|
284
328
|
): string => {
|
|
285
329
|
const folderPath = folder ? `${folder}/` : "";
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
while (app.vault.getAbstractFileByPath(filePath)) {
|
|
290
|
-
filePath = `${folderPath}${baseName} ${counter++}.${extension}`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return filePath;
|
|
330
|
+
const fullPath = `${folderPath}${baseName}.${extension}`;
|
|
331
|
+
return getUniqueFilePathFromFull(app, fullPath);
|
|
294
332
|
};
|
|
295
333
|
|
|
296
334
|
// ============================================================================
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { stringify as stringifyYAML } from "yaml";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates file content with YAML frontmatter and body content.
|
|
5
|
+
* This is the atomic file creation format that prevents race conditions.
|
|
6
|
+
*
|
|
7
|
+
* @param frontmatter - Frontmatter as JSON object
|
|
8
|
+
* @param content - Body content (markdown text after frontmatter)
|
|
9
|
+
* @returns Complete file content with YAML frontmatter
|
|
10
|
+
*/
|
|
11
|
+
export function createFileContentWithFrontmatter(
|
|
12
|
+
frontmatter: Record<string, unknown>,
|
|
13
|
+
content = ""
|
|
14
|
+
): string {
|
|
15
|
+
if (!frontmatter || Object.keys(frontmatter).length === 0) {
|
|
16
|
+
// No frontmatter, just return content
|
|
17
|
+
return content;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Use the yaml library to stringify frontmatter
|
|
21
|
+
// This handles all edge cases: special characters, multiline strings, arrays, nested objects, etc.
|
|
22
|
+
const yaml = stringifyYAML(frontmatter, {
|
|
23
|
+
// Use 2-space indentation (Obsidian standard)
|
|
24
|
+
indent: 2,
|
|
25
|
+
// Don't add document markers (--- at the end)
|
|
26
|
+
lineWidth: 0,
|
|
27
|
+
// Minimize unnecessary quotes
|
|
28
|
+
defaultStringType: "PLAIN",
|
|
29
|
+
// Handle null values explicitly
|
|
30
|
+
nullStr: "",
|
|
31
|
+
}).trim();
|
|
32
|
+
|
|
33
|
+
if (!yaml) {
|
|
34
|
+
// Empty frontmatter after stringification
|
|
35
|
+
return content;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Ensure content doesn't start with extra newlines
|
|
39
|
+
const trimmedContent = content.replace(/^\n+/, "");
|
|
40
|
+
|
|
41
|
+
if (trimmedContent) {
|
|
42
|
+
return `---\n${yaml}\n---\n\n${trimmedContent}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `---\n${yaml}\n---\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses file content to extract frontmatter and body separately.
|
|
50
|
+
* Returns both the frontmatter object and the body content.
|
|
51
|
+
*
|
|
52
|
+
* @param fileContent - Complete file content including frontmatter
|
|
53
|
+
* @returns Object with frontmatter and body
|
|
54
|
+
*/
|
|
55
|
+
export function parseFileContent(fileContent: string): {
|
|
56
|
+
frontmatter: Record<string, unknown>;
|
|
57
|
+
body: string;
|
|
58
|
+
} {
|
|
59
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n?([\s\S]*)$/;
|
|
60
|
+
const match = fileContent.match(frontmatterRegex);
|
|
61
|
+
|
|
62
|
+
if (!match) {
|
|
63
|
+
// No frontmatter found
|
|
64
|
+
return { frontmatter: {}, body: fileContent };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [, , body] = match;
|
|
68
|
+
|
|
69
|
+
// For this use case, we just need to split the file
|
|
70
|
+
// The frontmatter will be properly parsed when we read it back with Obsidian's APIs
|
|
71
|
+
return { frontmatter: {}, body: body.trim() };
|
|
72
|
+
}
|
package/src/file/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from "./file-utils";
|
|
|
5
5
|
export * from "./frontmatter";
|
|
6
6
|
export * from "./frontmatter-diff";
|
|
7
7
|
export * from "./frontmatter-propagation";
|
|
8
|
+
export * from "./frontmatter-serialization";
|
|
8
9
|
export * from "./link-parser";
|
|
9
10
|
export * from "./property-utils";
|
|
10
11
|
export * from "./templater";
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { App, TFile } from "obsidian";
|
|
2
2
|
import type { FileCreationOptions } from "./templater";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createFileManually,
|
|
5
|
+
createFromTemplate,
|
|
6
|
+
isTemplaterAvailable,
|
|
7
|
+
shouldUseTemplate,
|
|
8
|
+
} from "./templater";
|
|
4
9
|
|
|
5
10
|
export type { FileCreationOptions };
|
|
6
11
|
|
|
@@ -27,67 +32,25 @@ export class TemplaterService {
|
|
|
27
32
|
|
|
28
33
|
const finalFilename = filename || title;
|
|
29
34
|
|
|
30
|
-
// If content is provided, use manual creation to preserve the content
|
|
31
35
|
if (content) {
|
|
32
|
-
return this.
|
|
36
|
+
return createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
if (useTemplater && templatePath && this.shouldUseTemplate(templatePath)) {
|
|
39
|
+
if (useTemplater && shouldUseTemplate(this.app, templatePath)) {
|
|
37
40
|
const templateFile = await createFromTemplate(
|
|
38
41
|
this.app,
|
|
39
|
-
templatePath
|
|
42
|
+
templatePath!,
|
|
40
43
|
targetDirectory,
|
|
41
|
-
finalFilename
|
|
44
|
+
finalFilename,
|
|
45
|
+
false,
|
|
46
|
+
frontmatter
|
|
42
47
|
);
|
|
43
48
|
|
|
44
49
|
if (templateFile) {
|
|
45
|
-
// Apply frontmatter if provided
|
|
46
|
-
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
47
|
-
await this.app.fileManager.processFrontMatter(templateFile, (fm) => {
|
|
48
|
-
Object.assign(fm, frontmatter);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
50
|
return templateFile;
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
|
-
|
|
55
53
|
// Fallback to manual creation
|
|
56
|
-
return this.
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
private shouldUseTemplate(templatePath: string): boolean {
|
|
60
|
-
return !!(templatePath && this.isAvailable() && this.app.vault.getFileByPath(templatePath));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
private async createManually(
|
|
64
|
-
title: string,
|
|
65
|
-
targetDirectory: string,
|
|
66
|
-
filename: string,
|
|
67
|
-
customContent?: string,
|
|
68
|
-
frontmatter?: Record<string, unknown>
|
|
69
|
-
): Promise<TFile> {
|
|
70
|
-
const baseName = filename.replace(/\.md$/, "");
|
|
71
|
-
const filePath = `${targetDirectory}/${baseName}.md`;
|
|
72
|
-
|
|
73
|
-
// Check if file already exists
|
|
74
|
-
const existingFile = this.app.vault.getAbstractFileByPath(filePath);
|
|
75
|
-
if (existingFile instanceof TFile) {
|
|
76
|
-
return existingFile;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Use custom content or default
|
|
80
|
-
const content = customContent || `# ${title}\n\n`;
|
|
81
|
-
|
|
82
|
-
const file = await this.app.vault.create(filePath, content);
|
|
83
|
-
|
|
84
|
-
// Apply frontmatter if provided
|
|
85
|
-
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
86
|
-
await this.app.fileManager.processFrontMatter(file, (fm) => {
|
|
87
|
-
Object.assign(fm, frontmatter);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return file;
|
|
54
|
+
return createFileManually(this.app, targetDirectory, finalFilename, content, frontmatter);
|
|
92
55
|
}
|
|
93
56
|
}
|
package/src/file/templater.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type App, Notice, normalizePath, TFile } from "obsidian";
|
|
2
|
+
import { waitForFileReady } from "./file-utils";
|
|
3
|
+
import { createFileContentWithFrontmatter } from "./frontmatter-serialization";
|
|
2
4
|
|
|
3
5
|
const TEMPLATER_ID = "templater-obsidian";
|
|
4
6
|
|
|
@@ -45,12 +47,71 @@ export function isTemplaterAvailable(app: App): boolean {
|
|
|
45
47
|
return !!instance;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Checks if a template should be used based on availability and file existence.
|
|
52
|
+
*/
|
|
53
|
+
export function shouldUseTemplate(app: App, templatePath: string | undefined): boolean {
|
|
54
|
+
return !!(
|
|
55
|
+
templatePath &&
|
|
56
|
+
templatePath.trim() !== "" &&
|
|
57
|
+
isTemplaterAvailable(app) &&
|
|
58
|
+
app.vault.getFileByPath(templatePath)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a file at the specified full path with optional frontmatter and content.
|
|
64
|
+
* Returns existing file if it already exists.
|
|
65
|
+
*/
|
|
66
|
+
export async function createFileAtPath(
|
|
67
|
+
app: App,
|
|
68
|
+
filePath: string,
|
|
69
|
+
content?: string,
|
|
70
|
+
frontmatter?: Record<string, unknown>
|
|
71
|
+
): Promise<TFile> {
|
|
72
|
+
// Check if file already exists
|
|
73
|
+
const existingFile = app.vault.getAbstractFileByPath(filePath);
|
|
74
|
+
if (existingFile instanceof TFile) {
|
|
75
|
+
return existingFile;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bodyContent = content || "";
|
|
79
|
+
|
|
80
|
+
let fileContent: string;
|
|
81
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
82
|
+
fileContent = createFileContentWithFrontmatter(frontmatter, bodyContent);
|
|
83
|
+
} else {
|
|
84
|
+
fileContent = bodyContent;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const file = await app.vault.create(filePath, fileContent);
|
|
88
|
+
return file;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a file manually with optional frontmatter and content.
|
|
93
|
+
* Returns existing file if it already exists.
|
|
94
|
+
*/
|
|
95
|
+
export async function createFileManually(
|
|
96
|
+
app: App,
|
|
97
|
+
targetDirectory: string,
|
|
98
|
+
filename: string,
|
|
99
|
+
content?: string,
|
|
100
|
+
frontmatter?: Record<string, unknown>
|
|
101
|
+
): Promise<TFile> {
|
|
102
|
+
const baseName = filename.replace(/\.md$/, "");
|
|
103
|
+
const filePath = `${targetDirectory}/${baseName}.md`;
|
|
104
|
+
|
|
105
|
+
return createFileAtPath(app, filePath, content, frontmatter);
|
|
106
|
+
}
|
|
107
|
+
|
|
48
108
|
export async function createFromTemplate(
|
|
49
109
|
app: App,
|
|
50
110
|
templatePath: string,
|
|
51
111
|
targetFolder?: string,
|
|
52
112
|
filename?: string,
|
|
53
|
-
openNewNote = false
|
|
113
|
+
openNewNote = false,
|
|
114
|
+
frontmatter?: Record<string, unknown>
|
|
54
115
|
): Promise<TFile | null> {
|
|
55
116
|
const templater = await waitForTemplater(app);
|
|
56
117
|
if (!templater) {
|
|
@@ -76,7 +137,22 @@ export async function createFromTemplate(
|
|
|
76
137
|
openNewNote
|
|
77
138
|
);
|
|
78
139
|
|
|
79
|
-
|
|
140
|
+
if (!newFile) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (frontmatter && Object.keys(frontmatter).length > 0) {
|
|
145
|
+
const readyFile = await waitForFileReady(app, newFile.path);
|
|
146
|
+
|
|
147
|
+
if (readyFile) {
|
|
148
|
+
await app.fileManager.processFrontMatter(readyFile, (fm) => {
|
|
149
|
+
Object.assign(fm, frontmatter);
|
|
150
|
+
});
|
|
151
|
+
return readyFile;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return newFile;
|
|
80
156
|
} catch (error) {
|
|
81
157
|
console.error("Error creating file from template:", error);
|
|
82
158
|
new Notice("Error creating file from template. Please ensure the template file is valid.");
|
|
@@ -92,40 +168,27 @@ export async function createFileWithTemplate(
|
|
|
92
168
|
options;
|
|
93
169
|
|
|
94
170
|
const finalFilename = filename || title;
|
|
95
|
-
const baseName = finalFilename.replace(/\.md$/, "");
|
|
96
|
-
const filePath = normalizePath(`${targetDirectory}/${baseName}.md`);
|
|
97
171
|
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
return
|
|
172
|
+
// If content is provided, use manual creation to preserve the content
|
|
173
|
+
if (content) {
|
|
174
|
+
return createFileManually(app, targetDirectory, finalFilename, content, frontmatter);
|
|
101
175
|
}
|
|
102
176
|
|
|
103
|
-
|
|
177
|
+
// Try to use template if requested and available
|
|
178
|
+
if (useTemplater && shouldUseTemplate(app, templatePath)) {
|
|
104
179
|
const templateFile = await createFromTemplate(
|
|
105
180
|
app,
|
|
106
|
-
templatePath
|
|
181
|
+
templatePath!,
|
|
107
182
|
targetDirectory,
|
|
108
|
-
finalFilename
|
|
183
|
+
finalFilename,
|
|
184
|
+
false,
|
|
185
|
+
frontmatter
|
|
109
186
|
);
|
|
110
187
|
|
|
111
188
|
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
189
|
return templateFile;
|
|
118
190
|
}
|
|
119
191
|
}
|
|
120
192
|
|
|
121
|
-
|
|
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;
|
|
193
|
+
return createFileManually(app, targetDirectory, finalFilename, content, frontmatter);
|
|
131
194
|
}
|