gapmiss_blur/main.ts
@gapmiss b148d2455c fix: use document instead of activeDocument for hover class
Obsidian's new settings window is a separate document, so activeDocument
was targeting the wrong body when toggling the reveal-on-hover setting.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-30 16:39:53 -05:00

235 lines
No EOL
6.9 KiB
TypeScript

import { MarkdownPostProcessor, MarkdownPostProcessorContext, Plugin, PluginSettingTab, Setting, App, editorLivePreviewField } from 'obsidian';
import { Decoration, DecorationSet, EditorView, WidgetType, ViewPlugin, ViewUpdate } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
interface BlurPluginSettings {
revealOnHover: boolean;
}
const DEFAULT_SETTINGS: BlurPluginSettings = {
revealOnHover: false
};
type BlurType = 'blur' | 'brick' | 'bone';
class BlurWidget extends WidgetType {
constructor(
readonly content: string,
readonly type: BlurType
) {
super();
}
toDOM(): HTMLElement {
const span = createSpan({ cls: `blur-widget blur-widget-${this.type}` });
const code = span.createEl('code', { cls: this.getCssClass() });
code.setText(this.type === 'brick' ? this.content.replace(/[^\s]/g, '█') : this.content);
return span;
}
private getCssClass(): string {
switch (this.type) {
case 'blur': return 'blur-inline';
case 'brick': return 'blur-brick';
case 'bone': return 'blur-bone';
}
}
eq(other: BlurWidget): boolean {
return other.content === this.content && other.type === this.type;
}
ignoreEvent(): boolean {
return false;
}
}
const BLUR_REGEXP = /(`~\{.*?\}`|`~\[.*?\]`|`~\(.*?\)`)/gm;
const viewPlugin = ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
this.decorations = this.buildDecorations(update.view);
}
destroy() { }
buildDecorations(view: EditorView): DecorationSet {
if (!view.state.field(editorLivePreviewField)) {
return Decoration.none;
}
const builder = new RangeSetBuilder<Decoration>();
let lines: number[] = [];
if (view.state.doc.length > 0) {
lines = Array.from(
{ length: view.state.doc.lines },
(_, i) => i + 1,
);
}
const currentSelections = [...view.state.selection.ranges];
for (const n of lines) {
const line = view.state.doc.line(n);
const matches = Array.from(line.text.matchAll(BLUR_REGEXP));
for (const match of matches) {
let add = true;
const from = match.index != undefined ? match.index + line.from : -1;
const to = from + match[0].length;
if (to - from < 5) {
add = false;
}
currentSelections.forEach((r) => {
if (r.to >= from && r.from <= to) {
add = false;
}
});
if (add) {
const text = match[0];
let type: BlurType;
let content: string;
if (text.startsWith('`~{') && text.endsWith('}`')) {
type = 'blur';
content = text.slice(3, -2);
} else if (text.startsWith('`~[') && text.endsWith(']`')) {
type = 'brick';
content = text.slice(3, -2);
} else {
type = 'bone';
content = text.slice(3, -2);
}
builder.add(from, to, Decoration.replace({ widget: new BlurWidget(content, type), inclusive: true }));
}
}
}
return builder.finish();
}
}, {
decorations: (v) => v.decorations,
});
export default class BlurPlugin extends Plugin {
settings: BlurPluginSettings;
async onload(): Promise<void> {
await this.loadSettings();
this.addSettingTab(new BlurSettingTab(this.app, this));
this.applyHoverClass();
this.registerMarkdownCodeBlockProcessor("blur", (source, el, ctx) => this.blurBlockHandler(source, el, ctx));
this.registerMarkdownCodeBlockProcessor("blur-brick", (source, el, ctx) => this.blurBlockHandler(source, el, ctx));
this.registerMarkdownCodeBlockProcessor("blur-bone", (source, el, ctx) => this.blurBlockHandler(source, el, ctx));
this.registerMarkdownPostProcessor(
buildPostProcessor()
);
this.registerEditorExtension(viewPlugin);
}
onunload(): void {
// eslint-disable-next-line obsidianmd/prefer-active-doc -- must target main window
document.body.removeClass('obsidian-blur-hover');
}
async loadSettings(): Promise<void> {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<BlurPluginSettings>);
}
async saveSettings(): Promise<void> {
await this.saveData(this.settings);
}
applyHoverClass(): void {
// eslint-disable-next-line obsidianmd/prefer-active-doc -- must target main window, not settings window
const body = document.body;
if (this.settings.revealOnHover) {
body.addClass('obsidian-blur-hover');
} else {
body.removeClass('obsidian-blur-hover');
}
}
blurBlockHandler(source: string, el: HTMLElement, _ctx: MarkdownPostProcessorContext): void {
if (el.className==='block-language-blur-brick') {
const block = el.createDiv({cls: "blur-brick-block"});
const inputElement = block.createDiv({cls: "blur-brick-innerblock"});
source.split(/\W+/).forEach((w:string) => {
const word = w.trim();
if (word !== '') {
inputElement.appendChild(createEl('code', {text: word.replace(/[^\s]/g, '█'), cls: "blur-brick" }));
}
})
}
else if (el.className==='block-language-blur-bone') {
const block = el.createDiv({cls: "blur-bone-block"});
const inputElement = block.createDiv({cls: "blur-bone-innerblock"});
source.split(/\W+/).forEach((w:string) => {
let word = w.trim();
if (word !== '') {
inputElement.appendChild(createEl('code', {text: word, cls: "blur-bone"}));
}
})
}
else if (el.className==='block-language-blur') {
const block = el.createDiv({cls: "blur-block"});
const inputElement = block.createDiv({cls: "blur-innerblock"});
source.split(/\W+/).forEach((w:string) => {
let word = w.trim();
if (word !== '') {
inputElement.appendChild(createEl('code', {text: word, cls: "blur-inline"}));
}
})
}
}
}
export function buildPostProcessor(): MarkdownPostProcessor {
return (el) => {
el.findAll("code").forEach((code) => {
const text = code.innerText.trim();
if (text.startsWith('~[') && text.endsWith(']')) {
const content = text.slice(2, -1);
code.addClass('blur-brick');
code.innerText = content.replace(/[^\s]/g, '█');
}
else if (text.startsWith("~(") && text.endsWith(')')) {
const content = text.slice(2, -1);
code.addClass('blur-bone');
code.innerText = content;
}
else if (text.startsWith('~{') && text.endsWith('}')) {
const content = text.slice(2, -1);
code.addClass('blur-inline');
code.innerText = content;
}
});
};
}
class BlurSettingTab extends PluginSettingTab {
plugin: BlurPlugin;
constructor(app: App, plugin: BlurPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Reveal on hover')
.setDesc('Reveal obfuscated text when hovering with the mouse')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.revealOnHover)
.onChange(async (value) => {
this.plugin.settings.revealOnHover = value;
await this.plugin.saveSettings();
this.plugin.applyHoverClass();
}));
}
}