feat(#15): added regex filters to settings

This commit is contained in:
Ondrej Zavodny 2025-03-29 20:08:15 +01:00
parent 81cdf7a8e1
commit 8261b6dbab
7 changed files with 248 additions and 119 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-sample-plugin",
"version": "1.1.2",
"version": "1.3.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "obsidian-sample-plugin",
"version": "1.1.2",
"version": "1.3.0",
"license": "MIT",
"devDependencies": {
"@codemirror/language": "^6.8.0",

View file

@ -1,6 +1,6 @@
{
"name": "obsidian-sample-plugin",
"version": "1.2.0",
"version": "1.3.0",
"description": "This is a plugin for Obsidian that adds an icon inside each inline code, which when clicked, copies the content of the code into the clipboard.",
"main": "main.js",
"scripts": {

View file

@ -1,64 +1,78 @@
import { syntaxTree } from "@codemirror/language";
import { RangeSetBuilder } from "@codemirror/state";
import {
Decoration,
DecorationSet,
EditorView,
PluginSpec,
PluginValue,
ViewPlugin,
ViewUpdate,
Decoration,
DecorationSet,
EditorView,
PluginValue,
ViewPlugin,
ViewUpdate,
} from "@codemirror/view";
import { CopyWidget } from "./copy-code-widget";
import { RegexFilters, shouldExclude } from "./regex-exclude";
class CopyInlineCodeViewPlugin implements PluginValue {
decorations: DecorationSet;
showOnHover: boolean;
decorations: DecorationSet;
showOnHover: boolean;
filters: RegexFilters;
constructor(view: EditorView, showOnHover: boolean, filters: RegexFilters) {
this.showOnHover = showOnHover;
this.filters = filters;
constructor(view:EditorView, showOnHover: boolean) {
this.showOnHover = showOnHover;
this.decorations = this.buildDecorations(view);
}
this.decorations = this.buildDecorations(view);
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
update(update: ViewUpdate) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
destroy() {}
destroy() {}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const showOnHover = this.showOnHover
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith("inline-code")) {
builder.add(
node.to + 1,
node.to + 1,
Decoration.widget({
widget: new CopyWidget(showOnHover),
})
);
}
},
});
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder<Decoration>();
const showOnHover = this.showOnHover;
const filters = this.filters;
return builder.finish();
}
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter(node) {
if (node.type.name.startsWith("inline-code")) {
const codeText = view.state.doc.sliceString(
node.from,
node.to
);
if (shouldExclude(codeText, filters)) {
return;
}
builder.add(
node.to + 1,
node.to + 1,
Decoration.widget({
widget: new CopyWidget(showOnHover),
})
);
}
},
});
}
return builder.finish();
}
}
export const createCopyPlugin = (showOnHover: boolean) => {
return ViewPlugin.define(
(view: EditorView) => new CopyInlineCodeViewPlugin(view, showOnHover),
{
decorations: (p) => p.decorations,
}
);
export const createCopyPlugin = (
showOnHover: boolean,
filters: RegexFilters
) => {
return ViewPlugin.define(
(view: EditorView) =>
new CopyInlineCodeViewPlugin(view, showOnHover, filters),
{
decorations: (p) => p.decorations,
}
);
};

View file

@ -1,59 +1,79 @@
import { CopyInlineCodePluginTab } from "./settings";
import { Notice, Plugin } from 'obsidian';
import { createCopyPlugin } from './copy-inline-code-view-plugin';
import { Notice, Plugin } from "obsidian";
import { createCopyPlugin } from "./copy-inline-code-view-plugin";
import { RegexFilters, shouldExclude } from "./regex-exclude";
interface CopyInlineCodePluginSettings {
showOnHover: boolean;
showOnHover: boolean;
regexFilters: RegexFilters;
}
const DEFAULT_SETTINGS: Partial<CopyInlineCodePluginSettings> = {
showOnHover: false,
showOnHover: false,
regexFilters: [],
};
export default class CopyInlineCodePlugin extends Plugin {
settings: CopyInlineCodePluginSettings;
settings: CopyInlineCodePluginSettings;
async onload() {
await this.loadSettings();
this.addSettingTab(new CopyInlineCodePluginTab(this.app, this));
this.copyInlineCodeLogic();
}
async onload() {
await this.loadSettings();
this.addSettingTab(new CopyInlineCodePluginTab(this.app, this));
this.copyInlineCodeLogic();
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
async saveSettings() {
await this.saveData(this.settings);
}
async copyInlineCodeLogic() {
this.registerEditorExtension([createCopyPlugin(this.settings.showOnHover)]);
async copyInlineCodeLogic() {
this.registerEditorExtension([
createCopyPlugin(
this.settings.showOnHover,
this.settings.regexFilters
),
]);
this.registerMarkdownPostProcessor((element, context) => {
const inlineCodes = element.querySelectorAll("*:not(pre) > code");
inlineCodes.forEach(code => {
if(code.querySelector('span.copy-to-clipboard-icon')) {
return
inlineCodes.forEach((code) => {
if (code.querySelector("span.copy-to-clipboard-icon")) {
return;
}
const icon = createSpan({cls: "copy-to-clipboard-icon", text: "\xa0📋"})
icon.toggleClass("show-on-hover", this.settings.showOnHover)
const textToCopy = code.textContent
const textToCopy = code.textContent;
if (!textToCopy) {
return;
}
icon.onclick = (event) => {
if(textToCopy) {
if (shouldExclude(textToCopy, this.settings.regexFilters)) {
return;
}
const icon = createSpan({
cls: "copy-to-clipboard-icon",
text: "\xa0📋",
});
icon.toggleClass("show-on-hover", this.settings.showOnHover);
icon.onclick = (event) => {
if (textToCopy) {
event.stopPropagation();
navigator.clipboard.writeText(textToCopy)
navigator.clipboard.writeText(textToCopy);
new Notice(`Copied '${textToCopy}' to clipboard!`);
}
}
code.appendChild(icon)
})
})
}
}
};
code.appendChild(icon);
});
});
}
}

13
src/regex-exclude.ts Normal file
View file

@ -0,0 +1,13 @@
export type RegexFilters = [string, string][];
export function shouldExclude(text: string, regexFilters: RegexFilters) {
return (regexFilters || []).some((pattern) => {
try {
const regex = new RegExp(pattern[0], pattern[1]);
return regex.test(text);
} catch (e) {
console.error(`Invalid regex pattern: ${pattern}`);
return false;
}
});
}

View file

@ -2,33 +2,111 @@ import CopyInlineCodePlugin from "./main";
import { App, PluginSettingTab, Setting } from "obsidian";
export class CopyInlineCodePluginTab extends PluginSettingTab {
plugin: CopyInlineCodePlugin;
plugin: CopyInlineCodePlugin;
constructor(app: App, plugin: CopyInlineCodePlugin) {
super(app, plugin);
this.plugin = plugin;
}
constructor(app: App, plugin: CopyInlineCodePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("p", {
cls: "tasks-setting-important",
text: "Changing any settings requires a restart of obsidian.",
});
containerEl.empty();
containerEl.createEl('p', {
cls: 'tasks-setting-important',
text: 'Changing any settings requires a restart of obsidian.',
});
new Setting(containerEl)
.setName("Show on hover")
.setDesc("Copy icon only visible on hover (restart obsidian after change)")
.addToggle((component) => {
component
.setValue(this.plugin.settings.showOnHover)
.onChange(async (value) => {
this.plugin.settings.showOnHover = value;
await this.plugin.saveSettings();
})
});
}
new Setting(containerEl)
.setName("Show on hover")
.setDesc(
"Copy icon only visible on hover (restart obsidian after change)"
)
.addToggle((component) => {
component
.setValue(this.plugin.settings.showOnHover)
.onChange(async (value) => {
this.plugin.settings.showOnHover = value;
await this.plugin.saveSettings();
});
});
containerEl.createEl("h3", { text: "Exclusion Patterns" });
containerEl.createEl("p", {
text: "Add regex patterns to exclude code blocks from showing the copy icon. If no patterns are added, all code blocks will show the icon.",
});
const regexListContainer = containerEl.createDiv();
this.renderRegexList(regexListContainer);
new Setting(containerEl).addButton((button) => {
button.setButtonText("Add Exclusion Pattern").onClick(() => {
this.plugin.settings.regexFilters.push(["", ""]);
this.renderRegexList(regexListContainer);
});
});
}
private renderRegexList(container: HTMLElement) {
container.empty();
this.plugin.settings.regexFilters.forEach((regex, index) => {
const setting = new Setting(container);
setting
.setName(`Pattern #${index + 1}`)
.addText((text) => {
text.setValue(regex[0])
.setPlaceholder("regex pattern")
.onChange(async (value) => {
try {
new RegExp(value, "");
} catch {
text.inputEl.classList.add("regex-input-error");
return;
}
text.inputEl.classList.remove("regex-input-error");
this.plugin.settings.regexFilters[index] = [
value,
regex[1],
];
await this.plugin.saveSettings();
});
const textEl = text.inputEl;
textEl.style.width = "80%";
})
.addText((text) => {
text.setValue(regex[1])
.setPlaceholder("modifiers")
.onChange(async (value) => {
try {
new RegExp("", value);
} catch {
text.inputEl.classList.add("regex-input-error");
return;
}
text.inputEl.classList.remove("regex-input-error");
this.plugin.settings.regexFilters[index] = [
regex[0],
value,
];
await this.plugin.saveSettings();
this.renderRegexList(container);
});
const textEl = text.inputEl;
textEl.style.width = "20%";
})
.addButton((button) => {
button
.setIcon("trash")
.setClass("mod-warning")
.onClick(async () => {
this.plugin.settings.regexFilters.splice(index, 1);
await this.plugin.saveSettings();
this.renderRegexList(container);
});
});
});
}
}

View file

@ -1,22 +1,26 @@
.copy-to-clipboard-icon {
cursor: pointer
cursor: pointer;
}
.show-on-hover {
cursor: pointer;
opacity: 0;
cursor: pointer;
opacity: 0;
}
/* Source Mode */
.cm-line:hover .show-on-hover {
opacity: 1.0;
opacity: 1;
}
/* Reading Mode */
code:hover .show-on-hover {
opacity: 1.0;
opacity: 1;
}
.tasks-setting-important {
color: red;
font-weight: bold;
}
.regex-input-error {
border-color: var(--text-error) !important;
}