mirror of
https://github.com/ozavodny/obsidian-copy-inline-code-plugin.git
synced 2026-07-22 08:10:25 +00:00
Merge pull request #16 from ozavodny/feat/regex-filters
feat(#15): added regex filters to settings
This commit is contained in:
commit
e7f150a05f
7 changed files with 247 additions and 119 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
94
src/main.ts
94
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<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
13
src/regex-exclude.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
127
src/settings.ts
127
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
styles.css
14
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue