commit 2d5695e5b123e21c6a8df022b873569403cc7da8 Author: Erin Schnabel Date: Tue Oct 7 08:37:24 2025 -0400 ✨ Initial pass at simple flashcards 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" + ] +}