mirror of
https://github.com/ebullient/obsidian-deck-notes.git
synced 2026-07-22 06:40:43 +00:00
🎨 Plugin lint
This commit is contained in:
parent
d46be3cce5
commit
cedca2173d
10 changed files with 4697 additions and 64 deletions
35
SECURITY.md
Normal file
35
SECURITY.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# Security Policy
|
||||
|
||||
I only support the latest version of this plugin. Given the rapid
|
||||
evolution of the MCP protocol and dependencies, running/maintaining older
|
||||
versions isn't feasible.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
This plugin runs a local HTTP server on your machine to expose vault
|
||||
access via the MCP protocol:
|
||||
|
||||
- **Network exposure**: The server listens on localhost by default but
|
||||
supports CORS for remote access (e.g., via Tailscale)
|
||||
- **Authentication**: Currently there is no authentication mechanism - any
|
||||
client that can reach the configured port can access your vault
|
||||
- **Data privacy**: All data stays on your machine; the plugin does not
|
||||
connect to external services or collect telemetry
|
||||
- **Bridge script**: The `mcp-bridge.js` script runs as a Node.js process
|
||||
and bridges stdio to HTTP
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report suspected security issues privately using GitHub's "Report a
|
||||
vulnerability" link in the repository sidebar.
|
||||
|
||||
Do **not** open a public issue for suspected vulnerabilities.
|
||||
|
||||
When reporting, please include:
|
||||
|
||||
- A description of the issue and affected versions
|
||||
- Steps to reproduce (ideally a minimal proof-of-concept)
|
||||
- Assessment of potential impact (network exposure, data access, etc.)
|
||||
- Your contact information
|
||||
- Any specific requests, such as anonymity for you and/or the
|
||||
organization you represent
|
||||
35
eslint.config.mjs
Normal file
35
eslint.config.mjs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// eslint.config.mjs
|
||||
import globals from "globals";
|
||||
import tsparser from "@typescript-eslint/parser";
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
|
||||
export default defineConfig([
|
||||
...obsidianmd.configs.recommended,
|
||||
globalIgnores([
|
||||
"tests/",
|
||||
"*.mjs",
|
||||
"package.json"
|
||||
]),
|
||||
{
|
||||
files: ["src/**/*.ts"],
|
||||
languageOptions: {
|
||||
parser: tsparser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json"
|
||||
},
|
||||
globals: { ...globals.node, ...globals.browser },
|
||||
},
|
||||
// Optional project overrides
|
||||
rules: {
|
||||
"obsidianmd/ui/sentence-case": [
|
||||
"warn",
|
||||
{
|
||||
brands: ["Deck Notes", "journal/coping\nactivities/morning", "example"],
|
||||
acronyms: ["DN", "H2"],
|
||||
enforceCamelCaseLower: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
4565
package-lock.json
generated
4565
package-lock.json
generated
File diff suppressed because it is too large
Load diff
10
package.json
10
package.json
|
|
@ -8,12 +8,14 @@
|
|||
"dev": "node esbuild.config.mjs",
|
||||
"fix": "npx @biomejs/biome check --write ./src",
|
||||
"format": "npx @biomejs/biome format ./src",
|
||||
"eslint": "npx eslint",
|
||||
"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"
|
||||
"version": "auto-changelog -p",
|
||||
"brat-notes": "run() { auto-changelog --stdout --hide-credit --hide-empty-releases --template .github/changelog.hbs -v $1 --starting-version $1 > release-notes.md; }; run"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
|
|
@ -28,10 +30,14 @@
|
|||
"devDependencies": {
|
||||
"@biomejs/biome": "2.2.4",
|
||||
"@types/node": "^24.3.1",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"auto-changelog": "^2.5.0",
|
||||
"builtin-modules": "^5.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"obsidian": "^1.8.7",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-obsidianmd": "^0.1.9",
|
||||
"moment": "^2.30.1",
|
||||
"obsidian": "^1.10.2",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "5.9.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export class DeckNotesApi {
|
|||
* @param deck Tag for deck selection (e.g., "activities")
|
||||
* @returns The card embed text, or null if no cards available
|
||||
*/
|
||||
embedCard(deck: string | undefined = undefined): string | null {
|
||||
embedCard(deck?: string): string | null {
|
||||
return this.plugin.createEmbedText(deck);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,12 +80,26 @@ export class CardParser {
|
|||
*/
|
||||
extractFileLevelTags(fileCache: CachedMetadata | null): string[] {
|
||||
const tags: string[] = [];
|
||||
const frontmatterTags = fileCache?.frontmatter?.tags || [];
|
||||
const frontmatterTags = fileCache?.frontmatter?.tags as
|
||||
| string[]
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
|
||||
for (const tag of frontmatterTags) {
|
||||
const normalized = this.normalizeTag(tag);
|
||||
if (normalized) {
|
||||
tags.push(normalized);
|
||||
if (!frontmatterTags) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
// Handle both array and object formats
|
||||
const tagList = Array.isArray(frontmatterTags)
|
||||
? frontmatterTags
|
||||
: Object.keys(frontmatterTags);
|
||||
|
||||
for (const tag of tagList) {
|
||||
if (typeof tag === "string") {
|
||||
const normalized = this.normalizeTag(tag);
|
||||
if (normalized) {
|
||||
tags.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { type App, ButtonComponent, MarkdownRenderer, Modal } from "obsidian";
|
|||
import type { Card } from "./@types/settings";
|
||||
import type DeckNotesPlugin from "./dn-Plugin";
|
||||
|
||||
export class FlashcardModal extends Modal {
|
||||
export class CardModal extends Modal {
|
||||
plugin: DeckNotesPlugin;
|
||||
card: Card | null;
|
||||
deckTag: string | undefined;
|
||||
|
|
@ -40,14 +40,16 @@ export class FlashcardModal extends Modal {
|
|||
|
||||
// Render card content as markdown
|
||||
const contentDiv = contentEl.createEl("div", {
|
||||
cls: "flashcard-content",
|
||||
cls: "card-content",
|
||||
});
|
||||
|
||||
MarkdownRenderer.render(
|
||||
// Modal lifecycle is tied to plugin, safe to use as component
|
||||
void MarkdownRenderer.render(
|
||||
this.app,
|
||||
this.card.content,
|
||||
contentDiv,
|
||||
this.card.filePath,
|
||||
// eslint-disable-next-line obsidianmd/no-plugin-as-component
|
||||
this.plugin,
|
||||
);
|
||||
|
||||
|
|
@ -65,7 +67,7 @@ export class FlashcardModal extends Modal {
|
|||
const availableTags = this.plugin.api.getTags();
|
||||
if (availableTags.length > 1) {
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setButtonText("Switch Deck")
|
||||
.setButtonText("Switch deck")
|
||||
.onClick(() => {
|
||||
this.showDeckSwitcher();
|
||||
});
|
||||
|
|
@ -73,7 +75,7 @@ export class FlashcardModal extends Modal {
|
|||
|
||||
// Next Card button
|
||||
new ButtonComponent(buttonContainer)
|
||||
.setButtonText("Next Card")
|
||||
.setButtonText("Next card")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.showNextCard();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Card, DeckNotesData, DeckNotesSettings } from "./@types/settings";
|
|||
import { DeckNotesApi } from "./dn-Api";
|
||||
import { CardParser } from "./dn-CardParser";
|
||||
import { DEFAULT_SETTINGS } from "./dn-Constants";
|
||||
import { FlashcardModal } from "./dn-Modal";
|
||||
import { CardModal } from "./dn-Modal";
|
||||
import { DeckNotesSettingsTab } from "./dn-SettingsTab";
|
||||
|
||||
export default class DeckNotesPlugin extends Plugin {
|
||||
|
|
@ -14,7 +14,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
api!: DeckNotesApi;
|
||||
|
||||
async onload() {
|
||||
console.log("Loading Deck Notes plugin");
|
||||
console.debug("Loading Deck Notes plugin", `v${this.manifest.version}`);
|
||||
|
||||
await this.loadSettings();
|
||||
this.cardParser = new CardParser(this.app);
|
||||
|
|
@ -32,38 +32,38 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
}
|
||||
window.deckNotes.api = this.api;
|
||||
|
||||
// Command: Show Random Card
|
||||
// Command: Show card
|
||||
this.addCommand({
|
||||
id: "show-card",
|
||||
name: "Show Card",
|
||||
name: "Show card",
|
||||
callback: () => {
|
||||
this.showFlashCard();
|
||||
this.showCard();
|
||||
},
|
||||
});
|
||||
|
||||
// Command: Embed Card
|
||||
this.addCommand({
|
||||
id: "embed-card",
|
||||
name: "Embed Card",
|
||||
name: "Embed card",
|
||||
editorCallback: (editor) => {
|
||||
this.embedCard(editor);
|
||||
},
|
||||
});
|
||||
|
||||
// Command: Refresh Cards
|
||||
// Command: Refresh cards
|
||||
this.addCommand({
|
||||
id: "refresh-cards",
|
||||
name: "Refresh Cards",
|
||||
name: "Refresh cards",
|
||||
callback: async () => {
|
||||
await this.scanCards();
|
||||
new Notice("Flash cards refreshed");
|
||||
new Notice("Cards refreshed");
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log("Unloading Deck Notes plugin");
|
||||
console.debug("Unloading Deck Notes plugin");
|
||||
|
||||
// Clear API reference
|
||||
if (window.deckNotes) {
|
||||
|
|
@ -72,7 +72,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async loadSettings() {
|
||||
const data = await this.loadData();
|
||||
const data = (await this.loadData()) as DeckNotesData;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
|
||||
this.data = data || { cardViews: {} };
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
console.log(`Scanned ${this.cachedCards.length} cards`);
|
||||
console.debug(`Scanned ${this.cachedCards.length} cards`);
|
||||
}
|
||||
|
||||
private async scanFolder(folder: TFolder) {
|
||||
|
|
@ -131,31 +131,33 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
return null;
|
||||
}
|
||||
|
||||
// least-recent: sort by lastSeen timestamp
|
||||
if (this.settings.selectionMode === "random") {
|
||||
return pool[Math.floor(Math.random() * pool.length)];
|
||||
}
|
||||
|
||||
// Least-recent: sort by lastSeen timestamp
|
||||
const sorted = pool.slice().sort((a, b) => {
|
||||
const aTime = this.data.cardViews[a.key] || 0;
|
||||
const bTime = this.data.cardViews[b.key] || 0;
|
||||
if (aTime === bTime) {
|
||||
// consistent, pseudo-randomizing sort
|
||||
// Consistent, pseudo-randomizing sort
|
||||
return a.hash.localeCompare(b.hash);
|
||||
}
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
return this.settings.selectionMode === "random"
|
||||
? pool[Math.floor(Math.random() * pool.length)]
|
||||
: sorted[0];
|
||||
return sorted[0];
|
||||
}
|
||||
|
||||
recordView(cardKey: string) {
|
||||
this.data.cardViews[cardKey] = Date.now();
|
||||
this.saveData({
|
||||
void this.saveData({
|
||||
...this.settings,
|
||||
cardViews: this.data.cardViews,
|
||||
});
|
||||
}
|
||||
|
||||
showFlashCard() {
|
||||
showCard() {
|
||||
const deckTag = this.settings.defaultDeckTag || undefined;
|
||||
const card = this.selectCard(deckTag);
|
||||
|
||||
|
|
@ -164,10 +166,10 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
new FlashcardModal(this.app, this, card, deckTag).open();
|
||||
new CardModal(this.app, this, card, deckTag).open();
|
||||
}
|
||||
|
||||
createEmbedText(deck: string | undefined): string | null {
|
||||
createEmbedText(deck?: string): string | null {
|
||||
const deckTag = deck || this.settings.defaultDeckTag;
|
||||
const card = this.selectCard(deckTag);
|
||||
|
||||
|
|
@ -193,7 +195,7 @@ export default class DeckNotesPlugin extends Plugin {
|
|||
return embedText;
|
||||
}
|
||||
|
||||
embedCard(editor: Editor, deck: string | undefined = undefined) {
|
||||
embedCard(editor: Editor, deck?: string) {
|
||||
const embedText = this.createEmbedText(deck);
|
||||
|
||||
if (!embedText) {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private cloneSettings(): DeckNotesSettings {
|
||||
return JSON.parse(JSON.stringify(this.plugin.settings));
|
||||
return JSON.parse(
|
||||
JSON.stringify(this.plugin.settings),
|
||||
) as DeckNotesSettings;
|
||||
}
|
||||
|
||||
async reset() {
|
||||
|
|
@ -36,14 +38,14 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
new Setting(containerEl).setHeading().setName("Deck Notes");
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Save Settings")
|
||||
.setName("Save settings")
|
||||
.setClass("decknotes-save-reset")
|
||||
.addButton((button) =>
|
||||
button
|
||||
.setButtonText("Reset")
|
||||
.setTooltip("Reset to current saved settings")
|
||||
.onClick(() => {
|
||||
this.reset();
|
||||
.onClick(async () => {
|
||||
await this.reset();
|
||||
}),
|
||||
)
|
||||
.addButton((button) =>
|
||||
|
|
@ -61,13 +63,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Card Paths")
|
||||
.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")
|
||||
.setPlaceholder("journal/coping\nactivities/morning")
|
||||
.setValue(this.newSettings.cardPaths.join("\n"))
|
||||
.onChange((value) => {
|
||||
this.newSettings.cardPaths = value
|
||||
|
|
@ -83,13 +85,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Default Deck Tag")
|
||||
.setName("Default deck tag")
|
||||
.setDesc(
|
||||
"Default tag for 'Show Random Activity Card' command (e.g., 'activities' or 'activities/morning'). Leave empty for all cards.",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("activities")
|
||||
.setPlaceholder("Activities")
|
||||
.setValue(this.newSettings.defaultDeckTag)
|
||||
.onChange((value) => {
|
||||
this.newSettings.defaultDeckTag = value.trim();
|
||||
|
|
@ -97,7 +99,7 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Track Views")
|
||||
.setName("Track views")
|
||||
.setDesc(
|
||||
"Track when cards were last viewed (enables least-recent selection)",
|
||||
)
|
||||
|
|
@ -110,12 +112,12 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Selection Mode")
|
||||
.setName("Selection mode")
|
||||
.setDesc("How to select cards to display")
|
||||
.addDropdown((dropdown) =>
|
||||
dropdown
|
||||
.addOption("random", "Random")
|
||||
.addOption("least-recent", "Least Recently Viewed")
|
||||
.addOption("least-recent", "Least recently viewed")
|
||||
.setValue(this.newSettings.selectionMode)
|
||||
.onChange((value) => {
|
||||
this.newSettings.selectionMode = value as
|
||||
|
|
@ -125,13 +127,13 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Callout Type")
|
||||
.setName("Callout type")
|
||||
.setDesc(
|
||||
"Callout type for embedded cards (e.g., note, tip, warning)",
|
||||
)
|
||||
.addText((text) =>
|
||||
text
|
||||
.setPlaceholder("readaloud")
|
||||
.setPlaceholder("example")
|
||||
.setValue(this.newSettings.calloutType)
|
||||
.onChange((value) => {
|
||||
this.newSettings.calloutType = value.trim();
|
||||
|
|
@ -153,6 +155,6 @@ export class DeckNotesSettingsTab extends PluginSettingTab {
|
|||
|
||||
/** Save on exit */
|
||||
hide(): void {
|
||||
this.save();
|
||||
void this.save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2018",
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2018", "DOM"],
|
||||
"moduleResolution": "bundler",
|
||||
|
|
|
|||
Loading…
Reference in a new issue