diff --git a/src/i18n.ts b/src/i18n.ts index 89cd663..6e497f5 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -15,6 +15,7 @@ export type TranslationKey = | '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' | 'simplified-heading-to-note-link' | 'simplified-heading-to-note-link-desc' + | 'strict-heading-match' | 'strict-heading-match-desc' | 'use-frontmatter-as-display' | 'use-frontmatter-as-display-desc' | 'frontmatter-key' | 'frontmatter-key-desc' | 'link-format'| 'link-format-desc' | 'link-format-obsidian' | 'markdown-link' | 'wiki-link' | 'contextual-copy' | 'copy-current-file-link' | 'file-link-copied' @@ -97,6 +98,8 @@ export const translations: Record> = { 'use-heading-as-display-desc': 'Use the heading text as display text in copied heading links', 'simplified-heading-to-note-link': 'Simplify link when filename matches heading', 'simplified-heading-to-note-link-desc': 'When the filename matches the heading text (ignoring spaces), create a note link instead of a heading link', + 'strict-heading-match': 'Strict heading match', + 'strict-heading-match-desc': 'When enabled, filename must exactly match the heading (case-insensitive). When disabled, the link is also simplified if the filename contains the heading as a substring (e.g. "260422_note" matches heading "note").', 'heading-link-separator': 'Heading Link: Separator between filename and heading', 'heading-link-separator-desc': 'Customize the separator symbol between filename and heading (only shown when "Use heading as display text" is disabled)', 'link-format': 'Link format', @@ -193,6 +196,8 @@ export const translations: Record> = { 'use-heading-as-display-desc': '在复制的标题链接中,使用标题文本作为显示文本', 'simplified-heading-to-note-link': '文件名匹配标题时简化链接', 'simplified-heading-to-note-link-desc': '当文件名与标题文本匹配时(相同或包含),直接创建笔记链接而不是标题链接', + 'strict-heading-match': '严格匹配', + 'strict-heading-match-desc': '启用后,文件名必须与标题完全相同(忽略大小写)才会简化。关闭时,文件名包含标题也会简化(如文件名 "260422_note" 匹配标题 "note")。', 'heading-link-separator': '标题链接:文件名与标题间的连接符', 'heading-link-separator-desc': '自定义文件名与标题之间的连接符号(仅在禁用"使用标题作为显示文本"时显示)', 'link-format': '链接格式', @@ -299,6 +304,8 @@ export const translations: Record> = { 'use-heading-as-display-desc': '在複製的標題連結中,使用標題文本作為顯示文本', 'simplified-heading-to-note-link': '檔案名匹配標題時簡化連結', 'simplified-heading-to-note-link-desc': '當檔案名與標題文本匹配時(相同或包含),直接建立筆記連結而不是標題連結', + 'strict-heading-match': '嚴格匹配', + 'strict-heading-match-desc': '啟用後,檔案名必須與標題完全相同(忽略大小寫)才會簡化。關閉時,檔案名包含標題也會簡化(如檔案名 "260422_note" 匹配標題 "note")。', 'heading-link-separator': '標題連結:檔案名與標題間的連接符', 'heading-link-separator-desc': '自定義檔案名與標題之間的連接符號(僅在禁用「使用標題作為顯示文本」時顯示)', 'link-format': '連結格式', diff --git a/src/linkBuilder.test.ts b/src/linkBuilder.test.ts index 99bfa73..6f04940 100644 --- a/src/linkBuilder.test.ts +++ b/src/linkBuilder.test.ts @@ -276,17 +276,17 @@ describe('buildHeadingLink', () => { }); }); - // -- compareIgnoreCase: strict equality (no substring matching) ----------- + // -- compareIgnoreCase: default (includes) vs strict matching ----------- - describe('filename-heading matching (strict equality)', () => { - it('does not simplify when filename contains heading as substring', () => { + describe('filename-heading matching (default: includes)', () => { + it('simplifies when filename contains heading as substring', () => { const result = buildHeadingLink({ ...defaults, heading: 'Java', filename: 'JavaScript', }); - expect(result.link).toBe('[[JavaScript#Java|Java]]'); - expect(result.isNoteLink).toBe(false); + expect(result.link).toBe('[[JavaScript|Java]]'); + expect(result.isNoteLink).toBe(true); }); it('does not simplify when heading contains filename as substring', () => { @@ -299,15 +299,69 @@ describe('buildHeadingLink', () => { expect(result.isNoteLink).toBe(false); }); - it('does not simplify on space-removed substring match', () => { + it('simplifies on space-removed substring match', () => { const result = buildHeadingLink({ ...defaults, heading: 'Some Thing', filename: 'SomeThingElse', }); + expect(result.link).toBe('[[SomeThingElse|Some Thing]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('simplifies when filename contains heading (e.g. date-prefixed notes)', () => { + const result = buildHeadingLink({ + ...defaults, + heading: 'note', + filename: '260422_note', + }); + expect(result.link).toBe('[[260422_note|note]]'); + expect(result.isNoteLink).toBe(true); + }); + }); + + describe('filename-heading matching (strict mode)', () => { + const strict = { ...defaults, strictHeadingMatch: true }; + + it('does not simplify when filename contains heading as substring', () => { + const result = buildHeadingLink({ + ...strict, + heading: 'Java', + filename: 'JavaScript', + }); + expect(result.link).toBe('[[JavaScript#Java|Java]]'); + expect(result.isNoteLink).toBe(false); + }); + + it('does not simplify on space-removed substring match', () => { + const result = buildHeadingLink({ + ...strict, + heading: 'Some Thing', + filename: 'SomeThingElse', + }); expect(result.link).toBe('[[SomeThingElse#Some Thing|Some Thing]]'); expect(result.isNoteLink).toBe(false); }); + + it('still simplifies on exact case-insensitive match', () => { + const result = buildHeadingLink({ + ...strict, + heading: 'mynote', + filename: 'MyNote', + }); + expect(result.link).toBe('[[MyNote|mynote]]'); + expect(result.isNoteLink).toBe(true); + }); + + it('still simplifies on space-removed exact match', () => { + const result = buildHeadingLink({ + ...strict, + heading: 'Some Thing', + filename: 'SomeThing', + }); + expect(result.link).toBe('[[SomeThing|Some Thing]]'); + expect(result.isNoteLink).toBe(true); + }); }); // -- Combinatorial -------------------------------------------------------- diff --git a/src/linkBuilder.ts b/src/linkBuilder.ts index eca6cc8..f5a0091 100644 --- a/src/linkBuilder.ts +++ b/src/linkBuilder.ts @@ -34,6 +34,7 @@ export interface BuildHeadingLinkOptions { linkFormat: LinkFormat; useHeadingAsDisplayText: boolean; headingLinkSeparator: string; + strictHeadingMatch?: boolean; } export interface BuildHeadingLinkResult { @@ -42,7 +43,7 @@ export interface BuildHeadingLinkResult { } export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeadingLinkResult { - const { filename, linkFormat, useHeadingAsDisplayText, headingLinkSeparator } = options; + const { filename, linkFormat, useHeadingAsDisplayText, headingLinkSeparator, strictHeadingMatch } = options; const filenameOrTitle = options.frontmatterTitle || filename; // 提取标题文本和级别 @@ -65,7 +66,9 @@ export function buildHeadingLink(options: BuildHeadingLinkOptions): BuildHeading let isNoteLink = false; const compareIgnoreCase = (a: string, b: string): boolean => - a.toLowerCase() === b.toLowerCase(); + strictHeadingMatch + ? a.toLowerCase() === b.toLowerCase() + : a.toLowerCase() === b.toLowerCase() || a.toLowerCase().includes(b.toLowerCase()); // 特殊情况:如果文件名包含标题,则不添加指向标题的 # 部分 // 我自己的情况——会把 SomeThing 给拆成 Some Thing 来做标题,所以也考虑空格替换的部分 diff --git a/src/main.ts b/src/main.ts index ce3492b..4a0f43c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -563,6 +563,7 @@ export default class EasyCopy extends Plugin { linkFormat: this.getEffectiveLinkFormat(), useHeadingAsDisplayText: this.settings.useHeadingAsDisplayText, headingLinkSeparator: this.settings.headingLinkSeparator, + strictHeadingMatch: this.settings.strictHeadingMatch, }); navigator.clipboard.writeText(link); diff --git a/src/settingTab.ts b/src/settingTab.ts index f23fbfc..dba644e 100644 --- a/src/settingTab.ts +++ b/src/settingTab.ts @@ -159,6 +159,18 @@ export class EasyCopySettingTab extends PluginSettingTab { this.display(); }))); + if (this.plugin.settings.simplifiedHeadingToNoteLink) { + formatGroup.addSetting(setting => setting + .setName(this.plugin.t('strict-heading-match')) + .setDesc(this.plugin.t('strict-heading-match-desc')) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.strictHeadingMatch) + .onChange(async (value) => { + this.plugin.settings.strictHeadingMatch = value; + await this.plugin.saveSettings(); + }))); + } + // 新增:是否使用 frontmatter 属性作为显示文本 formatGroup.addSetting(setting => setting diff --git a/src/type.ts b/src/type.ts index f5c1d05..85ebef2 100644 --- a/src/type.ts +++ b/src/type.ts @@ -42,6 +42,7 @@ export interface EasyCopySettings { useHeadingAsDisplayText: boolean; headingLinkSeparator: string; // 文件名和标题间的连接符 simplifiedHeadingToNoteLink: boolean; // 是否简化标题到笔记链接 + strictHeadingMatch: boolean; // 标题与文件名简化匹配时是否使用严格匹配 linkFormat: LinkFormat; customizeTargets: boolean; enableInlineCode: boolean; @@ -73,6 +74,7 @@ export const DEFAULT_SETTINGS: EasyCopySettings = { useHeadingAsDisplayText: true, headingLinkSeparator: '#', simplifiedHeadingToNoteLink: true, + strictHeadingMatch: false, linkFormat: LinkFormat.OBSIDIAN, customizeTargets: false, enableInlineCode: true,