mirror of
https://github.com/moyf/easy-copy.git
synced 2026-07-22 05:43:47 +00:00
Merge 5fb84a97ff into 1d1bda8c80
This commit is contained in:
commit
6e8458c5c2
8 changed files with 942 additions and 184 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -31,3 +31,4 @@ easy-copy/
|
|||
# env
|
||||
.env
|
||||
/.sisyphus
|
||||
/.omo
|
||||
|
|
|
|||
108
src/copyMatcher.test.ts
Normal file
108
src/copyMatcher.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
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',
|
||||
'link',
|
||||
'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);
|
||||
});
|
||||
});
|
||||
152
src/copyMatcher.ts
Normal file
152
src/copyMatcher.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { BuiltinCopyMatcherId, ContextType, CustomCopyMatcherSetting, EasyCopySettings } from './type';
|
||||
|
||||
export const DEFAULT_BUILTIN_COPY_MATCHER_IDS: BuiltinCopyMatcherId[] = [
|
||||
'bold',
|
||||
'italic',
|
||||
'highlight',
|
||||
'strikethrough',
|
||||
'inline-code',
|
||||
'inline-latex',
|
||||
'link',
|
||||
'wiki-link',
|
||||
];
|
||||
|
||||
export interface CopyMatcher {
|
||||
id: string;
|
||||
type: ContextType;
|
||||
enabled: boolean;
|
||||
regex: RegExp;
|
||||
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[] {
|
||||
const knownIds = new Set<string>(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 normalizeMatcherOrder(order: readonly string[] | undefined, customMatchers: readonly CustomCopyMatcherSetting[]): string[] {
|
||||
const customIds = new Set(customMatchers.map(getCustomMatcherOrderId));
|
||||
const knownIds = new Set<string>([...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 平台使用只带前视的正则表达式,其他平台使用完整版本
|
||||
const italicRegex = isIosApp ?
|
||||
/(?:\*([^*]+)\*(?!\*)|_([^_]+)_(?!_))/g :
|
||||
/(?:(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_))/g;
|
||||
|
||||
const boldRegex = /(?:\*\*([^*]+)\*\*|__([^_]+)__)/g;
|
||||
|
||||
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 },
|
||||
{ id: 'strikethrough', type: ContextType.STRIKETHROUGH, regex: /~~([^~]+)~~/g, enabled: !settings.customizeTargets || settings.enableStrikethrough },
|
||||
{ id: 'inline-code', type: ContextType.INLINECODE, regex: /`([^`]+)`/g, enabled: !settings.customizeTargets || settings.enableInlineCode },
|
||||
{ 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))
|
||||
.filter((matcher): matcher is CopyMatcher => Boolean(matcher));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
174
src/i18n.ts
174
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-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'
|
||||
|
|
@ -49,7 +49,17 @@ 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'
|
||||
| 'custom-matchers' | 'custom-matchers-desc' | 'add-custom-matcher' | 'delete-custom-matcher'
|
||||
| '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'
|
||||
| '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';
|
||||
|
||||
// 本地化翻译字典
|
||||
export const translations: Record<Language, Record<TranslationKey, string>> = {
|
||||
|
|
@ -86,6 +96,10 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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-ai-prompt-copied': 'AI prompt copied!',
|
||||
'note-link-simplified': 'Link simplified (filename matches heading)',
|
||||
|
||||
// 设置界面
|
||||
|
|
@ -120,27 +134,27 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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-desc': 'Enable copying italic text like *italic example*',
|
||||
'enable-strikethrough': 'Enable strikethrough text',
|
||||
'enable-italic': 'Italic text',
|
||||
'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': '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.',
|
||||
'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',
|
||||
'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',
|
||||
|
|
@ -172,6 +186,34 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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': '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 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-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.',
|
||||
'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.',
|
||||
},
|
||||
[Language.ZH]: {
|
||||
// 复制 Block ID
|
||||
|
|
@ -205,6 +247,10 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'wiki-link-copied': 'Wiki链接已复制!',
|
||||
'callout-copied': '标注内容已复制!',
|
||||
'code-block-copied': '代码块已复制!',
|
||||
'custom-prefix': '(自定义)',
|
||||
'custom-matcher': '自定义规则',
|
||||
'custom-matcher-copied': '已复制!',
|
||||
'custom-matcher-ai-prompt-copied': 'AI 提示词已复制!',
|
||||
'note-link-simplified': '链接已简化(文件名与标题相匹配)',
|
||||
|
||||
// 设置界面
|
||||
|
|
@ -234,22 +280,22 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'resolve-link-path-on-paste-desc': '粘贴时根据目标文件重新生成链接路径。使用"跟随 Obsidian 设置"时,会沿用软件设置中的路径风格(最短/相对/绝对);否则,仅使用最短唯一路径。',
|
||||
'resolve-link-path-on-paste-tooltip': '越早加载的插件越先处理粘贴事件。若其他插件(如 Linter)抢先处理,本功能会被跳过。可在 community-plugins.json 文件中调整插件加载顺序。',
|
||||
'customize-targets': '自定义复制对象',
|
||||
'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认可复制所有元素)',
|
||||
'enable-inline-code': '启用行内代码',
|
||||
'customize-targets-desc': '启用后可以自定义哪些元素可以被复制(不启用则默认复制所有插件预设对象,如加粗、高亮等)',
|
||||
'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-desc': '启用复制斜体文本,如 *斜体示例*',
|
||||
'enable-strikethrough': '启用删除线文本',
|
||||
'enable-italic': '斜体文本',
|
||||
'enable-italic-desc': '启用复制斜体文本,如 *斜体示例* 或 _斜体示例_',
|
||||
'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-link': '链接文本',
|
||||
'enable-link-desc': '启用复制 Markdown 链接,如 [标题](链接),会基于光标所在位置复制不同内容',
|
||||
'enable-wikilink': 'Wiki 链接',
|
||||
'enable-wikilink-desc': '启用复制 [[Wiki]] 链接',
|
||||
'special-format': '特殊复制格式选项',
|
||||
'auto-embed-block-link': '块链接:自动添加 ! 符号(嵌入块)',
|
||||
|
|
@ -290,6 +336,34 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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': '重置顺序',
|
||||
'custom-matchers': '自定义复制规则',
|
||||
'custom-matchers-desc': '添加基于正则表达式的匹配规则,以适配更多的符号。只针对当前行生效。',
|
||||
'add-custom-matcher': '添加自定义对象',
|
||||
'delete-custom-matcher': '删除',
|
||||
'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': '正则表达式标志,例如 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': '无效的正则表达式,修改未保存。',
|
||||
},
|
||||
[Language.ZH_TW]: {
|
||||
// 复制 Block ID
|
||||
|
|
@ -331,6 +405,11 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'link-url-copied': '連結地址已複製!',
|
||||
'wiki-link-copied': 'Wiki連結已複製!',
|
||||
'callout-copied': '标注内容已複製!',
|
||||
'code-block-copied': '代碼塊已複製!',
|
||||
'custom-prefix': '(自定義)',
|
||||
'custom-matcher': '自定義規則',
|
||||
'custom-matcher-copied': '已複製!',
|
||||
'custom-matcher-ai-prompt-copied': 'AI 提示詞已複製!',
|
||||
'note-link-simplified': '連結已簡化(檔案名與標題相匹配)',
|
||||
|
||||
// 設置界面
|
||||
|
|
@ -359,21 +438,21 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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-desc': '啟用複製斜體文本,如 *斜體示例*',
|
||||
'enable-strikethrough': '啟用刪除線文本',
|
||||
'enable-italic': '斜體文本',
|
||||
'enable-italic-desc': '啟用複製斜體文本,如 *斜體示例* 或 _斜體示例_',
|
||||
'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-link': '連結文本',
|
||||
'enable-link-desc': '啟用複製Markdown連結,如 [標題](連結),會基於游標所在位置複製不同內容',
|
||||
'enable-wikilink': 'Wiki 連結',
|
||||
'enable-wikilink-desc': '啟用複製 [[Wiki]] 連結',
|
||||
'special-format': '特殊複製格式選項',
|
||||
'auto-embed-block-link': '塊連結:自動添加 ! 符號(嵌入塊)',
|
||||
|
|
@ -389,7 +468,6 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'code-block-copy-with-fences': '複製代碼塊(含 ```)',
|
||||
'code-block-generate-block-link': '生成塊連結',
|
||||
'code-block-disabled': '禁用',
|
||||
'code-block-copied': '代碼塊已複製!',
|
||||
'keep-wiki-brackets': 'Wiki連結:保留 [[ ]] 括號',
|
||||
'keep-wiki-brackets-desc': '複製 wiki 連結時保留兩側 [[ ]] 括號',
|
||||
|
||||
|
|
@ -409,6 +487,34 @@ export const translations: Record<Language, Record<TranslationKey, string>> = {
|
|||
'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': '重置順序',
|
||||
'custom-matchers': '自定義複製規則',
|
||||
'custom-matchers-desc': '添加基於正則表示式的匹配規則,以適配更多的符號。只針對當前行生效。',
|
||||
'add-custom-matcher': '添加自定義對象',
|
||||
'delete-custom-matcher': '刪除',
|
||||
'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': '正則表示式標誌,例如 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': '無效的正則表示式,修改未保存。',
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
101
src/main.ts
101
src/main.ts
|
|
@ -1,9 +1,10 @@
|
|||
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';
|
||||
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';
|
||||
|
|
@ -160,6 +161,11 @@ export default class EasyCopy extends Plugin {
|
|||
|
||||
async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
this.settings.customMatchers = (this.settings.customMatchers ?? []).map(matcher => ({
|
||||
note: '',
|
||||
...matcher,
|
||||
}));
|
||||
this.settings.matcherOrder = normalizeMatcherOrder(this.settings.matcherOrder, this.settings.customMatchers);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
|
|
@ -330,7 +336,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);
|
||||
|
|
@ -414,48 +420,35 @@ export default class EasyCopy extends Plugin {
|
|||
const afterCursor = curLine.slice(curCh); // 光标后的内容
|
||||
|
||||
|
||||
// iOS 16.4 之前不支持后视(Lookbehinds),但支持前视(Lookaheads)
|
||||
// 所以针对 iOS 平台使用只带前视的正则表达式,其他平台使用完整版本
|
||||
const italicRegex = Platform.isIosApp ?
|
||||
/(?:\*([^*]+)\*(?!\*)|_([^_]+)_(?!_))/g : // iOS 版本:支持 * 和 _ 格式,只使用前视
|
||||
/(?:(?<!\*)\*([^*]+)\*(?!\*)|(?<!_)_([^_]+)_(?!_))/g; // 其他平台:支持 * 和 _ 格式,使用前视和后视
|
||||
|
||||
const boldRegex = /(?:\*\*([^*]+)\*\*|__([^_]+)__)/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 },
|
||||
];
|
||||
// 匹配优先级由设置中的 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.enable) continue; // 如果当前类型未启用,则跳过
|
||||
const matchInfo = this.getMatchInfo(beforeCursor, afterCursor, matcher.regex);
|
||||
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 {
|
||||
type: matcher.type,
|
||||
curLine,
|
||||
match: matchInfo.content, // 返回内容,不包括语法
|
||||
range: matchInfo.range,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 检测链接
|
||||
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,
|
||||
matcherName: matcher.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -467,7 +460,7 @@ export default class EasyCopy extends Plugin {
|
|||
}
|
||||
|
||||
// 检测 block ID
|
||||
const blockIdInfo = this.detectBlockId(editor, view);
|
||||
const blockIdInfo = this.detectBlockId(editor);
|
||||
if (blockIdInfo) {
|
||||
return blockIdInfo;
|
||||
}
|
||||
|
|
@ -504,32 +497,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;
|
||||
|
|
@ -638,6 +605,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(`${this.t('custom-prefix')}${contextType.matcherName ?? this.t('custom-matcher')}${this.t('custom-matcher-copied')}`);
|
||||
}
|
||||
return;
|
||||
|
||||
case ContextType.LINKTITLE:
|
||||
// 复制链接标题
|
||||
|
|
|
|||
|
|
@ -1,18 +1,71 @@
|
|||
import {
|
||||
App,
|
||||
Notice,
|
||||
PluginSettingTab,
|
||||
Setting,
|
||||
requireApiVersion,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import { TranslationKey } from "./i18n";
|
||||
import * as ObsidianModule from "obsidian";
|
||||
import EasyCopy from "./main";
|
||||
import { LinkFormat, BlockIdInsertPosition, CodeBlockBehavior } from "./type";
|
||||
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;
|
||||
}
|
||||
|
||||
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<BuiltinCopyMatcherId, BuiltinMatcherLabelKey> = {
|
||||
'bold': 'enable-bold',
|
||||
'italic': 'enable-italic',
|
||||
'highlight': 'enable-highlight',
|
||||
'strikethrough': 'enable-strikethrough',
|
||||
'inline-code': 'enable-inline-code',
|
||||
'inline-latex': 'enable-inline-latex',
|
||||
'link': 'enable-link',
|
||||
'wiki-link': 'enable-wikilink',
|
||||
};
|
||||
|
||||
const BUILTIN_MATCHER_DESC_KEYS: Record<BuiltinCopyMatcherId, BuiltinMatcherDescKey> = {
|
||||
'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',
|
||||
'link': 'enable-link-desc',
|
||||
'wiki-link': 'enable-wikilink-desc',
|
||||
};
|
||||
|
||||
const BUILTIN_MATCHER_SETTING_KEYS: Record<BuiltinCopyMatcherId, BuiltinMatcherSettingKey> = {
|
||||
'bold': 'enableBold',
|
||||
'italic': 'enableItalic',
|
||||
'highlight': 'enableHighlight',
|
||||
'strikethrough': 'enableStrikethrough',
|
||||
'inline-code': 'enableInlineCode',
|
||||
'inline-latex': 'enableInlineLatex',
|
||||
'link': 'enableLink',
|
||||
'wiki-link': 'enableWikiLink',
|
||||
};
|
||||
|
||||
function createCustomMatcher(t: (key: TranslationKey) => string): CustomCopyMatcherSetting {
|
||||
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
return {
|
||||
id,
|
||||
name: t('custom-matcher-default-name'),
|
||||
note: t('custom-matcher-default-note'),
|
||||
pattern: '[“"]([^"]+)["”]',
|
||||
flags: 'g',
|
||||
captureGroup: 1,
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -62,6 +115,9 @@ function createSettingsGroup(containerEl: HTMLElement, heading?: string): Settin
|
|||
|
||||
export class EasyCopySettingTab extends PluginSettingTab {
|
||||
plugin: EasyCopy;
|
||||
private expandedCustomMatcherId: string | null = null;
|
||||
private expandedCustomMatcherEl: HTMLElement | null = null;
|
||||
private draggedMatcherId: string | null = null;
|
||||
|
||||
icon: string = 'copy-plus';
|
||||
|
||||
|
|
@ -70,10 +126,151 @@ 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-default-name');
|
||||
|
||||
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 toggleCustomMatcherConfig(settingEl: HTMLElement, customMatcher: CustomCopyMatcherSetting): void {
|
||||
const existingConfigEl = this.containerEl.querySelector<HTMLElement>(`.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;
|
||||
this.expandedCustomMatcherId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
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<HTMLElement>('.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<HTMLElement>(`.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<HTMLElement>(`.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);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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<HTMLInputElement>('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<HTMLInputElement>('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;
|
||||
|
||||
containerEl.empty();
|
||||
this.expandedCustomMatcherEl = null;
|
||||
|
||||
const generalGroup = createSettingsGroup(containerEl);
|
||||
|
||||
|
|
@ -133,8 +330,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');
|
||||
|
|
@ -343,89 +539,8 @@ export class EasyCopySettingTab extends PluginSettingTab {
|
|||
this.display();
|
||||
})));
|
||||
|
||||
// 只有当自定义复制对象选项开启时才显示具体的复制对象选项
|
||||
if (this.plugin.settings.customizeTargets) {
|
||||
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(); // 切换后刷新界面以显示/隐藏下方选项
|
||||
})));
|
||||
|
||||
this.renderMatcherList(targetGroup);
|
||||
}
|
||||
|
||||
targetGroup.addSetting(setting => setting
|
||||
|
|
@ -514,4 +629,238 @@ 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'))
|
||||
.setDesc(this.plugin.t('custom-matchers-desc'))
|
||||
.addButton(button => button
|
||||
.setButtonText(this.plugin.t('add-custom-matcher'))
|
||||
.onClick(() => {
|
||||
if (!listContainerEl) return;
|
||||
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();
|
||||
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(listContainerEl, matcherId);
|
||||
}
|
||||
}
|
||||
|
||||
private addMatcherSetting(containerEl: HTMLElement, matcherId: string): HTMLElement | null {
|
||||
const matcherName = this.getMatcherName(matcherId);
|
||||
if (!matcherName) return null;
|
||||
|
||||
const customMatcher = this.plugin.settings.customMatchers.find(matcher => getCustomMatcherOrderId(matcher) === matcherId);
|
||||
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();
|
||||
this.persistMatcherOrderFromDom();
|
||||
});
|
||||
setting.settingEl.addEventListener('dragend', () => {
|
||||
setting.settingEl.removeClass('is-dragging');
|
||||
this.draggedMatcherId = null;
|
||||
this.persistMatcherOrderFromDom();
|
||||
});
|
||||
|
||||
if (customMatcher) {
|
||||
setting.addExtraButton(button => button
|
||||
.setIcon('trash-2')
|
||||
.setTooltip(this.plugin.t('delete-custom-matcher'))
|
||||
.onClick(() => {
|
||||
this.deleteCustomMatcher(customMatcher.id, setting.settingEl);
|
||||
}));
|
||||
setting.addExtraButton(button => button
|
||||
.setIcon('settings')
|
||||
.setTooltip(this.plugin.t('configure-custom-matcher'))
|
||||
.onClick(() => {
|
||||
this.toggleCustomMatcherConfig(setting.settingEl, customMatcher);
|
||||
}));
|
||||
}
|
||||
|
||||
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');
|
||||
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'))
|
||||
.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-default-name'), customMatcher.note ?? '');
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(this.plugin.t('custom-matcher-note'))
|
||||
.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-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(patternDesc);
|
||||
const regexErrorEl = patternSetting.infoEl.createDiv({ cls: 'easy-copy-regex-error' });
|
||||
const updateRegexError = (value: string) => {
|
||||
regexErrorEl.setText(value);
|
||||
regexErrorEl.toggleClass('is-visible', Boolean(value));
|
||||
};
|
||||
|
||||
patternSetting.addText(text => {
|
||||
this.enableConfigInputTabNavigation(text.inputEl);
|
||||
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'))
|
||||
.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;
|
||||
}
|
||||
updateRegexError('');
|
||||
customMatcher.flags = value;
|
||||
void this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(this.plugin.t('custom-matcher-capture-group'))
|
||||
.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 {
|
||||
for (const row of Array.from(this.containerEl.querySelectorAll<HTMLElement>('.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 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
src/type.ts
28
src/type.ts
|
|
@ -30,15 +30,37 @@ export enum ContextType {
|
|||
LINKTITLE = 'link-title',
|
||||
LINEURL = 'line-url',
|
||||
WIKILINK = 'wiki-link', // 光标在 [[双链]] 内
|
||||
CUSTOM = 'custom', // 用户自定义正则复制对象
|
||||
CALLOUT = 'callout', // 光标在 callout 区块内
|
||||
CODEBLOCK = 'code-block', // 光标在代码块内
|
||||
}
|
||||
|
||||
export type BuiltinCopyMatcherId =
|
||||
| 'bold'
|
||||
| 'italic'
|
||||
| 'highlight'
|
||||
| 'strikethrough'
|
||||
| 'inline-code'
|
||||
| 'inline-latex'
|
||||
| 'link'
|
||||
| 'wiki-link';
|
||||
|
||||
export interface ContextData {
|
||||
type: ContextType;
|
||||
curLine: string;
|
||||
match: string | null;
|
||||
range: [number, number] | null;
|
||||
matcherName?: string;
|
||||
}
|
||||
|
||||
export interface CustomCopyMatcherSetting {
|
||||
id: string;
|
||||
name: string;
|
||||
note?: string;
|
||||
pattern: string;
|
||||
flags: string;
|
||||
captureGroup: number;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface EasyCopySettings {
|
||||
|
|
@ -62,6 +84,8 @@ export interface EasyCopySettings {
|
|||
enableInlineLatex: boolean;
|
||||
enableLink: boolean;
|
||||
enableWikiLink: boolean; // 是否启用 Wiki 链接复制
|
||||
matcherOrder: string[]; // 复制对象匹配优先级
|
||||
customMatchers: CustomCopyMatcherSetting[]; // 用户自定义正则复制对象
|
||||
keepWikiBrackets: boolean; // 复制 wiki-link 时保留 [[ ]]
|
||||
autoEmbedBlockLink: boolean; // 复制块链接时自动添加 !(嵌入块)
|
||||
enableCalloutCopy: boolean; // 是否启用复制 Callout 内文本
|
||||
|
|
@ -99,6 +123,8 @@ export const DEFAULT_SETTINGS: EasyCopySettings = {
|
|||
enableInlineLatex: true,
|
||||
enableLink: true,
|
||||
enableWikiLink: true,
|
||||
matcherOrder: ['bold', 'italic', 'highlight', 'strikethrough', 'inline-code', 'inline-latex', 'link', 'wiki-link'],
|
||||
customMatchers: [],
|
||||
keepWikiBrackets: true,
|
||||
autoEmbedBlockLink: false,
|
||||
enableCalloutCopy: true,
|
||||
|
|
@ -113,4 +139,4 @@ export const DEFAULT_SETTINGS: EasyCopySettings = {
|
|||
enableDisplayNameRegex: false,
|
||||
displayNameRegexFrom: '',
|
||||
displayNameRegexTo: '',
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
styles.css
43
styles.css
|
|
@ -24,4 +24,47 @@ If your plugin does not need CSS, delete this file.
|
|||
width: 100%;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.easy-copy-matcher-row {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.easy-copy-matcher-row:active {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
.setting-editor-extra-setting-button {
|
||||
display: inline-block;
|
||||
vertical-align:middle;
|
||||
cursor:help;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue