patruusbarba_ObsidianSpoile.../main.ts
root 118866ceff Address community review warnings
- Drop plugin id/name from command id and name
- Replace builtin-modules dep with node:module builtinModules
- Commit package-lock.json for reproducible builds

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:32:21 +00:00

86 lines
2.6 KiB
TypeScript

import { Editor, MarkdownView, Notice, Plugin } from "obsidian";
/** Matches the header line of a spoiler callout, e.g. `> [!spoiler]- title`. */
const SPOILER_HEADER = /^>\s*\[!spoiler\]-?/i;
/** Matches the header line once the leading callout marker (`> `) is stripped. */
const SPOILER_HEADER_STRIPPED = /^\[!spoiler\]-?/i;
/**
* Returns true when the given selection already is a spoiler callout, i.e. its
* first non-empty line is a `> [!spoiler]` header.
*/
export function isSpoiler(text: string): boolean {
const firstLine = text.split("\n").find((line) => line.trim() !== "");
return firstLine !== undefined && SPOILER_HEADER.test(firstLine);
}
/**
* Wraps arbitrary text in a collapsible spoiler callout. Each line of the
* selection becomes a callout continuation line, so the markdown inside the
* spoiler is preserved:
*
* ```
* > [!spoiler]-
* > selected text
* ```
*/
export function wrapSpoiler(text: string): string {
const body = text
.split("\n")
.map((line) => (line === "" ? ">" : "> " + line))
.join("\n");
return "> [!spoiler]-\n" + body;
}
/**
* Reverses {@link wrapSpoiler}: removes the callout marker (`> `) from every
* line and drops the `[!spoiler]` header, returning the inner content exactly
* as it was before wrapping. The content itself is never altered, so a
* selection that contains code fences (```) round-trips unchanged.
*/
export function unwrapSpoiler(text: string): string {
// Remove the callout marker (`> ` or `>`) from every line.
const lines = text.split("\n").map((line) => line.replace(/^> ?/, ""));
// Drop the `[!spoiler]` header line (and anything before it).
let start = 0;
for (let i = 0; i < lines.length; i++) {
if (SPOILER_HEADER_STRIPPED.test(lines[i].trim())) {
start = i + 1;
break;
}
}
return lines.slice(start).join("\n");
}
export default class SpoilerPlugin extends Plugin {
async onload() {
this.addRibbonIcon("eye-off", "Toggle spoiler on selection", () => {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) {
new Notice("Open a note to toggle a spoiler.");
return;
}
this.toggleSpoiler(view.editor);
});
this.addCommand({
id: "toggle",
name: "Toggle on selection",
editorCallback: (editor) => this.toggleSpoiler(editor),
});
}
private toggleSpoiler(editor: Editor): void {
const selection = editor.getSelection();
if (selection.length === 0) {
new Notice("Select some text to toggle a spoiler.");
return;
}
const replacement = isSpoiler(selection)
? unwrapSpoiler(selection)
: wrapSpoiler(selection);
editor.replaceSelection(replacement);
}
}