├── .eslintrc.js ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.d.ts ├── package.json ├── rollup.config.js ├── src ├── __mocks__ │ ├── obsidian.ts │ └── path.ts ├── __tests__ │ ├── daily.spec.ts │ ├── monthly.spec.ts │ ├── parse.spec.ts │ ├── quarterly.spec.ts │ ├── vault.spec.ts │ ├── weekly.spec.ts │ └── yearly.spec.ts ├── constants.ts ├── daily.ts ├── index.ts ├── monthly.ts ├── parse.ts ├── quarterly.ts ├── settings.ts ├── testUtils │ ├── mockApp.ts │ └── utils.ts ├── types.ts ├── vault.ts ├── weekly.ts └── yearly.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: "@typescript-eslint/parser", 4 | plugins: ["@typescript-eslint"], 5 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 6 | rules: { 7 | "@typescript-eslint/no-unused-vars": [ 8 | 2, 9 | { args: "all", argsIgnorePattern: "^_" }, 10 | ], 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [liamcain] 2 | custom: ["https://paypal.me/hiliam", "https://buymeacoffee.com/liamcain"] 3 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint-and-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Install modules 16 | run: yarn 17 | 18 | - name: Lint 19 | run: yarn run lint 20 | 21 | - name: Run tests 22 | run: yarn run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | main.js 3 | dist/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | rollup.config.js 2 | src 3 | yarn.lock -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Liam Cain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/obsidian-daily-notes-interface.png?mini=true)](https://npmjs.org/package/obsidian-daily-notes-interface) 2 | 3 | # Obsidian Daily Notes interface 4 | 5 | A collection of utility functions for working with dates and daily notes in Obsidian plugins. It reads from the user's Daily Notes settings to provide a consistent interface. 6 | 7 | ## Installation 8 | 9 | The best way to use this package is to add it to your dependencies: 10 | 11 | ``` 12 | # if you use npm: 13 | npm install --save obsidian-daily-notes-interface 14 | 15 | # or if you use Yarn: 16 | yarn add obsidian-daily-notes-interface 17 | ``` 18 | 19 | ## Utilities 20 | 21 | ### createDailyNote 22 | 23 | Replicates the Daily Notes plugin in Obsidian but allows creating a note for any day (past or present). 24 | 25 | #### Usage 26 | 27 | ```ts 28 | import { createDailyNote } from 'obsidian-daily-notes-interface'; 29 | ... 30 | const date = moment(); 31 | createDailyNote(date); 32 | ``` 33 | 34 | > Note: if you pass in a past or future date, {{date}} tokens in the user's daily notes template will resolve to the correct date. 35 | 36 | ### appHasDailyNotesPluginLoaded 37 | 38 | Check if the user has the Daily Notes plugin enabled. 39 | 40 | ### getAllDailyNotes 41 | 42 | Returns a map of all daily notes, keyed off by their `dateUID`. 43 | 44 | ### getDailyNote 45 | 46 | Returns the Daily Note for a given `Moment`. For performance reasons, this requires passing in the collection of all daily notes. 47 | 48 | ### getDailyNoteSettings 49 | 50 | Returns the settings stored in the Daily Notes plugin (`format`, `folder`, and `template`). 51 | 52 | ### getTemplateInfo 53 | 54 | Generic utility for reading the contents of a file given it's relative path. This does not apply any transformations. 55 | 56 | ## FAQ 57 | 58 | ### What is a `dateUID`? 59 | 60 | A `dateUID` uniquely identifies a note, allowing for faster note lookup. It is prefixed by a granularity: `day`, `week`, `month` to allow for additional supporting additional note types (the Calendar plugin uses this for Weekly Notes currently). 61 | 62 | ### Why do I have to pass in the a map of daily notes to `getDailyNote()`? 63 | 64 | This allows you to cache the collection of dailyNotes for a significant speed up. 65 | 66 | ## Sponsors 🙏 67 | 68 | A big thank you to everyone that has sponsored this project. 69 | 70 | - [Carlo Zottman](https://github.com/czottmann), creator of [Actions for Obsidian](https://actions.work/actions-for-obsidian) 71 | - [Brian Grohe](https://github.com/paxnovem) 72 | - [Ben Hong](https://github.com/bencodezen) 73 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { TFile } from "obsidian"; 3 | 4 | export interface IPeriodicNoteSettings { 5 | folder?: string; 6 | format?: string; 7 | template?: string; 8 | } 9 | 10 | // Errors 11 | export class DailyNotesFolderMissingError extends Error {} 12 | export class WeeklyNotesFolderMissingError extends Error {} 13 | export class MonthlyNotesFolderMissingError extends Error {} 14 | export class QuarterlyNotesFolderMissingError extends Error {} 15 | export class YearlyNotesFolderMissingError extends Error {} 16 | 17 | // Constants 18 | export const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD"; 19 | export const DEFAULT_WEEKLY_NOTE_FORMAT = "gggg-[W]ww"; 20 | export const DEFAULT_MONTHLY_NOTE_FORMAT = "YYYY-MM"; 21 | export const DEFAULT_QUARTERLY_NOTE_FORMAT = "YYYY-[Q]Q"; 22 | export const DEFAULT_YEARLY_NOTE_FORMAT = "YYYY"; 23 | 24 | export type IGranularity = "day" | "week" | "month" | "quarter" | "year"; 25 | 26 | interface IFold { 27 | from: number; 28 | to: number; 29 | } 30 | 31 | interface IFoldInfo { 32 | folds: IFold[]; 33 | } 34 | 35 | // Utils 36 | export function getDateFromFile( 37 | file: TFile, 38 | granularity: IGranularity 39 | ): Moment | null; 40 | export function getDateFromPath( 41 | path: string, 42 | granularity: IGranularity 43 | ): Moment | null; 44 | export function getDateUID(date: Moment, granularity: IGranularity): string; 45 | export function getTemplateInfo(template: string): Promise<[string, IFoldInfo]>; 46 | 47 | // Daily 48 | export function appHasDailyNotesPluginLoaded(): boolean; 49 | export function createDailyNote(date: Moment): Promise; 50 | export function getDailyNote( 51 | date: Moment, 52 | dailyNotes: Record 53 | ): TFile; 54 | export function getAllDailyNotes(): Record; 55 | export function getDailyNoteSettings(): IPeriodicNoteSettings; 56 | 57 | // Weekly 58 | export function appHasWeeklyNotesPluginLoaded(): boolean; 59 | export function createWeeklyNote(date: Moment): Promise; 60 | export function getWeeklyNote( 61 | date: Moment, 62 | weeklyNotes: Record 63 | ): TFile; 64 | export function getAllWeeklyNotes(): Record; 65 | export function getWeeklyNoteSettings(): IPeriodicNoteSettings; 66 | 67 | // Monthly 68 | export function appHasMonthlyNotesPluginLoaded(): boolean; 69 | export function createMonthlyNote(date: Moment): Promise; 70 | export function getMonthlyNote( 71 | date: Moment, 72 | monthlyNotes: Record 73 | ): TFile; 74 | export function getAllMonthlyNotes(): Record; 75 | export function getMonthlyNoteSettings(): IPeriodicNoteSettings; 76 | 77 | // Quarterly 78 | export function appHasQuarterlyNotesPluginLoaded(): boolean; 79 | export function createQuarterlyNote(date: Moment): Promise; 80 | export function getQuarterlyNote( 81 | date: Moment, 82 | quarterlyNotes: Record 83 | ): TFile; 84 | export function getAllQuarterlyNotes(): Record; 85 | export function getQuarterlyNoteSettings(): IPeriodicNoteSettings; 86 | 87 | // Yearly 88 | export function appHasYearlyNotesPluginLoaded(): boolean; 89 | export function createYearlyNote(date: Moment): Promise; 90 | export function getYearlyNote( 91 | date: Moment, 92 | yearlyNotes: Record 93 | ): TFile; 94 | export function getAllYearlyNotes(): Record; 95 | export function getYearlyNoteSettings(): IPeriodicNoteSettings; 96 | 97 | // Generic 98 | export function getPeriodicNoteSettings( 99 | granularity: IGranularity 100 | ): IPeriodicNoteSettings; 101 | export function createPeriodicNote( 102 | granularity: IGranularity, 103 | date: Moment 104 | ): Promise; 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-daily-notes-interface", 3 | "version": "0.9.4", 4 | "description": "Interface for creating daily notes in Obsidian", 5 | "author": "liamcain", 6 | "main": "./dist/main.js", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/liamcain/obsidian-daily-notes-interface" 11 | }, 12 | "bin": { 13 | "obsidian-daily-notes-interface": "./dist/main.js" 14 | }, 15 | "scripts": { 16 | "lint": "eslint . --ext .ts", 17 | "build": "npm run lint && rollup -c", 18 | "test": "jest", 19 | "test:watch": "yarn test -- --watch" 20 | }, 21 | "dependencies": { 22 | "obsidian": "obsidianmd/obsidian-api#master", 23 | "tslib": "2.1.0" 24 | }, 25 | "devDependencies": { 26 | "@rollup/plugin-commonjs": "18.0.0", 27 | "@rollup/plugin-node-resolve": "11.2.1", 28 | "@rollup/plugin-typescript": "8.2.1", 29 | "@types/jest": "26.0.22", 30 | "@types/moment": "2.13.0", 31 | "@typescript-eslint/eslint-plugin": "4.20.0", 32 | "@typescript-eslint/parser": "4.20.0", 33 | "eslint": "7.23.0", 34 | "jest": "26.6.3", 35 | "moment": "2.29.1", 36 | "moment-timezone": "0.5.33", 37 | "rollup": "2.44.0", 38 | "ts-jest": "26.5.4", 39 | "typescript": "4.2.3" 40 | }, 41 | "jest": { 42 | "clearMocks": true, 43 | "moduleNameMapper": { 44 | "src/(.*)": "/src/$1" 45 | }, 46 | "transform": { 47 | "^.+\\.ts$": "ts-jest" 48 | }, 49 | "moduleFileExtensions": [ 50 | "js", 51 | "ts" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | 5 | import pkg from "./package.json"; 6 | 7 | export default { 8 | input: "src/index.ts", 9 | output: { 10 | format: "cjs", 11 | file: "dist/main.js", 12 | name: pkg.name, 13 | }, 14 | external: ["obsidian"], 15 | plugins: [ 16 | typescript(), 17 | resolve({ 18 | browser: true, 19 | }), 20 | commonjs({ 21 | include: "node_modules/**", 22 | }), 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /src/__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | export class TAbstractFile {} 2 | 3 | export class TFile extends TAbstractFile { 4 | public basename: string; 5 | public path: string; 6 | } 7 | 8 | export class TFolder extends TAbstractFile { 9 | public children: TAbstractFile[]; 10 | public path: string; 11 | } 12 | 13 | export class PluginSettingTab {} 14 | export class Modal {} 15 | export class Notice {} 16 | export function normalizePath(notePath: string): string { 17 | if (!notePath.startsWith("/")) { 18 | return `/${notePath}`; 19 | } 20 | return notePath; 21 | } 22 | export class Vault { 23 | static recurseChildren(folder: TFolder, cb: (file: TFile) => void): void { 24 | folder.children.forEach((file) => { 25 | if (file instanceof TFile) { 26 | cb(file); 27 | } else if (file instanceof TFolder) { 28 | Vault.recurseChildren(file, cb); 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/__mocks__/path.ts: -------------------------------------------------------------------------------- 1 | // Always use / for join to simplify cross-platform unit testing 2 | export function join(pathA: string, pathB: string): string { 3 | return `${pathA}/${pathB}`; 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/daily.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import getMockApp, { createFile, createFolder } from "src/testUtils/mockApp"; 4 | 5 | import * as dailyNotesInterface from "../index"; 6 | import * as vaultUtils from "../vault"; 7 | import { setDailyConfig, setPeriodicNotesConfig } from "../testUtils/utils"; 8 | 9 | describe("getDailyNoteSettings", () => { 10 | beforeEach(() => { 11 | window.app = getMockApp(); 12 | window.existingFiles = {}; 13 | window.moment = moment; 14 | moment.tz.setDefault("America/New_York"); 15 | }); 16 | 17 | test("returns all the daily note settings", () => { 18 | setDailyConfig({ 19 | folder: "foo", 20 | format: "YYYY-MM-DD-HHmm", 21 | template: "template", 22 | }); 23 | 24 | expect(dailyNotesInterface.getDailyNoteSettings()).toEqual({ 25 | folder: "foo", 26 | format: "YYYY-MM-DD-HHmm", 27 | template: "template", 28 | }); 29 | }); 30 | 31 | test("cleanses data", () => { 32 | setDailyConfig({ 33 | folder: " foo/bar ", 34 | format: "MMM YYYY-MM-DD", 35 | template: " path/to/template ", 36 | }); 37 | 38 | expect(dailyNotesInterface.getDailyNoteSettings()).toEqual({ 39 | folder: "foo/bar", 40 | format: "MMM YYYY-MM-DD", 41 | template: "path/to/template", 42 | }); 43 | }); 44 | 45 | test("returns defaults if daily note settings don't exist", () => { 46 | expect(dailyNotesInterface.getDailyNoteSettings()).toEqual({ 47 | format: "YYYY-MM-DD", 48 | folder: "", 49 | template: "", 50 | }); 51 | }); 52 | 53 | test("uses settings from core daily notes if periodic-notes' `daily` is disabled", () => { 54 | setDailyConfig({ 55 | folder: " foo ", 56 | format: "YYYY/MM/MMM/YYYY-MM-DD", 57 | template: " path/to/daily ", 58 | }); 59 | 60 | setPeriodicNotesConfig("daily", { 61 | enabled: false, 62 | folder: " foo/bar ", 63 | format: "MMM YYYY-MM-DD", 64 | template: " path/to/template ", 65 | }); 66 | 67 | expect(dailyNotesInterface.getDailyNoteSettings()).toEqual({ 68 | folder: "foo", 69 | format: "YYYY/MM/MMM/YYYY-MM-DD", 70 | template: "path/to/daily", 71 | }); 72 | }); 73 | 74 | test("uses settings from Periodic Notes if periodic-notes and daily notes are both enabled", () => { 75 | setDailyConfig({ 76 | folder: " foo/bar ", 77 | format: "YYYY/MM/MMM/YYYY-MM-DD", 78 | template: " path/to/template ", 79 | }); 80 | 81 | setPeriodicNotesConfig("daily", { 82 | enabled: true, 83 | folder: " foo/bar ", 84 | format: "MMM YYYY-MM-DD", 85 | template: " path/to/template ", 86 | }); 87 | 88 | expect(dailyNotesInterface.getDailyNoteSettings()).toEqual({ 89 | format: "MMM YYYY-MM-DD", 90 | folder: "foo/bar", 91 | template: "path/to/template", 92 | }); 93 | }); 94 | }); 95 | 96 | describe("appHasDailyNotesPluginLoaded", () => { 97 | beforeEach(() => { 98 | window.app = getMockApp(); 99 | window.existingFiles = {}; 100 | window.moment = moment; 101 | }); 102 | 103 | test("returns true when daily notes plugin is enabled", () => { 104 | // eslint-disable-next-line 105 | (window.app).internalPlugins.plugins["daily-notes"].enabled = true; 106 | 107 | expect(dailyNotesInterface.appHasDailyNotesPluginLoaded()).toEqual(true); 108 | }); 109 | 110 | test("returns true when daily notes plugin is enabled", () => { 111 | // eslint-disable-next-line 112 | (window.app).internalPlugins.plugins["daily-notes"].enabled = false; 113 | 114 | expect(dailyNotesInterface.appHasDailyNotesPluginLoaded()).toEqual(false); 115 | }); 116 | }); 117 | 118 | describe("getAllDailyNotes", () => { 119 | beforeEach(() => { 120 | window.app = getMockApp(); 121 | window.moment = moment; 122 | window.existingFiles = {}; 123 | }); 124 | 125 | test("throws error if daily note folder can't be found", () => { 126 | setDailyConfig({ 127 | folder: "missing-folder/", 128 | format: "YYYY-MM-DD", 129 | template: "template", 130 | }); 131 | 132 | expect(dailyNotesInterface.getAllDailyNotes).toThrow( 133 | "Failed to find daily notes folder" 134 | ); 135 | }); 136 | 137 | test("returns a list of all daily notes with no nested folders", () => { 138 | setDailyConfig({ 139 | folder: "/", 140 | format: "YYYY-MM-DD", 141 | template: "template", 142 | }); 143 | 144 | const fileA = createFile("2020-12-01", ""); 145 | const fileB = createFile("2020-12-02", ""); 146 | const fileC = createFile("2020-12-03", ""); 147 | createFolder("/", [fileA, fileB, fileC]); 148 | 149 | expect(dailyNotesInterface.getAllDailyNotes()).toEqual({ 150 | "day-2020-12-01T00:00:00-05:00": fileA, 151 | "day-2020-12-02T00:00:00-05:00": fileB, 152 | "day-2020-12-03T00:00:00-05:00": fileC, 153 | }); 154 | }); 155 | 156 | test("returns a list of all daily notes including files nested in folders", () => { 157 | setDailyConfig({ 158 | folder: "/", 159 | format: "YYYY-MM-DD", 160 | template: "template", 161 | }); 162 | 163 | const fileA = createFile("2020-12-01", ""); 164 | const fileB = createFile("2020-12-02", ""); 165 | const fileC = createFile("2020-12-03", ""); 166 | createFolder("/", [fileA, fileB, createFolder("foo", [fileC])]); 167 | 168 | expect(dailyNotesInterface.getAllDailyNotes()).toEqual({ 169 | "day-2020-12-01T00:00:00-05:00": fileA, 170 | "day-2020-12-02T00:00:00-05:00": fileB, 171 | "day-2020-12-03T00:00:00-05:00": fileC, 172 | }); 173 | }); 174 | }); 175 | 176 | describe("createDailyNote", () => { 177 | beforeEach(() => { 178 | window.app = getMockApp(); 179 | window.moment = moment.bind(null, "2021-02-15T14:06:00-05:00"); 180 | moment.tz.setDefault("America/New_York"); 181 | }); 182 | 183 | test("replaces all mustaches in template", async () => { 184 | const getTemplateInfo = jest.spyOn(vaultUtils, "getTemplateInfo"); 185 | getTemplateInfo.mockResolvedValue([ 186 | ` 187 | {{date}} 188 | {{time}} 189 | {{title}} 190 | {{yesterday}} 191 | {{tomorrow}} 192 | {{date:YYYY}}-{{date:MM-DD}} 193 | {{date-1d:YYYY-MM-DD}} 194 | {{date+2d:YYYY-MM-DD}} 195 | {{date+1M:YYYY-MM-DD}} 196 | {{date+10y:YYYY-MM-DD}} 197 | {{date +7d}} 198 | `, 199 | null, 200 | ]); 201 | 202 | setDailyConfig({ 203 | folder: "/", 204 | format: "YYYY-MM-DD", 205 | template: "template", 206 | }); 207 | 208 | await dailyNotesInterface.createDailyNote(window.moment()); 209 | 210 | expect(window.app.vault.create).toHaveBeenCalledWith( 211 | "/2021-02-15.md", 212 | ` 213 | 2021-02-15 214 | 14:06 215 | 2021-02-15 216 | 2021-02-14 217 | 2021-02-16 218 | 2021-02-15 219 | 2021-02-14 220 | 2021-02-17 221 | 2021-03-15 222 | 2031-02-15 223 | 2021-02-22 224 | ` 225 | ); 226 | }); 227 | }); 228 | -------------------------------------------------------------------------------- /src/__tests__/monthly.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import getMockApp, { createFile, createFolder } from "src/testUtils/mockApp"; 4 | 5 | import * as dailyNotesInterface from "../index"; 6 | import { setMonthlyConfig } from "../testUtils/utils"; 7 | import * as vaultUtils from "../vault"; 8 | 9 | jest.mock("path"); 10 | 11 | moment.tz.setDefault("America/New_York"); 12 | 13 | describe("getMonthlyNoteSettings", () => { 14 | beforeEach(() => { 15 | window.app = getMockApp(); 16 | window.existingFiles = {}; 17 | window.moment = moment; 18 | }); 19 | 20 | test("returns all the monthly note settings", () => { 21 | setMonthlyConfig({ 22 | enabled: true, 23 | folder: "foo", 24 | format: "YYYY-MM", 25 | template: "template", 26 | }); 27 | 28 | expect(dailyNotesInterface.getMonthlyNoteSettings()).toEqual({ 29 | folder: "foo", 30 | format: "YYYY-MM", 31 | template: "template", 32 | }); 33 | }); 34 | 35 | test("cleanses data", () => { 36 | setMonthlyConfig({ 37 | enabled: true, 38 | folder: " foo/bar ", 39 | format: "YYYY-MM", 40 | template: " path/to/template ", 41 | }); 42 | 43 | expect(dailyNotesInterface.getMonthlyNoteSettings()).toEqual({ 44 | folder: "foo/bar", 45 | format: "YYYY-MM", 46 | template: "path/to/template", 47 | }); 48 | }); 49 | 50 | test("returns defaults if monthly note settings don't exist", () => { 51 | expect(dailyNotesInterface.getMonthlyNoteSettings()).toEqual({ 52 | format: "YYYY-MM", 53 | folder: "", 54 | template: "", 55 | }); 56 | }); 57 | }); 58 | 59 | describe("appHasMonthlyNotesPluginLoaded", () => { 60 | beforeEach(() => { 61 | window.app = getMockApp(); 62 | window.existingFiles = {}; 63 | window.moment = moment; 64 | }); 65 | 66 | test("returns true when periodic-notes plugin is enabled", () => { 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 69 | periodicNotes._loaded = true; 70 | periodicNotes.settings.monthly.enabled = true; 71 | 72 | expect(dailyNotesInterface.appHasMonthlyNotesPluginLoaded()).toEqual(true); 73 | }); 74 | 75 | test("returns false when periodic-notes plugin is enabled and weekly is disabled", () => { 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 78 | 79 | periodicNotes._loaded = true; 80 | periodicNotes.settings.monthly.enabled = false; 81 | 82 | expect(dailyNotesInterface.appHasMonthlyNotesPluginLoaded()).toEqual(false); 83 | }); 84 | 85 | test("returns false when periodic-notes plugin is disabled", () => { 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 88 | 89 | periodicNotes._loaded = false; 90 | periodicNotes.settings.monthly.enabled = false; 91 | 92 | expect(dailyNotesInterface.appHasMonthlyNotesPluginLoaded()).toEqual(false); 93 | }); 94 | }); 95 | 96 | describe("getAllMonthlyNotes", () => { 97 | beforeEach(() => { 98 | window.app = getMockApp(); 99 | window.moment = moment; 100 | window.existingFiles = {}; 101 | }); 102 | 103 | test("throws error if monthly note folder can't be found", () => { 104 | setMonthlyConfig({ 105 | enabled: true, 106 | folder: "missing-folder/", 107 | format: "YYYY-MM", 108 | template: "template", 109 | }); 110 | 111 | expect(dailyNotesInterface.getAllMonthlyNotes).toThrow( 112 | "Failed to find monthly notes folder" 113 | ); 114 | }); 115 | 116 | test("returns a list of all monthly notes with no nested folders", () => { 117 | setMonthlyConfig({ 118 | enabled: true, 119 | folder: "/", 120 | format: "YYYY-MM", 121 | template: "template", 122 | }); 123 | 124 | const fileA = createFile("2021-01", ""); 125 | const fileB = createFile("2021-02", ""); 126 | const fileC = createFile("2021-03", ""); 127 | createFolder("/", [fileA, fileB, fileC]); 128 | 129 | expect(dailyNotesInterface.getAllMonthlyNotes()).toEqual({ 130 | "month-2021-01-01T00:00:00-05:00": fileA, 131 | "month-2021-02-01T00:00:00-05:00": fileB, 132 | "month-2021-03-01T00:00:00-05:00": fileC, 133 | }); 134 | }); 135 | 136 | test("returns a list of all monthly notes including files nested in folders", () => { 137 | setMonthlyConfig({ 138 | enabled: true, 139 | folder: "/", 140 | format: "YYYY-MM", 141 | template: "template", 142 | }); 143 | 144 | const fileA = createFile("2021-01", ""); 145 | const fileB = createFile("2021-02", ""); 146 | const fileC = createFile("2021-03", ""); 147 | createFolder("/", [fileA, fileB, createFolder("foo", [fileC])]); 148 | 149 | expect(dailyNotesInterface.getAllMonthlyNotes()).toEqual({ 150 | "month-2021-01-01T00:00:00-05:00": fileA, 151 | "month-2021-02-01T00:00:00-05:00": fileB, 152 | "month-2021-03-01T00:00:00-05:00": fileC, 153 | }); 154 | }); 155 | }); 156 | 157 | describe("getMonthlyNote", () => { 158 | beforeEach(() => { 159 | window.app = getMockApp(); 160 | window.moment = moment; 161 | window.existingFiles = {}; 162 | }); 163 | 164 | test("returns note on the same day even if the HH:MM:SS is different", () => { 165 | setMonthlyConfig({ 166 | enabled: true, 167 | folder: "/", 168 | format: "YYYY-MM-HHmm", 169 | template: "template", 170 | }); 171 | 172 | const fileA = createFile("2020-02-0408", ""); 173 | 174 | expect( 175 | dailyNotesInterface.getMonthlyNote( 176 | moment("2021-02-0745", "YYYY-MM-HHmm", true), 177 | { 178 | "month-2021-02-01T00:00:00-05:00": fileA, 179 | } 180 | ) 181 | ).toEqual(fileA); 182 | }); 183 | 184 | test("returns null if there is no monthly note for a given date", () => { 185 | setMonthlyConfig({ 186 | enabled: true, 187 | folder: "/", 188 | format: "YYYY-ww", 189 | }); 190 | 191 | const fileA = createFile("2020-01", ""); 192 | 193 | expect( 194 | dailyNotesInterface.getMonthlyNote(moment("2020-01", "YYYY-ww", true), { 195 | "2020-12": fileA, 196 | }) 197 | ).toEqual(null); 198 | }); 199 | }); 200 | 201 | describe("createMonthlyNote", () => { 202 | beforeEach(() => { 203 | window.app = getMockApp(); 204 | window.moment = moment; 205 | window.existingFiles = {}; 206 | }); 207 | 208 | test("uses folder path from monthly note settings", async () => { 209 | setMonthlyConfig({ 210 | enabled: true, 211 | folder: "/monthly-notes", 212 | format: "YYYY-MM", 213 | }); 214 | 215 | const date = moment("2020-10", "YYYY-MM", true); 216 | await dailyNotesInterface.createMonthlyNote(date); 217 | 218 | expect(window.app.vault.create).toHaveBeenCalledWith( 219 | "/monthly-notes/2020-10.md", 220 | "" 221 | ); 222 | }); 223 | 224 | test("uses template contents when creating file", async () => { 225 | setMonthlyConfig({ 226 | enabled: true, 227 | folder: "/monthly-notes", 228 | format: "YYYY-MM", 229 | template: "template", 230 | }); 231 | 232 | createFile("template", "template contents"); 233 | 234 | const date = moment("2020-10", "YYYY-MM", true); 235 | await dailyNotesInterface.createMonthlyNote(date); 236 | 237 | expect(window.app.vault.create).toHaveBeenCalledWith( 238 | "/monthly-notes/2020-10.md", 239 | "template contents" 240 | ); 241 | }); 242 | 243 | test("shows error if file creation failed", async () => { 244 | const createFn = window.app.vault.create; 245 | (createFn as jest.MockedFunction).mockRejectedValue( 246 | "error" 247 | ); 248 | jest.spyOn(global.console, "error").mockImplementation(); 249 | 250 | setMonthlyConfig({ 251 | enabled: true, 252 | folder: "/monthly-notes", 253 | format: "YYYY-MM", 254 | template: "template", 255 | }); 256 | const date = moment("2020-10", "YYYY-MM", true); 257 | 258 | await dailyNotesInterface.createMonthlyNote(date); 259 | 260 | expect(console.error).toHaveBeenCalledWith( 261 | "Failed to create file: '/monthly-notes/2020-10.md'", 262 | "error" 263 | ); 264 | }); 265 | }); 266 | 267 | describe("createMonthlyNote", () => { 268 | beforeEach(() => { 269 | window.app = getMockApp(); 270 | window.moment = moment.bind(null, "2021-02-15T14:06:00-05:00"); 271 | moment.tz.setDefault("America/New_York"); 272 | }); 273 | 274 | test("replaces all mustaches in template", async () => { 275 | const getTemplateInfo = jest.spyOn(vaultUtils, "getTemplateInfo"); 276 | getTemplateInfo.mockResolvedValue([ 277 | ` 278 | {{date}} 279 | {{time}} 280 | {{title}} 281 | {{date:YYYY}} {{date:MMMM}} 282 | {{date+2d:YYYY-MM-DD}} 283 | {{date+1M:YYYY-MM-DD}} 284 | {{date+10y:YYYY-MM-DD}} 285 | {{date +1M}} 286 | `, 287 | null, 288 | ]); 289 | 290 | setMonthlyConfig({ 291 | enabled: true, 292 | folder: "/", 293 | format: "YYYY-[M]MM", 294 | template: "template", 295 | }); 296 | 297 | await dailyNotesInterface.createDailyNote(window.moment()); 298 | 299 | expect(window.app.vault.create).toHaveBeenCalledWith( 300 | "/2021-02-15.md", 301 | ` 302 | 2021-02-15 303 | 14:06 304 | 2021-02-15 305 | 2021 February 306 | 2021-02-17 307 | 2021-03-15 308 | 2031-02-15 309 | 2021-03-15 310 | ` 311 | ); 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /src/__tests__/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | import getMockApp, { createFile } from "src/testUtils/mockApp"; 3 | 4 | import * as dailyNotesInterface from "../index"; 5 | import { 6 | setDailyConfig, 7 | setMonthlyConfig, 8 | setWeeklyConfig, 9 | } from "../testUtils/utils"; 10 | 11 | jest.mock("path"); 12 | 13 | describe("getDateUID", () => { 14 | beforeAll(() => { 15 | window.moment = moment; 16 | }); 17 | 18 | beforeEach(() => { 19 | moment.locale("en"); 20 | moment.tz.setDefault("America/New_York"); 21 | }); 22 | 23 | test("it does not mutate the original date", () => { 24 | const date = moment("2021-01-05T04:12:21-05:00"); 25 | const clonedDate = date.clone(); 26 | 27 | dailyNotesInterface.getDateUID(date); 28 | 29 | expect(date.isSame(clonedDate)).toEqual(true); 30 | }); 31 | 32 | test("it uses 'day' for the default granularity", () => { 33 | const date = moment("2021-01-05T04:12:21-05:00"); 34 | expect(dailyNotesInterface.getDateUID(date)).toEqual( 35 | "day-2021-01-05T00:00:00-05:00" 36 | ); 37 | }); 38 | 39 | test("it supports 'week' granularity", () => { 40 | const date = moment("2021-01-05T04:12:21-05:00"); 41 | expect(dailyNotesInterface.getDateUID(date, "week")).toEqual( 42 | "week-2021-01-03T00:00:00-05:00" 43 | ); 44 | }); 45 | 46 | test("it supports 'month' granularity", () => { 47 | const date = moment("2021-01-05T04:12:21-05:00"); 48 | expect(dailyNotesInterface.getDateUID(date, "month")).toEqual( 49 | "month-2021-01-01T00:00:00-05:00" 50 | ); 51 | }); 52 | }); 53 | 54 | describe("getDateFromFile", () => { 55 | beforeAll(() => { 56 | window.moment = moment; 57 | window.app = getMockApp(); 58 | moment.tz.setDefault("America/New_York"); 59 | }); 60 | 61 | test("it supports 'month' granularity", () => { 62 | setMonthlyConfig({ 63 | enabled: true, 64 | format: "YYYY-MM", 65 | }); 66 | 67 | const file = createFile("2020-01", ""); 68 | expect(dailyNotesInterface.getDateFromFile(file, "month").format()).toEqual( 69 | "2020-01-01T00:00:00-05:00" 70 | ); 71 | }); 72 | 73 | test("it supports 'daily' granularity", () => { 74 | setDailyConfig({ 75 | format: "YYYY-MM-DD", 76 | }); 77 | 78 | const file = createFile("2020-01-03", ""); 79 | expect(dailyNotesInterface.getDateFromFile(file, "day").format()).toEqual( 80 | "2020-01-03T00:00:00-05:00" 81 | ); 82 | }); 83 | 84 | describe("weekly granularity", () => { 85 | test("it supports formats with year, month, and day", () => { 86 | setWeeklyConfig({ enabled: true, format: "YYYY-MM-DD" }); 87 | const file = createFile("2020-07-11", ""); 88 | 89 | expect( 90 | dailyNotesInterface.getDateFromFile(file, "week").format() 91 | ).toEqual("2020-07-11T00:00:00-04:00"); 92 | }); 93 | 94 | test("it supports formats with partial year, month, and week number", () => { 95 | setWeeklyConfig({ enabled: true, format: "ggMM[W]ww" }); 96 | const file = createFile("2002W07", ""); 97 | 98 | expect( 99 | dailyNotesInterface.getDateFromFile(file, "week").format() 100 | ).toEqual("2020-02-09T00:00:00-05:00"); 101 | }); 102 | 103 | test("it supports formats with year and week number", () => { 104 | setWeeklyConfig({ enabled: true, format: "gggg-[W]ww" }); 105 | const file = createFile("2020-W07", ""); 106 | 107 | expect( 108 | dailyNotesInterface.getDateFromFile(file, "week").format() 109 | ).toEqual("2020-02-09T00:00:00-05:00"); 110 | }); 111 | 112 | test("it supports formats with year, week number, and month", () => { 113 | setWeeklyConfig({ enabled: true, format: "gggg-[W]ww-MMM" }); 114 | const file = createFile("2020-W07-Feb", ""); 115 | 116 | expect( 117 | dailyNotesInterface.getDateFromFile(file, "week").format() 118 | ).toEqual("2020-02-09T00:00:00-05:00"); 119 | }); 120 | 121 | test("it supports formats with year, week number, and day", () => { 122 | setWeeklyConfig({ enabled: true, format: "gggg-[W]ww-DD" }); 123 | const file = createFile("2020-W07-09", ""); 124 | 125 | expect( 126 | dailyNotesInterface.getDateFromFile(file, "week").format() 127 | ).toEqual("2020-02-09T00:00:00-05:00"); 128 | }); 129 | 130 | test("it supports formats with year, month number, week number", () => { 131 | setWeeklyConfig({ enabled: true, format: "gggg-MM-[W]ww" }); 132 | const file = createFile("2020-02-W07", ""); 133 | 134 | expect( 135 | dailyNotesInterface.getDateFromFile(file, "week").format() 136 | ).toEqual("2020-02-09T00:00:00-05:00"); 137 | }); 138 | 139 | test("it supports formats with year, month number, week number without prefix", () => { 140 | setWeeklyConfig({ enabled: true, format: "gggg-MM-ww" }); 141 | const file = createFile("2020-02-07", ""); 142 | 143 | expect( 144 | dailyNotesInterface.getDateFromFile(file, "week").format() 145 | ).toEqual("2020-02-09T00:00:00-05:00"); 146 | }); 147 | 148 | test("it supports year, month, day, week number", () => { 149 | setWeeklyConfig({ enabled: true, format: "gggg-MM-DD_[W]ww" }); 150 | const file = createFile("2020-04-12_W16", ""); 151 | 152 | expect( 153 | dailyNotesInterface.getDateFromFile(file, "week").format() 154 | ).toEqual("2020-04-12T00:00:00-04:00"); 155 | }); 156 | 157 | test("[en-gb] it supports year, month, day, week number", () => { 158 | moment.locale("en-gb"); 159 | setWeeklyConfig({ enabled: true, format: "gggg-[W]ww_MM-DD" }); 160 | 161 | const file = createFile("2020-W53_12-28", ""); 162 | 163 | expect( 164 | dailyNotesInterface.getDateFromFile(file, "week").format() 165 | ).toEqual("2020-12-28T00:00:00-05:00"); 166 | }); 167 | 168 | test("ambiguous dates are still parsed strictly first", () => { 169 | setWeeklyConfig({ enabled: true, format: "gggg-MM-[W]ww" }); 170 | 171 | const fileWithSuffix = createFile("2020-02-W07 Foo", ""); 172 | const fileWithSpaces = createFile("2020 02 W07", ""); 173 | 174 | expect( 175 | dailyNotesInterface.getDateFromFile(fileWithSuffix, "week") 176 | ).toBeNull(); 177 | expect( 178 | dailyNotesInterface.getDateFromFile(fileWithSpaces, "week") 179 | ).toBeNull(); 180 | }); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /src/__tests__/quarterly.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import getMockApp, { createFile, createFolder } from "src/testUtils/mockApp"; 4 | 5 | import * as dailyNotesInterface from "../index"; 6 | import { setQuarterlyConfig } from "../testUtils/utils"; 7 | import * as vaultUtils from "../vault"; 8 | 9 | jest.mock("path"); 10 | 11 | moment.tz.setDefault("America/New_York"); 12 | 13 | describe("getQuarterlyNoteSettings", () => { 14 | beforeEach(() => { 15 | window.app = getMockApp(); 16 | window.existingFiles = {}; 17 | window.moment = moment; 18 | }); 19 | 20 | test("returns all the quarterly note settings", () => { 21 | setQuarterlyConfig({ 22 | enabled: true, 23 | folder: "foo", 24 | format: "YYYY-[Q]Q", 25 | template: "template", 26 | }); 27 | 28 | expect(dailyNotesInterface.getQuarterlyNoteSettings()).toEqual({ 29 | folder: "foo", 30 | format: "YYYY-[Q]Q", 31 | template: "template", 32 | }); 33 | }); 34 | 35 | test("cleanses data", () => { 36 | setQuarterlyConfig({ 37 | enabled: true, 38 | folder: " foo/bar ", 39 | format: "YYYY-[Q]Q", 40 | template: " path/to/template ", 41 | }); 42 | 43 | expect(dailyNotesInterface.getQuarterlyNoteSettings()).toEqual({ 44 | folder: "foo/bar", 45 | format: "YYYY-[Q]Q", 46 | template: "path/to/template", 47 | }); 48 | }); 49 | 50 | test("returns defaults if quarterly note settings don't exist", () => { 51 | expect(dailyNotesInterface.getQuarterlyNoteSettings()).toEqual({ 52 | format: "YYYY-[Q]Q", 53 | folder: "", 54 | template: "", 55 | }); 56 | }); 57 | }); 58 | 59 | describe("appHasQuarterlyNotesPluginLoaded", () => { 60 | beforeEach(() => { 61 | window.app = getMockApp(); 62 | window.existingFiles = {}; 63 | window.moment = moment; 64 | }); 65 | 66 | test("returns true when periodic-notes plugin is enabled", () => { 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 69 | periodicNotes._loaded = true; 70 | periodicNotes.settings.quarterly.enabled = true; 71 | 72 | expect(dailyNotesInterface.appHasQuarterlyNotesPluginLoaded()).toEqual( 73 | true 74 | ); 75 | }); 76 | 77 | test("returns false when periodic-notes plugin is enabled and weekly is disabled", () => { 78 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 79 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 80 | 81 | periodicNotes._loaded = true; 82 | periodicNotes.settings.quarterly.enabled = false; 83 | 84 | expect(dailyNotesInterface.appHasQuarterlyNotesPluginLoaded()).toEqual( 85 | false 86 | ); 87 | }); 88 | 89 | test("returns false when periodic-notes plugin is disabled", () => { 90 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 91 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 92 | 93 | periodicNotes._loaded = false; 94 | periodicNotes.settings.quarterly.enabled = false; 95 | 96 | expect(dailyNotesInterface.appHasQuarterlyNotesPluginLoaded()).toEqual( 97 | false 98 | ); 99 | }); 100 | }); 101 | 102 | describe("getAllQuarterlyNotes", () => { 103 | beforeEach(() => { 104 | window.app = getMockApp(); 105 | window.moment = moment; 106 | window.existingFiles = {}; 107 | }); 108 | 109 | test("throws error if quarterly note folder can't be found", () => { 110 | setQuarterlyConfig({ 111 | enabled: true, 112 | folder: "missing-folder/", 113 | format: "YYYY-[Q]Q", 114 | template: "template", 115 | }); 116 | 117 | expect(dailyNotesInterface.getAllQuarterlyNotes).toThrow( 118 | "Failed to find quarterly notes folder" 119 | ); 120 | }); 121 | 122 | test("returns a list of all quarterly notes with no nested folders", () => { 123 | setQuarterlyConfig({ 124 | enabled: true, 125 | folder: "/", 126 | format: "YYYY-[Q]Q", 127 | template: "template", 128 | }); 129 | 130 | const fileA = createFile("2021-Q1", ""); 131 | const fileB = createFile("2021-Q2", ""); 132 | const fileC = createFile("2021-Q3", ""); 133 | createFolder("/", [fileA, fileB, fileC]); 134 | 135 | expect(dailyNotesInterface.getAllQuarterlyNotes()).toEqual({ 136 | "quarter-2021-01-01T00:00:00-05:00": fileA, 137 | "quarter-2021-04-01T00:00:00-04:00": fileB, 138 | "quarter-2021-07-01T00:00:00-04:00": fileC, 139 | }); 140 | }); 141 | 142 | test("returns a list of all quarterly notes including files nested in folders", () => { 143 | setQuarterlyConfig({ 144 | enabled: true, 145 | folder: "/", 146 | format: "YYYY-[Q]Q", 147 | template: "template", 148 | }); 149 | 150 | const fileA = createFile("2021-Q1", ""); 151 | const fileB = createFile("2021-Q2", ""); 152 | const fileC = createFile("2021-Q3", ""); 153 | createFolder("/", [fileA, fileB, createFolder("foo", [fileC])]); 154 | 155 | expect(dailyNotesInterface.getAllQuarterlyNotes()).toEqual({ 156 | "quarter-2021-01-01T00:00:00-05:00": fileA, 157 | "quarter-2021-04-01T00:00:00-04:00": fileB, 158 | "quarter-2021-07-01T00:00:00-04:00": fileC, 159 | }); 160 | }); 161 | }); 162 | 163 | describe("getQuarterlyNote", () => { 164 | beforeEach(() => { 165 | window.app = getMockApp(); 166 | window.moment = moment; 167 | window.existingFiles = {}; 168 | }); 169 | 170 | test("returns note on the same day even if the HH:MM:SS is different", () => { 171 | setQuarterlyConfig({ 172 | enabled: true, 173 | folder: "/", 174 | format: "YYYY-MM-HHmm", 175 | template: "template", 176 | }); 177 | 178 | const fileA = createFile("2020-02-0408", ""); 179 | 180 | expect( 181 | dailyNotesInterface.getQuarterlyNote( 182 | moment("2021-02-0745", "YYYY-MM-HHmm", true), 183 | { 184 | "quarter-2021-01-01T00:00:00-05:00": fileA, 185 | } 186 | ) 187 | ).toEqual(fileA); 188 | }); 189 | 190 | test("returns null if there is no quarterly note for a given date", () => { 191 | setQuarterlyConfig({ 192 | enabled: true, 193 | folder: "/", 194 | format: "YYYY-ww", 195 | }); 196 | 197 | const fileA = createFile("2020-01", ""); 198 | 199 | expect( 200 | dailyNotesInterface.getQuarterlyNote(moment("2020-01", "YYYY-ww", true), { 201 | "2020-12": fileA, 202 | }) 203 | ).toEqual(null); 204 | }); 205 | }); 206 | 207 | describe("createQuarterlyNote", () => { 208 | beforeEach(() => { 209 | window.app = getMockApp(); 210 | window.moment = moment; 211 | window.existingFiles = {}; 212 | }); 213 | 214 | test("uses folder path from quarterly note settings", async () => { 215 | setQuarterlyConfig({ 216 | enabled: true, 217 | folder: "/quarterly-notes", 218 | format: "YYYY-[Q]Q", 219 | }); 220 | 221 | const date = moment("2020-Q4", "YYYY-[Q]Q", true); 222 | await dailyNotesInterface.createQuarterlyNote(date); 223 | 224 | expect(window.app.vault.create).toHaveBeenCalledWith( 225 | "/quarterly-notes/2020-Q4.md", 226 | "" 227 | ); 228 | }); 229 | 230 | test("uses template contents when creating file", async () => { 231 | setQuarterlyConfig({ 232 | enabled: true, 233 | folder: "/quarterly-notes", 234 | format: "YYYY-[Q]Q", 235 | template: "template", 236 | }); 237 | 238 | createFile("template", "template contents"); 239 | 240 | const date = moment("2020-Q4", "YYYY-[Q]Q", true); 241 | await dailyNotesInterface.createQuarterlyNote(date); 242 | 243 | expect(window.app.vault.create).toHaveBeenCalledWith( 244 | "/quarterly-notes/2020-Q4.md", 245 | "template contents" 246 | ); 247 | }); 248 | 249 | test("shows error if file creation failed", async () => { 250 | const createFn = window.app.vault.create; 251 | (createFn as jest.MockedFunction).mockRejectedValue( 252 | "error" 253 | ); 254 | jest.spyOn(global.console, "error").mockImplementation(); 255 | 256 | setQuarterlyConfig({ 257 | enabled: true, 258 | folder: "/quarterly-notes", 259 | format: "YYYY-[Q]Q", 260 | template: "template", 261 | }); 262 | const date = moment("2020-Q4", "YYYY-[Q]Q", true); 263 | 264 | await dailyNotesInterface.createQuarterlyNote(date); 265 | 266 | expect(console.error).toHaveBeenCalledWith( 267 | "Failed to create file: '/quarterly-notes/2020-Q4.md'", 268 | "error" 269 | ); 270 | }); 271 | }); 272 | 273 | describe("createQuarterlyNote", () => { 274 | beforeEach(() => { 275 | window.app = getMockApp(); 276 | window.moment = moment.bind(null, "2021-02-15T14:06:00-05:00"); 277 | moment.tz.setDefault("America/New_York"); 278 | }); 279 | 280 | test("replaces all mustaches in template", async () => { 281 | const getTemplateInfo = jest.spyOn(vaultUtils, "getTemplateInfo"); 282 | getTemplateInfo.mockResolvedValue([ 283 | ` 284 | {{date}} 285 | {{time}} 286 | {{title}} 287 | {{date:YYYY}} {{date:MMMM}} 288 | {{date+2d:YYYY-MM-DD}} 289 | {{date+1M:YYYY-MM-DD}} 290 | {{date+10y:YYYY-MM-DD}} 291 | {{date +1M}} 292 | `, 293 | null, 294 | ]); 295 | 296 | setQuarterlyConfig({ 297 | enabled: true, 298 | folder: "/", 299 | format: "YYYY-[Q]Q", 300 | template: "template", 301 | }); 302 | 303 | await dailyNotesInterface.createQuarterlyNote(window.moment()); 304 | 305 | expect(window.app.vault.create).toHaveBeenCalledWith( 306 | "/2021-Q1.md", 307 | ` 308 | 2021-Q1 309 | 2021-Q1 310 | 2021-Q1 311 | 2021 February 312 | 2021-02-17 313 | 2021-03-15 314 | 2031-02-15 315 | 2021-Q1 316 | ` 317 | ); 318 | }); 319 | }); 320 | -------------------------------------------------------------------------------- /src/__tests__/vault.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment"; 2 | 3 | import * as dailyNotesInterface from "../index"; 4 | import { join } from "../vault"; 5 | 6 | import getMockApp, { createFile } from "src/testUtils/mockApp"; 7 | 8 | describe("join function", () => { 9 | test("join() mimics path.join", () => { 10 | expect(join("test", "/foo", "/bar/", "baz.md")).toEqual( 11 | "test/foo/bar/baz.md" 12 | ); 13 | expect(join("/test", "baz.md")).toEqual("/test/baz.md"); 14 | expect(join("/test.md")).toEqual("/test.md"); 15 | }); 16 | }); 17 | 18 | describe("getTemplateInfo", () => { 19 | beforeEach(() => { 20 | window.app = getMockApp(); 21 | window.moment = moment; 22 | window.existingFiles = {}; 23 | 24 | createFile("fileA", "A. Lorem ipsum dolor sit amet"); 25 | createFile("fileB", "B. Lorem ipsum dolor sit amet"); 26 | createFile("fileC", "C. Lorem ipsum dolor sit amet"); 27 | }); 28 | 29 | test("returns '' if path is empty", async () => { 30 | const [templateContents] = await dailyNotesInterface.getTemplateInfo(""); 31 | expect(templateContents).toEqual(""); 32 | }); 33 | 34 | test("returns contents of the template file", async () => { 35 | const [templateContents] = await dailyNotesInterface.getTemplateInfo( 36 | "fileA" 37 | ); 38 | expect(templateContents).toEqual("A. Lorem ipsum dolor sit amet"); 39 | }); 40 | 41 | test("throws error if file can't be found", async () => { 42 | jest.spyOn(global.console, "error").mockImplementation(); 43 | 44 | const [templateContents] = await dailyNotesInterface.getTemplateInfo( 45 | "nonexistent-file" 46 | ); 47 | 48 | expect(console.error).toHaveBeenCalledWith( 49 | "Failed to read the daily note template '/nonexistent-file'", 50 | "error" 51 | ); 52 | expect(templateContents).toEqual(""); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /src/__tests__/weekly.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import getMockApp, { createFile, createFolder } from "src/testUtils/mockApp"; 4 | 5 | import * as dailyNotesInterface from "../index"; 6 | import { getDayOfWeekNumericalValue } from "../weekly"; 7 | import * as vaultUtils from "../vault"; 8 | import { setWeeklyConfig } from "../testUtils/utils"; 9 | 10 | jest.mock("path"); 11 | 12 | describe("getDayOfWeekNumericalValue", () => { 13 | beforeEach(() => { 14 | window.app = getMockApp(); 15 | window.moment = moment; 16 | moment.tz.setDefault("America/New_York"); 17 | }); 18 | 19 | describe("start week on Sunday", () => { 20 | beforeEach(() => { 21 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 | (moment.localeData())._week.dow = 0; 23 | }); 24 | 25 | test("returns 0 for sunday", () => { 26 | expect(getDayOfWeekNumericalValue("sunday")).toEqual(0); 27 | }); 28 | 29 | test("returns 1 for monday", () => { 30 | expect(getDayOfWeekNumericalValue("monday")).toEqual(1); 31 | }); 32 | }); 33 | 34 | describe("start week on Monday", () => { 35 | beforeEach(() => { 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | (moment.localeData())._week.dow = 1; 38 | }); 39 | 40 | test("returns 0 for sunday", () => { 41 | expect(getDayOfWeekNumericalValue("sunday")).toEqual(6); 42 | }); 43 | 44 | test("returns 1 for monday", () => { 45 | expect(getDayOfWeekNumericalValue("monday")).toEqual(0); 46 | }); 47 | }); 48 | }); 49 | 50 | describe("getWeeklyNoteSettings", () => { 51 | beforeEach(() => { 52 | window.app = getMockApp(); 53 | window.existingFiles = {}; 54 | window.moment = moment; 55 | }); 56 | 57 | test("returns all the weekly note settings", () => { 58 | setWeeklyConfig({ 59 | enabled: true, 60 | folder: "foo", 61 | format: "gggg-MM-DD", 62 | template: "template", 63 | }); 64 | 65 | expect(dailyNotesInterface.getWeeklyNoteSettings()).toEqual({ 66 | folder: "foo", 67 | format: "gggg-MM-DD", 68 | template: "template", 69 | }); 70 | }); 71 | 72 | test("cleanses data", () => { 73 | setWeeklyConfig({ 74 | enabled: true, 75 | folder: " foo/bar ", 76 | format: "gggg-MM-DD", 77 | template: " path/to/template ", 78 | }); 79 | 80 | expect(dailyNotesInterface.getWeeklyNoteSettings()).toEqual({ 81 | folder: "foo/bar", 82 | format: "gggg-MM-DD", 83 | template: "path/to/template", 84 | }); 85 | }); 86 | 87 | test("returns defaults if weekly note settings don't exist", () => { 88 | expect(dailyNotesInterface.getWeeklyNoteSettings()).toEqual({ 89 | format: "gggg-[W]ww", 90 | folder: "", 91 | template: "", 92 | }); 93 | }); 94 | }); 95 | 96 | describe("appHasWeeklyNotesPluginLoaded", () => { 97 | beforeEach(() => { 98 | window.app = getMockApp(); 99 | window.existingFiles = {}; 100 | window.moment = moment; 101 | }); 102 | 103 | test("returns true when weekly notes plugin is enabled", () => { 104 | // eslint-disable-next-line 105 | (window.app).plugins.plugins["calendar"]._loaded = true; 106 | 107 | expect(dailyNotesInterface.appHasWeeklyNotesPluginLoaded()).toEqual(true); 108 | }); 109 | 110 | test("returns false when weekly notes plugin is disabled", () => { 111 | // eslint-disable-next-line 112 | (window.app).plugins.plugins["periodic-notes"]._loaded = false; 113 | // eslint-disable-next-line 114 | (window.app).plugins.plugins["calendar"]._loaded = false; 115 | 116 | expect(dailyNotesInterface.appHasWeeklyNotesPluginLoaded()).toEqual(false); 117 | }); 118 | }); 119 | 120 | describe("getAllWeeklyNotes", () => { 121 | beforeEach(() => { 122 | window.app = getMockApp(); 123 | window.moment = moment; 124 | window.existingFiles = {}; 125 | 126 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 127 | (moment.localeData())._week.dow = 0; 128 | }); 129 | 130 | test("throws error if weekly note folder can't be found", () => { 131 | setWeeklyConfig({ 132 | enabled: true, 133 | folder: "missing-folder/", 134 | format: "gggg-[W]ww", 135 | template: "template", 136 | }); 137 | 138 | expect(dailyNotesInterface.getAllWeeklyNotes).toThrow( 139 | "Failed to find weekly notes folder" 140 | ); 141 | }); 142 | 143 | test("returns a list of all weekly notes with no nested folders", () => { 144 | setWeeklyConfig({ 145 | enabled: true, 146 | folder: "/", 147 | format: "gggg-[W]ww", 148 | template: "template", 149 | }); 150 | 151 | const fileA = createFile("2021-W02", ""); 152 | const fileB = createFile("2021-W03", ""); 153 | const fileC = createFile("2021-W04", ""); 154 | createFolder("/", [fileA, fileB, fileC]); 155 | 156 | expect(dailyNotesInterface.getAllWeeklyNotes()).toEqual({ 157 | "week-2021-01-03T00:00:00-05:00": fileA, 158 | "week-2021-01-10T00:00:00-05:00": fileB, 159 | "week-2021-01-17T00:00:00-05:00": fileC, 160 | }); 161 | }); 162 | 163 | test("returns a list of all weekly notes including files nested in folders", () => { 164 | setWeeklyConfig({ 165 | enabled: true, 166 | folder: "/", 167 | format: "gggg-[W]ww", 168 | template: "template", 169 | }); 170 | 171 | const fileA = createFile("2021-W02", ""); 172 | const fileB = createFile("2021-W03", ""); 173 | const fileC = createFile("2021-W04", ""); 174 | createFolder("/", [fileA, fileB, createFolder("foo", [fileC])]); 175 | 176 | expect(dailyNotesInterface.getAllWeeklyNotes()).toEqual({ 177 | "week-2021-01-03T00:00:00-05:00": fileA, 178 | "week-2021-01-10T00:00:00-05:00": fileB, 179 | "week-2021-01-17T00:00:00-05:00": fileC, 180 | }); 181 | }); 182 | }); 183 | 184 | describe("getWeeklyNote", () => { 185 | beforeEach(() => { 186 | window.existingFiles = {}; 187 | }); 188 | 189 | test("returns note on the same day even if the HH:MM:SS is different", () => { 190 | setWeeklyConfig({ 191 | enabled: true, 192 | folder: "/", 193 | format: "gggg-[W]ww-HHmm", 194 | template: "template", 195 | }); 196 | 197 | const fileA = createFile("2020-W02-0408", ""); 198 | 199 | expect( 200 | dailyNotesInterface.getWeeklyNote( 201 | moment("2021-W02-0745", "gggg-[W]ww-HHmm", true), 202 | { 203 | "week-2021-01-03T00:00:00-05:00": fileA, 204 | } 205 | ) 206 | ).toEqual(fileA); 207 | }); 208 | 209 | test("returns null if there is no weekly note for a given date", () => { 210 | setWeeklyConfig({ 211 | enabled: true, 212 | folder: "/", 213 | format: "gggg-ww", 214 | template: "template", 215 | }); 216 | 217 | const fileA = createFile("2020-01", ""); 218 | 219 | expect( 220 | dailyNotesInterface.getWeeklyNote(moment("2020-01", "gggg-ww", true), { 221 | "2020-12-03": fileA, 222 | }) 223 | ).toEqual(null); 224 | }); 225 | }); 226 | 227 | describe("createWeeklyNote", () => { 228 | beforeEach(() => { 229 | window.existingFiles = {}; 230 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 231 | (moment.localeData())._week.dow = 1; 232 | }); 233 | 234 | test("uses folder path from weekly note settings", async () => { 235 | setWeeklyConfig({ 236 | enabled: true, 237 | folder: "/weekly-notes", 238 | format: "gggg-MM-DD", 239 | }); 240 | 241 | const date = moment({ day: 5, month: 10, year: 2020 }); 242 | await dailyNotesInterface.createWeeklyNote(date); 243 | 244 | expect(window.app.vault.create).toHaveBeenCalledWith( 245 | "/weekly-notes/2020-11-05.md", 246 | "" 247 | ); 248 | }); 249 | 250 | test("uses template contents when creating file", async () => { 251 | setWeeklyConfig({ 252 | enabled: true, 253 | folder: "/weekly-notes", 254 | format: "gggg-MM-DD", 255 | template: "template", 256 | }); 257 | 258 | createFile("template", "template contents"); 259 | 260 | const date = moment({ day: 5, month: 10, year: 2020 }); 261 | await dailyNotesInterface.createWeeklyNote(date); 262 | 263 | expect(window.app.vault.create).toHaveBeenCalledWith( 264 | "/weekly-notes/2020-11-05.md", 265 | "template contents" 266 | ); 267 | }); 268 | 269 | test("replaces {{sunday}} and {{monday}} in weekly note", async () => { 270 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 271 | (moment.localeData())._week.dow = 0; 272 | 273 | setWeeklyConfig({ 274 | enabled: true, 275 | folder: "/weekly-notes", 276 | format: "gggg-[W]ww", 277 | template: "template", 278 | }); 279 | 280 | createFile( 281 | "template", 282 | "# {{sunday:gggg-MM-DD}}, {{monday:gggg-MM-DD}}, etc" 283 | ); 284 | 285 | const date = moment({ day: 5, month: 0, year: 2021 }); 286 | 287 | await dailyNotesInterface.createWeeklyNote(date); 288 | 289 | expect(window.app.vault.create).toHaveBeenCalledWith( 290 | "/weekly-notes/2021-W02.md", 291 | "# 2021-01-03, 2021-01-04, etc" 292 | ); 293 | }); 294 | 295 | test("shows error if file creation failed", async () => { 296 | const createFn = window.app.vault.create; 297 | (createFn as jest.MockedFunction).mockRejectedValue( 298 | "error" 299 | ); 300 | jest.spyOn(global.console, "error").mockImplementation(); 301 | 302 | setWeeklyConfig({ 303 | enabled: true, 304 | folder: "/weekly-notes", 305 | format: "gggg-[W]ww", 306 | }); 307 | const date = moment({ day: 5, month: 10, year: 2020 }); 308 | 309 | await dailyNotesInterface.createWeeklyNote(date); 310 | 311 | expect(console.error).toHaveBeenCalledWith( 312 | "Failed to create file: '/weekly-notes/2020-W45.md'", 313 | "error" 314 | ); 315 | }); 316 | }); 317 | 318 | describe("createWeeklyNote", () => { 319 | beforeEach(() => { 320 | window.app = getMockApp(); 321 | window.moment = moment.bind(null, "2021-02-15T14:06:00-05:00"); 322 | moment.tz.setDefault("America/New_York"); 323 | }); 324 | 325 | test("replaces all mustaches in template", async () => { 326 | const getTemplateInfo = jest.spyOn(vaultUtils, "getTemplateInfo"); 327 | getTemplateInfo.mockResolvedValue([ 328 | ` 329 | {{date}} 330 | {{time}} 331 | {{title}} 332 | {{date:gggg}} {{date:[W]ww}} 333 | {{date-1w:gggg-[W]ww-DD}} 334 | {{date+2d:gggg-[W]ww-DD}} 335 | {{date+1M:gggg-[W]ww-DD}} 336 | {{date+10y:gggg-[W]ww-DD}} 337 | {{date +7d}} 338 | `, 339 | null, 340 | ]); 341 | 342 | setWeeklyConfig({ 343 | enabled: true, 344 | folder: "/", 345 | format: "gggg-[W]ww", 346 | template: "template", 347 | }); 348 | 349 | await dailyNotesInterface.createDailyNote(window.moment()); 350 | 351 | expect(window.app.vault.create).toHaveBeenCalledWith( 352 | "/2021-02-15.md", 353 | ` 354 | 2021-02-15 355 | 14:06 356 | 2021-02-15 357 | 2021 W08 358 | 2021-W07-08 359 | 2021-W08-17 360 | 2021-W12-15 361 | 2031-W07-15 362 | 2021-02-22 363 | ` 364 | ); 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /src/__tests__/yearly.spec.ts: -------------------------------------------------------------------------------- 1 | import * as moment from "moment-timezone"; 2 | 3 | import getMockApp, { createFile, createFolder } from "src/testUtils/mockApp"; 4 | 5 | import * as dailyNotesInterface from "../index"; 6 | import { setYearlyConfig } from "../testUtils/utils"; 7 | import * as vaultUtils from "../vault"; 8 | 9 | jest.mock("path"); 10 | 11 | moment.tz.setDefault("America/New_York"); 12 | 13 | describe("getYearlyNoteSettings", () => { 14 | beforeEach(() => { 15 | window.app = getMockApp(); 16 | window.existingFiles = {}; 17 | window.moment = moment; 18 | }); 19 | 20 | test("returns all the yearly note settings", () => { 21 | setYearlyConfig({ 22 | enabled: true, 23 | folder: "foo", 24 | format: "YYYY", 25 | template: "template", 26 | }); 27 | 28 | expect(dailyNotesInterface.getYearlyNoteSettings()).toEqual({ 29 | folder: "foo", 30 | format: "YYYY", 31 | template: "template", 32 | }); 33 | }); 34 | 35 | test("cleanses data", () => { 36 | setYearlyConfig({ 37 | enabled: true, 38 | folder: " foo/bar ", 39 | format: "YYYY", 40 | template: " path/to/template ", 41 | }); 42 | 43 | expect(dailyNotesInterface.getYearlyNoteSettings()).toEqual({ 44 | folder: "foo/bar", 45 | format: "YYYY", 46 | template: "path/to/template", 47 | }); 48 | }); 49 | 50 | test("returns defaults if yearly note settings don't exist", () => { 51 | expect(dailyNotesInterface.getYearlyNoteSettings()).toEqual({ 52 | format: "YYYY", 53 | folder: "", 54 | template: "", 55 | }); 56 | }); 57 | }); 58 | 59 | describe("appHasYearlyNotesPluginLoaded", () => { 60 | beforeEach(() => { 61 | window.app = getMockApp(); 62 | window.existingFiles = {}; 63 | window.moment = moment; 64 | }); 65 | 66 | test("returns true when periodic-notes plugin is enabled", () => { 67 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 68 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 69 | periodicNotes._loaded = true; 70 | periodicNotes.settings.yearly.enabled = true; 71 | 72 | expect(dailyNotesInterface.appHasYearlyNotesPluginLoaded()).toEqual(true); 73 | }); 74 | 75 | test("returns false when periodic-notes plugin is enabled and weekly is disabled", () => { 76 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 77 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 78 | 79 | periodicNotes._loaded = true; 80 | periodicNotes.settings.yearly.enabled = false; 81 | 82 | expect(dailyNotesInterface.appHasYearlyNotesPluginLoaded()).toEqual(false); 83 | }); 84 | 85 | test("returns false when periodic-notes plugin is disabled", () => { 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 88 | 89 | periodicNotes._loaded = false; 90 | periodicNotes.settings.yearly.enabled = false; 91 | 92 | expect(dailyNotesInterface.appHasYearlyNotesPluginLoaded()).toEqual(false); 93 | }); 94 | }); 95 | 96 | describe("getAllYearlyNotes", () => { 97 | beforeEach(() => { 98 | window.app = getMockApp(); 99 | window.moment = moment; 100 | window.existingFiles = {}; 101 | }); 102 | 103 | test("throws error if yearly note folder can't be found", () => { 104 | setYearlyConfig({ 105 | enabled: true, 106 | folder: "missing-folder/", 107 | format: "YYYY", 108 | template: "template", 109 | }); 110 | 111 | expect(dailyNotesInterface.getAllYearlyNotes).toThrow( 112 | "Failed to find yearly notes folder" 113 | ); 114 | }); 115 | 116 | test("returns a list of all yearly notes with no nested folders", () => { 117 | setYearlyConfig({ 118 | enabled: true, 119 | folder: "/", 120 | format: "YYYY", 121 | template: "template", 122 | }); 123 | 124 | const fileA = createFile("2020", ""); 125 | const fileB = createFile("2021", ""); 126 | const fileC = createFile("2022", ""); 127 | createFolder("/", [fileA, fileB, fileC]); 128 | 129 | expect(dailyNotesInterface.getAllYearlyNotes()).toEqual({ 130 | "year-2020-01-01T00:00:00-05:00": fileA, 131 | "year-2021-01-01T00:00:00-05:00": fileB, 132 | "year-2022-01-01T00:00:00-05:00": fileC, 133 | }); 134 | }); 135 | 136 | test("returns a list of all yearly notes including files nested in folders", () => { 137 | setYearlyConfig({ 138 | enabled: true, 139 | folder: "/", 140 | format: "YYYY", 141 | template: "template", 142 | }); 143 | 144 | const fileA = createFile("2020", ""); 145 | const fileB = createFile("2021", ""); 146 | const fileC = createFile("2022", ""); 147 | createFolder("/", [fileA, fileB, createFolder("foo", [fileC])]); 148 | 149 | expect(dailyNotesInterface.getAllYearlyNotes()).toEqual({ 150 | "year-2020-01-01T00:00:00-05:00": fileA, 151 | "year-2021-01-01T00:00:00-05:00": fileB, 152 | "year-2022-01-01T00:00:00-05:00": fileC, 153 | }); 154 | }); 155 | }); 156 | 157 | describe("getYearlyNote", () => { 158 | beforeEach(() => { 159 | window.app = getMockApp(); 160 | window.moment = moment; 161 | window.existingFiles = {}; 162 | }); 163 | 164 | test("returns note on the same year even if the HH:mm:SS is different", () => { 165 | setYearlyConfig({ 166 | enabled: true, 167 | folder: "/", 168 | format: "YYYY-HHmm", 169 | template: "template", 170 | }); 171 | 172 | const fileA = createFile("2021-0408", ""); 173 | 174 | expect( 175 | dailyNotesInterface.getYearlyNote( 176 | moment("2021-01-0745", "YYYY-MM-HHmm", true), 177 | { 178 | "year-2021-01-01T00:00:00-05:00": fileA, 179 | } 180 | ) 181 | ).toEqual(fileA); 182 | }); 183 | 184 | test("returns null if there is no yearly note for a given date", () => { 185 | setYearlyConfig({ 186 | enabled: true, 187 | folder: "/", 188 | format: "YYYY-ww", 189 | }); 190 | 191 | const fileA = createFile("2020-01", ""); 192 | 193 | expect( 194 | dailyNotesInterface.getYearlyNote(moment("2020-01", "YYYY-ww", true), { 195 | "2020-12": fileA, 196 | }) 197 | ).toEqual(null); 198 | }); 199 | }); 200 | 201 | describe("createYearlyNote", () => { 202 | beforeEach(() => { 203 | window.app = getMockApp(); 204 | window.moment = moment; 205 | window.existingFiles = {}; 206 | }); 207 | 208 | test("uses folder path from yearly note settings", async () => { 209 | setYearlyConfig({ 210 | enabled: true, 211 | folder: "/yearly-notes", 212 | format: "YYYY", 213 | }); 214 | 215 | const date = moment("2020-10", "YYYY-MM", true); 216 | await dailyNotesInterface.createYearlyNote(date); 217 | 218 | expect(window.app.vault.create).toHaveBeenCalledWith( 219 | "/yearly-notes/2020.md", 220 | "" 221 | ); 222 | }); 223 | 224 | test("uses template contents when creating file", async () => { 225 | setYearlyConfig({ 226 | enabled: true, 227 | folder: "/yearly-notes", 228 | format: "YYYY", 229 | template: "template", 230 | }); 231 | 232 | createFile("template", "template contents"); 233 | 234 | const date = moment("2020-10", "YYYY-MM", true); 235 | await dailyNotesInterface.createYearlyNote(date); 236 | 237 | expect(window.app.vault.create).toHaveBeenCalledWith( 238 | "/yearly-notes/2020.md", 239 | "template contents" 240 | ); 241 | }); 242 | 243 | test("shows error if file creation failed", async () => { 244 | const createFn = window.app.vault.create; 245 | (createFn as jest.MockedFunction).mockRejectedValue( 246 | "error" 247 | ); 248 | jest.spyOn(global.console, "error").mockImplementation(); 249 | 250 | setYearlyConfig({ 251 | enabled: true, 252 | folder: "/yearly-notes", 253 | format: "YYYY", 254 | template: "template", 255 | }); 256 | const date = moment("2020-10", "YYYY-MM", true); 257 | 258 | await dailyNotesInterface.createYearlyNote(date); 259 | 260 | expect(console.error).toHaveBeenCalledWith( 261 | "Failed to create file: '/yearly-notes/2020.md'", 262 | "error" 263 | ); 264 | }); 265 | }); 266 | 267 | describe("createYearlyNote", () => { 268 | beforeEach(() => { 269 | window.app = getMockApp(); 270 | window.moment = moment.bind(null, "2021-02-15T14:06:00-05:00"); 271 | moment.tz.setDefault("America/New_York"); 272 | }); 273 | 274 | test("replaces all mustaches in template", async () => { 275 | const getTemplateInfo = jest.spyOn(vaultUtils, "getTemplateInfo"); 276 | getTemplateInfo.mockResolvedValue([ 277 | ` 278 | {{date}} 279 | {{time}} 280 | {{title}} 281 | {{date:YYYY}} {{date:MMMM}} 282 | {{date+2d:YYYY-MM-DD}} 283 | {{date+1M:YYYY-MM-DD}} 284 | {{date+10y:YYYY-MM-DD}} 285 | {{date +1M}} 286 | `, 287 | null, 288 | ]); 289 | 290 | setYearlyConfig({ 291 | enabled: true, 292 | folder: "/", 293 | format: "YYYY", 294 | template: "template", 295 | }); 296 | 297 | await dailyNotesInterface.createYearlyNote(window.moment()); 298 | 299 | expect(window.app.vault.create).toHaveBeenCalledWith( 300 | "/2021.md", 301 | ` 302 | 2021 303 | 2021 304 | 2021 305 | 2021 February 306 | 2021-02-17 307 | 2021-03-15 308 | 2031-02-15 309 | 2021 310 | ` 311 | ); 312 | }); 313 | }); 314 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD"; 2 | export const DEFAULT_WEEKLY_NOTE_FORMAT = "gggg-[W]ww"; 3 | export const DEFAULT_MONTHLY_NOTE_FORMAT = "YYYY-MM"; 4 | export const DEFAULT_QUARTERLY_NOTE_FORMAT = "YYYY-[Q]Q"; 5 | export const DEFAULT_YEARLY_NOTE_FORMAT = "YYYY"; 6 | -------------------------------------------------------------------------------- /src/daily.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { App, normalizePath, Notice, TFile, TFolder, Vault } from "obsidian"; 3 | 4 | import { getDateFromFile, getDateUID } from "./parse"; 5 | import { getDailyNoteSettings } from "./settings"; 6 | import { getTemplateInfo, getNotePath } from "./vault"; 7 | 8 | export class DailyNotesFolderMissingError extends Error {} 9 | 10 | /** 11 | * This function mimics the behavior of the daily-notes plugin 12 | * so it will replace {{date}}, {{title}}, and {{time}} with the 13 | * formatted timestamp. 14 | * 15 | * Note: it has an added bonus that it's not 'today' specific. 16 | */ 17 | export async function createDailyNote(date: Moment): Promise { 18 | const app = window.app as App; 19 | const { vault } = app; 20 | const moment = window.moment; 21 | 22 | const { template, format, folder } = getDailyNoteSettings(); 23 | 24 | const [templateContents, IFoldInfo] = await getTemplateInfo(template); 25 | const filename = date.format(format); 26 | const normalizedPath = await getNotePath(folder, filename); 27 | 28 | try { 29 | const createdFile = await vault.create( 30 | normalizedPath, 31 | templateContents 32 | .replace(/{{\s*date\s*}}/gi, filename) 33 | .replace(/{{\s*time\s*}}/gi, moment().format("HH:mm")) 34 | .replace(/{{\s*title\s*}}/gi, filename) 35 | .replace( 36 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, 37 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { 38 | const now = moment(); 39 | const currentDate = date.clone().set({ 40 | hour: now.get("hour"), 41 | minute: now.get("minute"), 42 | second: now.get("second"), 43 | }); 44 | if (calc) { 45 | currentDate.add(parseInt(timeDelta, 10), unit); 46 | } 47 | 48 | if (momentFormat) { 49 | return currentDate.format(momentFormat.substring(1).trim()); 50 | } 51 | return currentDate.format(format); 52 | } 53 | ) 54 | .replace( 55 | /{{\s*yesterday\s*}}/gi, 56 | date.clone().subtract(1, "day").format(format) 57 | ) 58 | .replace( 59 | /{{\s*tomorrow\s*}}/gi, 60 | date.clone().add(1, "d").format(format) 61 | ) 62 | ); 63 | 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | (app as any).foldManager.save(createdFile, IFoldInfo); 66 | 67 | return createdFile; 68 | } catch (err) { 69 | console.error(`Failed to create file: '${normalizedPath}'`, err); 70 | new Notice("Unable to create new file."); 71 | } 72 | } 73 | 74 | export function getDailyNote( 75 | date: Moment, 76 | dailyNotes: Record 77 | ): TFile { 78 | return dailyNotes[getDateUID(date, "day")] ?? null; 79 | } 80 | 81 | export function getAllDailyNotes(): Record { 82 | /** 83 | * Find all daily notes in the daily note folder 84 | */ 85 | const { vault } = window.app; 86 | const { folder } = getDailyNoteSettings(); 87 | 88 | const dailyNotesFolder = vault.getAbstractFileByPath( 89 | normalizePath(folder) 90 | ) as TFolder; 91 | 92 | if (!dailyNotesFolder) { 93 | throw new DailyNotesFolderMissingError("Failed to find daily notes folder"); 94 | } 95 | 96 | const dailyNotes: Record = {}; 97 | Vault.recurseChildren(dailyNotesFolder, (note) => { 98 | if (note instanceof TFile) { 99 | const date = getDateFromFile(note, "day"); 100 | if (date) { 101 | const dateString = getDateUID(date, "day"); 102 | dailyNotes[dateString] = note; 103 | } 104 | } 105 | }); 106 | 107 | return dailyNotes; 108 | } 109 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type moment from "moment"; 2 | import type { Moment } from "moment"; 3 | import { App, TFile } from "obsidian"; 4 | 5 | declare global { 6 | interface Window { 7 | app: App; 8 | moment: typeof moment; 9 | } 10 | } 11 | 12 | export function appHasDailyNotesPluginLoaded(): boolean { 13 | const { app } = window; 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | const dailyNotesPlugin = (app).internalPlugins.plugins["daily-notes"]; 16 | if (dailyNotesPlugin && dailyNotesPlugin.enabled) { 17 | return true; 18 | } 19 | 20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 21 | const periodicNotes = (app).plugins.getPlugin("periodic-notes"); 22 | return periodicNotes && periodicNotes.settings?.daily?.enabled; 23 | } 24 | 25 | /** 26 | * XXX: "Weekly Notes" live in either the Calendar plugin or the periodic-notes plugin. 27 | * Check both until the weekly notes feature is removed from the Calendar plugin. 28 | */ 29 | export function appHasWeeklyNotesPluginLoaded(): boolean { 30 | const { app } = window; 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 33 | if ((app).plugins.getPlugin("calendar")) { 34 | return true; 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | const periodicNotes = (app).plugins.getPlugin("periodic-notes"); 39 | return periodicNotes && periodicNotes.settings?.weekly?.enabled; 40 | } 41 | 42 | export function appHasMonthlyNotesPluginLoaded(): boolean { 43 | const { app } = window; 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | const periodicNotes = (app).plugins.getPlugin("periodic-notes"); 46 | return periodicNotes && periodicNotes.settings?.monthly?.enabled; 47 | } 48 | 49 | export function appHasQuarterlyNotesPluginLoaded(): boolean { 50 | const { app } = window; 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | const periodicNotes = (app).plugins.getPlugin("periodic-notes"); 53 | return periodicNotes && periodicNotes.settings?.quarterly?.enabled; 54 | } 55 | 56 | export function appHasYearlyNotesPluginLoaded(): boolean { 57 | const { app } = window; 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | const periodicNotes = (app).plugins.getPlugin("periodic-notes"); 60 | return periodicNotes && periodicNotes.settings?.yearly?.enabled; 61 | } 62 | 63 | export { 64 | DEFAULT_DAILY_NOTE_FORMAT, 65 | DEFAULT_WEEKLY_NOTE_FORMAT, 66 | DEFAULT_MONTHLY_NOTE_FORMAT, 67 | DEFAULT_QUARTERLY_NOTE_FORMAT, 68 | DEFAULT_YEARLY_NOTE_FORMAT, 69 | } from "./constants"; 70 | 71 | import type { IGranularity, IPeriodicNoteSettings } from "./types"; 72 | import { 73 | getDailyNoteSettings, 74 | getWeeklyNoteSettings, 75 | getMonthlyNoteSettings, 76 | getQuarterlyNoteSettings, 77 | getYearlyNoteSettings, 78 | } from "./settings"; 79 | import { createDailyNote, getDailyNote, getAllDailyNotes } from "./daily"; 80 | import { createWeeklyNote, getAllWeeklyNotes, getWeeklyNote } from "./weekly"; 81 | import { 82 | createMonthlyNote, 83 | getAllMonthlyNotes, 84 | getMonthlyNote, 85 | } from "./monthly"; 86 | import { 87 | createQuarterlyNote, 88 | getAllQuarterlyNotes, 89 | getQuarterlyNote, 90 | } from "./quarterly"; 91 | import { createYearlyNote, getAllYearlyNotes, getYearlyNote } from "./yearly"; 92 | 93 | export { getDateUID, getDateFromFile, getDateFromPath } from "./parse"; 94 | export { getTemplateInfo } from "./vault"; 95 | 96 | function getPeriodicNoteSettings( 97 | granularity: IGranularity 98 | ): IPeriodicNoteSettings { 99 | const getSettings = { 100 | day: getDailyNoteSettings, 101 | week: getWeeklyNoteSettings, 102 | month: getMonthlyNoteSettings, 103 | quarter: getQuarterlyNoteSettings, 104 | year: getYearlyNoteSettings, 105 | }[granularity]; 106 | 107 | return getSettings(); 108 | } 109 | 110 | function createPeriodicNote( 111 | granularity: IGranularity, 112 | date: Moment 113 | ): Promise { 114 | const createFn = { 115 | day: createDailyNote, 116 | month: createMonthlyNote, 117 | week: createWeeklyNote, 118 | }; 119 | return createFn[granularity](date); 120 | } 121 | 122 | export type { IGranularity, IPeriodicNoteSettings }; 123 | export { 124 | createDailyNote, 125 | createMonthlyNote, 126 | createWeeklyNote, 127 | createQuarterlyNote, 128 | createYearlyNote, 129 | createPeriodicNote, 130 | getAllDailyNotes, 131 | getAllMonthlyNotes, 132 | getAllWeeklyNotes, 133 | getAllQuarterlyNotes, 134 | getAllYearlyNotes, 135 | getDailyNote, 136 | getDailyNoteSettings, 137 | getMonthlyNote, 138 | getMonthlyNoteSettings, 139 | getPeriodicNoteSettings, 140 | getWeeklyNote, 141 | getWeeklyNoteSettings, 142 | getQuarterlyNote, 143 | getQuarterlyNoteSettings, 144 | getYearlyNote, 145 | getYearlyNoteSettings, 146 | }; 147 | -------------------------------------------------------------------------------- /src/monthly.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { normalizePath, Notice, TFile, TFolder, Vault } from "obsidian"; 3 | 4 | import { appHasMonthlyNotesPluginLoaded } from "./index"; 5 | import { getDateFromFile, getDateUID } from "./parse"; 6 | import { getMonthlyNoteSettings } from "./settings"; 7 | import { getNotePath, getTemplateInfo } from "./vault"; 8 | 9 | export class MonthlyNotesFolderMissingError extends Error {} 10 | 11 | /** 12 | * This function mimics the behavior of the daily-notes plugin 13 | * so it will replace {{date}}, {{title}}, and {{time}} with the 14 | * formatted timestamp. 15 | * 16 | * Note: it has an added bonus that it's not 'today' specific. 17 | */ 18 | export async function createMonthlyNote(date: Moment): Promise { 19 | const { vault } = window.app; 20 | const { template, format, folder } = getMonthlyNoteSettings(); 21 | const [templateContents, IFoldInfo] = await getTemplateInfo(template); 22 | const filename = date.format(format); 23 | const normalizedPath = await getNotePath(folder, filename); 24 | 25 | try { 26 | const createdFile = await vault.create( 27 | normalizedPath, 28 | templateContents 29 | .replace( 30 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, 31 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { 32 | const now = window.moment(); 33 | const currentDate = date.clone().set({ 34 | hour: now.get("hour"), 35 | minute: now.get("minute"), 36 | second: now.get("second"), 37 | }); 38 | if (calc) { 39 | currentDate.add(parseInt(timeDelta, 10), unit); 40 | } 41 | 42 | if (momentFormat) { 43 | return currentDate.format(momentFormat.substring(1).trim()); 44 | } 45 | return currentDate.format(format); 46 | } 47 | ) 48 | .replace(/{{\s*date\s*}}/gi, filename) 49 | .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) 50 | .replace(/{{\s*title\s*}}/gi, filename) 51 | ); 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | (window.app as any).foldManager.save(createdFile, IFoldInfo); 55 | 56 | return createdFile; 57 | } catch (err) { 58 | console.error(`Failed to create file: '${normalizedPath}'`, err); 59 | new Notice("Unable to create new file."); 60 | } 61 | } 62 | 63 | export function getMonthlyNote( 64 | date: Moment, 65 | monthlyNotes: Record 66 | ): TFile { 67 | return monthlyNotes[getDateUID(date, "month")] ?? null; 68 | } 69 | 70 | export function getAllMonthlyNotes(): Record { 71 | const monthlyNotes: Record = {}; 72 | 73 | if (!appHasMonthlyNotesPluginLoaded()) { 74 | return monthlyNotes; 75 | } 76 | const { vault } = window.app; 77 | const { folder } = getMonthlyNoteSettings(); 78 | 79 | const monthlyNotesFolder = vault.getAbstractFileByPath( 80 | normalizePath(folder) 81 | ) as TFolder; 82 | 83 | if (!monthlyNotesFolder) { 84 | throw new MonthlyNotesFolderMissingError( 85 | "Failed to find monthly notes folder" 86 | ); 87 | } 88 | 89 | Vault.recurseChildren(monthlyNotesFolder, (note) => { 90 | if (note instanceof TFile) { 91 | const date = getDateFromFile(note, "month"); 92 | if (date) { 93 | const dateString = getDateUID(date, "month"); 94 | monthlyNotes[dateString] = note; 95 | } 96 | } 97 | }); 98 | 99 | return monthlyNotes; 100 | } 101 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { TFile } from "obsidian"; 3 | 4 | import { 5 | getDailyNoteSettings, 6 | getWeeklyNoteSettings, 7 | getMonthlyNoteSettings, 8 | getQuarterlyNoteSettings, 9 | getYearlyNoteSettings, 10 | } from "./settings"; 11 | 12 | import { IGranularity } from "./types"; 13 | import { basename } from "./vault"; 14 | 15 | /** 16 | * dateUID is a way of weekly identifying daily/weekly/monthly notes. 17 | * They are prefixed with the granularity to avoid ambiguity. 18 | */ 19 | export function getDateUID( 20 | date: Moment, 21 | granularity: IGranularity = "day" 22 | ): string { 23 | const ts = date.clone().startOf(granularity).format(); 24 | return `${granularity}-${ts}`; 25 | } 26 | 27 | function removeEscapedCharacters(format: string): string { 28 | return format.replace(/\[[^\]]*\]/g, ""); // remove everything within brackets 29 | } 30 | 31 | /** 32 | * XXX: When parsing dates that contain both week numbers and months, 33 | * Moment choses to ignore the week numbers. For the week dateUID, we 34 | * want the opposite behavior. Strip the MMM from the format to patch. 35 | */ 36 | function isFormatAmbiguous(format: string, granularity: IGranularity) { 37 | if (granularity === "week") { 38 | const cleanFormat = removeEscapedCharacters(format); 39 | return ( 40 | /w{1,2}/i.test(cleanFormat) && 41 | (/M{1,4}/.test(cleanFormat) || /D{1,4}/.test(cleanFormat)) 42 | ); 43 | } 44 | return false; 45 | } 46 | 47 | export function getDateFromFile( 48 | file: TFile, 49 | granularity: IGranularity 50 | ): Moment | null { 51 | return getDateFromFilename(file.basename, granularity); 52 | } 53 | 54 | export function getDateFromPath( 55 | path: string, 56 | granularity: IGranularity 57 | ): Moment | null { 58 | return getDateFromFilename(basename(path), granularity); 59 | } 60 | 61 | function getDateFromFilename( 62 | filename: string, 63 | granularity: IGranularity 64 | ): Moment | null { 65 | const getSettings = { 66 | day: getDailyNoteSettings, 67 | week: getWeeklyNoteSettings, 68 | month: getMonthlyNoteSettings, 69 | quarter: getQuarterlyNoteSettings, 70 | year: getYearlyNoteSettings, 71 | }; 72 | 73 | const format = getSettings[granularity]().format.split("/").pop(); 74 | const noteDate = window.moment(filename, format, true); 75 | 76 | if (!noteDate.isValid()) { 77 | return null; 78 | } 79 | 80 | if (isFormatAmbiguous(format, granularity)) { 81 | if (granularity === "week") { 82 | const cleanFormat = removeEscapedCharacters(format); 83 | if (/w{1,2}/i.test(cleanFormat)) { 84 | return window.moment( 85 | filename, 86 | // If format contains week, remove day & month formatting 87 | format.replace(/M{1,4}/g, "").replace(/D{1,4}/g, ""), 88 | false 89 | ); 90 | } 91 | } 92 | } 93 | 94 | return noteDate; 95 | } 96 | -------------------------------------------------------------------------------- /src/quarterly.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { normalizePath, Notice, TFile, TFolder, Vault } from "obsidian"; 3 | 4 | import { appHasQuarterlyNotesPluginLoaded } from "./index"; 5 | import { getDateFromFile, getDateUID } from "./parse"; 6 | import { getQuarterlyNoteSettings } from "./settings"; 7 | import { getNotePath, getTemplateInfo } from "./vault"; 8 | 9 | export class QuarterlyNotesFolderMissingError extends Error {} 10 | 11 | /** 12 | * This function mimics the behavior of the daily-notes plugin 13 | * so it will replace {{date}}, {{title}}, and {{time}} with the 14 | * formatted timestamp. 15 | * 16 | * Note: it has an added bonus that it's not 'today' specific. 17 | */ 18 | export async function createQuarterlyNote(date: Moment): Promise { 19 | const { vault } = window.app; 20 | const { template, format, folder } = getQuarterlyNoteSettings(); 21 | const [templateContents, IFoldInfo] = await getTemplateInfo(template); 22 | const filename = date.format(format); 23 | const normalizedPath = await getNotePath(folder, filename); 24 | 25 | try { 26 | const createdFile = await vault.create( 27 | normalizedPath, 28 | templateContents 29 | .replace( 30 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, 31 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { 32 | const now = window.moment(); 33 | const currentDate = date.clone().set({ 34 | hour: now.get("hour"), 35 | minute: now.get("minute"), 36 | second: now.get("second"), 37 | }); 38 | if (calc) { 39 | currentDate.add(parseInt(timeDelta, 10), unit); 40 | } 41 | 42 | if (momentFormat) { 43 | return currentDate.format(momentFormat.substring(1).trim()); 44 | } 45 | return currentDate.format(format); 46 | } 47 | ) 48 | .replace(/{{\s*date\s*}}/gi, filename) 49 | .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) 50 | .replace(/{{\s*title\s*}}/gi, filename) 51 | ); 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | (window.app as any).foldManager.save(createdFile, IFoldInfo); 55 | 56 | return createdFile; 57 | } catch (err) { 58 | console.error(`Failed to create file: '${normalizedPath}'`, err); 59 | new Notice("Unable to create new file."); 60 | } 61 | } 62 | 63 | export function getQuarterlyNote( 64 | date: Moment, 65 | quarterly: Record 66 | ): TFile { 67 | return quarterly[getDateUID(date, "quarter")] ?? null; 68 | } 69 | 70 | export function getAllQuarterlyNotes(): Record { 71 | const quarterly: Record = {}; 72 | 73 | if (!appHasQuarterlyNotesPluginLoaded()) { 74 | return quarterly; 75 | } 76 | const { vault } = window.app; 77 | const { folder } = getQuarterlyNoteSettings(); 78 | 79 | const quarterlyFolder = vault.getAbstractFileByPath( 80 | normalizePath(folder) 81 | ) as TFolder; 82 | 83 | if (!quarterlyFolder) { 84 | throw new QuarterlyNotesFolderMissingError( 85 | "Failed to find quarterly notes folder" 86 | ); 87 | } 88 | 89 | Vault.recurseChildren(quarterlyFolder, (note) => { 90 | if (note instanceof TFile) { 91 | const date = getDateFromFile(note, "quarter"); 92 | if (date) { 93 | const dateString = getDateUID(date, "quarter"); 94 | quarterly[dateString] = note; 95 | } 96 | } 97 | }); 98 | 99 | return quarterly; 100 | } 101 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEFAULT_DAILY_NOTE_FORMAT, 3 | DEFAULT_MONTHLY_NOTE_FORMAT, 4 | DEFAULT_WEEKLY_NOTE_FORMAT, 5 | DEFAULT_QUARTERLY_NOTE_FORMAT, 6 | DEFAULT_YEARLY_NOTE_FORMAT, 7 | } from "./constants"; 8 | import { IPeriodicNoteSettings } from "./types"; 9 | 10 | export function shouldUsePeriodicNotesSettings( 11 | periodicity: "daily" | "weekly" | "monthly" | "quarterly" | "yearly" 12 | ): boolean { 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | const periodicNotes = (window.app).plugins.getPlugin("periodic-notes"); 15 | return periodicNotes && periodicNotes.settings?.[periodicity]?.enabled; 16 | } 17 | 18 | /** 19 | * Read the user settings for the `daily-notes` plugin 20 | * to keep behavior of creating a new note in-sync. 21 | */ 22 | export function getDailyNoteSettings(): IPeriodicNoteSettings { 23 | try { 24 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 25 | const { internalPlugins, plugins } = window.app; 26 | 27 | if (shouldUsePeriodicNotesSettings("daily")) { 28 | const { format, folder, template } = 29 | plugins.getPlugin("periodic-notes")?.settings?.daily || {}; 30 | return { 31 | format: format || DEFAULT_DAILY_NOTE_FORMAT, 32 | folder: folder?.trim() || "", 33 | template: template?.trim() || "", 34 | }; 35 | } 36 | 37 | const { folder, format, template } = 38 | internalPlugins.getPluginById("daily-notes")?.instance?.options || {}; 39 | return { 40 | format: format || DEFAULT_DAILY_NOTE_FORMAT, 41 | folder: folder?.trim() || "", 42 | template: template?.trim() || "", 43 | }; 44 | } catch (err) { 45 | console.info("No custom daily note settings found!", err); 46 | } 47 | } 48 | 49 | /** 50 | * Read the user settings for the `weekly-notes` plugin 51 | * to keep behavior of creating a new note in-sync. 52 | */ 53 | export function getWeeklyNoteSettings(): IPeriodicNoteSettings { 54 | try { 55 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 56 | const pluginManager = (window.app).plugins; 57 | 58 | const calendarSettings = pluginManager.getPlugin("calendar")?.options; 59 | const periodicNotesSettings = 60 | pluginManager.getPlugin("periodic-notes")?.settings?.weekly; 61 | 62 | if (shouldUsePeriodicNotesSettings("weekly")) { 63 | return { 64 | format: periodicNotesSettings.format || DEFAULT_WEEKLY_NOTE_FORMAT, 65 | folder: periodicNotesSettings.folder?.trim() || "", 66 | template: periodicNotesSettings.template?.trim() || "", 67 | }; 68 | } 69 | 70 | const settings = calendarSettings || {}; 71 | return { 72 | format: settings.weeklyNoteFormat || DEFAULT_WEEKLY_NOTE_FORMAT, 73 | folder: settings.weeklyNoteFolder?.trim() || "", 74 | template: settings.weeklyNoteTemplate?.trim() || "", 75 | }; 76 | } catch (err) { 77 | console.info("No custom weekly note settings found!", err); 78 | } 79 | } 80 | 81 | /** 82 | * Read the user settings for the `periodic-notes` plugin 83 | * to keep behavior of creating a new note in-sync. 84 | */ 85 | export function getMonthlyNoteSettings(): IPeriodicNoteSettings { 86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 87 | const pluginManager = (window.app).plugins; 88 | 89 | try { 90 | const settings = 91 | (shouldUsePeriodicNotesSettings("monthly") && 92 | pluginManager.getPlugin("periodic-notes")?.settings?.monthly) || 93 | {}; 94 | 95 | return { 96 | format: settings.format || DEFAULT_MONTHLY_NOTE_FORMAT, 97 | folder: settings.folder?.trim() || "", 98 | template: settings.template?.trim() || "", 99 | }; 100 | } catch (err) { 101 | console.info("No custom monthly note settings found!", err); 102 | } 103 | } 104 | 105 | /** 106 | * Read the user settings for the `periodic-notes` plugin 107 | * to keep behavior of creating a new note in-sync. 108 | */ 109 | export function getQuarterlyNoteSettings(): IPeriodicNoteSettings { 110 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 111 | const pluginManager = (window.app).plugins; 112 | 113 | try { 114 | const settings = 115 | (shouldUsePeriodicNotesSettings("quarterly") && 116 | pluginManager.getPlugin("periodic-notes")?.settings?.quarterly) || 117 | {}; 118 | 119 | return { 120 | format: settings.format || DEFAULT_QUARTERLY_NOTE_FORMAT, 121 | folder: settings.folder?.trim() || "", 122 | template: settings.template?.trim() || "", 123 | }; 124 | } catch (err) { 125 | console.info("No custom quarterly note settings found!", err); 126 | } 127 | } 128 | 129 | /** 130 | * Read the user settings for the `periodic-notes` plugin 131 | * to keep behavior of creating a new note in-sync. 132 | */ 133 | export function getYearlyNoteSettings(): IPeriodicNoteSettings { 134 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 135 | const pluginManager = (window.app).plugins; 136 | 137 | try { 138 | const settings = 139 | (shouldUsePeriodicNotesSettings("yearly") && 140 | pluginManager.getPlugin("periodic-notes")?.settings?.yearly) || 141 | {}; 142 | 143 | return { 144 | format: settings.format || DEFAULT_YEARLY_NOTE_FORMAT, 145 | folder: settings.folder?.trim() || "", 146 | template: settings.template?.trim() || "", 147 | }; 148 | } catch (err) { 149 | console.info("No custom yearly note settings found!", err); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/testUtils/mockApp.ts: -------------------------------------------------------------------------------- 1 | import { App, TAbstractFile, TFile, TFolder } from "obsidian"; 2 | 3 | declare global { 4 | interface Window { 5 | existingFiles: Record; 6 | } 7 | } 8 | 9 | window.existingFiles = {}; 10 | 11 | export function createFile(basename: string, contents: string): TFile { 12 | const file = new TFile(); 13 | file.basename = basename; 14 | file.path = `/${basename}.md`; 15 | // eslint-disable-next-line 16 | (file as any).unsafeCachedData = contents; 17 | 18 | window.existingFiles[file.path] = file; 19 | return file; 20 | } 21 | 22 | export function createFolder(path: string, children: TAbstractFile[]): TFolder { 23 | const folder = new TFolder(); 24 | folder.path = path; 25 | folder.children = children; 26 | 27 | window.existingFiles[path] = folder; 28 | return folder; 29 | } 30 | 31 | interface IPluginInstance { 32 | _loaded: boolean; 33 | settings?: Record; 34 | options?: Record; 35 | } 36 | 37 | interface IInternalPluginInstance { 38 | options: Record; 39 | } 40 | 41 | interface IInternalPlugin { 42 | instance: IInternalPluginInstance; 43 | } 44 | 45 | /* eslint-disable */ 46 | export default function getMockApp(): App { 47 | const pluginList: Record = { 48 | calendar: { 49 | _loaded: true, 50 | options: {}, 51 | }, 52 | "periodic-notes": { 53 | settings: { 54 | daily: { 55 | enabled: false, 56 | format: "", 57 | template: "", 58 | folder: "", 59 | }, 60 | weekly: { 61 | enabled: false, 62 | format: "", 63 | template: "", 64 | folder: "", 65 | }, 66 | monthly: { 67 | enabled: false, 68 | format: "", 69 | template: "", 70 | folder: "", 71 | }, 72 | quarterly: { 73 | enabled: false, 74 | format: "", 75 | template: "", 76 | folder: "", 77 | }, 78 | yearly: { 79 | enabled: false, 80 | format: "", 81 | template: "", 82 | folder: "", 83 | }, 84 | }, 85 | _loaded: true, 86 | }, 87 | }; 88 | const plugins = { 89 | plugins: pluginList, 90 | getPlugin: (pluginId: string) => { 91 | const plugin = pluginList[pluginId]; 92 | return plugin._loaded && plugin; 93 | }, 94 | }; 95 | 96 | const internalPluginList: Record = { 97 | "daily-notes": { 98 | instance: { 99 | options: {}, 100 | }, 101 | }, 102 | }; 103 | const internalPlugins = { 104 | plugins: internalPluginList, 105 | getPluginById: (pluginId: string) => internalPluginList[pluginId], 106 | }; 107 | return { 108 | // @ts-ignore 109 | foldManager: { 110 | save: jest.fn(), 111 | load: jest.fn(), 112 | }, 113 | vault: { 114 | configDir: "", 115 | adapter: { 116 | exists: () => Promise.resolve(false), 117 | getName: () => "", 118 | list: () => Promise.resolve(null), 119 | read: () => Promise.resolve(null), 120 | readBinary: () => Promise.resolve(null), 121 | write: () => Promise.resolve(), 122 | writeBinary: () => Promise.resolve(), 123 | getResourcePath: () => "", 124 | mkdir: () => Promise.resolve(), 125 | trashSystem: () => Promise.resolve(true), 126 | trashLocal: () => Promise.resolve(), 127 | rmdir: () => Promise.resolve(), 128 | remove: () => Promise.resolve(), 129 | rename: () => Promise.resolve(), 130 | copy: () => Promise.resolve(), 131 | }, 132 | getName: () => "", 133 | getAbstractFileByPath: (path: string) => 134 | window.existingFiles[path] || null, 135 | getRoot: () => ({ 136 | children: [], 137 | isRoot: () => true, 138 | name: "", 139 | parent: null, 140 | path: "", 141 | vault: null, 142 | }), 143 | create: jest.fn(), 144 | createFolder: () => Promise.resolve(null), 145 | createBinary: () => Promise.resolve(null), 146 | read: () => Promise.resolve(""), 147 | cachedRead: (file: TFile) => { 148 | if (!file) { 149 | return Promise.reject("error"); 150 | } 151 | // eslint-disable-next-line 152 | return Promise.resolve((file as any).unsafeCachedData); 153 | }, 154 | readBinary: () => Promise.resolve(null), 155 | getResourcePath: () => null, 156 | delete: () => Promise.resolve(), 157 | trash: () => Promise.resolve(), 158 | rename: () => Promise.resolve(), 159 | modify: () => Promise.resolve(), 160 | modifyBinary: () => Promise.resolve(), 161 | copy: () => Promise.resolve(null), 162 | getAllLoadedFiles: () => [], 163 | getMarkdownFiles: () => [], 164 | getFiles: () => [], 165 | on: () => null, 166 | off: () => null, 167 | offref: () => null, 168 | tryTrigger: () => null, 169 | trigger: () => null, 170 | }, 171 | workspace: null, 172 | metadataCache: { 173 | getCache: () => null, 174 | getFileCache: () => null, 175 | getFirstLinkpathDest: (linkpath: string, sourcePath: string) => 176 | (window.existingFiles[`${linkpath}.md`] as TFile) || null, 177 | on: () => null, 178 | off: () => null, 179 | offref: () => null, 180 | tryTrigger: () => null, 181 | fileToLinktext: () => "", 182 | trigger: () => null, 183 | resolvedLinks: null, 184 | unresolvedLinks: null, 185 | }, 186 | // @ts-ignore 187 | plugins, 188 | // @ts-ignore 189 | internalPlugins, 190 | }; 191 | } 192 | /* eslint-enable */ 193 | -------------------------------------------------------------------------------- /src/testUtils/utils.ts: -------------------------------------------------------------------------------- 1 | import * as dailyNotesInterface from "../index"; 2 | 3 | interface IPerioditySettings extends dailyNotesInterface.IPeriodicNoteSettings { 4 | enabled: boolean; 5 | } 6 | 7 | export function setYearlyConfig(config: IPerioditySettings): void { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const plugin = (window.app).plugins.plugins["periodic-notes"]; 10 | 11 | plugin._loaded = true; 12 | plugin.settings.yearly = { 13 | ...plugin.settings.yearly, 14 | ...config, 15 | }; 16 | } 17 | 18 | export function setQuarterlyConfig(config: IPerioditySettings): void { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | const plugin = (window.app).plugins.plugins["periodic-notes"]; 21 | 22 | plugin._loaded = true; 23 | plugin.settings.quarterly = { 24 | ...plugin.settings.quarterly, 25 | ...config, 26 | }; 27 | } 28 | 29 | export function setMonthlyConfig(config: IPerioditySettings): void { 30 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 31 | const plugin = (window.app).plugins.plugins["periodic-notes"]; 32 | 33 | plugin._loaded = true; 34 | plugin.settings.monthly = { 35 | ...plugin.settings.monthly, 36 | ...config, 37 | }; 38 | } 39 | 40 | export function setWeeklyConfig(config: IPerioditySettings): void { 41 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 42 | (window.app).plugins.plugins["periodic-notes"]._loaded = false; 43 | 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | const plugin = (window.app).plugins.plugins["calendar"]; 46 | 47 | plugin._loaded = true; 48 | plugin.options = { 49 | weeklyNoteFolder: config.folder, 50 | weeklyNoteFormat: config.format, 51 | weeklyNoteTemplate: config.template, 52 | }; 53 | } 54 | export function setPeriodicNotesConfig( 55 | periodicity: "daily" | "weekly" | "monthly", 56 | config: IPerioditySettings 57 | ): void { 58 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 59 | const periodicNotes = (window.app).plugins.plugins["periodic-notes"]; 60 | 61 | periodicNotes._loaded = true; 62 | periodicNotes.settings[periodicity] = config; 63 | } 64 | 65 | export function setDailyConfig( 66 | config: dailyNotesInterface.IPeriodicNoteSettings 67 | ): void { 68 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 69 | (window.app).plugins.plugins["periodic-notes"]._loaded = false; 70 | 71 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 72 | (window.app).internalPlugins.plugins["daily-notes"].instance.options = 73 | config; 74 | } 75 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type IGranularity = "day" | "week" | "month" | "quarter" | "year"; 2 | export interface IPeriodicNoteSettings { 3 | folder?: string; 4 | format?: string; 5 | template?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/vault.ts: -------------------------------------------------------------------------------- 1 | import { normalizePath, Notice } from "obsidian"; 2 | 3 | interface IFold { 4 | from: number; 5 | to: number; 6 | } 7 | 8 | interface IFoldInfo { 9 | folds: IFold[]; 10 | } 11 | 12 | // Credit: @creationix/path.js 13 | export function join(...partSegments: string[]): string { 14 | // Split the inputs into a list of path commands. 15 | let parts = []; 16 | for (let i = 0, l = partSegments.length; i < l; i++) { 17 | parts = parts.concat(partSegments[i].split("/")); 18 | } 19 | // Interpret the path commands to get the new resolved path. 20 | const newParts = []; 21 | for (let i = 0, l = parts.length; i < l; i++) { 22 | const part = parts[i]; 23 | // Remove leading and trailing slashes 24 | // Also remove "." segments 25 | if (!part || part === ".") continue; 26 | // Push new path segments. 27 | else newParts.push(part); 28 | } 29 | // Preserve the initial slash if there was one. 30 | if (parts[0] === "") newParts.unshift(""); 31 | // Turn back into a single string path. 32 | return newParts.join("/"); 33 | } 34 | 35 | export function basename(fullPath: string): string { 36 | let base = fullPath.substring(fullPath.lastIndexOf("/") + 1); 37 | if (base.lastIndexOf(".") != -1) 38 | base = base.substring(0, base.lastIndexOf(".")); 39 | return base; 40 | } 41 | 42 | async function ensureFolderExists(path: string): Promise { 43 | const dirs = path.replace(/\\/g, "/").split("/"); 44 | dirs.pop(); // remove basename 45 | 46 | if (dirs.length) { 47 | const dir = join(...dirs); 48 | if (!window.app.vault.getAbstractFileByPath(dir)) { 49 | await window.app.vault.createFolder(dir); 50 | } 51 | } 52 | } 53 | 54 | export async function getNotePath( 55 | directory: string, 56 | filename: string 57 | ): Promise { 58 | if (!filename.endsWith(".md")) { 59 | filename += ".md"; 60 | } 61 | const path = normalizePath(join(directory, filename)); 62 | 63 | await ensureFolderExists(path); 64 | 65 | return path; 66 | } 67 | 68 | export async function getTemplateInfo( 69 | template: string 70 | ): Promise<[string, IFoldInfo]> { 71 | const { metadataCache, vault } = window.app; 72 | 73 | const templatePath = normalizePath(template); 74 | if (templatePath === "/") { 75 | return Promise.resolve(["", null]); 76 | } 77 | 78 | try { 79 | const templateFile = metadataCache.getFirstLinkpathDest(templatePath, ""); 80 | const contents = await vault.cachedRead(templateFile); 81 | 82 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 83 | const IFoldInfo = (window.app as any).foldManager.load(templateFile); 84 | return [contents, IFoldInfo]; 85 | } catch (err) { 86 | console.error( 87 | `Failed to read the daily note template '${templatePath}'`, 88 | err 89 | ); 90 | new Notice("Failed to read the daily note template"); 91 | return ["", null]; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/weekly.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { normalizePath, Notice, TFile, TFolder, Vault } from "obsidian"; 3 | 4 | import { appHasWeeklyNotesPluginLoaded } from "./index"; 5 | import { getDateFromFile, getDateUID } from "./parse"; 6 | import { getWeeklyNoteSettings } from "./settings"; 7 | import { getNotePath, getTemplateInfo } from "./vault"; 8 | 9 | export class WeeklyNotesFolderMissingError extends Error {} 10 | 11 | function getDaysOfWeek(): string[] { 12 | const { moment } = window; 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | let weekStart = (moment.localeData())._week.dow; 15 | const daysOfWeek = [ 16 | "sunday", 17 | "monday", 18 | "tuesday", 19 | "wednesday", 20 | "thursday", 21 | "friday", 22 | "saturday", 23 | ]; 24 | 25 | while (weekStart) { 26 | daysOfWeek.push(daysOfWeek.shift()); 27 | weekStart--; 28 | } 29 | return daysOfWeek; 30 | } 31 | 32 | export function getDayOfWeekNumericalValue(dayOfWeekName: string): number { 33 | return getDaysOfWeek().indexOf(dayOfWeekName.toLowerCase()); 34 | } 35 | 36 | export async function createWeeklyNote(date: Moment): Promise { 37 | const { vault } = window.app; 38 | const { template, format, folder } = getWeeklyNoteSettings(); 39 | const [templateContents, IFoldInfo] = await getTemplateInfo(template); 40 | const filename = date.format(format); 41 | const normalizedPath = await getNotePath(folder, filename); 42 | 43 | try { 44 | const createdFile = await vault.create( 45 | normalizedPath, 46 | templateContents 47 | .replace( 48 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, 49 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { 50 | const now = window.moment(); 51 | const currentDate = date.clone().set({ 52 | hour: now.get("hour"), 53 | minute: now.get("minute"), 54 | second: now.get("second"), 55 | }); 56 | if (calc) { 57 | currentDate.add(parseInt(timeDelta, 10), unit); 58 | } 59 | 60 | if (momentFormat) { 61 | return currentDate.format(momentFormat.substring(1).trim()); 62 | } 63 | return currentDate.format(format); 64 | } 65 | ) 66 | .replace(/{{\s*title\s*}}/gi, filename) 67 | .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) 68 | .replace( 69 | /{{\s*(sunday|monday|tuesday|wednesday|thursday|friday|saturday)\s*:(.*?)}}/gi, 70 | (_, dayOfWeek, momentFormat) => { 71 | const day = getDayOfWeekNumericalValue(dayOfWeek); 72 | return date.weekday(day).format(momentFormat.trim()); 73 | } 74 | ) 75 | ); 76 | 77 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 78 | (window.app as any).foldManager.save(createdFile, IFoldInfo); 79 | 80 | return createdFile; 81 | } catch (err) { 82 | console.error(`Failed to create file: '${normalizedPath}'`, err); 83 | new Notice("Unable to create new file."); 84 | } 85 | } 86 | 87 | export function getWeeklyNote( 88 | date: Moment, 89 | weeklyNotes: Record 90 | ): TFile { 91 | return weeklyNotes[getDateUID(date, "week")] ?? null; 92 | } 93 | 94 | export function getAllWeeklyNotes(): Record { 95 | const weeklyNotes: Record = {}; 96 | 97 | if (!appHasWeeklyNotesPluginLoaded()) { 98 | return weeklyNotes; 99 | } 100 | 101 | const { vault } = window.app; 102 | const { folder } = getWeeklyNoteSettings(); 103 | const weeklyNotesFolder = vault.getAbstractFileByPath( 104 | normalizePath(folder) 105 | ) as TFolder; 106 | 107 | if (!weeklyNotesFolder) { 108 | throw new WeeklyNotesFolderMissingError( 109 | "Failed to find weekly notes folder" 110 | ); 111 | } 112 | 113 | Vault.recurseChildren(weeklyNotesFolder, (note) => { 114 | if (note instanceof TFile) { 115 | const date = getDateFromFile(note, "week"); 116 | if (date) { 117 | const dateString = getDateUID(date, "week"); 118 | weeklyNotes[dateString] = note; 119 | } 120 | } 121 | }); 122 | 123 | return weeklyNotes; 124 | } 125 | -------------------------------------------------------------------------------- /src/yearly.ts: -------------------------------------------------------------------------------- 1 | import type { Moment } from "moment"; 2 | import { normalizePath, Notice, TFile, TFolder, Vault } from "obsidian"; 3 | 4 | import { appHasYearlyNotesPluginLoaded } from "./index"; 5 | import { getDateFromFile, getDateUID } from "./parse"; 6 | import { getYearlyNoteSettings } from "./settings"; 7 | import { getNotePath, getTemplateInfo } from "./vault"; 8 | 9 | export class YearlyNotesFolderMissingError extends Error {} 10 | 11 | /** 12 | * This function mimics the behavior of the daily-notes plugin 13 | * so it will replace {{date}}, {{title}}, and {{time}} with the 14 | * formatted timestamp. 15 | * 16 | * Note: it has an added bonus that it's not 'today' specific. 17 | */ 18 | export async function createYearlyNote(date: Moment): Promise { 19 | const { vault } = window.app; 20 | const { template, format, folder } = getYearlyNoteSettings(); 21 | const [templateContents, IFoldInfo] = await getTemplateInfo(template); 22 | const filename = date.format(format); 23 | const normalizedPath = await getNotePath(folder, filename); 24 | 25 | try { 26 | const createdFile = await vault.create( 27 | normalizedPath, 28 | templateContents 29 | .replace( 30 | /{{\s*(date|time)\s*(([+-]\d+)([yqmwdhs]))?\s*(:.+?)?}}/gi, 31 | (_, _timeOrDate, calc, timeDelta, unit, momentFormat) => { 32 | const now = window.moment(); 33 | const currentDate = date.clone().set({ 34 | hour: now.get("hour"), 35 | minute: now.get("minute"), 36 | second: now.get("second"), 37 | }); 38 | if (calc) { 39 | currentDate.add(parseInt(timeDelta, 10), unit); 40 | } 41 | 42 | if (momentFormat) { 43 | return currentDate.format(momentFormat.substring(1).trim()); 44 | } 45 | return currentDate.format(format); 46 | } 47 | ) 48 | .replace(/{{\s*date\s*}}/gi, filename) 49 | .replace(/{{\s*time\s*}}/gi, window.moment().format("HH:mm")) 50 | .replace(/{{\s*title\s*}}/gi, filename) 51 | ); 52 | 53 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 54 | (window.app as any).foldManager.save(createdFile, IFoldInfo); 55 | 56 | return createdFile; 57 | } catch (err) { 58 | console.error(`Failed to create file: '${normalizedPath}'`, err); 59 | new Notice("Unable to create new file."); 60 | } 61 | } 62 | 63 | export function getYearlyNote( 64 | date: Moment, 65 | yearlyNotes: Record 66 | ): TFile { 67 | return yearlyNotes[getDateUID(date, "year")] ?? null; 68 | } 69 | 70 | export function getAllYearlyNotes(): Record { 71 | const yearlyNotes: Record = {}; 72 | 73 | if (!appHasYearlyNotesPluginLoaded()) { 74 | return yearlyNotes; 75 | } 76 | const { vault } = window.app; 77 | const { folder } = getYearlyNoteSettings(); 78 | 79 | const yearlyNotesFolder = vault.getAbstractFileByPath( 80 | normalizePath(folder) 81 | ) as TFolder; 82 | 83 | if (!yearlyNotesFolder) { 84 | throw new YearlyNotesFolderMissingError( 85 | "Failed to find yearly notes folder" 86 | ); 87 | } 88 | 89 | Vault.recurseChildren(yearlyNotesFolder, (note) => { 90 | if (note instanceof TFile) { 91 | const date = getDateFromFile(note, "year"); 92 | if (date) { 93 | const dateString = getDateUID(date, "year"); 94 | yearlyNotes[dateString] = note; 95 | } 96 | } 97 | }); 98 | 99 | return yearlyNotes; 100 | } 101 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "baseUrl": ".", 5 | "inlineSourceMap": true, 6 | "inlineSources": true, 7 | "module": "ESNext", 8 | "target": "esnext", 9 | "moduleResolution": "node", 10 | "lib": ["dom", "es2020"], 11 | "types": ["node", "jest"] 12 | }, 13 | "include": ["**/*.ts"] 14 | } 15 | --------------------------------------------------------------------------------