From 0bfb72a41d76677901dee1b4ee4095e8373558af Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 12:35:45 +0800 Subject: [PATCH 1/9] refactor: extract copy matcher definitions --- src/copyMatcher.ts | 37 +++++++++++++++++++++++++++++++++++++ src/main.ts | 22 +++------------------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 src/copyMatcher.ts diff --git a/src/copyMatcher.ts b/src/copyMatcher.ts new file mode 100644 index 0000000..1470b6e --- /dev/null +++ b/src/copyMatcher.ts @@ -0,0 +1,37 @@ +import { ContextType, EasyCopySettings } from './type'; + +export type BuiltinCopyMatcherId = + | 'bold' + | 'italic' + | 'highlight' + | 'strikethrough' + | 'inline-code' + | 'inline-latex' + | 'wiki-link'; + +export interface CopyMatcher { + id: BuiltinCopyMatcherId; + type: ContextType; + regex: RegExp; + enabled: boolean; +} + +export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] { + // iOS 16.4 之前不支持后视(Lookbehinds),但支持前视(Lookaheads) + // 所以针对 iOS 平台使用只带前视的正则表达式,其他平台使用完整版本 + const italicRegex = isIosApp ? + /(?:\*([^*]+)\*(?!\*)|_([^_]+)_(?!_))/g : + /(?:(? 斜体 > 高亮 > 删除线 > 行内代码 > 行内Latex > 块ID > 双链 - const matchers = [ - { type: ContextType.BOLD, regex: boldRegex , enable: !this.settings.customizeTargets || this.settings.enableBold}, - // 使用前后断言确保不被包裹在更长的语法中,同时支持 * 和 _ 两种格式 - { type: ContextType.ITALIC, regex: italicRegex , enable: !this.settings.customizeTargets || this.settings.enableItalic}, - { type: ContextType.HIGHLIGHT, regex: /==([^=]+)==/g , enable: !this.settings.customizeTargets || this.settings.enableHighlight}, - { type: ContextType.STRIKETHROUGH, regex: /~~([^~]+)~~/g , enable: !this.settings.customizeTargets || this.settings.enableStrikethrough}, - { type: ContextType.INLINECODE, regex: /`([^`]+)`/g , enable: !this.settings.customizeTargets || this.settings.enableInlineCode}, - { type: ContextType.INLINELATEX, regex: /\$([^$]+)\$/g , enable: !this.settings.customizeTargets || this.settings.enableInlineLatex}, - { type: ContextType.WIKILINK, regex: /\[\[([^\]]+)\]\]/g, enable: !this.settings.customizeTargets || this.settings.enableWikiLink }, - ]; + const matchers = buildBuiltinCopyMatchers(this.settings, Platform.isIosApp); for (const matcher of matchers) { - if (!matcher.enable) continue; // 如果当前类型未启用,则跳过 + if (!matcher.enabled) continue; // 如果当前类型未启用,则跳过 const matchInfo = this.getMatchInfo(beforeCursor, afterCursor, matcher.regex); if (matchInfo) { return { From 3e72472a4df46f378a5422c1761d39e9652d9897 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 12:38:39 +0800 Subject: [PATCH 2/9] feat: allow built-in matcher reordering --- src/copyMatcher.ts | 41 +++++++++++++++++++++++-------- src/i18n.ts | 18 +++++++++++++- src/main.ts | 3 ++- src/settingTab.ts | 61 +++++++++++++++++++++++++++++++++++++++++++--- src/type.ts | 13 +++++++++- 5 files changed, 120 insertions(+), 16 deletions(-) diff --git a/src/copyMatcher.ts b/src/copyMatcher.ts index 1470b6e..4977f22 100644 --- a/src/copyMatcher.ts +++ b/src/copyMatcher.ts @@ -1,13 +1,14 @@ -import { ContextType, EasyCopySettings } from './type'; +import { BuiltinCopyMatcherId, ContextType, EasyCopySettings } from './type'; -export type BuiltinCopyMatcherId = - | 'bold' - | 'italic' - | 'highlight' - | 'strikethrough' - | 'inline-code' - | 'inline-latex' - | 'wiki-link'; +export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [ + 'bold', + 'italic', + 'highlight', + 'strikethrough', + 'inline-code', + 'inline-latex', + 'wiki-link', +]; export interface CopyMatcher { id: BuiltinCopyMatcherId; @@ -16,6 +17,23 @@ export interface CopyMatcher { enabled: boolean; } +export function normalizeBuiltinMatcherOrder(order: readonly string[] | undefined): BuiltinCopyMatcherId[] { + const knownIds = new Set(DEFAULT_BUILTIN_COPY_MATCHER_IDS); + const orderedIds: BuiltinCopyMatcherId[] = []; + + for (const id of order ?? []) { + if (knownIds.has(id) && !orderedIds.includes(id as BuiltinCopyMatcherId)) { + orderedIds.push(id as BuiltinCopyMatcherId); + } + } + + for (const id of DEFAULT_BUILTIN_COPY_MATCHER_IDS) { + if (!orderedIds.includes(id)) orderedIds.push(id); + } + + return orderedIds; +} + export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] { // iOS 16.4 之前不支持后视(Lookbehinds),但支持前视(Lookaheads) // 所以针对 iOS 平台使用只带前视的正则表达式,其他平台使用完整版本 @@ -25,7 +43,7 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b const boldRegex = /(?:\*\*([^*]+)\*\*|__([^_]+)__)/g; - return [ + const matchers: CopyMatcher[] = [ { id: 'bold', type: ContextType.BOLD, regex: boldRegex, enabled: !settings.customizeTargets || settings.enableBold }, { id: 'italic', type: ContextType.ITALIC, regex: italicRegex, enabled: !settings.customizeTargets || settings.enableItalic }, { id: 'highlight', type: ContextType.HIGHLIGHT, regex: /==([^=]+)==/g, enabled: !settings.customizeTargets || settings.enableHighlight }, @@ -34,4 +52,7 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b { id: 'inline-latex', type: ContextType.INLINELATEX, regex: /\$([^$]+)\$/g, enabled: !settings.customizeTargets || settings.enableInlineLatex }, { id: 'wiki-link', type: ContextType.WIKILINK, regex: /\[\[([^\]]+)\]\]/g, enabled: !settings.customizeTargets || settings.enableWikiLink }, ]; + + const matcherById = new Map(matchers.map(matcher => [matcher.id, matcher])); + return normalizeBuiltinMatcherOrder(settings.matcherOrder).map(id => matcherById.get(id)!); } diff --git a/src/i18n.ts b/src/i18n.ts index d6208a3..a56193c 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -49,7 +49,8 @@ export type TranslationKey = | 'error-block-id-empty' | 'error-block-id-invalid' | 'enable-display-name-regex' | 'enable-display-name-regex-desc' | 'display-name-regex-from' | 'display-name-regex-from-desc' - | 'display-name-regex-to' | 'display-name-regex-to-desc'; + | 'display-name-regex-to' | 'display-name-regex-to-desc' + | 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order'; // 本地化翻译字典 export const translations: Record> = { @@ -172,6 +173,11 @@ export const translations: Record> = { 'display-name-regex-from-desc': 'Regular expression pattern to match in the display text (e.g. ^\\d+\\.\\s* to remove leading numbers)', 'display-name-regex-to': 'Replace with', 'display-name-regex-to-desc': 'Replacement string, supports capture groups like $1, $2 (e.g. $1 to keep only the first captured group)', + 'matcher-priority': 'Copy target priority', + 'matcher-priority-desc': 'When multiple targets match the cursor, targets higher in this list are copied first.', + 'move-up': 'Move up', + 'move-down': 'Move down', + 'reset-order': 'Reset order', }, [Language.ZH]: { // 复制 Block ID @@ -290,6 +296,11 @@ export const translations: Record> = { 'display-name-regex-from-desc': '用于匹配显示文本的正则表达式(如 ^\\d+\\.\\s* 可去除开头的序号)', 'display-name-regex-to': '替换为', 'display-name-regex-to-desc': '替换字符串,支持捕获组引用如 $1、$2(如 $1 只保留第一个捕获组的内容)', + 'matcher-priority': '复制对象优先级', + 'matcher-priority-desc': '当多种复制对象同时匹配光标位置时,列表中更靠上的对象会优先生效。', + 'move-up': '上移', + 'move-down': '下移', + 'reset-order': '重置顺序', }, [Language.ZH_TW]: { // 复制 Block ID @@ -409,6 +420,11 @@ export const translations: Record> = { 'display-name-regex-from-desc': '用於匹配顯示文本的正規表示式(如 ^\\d+\\.\\s* 可去除開頭的序號)', 'display-name-regex-to': '替換為', 'display-name-regex-to-desc': '替換字符串,支持捕獲組引用如 $1、$2', + 'matcher-priority': '複製對象優先級', + 'matcher-priority-desc': '當多種複製對象同時匹配游標位置時,列表中更靠上的對象會優先生效。', + 'move-up': '上移', + 'move-down': '下移', + 'reset-order': '重置順序', } }; diff --git a/src/main.ts b/src/main.ts index 41a984e..9c244e3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkForma import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; import { detectCodeBlockFromLines } from './codeBlockDetect'; -import { buildBuiltinCopyMatchers } from './copyMatcher'; +import { buildBuiltinCopyMatchers, normalizeBuiltinMatcherOrder } from './copyMatcher'; import { buildHeadingLink, buildBlockLink, buildFileLink, buildExplicitPasteLink } from './linkBuilder'; import { CopyMetadata, buildBlockCopyMetadata, buildHeadingCopyMetadata, buildFileCopyMetadata } from './copyMetadata'; import { decidePasteResolution, shouldOmitAliasForSameFile, shouldRegisterPasteHandler } from './pasteResolution'; @@ -161,6 +161,7 @@ export default class EasyCopy extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + this.settings.matcherOrder = normalizeBuiltinMatcherOrder(this.settings.matcherOrder); } async saveSettings() { diff --git a/src/settingTab.ts b/src/settingTab.ts index d0535c3..30eed51 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -7,12 +7,23 @@ import { } from "obsidian"; import * as ObsidianModule from "obsidian"; import EasyCopy from "./main"; -import { LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; +import { BuiltinCopyMatcherId, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; +import { DEFAULT_BUILTIN_COPY_MATCHER_IDS, normalizeBuiltinMatcherOrder } from "./copyMatcher"; interface SettingsContainer { addSetting(cb: (setting: Setting) => void): void; } +const BUILTIN_MATCHER_LABEL_KEYS: Record = { + 'bold': 'enable-bold', + 'italic': 'enable-italic', + 'highlight': 'enable-highlight', + 'strikethrough': 'enable-strikethrough', + 'inline-code': 'enable-inline-code', + 'inline-latex': 'enable-inline-latex', + 'wiki-link': 'enable-wikilink', +}; + function createSettingsGroup(containerEl: HTMLElement, heading?: string): SettingsContainer { // Check if SettingGroup is available (API 1.11.0+) // requireApiVersion is the official Obsidian API method for version checking @@ -345,6 +356,8 @@ export class EasyCopySettingTab extends PluginSettingTab { // 只有当自定义复制对象选项开启时才显示具体的复制对象选项 if (this.plugin.settings.customizeTargets) { + this.plugin.settings.matcherOrder = normalizeBuiltinMatcherOrder(this.plugin.settings.matcherOrder); + targetGroup.addSetting(setting => setting .setName(this.plugin.t('enable-inline-code')) .setDesc(this.plugin.t('enable-inline-code-desc')) @@ -424,8 +437,38 @@ export class EasyCopySettingTab extends PluginSettingTab { this.plugin.settings.enableWikiLink = value; void this.plugin.saveSettings(); this.display(); // 切换后刷新界面以显示/隐藏下方选项 - }))); - + }))); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('matcher-priority')) + .setDesc(this.plugin.t('matcher-priority-desc')) + .addButton(button => button + .setButtonText(this.plugin.t('reset-order')) + .onClick(() => { + this.plugin.settings.matcherOrder = [...DEFAULT_BUILTIN_COPY_MATCHER_IDS]; + void this.plugin.saveSettings(); + this.display(); + }))); + + this.plugin.settings.matcherOrder.forEach((matcherId, index) => { + targetGroup.addSetting(setting => setting + .setName(this.plugin.t(BUILTIN_MATCHER_LABEL_KEYS[matcherId])) + .addButton(button => button + .setButtonText('↑') + .setTooltip(this.plugin.t('move-up')) + .setDisabled(index === 0) + .onClick(() => { + this.moveMatcher(matcherId, -1); + })) + .addButton(button => button + .setButtonText('↓') + .setTooltip(this.plugin.t('move-down')) + .setDisabled(index === this.plugin.settings.matcherOrder.length - 1) + .onClick(() => { + this.moveMatcher(matcherId, 1); + }))); + }); + } targetGroup.addSetting(setting => setting @@ -514,4 +557,16 @@ export class EasyCopySettingTab extends PluginSettingTab { }))); } } + + private moveMatcher(matcherId: BuiltinCopyMatcherId, direction: -1 | 1): void { + const matcherOrder = normalizeBuiltinMatcherOrder(this.plugin.settings.matcherOrder); + const currentIndex = matcherOrder.indexOf(matcherId); + const nextIndex = currentIndex + direction; + if (nextIndex < 0 || nextIndex >= matcherOrder.length) return; + + [matcherOrder[currentIndex], matcherOrder[nextIndex]] = [matcherOrder[nextIndex], matcherOrder[currentIndex]]; + this.plugin.settings.matcherOrder = matcherOrder; + void this.plugin.saveSettings(); + this.display(); + } } diff --git a/src/type.ts b/src/type.ts index 404b868..6d471f3 100644 --- a/src/type.ts +++ b/src/type.ts @@ -34,6 +34,15 @@ export enum ContextType { CODEBLOCK = 'code-block', // 光标在代码块内 } +export type BuiltinCopyMatcherId = + | 'bold' + | 'italic' + | 'highlight' + | 'strikethrough' + | 'inline-code' + | 'inline-latex' + | 'wiki-link'; + export interface ContextData { type: ContextType; curLine: string; @@ -62,6 +71,7 @@ export interface EasyCopySettings { enableInlineLatex: boolean; enableLink: boolean; enableWikiLink: boolean; // 是否启用 Wiki 链接复制 + matcherOrder: BuiltinCopyMatcherId[]; // 复制对象匹配优先级 keepWikiBrackets: boolean; // 复制 wiki-link 时保留 [[ ]] autoEmbedBlockLink: boolean; // 复制块链接时自动添加 !(嵌入块) enableCalloutCopy: boolean; // 是否启用复制 Callout 内文本 @@ -99,6 +109,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { enableInlineLatex: true, enableLink: true, enableWikiLink: true, + matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'wiki-link'], keepWikiBrackets: true, autoEmbedBlockLink: false, enableCalloutCopy: true, @@ -113,4 +124,4 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { enableDisplayNameRegex: false, displayNameRegexFrom: '', displayNameRegexTo: '', -} \ No newline at end of file +} From 373b7da8b84ee787e2badb42bd13e7061cf3751b Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 12:42:37 +0800 Subject: [PATCH 3/9] feat: add custom regex copy matchers --- src/copyMatcher.test.ts | 107 ++++++++++++++++++++++++++++++++++++++++ src/copyMatcher.ts | 95 ++++++++++++++++++++++++++++++++++- src/i18n.ts | 36 +++++++++++++- src/main.ts | 42 +++++----------- src/settingTab.ts | 104 +++++++++++++++++++++++++++++++++++--- src/type.ts | 15 +++++- 6 files changed, 357 insertions(+), 42 deletions(-) create mode 100644 src/copyMatcher.test.ts diff --git a/src/copyMatcher.test.ts b/src/copyMatcher.test.ts new file mode 100644 index 0000000..cb416c2 --- /dev/null +++ b/src/copyMatcher.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from 'vitest'; +import { + buildCopyMatchers, + DEFAULT_BUILTIN_COPY_MATCHER_IDS, + getCustomMatcherOrderId, + normalizeMatcherOrder, +} from './copyMatcher'; +import { ContextType, DEFAULT_SETTINGS } from './type'; + +describe('normalizeMatcherOrder', () => { + it('keeps saved order and appends missing built-in and custom matchers', () => { + const customMatcher = { + id: 'quotes', + name: 'Quotes', + pattern: '"([^"]+)"', + flags: 'g', + captureGroup: 1, + enabled: true, + }; + + expect(normalizeMatcherOrder(['inline-code', 'bold'], [customMatcher])).toEqual([ + 'inline-code', + 'bold', + 'italic', + 'highlight', + 'strikethrough', + 'inline-latex', + 'wiki-link', + getCustomMatcherOrderId(customMatcher), + ]); + }); + + it('drops unknown and duplicate matcher ids', () => { + expect(normalizeMatcherOrder(['missing', 'bold', 'bold'], [])).toEqual(DEFAULT_BUILTIN_COPY_MATCHER_IDS); + }); +}); + +describe('buildCopyMatchers', () => { + it('orders custom regex matchers with built-in matchers', () => { + const customMatcher = { + id: 'quotes', + name: 'Quotes', + pattern: '"([^"]+)"', + flags: 'g', + captureGroup: 1, + enabled: true, + }; + const settings = { + ...DEFAULT_SETTINGS, + customizeTargets: true, + customMatchers: [customMatcher], + matcherOrder: [getCustomMatcherOrderId(customMatcher), ...DEFAULT_BUILTIN_COPY_MATCHER_IDS], + }; + + const matchers = buildCopyMatchers(settings, false); + + expect(matchers[0]).toMatchObject({ + id: getCustomMatcherOrderId(customMatcher), + type: ContextType.CUSTOM, + enabled: true, + captureGroup: 1, + name: 'Quotes', + }); + expect(matchers[0].regex.exec('copy "inside"')?.[1]).toBe('inside'); + }); + + it('skips invalid custom regex without removing valid matchers', () => { + const settings = { + ...DEFAULT_SETTINGS, + customizeTargets: true, + customMatchers: [{ + id: 'broken', + name: 'Broken', + pattern: '(', + flags: 'g', + captureGroup: 1, + enabled: true, + }], + matcherOrder: ['custom:broken', ...DEFAULT_BUILTIN_COPY_MATCHER_IDS], + }; + + const matchers = buildCopyMatchers(settings, false); + + expect(matchers.some(matcher => matcher.id === 'custom:broken')).toBe(false); + expect(matchers[0].id).toBe('bold'); + }); + + it('disables custom regex matchers when target customization is off', () => { + const settings = { + ...DEFAULT_SETTINGS, + customizeTargets: false, + customMatchers: [{ + id: 'quotes', + name: 'Quotes', + pattern: '"([^"]+)"', + flags: 'g', + captureGroup: 1, + enabled: true, + }], + matcherOrder: ['custom:quotes', ...DEFAULT_BUILTIN_COPY_MATCHER_IDS], + }; + + const customMatcher = buildCopyMatchers(settings, false).find(matcher => matcher.id === 'custom:quotes'); + + expect(customMatcher?.enabled).toBe(false); + }); +}); diff --git a/src/copyMatcher.ts b/src/copyMatcher.ts index 4977f22..c3c562d 100644 --- a/src/copyMatcher.ts +++ b/src/copyMatcher.ts @@ -1,4 +1,4 @@ -import { BuiltinCopyMatcherId, ContextType, EasyCopySettings } from './type'; +import { BuiltinCopyMatcherId, ContextType, CustomCopyMatcherSetting, EasyCopySettings } from './type'; export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [ 'bold', @@ -11,10 +11,21 @@ export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [ ]; export interface CopyMatcher { - id: BuiltinCopyMatcherId; + id: string; type: ContextType; regex: RegExp; enabled: boolean; + captureGroup?: number; + name?: string; +} + +export interface CopyMatchInfo { + content: string; + range: [number, number]; +} + +export function getCustomMatcherOrderId(matcher: CustomCopyMatcherSetting): string { + return `custom:${matcher.id}`; } export function normalizeBuiltinMatcherOrder(order: readonly string[] | undefined): BuiltinCopyMatcherId[] { @@ -34,6 +45,74 @@ export function normalizeBuiltinMatcherOrder(order: readonly string[] | undefine return orderedIds; } +export function normalizeMatcherOrder(order: readonly string[] | undefined, customMatchers: readonly CustomCopyMatcherSetting[]): string[] { + const customIds = new Set(customMatchers.map(getCustomMatcherOrderId)); + const knownIds = new Set([...DEFAULT_BUILTIN_COPY_MATCHER_IDS, ...customIds]); + const orderedIds: string[] = []; + + for (const id of order ?? []) { + if (knownIds.has(id) && !orderedIds.includes(id)) orderedIds.push(id); + } + + for (const id of DEFAULT_BUILTIN_COPY_MATCHER_IDS) { + if (!orderedIds.includes(id)) orderedIds.push(id); + } + + for (const id of customIds) { + if (!orderedIds.includes(id)) orderedIds.push(id); + } + + return orderedIds; +} + +function buildCustomCopyMatcher(setting: CustomCopyMatcherSetting, enabled: boolean): CopyMatcher | null { + if (!setting.pattern.trim()) return null; + + try { + const flags = Array.from(new Set(`${setting.flags || ''}g`)).join(''); + return { + id: getCustomMatcherOrderId(setting), + type: ContextType.CUSTOM, + regex: new RegExp(setting.pattern, flags), + enabled: enabled && setting.enabled, + captureGroup: setting.captureGroup, + name: setting.name.trim() || 'Custom matcher', + }; + } catch { + return null; + } +} + +export function getMatcherContent(match: RegExpExecArray, captureGroup?: number): string { + if (captureGroup !== undefined) return match[captureGroup] ?? match[0]; + + for (let i = 1; i < match.length; i++) { + if (match[i] !== undefined) return match[i]; + } + + return match[0]; +} + +export function getMatchInfo(fullText: string, cursorPosition: number, regex: RegExp, captureGroup?: number): CopyMatchInfo | null { + let match: RegExpExecArray | null; + regex.lastIndex = 0; + while ((match = regex.exec(fullText)) !== null) { + const matchStart = match.index; + const matchEnd = match.index + match[0].length; + + if (cursorPosition >= matchStart && cursorPosition <= matchEnd) { + return { + content: getMatcherContent(match, captureGroup), + range: [matchStart, matchEnd], + }; + } + + if (match[0].length === 0) regex.lastIndex++; + } + + return null; +} + export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] { // iOS 16.4 之前不支持后视(Lookbehinds),但支持前视(Lookaheads) // 所以针对 iOS 平台使用只带前视的正则表达式,其他平台使用完整版本 @@ -56,3 +135,15 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b const matcherById = new Map(matchers.map(matcher => [matcher.id, matcher])); return normalizeBuiltinMatcherOrder(settings.matcherOrder).map(id => matcherById.get(id)!); } + +export function buildCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] { + const matchers = [ + ...buildBuiltinCopyMatchers(settings, isIosApp), + ...settings.customMatchers.map(matcher => buildCustomCopyMatcher(matcher, settings.customizeTargets)).filter(matcher => matcher !== null), + ]; + const matcherById = new Map(matchers.map(matcher => [matcher.id, matcher])); + + return normalizeMatcherOrder(settings.matcherOrder, settings.customMatchers) + .map(id => matcherById.get(id)) + .filter(matcher => matcher !== undefined); +} diff --git a/src/i18n.ts b/src/i18n.ts index a56193c..2a557a3 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -11,7 +11,7 @@ export type TranslationKey = | 'inline-code-copied' | 'block-id-copied' | 'note-link-copied' | 'heading-copied' | 'strikethrough-copied' | 'inline-latex-copied' | 'bold-copied' | 'highlight-copied' | 'italic-copied' | 'link-text-copied' | 'link-url-copied' | 'wiki-link-copied' | 'callout-copied' | 'note-link-simplified' - | 'code-block-copied' + | 'code-block-copied' | 'custom-matcher' | 'custom-matcher-copied' | 'format' | 'add-to-menu' | 'add-to-menu-desc' | 'show-notice' | 'show-notice-desc' | 'add-extra-commands' | 'add-extra-commands-desc' | 'use-heading-as-display' | 'use-heading-as-display-desc' | 'heading-link-separator' | 'heading-link-separator-desc' | 'block-id' @@ -50,7 +50,9 @@ export type TranslationKey = | 'enable-display-name-regex' | 'enable-display-name-regex-desc' | 'display-name-regex-from' | 'display-name-regex-from-desc' | 'display-name-regex-to' | 'display-name-regex-to-desc' - | 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order'; + | 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order' + | 'custom-matchers' | 'custom-matchers-desc' | 'add-custom-matcher' | 'delete-custom-matcher' + | 'custom-matcher-name' | 'custom-matcher-pattern' | 'custom-matcher-flags' | 'custom-matcher-capture-group'; // 本地化翻译字典 export const translations: Record> = { @@ -87,6 +89,8 @@ export const translations: Record> = { 'wiki-link-copied': 'Wiki link copied!', 'callout-copied': 'Callout copied!', 'code-block-copied': 'Code block copied!', + 'custom-matcher': 'Custom matcher', + 'custom-matcher-copied': 'copied!', 'note-link-simplified': 'Link simplified (filename matches heading)', // 设置界面 @@ -178,6 +182,14 @@ export const translations: Record> = { 'move-up': 'Move up', 'move-down': 'Move down', 'reset-order': 'Reset order', + 'custom-matchers': 'Custom regex targets', + 'custom-matchers-desc': 'Add current-line regex rules. Use capture group 1 to copy the text inside quotes or brackets.', + 'add-custom-matcher': 'Add custom target', + 'delete-custom-matcher': 'Delete', + 'custom-matcher-name': 'Name', + 'custom-matcher-pattern': 'Regex', + 'custom-matcher-flags': 'Flags', + 'custom-matcher-capture-group': 'Capture group', }, [Language.ZH]: { // 复制 Block ID @@ -211,6 +223,8 @@ export const translations: Record> = { 'wiki-link-copied': 'Wiki链接已复制!', 'callout-copied': '标注内容已复制!', 'code-block-copied': '代码块已复制!', + 'custom-matcher': '自定义规则', + 'custom-matcher-copied': '已复制!', 'note-link-simplified': '链接已简化(文件名与标题相匹配)', // 设置界面 @@ -301,6 +315,14 @@ export const translations: Record> = { 'move-up': '上移', 'move-down': '下移', 'reset-order': '重置顺序', + 'custom-matchers': '自定义正则复制对象', + 'custom-matchers-desc': '添加仅匹配当前行的正则规则。可用捕获组 1 复制引号或括号内部文本。', + 'add-custom-matcher': '添加自定义对象', + 'delete-custom-matcher': '删除', + 'custom-matcher-name': '名称', + 'custom-matcher-pattern': '正则', + 'custom-matcher-flags': '标志', + 'custom-matcher-capture-group': '捕获组', }, [Language.ZH_TW]: { // 复制 Block ID @@ -401,6 +423,8 @@ export const translations: Record> = { 'code-block-generate-block-link': '生成塊連結', 'code-block-disabled': '禁用', 'code-block-copied': '代碼塊已複製!', + 'custom-matcher': '自定義規則', + 'custom-matcher-copied': '已複製!', 'keep-wiki-brackets': 'Wiki連結:保留 [[ ]] 括號', 'keep-wiki-brackets-desc': '複製 wiki 連結時保留兩側 [[ ]] 括號', @@ -425,6 +449,14 @@ export const translations: Record> = { 'move-up': '上移', 'move-down': '下移', 'reset-order': '重置順序', + 'custom-matchers': '自定義正則複製對象', + 'custom-matchers-desc': '添加僅匹配當前行的正則規則。可用捕獲組 1 複製引號或括號內部文本。', + 'add-custom-matcher': '添加自定義對象', + 'delete-custom-matcher': '刪除', + 'custom-matcher-name': '名稱', + 'custom-matcher-pattern': '正則', + 'custom-matcher-flags': '標誌', + 'custom-matcher-capture-group': '捕獲組', } }; diff --git a/src/main.ts b/src/main.ts index 9c244e3..59cc78b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkForma import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; import { detectCodeBlockFromLines } from './codeBlockDetect'; -import { buildBuiltinCopyMatchers, normalizeBuiltinMatcherOrder } from './copyMatcher'; +import { buildCopyMatchers, getMatchInfo, normalizeMatcherOrder } from './copyMatcher'; import { buildHeadingLink, buildBlockLink, buildFileLink, buildExplicitPasteLink } from './linkBuilder'; import { CopyMetadata, buildBlockCopyMetadata, buildHeadingCopyMetadata, buildFileCopyMetadata } from './copyMetadata'; import { decidePasteResolution, shouldOmitAliasForSameFile, shouldRegisterPasteHandler } from './pasteResolution'; @@ -161,7 +161,8 @@ export default class EasyCopy extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - this.settings.matcherOrder = normalizeBuiltinMatcherOrder(this.settings.matcherOrder); + this.settings.customMatchers = this.settings.customMatchers ?? []; + this.settings.matcherOrder = normalizeMatcherOrder(this.settings.matcherOrder, this.settings.customMatchers); } async saveSettings() { @@ -417,17 +418,18 @@ export default class EasyCopy extends Plugin { // 匹配优先级:加粗 > 斜体 > 高亮 > 删除线 > 行内代码 > 行内Latex > 块ID > 双链 - const matchers = buildBuiltinCopyMatchers(this.settings, Platform.isIosApp); + const matchers = buildCopyMatchers(this.settings, Platform.isIosApp); for (const matcher of matchers) { if (!matcher.enabled) continue; // 如果当前类型未启用,则跳过 - const matchInfo = this.getMatchInfo(beforeCursor, afterCursor, matcher.regex); + const matchInfo = getMatchInfo(beforeCursor + afterCursor, beforeCursor.length, matcher.regex, matcher.captureGroup); if (matchInfo) { return { type: matcher.type, curLine, match: matchInfo.content, // 返回内容,不包括语法 range: matchInfo.range, + matcherName: matcher.name, }; } } @@ -489,32 +491,6 @@ export default class EasyCopy extends Plugin { * @param regex 匹配的正则表达式 * @returns 匹配信息,包括匹配内容和范围 */ - private getMatchInfo(beforeCursor: string, afterCursor: string, regex: RegExp): { content: string; range: [number, number] } | null { - let match; - while ((match = regex.exec(beforeCursor + afterCursor)) !== null) { - const matchStart = match.index; - const matchEnd = match.index + match[0].length; - - // 判断光标是否在匹配范围内 - if (beforeCursor.length >= matchStart && beforeCursor.length <= matchEnd) { - // 找到第一个非空且非整体匹配的捕获组作为内容 - let content = ''; - for (let i = 1; i < match.length; i++) { - if (match[i] !== undefined) { - content = match[i]; - break; - } - } - - return { - content: content, // 返回内容,不包括语法 - range: [matchStart, matchEnd], - }; - } - } - return null; - } - private isCursorInLink(beforeCursor: string, afterCursor: string): {type: ContextType.LINKTITLE | ContextType.LINEURL, content: string, range: [number, number]} | null { // 匹配链接的正则表达式 const linkRegex = /\[([^\]]*?)\]\(([^)]*?)\)/g; @@ -623,6 +599,12 @@ export default class EasyCopy extends Plugin { new Notice(this.t('inline-latex-copied')); } return; + case ContextType.CUSTOM: + void navigator.clipboard.writeText(contextType.match!); + if (this.settings.showNotice) { + new Notice(`${contextType.matcherName ?? this.t('custom-matcher')} ${this.t('custom-matcher-copied')}`); + } + return; case ContextType.LINKTITLE: // 复制链接标题 diff --git a/src/settingTab.ts b/src/settingTab.ts index 30eed51..7e794b8 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -7,8 +7,8 @@ import { } from "obsidian"; import * as ObsidianModule from "obsidian"; import EasyCopy from "./main"; -import { BuiltinCopyMatcherId, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; -import { DEFAULT_BUILTIN_COPY_MATCHER_IDS, normalizeBuiltinMatcherOrder } from "./copyMatcher"; +import { BuiltinCopyMatcherId, CustomCopyMatcherSetting, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; +import { DEFAULT_BUILTIN_COPY_MATCHER_IDS, getCustomMatcherOrderId, normalizeMatcherOrder } from "./copyMatcher"; interface SettingsContainer { addSetting(cb: (setting: Setting) => void): void; @@ -24,6 +24,18 @@ const BUILTIN_MATCHER_LABEL_KEYS: Record setting .setName(this.plugin.t('enable-inline-code')) @@ -445,14 +457,17 @@ export class EasyCopySettingTab extends PluginSettingTab { .addButton(button => button .setButtonText(this.plugin.t('reset-order')) .onClick(() => { - this.plugin.settings.matcherOrder = [...DEFAULT_BUILTIN_COPY_MATCHER_IDS]; + this.plugin.settings.matcherOrder = normalizeMatcherOrder(DEFAULT_BUILTIN_COPY_MATCHER_IDS, this.plugin.settings.customMatchers); void this.plugin.saveSettings(); this.display(); }))); this.plugin.settings.matcherOrder.forEach((matcherId, index) => { + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + const matcherName = customMatcher ? customMatcher.name : this.plugin.t(BUILTIN_MATCHER_LABEL_KEYS[matcherId as BuiltinCopyMatcherId]); + targetGroup.addSetting(setting => setting - .setName(this.plugin.t(BUILTIN_MATCHER_LABEL_KEYS[matcherId])) + .setName(matcherName) .addButton(button => button .setButtonText('↑') .setTooltip(this.plugin.t('move-up')) @@ -468,6 +483,74 @@ export class EasyCopySettingTab extends PluginSettingTab { this.moveMatcher(matcherId, 1); }))); }); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matchers')) + .setDesc(this.plugin.t('custom-matchers-desc')) + .addButton(button => button + .setButtonText(this.plugin.t('add-custom-matcher')) + .onClick(() => { + const customMatcher = createCustomMatcher(); + this.plugin.settings.customMatchers.push(customMatcher); + this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + this.display(); + }))); + + for (const customMatcher of this.plugin.settings.customMatchers) { + targetGroup.addSetting(setting => setting + .setName(customMatcher.name || this.plugin.t('custom-matcher')) + .addToggle(toggle => toggle + .setValue(customMatcher.enabled) + .onChange(value => { + customMatcher.enabled = value; + void this.plugin.saveSettings(); + })) + .addButton(button => button + .setButtonText(this.plugin.t('delete-custom-matcher')) + .onClick(() => { + this.deleteCustomMatcher(customMatcher.id); + }))); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matcher-name')) + .addText(text => text + .setValue(customMatcher.name) + .onChange(value => { + customMatcher.name = value; + void this.plugin.saveSettings(); + }))); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matcher-pattern')) + .addText(text => text + .setPlaceholder('"([^"]+)"') + .setValue(customMatcher.pattern) + .onChange(value => { + customMatcher.pattern = value; + void this.plugin.saveSettings(); + }))); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matcher-flags')) + .addText(text => text + .setPlaceholder('g') + .setValue(customMatcher.flags) + .onChange(value => { + customMatcher.flags = value; + void this.plugin.saveSettings(); + }))); + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matcher-capture-group')) + .addText(text => text + .setPlaceholder('1') + .setValue(String(customMatcher.captureGroup)) + .onChange(value => { + customMatcher.captureGroup = Math.max(0, parseInt(value) || 0); + void this.plugin.saveSettings(); + }))); + } } @@ -558,8 +641,8 @@ export class EasyCopySettingTab extends PluginSettingTab { } } - private moveMatcher(matcherId: BuiltinCopyMatcherId, direction: -1 | 1): void { - const matcherOrder = normalizeBuiltinMatcherOrder(this.plugin.settings.matcherOrder); + private moveMatcher(matcherId: string, direction: -1 | 1): void { + const matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); const currentIndex = matcherOrder.indexOf(matcherId); const nextIndex = currentIndex + direction; if (nextIndex < 0 || nextIndex >= matcherOrder.length) return; @@ -569,4 +652,11 @@ export class EasyCopySettingTab extends PluginSettingTab { void this.plugin.saveSettings(); this.display(); } + + private deleteCustomMatcher(id: string): void { + this.plugin.settings.customMatchers = this.plugin.settings.customMatchers.filter(matcher => matcher.id !== id); + this.plugin.settings.matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + this.display(); + } } diff --git a/src/type.ts b/src/type.ts index 6d471f3..19a6a27 100644 --- a/src/type.ts +++ b/src/type.ts @@ -30,6 +30,7 @@ export enum ContextType { LINKTITLE = 'link-title', LINEURL = 'line-url', WIKILINK = 'wiki-link', // 光标在 [[双链]] 内 + CUSTOM = 'custom', // 用户自定义正则复制对象 CALLOUT = 'callout', // 光标在 callout 区块内 CODEBLOCK = 'code-block', // 光标在代码块内 } @@ -48,6 +49,16 @@ export interface ContextData { curLine: string; match: string | null; range: [number, number] | null; + matcherName?: string; +} + +export interface CustomCopyMatcherSetting { + id: string; + name: string; + pattern: string; + flags: string; + captureGroup: number; + enabled: boolean; } export interface EasyCopySettings { @@ -71,7 +82,8 @@ export interface EasyCopySettings { enableInlineLatex: boolean; enableLink: boolean; enableWikiLink: boolean; // 是否启用 Wiki 链接复制 - matcherOrder: BuiltinCopyMatcherId[]; // 复制对象匹配优先级 + matcherOrder: string[]; // 复制对象匹配优先级 + customMatchers: CustomCopyMatcherSetting[]; // 用户自定义正则复制对象 keepWikiBrackets: boolean; // 复制 wiki-link 时保留 [[ ]] autoEmbedBlockLink: boolean; // 复制块链接时自动添加 !(嵌入块) enableCalloutCopy: boolean; // 是否启用复制 Callout 内文本 @@ -110,6 +122,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { enableLink: true, enableWikiLink: true, matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'wiki-link'], + customMatchers: [], keepWikiBrackets: true, autoEmbedBlockLink: false, enableCalloutCopy: true, From cdb9e0800d4759e852045110d2a75edb589c222a Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 12:44:54 +0800 Subject: [PATCH 4/9] chore: clean up main lint warnings --- src/main.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.ts b/src/main.ts index 59cc78b..04859df 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { Editor, MarkdownView, Notice, Plugin, Menu, Platform, MarkdownFileInfo, TFile, getLanguage } from 'obsidian'; import { Language, TranslationKey, I18n } from './i18n'; -import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from './type'; +import { ContextData, ContextType, DEFAULT_SETTINGS, EasyCopySettings, LinkFormat, BlockIdInsertPosition } from './type'; import { EasyCopySettingTab } from './settingTab'; import { BlockIdInputModal } from './blockIdModal'; import { detectCodeBlockFromLines } from './codeBlockDetect'; @@ -333,7 +333,7 @@ export default class EasyCopy extends Plugin { /* * 从给定的块里查找 Block ID(最多延伸至下一个空行+下第二行) */ - private detectBlockId(editor: Editor, view: MarkdownView): ContextData | null { + private detectBlockId(editor: Editor): ContextData | null { const cursor = editor.getCursor(); const { end } = this.detectBlockRange(editor, cursor.line); let lastLine = editor.getLine(end); @@ -454,7 +454,7 @@ export default class EasyCopy extends Plugin { } // 检测 block ID - const blockIdInfo = this.detectBlockId(editor, view); + const blockIdInfo = this.detectBlockId(editor); if (blockIdInfo) { return blockIdInfo; } From 23f330914d16a701a2dada74077ff9373f2fbd63 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 13:11:30 +0800 Subject: [PATCH 5/9] fix: simplify matcher settings list --- src/i18n.ts | 57 +++--- src/main.ts | 5 +- src/settingTab.ts | 440 ++++++++++++++++++++++++++-------------------- src/type.ts | 1 + styles.css | 27 ++- 5 files changed, 314 insertions(+), 216 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 2a557a3..52c688d 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -52,7 +52,8 @@ export type TranslationKey = | 'display-name-regex-to' | 'display-name-regex-to-desc' | 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order' | 'custom-matchers' | 'custom-matchers-desc' | 'add-custom-matcher' | 'delete-custom-matcher' - | 'custom-matcher-name' | 'custom-matcher-pattern' | 'custom-matcher-flags' | 'custom-matcher-capture-group'; + | 'custom-matcher-name' | 'custom-matcher-note' | 'custom-matcher-pattern' | 'custom-matcher-flags' | 'custom-matcher-capture-group' + | 'configure-custom-matcher' | 'drag-copy-target' | 'invalid-regex'; // 本地化翻译字典 export const translations: Record> = { @@ -125,17 +126,17 @@ export const translations: Record> = { 'resolve-link-path-on-paste-tooltip': 'Plugins loaded earlier take priority for paste handling. If another plugin (e.g. Linter) intercepts paste first, this feature will be bypassed. You can adjust plugin load order in the community-plugins.json file.', 'customize-targets': 'Customize targets', 'customize-targets-desc': 'Enable to customize which elements can be copied (disable to copy all elements)', - 'enable-inline-code': 'Enable inline code', + 'enable-inline-code': 'Inline code', 'enable-inline-code-desc': 'Enable copying inline code like `code example`', - 'enable-bold': 'Enable bold text', + 'enable-bold': 'Bold text', 'enable-bold-desc': 'Enable copying bold text like **bold example**', - 'enable-highlight': 'Enable highlighted text', + 'enable-highlight': 'Highlighted text', 'enable-highlight-desc': 'Enable copying highlighted text like ==highlight example==', - 'enable-italic': 'Enable italic text', + 'enable-italic': 'Italic text', 'enable-italic-desc': 'Enable copying italic text like *italic example*', - 'enable-strikethrough': 'Enable strikethrough text', + 'enable-strikethrough': 'Strikethrough text', 'enable-strikethrough-desc': 'Enable copying strikethrough text like ~~strikethrough example~~', - 'enable-inline-latex': 'Enable inline LaTeX', + 'enable-inline-latex': 'Inline LaTeX', 'enable-inline-latex-desc': 'Enable copying inline LaTeX like $latex example$', 'enable-link': 'Enable link title/url', 'enable-link-desc': 'Enable copying link like [linktitle](linkurl) - the plugin will copy the title or the URL of the link based on the current cursor position.', @@ -145,7 +146,7 @@ export const translations: Record> = { 'block-display-word-limit-desc': 'Maximum number of words to show in block display text for space-separated languages (e.g., English "this is a sentence")', 'block-display-char-limit': 'Block Display Text: Character limit for CJK-like languages', 'block-display-char-limit-desc': 'Maximum number of characters to show in block display text for non-space-separated languages (e.g., Chinese "这是一句话") - This setting will be used when the first line contains non-ASCII characters.', - 'enable-wikilink': 'Enable Wiki Link', + 'enable-wikilink': 'Wiki Link', 'enable-wikilink-desc': 'Enable copying of [[Wiki]] links', 'special-format': 'Special copy format options', @@ -187,9 +188,13 @@ export const translations: Record> = { 'add-custom-matcher': 'Add custom target', 'delete-custom-matcher': 'Delete', 'custom-matcher-name': 'Name', + 'custom-matcher-note': 'Note', 'custom-matcher-pattern': 'Regex', 'custom-matcher-flags': 'Flags', 'custom-matcher-capture-group': 'Capture group', + 'configure-custom-matcher': 'Configure', + 'drag-copy-target': 'Drag to reorder', + 'invalid-regex': 'Invalid regular expression; changes were not saved.', }, [Language.ZH]: { // 复制 Block ID @@ -255,21 +260,21 @@ export const translations: Record> = { 'resolve-link-path-on-paste-tooltip': '越早加载的插件越先处理粘贴事件。若其他插件(如 Linter)抢先处理,本功能会被跳过。可在 community-plugins.json 文件中调整插件加载顺序。', 'customize-targets': '自定义复制对象', 'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认可复制所有元素)', - 'enable-inline-code': '启用行内代码', + 'enable-inline-code': '行内代码', 'enable-inline-code-desc': '启用复制行内代码,如 `代码示例`', - 'enable-bold': '启用加粗文本', + 'enable-bold': '加粗文本', 'enable-bold-desc': '启用复制加粗文本,如 **加粗示例**', - 'enable-highlight': '启用高亮文本', + 'enable-highlight': '高亮文本', 'enable-highlight-desc': '启用复制高亮文本,如 ==高亮示例==', - 'enable-italic': '启用斜体文本', + 'enable-italic': '斜体文本', 'enable-italic-desc': '启用复制斜体文本,如 *斜体示例*', - 'enable-strikethrough': '启用删除线文本', + 'enable-strikethrough': '删除线文本', 'enable-strikethrough-desc': '启用复制删除线文本,如 ~~删除线示例~~', - 'enable-inline-latex': '启用行内LaTeX', + 'enable-inline-latex': '行内LaTeX', 'enable-inline-latex-desc': '启用复制行内LaTeX,如 $latex 示例$', 'enable-link': '启用链接文本', 'enable-link-desc': '启用复制 Markdown 链接', - 'enable-wikilink': '启用 Wiki 链接', + 'enable-wikilink': 'Wiki 链接', 'enable-wikilink-desc': '启用复制 [[Wiki]] 链接', 'special-format': '特殊复制格式选项', 'auto-embed-block-link': '块链接:自动添加 ! 符号(嵌入块)', @@ -320,9 +325,13 @@ export const translations: Record> = { 'add-custom-matcher': '添加自定义对象', 'delete-custom-matcher': '删除', 'custom-matcher-name': '名称', + 'custom-matcher-note': '注释', 'custom-matcher-pattern': '正则', 'custom-matcher-flags': '标志', 'custom-matcher-capture-group': '捕获组', + 'configure-custom-matcher': '配置', + 'drag-copy-target': '拖动排序', + 'invalid-regex': '无效的正则表达式,修改未保存。', }, [Language.ZH_TW]: { // 复制 Block ID @@ -392,21 +401,21 @@ export const translations: Record> = { 'resolve-link-path-on-paste-tooltip': '越早載入的外掛越先處理貼上事件。若其他外掛(如 Linter)搶先處理,本功能會被略過。可在 community-plugins.json 檔案中調整外掛載入順序。', 'customize-targets': '自定義複製對象', 'customize-targets-desc': '啟用後可以自定義哪些元素可以被複製(不啟用則默认可複製所有元素)', - 'enable-inline-code': '啟用行內代碼', + 'enable-inline-code': '行內代碼', 'enable-inline-code-desc': '啟用複製行內代碼,如 `代碼示例`', - 'enable-bold': '啟用加粗文本', + 'enable-bold': '加粗文本', 'enable-bold-desc': '啟用複製加粗文本,如 **加粗示例**', - 'enable-highlight': '啟用高亮文本', + 'enable-highlight': '高亮文本', 'enable-highlight-desc': '啟用複製高亮文本,如 ==高亮示例==', - 'enable-italic': '啟用斜體文本', + 'enable-italic': '斜體文本', 'enable-italic-desc': '啟用複製斜體文本,如 *斜體示例*', - 'enable-strikethrough': '啟用刪除線文本', + 'enable-strikethrough': '刪除線文本', 'enable-strikethrough-desc': '啟用複製刪除線文本,如 ~~刪除線示例~~', - 'enable-inline-latex': '啟用行內LaTeX', + 'enable-inline-latex': '行內LaTeX', 'enable-inline-latex-desc': '啟用複製行內LaTeX,如 $latex 示例$', 'enable-link': '啟用連結文本', 'enable-link-desc': '啟用複製 Markdown 連結', - 'enable-wikilink': '啟用 Wiki 連結', + 'enable-wikilink': 'Wiki 連結', 'enable-wikilink-desc': '啟用複製 [[Wiki]] 連結', 'special-format': '特殊複製格式選項', 'auto-embed-block-link': '塊連結:自動添加 ! 符號(嵌入塊)', @@ -454,9 +463,13 @@ export const translations: Record> = { 'add-custom-matcher': '添加自定義對象', 'delete-custom-matcher': '刪除', 'custom-matcher-name': '名稱', + 'custom-matcher-note': '註釋', 'custom-matcher-pattern': '正則', 'custom-matcher-flags': '標誌', 'custom-matcher-capture-group': '捕獲組', + 'configure-custom-matcher': '配置', + 'drag-copy-target': '拖動排序', + 'invalid-regex': '無效的正則表示式,修改未保存。', } }; diff --git a/src/main.ts b/src/main.ts index 04859df..edeecab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -161,7 +161,10 @@ export default class EasyCopy extends Plugin { async loadSettings() { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - this.settings.customMatchers = this.settings.customMatchers ?? []; + this.settings.customMatchers = (this.settings.customMatchers ?? []).map(matcher => ({ + note: '', + ...matcher, + })); this.settings.matcherOrder = normalizeMatcherOrder(this.settings.matcherOrder, this.settings.customMatchers); } diff --git a/src/settingTab.ts b/src/settingTab.ts index 7e794b8..5cdd41f 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -14,7 +14,11 @@ interface SettingsContainer { addSetting(cb: (setting: Setting) => void): void; } -const BUILTIN_MATCHER_LABEL_KEYS: Record = { +type BuiltinMatcherLabelKey = 'enable-bold' | 'enable-italic' | 'enable-highlight' | 'enable-strikethrough' | 'enable-inline-code' | 'enable-inline-latex' | 'enable-wikilink'; +type BuiltinMatcherDescKey = 'enable-bold-desc' | 'enable-italic-desc' | 'enable-highlight-desc' | 'enable-strikethrough-desc' | 'enable-inline-code-desc' | 'enable-inline-latex-desc' | 'enable-wikilink-desc'; +type BuiltinMatcherSettingKey = 'enableBold' | 'enableItalic' | 'enableHighlight' | 'enableStrikethrough' | 'enableInlineCode' | 'enableInlineLatex' | 'enableWikiLink'; + +const BUILTIN_MATCHER_LABEL_KEYS: Record = { 'bold': 'enable-bold', 'italic': 'enable-italic', 'highlight': 'enable-highlight', @@ -24,11 +28,32 @@ const BUILTIN_MATCHER_LABEL_KEYS: Record = { + 'bold': 'enable-bold-desc', + 'italic': 'enable-italic-desc', + 'highlight': 'enable-highlight-desc', + 'strikethrough': 'enable-strikethrough-desc', + 'inline-code': 'enable-inline-code-desc', + 'inline-latex': 'enable-inline-latex-desc', + 'wiki-link': 'enable-wikilink-desc', +}; + +const BUILTIN_MATCHER_SETTING_KEYS: Record = { + 'bold': 'enableBold', + 'italic': 'enableItalic', + 'highlight': 'enableHighlight', + 'strikethrough': 'enableStrikethrough', + 'inline-code': 'enableInlineCode', + 'inline-latex': 'enableInlineLatex', + 'wiki-link': 'enableWikiLink', +}; + function createCustomMatcher(): CustomCopyMatcherSetting { const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; return { id, name: 'Double quotes', + note: 'Copy text inside double quotes.', pattern: '"([^"]+)"', flags: 'g', captureGroup: 1, @@ -85,6 +110,7 @@ function createSettingsGroup(containerEl: HTMLElement, heading?: string): Settin export class EasyCopySettingTab extends PluginSettingTab { plugin: EasyCopy; + private expandedCustomMatcherId: string | null = null; icon: string = 'copy-plus'; @@ -93,6 +119,53 @@ export class EasyCopySettingTab extends PluginSettingTab { this.plugin = plugin; } + private getMatcherName(matcherId: string): string | null { + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + if (customMatcher) return customMatcher.name || this.plugin.t('custom-matcher'); + + const builtinKey = BUILTIN_MATCHER_LABEL_KEYS[matcherId as BuiltinCopyMatcherId]; + return builtinKey ? this.plugin.t(builtinKey) : null; + } + + private getMatcherDesc(matcherId: string): string { + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + if (customMatcher) return customMatcher.note ?? ''; + + const builtinDescKey = BUILTIN_MATCHER_DESC_KEYS[matcherId as BuiltinCopyMatcherId]; + return builtinDescKey ? this.plugin.t(builtinDescKey) : ''; + } + + private isMatcherEnabled(matcherId: string): boolean { + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + if (customMatcher) return customMatcher.enabled; + + const settingKey = BUILTIN_MATCHER_SETTING_KEYS[matcherId as BuiltinCopyMatcherId]; + return settingKey ? Boolean(this.plugin.settings[settingKey]) : false; + } + + private setMatcherEnabled(matcherId: string, enabled: boolean): void { + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + if (customMatcher) { + customMatcher.enabled = enabled; + void this.plugin.saveSettings(); + return; + } + + const settingKey = BUILTIN_MATCHER_SETTING_KEYS[matcherId as BuiltinCopyMatcherId]; + if (!settingKey) return; + this.plugin.settings[settingKey] = enabled; + void this.plugin.saveSettings(); + } + + private isRegexValid(pattern: string, flags: string): boolean { + try { + new RegExp(pattern, flags); + return true; + } catch { + return false; + } + } + display(): void { const {containerEl} = this; @@ -366,192 +439,8 @@ export class EasyCopySettingTab extends PluginSettingTab { this.display(); }))); - // 只有当自定义复制对象选项开启时才显示具体的复制对象选项 if (this.plugin.settings.customizeTargets) { - this.plugin.settings.matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-inline-code')) - .setDesc(this.plugin.t('enable-inline-code-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableInlineCode) - .onChange( value => { - this.plugin.settings.enableInlineCode = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-bold')) - .setDesc(this.plugin.t('enable-bold-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableBold) - .onChange( value => { - this.plugin.settings.enableBold = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-highlight')) - .setDesc(this.plugin.t('enable-highlight-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableHighlight) - .onChange( value => { - this.plugin.settings.enableHighlight = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-italic')) - .setDesc(this.plugin.t('enable-italic-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableItalic) - .onChange( value => { - this.plugin.settings.enableItalic = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-strikethrough')) - .setDesc(this.plugin.t('enable-strikethrough-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableStrikethrough) - .onChange( value => { - this.plugin.settings.enableStrikethrough = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-inline-latex')) - .setDesc(this.plugin.t('enable-inline-latex-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableInlineLatex) - .onChange( value => { - this.plugin.settings.enableInlineLatex = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-link')) - .setDesc(this.plugin.t('enable-link-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableLink) - .onChange( value => { - this.plugin.settings.enableLink = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('enable-wikilink')) - .setDesc(this.plugin.t('enable-wikilink-desc')) - .addToggle(toggle => toggle - .setValue(this.plugin.settings.enableWikiLink ?? true) - .onChange( value => { - this.plugin.settings.enableWikiLink = value; - void this.plugin.saveSettings(); - this.display(); // 切换后刷新界面以显示/隐藏下方选项 - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('matcher-priority')) - .setDesc(this.plugin.t('matcher-priority-desc')) - .addButton(button => button - .setButtonText(this.plugin.t('reset-order')) - .onClick(() => { - this.plugin.settings.matcherOrder = normalizeMatcherOrder(DEFAULT_BUILTIN_COPY_MATCHER_IDS, this.plugin.settings.customMatchers); - void this.plugin.saveSettings(); - this.display(); - }))); - - this.plugin.settings.matcherOrder.forEach((matcherId, index) => { - const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); - const matcherName = customMatcher ? customMatcher.name : this.plugin.t(BUILTIN_MATCHER_LABEL_KEYS[matcherId as BuiltinCopyMatcherId]); - - targetGroup.addSetting(setting => setting - .setName(matcherName) - .addButton(button => button - .setButtonText('↑') - .setTooltip(this.plugin.t('move-up')) - .setDisabled(index === 0) - .onClick(() => { - this.moveMatcher(matcherId, -1); - })) - .addButton(button => button - .setButtonText('↓') - .setTooltip(this.plugin.t('move-down')) - .setDisabled(index === this.plugin.settings.matcherOrder.length - 1) - .onClick(() => { - this.moveMatcher(matcherId, 1); - }))); - }); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('custom-matchers')) - .setDesc(this.plugin.t('custom-matchers-desc')) - .addButton(button => button - .setButtonText(this.plugin.t('add-custom-matcher')) - .onClick(() => { - const customMatcher = createCustomMatcher(); - this.plugin.settings.customMatchers.push(customMatcher); - this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); - void this.plugin.saveSettings(); - this.display(); - }))); - - for (const customMatcher of this.plugin.settings.customMatchers) { - targetGroup.addSetting(setting => setting - .setName(customMatcher.name || this.plugin.t('custom-matcher')) - .addToggle(toggle => toggle - .setValue(customMatcher.enabled) - .onChange(value => { - customMatcher.enabled = value; - void this.plugin.saveSettings(); - })) - .addButton(button => button - .setButtonText(this.plugin.t('delete-custom-matcher')) - .onClick(() => { - this.deleteCustomMatcher(customMatcher.id); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('custom-matcher-name')) - .addText(text => text - .setValue(customMatcher.name) - .onChange(value => { - customMatcher.name = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('custom-matcher-pattern')) - .addText(text => text - .setPlaceholder('"([^"]+)"') - .setValue(customMatcher.pattern) - .onChange(value => { - customMatcher.pattern = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('custom-matcher-flags')) - .addText(text => text - .setPlaceholder('g') - .setValue(customMatcher.flags) - .onChange(value => { - customMatcher.flags = value; - void this.plugin.saveSettings(); - }))); - - targetGroup.addSetting(setting => setting - .setName(this.plugin.t('custom-matcher-capture-group')) - .addText(text => text - .setPlaceholder('1') - .setValue(String(customMatcher.captureGroup)) - .onChange(value => { - customMatcher.captureGroup = Math.max(0, parseInt(value) || 0); - void this.plugin.saveSettings(); - }))); - } - + this.renderMatcherList(targetGroup); } targetGroup.addSetting(setting => setting @@ -641,13 +530,180 @@ export class EasyCopySettingTab extends PluginSettingTab { } } - private moveMatcher(matcherId: string, direction: -1 | 1): void { - const matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); - const currentIndex = matcherOrder.indexOf(matcherId); - const nextIndex = currentIndex + direction; - if (nextIndex < 0 || nextIndex >= matcherOrder.length) return; + private renderMatcherList(targetGroup: SettingsContainer): void { + this.plugin.settings.matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); - [matcherOrder[currentIndex], matcherOrder[nextIndex]] = [matcherOrder[nextIndex], matcherOrder[currentIndex]]; + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('custom-matchers')) + .setDesc(this.plugin.t('custom-matchers-desc')) + .addButton(button => button + .setButtonText(this.plugin.t('add-custom-matcher')) + .onClick(() => { + const customMatcher = createCustomMatcher(); + this.plugin.settings.customMatchers.push(customMatcher); + this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); + this.expandedCustomMatcherId = customMatcher.id; + void this.plugin.saveSettings(); + this.display(); + })) + .addButton(button => button + .setButtonText(this.plugin.t('reset-order')) + .onClick(() => { + this.plugin.settings.matcherOrder = normalizeMatcherOrder(DEFAULT_BUILTIN_COPY_MATCHER_IDS, this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + this.display(); + }))); + + for (const matcherId of this.plugin.settings.matcherOrder) { + this.addMatcherSetting(targetGroup, matcherId); + } + } + + private addMatcherSetting(targetGroup: SettingsContainer, matcherId: string): void { + const matcherName = this.getMatcherName(matcherId); + if (!matcherName) return; + + const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); + targetGroup.addSetting(setting => { + setting + .setName(matcherName) + .setDesc(this.getMatcherDesc(matcherId)); + + setting.settingEl.addClass('easy-copy-matcher-row'); + setting.settingEl.setAttr('draggable', 'true'); + setting.settingEl.dataset.matcherId = matcherId; + setting.settingEl.addEventListener('dragstart', event => { + event.dataTransfer?.setData('text/plain', matcherId); + }); + setting.settingEl.addEventListener('dragover', event => { + event.preventDefault(); + }); + setting.settingEl.addEventListener('drop', event => { + event.preventDefault(); + const sourceId = event.dataTransfer?.getData('text/plain'); + if (sourceId) this.moveMatcherBefore(sourceId, matcherId); + }); + + setting.addExtraButton(button => button + .setIcon('grip-vertical') + .setTooltip(this.plugin.t('drag-copy-target'))); + + if (customMatcher) { + setting.addExtraButton(button => button + .setIcon('trash-2') + .setTooltip(this.plugin.t('delete-custom-matcher')) + .onClick(() => { + this.deleteCustomMatcher(customMatcher.id); + })); + setting.addExtraButton(button => button + .setIcon('settings') + .setTooltip(this.plugin.t('configure-custom-matcher')) + .onClick(() => { + this.expandedCustomMatcherId = this.expandedCustomMatcherId === customMatcher.id ? null : customMatcher.id; + this.display(); + })); + } + + setting.addToggle(toggle => toggle + .setValue(this.isMatcherEnabled(matcherId)) + .onChange(value => { + this.setMatcherEnabled(matcherId, value); + })); + + if (customMatcher && this.expandedCustomMatcherId === customMatcher.id) { + const configEl = activeDocument.createElement('div'); + configEl.addClass('easy-copy-matcher-config'); + setting.settingEl.insertAdjacentElement('afterend', configEl); + this.renderCustomMatcherConfig(configEl, customMatcher); + } + }); + } + + private renderCustomMatcherConfig(containerEl: HTMLElement, customMatcher: CustomCopyMatcherSetting): void { + new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-name')) + .addText(text => text + .setValue(customMatcher.name) + .onChange(value => { + customMatcher.name = value; + this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), value || this.plugin.t('custom-matcher'), customMatcher.note ?? ''); + void this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-note')) + .addText(text => text + .setValue(customMatcher.note ?? '') + .onChange(value => { + customMatcher.note = value; + this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), customMatcher.name || this.plugin.t('custom-matcher'), value); + void this.plugin.saveSettings(); + })); + + const regexErrorEl = containerEl.createDiv({ cls: 'easy-copy-regex-error' }); + const updateRegexError = (value: string) => { + regexErrorEl.setText(value); + regexErrorEl.toggleClass('is-visible', Boolean(value)); + }; + + new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-pattern')) + .addText(text => text + .setPlaceholder('"([^"]+)"') + .setValue(customMatcher.pattern) + .onChange(value => { + if (!this.isRegexValid(value, customMatcher.flags)) { + updateRegexError(this.plugin.t('invalid-regex')); + return; + } + updateRegexError(''); + customMatcher.pattern = value; + void this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-flags')) + .addText(text => text + .setPlaceholder('g') + .setValue(customMatcher.flags) + .onChange(value => { + if (!this.isRegexValid(customMatcher.pattern, value)) { + updateRegexError(this.plugin.t('invalid-regex')); + return; + } + updateRegexError(''); + customMatcher.flags = value; + void this.plugin.saveSettings(); + })); + + new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-capture-group')) + .addText(text => text + .setPlaceholder('1') + .setValue(String(customMatcher.captureGroup)) + .onChange(value => { + customMatcher.captureGroup = Math.max(0, parseInt(value) || 0); + void this.plugin.saveSettings(); + })); + } + + private updateMatcherRowText(matcherId: string, name: string, desc: string): void { + for (const row of Array.from(this.containerEl.querySelectorAll('.easy-copy-matcher-row'))) { + if (row.dataset.matcherId !== matcherId) continue; + row.querySelector('.setting-item-name')?.setText(name); + row.querySelector('.setting-item-description')?.setText(desc); + } + } + + private moveMatcherBefore(sourceId: string, targetId: string): void { + if (sourceId === targetId) return; + + const matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers) + .filter(id => id !== sourceId); + const targetIndex = matcherOrder.indexOf(targetId); + if (targetIndex < 0) return; + + matcherOrder.splice(targetIndex, 0, sourceId); this.plugin.settings.matcherOrder = matcherOrder; void this.plugin.saveSettings(); this.display(); diff --git a/src/type.ts b/src/type.ts index 19a6a27..00cd0f2 100644 --- a/src/type.ts +++ b/src/type.ts @@ -55,6 +55,7 @@ export interface ContextData { export interface CustomCopyMatcherSetting { id: string; name: string; + note?: string; pattern: string; flags: string; captureGroup: number; diff --git a/styles.css b/styles.css index e365d4d..8c978ee 100644 --- a/styles.css +++ b/styles.css @@ -24,4 +24,29 @@ If your plugin does not need CSS, delete this file. width: 100%; margin-top: 0.5em; margin-bottom: 0.5em; -} \ No newline at end of file +} + +.easy-copy-matcher-row { + cursor: grab; +} + +.easy-copy-matcher-row:active { + cursor: grabbing; +} + +.easy-copy-matcher-config { + margin-left: 1.5em; + padding: 0.5em 0 0.75em 0; + border-bottom: 1px solid var(--background-modifier-border); +} + +.easy-copy-regex-error { + display: none; + color: var(--text-error); + font-size: var(--font-ui-smaller); + margin: 0.25em 0 0.5em 0; +} + +.easy-copy-regex-error.is-visible { + display: block; +} From 05b868e20d0b25b39edd34747180e680b6fb3307 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 13:43:05 +0800 Subject: [PATCH 6/9] fix: expand matcher config without rerender --- src/settingTab.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/settingTab.ts b/src/settingTab.ts index 5cdd41f..9cb8704 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -111,6 +111,7 @@ function createSettingsGroup(containerEl: HTMLElement, heading?: string): Settin export class EasyCopySettingTab extends PluginSettingTab { plugin: EasyCopy; private expandedCustomMatcherId: string | null = null; + private expandedCustomMatcherEl: HTMLElement | null = null; icon: string = 'copy-plus'; @@ -157,6 +158,23 @@ export class EasyCopySettingTab extends PluginSettingTab { void this.plugin.saveSettings(); } + private toggleCustomMatcherConfig(settingEl: HTMLElement, customMatcher: CustomCopyMatcherSetting): void { + if (this.expandedCustomMatcherId === customMatcher.id && this.expandedCustomMatcherEl) { + this.expandedCustomMatcherEl.remove(); + this.expandedCustomMatcherEl = null; + this.expandedCustomMatcherId = null; + return; + } + + this.expandedCustomMatcherEl?.remove(); + const configEl = activeDocument.createElement('div'); + configEl.addClass('easy-copy-matcher-config'); + settingEl.insertAdjacentElement('afterend', configEl); + this.renderCustomMatcherConfig(configEl, customMatcher); + this.expandedCustomMatcherEl = configEl; + this.expandedCustomMatcherId = customMatcher.id; + } + private isRegexValid(pattern: string, flags: string): boolean { try { new RegExp(pattern, flags); @@ -170,6 +188,7 @@ export class EasyCopySettingTab extends PluginSettingTab { const {containerEl} = this; containerEl.empty(); + this.expandedCustomMatcherEl = null; const generalGroup = createSettingsGroup(containerEl); @@ -599,8 +618,7 @@ export class EasyCopySettingTab extends PluginSettingTab { .setIcon('settings') .setTooltip(this.plugin.t('configure-custom-matcher')) .onClick(() => { - this.expandedCustomMatcherId = this.expandedCustomMatcherId === customMatcher.id ? null : customMatcher.id; - this.display(); + this.toggleCustomMatcherConfig(setting.settingEl, customMatcher); })); } From 6e83e47cc5ac756f9bd10b4086421d522df9cda1 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 16:06:46 +0800 Subject: [PATCH 7/9] fix: include link in ordered copy targets --- .gitignore | 1 + src/copyMatcher.test.ts | 1 + src/copyMatcher.ts | 7 +- src/i18n.ts | 84 +++++++++----- src/main.ts | 39 ++++--- src/settingTab.ts | 245 +++++++++++++++++++++++++++++----------- src/type.ts | 3 +- styles.css | 8 ++ 8 files changed, 271 insertions(+), 117 deletions(-) diff --git a/.gitignore b/.gitignore index df3afee..e73b4ee 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ easy-copy/ # env .env /.sisyphus +/.omo diff --git a/src/copyMatcher.test.ts b/src/copyMatcher.test.ts index cb416c2..1a0ab74 100644 --- a/src/copyMatcher.test.ts +++ b/src/copyMatcher.test.ts @@ -25,6 +25,7 @@ describe('normalizeMatcherOrder', () => { 'highlight', 'strikethrough', 'inline-latex', + 'link', 'wiki-link', getCustomMatcherOrderId(customMatcher), ]); diff --git a/src/copyMatcher.ts b/src/copyMatcher.ts index c3c562d..40478db 100644 --- a/src/copyMatcher.ts +++ b/src/copyMatcher.ts @@ -7,14 +7,15 @@ export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [ 'strikethrough', 'inline-code', 'inline-latex', + 'link', 'wiki-link', ]; export interface CopyMatcher { id: string; type: ContextType; - regex: RegExp; enabled: boolean; + regex: RegExp; captureGroup?: number; name?: string; } @@ -133,7 +134,9 @@ export function buildBuiltinCopyMatchers(settings: EasyCopySettings, isIosApp: b ]; const matcherById = new Map(matchers.map(matcher => [matcher.id, matcher])); - return normalizeBuiltinMatcherOrder(settings.matcherOrder).map(id => matcherById.get(id)!); + return normalizeBuiltinMatcherOrder(settings.matcherOrder) + .map(id => matcherById.get(id)) + .filter((matcher): matcher is CopyMatcher => Boolean(matcher)); } export function buildCopyMatchers(settings: EasyCopySettings, isIosApp: boolean): CopyMatcher[] { diff --git a/src/i18n.ts b/src/i18n.ts index 52c688d..7eaf21f 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -11,7 +11,7 @@ export type TranslationKey = | 'inline-code-copied' | 'block-id-copied' | 'note-link-copied' | 'heading-copied' | 'strikethrough-copied' | 'inline-latex-copied' | 'bold-copied' | 'highlight-copied' | 'italic-copied' | 'link-text-copied' | 'link-url-copied' | 'wiki-link-copied' | 'callout-copied' | 'note-link-simplified' - | 'code-block-copied' | 'custom-matcher' | 'custom-matcher-copied' + | 'code-block-copied' | 'custom-prefix' | 'custom-matcher' | 'custom-matcher-copied' | 'format' | 'add-to-menu' | 'add-to-menu-desc' | 'show-notice' | 'show-notice-desc' | 'add-extra-commands' | 'add-extra-commands-desc' | 'use-heading-as-display' | 'use-heading-as-display-desc' | 'heading-link-separator' | 'heading-link-separator-desc' | 'block-id' @@ -52,7 +52,11 @@ export type TranslationKey = | 'display-name-regex-to' | 'display-name-regex-to-desc' | 'matcher-priority' | 'matcher-priority-desc' | 'move-up' | 'move-down' | 'reset-order' | 'custom-matchers' | 'custom-matchers-desc' | 'add-custom-matcher' | 'delete-custom-matcher' - | 'custom-matcher-name' | 'custom-matcher-note' | 'custom-matcher-pattern' | 'custom-matcher-flags' | 'custom-matcher-capture-group' + | 'custom-matcher-name' | 'custom-matcher-name-desc' + | 'custom-matcher-note' | 'custom-matcher-note-desc' + | 'custom-matcher-pattern' | 'custom-matcher-pattern-desc' + | 'custom-matcher-flags' | 'custom-matcher-flags-desc' + | 'custom-matcher-capture-group' | 'custom-matcher-capture-group-desc' | 'configure-custom-matcher' | 'drag-copy-target' | 'invalid-regex'; // 本地化翻译字典 @@ -90,8 +94,9 @@ export const translations: Record> = { 'wiki-link-copied': 'Wiki link copied!', 'callout-copied': 'Callout copied!', 'code-block-copied': 'Code block copied!', + 'custom-prefix': '(Custom) ', 'custom-matcher': 'Custom matcher', - 'custom-matcher-copied': 'copied!', + 'custom-matcher-copied': ' copied!', 'note-link-simplified': 'Link simplified (filename matches heading)', // 设置界面 @@ -133,13 +138,13 @@ export const translations: Record> = { 'enable-highlight': 'Highlighted text', 'enable-highlight-desc': 'Enable copying highlighted text like ==highlight example==', 'enable-italic': 'Italic text', - 'enable-italic-desc': 'Enable copying italic text like *italic example*', + 'enable-italic-desc': 'Enable copying italic text like *italic example* or _italic example_', 'enable-strikethrough': 'Strikethrough text', 'enable-strikethrough-desc': 'Enable copying strikethrough text like ~~strikethrough example~~', 'enable-inline-latex': 'Inline LaTeX', 'enable-inline-latex-desc': 'Enable copying inline LaTeX like $latex example$', - 'enable-link': 'Enable link title/url', - 'enable-link-desc': 'Enable copying link like [linktitle](linkurl) - the plugin will copy the title or the URL of the link based on the current cursor position.', + 'enable-link': 'Link title/url', + 'enable-link-desc': 'Enable copying Markdown links like [title](link). The copied content depends on the cursor position.', 'auto-block-display-text': 'Generate display text for block links', 'auto-block-display-text-desc': 'If enabled, display text will be automatically added to generated block ID links', 'block-display-word-limit': 'Block Display Text: Word limit for English-like languages', @@ -178,20 +183,25 @@ export const translations: Record> = { 'display-name-regex-from-desc': 'Regular expression pattern to match in the display text (e.g. ^\\d+\\.\\s* to remove leading numbers)', 'display-name-regex-to': 'Replace with', 'display-name-regex-to-desc': 'Replacement string, supports capture groups like $1, $2 (e.g. $1 to keep only the first captured group)', - 'matcher-priority': 'Copy target priority', - 'matcher-priority-desc': 'When multiple targets match the cursor, targets higher in this list are copied first.', + 'matcher-priority': 'Custom copy order', + 'matcher-priority-desc': 'Define the recognition order. Once a rule matches content, it will be copied. Rules higher in the list have higher priority.', 'move-up': 'Move up', 'move-down': 'Move down', 'reset-order': 'Reset order', - 'custom-matchers': 'Custom regex targets', - 'custom-matchers-desc': 'Add current-line regex rules. Use capture group 1 to copy the text inside quotes or brackets.', + 'custom-matchers': 'Custom copy rules', + 'custom-matchers-desc': 'Add regex-based matching rules to support more symbols. Applies only to the current line.', 'add-custom-matcher': 'Add custom target', 'delete-custom-matcher': 'Delete', 'custom-matcher-name': 'Name', + 'custom-matcher-name-desc': 'Name of the custom rule.', 'custom-matcher-note': 'Note', - 'custom-matcher-pattern': 'Regex', + 'custom-matcher-note-desc': 'Description text for the custom rule (can be empty).', + 'custom-matcher-pattern': 'Regular expression', + 'custom-matcher-pattern-desc': 'Regular expression used to match content on the current line.', 'custom-matcher-flags': 'Flags', + 'custom-matcher-flags-desc': 'Regular expression flags, such as g or i. The g flag is added automatically when matching.', 'custom-matcher-capture-group': 'Capture group', + 'custom-matcher-capture-group-desc': 'Capture group index to copy. Use 1 to copy the first parenthesized group; use 0 to copy the full match.', 'configure-custom-matcher': 'Configure', 'drag-copy-target': 'Drag to reorder', 'invalid-regex': 'Invalid regular expression; changes were not saved.', @@ -228,6 +238,7 @@ export const translations: Record> = { 'wiki-link-copied': 'Wiki链接已复制!', 'callout-copied': '标注内容已复制!', 'code-block-copied': '代码块已复制!', + 'custom-prefix': '(自定义)', 'custom-matcher': '自定义规则', 'custom-matcher-copied': '已复制!', 'note-link-simplified': '链接已简化(文件名与标题相匹配)', @@ -259,7 +270,7 @@ export const translations: Record> = { 'resolve-link-path-on-paste-desc': '粘贴时根据目标文件重新生成链接路径。使用"跟随 Obsidian 设置"时,会沿用软件设置中的路径风格(最短/相对/绝对);否则,仅使用最短唯一路径。', 'resolve-link-path-on-paste-tooltip': '越早加载的插件越先处理粘贴事件。若其他插件(如 Linter)抢先处理,本功能会被跳过。可在 community-plugins.json 文件中调整插件加载顺序。', 'customize-targets': '自定义复制对象', - 'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认可复制所有元素)', + 'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认复制所有插件预设对象,如加粗、高亮等)', 'enable-inline-code': '行内代码', 'enable-inline-code-desc': '启用复制行内代码,如 `代码示例`', 'enable-bold': '加粗文本', @@ -267,13 +278,13 @@ export const translations: Record> = { 'enable-highlight': '高亮文本', 'enable-highlight-desc': '启用复制高亮文本,如 ==高亮示例==', 'enable-italic': '斜体文本', - 'enable-italic-desc': '启用复制斜体文本,如 *斜体示例*', + 'enable-italic-desc': '启用复制斜体文本,如 *斜体示例* 或 _斜体示例_', 'enable-strikethrough': '删除线文本', 'enable-strikethrough-desc': '启用复制删除线文本,如 ~~删除线示例~~', 'enable-inline-latex': '行内LaTeX', 'enable-inline-latex-desc': '启用复制行内LaTeX,如 $latex 示例$', - 'enable-link': '启用链接文本', - 'enable-link-desc': '启用复制 Markdown 链接', + 'enable-link': '链接文本', + 'enable-link-desc': '启用复制 Markdown 链接,如 [标题](链接),会基于光标所在位置复制不同内容', 'enable-wikilink': 'Wiki 链接', 'enable-wikilink-desc': '启用复制 [[Wiki]] 链接', 'special-format': '特殊复制格式选项', @@ -315,20 +326,25 @@ export const translations: Record> = { 'display-name-regex-from-desc': '用于匹配显示文本的正则表达式(如 ^\\d+\\.\\s* 可去除开头的序号)', 'display-name-regex-to': '替换为', 'display-name-regex-to-desc': '替换字符串,支持捕获组引用如 $1、$2(如 $1 只保留第一个捕获组的内容)', - 'matcher-priority': '复制对象优先级', - 'matcher-priority-desc': '当多种复制对象同时匹配光标位置时,列表中更靠上的对象会优先生效。', + 'matcher-priority': '自定义复制顺序', + 'matcher-priority-desc': '用于定义识别顺序,一旦识别到匹配的内容就会复制。越上面的规则优先级越高。', 'move-up': '上移', 'move-down': '下移', 'reset-order': '重置顺序', - 'custom-matchers': '自定义正则复制对象', - 'custom-matchers-desc': '添加仅匹配当前行的正则规则。可用捕获组 1 复制引号或括号内部文本。', + 'custom-matchers': '自定义复制规则', + 'custom-matchers-desc': '添加基于正则表达式的匹配规则,以适配更多的符号。只针对当前行生效。', 'add-custom-matcher': '添加自定义对象', 'delete-custom-matcher': '删除', 'custom-matcher-name': '名称', + 'custom-matcher-name-desc': '自定义规则的名称', 'custom-matcher-note': '注释', - 'custom-matcher-pattern': '正则', + 'custom-matcher-note-desc': '自定义规则的说明文本(可以留空)', + 'custom-matcher-pattern': '正则表达式', + 'custom-matcher-pattern-desc': '用于匹配当前行内容的正则表达式。', 'custom-matcher-flags': '标志', + 'custom-matcher-flags-desc': '正则表达式标志,例如 g 或 i。匹配时会自动添加 g 标志。', 'custom-matcher-capture-group': '捕获组', + 'custom-matcher-capture-group-desc': '要复制的捕获组序号。1 表示第一个括号捕获组,0 表示完整匹配内容。', 'configure-custom-matcher': '配置', 'drag-copy-target': '拖动排序', 'invalid-regex': '无效的正则表达式,修改未保存。', @@ -373,6 +389,10 @@ export const translations: Record> = { 'link-url-copied': '連結地址已複製!', 'wiki-link-copied': 'Wiki連結已複製!', 'callout-copied': '标注内容已複製!', + 'code-block-copied': '代碼塊已複製!', + 'custom-prefix': '(自定義)', + 'custom-matcher': '自定義規則', + 'custom-matcher-copied': '已複製!', 'note-link-simplified': '連結已簡化(檔案名與標題相匹配)', // 設置界面 @@ -408,13 +428,13 @@ export const translations: Record> = { 'enable-highlight': '高亮文本', 'enable-highlight-desc': '啟用複製高亮文本,如 ==高亮示例==', 'enable-italic': '斜體文本', - 'enable-italic-desc': '啟用複製斜體文本,如 *斜體示例*', + 'enable-italic-desc': '啟用複製斜體文本,如 *斜體示例* 或 _斜體示例_', 'enable-strikethrough': '刪除線文本', 'enable-strikethrough-desc': '啟用複製刪除線文本,如 ~~刪除線示例~~', 'enable-inline-latex': '行內LaTeX', 'enable-inline-latex-desc': '啟用複製行內LaTeX,如 $latex 示例$', - 'enable-link': '啟用連結文本', - 'enable-link-desc': '啟用複製 Markdown 連結', + 'enable-link': '連結文本', + 'enable-link-desc': '啟用複製Markdown連結,如 [標題](連結),會基於游標所在位置複製不同內容', 'enable-wikilink': 'Wiki 連結', 'enable-wikilink-desc': '啟用複製 [[Wiki]] 連結', 'special-format': '特殊複製格式選項', @@ -431,9 +451,6 @@ export const translations: Record> = { 'code-block-copy-with-fences': '複製代碼塊(含 ```)', 'code-block-generate-block-link': '生成塊連結', 'code-block-disabled': '禁用', - 'code-block-copied': '代碼塊已複製!', - 'custom-matcher': '自定義規則', - 'custom-matcher-copied': '已複製!', 'keep-wiki-brackets': 'Wiki連結:保留 [[ ]] 括號', 'keep-wiki-brackets-desc': '複製 wiki 連結時保留兩側 [[ ]] 括號', @@ -453,20 +470,25 @@ export const translations: Record> = { 'display-name-regex-from-desc': '用於匹配顯示文本的正規表示式(如 ^\\d+\\.\\s* 可去除開頭的序號)', 'display-name-regex-to': '替換為', 'display-name-regex-to-desc': '替換字符串,支持捕獲組引用如 $1、$2', - 'matcher-priority': '複製對象優先級', - 'matcher-priority-desc': '當多種複製對象同時匹配游標位置時,列表中更靠上的對象會優先生效。', + 'matcher-priority': '自定義複製順序', + 'matcher-priority-desc': '用於定義識別順序,一旦識別到匹配的內容就會複製。越上面的規則優先級越高。', 'move-up': '上移', 'move-down': '下移', 'reset-order': '重置順序', - 'custom-matchers': '自定義正則複製對象', - 'custom-matchers-desc': '添加僅匹配當前行的正則規則。可用捕獲組 1 複製引號或括號內部文本。', + 'custom-matchers': '自定義複製規則', + 'custom-matchers-desc': '添加基於正則表示式的匹配規則,以適配更多的符號。只針對當前行生效。', 'add-custom-matcher': '添加自定義對象', 'delete-custom-matcher': '刪除', 'custom-matcher-name': '名稱', + 'custom-matcher-name-desc': '自定義規則的名稱', 'custom-matcher-note': '註釋', - 'custom-matcher-pattern': '正則', + 'custom-matcher-note-desc': '自定義規則的說明文本(可以留空)', + 'custom-matcher-pattern': '正則表示式', + 'custom-matcher-pattern-desc': '用於匹配當前行內容的正則表示式。', 'custom-matcher-flags': '標誌', + 'custom-matcher-flags-desc': '正則表示式標誌,例如 g 或 i。匹配時會自動添加 g 標誌。', 'custom-matcher-capture-group': '捕獲組', + 'custom-matcher-capture-group-desc': '要複製的捕獲組序號。1 表示第一個括號捕獲組,0 表示完整匹配內容。', 'configure-custom-matcher': '配置', 'drag-copy-target': '拖動排序', 'invalid-regex': '無效的正則表示式,修改未保存。', diff --git a/src/main.ts b/src/main.ts index edeecab..cc66d96 100644 --- a/src/main.ts +++ b/src/main.ts @@ -420,11 +420,27 @@ export default class EasyCopy extends Plugin { const afterCursor = curLine.slice(curCh); // 光标后的内容 - // 匹配优先级:加粗 > 斜体 > 高亮 > 删除线 > 行内代码 > 行内Latex > 块ID > 双链 - const matchers = buildCopyMatchers(this.settings, Platform.isIosApp); + // 匹配优先级由设置中的 matcherOrder 决定;regex matcher 和特殊 detector 共用同一排序 + const matcherById = new Map(buildCopyMatchers(this.settings, Platform.isIosApp).map(matcher => [matcher.id, matcher])); + const orderedTargetIds = normalizeMatcherOrder(this.settings.matcherOrder, this.settings.customMatchers); - for (const matcher of matchers) { - if (!matcher.enabled) continue; // 如果当前类型未启用,则跳过 + for (const targetId of orderedTargetIds) { + if (targetId === 'link') { + if (this.settings.customizeTargets && !this.settings.enableLink) continue; + const linkInfo = this.isCursorInLink(beforeCursor, afterCursor); + if (linkInfo) { + return { + type: linkInfo.type, + curLine, + match: linkInfo.content, + range: linkInfo.range, + }; + } + continue; + } + + const matcher = matcherById.get(targetId); + if (!matcher?.enabled) continue; // 如果当前类型未启用,则跳过 const matchInfo = getMatchInfo(beforeCursor + afterCursor, beforeCursor.length, matcher.regex, matcher.captureGroup); if (matchInfo) { return { @@ -437,19 +453,6 @@ export default class EasyCopy extends Plugin { } } - // 检测链接 - if (!this.settings.customizeTargets || this.settings.enableLink) { - const linkInfo = this.isCursorInLink(beforeCursor, afterCursor); - if (linkInfo) { - return { - type: linkInfo.type, - curLine, - match: linkInfo.content, - range: linkInfo.range, - }; - } - } - // 检测代码块(在 block ID 之前,因为两者会冲突) const codeBlockInfo = this.detectCodeBlock(editor); if (codeBlockInfo) { @@ -605,7 +608,7 @@ export default class EasyCopy extends Plugin { case ContextType.CUSTOM: void navigator.clipboard.writeText(contextType.match!); if (this.settings.showNotice) { - new Notice(`${contextType.matcherName ?? this.t('custom-matcher')} ${this.t('custom-matcher-copied')}`); + new Notice(`${this.t('custom-prefix')}${contextType.matcherName ?? this.t('custom-matcher')}${this.t('custom-matcher-copied')}`); } return; diff --git a/src/settingTab.ts b/src/settingTab.ts index 9cb8704..4179d06 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -14,9 +14,9 @@ interface SettingsContainer { addSetting(cb: (setting: Setting) => void): void; } -type BuiltinMatcherLabelKey = 'enable-bold' | 'enable-italic' | 'enable-highlight' | 'enable-strikethrough' | 'enable-inline-code' | 'enable-inline-latex' | 'enable-wikilink'; -type BuiltinMatcherDescKey = 'enable-bold-desc' | 'enable-italic-desc' | 'enable-highlight-desc' | 'enable-strikethrough-desc' | 'enable-inline-code-desc' | 'enable-inline-latex-desc' | 'enable-wikilink-desc'; -type BuiltinMatcherSettingKey = 'enableBold' | 'enableItalic' | 'enableHighlight' | 'enableStrikethrough' | 'enableInlineCode' | 'enableInlineLatex' | 'enableWikiLink'; +type BuiltinMatcherLabelKey = 'enable-bold' | 'enable-italic' | 'enable-highlight' | 'enable-strikethrough' | 'enable-inline-code' | 'enable-inline-latex' | 'enable-link' | 'enable-wikilink'; +type BuiltinMatcherDescKey = 'enable-bold-desc' | 'enable-italic-desc' | 'enable-highlight-desc' | 'enable-strikethrough-desc' | 'enable-inline-code-desc' | 'enable-inline-latex-desc' | 'enable-link-desc' | 'enable-wikilink-desc'; +type BuiltinMatcherSettingKey = 'enableBold' | 'enableItalic' | 'enableHighlight' | 'enableStrikethrough' | 'enableInlineCode' | 'enableInlineLatex' | 'enableLink' | 'enableWikiLink'; const BUILTIN_MATCHER_LABEL_KEYS: Record = { 'bold': 'enable-bold', @@ -25,6 +25,7 @@ const BUILTIN_MATCHER_LABEL_KEYS: Record(`.easy-copy-matcher-config[data-matcher-id="${customMatcher.id}"]`); + if (existingConfigEl) { + existingConfigEl.remove(); + if (this.expandedCustomMatcherEl === existingConfigEl) this.expandedCustomMatcherEl = null; + this.expandedCustomMatcherId = null; + return; + } + if (this.expandedCustomMatcherId === customMatcher.id && this.expandedCustomMatcherEl) { this.expandedCustomMatcherEl.remove(); this.expandedCustomMatcherEl = null; @@ -169,12 +181,51 @@ export class EasyCopySettingTab extends PluginSettingTab { this.expandedCustomMatcherEl?.remove(); const configEl = activeDocument.createElement('div'); configEl.addClass('easy-copy-matcher-config'); + configEl.dataset.matcherId = customMatcher.id; settingEl.insertAdjacentElement('afterend', configEl); this.renderCustomMatcherConfig(configEl, customMatcher); this.expandedCustomMatcherEl = configEl; this.expandedCustomMatcherId = customMatcher.id; } + private persistMatcherOrderFromDom(): void { + const matcherOrder = Array.from(this.containerEl.querySelectorAll('.easy-copy-matcher-row')) + .map(row => row.dataset.matcherId) + .filter((id): id is string => Boolean(id)); + + this.plugin.settings.matcherOrder = normalizeMatcherOrder(matcherOrder, this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + } + + private closeExpandedCustomMatcherConfig(): void { + this.expandedCustomMatcherEl?.remove(); + this.expandedCustomMatcherEl = null; + this.expandedCustomMatcherId = null; + } + + private reorderMatcherRows(containerEl: HTMLElement): void { + this.closeExpandedCustomMatcherConfig(); + for (const matcherId of this.plugin.settings.matcherOrder) { + const rowEl = containerEl.querySelector(`.easy-copy-matcher-row[data-matcher-id="${matcherId}"]`); + if (rowEl) containerEl.appendChild(rowEl); + } + } + + private moveDraggedMatcherPreview(targetEl: HTMLElement, targetMatcherId: string, pointerY: number): void { + const sourceMatcherId = this.draggedMatcherId; + if (!sourceMatcherId || sourceMatcherId === targetMatcherId) return; + + const sourceEl = this.containerEl.querySelector(`.easy-copy-matcher-row[data-matcher-id="${sourceMatcherId}"]`); + if (!sourceEl) return; + + const targetRect = targetEl.getBoundingClientRect(); + const placeAfter = pointerY > targetRect.top + targetRect.height / 2; + const referenceEl = placeAfter ? targetEl.nextElementSibling : targetEl; + if (referenceEl === sourceEl) return; + + targetEl.parentElement?.insertBefore(sourceEl, referenceEl); + } + private isRegexValid(pattern: string, flags: string): boolean { try { new RegExp(pattern, flags); @@ -184,6 +235,35 @@ export class EasyCopySettingTab extends PluginSettingTab { } } + private focusNextConfigInput(inputEl: HTMLInputElement, direction: 1 | -1): void { + const configEl = inputEl.closest('.easy-copy-matcher-config'); + if (!configEl) return; + + const inputs = Array.from(configEl.querySelectorAll('input')); + const currentIndex = inputs.indexOf(inputEl); + const nextInput = inputs[currentIndex + direction]; + if (!nextInput) return; + + nextInput.focus(); + nextInput.select(); + } + + private enableConfigInputTabNavigation(inputEl: HTMLInputElement): void { + inputEl.addEventListener('keydown', event => { + if (event.key !== 'Tab') return; + + const direction = event.shiftKey ? -1 : 1; + const configEl = inputEl.closest('.easy-copy-matcher-config'); + const inputs = configEl ? Array.from(configEl.querySelectorAll('input')) : []; + const currentIndex = inputs.indexOf(inputEl); + const nextInput = inputs[currentIndex + direction]; + if (!nextInput) return; + + event.preventDefault(); + this.focusNextConfigInput(inputEl, direction); + }); + } + display(): void { const {containerEl} = this; @@ -551,6 +631,19 @@ export class EasyCopySettingTab extends PluginSettingTab { private renderMatcherList(targetGroup: SettingsContainer): void { this.plugin.settings.matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); + let listContainerEl: HTMLElement | null = null; + + targetGroup.addSetting(setting => setting + .setName(this.plugin.t('matcher-priority')) + .setDesc(this.plugin.t('matcher-priority-desc')) + .addButton(button => button + .setButtonText(this.plugin.t('reset-order')) + .onClick(() => { + if (!listContainerEl) return; + this.plugin.settings.matcherOrder = normalizeMatcherOrder(DEFAULT_BUILTIN_COPY_MATCHER_IDS, this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + this.reorderMatcherRows(listContainerEl); + }))); targetGroup.addSetting(setting => setting .setName(this.plugin.t('custom-matchers')) @@ -558,61 +651,79 @@ export class EasyCopySettingTab extends PluginSettingTab { .addButton(button => button .setButtonText(this.plugin.t('add-custom-matcher')) .onClick(() => { + if (!listContainerEl) return; const customMatcher = createCustomMatcher(); this.plugin.settings.customMatchers.push(customMatcher); - this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); - this.expandedCustomMatcherId = customMatcher.id; - void this.plugin.saveSettings(); - this.display(); - })) - .addButton(button => button - .setButtonText(this.plugin.t('reset-order')) - .onClick(() => { - this.plugin.settings.matcherOrder = normalizeMatcherOrder(DEFAULT_BUILTIN_COPY_MATCHER_IDS, this.plugin.settings.customMatchers); - void this.plugin.saveSettings(); - this.display(); + this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); + void this.plugin.saveSettings(); + const settingEl = this.addMatcherSetting(listContainerEl, getCustomMatcherOrderId(customMatcher)); + if (settingEl) this.toggleCustomMatcherConfig(settingEl, customMatcher); }))); + listContainerEl = activeDocument.createElement('div'); + listContainerEl.addClass('easy-copy-matcher-list'); + this.containerEl.querySelector('.easy-copy-matcher-list')?.remove(); + const lastTargetSetting = this.containerEl.querySelectorAll('.setting-item'); + lastTargetSetting[lastTargetSetting.length - 1]?.insertAdjacentElement('afterend', listContainerEl); + for (const matcherId of this.plugin.settings.matcherOrder) { - this.addMatcherSetting(targetGroup, matcherId); + this.addMatcherSetting(listContainerEl, matcherId); } } - private addMatcherSetting(targetGroup: SettingsContainer, matcherId: string): void { + private addMatcherSetting(containerEl: HTMLElement, matcherId: string): HTMLElement | null { const matcherName = this.getMatcherName(matcherId); - if (!matcherName) return; + if (!matcherName) return null; const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); - targetGroup.addSetting(setting => { + let settingEl: HTMLElement | null = null; + new Setting(containerEl).then(setting => { + settingEl = setting.settingEl; setting .setName(matcherName) .setDesc(this.getMatcherDesc(matcherId)); setting.settingEl.addClass('easy-copy-matcher-row'); + const dragHandleEl = activeDocument.createElement('div'); + dragHandleEl.addClass('clickable-icon'); + dragHandleEl.addClass('extra-setting-button'); + dragHandleEl.addClass('mod-drag-handle'); + dragHandleEl.setAttr('aria-label', this.plugin.t('drag-copy-target')); + dragHandleEl.setAttr('tabindex', '-1'); + setIcon(dragHandleEl, 'menu'); + setting.settingEl.insertBefore(dragHandleEl, setting.infoEl); + setting.settingEl.setAttr('draggable', 'true'); setting.settingEl.dataset.matcherId = matcherId; setting.settingEl.addEventListener('dragstart', event => { + this.expandedCustomMatcherEl?.remove(); + this.expandedCustomMatcherEl = null; + this.expandedCustomMatcherId = null; + this.draggedMatcherId = matcherId; + setting.settingEl.addClass('is-dragging'); event.dataTransfer?.setData('text/plain', matcherId); + if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'; }); setting.settingEl.addEventListener('dragover', event => { event.preventDefault(); + this.moveDraggedMatcherPreview(setting.settingEl, matcherId, event.clientY); }); setting.settingEl.addEventListener('drop', event => { event.preventDefault(); - const sourceId = event.dataTransfer?.getData('text/plain'); - if (sourceId) this.moveMatcherBefore(sourceId, matcherId); + this.persistMatcherOrderFromDom(); + }); + setting.settingEl.addEventListener('dragend', () => { + setting.settingEl.removeClass('is-dragging'); + this.draggedMatcherId = null; + this.persistMatcherOrderFromDom(); }); - - setting.addExtraButton(button => button - .setIcon('grip-vertical') - .setTooltip(this.plugin.t('drag-copy-target'))); if (customMatcher) { setting.addExtraButton(button => button .setIcon('trash-2') .setTooltip(this.plugin.t('delete-custom-matcher')) .onClick(() => { - this.deleteCustomMatcher(customMatcher.id); + this.deleteCustomMatcher(customMatcher.id, setting.settingEl); })); setting.addExtraButton(button => button .setIcon('settings') @@ -631,43 +742,53 @@ export class EasyCopySettingTab extends PluginSettingTab { if (customMatcher && this.expandedCustomMatcherId === customMatcher.id) { const configEl = activeDocument.createElement('div'); configEl.addClass('easy-copy-matcher-config'); + configEl.dataset.matcherId = customMatcher.id; setting.settingEl.insertAdjacentElement('afterend', configEl); this.renderCustomMatcherConfig(configEl, customMatcher); } }); + return settingEl; } private renderCustomMatcherConfig(containerEl: HTMLElement, customMatcher: CustomCopyMatcherSetting): void { new Setting(containerEl) .setName(this.plugin.t('custom-matcher-name')) - .addText(text => text - .setValue(customMatcher.name) - .onChange(value => { + .setDesc(this.plugin.t('custom-matcher-name-desc')) + .addText(text => { + this.enableConfigInputTabNavigation(text.inputEl); + text.setValue(customMatcher.name) + .onChange(value => { customMatcher.name = value; this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), value || this.plugin.t('custom-matcher'), customMatcher.note ?? ''); void this.plugin.saveSettings(); - })); + }); + }); new Setting(containerEl) .setName(this.plugin.t('custom-matcher-note')) - .addText(text => text - .setValue(customMatcher.note ?? '') - .onChange(value => { + .setDesc(this.plugin.t('custom-matcher-note-desc')) + .addText(text => { + this.enableConfigInputTabNavigation(text.inputEl); + text.setValue(customMatcher.note ?? '') + .onChange(value => { customMatcher.note = value; this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), customMatcher.name || this.plugin.t('custom-matcher'), value); void this.plugin.saveSettings(); - })); + }); + }); - const regexErrorEl = containerEl.createDiv({ cls: 'easy-copy-regex-error' }); + const patternSetting = new Setting(containerEl) + .setName(this.plugin.t('custom-matcher-pattern')) + .setDesc(this.plugin.t('custom-matcher-pattern-desc')); + const regexErrorEl = patternSetting.infoEl.createDiv({ cls: 'easy-copy-regex-error' }); const updateRegexError = (value: string) => { regexErrorEl.setText(value); regexErrorEl.toggleClass('is-visible', Boolean(value)); }; - new Setting(containerEl) - .setName(this.plugin.t('custom-matcher-pattern')) - .addText(text => text - .setPlaceholder('"([^"]+)"') + patternSetting.addText(text => { + this.enableConfigInputTabNavigation(text.inputEl); + text.setPlaceholder('"([^"]+)"') .setValue(customMatcher.pattern) .onChange(value => { if (!this.isRegexValid(value, customMatcher.flags)) { @@ -677,14 +798,17 @@ export class EasyCopySettingTab extends PluginSettingTab { updateRegexError(''); customMatcher.pattern = value; void this.plugin.saveSettings(); - })); + }); + }); new Setting(containerEl) .setName(this.plugin.t('custom-matcher-flags')) - .addText(text => text - .setPlaceholder('g') - .setValue(customMatcher.flags) - .onChange(value => { + .setDesc(this.plugin.t('custom-matcher-flags-desc')) + .addText(text => { + this.enableConfigInputTabNavigation(text.inputEl); + text.setPlaceholder('g') + .setValue(customMatcher.flags) + .onChange(value => { if (!this.isRegexValid(customMatcher.pattern, value)) { updateRegexError(this.plugin.t('invalid-regex')); return; @@ -692,17 +816,21 @@ export class EasyCopySettingTab extends PluginSettingTab { updateRegexError(''); customMatcher.flags = value; void this.plugin.saveSettings(); - })); + }); + }); new Setting(containerEl) .setName(this.plugin.t('custom-matcher-capture-group')) - .addText(text => text - .setPlaceholder('1') - .setValue(String(customMatcher.captureGroup)) - .onChange(value => { + .setDesc(this.plugin.t('custom-matcher-capture-group-desc')) + .addText(text => { + this.enableConfigInputTabNavigation(text.inputEl); + text.setPlaceholder('1') + .setValue(String(customMatcher.captureGroup)) + .onChange(value => { customMatcher.captureGroup = Math.max(0, parseInt(value) || 0); void this.plugin.saveSettings(); - })); + }); + }); } private updateMatcherRowText(matcherId: string, name: string, desc: string): void { @@ -713,24 +841,11 @@ export class EasyCopySettingTab extends PluginSettingTab { } } - private moveMatcherBefore(sourceId: string, targetId: string): void { - if (sourceId === targetId) return; - - const matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers) - .filter(id => id !== sourceId); - const targetIndex = matcherOrder.indexOf(targetId); - if (targetIndex < 0) return; - - matcherOrder.splice(targetIndex, 0, sourceId); - this.plugin.settings.matcherOrder = matcherOrder; - void this.plugin.saveSettings(); - this.display(); - } - - private deleteCustomMatcher(id: string): void { + private deleteCustomMatcher(id: string, rowEl?: HTMLElement): void { this.plugin.settings.customMatchers = this.plugin.settings.customMatchers.filter(matcher => matcher.id !== id); this.plugin.settings.matcherOrder = normalizeMatcherOrder(this.plugin.settings.matcherOrder, this.plugin.settings.customMatchers); + if (this.expandedCustomMatcherId === id) this.closeExpandedCustomMatcherConfig(); + rowEl?.remove(); void this.plugin.saveSettings(); - this.display(); } } diff --git a/src/type.ts b/src/type.ts index 00cd0f2..a588b27 100644 --- a/src/type.ts +++ b/src/type.ts @@ -42,6 +42,7 @@ export type BuiltinCopyMatcherId = | 'strikethrough' | 'inline-code' | 'inline-latex' + | 'link' | 'wiki-link'; export interface ContextData { @@ -122,7 +123,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { enableInlineLatex: true, enableLink: true, enableWikiLink: true, - matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'wiki-link'], + matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'link', 'wiki-link'], customMatchers: [], keepWikiBrackets: true, autoEmbedBlockLink: false, diff --git a/styles.css b/styles.css index 8c978ee..9b6747a 100644 --- a/styles.css +++ b/styles.css @@ -34,6 +34,14 @@ If your plugin does not need CSS, delete this file. cursor: grabbing; } +.easy-copy-matcher-row.is-dragging { + opacity: 0.55; +} + +.easy-copy-matcher-row .mod-drag-handle { + cursor: grab; +} + .easy-copy-matcher-config { margin-left: 1.5em; padding: 0.5em 0 0.75em 0; From ca78f782cfc016b23de9a167bcf0c8e687d1daf5 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 16:22:35 +0800 Subject: [PATCH 8/9] style: move settings info icon styles to css --- src/settingTab.ts | 3 +-- styles.css | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/settingTab.ts b/src/settingTab.ts index 4179d06..7a6f191 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -328,8 +328,7 @@ export class EasyCopySettingTab extends PluginSettingTab { const infoIcon = descFragment.createEl('span', { attr: { 'aria-label': this.plugin.t('resolve-link-path-on-paste-tooltip'), - 'class': 'clickable-icon setting-editor-extra-setting-button', - 'style': 'display:inline; vertical-align:middle; cursor:help;', + 'class': 'setting-editor-extra-setting-button' }, }); setIcon(infoIcon, 'info'); diff --git a/styles.css b/styles.css index 9b6747a..f8c910d 100644 --- a/styles.css +++ b/styles.css @@ -58,3 +58,13 @@ If your plugin does not need CSS, delete this file. .easy-copy-regex-error.is-visible { display: block; } + +.setting-editor-extra-setting-button { + display: inline-block; + vertical-align:middle; + cursor:help; + + &:hover { + color: var(--color-accent); + } +} \ No newline at end of file From 5fb84a97ffb8e5e904b916cb07024d3fe6e5b207 Mon Sep 17 00:00:00 2001 From: Moy Date: Wed, 10 Jun 2026 17:03:23 +0800 Subject: [PATCH 9/9] feat: add AI prompt helper for custom regex rules --- src/i18n.ts | 23 +++++++++++++++++++++++ src/settingTab.ts | 48 +++++++++++++++++++++++++++++++---------------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/i18n.ts b/src/i18n.ts index 7eaf21f..551043a 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -57,6 +57,8 @@ export type TranslationKey = | 'custom-matcher-pattern' | 'custom-matcher-pattern-desc' | 'custom-matcher-flags' | 'custom-matcher-flags-desc' | 'custom-matcher-capture-group' | 'custom-matcher-capture-group-desc' + | 'custom-matcher-default-name' | 'custom-matcher-default-note' + | 'custom-matcher-pattern-tooltip' | 'custom-matcher-ai-prompt' | 'custom-matcher-ai-prompt-copied' | 'configure-custom-matcher' | 'drag-copy-target' | 'invalid-regex'; // 本地化翻译字典 @@ -97,6 +99,7 @@ export const translations: Record> = { 'custom-prefix': '(Custom) ', 'custom-matcher': 'Custom matcher', 'custom-matcher-copied': ' copied!', + 'custom-matcher-ai-prompt-copied': 'AI prompt copied!', 'note-link-simplified': 'Link simplified (filename matches heading)', // 设置界面 @@ -202,6 +205,12 @@ export const translations: Record> = { 'custom-matcher-flags-desc': 'Regular expression flags, such as g or i. The g flag is added automatically when matching.', 'custom-matcher-capture-group': 'Capture group', 'custom-matcher-capture-group-desc': 'Capture group index to copy. Use 1 to copy the first parenthesized group; use 0 to copy the full match.', + 'custom-matcher-default-name': 'Double quotes', + 'custom-matcher-default-note': 'Copy text inside Chinese or English double quotes.', + 'custom-matcher-pattern-tooltip': 'The plugin searches the current line for a capture group matched by the regular expression and copies it. If you do not know what this is, click the icon to copy a prompt and ask AI to generate one for you.', + 'custom-matcher-ai-prompt': `I need a regular expression for matching specific content. +For example, [“"]([^"]+)["”] matches content inside Chinese or English double quotes and captures only the first group. +I want to extract:`, 'configure-custom-matcher': 'Configure', 'drag-copy-target': 'Drag to reorder', 'invalid-regex': 'Invalid regular expression; changes were not saved.', @@ -241,6 +250,7 @@ export const translations: Record> = { 'custom-prefix': '(自定义)', 'custom-matcher': '自定义规则', 'custom-matcher-copied': '已复制!', + 'custom-matcher-ai-prompt-copied': 'AI 提示词已复制!', 'note-link-simplified': '链接已简化(文件名与标题相匹配)', // 设置界面 @@ -345,6 +355,12 @@ export const translations: Record> = { 'custom-matcher-flags-desc': '正则表达式标志,例如 g 或 i。匹配时会自动添加 g 标志。', 'custom-matcher-capture-group': '捕获组', 'custom-matcher-capture-group-desc': '要复制的捕获组序号。1 表示第一个括号捕获组,0 表示完整匹配内容。', + 'custom-matcher-default-name': '双引号', + 'custom-matcher-default-note': '复制中文或英文双引号内的文本。', + 'custom-matcher-pattern-tooltip': '插件会在当前行查找正则表达式匹配的捕获组并进行复制。如果你不了解这是什么,可以点击图标复制提示词,让 AI 帮你生成。', + 'custom-matcher-ai-prompt': `我需要一个正则表达式,用于匹配特定的内容。 +例如,[“"]([^"]+)["”] 代表匹配中英文双引号内的内容,仅捕获第一个组。 +我希望提取的是:`, 'configure-custom-matcher': '配置', 'drag-copy-target': '拖动排序', 'invalid-regex': '无效的正则表达式,修改未保存。', @@ -393,6 +409,7 @@ export const translations: Record> = { 'custom-prefix': '(自定義)', 'custom-matcher': '自定義規則', 'custom-matcher-copied': '已複製!', + 'custom-matcher-ai-prompt-copied': 'AI 提示詞已複製!', 'note-link-simplified': '連結已簡化(檔案名與標題相匹配)', // 設置界面 @@ -489,6 +506,12 @@ export const translations: Record> = { 'custom-matcher-flags-desc': '正則表示式標誌,例如 g 或 i。匹配時會自動添加 g 標誌。', 'custom-matcher-capture-group': '捕獲組', 'custom-matcher-capture-group-desc': '要複製的捕獲組序號。1 表示第一個括號捕獲組,0 表示完整匹配內容。', + 'custom-matcher-default-name': '雙引號', + 'custom-matcher-default-note': '複製中文或英文雙引號內的文字。', + 'custom-matcher-pattern-tooltip': '外掛會在目前行查找正則表示式匹配的捕獲組並進行複製。如果你不了解這是什麼,可以點擊圖示複製提示詞,讓 AI 幫你生成。', + 'custom-matcher-ai-prompt': `我需要一個正則表示式,用於匹配特定的內容。 +例如,[“"]([^"]+)["”] 代表匹配中英文雙引號內的內容,僅捕獲第一個組。 +我希望提取的是:`, 'configure-custom-matcher': '配置', 'drag-copy-target': '拖動排序', 'invalid-regex': '無效的正則表示式,修改未保存。', diff --git a/src/settingTab.ts b/src/settingTab.ts index 7a6f191..f5d48dd 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -1,10 +1,12 @@ import { App, + Notice, PluginSettingTab, Setting, requireApiVersion, setIcon, } from "obsidian"; +import { TranslationKey } from "./i18n"; import * as ObsidianModule from "obsidian"; import EasyCopy from "./main"; import { BuiltinCopyMatcherId, CustomCopyMatcherSetting, LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type"; @@ -51,13 +53,13 @@ const BUILTIN_MATCHER_SETTING_KEYS: Record string): CustomCopyMatcherSetting { const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; return { id, - name: 'Double quotes', - note: 'Copy text inside double quotes.', - pattern: '"([^"]+)"', + name: t('custom-matcher-default-name'), + note: t('custom-matcher-default-note'), + pattern: '[“"]([^"]+)["”]', flags: 'g', captureGroup: 1, enabled: true, @@ -126,7 +128,7 @@ export class EasyCopySettingTab extends PluginSettingTab { private getMatcherName(matcherId: string): string | null { const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId); - if (customMatcher) return customMatcher.name || this.plugin.t('custom-matcher'); + if (customMatcher) return customMatcher.name || this.plugin.t('custom-matcher-default-name'); const builtinKey = BUILTIN_MATCHER_LABEL_KEYS[matcherId as BuiltinCopyMatcherId]; return builtinKey ? this.plugin.t(builtinKey) : null; @@ -651,7 +653,7 @@ export class EasyCopySettingTab extends PluginSettingTab { .setButtonText(this.plugin.t('add-custom-matcher')) .onClick(() => { if (!listContainerEl) return; - const customMatcher = createCustomMatcher(); + const customMatcher = createCustomMatcher(key => this.plugin.t(key)); this.plugin.settings.customMatchers.push(customMatcher); this.plugin.settings.matcherOrder = normalizeMatcherOrder([...this.plugin.settings.matcherOrder, getCustomMatcherOrderId(customMatcher)], this.plugin.settings.customMatchers); void this.plugin.saveSettings(); @@ -757,10 +759,10 @@ export class EasyCopySettingTab extends PluginSettingTab { this.enableConfigInputTabNavigation(text.inputEl); text.setValue(customMatcher.name) .onChange(value => { - customMatcher.name = value; - this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), value || this.plugin.t('custom-matcher'), customMatcher.note ?? ''); - void this.plugin.saveSettings(); - }); + customMatcher.name = value; + this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), value || this.plugin.t('custom-matcher-default-name'), customMatcher.note ?? ''); + void this.plugin.saveSettings(); + }); }); new Setting(containerEl) @@ -770,15 +772,29 @@ export class EasyCopySettingTab extends PluginSettingTab { this.enableConfigInputTabNavigation(text.inputEl); text.setValue(customMatcher.note ?? '') .onChange(value => { - customMatcher.note = value; - this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), customMatcher.name || this.plugin.t('custom-matcher'), value); - void this.plugin.saveSettings(); - }); + customMatcher.note = value; + this.updateMatcherRowText(getCustomMatcherOrderId(customMatcher), customMatcher.name || this.plugin.t('custom-matcher-default-name'), value); + void this.plugin.saveSettings(); + }); }); + const patternDesc = activeDocument.createDocumentFragment(); + patternDesc.append(this.plugin.t('custom-matcher-pattern-desc') + ' '); + const patternInfoIcon = patternDesc.createEl('span', { + attr: { + 'aria-label': this.plugin.t('custom-matcher-pattern-tooltip'), + 'class': 'setting-editor-extra-setting-button', + }, + }); + setIcon(patternInfoIcon, 'info'); + patternInfoIcon.addEventListener('click', async () => { + await navigator.clipboard.writeText(this.plugin.t('custom-matcher-ai-prompt')); + new Notice(this.plugin.t('custom-matcher-ai-prompt-copied')); + }); + const patternSetting = new Setting(containerEl) .setName(this.plugin.t('custom-matcher-pattern')) - .setDesc(this.plugin.t('custom-matcher-pattern-desc')); + .setDesc(patternDesc); const regexErrorEl = patternSetting.infoEl.createDiv({ cls: 'easy-copy-regex-error' }); const updateRegexError = (value: string) => { regexErrorEl.setText(value); @@ -787,7 +803,7 @@ export class EasyCopySettingTab extends PluginSettingTab { patternSetting.addText(text => { this.enableConfigInputTabNavigation(text.inputEl); - text.setPlaceholder('"([^"]+)"') + text.setPlaceholder('[“"]([^"]+)["”]') .setValue(customMatcher.pattern) .onChange(value => { if (!this.isRegexValid(value, customMatcher.flags)) {