diff options
Diffstat (limited to 'src/calendar/daily-notes.ts')
| -rw-r--r-- | src/calendar/daily-notes.ts | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/src/calendar/daily-notes.ts b/src/calendar/daily-notes.ts new file mode 100644 index 0000000..2e66834 --- /dev/null +++ b/src/calendar/daily-notes.ts @@ -0,0 +1,223 @@ +import { App, TFile, Vault, WorkspaceLeaf } from "obsidian"; +import type { Moment } from "moment"; + +const DATE_FORMAT = "YYYY-MM-DD"; + +/** + * Computes the vault-relative path for a daily note. + * Format: {rootFolder}/{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md + */ +export function getDailyNotePath(date: Moment, rootFolder: string): string { + const year = date.format("YYYY"); + const month = date.format("MM"); + const day = date.format("DD"); + const filename = date.format(DATE_FORMAT); + return `${rootFolder}/${year}/${month}/${day}/${filename}.md`; +} + +/** + * Looks up an existing daily note in the vault for the given date. + * Returns null if the file does not exist. + */ +export function getDailyNote( + app: App, + date: Moment, + rootFolder: string, +): TFile | null { + const path = getDailyNotePath(date, rootFolder); + return app.vault.getFileByPath(path); +} + +/** + * Creates a new daily note for the given date. + * Creates parent folders ({rootFolder}/{YYYY}/{MM}/{DD}/) if they don't exist. + * If a template path is provided and the template file exists, its content is + * used with `{{date}}` placeholders replaced by the ISO date string. + * Otherwise, a minimal file with date frontmatter is created. + */ +export async function createDailyNote( + app: App, + date: Moment, + rootFolder: string, + template?: string, +): Promise<TFile> { + const path = getDailyNotePath(date, rootFolder); + const dateStr = date.format(DATE_FORMAT); + + // Ensure parent folders exist + const folderPath = path.substring(0, path.lastIndexOf("/")); + await ensureFolderExists(app.vault, folderPath); + + // Resolve content + let content: string; + if (template !== undefined && template !== "") { + const templateFile = app.vault.getFileByPath(template); + if (templateFile !== null) { + const raw = await app.vault.cachedRead(templateFile); + content = raw.replace(/\{\{date\}\}/g, dateStr); + } else { + content = defaultDailyNoteContent(dateStr); + } + } else { + content = defaultDailyNoteContent(dateStr); + } + + return app.vault.create(path, content); +} + +/** + * Opens an existing daily note or creates one first, then opens it. + * When `newLeaf` is true, opens in a new tab; otherwise reuses the current leaf. + */ +export async function openDailyNote( + app: App, + date: Moment, + rootFolder: string, + opts: { newLeaf: boolean }, + template?: string, +): Promise<void> { + let file = getDailyNote(app, date, rootFolder); + if (file === null) { + file = await createDailyNote(app, date, rootFolder, template); + } + + const leaf: WorkspaceLeaf = app.workspace.getLeaf(opts.newLeaf); + await leaf.openFile(file); +} + +/** + * Scans the calendar root folder recursively and builds an index of all + * daily notes, keyed by their ISO date string ("YYYY-MM-DD"). + * + * Only files that match the expected `{YYYY}/{MM}/{DD}/{YYYY-MM-DD}.md` + * structure within the root folder are included. + */ +export function indexDailyNotes( + app: App, + rootFolder: string, +): Map<string, TFile> { + const index = new Map<string, TFile>(); + const root = app.vault.getFolderByPath(rootFolder); + if (root === null) { + return index; + } + + // Pattern: rootFolder/YYYY/MM/DD/YYYY-MM-DD.md + // After stripping rootFolder prefix, remainder is: YYYY/MM/DD/YYYY-MM-DD.md + const datePathRegex = /^(\d{4})\/(\d{2})\/(\d{2})\/(\d{4}-\d{2}-\d{2})\.md$/; + + Vault.recurseChildren(root, (abstractFile) => { + if (!(abstractFile instanceof TFile)) { + return; + } + if (abstractFile.extension !== "md") { + return; + } + + const relativePath = abstractFile.path.substring(rootFolder.length + 1); + const match = datePathRegex.exec(relativePath); + if (match === null) { + return; + } + + const [, year, month, day, filename] = match; + if ( + year === undefined || + month === undefined || + day === undefined || + filename === undefined + ) { + return; + } + + // Verify the folder components match the filename + const expectedFilename = `${year}-${month}-${day}`; + if (filename !== expectedFilename) { + return; + } + + // Validate it's a real date + const m = window.moment(filename, DATE_FORMAT, true); + if (!m.isValid()) { + return; + } + + index.set(filename, abstractFile); + }); + + return index; +} + +/** + * Given a TFile, determines whether it lives in the daily note folder + * structure and extracts the date if so. + * Returns null if the file is not a recognized daily note. + */ +export function getDateFromDailyNote( + file: TFile, + rootFolder: string, +): Moment | null { + // File must be under the root folder + if (!file.path.startsWith(rootFolder + "/")) { + return null; + } + + const relativePath = file.path.substring(rootFolder.length + 1); + const datePathRegex = /^(\d{4})\/(\d{2})\/(\d{2})\/(\d{4}-\d{2}-\d{2})\.md$/; + const match = datePathRegex.exec(relativePath); + if (match === null) { + return null; + } + + const [, year, month, day, filename] = match; + if ( + year === undefined || + month === undefined || + day === undefined || + filename === undefined + ) { + return null; + } + + const expectedFilename = `${year}-${month}-${day}`; + if (filename !== expectedFilename) { + return null; + } + + const m = window.moment(filename, DATE_FORMAT, true); + if (!m.isValid()) { + return null; + } + + return m; +} + +// --------------------------------------------------------------------------- +// Internal helpers +// --------------------------------------------------------------------------- + +/** + * Recursively creates folders for the given path if they don't already exist. + */ +async function ensureFolderExists(vault: Vault, folderPath: string): Promise<void> { + if (vault.getFolderByPath(folderPath) !== null) { + return; + } + + // Walk from root to deepest folder, creating each segment + const segments = folderPath.split("/"); + let current = ""; + for (const segment of segments) { + current = current === "" ? segment : `${current}/${segment}`; + if (vault.getFolderByPath(current) === null) { + await vault.createFolder(current); + } + } +} + +/** + * Returns the default content for a new daily note with date frontmatter. + */ +function defaultDailyNoteContent(dateStr: string): string { + return `---\ndate: ${dateStr}\n---\n`; +} |
