Initial pass at simple flashcards

This commit is contained in:
Erin Schnabel 2025-10-07 08:37:24 -04:00
commit 2d5695e5b1
No known key found for this signature in database
17 changed files with 2050 additions and 0 deletions

23
.editorconfig Normal file
View file

@ -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

19
.gitignore vendored Normal file
View file

@ -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

72
CLAUDE.md Normal file
View file

@ -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`)

200
README.md Normal file
View file

@ -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: `<vault>/.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 <vault>/.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)

47
biome.json Normal file
View file

@ -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"
}
}
}

57
esbuild.config.mjs Normal file
View file

@ -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()
}

10
manifest.json Normal file
View file

@ -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
}

1039
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

43
package.json Normal file
View file

@ -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).*"
}
}

17
src/@types/settings.d.ts vendored Normal file
View file

@ -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<string, number>; // "file.md#heading" → timestamp
}

View file

@ -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<Card[]> {
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;
}
}

View file

@ -0,0 +1,8 @@
import type { SimpleFlashcardsSettings } from "./@types/settings";
export const DEFAULT_SETTINGS: SimpleFlashcardsSettings = {
cardPaths: [],
defaultDeckPath: "",
trackViews: true,
selectionMode: "least-recent",
};

134
src/flashcards-Modal.ts Normal file
View file

@ -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();
}
}

144
src/flashcards-Plugin.ts Normal file
View file

@ -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();
}
}

View file

@ -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();
}
}

3
src/main.ts Normal file
View file

@ -0,0 +1,3 @@
import SimpleFlashcardsPlugin from "./flashcards-Plugin";
export default SimpleFlashcardsPlugin;

22
tsconfig.json Normal file
View file

@ -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"
]
}