diff --git a/package-lock.json b/package-lock.json index 8657d4f..8a20e6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 9cfa460..26656ce 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/copy-inline-code-view-plugin.ts b/src/copy-inline-code-view-plugin.ts index 819ff42..17ca492 100644 --- a/src/copy-inline-code-view-plugin.ts +++ b/src/copy-inline-code-view-plugin.ts @@ -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(); - 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(); + 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, + } + ); }; diff --git a/src/main.ts b/src/main.ts index bdccb70..c69856b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 = { - 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); + }); + }); + } +} diff --git a/src/regex-exclude.ts b/src/regex-exclude.ts new file mode 100644 index 0000000..a09bac6 --- /dev/null +++ b/src/regex-exclude.ts @@ -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; + } + }); +} diff --git a/src/settings.ts b/src/settings.ts index dae4036..bfbd054 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,33 +2,110 @@ 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(); + }); + 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); + }); + }); + }); + } } diff --git a/styles.css b/styles.css index 57b7ed1..d331f06 100644 --- a/styles.css +++ b/styles.css @@ -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; +}