From 2d5695e5b123e21c6a8df022b873569403cc7da8 Mon Sep 17 00:00:00 2001 From: Erin Schnabel Date: Tue, 7 Oct 2025 08:37:24 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Initial=20pass=20at=20simple=20flas?= =?UTF-8?q?hcards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 23 + .gitignore | 19 + CLAUDE.md | 72 +++ README.md | 200 +++++++ biome.json | 47 ++ esbuild.config.mjs | 57 ++ manifest.json | 10 + package-lock.json | 1039 +++++++++++++++++++++++++++++++++ package.json | 43 ++ src/@types/settings.d.ts | 17 + src/flashcards-CardParser.ts | 68 +++ src/flashcards-Constants.ts | 8 + src/flashcards-Modal.ts | 134 +++++ src/flashcards-Plugin.ts | 144 +++++ src/flashcards-SettingsTab.ts | 144 +++++ src/main.ts | 3 + tsconfig.json | 22 + 17 files changed, 2050 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 biome.json create mode 100644 esbuild.config.mjs create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/@types/settings.d.ts create mode 100644 src/flashcards-CardParser.ts create mode 100644 src/flashcards-Constants.ts create mode 100644 src/flashcards-Modal.ts create mode 100644 src/flashcards-Plugin.ts create mode 100644 src/flashcards-SettingsTab.ts create mode 100644 src/main.ts create mode 100644 tsconfig.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..acd8ebb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,html,rb,css,xml,scss,json}] +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76e9cb7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Intellij +*.iml +.idea + +# VS Code +.vscode + +# npm +node_modules +.env + +# build +build + +# obsidian +data.json +references +*.out +.claude/settings.local.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5074554 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +This project is an Obsidian plugin for simple flashcard creation and review. **Read README.md for full feature details and usage instructions.** + +## Your Role + +You are a senior development peer working alongside a Senior Software Engineer (25+ years, primarily Java background) on this hobby TypeScript project. Act as a collaborative partner for: +- **Code review and feedback** when requested - focus on patterns, maintainability, and TypeScript/JS idioms +- **Implementation assistance** when explicitly asked - suggest approaches, don't implement unless requested +- **Technical discussion** and problem-solving - challenge assumptions, ask probing questions, offer alternatives + +## Development Guidelines + +**Core Principles:** +- **Follow existing patterns** - Before writing new code: + 1. Search for similar functions in the same module (use `Grep` tool) + 2. Check method chaining, line breaks, and error handling patterns + 3. Emulate the style exactly, especially for method chains and async/await +- **Understand before acting** - Read project structure, but defer extensive file reading until user specifies what to work on +- **Ask for clarification** when implementation choices or requirements are unclear +- **Be direct and concise** - Assume high technical competence, reference specific files/line numbers +- **Never speculate** - Don't make up code unless asked +- **Point out issues proactively** but wait for explicit requests to fix them + +## Commands + +- `npm run build` - Build the plugin +- `npm run dev` - Build and watch for changes +- `npm run lint` - Lint TypeScript files +- `npm run fix` - Auto-fix linting issues +- `npm run format` - Format code + +## Architecture + +**Core files:** +- `main.ts` - Main plugin class +- Plugin structure TBD based on flashcard implementation + +**Key features:** +- Simple flashcard syntax +- Spaced repetition algorithm +- Review interface + +## Code Style Guidelines + +- **Line length**: 80 characters (hard limit) +- **Always use braces** for conditionals +- **Method chaining**: Break at dots for readability, even for single chains. This keeps lines under 80 chars and prevents Biome from wrapping unpredictably. + ```typescript + // GOOD - break at dots + const patterns = this.settings.excludeLinkPatterns + .split("\n") + .map((p) => p.trim()) + .filter((p) => p.length > 0); + + // BAD - all on one line + const patterns = this.settings.excludeLinkPatterns.split("\n").map((p) => p.trim()); + + // GOOD - even single chains if they approach 80 chars + const models = data.models + ?.map((model) => model.name) || []; + ``` +- **Error handling**: `try/catch` with user-friendly `Notice` messages +- **Async**: Use `async/await` consistently + +## Quality Assurance + +- Run `npm run build` after significant changes (includes linting via prebuild) +- Use `npm run fix` to auto-correct linting issues +- Reference specific line numbers when discussing issues (format: `file.ts:123`) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6419916 --- /dev/null +++ b/README.md @@ -0,0 +1,200 @@ +# Simple Flashcards + +A simple activity card plugin for Obsidian. Display random cards from your markdown files for meditations, prompts, exercises, or any repeatable activities. + +## Features + +- **Random or Least-Recent Selection** - View cards randomly or ensure you see all cards evenly +- **Deck Organization** - Organize cards into different folders/paths +- **View Tracking** - Optional tracking to prioritize cards you haven't seen recently +- **Simple Markdown Format** - Use H2 headings to define cards +- **Switch Decks on the Fly** - Change between different card collections within the modal + +## Installation + +### Manual Installation + +1. Download the latest release from the [Releases page](https://github.com/ebullient/obsidian-flashcards/releases) +2. Extract the files into your vault's plugins folder: `/.obsidian/plugins/simple-flashcards/` +3. Reload Obsidian +4. Enable the plugin in Settings → Community Plugins + +### Development Installation + +1. Clone this repository into your vault's plugins folder: + ```bash + cd /.obsidian/plugins + git clone https://github.com/ebullient/obsidian-flashcards.git simple-flashcards + cd simple-flashcards + ``` + +2. Install dependencies and build: + ```bash + npm install + npm run build + ``` + +3. Reload Obsidian and enable the plugin + +## Card Format + +Cards are created from markdown files using H2 headings (`##`). Each heading becomes one card. + +### Example + +```markdown +--- +tags: +- flashcards/activities +--- + +#flashcards/activities/morning +## CARD 1: Morning Stretching Routine + +Start your day with gentle movement: + +1. Neck rolls - 5 each direction +2. Shoulder shrugs - 10 reps +3. Side stretches - hold 15 seconds each side +4. Forward fold - hold 30 seconds + +Take your time and breathe deeply. + +--- + +#flashcards/activities/creative +## CARD 2: Creative Writing Prompt + +**Setting:** A library that only opens at midnight + +**Challenge:** Write for 10 minutes about what books are found there and who visits. + +--- + +#flashcards/activities/mindfulness +## CARD 3: Breathing Exercise + +**Box Breathing:** + +- Inhale for 4 counts +- Hold for 4 counts +- Exhale for 4 counts +- Hold for 4 counts + +Repeat 4 times. + +--- +``` + +### Format Rules + +- **H2 headings** (`##`) mark the start of each card +- **Horizontal rules** (`---`) mark the end of card content (optional) +- **Tag lines** starting with `#flashcards` are automatically stripped from display +- Content after `---` is ignored (use for notes, metadata, etc.) + +## Configuration + +Open Settings → Simple Flashcards to configure: + +### Card Paths +List of folders or files to scan for cards (one per line, relative to vault root). + +Example: +``` +Activities/Exercises +Daily/Prompts +Resources/Meditations.md +``` + +### Default Deck Path +The default path used by "Show Random Activity Card" command. Leave empty to select from all configured paths. + +### Track Views +Enable tracking of when cards were last viewed. Required for "Least Recently Viewed" selection mode. + +### Selection Mode +- **Random** - Pure random selection +- **Least Recently Viewed** - Prioritizes cards you haven't seen recently + +## Commands + +### Show Random Activity Card +Opens a modal displaying a card from your default deck (or all cards if no default is set). + +**Modal Controls:** +- **Switch Deck** - Cycle through configured card paths +- **Next Card** - View another card (records the current card as viewed) +- **Done** - Close the modal + +### Refresh Activity Cards +Rescans all configured paths to pick up new or modified cards. + +## Usage Tips + +### Organizing Cards + +**Multiple files in folders:** +``` +Activities/ + ├── stretches.md + ├── writing-prompts.md + └── meditations.md +``` + +**Single file per deck:** +``` +Daily/ + └── morning-routine.md +``` + +**Mixed approach:** +Configure both folders and individual files in Card Paths. + +### View Tracking + +With "Track Views" enabled and "Least Recently Viewed" selection mode, the plugin ensures you see all cards before repeating. Perfect for: +- Daily meditation rotations +- Exercise variety +- Prompt cycling + +### Tags and Metadata + +While `#flashcards` tag lines are stripped for display, you can still: +- Use frontmatter tags for vault organization +- Add inline tags for sub-categorization +- Include any markdown formatting (bold, lists, links, etc.) + +## Development + +### Building + +```bash +npm run build # Production build +npm run dev # Watch mode +npm run lint # Check for issues +npm run fix # Auto-fix linting issues +npm run format # Format code +``` + +### Project Structure + +``` +src/ + ├── @types/ + │ └── settings.d.ts # TypeScript interfaces + ├── flashcards-Plugin.ts # Main plugin class + ├── flashcards-CardParser.ts # Parse files into cards + ├── flashcards-Modal.ts # Card display modal + ├── flashcards-SettingsTab.ts # Settings UI + ├── flashcards-Constants.ts # Default settings + └── main.ts # Entry point +``` + +## License + +MIT + +## Author + +[ebullient](https://github.com/ebullient) diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..264c779 --- /dev/null +++ b/biome.json @@ -0,0 +1,47 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": ["src/**"] + }, + "formatter": { + "enabled": true, + "indentWidth": 4, + "useEditorconfig": true + }, + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} \ No newline at end of file diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..ccdebde --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,57 @@ +import esbuild from "esbuild"; +import process from "node:process"; +import builtins from 'builtin-modules'; + +const banner = `/* +THIS IS A GENERATED/BUNDLED FILE BY ESBUILD +if you want to view the source, please visit the github repository of this plugin +*/ +`; + +const prod = (process.argv[2] === 'production'); +const dir = prod || !process.env.OUTDIR ? "./build" : process.env.OUTDIR; + +const parameters = { + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: [ + 'obsidian', + 'electron', + '@codemirror/autocomplete', + '@codemirror/collab', + '@codemirror/commands', + '@codemirror/language', + '@codemirror/lint', + '@codemirror/search', + '@codemirror/state', + '@codemirror/view', + '@lezer/common', + '@lezer/highlight', + '@lezer/lr', + ...builtins + ], + format: 'cjs', + logLevel: 'info', + target: 'es2020', + treeShaking: true, + sourcemap: prod ? false : 'inline', + minify: prod, + outdir: dir, +}; + +if (prod) { + await esbuild.build(parameters).catch((x) => { + if (x.errors) { + console.error(x.errors); + } else { + console.error(x); + } + process.exit(1) + }); +} else { + const ctx = await esbuild.context(parameters); + await ctx.watch() +} \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..5cd9596 --- /dev/null +++ b/manifest.json @@ -0,0 +1,10 @@ +{ + "id": "simple-flashcards", + "name": "Simple Flashcards", + "version": "0.1.0", + "minAppVersion": "0.13.10", + "description": "A simple flashcard plugin for Obsidian", + "author": "ebullient", + "authorUrl": "https://github.com/ebullient", + "isDesktopOnly": false +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2f7714a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1039 @@ +{ + "name": "simple-flashcards", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simple-flashcards", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "2.2.3", + "@types/node": "^24.3.1", + "auto-changelog": "^2.5.0", + "builtin-modules": "^5.0.0", + "esbuild": "^0.25.9", + "obsidian": "^1.8.7", + "tslib": "^2.8.1", + "typescript": "5.9.2" + } + }, + "node_modules/@biomejs/biome": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.2.3.tgz", + "integrity": "sha512-9w0uMTvPrIdvUrxazZ42Ib7t8Y2yoGLKLdNne93RLICmaHw7mcLv4PPb5LvZLJF3141gQHiCColOh/v6VWlWmg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.2.3", + "@biomejs/cli-darwin-x64": "2.2.3", + "@biomejs/cli-linux-arm64": "2.2.3", + "@biomejs/cli-linux-arm64-musl": "2.2.3", + "@biomejs/cli-linux-x64": "2.2.3", + "@biomejs/cli-linux-x64-musl": "2.2.3", + "@biomejs/cli-win32-arm64": "2.2.3", + "@biomejs/cli-win32-x64": "2.2.3" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.2.3.tgz", + "integrity": "sha512-OrqQVBpadB5eqzinXN4+Q6honBz+tTlKVCsbEuEpljK8ASSItzIRZUA02mTikl3H/1nO2BMPFiJ0nkEZNy3B1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.2.3.tgz", + "integrity": "sha512-OCdBpb1TmyfsTgBAM1kPMXyYKTohQ48WpiN9tkt9xvU6gKVKHY4oVwteBebiOqyfyzCNaSiuKIPjmHjUZ2ZNMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.2.3.tgz", + "integrity": "sha512-g/Uta2DqYpECxG+vUmTAmUKlVhnGEcY7DXWgKP8ruLRa8Si1QHsWknPY3B/wCo0KgYiFIOAZ9hjsHfNb9L85+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.3.tgz", + "integrity": "sha512-q3w9jJ6JFPZPeqyvwwPeaiS/6NEszZ+pXKF+IczNo8Xj6fsii45a4gEEicKyKIytalV+s829ACZujQlXAiVLBQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.2.3.tgz", + "integrity": "sha512-LEtyYL1fJsvw35CxrbQ0gZoxOG3oZsAjzfRdvRBRHxOpQ91Q5doRVjvWW/wepgSdgk5hlaNzfeqpyGmfSD0Eyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.3.tgz", + "integrity": "sha512-y76Dn4vkP1sMRGPFlNc+OTETBhGPJ90jY3il6jAfur8XWrYBQV3swZ1Jo0R2g+JpOeeoA0cOwM7mJG6svDz79w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.2.3.tgz", + "integrity": "sha512-Ms9zFYzjcJK7LV+AOMYnjN3pV3xL8Prxf9aWdDVL74onLn5kcvZ1ZMQswE5XHtnd/r/0bnUd928Rpbs14BzVmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.2.3.tgz", + "integrity": "sha512-gvCpewE7mBwBIpqk1YrUqNR4mCiyJm6UI3YWQQXkedSSEwzRdodRpaKhbdbHw1/hmTWOVXQ+Eih5Qctf4TCVOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.38.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", + "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.0.tgz", + "integrity": "sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/auto-changelog": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz", + "integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "handlebars": "^4.7.7", + "import-cwd": "^3.0.0", + "node-fetch": "^2.6.1", + "parse-github-url": "^1.0.3", + "semver": "^7.3.5" + }, + "bin": { + "auto-changelog": "src/index.js" + }, + "engines": { + "node": ">=8.3" + } + }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/import-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz", + "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "import-from": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/import-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz", + "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/obsidian": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.10.0.tgz", + "integrity": "sha512-F7hhnmGRQD1TanDPFT//LD3iKNUVd7N8sKL7flCCHRszfTxpDJ39j3T7LHbcGpyid906i6lD5oO+cnfLBzJMKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.1" + } + }, + "node_modules/parse-github-url": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz", + "integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==", + "dev": true, + "license": "MIT", + "bin": { + "parse-github-url": "cli.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..db0a831 --- /dev/null +++ b/package.json @@ -0,0 +1,43 @@ +{ + "name": "simple-flashcards", + "version": "0.1.0", + "private": true, + "description": "Simple flashcard plugin for Obsidian (https://obsidian.md)", + "main": "main.js", + "scripts": { + "dev": "node esbuild.config.mjs", + "fix": "npx @biomejs/biome check --write ./src", + "format": "npx @biomejs/biome format ./src", + "lint": "npx @biomejs/biome lint ./src", + "prebuild": "npx @biomejs/biome check ./src", + "build": "node esbuild.config.mjs production", + "postbuild": "cp -v manifest.json README.md build", + "preversion": "npm run build", + "version": "auto-changelog -p" + }, + "keywords": [ + "obsidian", + "obsidian-md", + "obsidian-plugin", + "obsidian-md-plugin", + "flashcards" + ], + "author": "ebullient", + "repository": "github.com:ebullient/obsidian-flashcards", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "2.2.3", + "@types/node": "^24.3.1", + "auto-changelog": "^2.5.0", + "builtin-modules": "^5.0.0", + "esbuild": "^0.25.9", + "obsidian": "^1.8.7", + "tslib": "^2.8.1", + "typescript": "5.9.2" + }, + "auto-changelog": { + "backfillLimit": false, + "commitLimit": false, + "ignoreCommitPattern": "(🔖|🔨|🧹|changelog|release|Update README).*" + } +} diff --git a/src/@types/settings.d.ts b/src/@types/settings.d.ts new file mode 100644 index 0000000..3b579e4 --- /dev/null +++ b/src/@types/settings.d.ts @@ -0,0 +1,17 @@ +export interface Card { + filePath: string; + heading: string; + content: string; + key: string; // "filepath#heading" for tracking +} + +export interface SimpleFlashcardsSettings { + cardPaths: string[]; // ["Journal/Coping", "Activities"] + defaultDeckPath: string; // Default path for quick command + trackViews: boolean; // Enable view tracking + selectionMode: "random" | "least-recent"; +} + +export interface SimpleFlashcardsData { + cardViews: Record; // "file.md#heading" → timestamp +} diff --git a/src/flashcards-CardParser.ts b/src/flashcards-CardParser.ts new file mode 100644 index 0000000..8d8a95e --- /dev/null +++ b/src/flashcards-CardParser.ts @@ -0,0 +1,68 @@ +import type { App, TFile } from "obsidian"; +import type { Card } from "./@types/settings"; + +export class CardParser { + app: App; + + constructor(app: App) { + this.app = app; + } + + async parseFile(file: TFile): Promise { + const content = await this.app.vault.read(file); + const cards: Card[] = []; + const headingRegex = /^## (.+)$/gm; + + // Find all H2 headings + const headings: { text: string; index: number }[] = []; + let match = headingRegex.exec(content); + while (match !== null) { + headings.push({ text: match[1], index: match.index }); + match = headingRegex.exec(content); + } + + if (headings.length === 0) { + console.warn(`Skipping ${file.path}: No H2 headings found`); + return []; + } + + // Extract content for each heading + for (let i = 0; i < headings.length; i++) { + const heading = headings[i]; + const nextIndex = + i + 1 < headings.length + ? headings[i + 1].index + : content.length; + + let cardContent = content.substring(heading.index, nextIndex); + + // Remove the H2 heading line itself + const headingLineEnd = cardContent.indexOf("\n"); + if (headingLineEnd !== -1) { + cardContent = cardContent.substring(headingLineEnd + 1); + } + + // Truncate at first --- + const dividerIndex = cardContent.indexOf("\n---"); + if (dividerIndex !== -1) { + cardContent = cardContent.substring(0, dividerIndex); + } + + // Strip lines that are ONLY #flashcards tags + cardContent = cardContent + .split("\n") + .filter((line) => !line.trim().startsWith("#flashcards")) + .join("\n") + .trim(); + + cards.push({ + filePath: file.path, + heading: heading.text, + content: cardContent, + key: `${file.path}#${heading.text}`, + }); + } + + return cards; + } +} diff --git a/src/flashcards-Constants.ts b/src/flashcards-Constants.ts new file mode 100644 index 0000000..81edd67 --- /dev/null +++ b/src/flashcards-Constants.ts @@ -0,0 +1,8 @@ +import type { SimpleFlashcardsSettings } from "./@types/settings"; + +export const DEFAULT_SETTINGS: SimpleFlashcardsSettings = { + cardPaths: [], + defaultDeckPath: "", + trackViews: true, + selectionMode: "least-recent", +}; diff --git a/src/flashcards-Modal.ts b/src/flashcards-Modal.ts new file mode 100644 index 0000000..4870cfa --- /dev/null +++ b/src/flashcards-Modal.ts @@ -0,0 +1,134 @@ +import { type App, ButtonComponent, MarkdownRenderer, Modal } from "obsidian"; +import type { Card } from "./@types/settings"; +import type SimpleFlashcardsPlugin from "./flashcards-Plugin"; + +export class FlashcardModal extends Modal { + plugin: SimpleFlashcardsPlugin; + card: Card | null; + deckPath: string | undefined; + contentEl: HTMLElement; + + constructor( + app: App, + plugin: SimpleFlashcardsPlugin, + card: Card | null, + deckPath?: string, + ) { + super(app); + this.plugin = plugin; + this.card = card; + this.deckPath = deckPath; + } + + onOpen() { + this.display(); + } + + display() { + const { contentEl } = this; + contentEl.empty(); + + if (!this.card) { + contentEl.createEl("p", { + text: "No cards available. Check your card paths in settings.", + }); + this.addCloseButton(); + return; + } + + // Render card heading + contentEl.createEl("h2", { text: this.card.heading }); + + // Render card content as markdown + const contentDiv = contentEl.createEl("div", { + cls: "flashcard-content", + }); + + MarkdownRenderer.render( + this.app, + this.card.content, + contentDiv, + this.card.filePath, + this.plugin, + ); + + // Add button controls + this.addControls(); + } + + private addControls() { + const { contentEl } = this; + const buttonContainer = contentEl.createEl("div", { + cls: "modal-button-container", + }); + + // Switch Deck button (only if multiple decks configured) + if (this.plugin.settings.cardPaths.length > 1) { + new ButtonComponent(buttonContainer) + .setButtonText("Switch Deck") + .onClick(() => { + this.showDeckSwitcher(); + }); + } + + // Next Card button + new ButtonComponent(buttonContainer) + .setButtonText("Next Card") + .setCta() + .onClick(() => { + this.showNextCard(); + }); + + // Done button + new ButtonComponent(buttonContainer) + .setButtonText("Done") + .onClick(() => { + this.close(); + }); + } + + private addCloseButton() { + const { contentEl } = this; + const buttonContainer = contentEl.createEl("div", { + cls: "modal-button-container", + }); + + new ButtonComponent(buttonContainer) + .setButtonText("Close") + .onClick(() => { + this.close(); + }); + } + + private showDeckSwitcher() { + const availableDecks = this.plugin.settings.cardPaths; + if (availableDecks.length === 0) { + return; + } + + // Simple approach: cycle through available decks + const currentIndex = this.deckPath + ? availableDecks.indexOf(this.deckPath) + : -1; + const nextIndex = (currentIndex + 1) % availableDecks.length; + this.deckPath = availableDecks[nextIndex]; + + this.showNextCard(); + } + + private showNextCard() { + // Record view for current card + if (this.card && this.plugin.settings.trackViews) { + this.plugin.recordView(this.card.key); + } + + // Select next card + this.card = this.plugin.selectCard(this.deckPath); + this.display(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/flashcards-Plugin.ts b/src/flashcards-Plugin.ts new file mode 100644 index 0000000..d4e7ddb --- /dev/null +++ b/src/flashcards-Plugin.ts @@ -0,0 +1,144 @@ +import { Notice, Plugin, TFile, TFolder } from "obsidian"; +import type { + Card, + SimpleFlashcardsData, + SimpleFlashcardsSettings, +} from "./@types/settings"; +import { CardParser } from "./flashcards-CardParser"; +import { DEFAULT_SETTINGS } from "./flashcards-Constants"; +import { FlashcardModal } from "./flashcards-Modal"; +import { SimpleFlashcardsSettingsTab } from "./flashcards-SettingsTab"; + +export default class SimpleFlashcardsPlugin extends Plugin { + settings!: SimpleFlashcardsSettings; + data!: SimpleFlashcardsData; + cachedCards: Card[] = []; + cardParser!: CardParser; + + async onload() { + console.log("Loading Simple Flashcards plugin"); + + await this.loadSettings(); + this.cardParser = new CardParser(this.app); + + this.addSettingTab(new SimpleFlashcardsSettingsTab(this.app, this)); + + // Command: Show Random Activity Card + this.addCommand({ + id: "show-random-card", + name: "Show Random Activity Card", + callback: () => { + this.showRandomCard(); + }, + }); + + // Command: Refresh Activity Cards + this.addCommand({ + id: "refresh-cards", + name: "Refresh Activity Cards", + callback: async () => { + await this.scanCards(); + new Notice("Activity cards refreshed"); + }, + }); + + // Initial card scan + await this.scanCards(); + } + + onunload() { + console.log("Unloading Simple Flashcards plugin"); + } + + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ); + this.data = (await this.loadData()) || { cardViews: {} }; + } + + async saveSettings() { + await this.saveData({ + ...this.settings, + cardViews: this.data.cardViews, + }); + await this.scanCards(); + } + + async scanCards() { + this.cachedCards = []; + + for (const cardPath of this.settings.cardPaths) { + const abstractFile = this.app.vault.getAbstractFileByPath(cardPath); + + if (abstractFile instanceof TFolder) { + await this.scanFolder(abstractFile); + } else if (abstractFile instanceof TFile) { + const cards = await this.cardParser.parseFile(abstractFile); + this.cachedCards.push(...cards); + } else { + console.warn(`Card path not found: ${cardPath}`); + } + } + + console.log(`Scanned ${this.cachedCards.length} cards`); + } + + private async scanFolder(folder: TFolder) { + for (const child of folder.children) { + if (child instanceof TFile && child.extension === "md") { + const cards = await this.cardParser.parseFile(child); + this.cachedCards.push(...cards); + } else if (child instanceof TFolder) { + await this.scanFolder(child); + } + } + } + + selectCard(deckPath?: string): Card | null { + let pool = this.cachedCards; + + if (deckPath) { + pool = pool.filter((c) => c.filePath.startsWith(deckPath)); + } + + if (pool.length === 0) { + return null; + } + + if (this.settings.selectionMode === "random") { + return pool[Math.floor(Math.random() * pool.length)]; + } + + // least-recent: sort by lastSeen timestamp + const sorted = pool.sort((a, b) => { + const aTime = this.data.cardViews[a.key] || 0; + const bTime = this.data.cardViews[b.key] || 0; + return aTime - bTime; + }); + + return sorted[0]; + } + + recordView(cardKey: string) { + this.data.cardViews[cardKey] = Date.now(); + this.saveData({ + ...this.settings, + cardViews: this.data.cardViews, + }); + } + + showRandomCard() { + const deckPath = this.settings.defaultDeckPath || undefined; + const card = this.selectCard(deckPath); + + if (!card) { + new Notice("No cards available. Check your settings."); + return; + } + + new FlashcardModal(this.app, this, card, deckPath).open(); + } +} diff --git a/src/flashcards-SettingsTab.ts b/src/flashcards-SettingsTab.ts new file mode 100644 index 0000000..fcbaec3 --- /dev/null +++ b/src/flashcards-SettingsTab.ts @@ -0,0 +1,144 @@ +import { type App, PluginSettingTab, Setting } from "obsidian"; +import type { SimpleFlashcardsSettings } from "./@types/settings"; +import type SimpleFlashcardsPlugin from "./flashcards-Plugin"; + +export class SimpleFlashcardsSettingsTab extends PluginSettingTab { + plugin: SimpleFlashcardsPlugin; + newSettings!: SimpleFlashcardsSettings; + + constructor(app: App, plugin: SimpleFlashcardsPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + async save() { + this.plugin.settings = this.newSettings; + await this.plugin.saveSettings(); + } + + private cloneSettings(): SimpleFlashcardsSettings { + return JSON.parse(JSON.stringify(this.plugin.settings)); + } + + async reset() { + this.newSettings = this.cloneSettings(); + this.display(); + } + + display(): void { + if (!this.newSettings) { + this.newSettings = this.cloneSettings(); + } + + const { containerEl } = this; + containerEl.empty(); + + containerEl.createEl("h2", { text: "Simple Flashcards Settings" }); + + new Setting(containerEl) + .setName("Save Settings") + .setClass("flashcards-save-reset") + .addButton((button) => + button + .setButtonText("Reset") + .setTooltip("Reset to current saved settings") + .onClick(() => { + this.reset(); + }), + ) + .addButton((button) => + button + .setButtonText("Save") + .setCta() + .setTooltip("Save all changes") + .onClick(async () => { + await this.save(); + }), + ); + + containerEl.createEl("p", { + text: "Configure activity card decks for random selection.", + }); + + new Setting(containerEl) + .setName("Card Paths") + .setDesc( + "Paths to folders containing card files (one per line, relative to vault root)", + ) + .addTextArea((text) => + text + .setPlaceholder("Journal/Coping\nActivities/Morning") + .setValue(this.newSettings.cardPaths.join("\n")) + .onChange((value) => { + this.newSettings.cardPaths = value + .split("\n") + .map((p) => p.trim()) + .filter((p) => p.length > 0); + }), + ) + .then((setting) => { + setting.controlEl + .querySelector("textarea") + ?.setAttribute("rows", "4"); + }); + + new Setting(containerEl) + .setName("Default Deck Path") + .setDesc( + "Default path for 'Show Random Activity Card' command (leave empty for all cards)", + ) + .addText((text) => + text + .setPlaceholder("Journal/Coping") + .setValue(this.newSettings.defaultDeckPath) + .onChange((value) => { + this.newSettings.defaultDeckPath = value.trim(); + }), + ); + + new Setting(containerEl) + .setName("Track Views") + .setDesc( + "Track when cards were last viewed (enables least-recent selection)", + ) + .addToggle((toggle) => + toggle + .setValue(this.newSettings.trackViews) + .onChange((value) => { + this.newSettings.trackViews = value; + }), + ); + + new Setting(containerEl) + .setName("Selection Mode") + .setDesc("How to select cards to display") + .addDropdown((dropdown) => + dropdown + .addOption("random", "Random") + .addOption("least-recent", "Least Recently Viewed") + .setValue(this.newSettings.selectionMode) + .onChange((value) => { + this.newSettings.selectionMode = value as + | "random" + | "least-recent"; + }), + ); + + containerEl.createEl("h3", { text: "Usage" }); + const usage = containerEl.createEl("div"); + usage.createEl("p", { + text: "Cards are created from markdown files with H2 headings (##). Each heading becomes one card.", + }); + usage.createEl("p", { + text: "Use --- (horizontal rule) to mark the end of card content. Anything after --- will be ignored.", + }); + usage.createEl("p", { + text: "Lines starting with #flashcards will be stripped from card display.", + }); + } + + /** Save on exit */ + hide(): void { + this.save(); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..db9fc7b --- /dev/null +++ b/src/main.ts @@ -0,0 +1,3 @@ +import SimpleFlashcardsPlugin from "./flashcards-Plugin"; + +export default SimpleFlashcardsPlugin; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fb0c4a1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES2020", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": [ + "DOM", + "ES2020" + ] + }, + "include": [ + "**/*.ts" + ] +}