samuelbeard_obsidian-repert.../main.ts

170 lines
5.3 KiB
TypeScript

import {
chordForDegree,
isChordNumberToken,
normalizeCapo,
parseChordNumber,
parseKey,
type Key,
} from "degree-chords";
import { MarkdownView, Plugin } from "obsidian";
import { RepertoireSettingTab } from "./lib/settings";
interface RepertoirePluginSettings {
showKeyInBlock?: boolean;
}
const DEFAULT_SETTINGS: Partial<RepertoirePluginSettings> = {
showKeyInBlock: true,
};
export default class ObsidianRepertoirePlugin extends Plugin {
settings: RepertoirePluginSettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new RepertoireSettingTab(this.app, this));
this.registerMarkdownCodeBlockProcessor("repertoire", (source, el, ctx) => {
el.setCssProps({ fontFamily: "var(--font-monospace)" });
el.empty();
const fileCache = this.app.metadataCache.getCache(ctx.sourcePath);
const key = fileCache?.frontmatter?.Key || "C";
const capoRaw = fileCache?.frontmatter?.Capo;
const capo =
capoRaw === undefined || capoRaw === null || capoRaw === "" ? 0 : Number(capoRaw);
let parsedKey: Key;
try {
parsedKey = parseKey(key);
} catch {
const errorEl = el.createDiv();
errorEl.createEl("strong", {
text: `Error: Could not parse key "${key}"`,
});
errorEl.setCssProps({ color: "var(--text-error)" });
return;
}
let capoFret: number | undefined;
if (capo > 0) {
if (!Number.isInteger(capo)) {
const errorEl = el.createDiv();
errorEl.createEl("strong", {
text: `Error: Capo must be an integer between 0 and 11 (got ${capoRaw})`,
});
errorEl.setCssProps({ color: "var(--text-error)" });
return;
}
try {
capoFret = normalizeCapo(capo);
} catch {
const errorEl = el.createDiv();
errorEl.createEl("strong", {
text: `Error: Capo must be an integer between 0 and 11 (got ${capoRaw})`,
});
errorEl.setCssProps({ color: "var(--text-error)" });
return;
}
}
const infoEl = el.createDiv();
infoEl.setCssProps({
marginBottom: "1em",
opacity: "0.7",
fontSize: "0.9em",
});
let infoText = `Key: ${key}`;
if (capo > 0) {
infoText += ` | Capo: ${capo}`;
}
if (this.settings.showKeyInBlock) {
infoEl.setText(infoText);
}
const lines = source.split("\n");
lines.forEach((line) => {
const lineEl = el.createDiv();
lineEl.setCssProps({ whiteSpace: "pre" });
if (line.trim() === "") {
lineEl.createEl("br");
return;
}
// Use match to get an array of non-whitespace and whitespace parts
const parts = line.match(/(\s+)|(\S+)/g) || [];
parts.forEach((part) => {
const hasParen = part.startsWith("(") && part.endsWith(")");
const cleanWord = part.replace(/[.,!$%^&*;:{}=\-_`~()]/g, "");
let chord: string | null = null;
if (isChordNumberToken(cleanWord)) {
const parsed = parseChordNumber(cleanWord);
chord = chordForDegree(parsed, parsedKey, { capo: capoFret });
}
if (chord) {
const chordEl = lineEl.createEl("strong");
chordEl.setCssProps({ color: "var(--text-accent)" });
// Regex to separate the root chord from the suffix
const match = chord.match(/^([A-G][b#]?(?:m|dim|aug)?)(.*)/);
if (match && !chord.includes("/")) {
const [, root, suffix] = match;
if (hasParen) chordEl.appendText("(");
chordEl.appendText(root);
if (suffix) {
chordEl.createEl("sup", { text: suffix });
}
if (hasParen) chordEl.appendText(")");
} else {
// Fallback for slash chords or non-standard chords
chordEl.setText(hasParen ? `(${chord})` : chord);
}
} else if (part.startsWith("[") && part.endsWith("]")) {
const sectionText = part.substring(1, part.length - 1);
const sectionEl = lineEl.createEl("strong");
sectionEl.createEl("em", { text: sectionText });
sectionEl.setCssProps({ opacity: "0.7" });
} else {
lineEl.appendText(part);
}
});
});
});
this.registerEvent(
this.app.metadataCache.on("changed", (file) => {
const leaves = this.app.workspace.getLeavesOfType("markdown");
for (const leaf of leaves) {
if (leaf.view instanceof MarkdownView && leaf.view.file?.path === file.path) {
// Force the view to reload, which will re-run the code block processor
leaf.view.previewMode.rerender(true);
}
}
}),
);
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
this.rerenderAllMarkdownViews();
}
rerenderAllMarkdownViews() {
const leaves = this.app.workspace.getLeavesOfType("markdown");
for (const leaf of leaves) {
if (leaf.view instanceof MarkdownView) {
// Force the view to reload, which will re-run the code block processor
leaf.view.previewMode.rerender(true);
}
}
}
}