├── .npmrc ├── makefile ├── assets ├── demoPlugin.png └── demoSettings.png ├── .github ├── dependabot.yml └── workflows │ ├── dependabot-pr.yml │ ├── release.yml │ └── ci.yml ├── .editorconfig ├── .gitignore ├── manifest.json ├── scripts ├── linter.sh ├── release.sh └── validate.sh ├── tsconfig.json ├── versions.json ├── tests ├── birthday.spec.ts ├── person.spec.ts └── dateFormatter.spec.ts ├── biome.json ├── src ├── modals │ ├── SearchPersonModal.ts │ └── PersonModal.ts ├── person.ts ├── views │ ├── birthdayTrackerView.ts │ └── yearOverviewView.ts ├── birthday.ts ├── settings.ts ├── dateFormatter.ts └── main.ts ├── package.json ├── version-bump.mjs ├── LICENSE ├── styles.css ├── esbuild.config.mjs ├── README.md └── bun.lock /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | release: 2 | scripts/validate.sh 3 | scripts/linter.sh 4 | scripts/release.sh -------------------------------------------------------------------------------- /assets/demoPlugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raboro/Obsidian-Birthday-Tracker-Plugin/HEAD/assets/demoPlugin.png -------------------------------------------------------------------------------- /assets/demoSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Raboro/Obsidian-Birthday-Tracker-Plugin/HEAD/assets/demoSettings.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "bun" 4 | directory: '/' 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | *.iml 3 | .idea 4 | 5 | # npm 6 | node_modules 7 | package-lock.json 8 | 9 | # build 10 | main.js 11 | *.js.map 12 | stats.html 13 | hot-reload.bat 14 | data.json 15 | lib 16 | 17 | #VSCode 18 | .vscode 19 | yarn.lock 20 | .DS_Store 21 | .lock 22 | .lock -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "birthday-tracker", 3 | "name": "Birthday-Tracker", 4 | "version": "1.2.2", 5 | "minAppVersion": "0.15.0", 6 | "description": "Keep track of all birthdays of your family and friends.", 7 | "author": "Marius Wörfel", 8 | "authorUrl": "https://github.com/Raboro", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /scripts/linter.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "\ntry to fix all issues if present" 4 | 5 | bun run biome:write 6 | 7 | git commit -am "refactor(GHActionbot): :art: formatted & linting & organized imports with biome" 8 | 9 | echo -e "\ncheck all issues fixed" 10 | 11 | bun run biome:ci 12 | biome_exit_code=$? 13 | 14 | if [ $biome_exit_code -ne 0 ]; then 15 | echo "Biome errors still exist. Exiting." 16 | exit 1 17 | fi -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": ["DOM", "ES5", "ES6", "ES7"] 15 | }, 16 | "include": ["**/*.ts"] 17 | } 18 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.15.0", 3 | "1.0.1": "0.15.0", 4 | "1.0.2": "0.15.0", 5 | "1.0.3": "0.15.0", 6 | "1.0.4": "0.15.0", 7 | "1.0.5": "0.15.0", 8 | "1.0.6": "0.15.0", 9 | "1.0.7": "0.15.0", 10 | "1.0.8": "0.15.0", 11 | "1.0.9": "0.15.0", 12 | "1.1.0": "0.15.0", 13 | "1.1.1": "0.15.0", 14 | "1.1.2": "0.15.0", 15 | "1.1.3": "0.15.0", 16 | "1.1.4": "0.15.0", 17 | "1.1.5": "0.15.0", 18 | "1.1.6": "0.15.0", 19 | "1.1.7": "0.15.0", 20 | "1.1.8": "0.15.0", 21 | "1.1.9": "0.15.0", 22 | "1.2.0": "0.15.0", 23 | "1.2.1": "0.15.0", 24 | "1.2.2": "0.15.0" 25 | } 26 | -------------------------------------------------------------------------------- /tests/birthday.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import Birthday from 'src/birthday'; 3 | import { DefaultDateFormatter } from 'src/dateFormatter'; 4 | 5 | describe('Birthday', () => { 6 | test('toString() should be same as parsed str', () => { 7 | const dateAsString = '20/08/2000'; 8 | const birthday = new Birthday( 9 | '20/08/2000', 10 | // biome-ignore lint/style/noNonNullAssertion: is valid 11 | DefaultDateFormatter.createFormat('DD/MM/YYYY')!, 12 | ); 13 | expect(birthday.toString()).toEqual(dateAsString); 14 | expect(birthday.getAge()).toBeGreaterThan(0); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", 3 | "assist": { "actions": { "source": { "organizeImports": "on" } } }, 4 | "linter": { 5 | "enabled": true, 6 | "rules": { 7 | "recommended": true 8 | } 9 | }, 10 | "formatter": { 11 | "indentStyle": "space" 12 | }, 13 | "javascript": { 14 | "formatter": { 15 | "quoteStyle": "single", 16 | "indentStyle": "space", 17 | "semicolons": "always" 18 | } 19 | }, 20 | "files": { 21 | "includes": [ 22 | "**", 23 | "!**/node_modules/", 24 | "!**/.idea", 25 | "!**/assets", 26 | "!**/bun.lockb", 27 | "!**/main.js" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/modals/SearchPersonModal.ts: -------------------------------------------------------------------------------- 1 | import { type App, FuzzySuggestModal } from 'obsidian'; 2 | import type { PersonDTO } from '../person'; 3 | import PersonModal from './PersonModal'; 4 | 5 | export default class SearchPersonModal extends FuzzySuggestModal { 6 | private readonly persons: PersonDTO[]; 7 | 8 | constructor(app: App, persons: PersonDTO[]) { 9 | super(app); 10 | this.persons = persons; 11 | } 12 | 13 | getItems(): PersonDTO[] { 14 | return this.persons; 15 | } 16 | 17 | getItemText(item: PersonDTO): string { 18 | return item.name; 19 | } 20 | 21 | onChooseItem(item: PersonDTO, _evt: MouseEvent | KeyboardEvent): void { 22 | new PersonModal(this.app, item).open(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-pr.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot CI 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "package.json" 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | ci: 14 | name: "Dependabot CI" 15 | if: github.actor == 'dependabot[bot]' 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | ref: ${{ github.event.pull_request.head.ref }} 23 | 24 | - name: Setup bun 25 | uses: oven-sh/setup-bun@v2 26 | with: 27 | bun-version: latest 28 | 29 | - name: Update lock file 30 | run: | 31 | bun install 32 | 33 | - name: Test 34 | run: bun test 35 | 36 | - name: Build 37 | run: bun run build 38 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo -e "\nupdate version in manifest.json/package.json and version.json" 4 | 5 | bun run version 6 | 7 | current_version=$(node -pe "require('./manifest.json').version") 8 | package_version=$(node -pe "require('./package.json').version") 9 | 10 | echo "check version" 11 | 12 | if [ "$current_version" != "$package_version" ]; then 13 | echo "Version mismatch between manifest.json and package.json. Exiting." 14 | exit 1 15 | fi 16 | 17 | if git rev-parse "$current_version" >/dev/null 2>&1; then 18 | echo "Tag $current_version already exists. Skipping push." 19 | exit 0 20 | fi 21 | 22 | echo "commit and push all changes with version" 23 | 24 | git commit -am "chore(Release): :bookmark: to version $current_version" 25 | 26 | git tag -a $current_version -m "$current_version" 27 | 28 | git push 29 | 30 | git push origin $current_version -------------------------------------------------------------------------------- /src/modals/PersonModal.ts: -------------------------------------------------------------------------------- 1 | import { type App, Modal } from 'obsidian'; 2 | import type { PersonDTO } from '../person'; 3 | 4 | export default class PersonModal extends Modal { 5 | private person: PersonDTO; 6 | 7 | constructor(app: App, person: PersonDTO) { 8 | super(app); 9 | this.person = person; 10 | } 11 | 12 | onOpen(): void { 13 | const { contentEl } = this; 14 | const div: HTMLDivElement = contentEl.createDiv({ 15 | cls: 'personContainer smallerScale', 16 | }); 17 | div.createEl('p', { 18 | text: `Name: ${this.person.name} (${this.person.age})`, 19 | }); 20 | div.createEl('p', { 21 | text: `Days next birthday: ${this.person.daysTillNextBirthday}`, 22 | }); 23 | div.createEl('p', { text: `Birthday: ${this.person.birthday}` }); 24 | } 25 | 26 | onClose(): void { 27 | const { contentEl } = this; 28 | contentEl.empty(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "birthday-tracker", 3 | "version": "1.2.2", 4 | "description": "Keep track of all birthdays of your family and friends.", 5 | "main": "src/main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "biome:write": "bunx biome check --write", 11 | "biome:ci": "bunx biome ci", 12 | "biome:migrate": "bunx biome migrate --write" 13 | }, 14 | "keywords": [], 15 | "author": "Marius Wörfel", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@biomejs/biome": "^2.3.2", 19 | "@types/bun": "^1.3.1", 20 | "@types/node": "^24.9.2", 21 | "builtin-modules": "5.0.0", 22 | "esbuild": "0.25.11", 23 | "obsidian": "1.10.0", 24 | "tslib": "2.8.1", 25 | "typescript": "5.9.3" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/person.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import Birthday from 'src/birthday'; 3 | import { DefaultDateFormatter } from 'src/dateFormatter'; 4 | import Person from 'src/person'; 5 | 6 | describe('Person', () => { 7 | test('creation of person with birthday today should have birthday today', () => { 8 | const today = new Date(); 9 | const day = today.getDate(); 10 | const month = today.getMonth() + 1; // needed offset cause getMonth() returns wrong month => WHYY?? 11 | const year = today.getFullYear(); 12 | const todayAsString = `${(day < 10 ? '0' : '') + day}/${(month < 10 ? '0' : '') + month}/${year}`; 13 | // biome-ignore lint/style/noNonNullAssertion: is valid 14 | const formatter = DefaultDateFormatter.createFormat('DD/MM/YYYY')!; 15 | const birthday = new Birthday(todayAsString, formatter); 16 | const person = new Person('selina', birthday); 17 | expect(person.hasBirthdayToday()).toBeTrue(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/person.ts: -------------------------------------------------------------------------------- 1 | import type Birthday from './birthday'; 2 | 3 | export default class Person { 4 | private readonly name: string; 5 | private readonly birthday: Birthday; 6 | 7 | constructor(name: string, birthday: Birthday) { 8 | this.name = name; 9 | this.birthday = birthday; 10 | } 11 | 12 | compareTo(other: Person): number { 13 | return this.birthday.compareTo(other.birthday); 14 | } 15 | 16 | hasBirthdayToday(): boolean { 17 | return this.birthday.hasBirthdayToday(); 18 | } 19 | 20 | toDTO(): PersonDTO { 21 | return new PersonDTO( 22 | this.name, 23 | this.birthday.toString(), 24 | this.birthday.getDaysTillNextBirthday(), 25 | this.birthday.getAge(), 26 | this.birthday.getMonth(), 27 | ); 28 | } 29 | } 30 | 31 | export class PersonDTO { 32 | constructor( 33 | readonly name: string, 34 | readonly birthday: string, 35 | readonly daysTillNextBirthday: number, 36 | readonly age: number, 37 | readonly month: number, 38 | ) {} 39 | } 40 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import { readFileSync, writeFileSync } from 'node:fs'; 3 | 4 | const highestTag = execSync('git describe --tags --abbrev=0').toString().trim(); 5 | 6 | const incrementVersion = (version) => { 7 | const [major, minor, patch] = version.split('.').map(Number); 8 | if (patch < 9) { 9 | return `${major}.${minor}.${patch + 1}`; 10 | } 11 | if (minor < 9) { 12 | return `${major}.${minor + 1}.0`; 13 | } 14 | return `${major + 1}.0.0`; 15 | }; 16 | 17 | function updateVersion(name) { 18 | const file = JSON.parse(readFileSync(name, 'utf-8')); 19 | file.version = targetVersion; 20 | writeFileSync(name, JSON.stringify(file, null, '\t')); 21 | } 22 | 23 | const targetVersion = incrementVersion(highestTag); 24 | updateVersion('manifest.json'); 25 | const { minAppVersion } = JSON.parse(readFileSync('manifest.json', 'utf8')); 26 | const versions = JSON.parse(readFileSync('versions.json', 'utf8')); 27 | versions[targetVersion] = minAppVersion; 28 | writeFileSync('versions.json', JSON.stringify(versions, null, '\t')); 29 | updateVersion('package.json'); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marius Wörfel 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 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .personsFlexboxContainer { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 2vh; 5 | } 6 | 7 | .personContainer { 8 | border: 3px solid #3d3d3c; 9 | border-radius: 10px; 10 | transition-duration: 0.5s; 11 | box-shadow: rgb(0 0 0 / 10%) 0 4px 12px; 12 | scale: 0.95; 13 | } 14 | 15 | .personContainer > p { 16 | text-align: center; 17 | } 18 | 19 | .personContainer:hover { 20 | scale: 0.98; 21 | animation-duration: 0.5s; 22 | box-shadow: rgb(0 0 0 / 20%) 0 8px 24px; 23 | } 24 | 25 | .smallerScale:hover { 26 | scale: 0.96; 27 | } 28 | 29 | .yearContainer { 30 | display: grid; 31 | grid-template-columns: 25% 25% 25% 25%; 32 | border: 3px solid #3d3d3c; 33 | border-radius: 10px; 34 | margin-top: 5vh; 35 | height: 86%; 36 | } 37 | 38 | .monthContainer { 39 | border: 3px solid #3d3d3c; 40 | } 41 | 42 | .monthName { 43 | text-align: center; 44 | } 45 | 46 | .personsYearViewContainer { 47 | border: 3px solid #3d3d3c; 48 | border-radius: 10px; 49 | margin: 2vh 2vw; 50 | height: 16vh; 51 | overflow-y: scroll; 52 | } 53 | 54 | .personsYearViewContainer > p { 55 | text-align: center; 56 | cursor: pointer; 57 | } 58 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import builtins from 'builtin-modules'; 3 | import esbuild from 'esbuild'; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === 'production'; 12 | 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ['src/main.ts'], 18 | bundle: true, 19 | external: [ 20 | 'obsidian', 21 | 'electron', 22 | '@codemirror/autocomplete', 23 | '@codemirror/collab', 24 | '@codemirror/commands', 25 | '@codemirror/language', 26 | '@codemirror/lint', 27 | '@codemirror/search', 28 | '@codemirror/state', 29 | '@codemirror/view', 30 | '@lezer/common', 31 | '@lezer/highlight', 32 | '@lezer/lr', 33 | ...builtins, 34 | ], 35 | format: 'cjs', 36 | target: 'es2018', 37 | logLevel: 'info', 38 | sourcemap: prod ? false : 'inline', 39 | treeShaking: true, 40 | outfile: 'main.js', 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Obsidian plugin 2 | 3 | on: 4 | workflow_run: # only release if CI passes 5 | workflows: 6 | - "CI" 7 | types: 8 | - completed 9 | push: 10 | tags: 11 | - "*.*.*" 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Setup bun 21 | uses: oven-sh/setup-bun@v2 22 | with: 23 | bun-version: latest 24 | 25 | - name: Build plugin 26 | run: | 27 | bun install 28 | bun run build 29 | 30 | - name: Create release 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | run: | 34 | tag="${GITHUB_REF#refs/tags/}" 35 | 36 | notes_file="release-notes.md" 37 | notes_option="" 38 | if [ -f "$notes_file" ]; then 39 | notes_option="--notes-file=$notes_file" 40 | fi 41 | 42 | gh release create "$tag" \ 43 | --title="$tag" \ 44 | $notes_option \ 45 | main.js manifest.json styles.css 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "dependabot/**" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | ci: 11 | name: CI 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Setup bun 17 | uses: oven-sh/setup-bun@v2 18 | with: 19 | bun-version: latest 20 | 21 | - name: Install dependencies 22 | run: bun install 23 | 24 | - name: Fix biome issues 25 | run: bun run biome:write 26 | 27 | - name: Commit changes 28 | run: | 29 | git config user.name github-actions[bot] 30 | git config user.email github-actions[bot]@users.noreply.github.com 31 | git commit -am "refactor(GHActionbot): :art: formatted & linting & organized imports with biome" || true 32 | 33 | - name: Push changes 34 | uses: ad-m/github-push-action@master 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | branch: ${{ github.ref }} 38 | 39 | - name: Check if all issues are fixed with biome 40 | run: bun run biome:ci 41 | 42 | - name: Test 43 | run: bun test 44 | 45 | - name: Build 46 | run: bun run build 47 | -------------------------------------------------------------------------------- /src/views/birthdayTrackerView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView } from 'obsidian'; 2 | import type Person from '../person'; 3 | import type { PersonDTO } from '../person'; 4 | 5 | export const BIRTHDAY_TRACKER_VIEW_TYPE = 'Birthday-Tracker'; 6 | 7 | export class BirthdayTrackerView extends ItemView { 8 | private container: HTMLDivElement; 9 | icon = 'cake'; 10 | 11 | getViewType(): string { 12 | return BIRTHDAY_TRACKER_VIEW_TYPE; 13 | } 14 | 15 | getDisplayText(): string { 16 | return BIRTHDAY_TRACKER_VIEW_TYPE; 17 | } 18 | 19 | async onOpen() { 20 | const { contentEl } = this; 21 | contentEl.createEl('h1', { text: 'Birthday Tracker' }); 22 | this.container = contentEl.createDiv({ cls: 'personsFlexboxContainer' }); 23 | } 24 | 25 | displayPersons(persons: Array): void { 26 | while (this.container.firstChild) { 27 | this.container.removeChild(this.container.lastChild as Node); 28 | } 29 | // biome-ignore lint: performance issue to use for..of not relevant 30 | persons.forEach((person) => this.displayPerson(person.toDTO())); 31 | } 32 | 33 | displayPerson(person: PersonDTO): void { 34 | const div: HTMLDivElement = this.container.createDiv({ 35 | cls: 'personContainer', 36 | }); 37 | div.createEl('p', { 38 | text: `Name: ${person.name} (${person.age})`, 39 | }); 40 | div.createEl('p', { 41 | text: `Days next birthday: ${person.daysTillNextBirthday}`, 42 | }); 43 | div.createEl('p', { text: `Birthday: ${person.birthday}` }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /scripts/validate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ends_not_with_period() { 4 | local string="$1" 5 | if [ "${string: -1}" == "." ]; then 6 | return 0 7 | else 8 | return 1 9 | fi 10 | } 11 | 12 | package_name=$(node -pe "require('./package.json').name") 13 | manifest_id=$(node -pe "require('./manifest.json').id") 14 | package_description=$(node -pe "require('./package.json').description") 15 | manifest_description=$(node -pe "require('./manifest.json').description") 16 | 17 | echo -e "\nvalidate that package.json and manifest.json names are equal" 18 | 19 | if [ "$package_name" != "$manifest_id" ]; then 20 | echo "Name mismatch between package.json and manifest.json. Exiting." 21 | exit 1 22 | fi 23 | 24 | echo "validate that package.json and manifest.json descriptions are equal" 25 | 26 | if [ "$package_description" != "$manifest_description" ]; then 27 | echo "Description mismatch between package.json and manifest.json. Exiting." 28 | exit 1 29 | fi 30 | 31 | echo "validate that names and descriptions are not empty" 32 | 33 | if [ -z "$package_name" ] || [ -z "$package_description" ]; then 34 | echo "Name or description is empty in package.json. Exiting." 35 | exit 1 36 | fi 37 | 38 | echo "validate that discription not contains'obsidian' and 'plugin'" 39 | 40 | if [ "$package_description" == "obsidian" ] || [ "$package_description" == "plugin" ]; then 41 | echo "Description in package.json contains obsidian or plugin, which is bad" 42 | exit 1 43 | fi 44 | 45 | echo -e "validate that descripltion ends with '.'\n" 46 | 47 | if ! ends_not_with_period "$package_description"; then 48 | echo "Description not ending with '.'" 49 | exit 1 50 | fi -------------------------------------------------------------------------------- /tests/dateFormatter.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { DefaultDateFormatter } from 'src/dateFormatter'; 3 | 4 | describe('DateFormatter', () => { 5 | test('parseDate should parse default correctly', () => { 6 | // biome-ignore lint/style/noNonNullAssertion: is valid 7 | const formatter = DefaultDateFormatter.createFormat('DD/MM/YYYY')!; 8 | const date = formatter.parseToDate('20/08/2000'); 9 | expect(date.toISOString()).toStartWith('2000-08-20'); 10 | }); 11 | 12 | test('parseDate should parse with different format correctly', () => { 13 | // biome-ignore lint/style/noNonNullAssertion: is valid 14 | const formatter = DefaultDateFormatter.createFormat('MM/DD/YYYY')!; 15 | const date = formatter.parseToDate('08/20/2000'); 16 | expect(date.toISOString()).toStartWith('2000-08-20'); 17 | }); 18 | 19 | test('creation should create valid formatter', () => { 20 | expect(DefaultDateFormatter.createFormat('MM/DD/YYYY')).not.toBe(undefined); 21 | expect(DefaultDateFormatter.createFormat('DD/MM/YYYY')).not.toBe(undefined); 22 | expect(DefaultDateFormatter.createFormat('YYYY/MM/DD')).not.toBe(undefined); 23 | expect(DefaultDateFormatter.createFormat('YYYY/DD/MM')).not.toBe(undefined); 24 | expect(DefaultDateFormatter.createFormat('DD-MM-YYYY')).not.toBe(undefined); 25 | }); 26 | 27 | test('creation should not create invalid formatter', () => { 28 | expect(DefaultDateFormatter.createFormat('MX/DD/YYYY')).toBeUndefined(); 29 | expect(DefaultDateFormatter.createFormat('DL/MM/YYYY')).toBeUndefined(); 30 | expect(DefaultDateFormatter.createFormat('YYY/MM/DD')).toBeUndefined(); 31 | expect(DefaultDateFormatter.createFormat('Y1EY/DD/MM')).toBeUndefined(); 32 | expect(DefaultDateFormatter.createFormat('D9-MM-YYYY')).toBeUndefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/views/yearOverviewView.ts: -------------------------------------------------------------------------------- 1 | import { ItemView } from 'obsidian'; 2 | import PersonModal from 'src/modals/PersonModal'; 3 | import type { PersonDTO } from 'src/person'; 4 | 5 | export const BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE = 6 | 'Birthday-Tracker-Year-Overview'; 7 | 8 | const MONTHS = [ 9 | 'January', 10 | 'February', 11 | 'March', 12 | 'April', 13 | 'May', 14 | 'June', 15 | 'July', 16 | 'August', 17 | 'September', 18 | 'October', 19 | 'November', 20 | 'December', 21 | ]; 22 | 23 | export class YearOverviewView extends ItemView { 24 | icon = 'calendar-days'; 25 | persons: PersonDTO[] = []; 26 | 27 | getViewType(): string { 28 | return BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE; 29 | } 30 | 31 | getDisplayText(): string { 32 | return BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE; 33 | } 34 | 35 | async onOpen() { 36 | const { contentEl } = this; 37 | contentEl.createEl('h1', { text: 'Birthday Tracker - Year Overview' }); 38 | const container = contentEl.createDiv({ cls: 'yearContainer' }); 39 | for (let i = 0; i < 12; i++) { 40 | const month = container.createDiv({ cls: 'monthContainer' }); 41 | month.createEl('h4', { text: MONTHS[i], cls: 'monthName' }); 42 | const personContainer = month.createDiv({ 43 | cls: 'personsYearViewContainer', 44 | }); 45 | if (this.persons.length === 0) { 46 | continue; 47 | } 48 | for (const person of this.persons) { 49 | if (person.month === i) { 50 | this.createPerson(person, personContainer); 51 | } 52 | } 53 | } 54 | } 55 | 56 | createPerson = (person: PersonDTO, personContainer: HTMLDivElement) => { 57 | const para = personContainer.createEl('p', { text: person.name }); 58 | para.onclick = () => new PersonModal(this.app, person).open(); 59 | }; 60 | 61 | async updatePersons(persons: PersonDTO[]) { 62 | this.persons = persons; 63 | const { contentEl } = this; 64 | contentEl.empty(); 65 | await this.onOpen(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian-Birthday-Tracker-Plugin 2 | 3 | [![Latest Release](https://img.shields.io/github/v/release/Raboro/Obsidian-Birthday-Tracker-Plugin?include_prereleases&sort=semver&style=for-the-badge)](https://github.com/Raboro/Obsidian-Birthday-Tracker-Plugin/releases/latest) [![Total Downloads](https://img.shields.io/github/downloads/Raboro/Obsidian-Birthday-Tracker-Plugin/total?style=for-the-badge)](https://github.com/Raboro/Obsidian-Birthday-Tracker-Plugin/releases/latest) 4 | [![CI Workflow Status](https://img.shields.io/github/actions/workflow/status/Raboro/Obsidian-Birthday-Tracker-Plugin/ci.yml?branch=master&logo=github&style=for-the-badge)](https://github.com/Raboro/Obsidian-Birthday-Tracker-Plugin/actions/workflows/ci.yml) 5 | 6 | This plugin allows you to keep track of all birthdays of your family and friends. 7 | 8 | ![Demo image](assets/demoPlugin.png) 9 | 10 | ## Using 11 | You need a file containing all the birthday data. 12 | To find this file you need to add the location in the settings. After that you can add your persons with: 13 | ``name=; birthday=`` \ 14 | For example: ``name=Peter Rudolf; birthday=17/08/2033``. 15 | You need the add all those persons line after line: 16 | ``` 17 | name=Peter Rudolf; birthday=17/08/2033 18 | name=Hans Ap; birthday=01/05/2004 19 | name=Mats Mattis; birthday=21/03/1999 20 | ``` 21 | 22 | After that you can click on the ribbon icon or use the command to trigger the plugin. \ 23 | You will receive a notice for all persons who have birthday today and get in a separate view an overview over all persons sorted by their next birthday. 24 | 25 | ### Year View 26 | You can also use the **Year View** to get an overview over all birthdays. You can also click on the names to get more infomation of this person. 27 | 28 | ![image](https://github.com/Raboro/Obsidian-Birthday-Tracker-Plugin/assets/88288557/9b2a1915-3e2a-42e7-ba94-e6140b484ff4) 29 | 30 | 31 | ## Settings 32 | You can set a date formatting. The default is: ``DD/MM/YYYY``. This is needed to collect your dates and display them. Also you can set the location of the file containing the birthday data. This must include `.md` as postfix. The default value is: `birthdayNode.md` 33 | 34 | ![Demo settings](assets/demoSettings.png) 35 | -------------------------------------------------------------------------------- /src/birthday.ts: -------------------------------------------------------------------------------- 1 | import type { DateFormatter } from './dateFormatter'; 2 | 3 | export default class Birthday { 4 | private readonly birthdayAsString: string; 5 | private readonly age: number; 6 | private readonly daysTillNextBirthday: number; 7 | private readonly date: Date; 8 | 9 | constructor(birthdayAsString: string, dateFormatter: DateFormatter) { 10 | this.birthdayAsString = birthdayAsString; 11 | this.date = dateFormatter.parseToDate(birthdayAsString); 12 | this.age = this.determineAge(); 13 | this.daysTillNextBirthday = this.calcDaysTillNextBirthday(); 14 | } 15 | 16 | private determineAge(): number { 17 | let age = new Date().getFullYear() - this.date.getFullYear(); 18 | return this.hadBirthdayThisYear() ? age : --age; 19 | } 20 | 21 | private hadBirthdayThisYear(): boolean { 22 | const monthPassed = new Date().getMonth() > this.date.getMonth(); 23 | const daysPassed = 24 | new Date().getMonth() === this.date.getMonth() && 25 | new Date().getDate() >= this.date.getDate(); //getDay returns Day of the week, getDate the Day number 26 | return monthPassed || daysPassed; 27 | } 28 | 29 | private calcDaysTillNextBirthday(): number { 30 | const days = this.calcDays(new Date().getFullYear()); 31 | if (-days === 0) { 32 | return 0; 33 | } 34 | return days > 0 ? days : this.calcDays(new Date().getFullYear() + 1); 35 | } 36 | 37 | private calcDays(newYear: number): number { 38 | const dateCurrentYear: Date = new Date(this.date); 39 | dateCurrentYear.setFullYear(newYear); 40 | dateCurrentYear.setHours(0, 0, 0, 0); 41 | 42 | const today = new Date(); 43 | today.setHours(0, 0, 0, 0); 44 | 45 | const timeDifference = today.getTime() - dateCurrentYear.getTime(); 46 | return -Math.ceil(timeDifference / (1000 * 60 * 60 * 24)); 47 | } 48 | 49 | compareTo(other: Birthday): number { 50 | return this.daysTillNextBirthday - other.daysTillNextBirthday; 51 | } 52 | 53 | hasBirthdayToday(): boolean { 54 | return this.daysTillNextBirthday === 0; 55 | } 56 | 57 | getAge(): number { 58 | return this.age; 59 | } 60 | 61 | getDaysTillNextBirthday(): number { 62 | return this.daysTillNextBirthday; 63 | } 64 | 65 | getMonth(): number { 66 | return this.date.getMonth(); 67 | } 68 | 69 | toString(): string { 70 | return this.birthdayAsString; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import { type App, Notice, PluginSettingTab, Setting } from 'obsidian'; 2 | import { DefaultDateFormatter } from './dateFormatter'; 3 | import type BirthdayTrackerPlugin from './main'; 4 | 5 | export interface BirthdayTrackerSettings { 6 | dateFormatting: string; 7 | birthdayNodeLocation: string; 8 | automaticallyOpenBirthdayViewOnStart: boolean; 9 | } 10 | 11 | export const DEFAULT_SETTINGS: BirthdayTrackerSettings = { 12 | dateFormatting: 'DD/MM/YYYY', 13 | birthdayNodeLocation: 'birthdayNode.md', 14 | automaticallyOpenBirthdayViewOnStart: true, 15 | }; 16 | 17 | export class BirthdayTrackerSettingTab extends PluginSettingTab { 18 | plugin: BirthdayTrackerPlugin; 19 | 20 | constructor(app: App, plugin: BirthdayTrackerPlugin) { 21 | super(app, plugin); 22 | this.plugin = plugin; 23 | } 24 | 25 | display(): void { 26 | this.containerEl.empty(); 27 | this.dateFormattingSettings(); 28 | this.birthdayNodeLocationSettings(); 29 | this.automaticallyOpenBirthdayViewOnStartSettings(); 30 | } 31 | 32 | dateFormattingSettings(): Setting { 33 | return new Setting(this.containerEl) 34 | .setName('Date formatting') 35 | .setDesc('Format your dates will be displayed and collected') 36 | .addText((text) => 37 | text 38 | .setPlaceholder('Enter your format') 39 | .setValue(this.plugin.settings.dateFormatting) 40 | .onChange(async (v) => await this.dateFormattingSettingsOnChange(v)), 41 | ); 42 | } 43 | 44 | dateFormattingSettingsOnChange = async (value: string) => { 45 | let noticeMessage = 'Wrong date formatting!!'; 46 | const dateFormatter = DefaultDateFormatter.createFormat(value); 47 | if (dateFormatter) { 48 | this.plugin.settings.dateFormatting = dateFormatter.format; 49 | await this.plugin.saveSettings(); 50 | noticeMessage = 'Valid date formatting'; 51 | } 52 | new Notice(noticeMessage); 53 | }; 54 | 55 | birthdayNodeLocationSettings(): Setting { 56 | return new Setting(this.containerEl) 57 | .setName('Birthday node location') 58 | .setDesc( 59 | 'Location of your Node containing the birthday data with .md as postfix', 60 | ) 61 | .addTextArea((text) => 62 | text 63 | .setPlaceholder('Enter the node location') 64 | .setValue(this.plugin.settings.birthdayNodeLocation) 65 | .onChange(async (value) => { 66 | this.plugin.settings.birthdayNodeLocation = value; 67 | await this.plugin.saveSettings(); 68 | }), 69 | ); 70 | } 71 | 72 | automaticallyOpenBirthdayViewOnStartSettings(): Setting { 73 | return new Setting(this.containerEl) 74 | .setName('Automatically open birthday view on startup') 75 | .setDesc( 76 | 'If enabled, the birthday view is automatically opened in the right leaf when Obsidian starts', 77 | ) 78 | .addToggle((toggle) => { 79 | toggle 80 | .setValue(this.plugin.settings.automaticallyOpenBirthdayViewOnStart) 81 | .onChange(async (value) => { 82 | this.plugin.settings.automaticallyOpenBirthdayViewOnStart = value; 83 | await this.plugin.saveSettings(); 84 | }); 85 | }); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/dateFormatter.ts: -------------------------------------------------------------------------------- 1 | export interface DateFormatter { 2 | readonly format: string; 3 | parseToDate(dateAsString: string): Date; 4 | } 5 | 6 | interface DateComponentRange { 7 | start: number; 8 | end: number; 9 | offset?: number; 10 | } 11 | 12 | export class DefaultDateFormatter implements DateFormatter { 13 | private static readonly DAY_IDENTIFIER = 'DD'; 14 | private static readonly MONTH_IDENTIFIER = 'MM'; 15 | private static readonly YEAR_IDENTIFIER = 'YYYY'; 16 | 17 | private readonly dayIndex: number; 18 | private readonly monthIndex: number; 19 | private readonly yearIndex: number; 20 | readonly format: string; 21 | 22 | private constructor(format: string) { 23 | this.format = format; 24 | this.dayIndex = format.search(DefaultDateFormatter.DAY_IDENTIFIER); 25 | this.monthIndex = format.search(DefaultDateFormatter.MONTH_IDENTIFIER); 26 | this.yearIndex = format.search(DefaultDateFormatter.YEAR_IDENTIFIER); 27 | } 28 | 29 | static createFormat(format: string): DateFormatter | undefined { 30 | return DefaultDateFormatter.isValidFormat(format) 31 | ? new DefaultDateFormatter(format) 32 | : undefined; 33 | } 34 | 35 | private static isValidFormat(format: string): boolean { 36 | const containsDay = DefaultDateFormatter.formatContains( 37 | DefaultDateFormatter.DAY_IDENTIFIER, 38 | format, 39 | ); 40 | const containsMonth = DefaultDateFormatter.formatContains( 41 | DefaultDateFormatter.MONTH_IDENTIFIER, 42 | format, 43 | ); 44 | const containsYear = DefaultDateFormatter.formatContains( 45 | DefaultDateFormatter.YEAR_IDENTIFIER, 46 | format, 47 | ); 48 | return ( 49 | containsDay && 50 | containsMonth && 51 | containsYear && 52 | !DefaultDateFormatter.containsInvalidChars(format) 53 | ); 54 | } 55 | 56 | private static formatContains(subStr: string, format: string): boolean { 57 | return format.includes(subStr) || format.includes(subStr.toLowerCase()); 58 | } 59 | 60 | private static containsInvalidChars(format: string): boolean { 61 | const invalidChars: string[] = [ 62 | 'A', 63 | 'B', 64 | 'C', 65 | 'E', 66 | 'F', 67 | 'G', 68 | 'H', 69 | 'I', 70 | 'J', 71 | 'K', 72 | 'L', 73 | 'N', 74 | 'O', 75 | 'P', 76 | 'Q', 77 | 'R', 78 | 'S', 79 | 'T', 80 | 'U', 81 | 'V', 82 | 'W', 83 | 'X', 84 | 'Z', 85 | '0', 86 | '1', 87 | '2', 88 | '3', 89 | '4', 90 | '5', 91 | '6', 92 | '7', 93 | '8', 94 | '9', 95 | ]; 96 | for (const invalidChar in invalidChars) { 97 | if (DefaultDateFormatter.formatContains(invalidChar, format)) { 98 | return true; 99 | } 100 | } 101 | return false; 102 | } 103 | 104 | parseToDate(dateAsString: string): Date { 105 | const date = new Date(); 106 | const year = this.extractComponentOfDate(dateAsString, { 107 | start: this.yearIndex, 108 | end: this.yearIndex + DefaultDateFormatter.YEAR_IDENTIFIER.length, 109 | }); 110 | const month = this.extractComponentOfDate(dateAsString, { 111 | start: this.monthIndex, 112 | end: this.monthIndex + DefaultDateFormatter.MONTH_IDENTIFIER.length, 113 | offset: 1, // needed offset cause Date API returns wrong month of date => WHYYY??? 114 | }); 115 | const day = this.extractComponentOfDate(dateAsString, { 116 | start: this.dayIndex, 117 | end: this.dayIndex + DefaultDateFormatter.DAY_IDENTIFIER.length, 118 | }); 119 | date.setFullYear(year, month, day); 120 | return date; 121 | } 122 | 123 | private extractComponentOfDate( 124 | dateAsString: string, 125 | range: DateComponentRange, 126 | ): number { 127 | return ( 128 | Number.parseInt(dateAsString.substring(range.start, range.end), 10) - 129 | (range.offset ?? 0) 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Notice, Plugin, TFile, type WorkspaceLeaf } from 'obsidian'; 2 | import Birthday from './birthday'; 3 | import { DefaultDateFormatter } from './dateFormatter'; 4 | import SearchPersonModal from './modals/SearchPersonModal'; 5 | import Person from './person'; 6 | import { 7 | type BirthdayTrackerSettings, 8 | BirthdayTrackerSettingTab, 9 | DEFAULT_SETTINGS, 10 | } from './settings'; 11 | import { 12 | BIRTHDAY_TRACKER_VIEW_TYPE, 13 | BirthdayTrackerView, 14 | } from './views/birthdayTrackerView'; 15 | import { 16 | BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE, 17 | YearOverviewView, 18 | } from './views/yearOverviewView'; 19 | 20 | export default class BirthdayTrackerPlugin extends Plugin { 21 | settings: BirthdayTrackerSettings; 22 | persons: Array; 23 | 24 | async onload() { 25 | await this.loadSettings(); 26 | 27 | this.registerView( 28 | BIRTHDAY_TRACKER_VIEW_TYPE, 29 | (leaf) => new BirthdayTrackerView(leaf), 30 | ); 31 | this.registerView( 32 | BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE, 33 | (leaf) => new YearOverviewView(leaf), 34 | ); 35 | 36 | const ribbonIconEl = this.addRibbonIcon( 37 | 'cake', 38 | 'Track birthdays', 39 | this.trackBirthdays, 40 | ); 41 | ribbonIconEl.addClass('birthday-tracker-plugin-ribbon-class'); 42 | this.addRibbonIcon( 43 | 'calendar-days', 44 | 'Open year overview', 45 | this.openYearView, 46 | ); 47 | 48 | this.addCommands(); 49 | 50 | this.addSettingTab(new BirthdayTrackerSettingTab(this.app, this)); 51 | this.app.workspace.onLayoutReady( 52 | async () => 53 | await this.trackBirthdaysWithOpenOption( 54 | this.settings.automaticallyOpenBirthdayViewOnStart, 55 | ), 56 | ); 57 | } 58 | 59 | private addCommands() { 60 | this.addCommand({ 61 | id: 'track-birthdays', 62 | name: 'Track Birthdays', 63 | callback: this.trackBirthdays, 64 | }); 65 | this.addCommand({ 66 | id: 'search-person', 67 | name: 'Search Person', 68 | callback: this.searchPerson, 69 | }); 70 | this.addCommand({ 71 | id: 'year-overview', 72 | name: 'Year Overview', 73 | callback: this.openYearView, 74 | }); 75 | } 76 | 77 | onunload() {} 78 | 79 | trackBirthdays = async () => await this.trackBirthdaysWithOpenOption(true); 80 | 81 | trackBirthdaysWithOpenOption = async (shouldOpenView: boolean) => { 82 | const content = await this.fetchContent(); 83 | if (content) { 84 | this.trackBirthdaysOfContent(content); 85 | if (shouldOpenView) { 86 | await this.openBirthdayView(); 87 | } 88 | } else { 89 | new Notice('Nothing inside your node'); 90 | } 91 | }; 92 | 93 | async fetchContent(): Promise { 94 | const file = this.app.vault.getAbstractFileByPath( 95 | this.settings.birthdayNodeLocation, 96 | ); 97 | if (file && file instanceof TFile) { 98 | return (await this.app.vault.read(file)).trim(); 99 | } 100 | new Notice( 101 | `Node could not be found at location: ${this.settings.birthdayNodeLocation}`, 102 | ); 103 | return undefined; 104 | } 105 | 106 | trackBirthdaysOfContent = (content: string) => { 107 | this.persons = this.collectPersons(content); 108 | this.persons.sort((p1: Person, p2: Person) => p1.compareTo(p2)); 109 | this.noticeIfBirthdayToday(this.persons); 110 | }; 111 | 112 | collectPersons(content: string): Array { 113 | const persons: Array = []; 114 | content.split(/\r?\n/).forEach((line) => { 115 | if (this.lineContainsPerson(line)) { 116 | const splittedLine = line.split(';'); 117 | const name = splittedLine[0]?.trim().split('=').last()?.trim() ?? ''; 118 | const birthdayAsString = 119 | splittedLine[1]?.replace(' ', '').split('=').last()?.trim() ?? ''; 120 | const birthday = new Birthday( 121 | birthdayAsString, 122 | // biome-ignore lint/style/noNonNullAssertion: Should work, because this check is already done before in settings.ts when dateFormatting is updated 123 | DefaultDateFormatter.createFormat(this.settings.dateFormatting)!, 124 | ); 125 | persons.push(new Person(name, birthday)); 126 | } 127 | }); 128 | return persons; 129 | } 130 | 131 | lineContainsPerson = (line: string) => { 132 | return line.contains('name=') && line.contains('birthday='); 133 | }; 134 | 135 | noticeIfBirthdayToday(persons: Array): void { 136 | const personsBirthdayToday: Array = persons.filter((person) => 137 | person.hasBirthdayToday(), 138 | ); 139 | if (personsBirthdayToday.length !== 0) { 140 | this.noticeForAllBirthdaysToday(personsBirthdayToday); 141 | } 142 | } 143 | 144 | noticeForAllBirthdaysToday(personsBirthdayToday: Array): void { 145 | let message = 'Today '; 146 | // biome-ignore lint: performance issue to use for..of not relevant 147 | personsBirthdayToday.forEach( 148 | // biome-ignore lint: message can be overwritten here 149 | (person) => (message = message.concat(person.toDTO().name).concat(', ')), 150 | ); 151 | message = message.substring(0, message.length - 2); // remove last not needed ", " 152 | new Notice( 153 | message.concat( 154 | `${personsBirthdayToday.length > 1 ? ' have' : ' has'} birthday`, 155 | ), 156 | ); 157 | } 158 | 159 | async openBirthdayView(): Promise { 160 | const leaves: WorkspaceLeaf[] = this.app.workspace.getLeavesOfType( 161 | BIRTHDAY_TRACKER_VIEW_TYPE, 162 | ); 163 | if (this.persons) { 164 | (await this.getBirthdayView(leaves)).displayPersons(this.persons); 165 | } 166 | this.app.workspace.revealLeaf(leaves[0]); 167 | } 168 | 169 | async getBirthdayView(leaves: WorkspaceLeaf[]): Promise { 170 | if (leaves.length === 0) { 171 | const leaf: WorkspaceLeaf | null = this.app.workspace.getRightLeaf(false); 172 | if (leaf) { 173 | leaves[0] = leaf; 174 | await leaves[0].setViewState({ type: BIRTHDAY_TRACKER_VIEW_TYPE }); 175 | } 176 | } 177 | return leaves[0].view as BirthdayTrackerView; 178 | } 179 | 180 | openYearView = async (): Promise => { 181 | const leaves: WorkspaceLeaf[] = this.app.workspace.getLeavesOfType( 182 | BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE, 183 | ); 184 | if (leaves.length === 0) { 185 | leaves[0] = this.app.workspace.getLeaf(false); 186 | await leaves[0].setViewState({ 187 | type: BIRTHDAY_TRACKER_YEAR_OVERVIEW_VIEW_TYPE, 188 | }); 189 | } 190 | const persons: Person[] = await this.getPersons(); 191 | await (leaves[0].view as YearOverviewView).updatePersons( 192 | persons.map((p) => p.toDTO()), 193 | ); 194 | this.app.workspace.revealLeaf(leaves[0]); 195 | }; 196 | 197 | async getPersons(): Promise { 198 | const content = await this.fetchContent(); 199 | if (content) { 200 | this.trackBirthdaysOfContent(content); 201 | } 202 | return this.persons; 203 | } 204 | 205 | searchPerson = async () => { 206 | await this.fetchContent(); 207 | if (this.persons.length >= 1) { 208 | new SearchPersonModal( 209 | this.app, 210 | this.persons.map((person) => person.toDTO()), 211 | ).open(); 212 | } else { 213 | new Notice('No persons were found'); 214 | } 215 | }; 216 | 217 | async loadSettings() { 218 | this.settings = { ...DEFAULT_SETTINGS, ...(await this.loadData()) }; 219 | } 220 | 221 | async saveSettings() { 222 | await this.saveData(this.settings); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "birthday-tracker", 6 | "devDependencies": { 7 | "@biomejs/biome": "^2.3.2", 8 | "@types/bun": "^1.3.1", 9 | "@types/node": "^24.9.2", 10 | "builtin-modules": "5.0.0", 11 | "esbuild": "0.25.11", 12 | "obsidian": "1.10.0", 13 | "tslib": "2.8.1", 14 | "typescript": "5.9.3", 15 | }, 16 | }, 17 | }, 18 | "packages": { 19 | "@biomejs/biome": ["@biomejs/biome@2.3.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.2", "@biomejs/cli-darwin-x64": "2.3.2", "@biomejs/cli-linux-arm64": "2.3.2", "@biomejs/cli-linux-arm64-musl": "2.3.2", "@biomejs/cli-linux-x64": "2.3.2", "@biomejs/cli-linux-x64-musl": "2.3.2", "@biomejs/cli-win32-arm64": "2.3.2", "@biomejs/cli-win32-x64": "2.3.2" }, "bin": { "biome": "bin/biome" } }, "sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg=="], 20 | 21 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew=="], 22 | 23 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA=="], 24 | 25 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw=="], 26 | 27 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw=="], 28 | 29 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA=="], 30 | 31 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA=="], 32 | 33 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg=="], 34 | 35 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ=="], 36 | 37 | "@codemirror/state": ["@codemirror/state@6.5.2", "", { "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA=="], 38 | 39 | "@codemirror/view": ["@codemirror/view@6.38.2", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-bTWAJxL6EOFLPzTx+O5P5xAO3gTqpatQ2b/ARQ8itfU/v2LlpS3pH2fkL0A3E/Fx8Y2St2KES7ZEV0sHTsSW/A=="], 40 | 41 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], 42 | 43 | "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], 44 | 45 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], 46 | 47 | "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], 48 | 49 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], 50 | 51 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], 52 | 53 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], 54 | 55 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], 56 | 57 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], 58 | 59 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], 60 | 61 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], 62 | 63 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], 64 | 65 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], 66 | 67 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], 68 | 69 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], 70 | 71 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], 72 | 73 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], 74 | 75 | "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], 76 | 77 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], 78 | 79 | "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], 80 | 81 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], 82 | 83 | "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], 84 | 85 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], 86 | 87 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], 88 | 89 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], 90 | 91 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], 92 | 93 | "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], 94 | 95 | "@types/bun": ["@types/bun@1.3.1", "", { "dependencies": { "bun-types": "1.3.1" } }, "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ=="], 96 | 97 | "@types/codemirror": ["@types/codemirror@5.60.8", "", { "dependencies": { "@types/tern": "*" } }, "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw=="], 98 | 99 | "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], 100 | 101 | "@types/node": ["@types/node@24.9.2", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA=="], 102 | 103 | "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], 104 | 105 | "@types/tern": ["@types/tern@0.23.9", "", { "dependencies": { "@types/estree": "*" } }, "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw=="], 106 | 107 | "builtin-modules": ["builtin-modules@5.0.0", "", {}, "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg=="], 108 | 109 | "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], 110 | 111 | "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], 112 | 113 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 114 | 115 | "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], 116 | 117 | "moment": ["moment@2.29.4", "", {}, "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="], 118 | 119 | "obsidian": ["obsidian@1.10.0", "", { "dependencies": { "@types/codemirror": "5.60.8", "moment": "2.29.4" }, "peerDependencies": { "@codemirror/state": "6.5.0", "@codemirror/view": "6.38.1" } }, "sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw=="], 120 | 121 | "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], 122 | 123 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 124 | 125 | "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 126 | 127 | "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 128 | 129 | "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], 130 | 131 | "bun-types/@types/node": ["@types/node@24.9.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-MKNwXh3seSK8WurXF7erHPJ2AONmMwkI7zAMrXZDPIru8jRqkk6rGDBVbw4mLwfqA+ZZliiDPg05JQ3uW66tKQ=="], 132 | } 133 | } 134 | --------------------------------------------------------------------------------