mirror of
https://github.com/moyf/easy-copy.git
synced 2026-07-22 05:43:47 +00:00
Two review findings (pass 3) on the full-block text path: - a block id alone on its own line, one blank line below the content, belongs to the block above it (Obsidian's gap form). getBlockText() now jumps back to that block before expanding, so the alias carries the real block text instead of falling back to the block id. - the per-line trailing-id strip accepted a caret glued to content, eating e.g. the exponent of a line-final `2^10`. Obsidian only parses `^id` as a block id when it is whitespace-separated (or alone on the line) — the strip regex now requires the same.
1380 lines
43 KiB
TypeScript
1380 lines
43 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
||
import {
|
||
buildBlockLink,
|
||
buildExplicitPasteLink,
|
||
buildFileLink,
|
||
buildHeadingLink,
|
||
encodeMarkdownLinkUrl,
|
||
extractBlockDisplayText,
|
||
sanitizeHeadingForLink,
|
||
} from './linkBuilder';
|
||
import { LinkFormat } from './type';
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// encodeMarkdownLinkUrl
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('encodeMarkdownLinkUrl', () => {
|
||
it('encodes spaces as %20', () => {
|
||
expect(encodeMarkdownLinkUrl('My Note')).toBe('My%20Note');
|
||
});
|
||
|
||
it('encodes multiple spaces', () => {
|
||
expect(encodeMarkdownLinkUrl('a b c')).toBe('a%20b%20c');
|
||
});
|
||
|
||
it('returns string unchanged when no spaces', () => {
|
||
expect(encodeMarkdownLinkUrl('MyNote#Heading')).toBe('MyNote#Heading');
|
||
});
|
||
|
||
it('does not encode other special characters', () => {
|
||
expect(encodeMarkdownLinkUrl('Note#Head^id')).toBe('Note#Head^id');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// sanitizeHeadingForLink
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('sanitizeHeadingForLink', () => {
|
||
it('strips # | ^ : with surrounding whitespace', () => {
|
||
expect(sanitizeHeadingForLink('Test | If # This ^ Heading : Works'))
|
||
.toBe('Test If This Heading Works');
|
||
});
|
||
|
||
it('strips special chars without surrounding whitespace', () => {
|
||
expect(sanitizeHeadingForLink('Test|If#This^Heading:Works, Too'))
|
||
.toBe('Test If This Heading Works, Too');
|
||
});
|
||
|
||
it('strips only the special chars that are present', () => {
|
||
expect(sanitizeHeadingForLink('Test | If This ^ Heading Works (No Hash or Colon)'))
|
||
.toBe('Test If This Heading Works (No Hash or Colon)');
|
||
});
|
||
|
||
it('strips %% (comment marker)', () => {
|
||
expect(sanitizeHeadingForLink('Before %% comment %% After'))
|
||
.toBe('Before comment After');
|
||
});
|
||
|
||
it('strips [[ and ]]', () => {
|
||
expect(sanitizeHeadingForLink('See [[Other Note]] here'))
|
||
.toBe('See Other Note here');
|
||
});
|
||
|
||
it('returns heading unchanged when no special chars', () => {
|
||
expect(sanitizeHeadingForLink('Normal Heading Text'))
|
||
.toBe('Normal Heading Text');
|
||
});
|
||
|
||
it('handles empty string', () => {
|
||
expect(sanitizeHeadingForLink('')).toBe('');
|
||
});
|
||
|
||
it('handles heading that is only special chars', () => {
|
||
expect(sanitizeHeadingForLink('# | ^')).toBe('');
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// buildHeadingLink
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('buildHeadingLink', () => {
|
||
const defaults = {
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
useHeadingAsDisplayText: true,
|
||
headingLinkSeparator: '#',
|
||
simplifiedHeadingToNoteLink: true,
|
||
};
|
||
|
||
// -- Wiki 格式 ---------------------------------------------------------
|
||
|
||
describe('wiki format', () => {
|
||
it('creates a basic heading link with display text', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Getting Started',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Getting Started|Getting Started]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('simplifies to [[filename]] when heading equals filename exactly', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote]]');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('simplifies linkContent when heading matches filename case-insensitively', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'mynote',
|
||
filename: 'MyNote',
|
||
});
|
||
// linkContent 被简化为 filename,但因 filename !== heading,
|
||
// 会落到 else 分支,比较 displayText 与 linkContent
|
||
expect(result.link).toBe('[[MyNote|mynote]]');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('simplifies when heading matches filename with spaces removed', () => {
|
||
// 例如 filename "SomeThing"、heading "Some Thing"
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
});
|
||
expect(result.link).toBe('[[SomeThing|Some Thing]]');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('omits display text alias when displayText equals linkContent', () => {
|
||
// useHeadingAsDisplayText 为 false 且分隔符为 '#' 时,
|
||
// displayText = "filename#heading",与 linkContent 相同
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Intro',
|
||
filename: 'MyNote',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Intro]]');
|
||
});
|
||
|
||
it('includes alias when using non-default separator', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Intro',
|
||
filename: 'MyNote',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: ' > ',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Intro|MyNote > Intro]]');
|
||
});
|
||
|
||
it('uses frontmatter title in display text when heading-as-display is off', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Setup',
|
||
filename: 'my-note',
|
||
frontmatterTitle: 'My Note',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
// displayText = "My Note#Setup",linkContent = "my-note#Setup"
|
||
expect(result.link).toBe('[[my-note#Setup|My Note#Setup]]');
|
||
});
|
||
|
||
it('strips [[ ]] wrapper from heading', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: '[[Inner Link]]',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Inner Link|Inner Link]]');
|
||
});
|
||
|
||
it('falls back separator to # when headingLinkSeparator is empty', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Intro',
|
||
filename: 'MyNote',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Intro]]');
|
||
});
|
||
});
|
||
|
||
// -- Markdown 格式 ------------------------------------------------------
|
||
|
||
describe('markdown format', () => {
|
||
const md = { ...defaults, linkFormat: LinkFormat.MDLINK };
|
||
|
||
it('creates a basic markdown heading link', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Getting Started',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[Getting Started](MyNote#Getting%20Started)');
|
||
});
|
||
|
||
it('simplifies to file link when filename equals heading exactly', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
});
|
||
// simplifiedHeadingToNoteLink(默认 true)将 MyNote#MyNote 折叠为 MyNote
|
||
expect(result.link).toBe('[MyNote](MyNote)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('uses filename+separator+heading when heading-as-display is off', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Intro',
|
||
filename: 'MyNote',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: ' - ',
|
||
});
|
||
// 显示文本不做编码,只对 URL 部分编码
|
||
expect(result.link).toBe('[MyNote - Intro](MyNote#Intro)');
|
||
});
|
||
|
||
it('uses frontmatter title in display text', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Setup',
|
||
filename: 'my-note',
|
||
frontmatterTitle: 'My Note',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
// filename 和 heading 都没有空格,不需要编码
|
||
expect(result.link).toBe('[My Note#Setup](my-note#Setup)');
|
||
});
|
||
|
||
// -- Markdown 格式下的 simplifiedHeadingToNoteLink --------------------
|
||
// 对应下方 wiki 的「文件名-标题匹配」测试套件。
|
||
|
||
describe('simplifiedHeadingToNoteLink (default: includes match)', () => {
|
||
it('simplifies on case-insensitive exact match', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'mynote',
|
||
filename: 'MyNote',
|
||
});
|
||
// 显示文本保持 heading 的大小写,链接目标保持 filename 的大小写
|
||
expect(result.link).toBe('[mynote](MyNote)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('simplifies on space-removed match', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
});
|
||
expect(result.link).toBe('[Some Thing](SomeThing)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('simplifies when filename contains heading as substring', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Java',
|
||
filename: 'JavaScript',
|
||
});
|
||
expect(result.link).toBe('[Java](JavaScript)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('simplifies for date-prefixed filenames containing heading', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'note',
|
||
filename: '260422_note',
|
||
});
|
||
expect(result.link).toBe('[note](260422_note)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('does not simplify when heading contains filename (asymmetric)', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Notebook',
|
||
filename: 'Note',
|
||
});
|
||
expect(result.link).toBe('[Notebook](Note#Notebook)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('encodes spaces in target when not simplified', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Some Heading',
|
||
filename: 'My Note',
|
||
});
|
||
// 不会触发简化,因此目标保留标题片段
|
||
expect(result.link).toBe('[Some Heading](My%20Note#Some%20Heading)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('encodes spaces in target when simplified to filename', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'My Note',
|
||
filename: 'My Note',
|
||
});
|
||
expect(result.link).toBe('[My Note](My%20Note)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('simplifiedHeadingToNoteLink (strict mode)', () => {
|
||
const strictMd = { ...md, strictHeadingMatch: true };
|
||
|
||
it('does not simplify on substring match', () => {
|
||
const result = buildHeadingLink({
|
||
...strictMd,
|
||
heading: 'Java',
|
||
filename: 'JavaScript',
|
||
});
|
||
expect(result.link).toBe('[Java](JavaScript#Java)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('still simplifies on case-insensitive exact match', () => {
|
||
const result = buildHeadingLink({
|
||
...strictMd,
|
||
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({
|
||
...strictMd,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
});
|
||
expect(result.link).toBe('[Some Thing](SomeThing)');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('simplifiedHeadingToNoteLink gated on useHeadingAsDisplayText=true (A1.5)', () => {
|
||
// 当用户希望显示「filename#heading」时,丢掉标题锚点会
|
||
// 产生误导性的 [filename#heading](filename)。这道闸门保留
|
||
// 标题链接,让显示意图与目标保持一致。
|
||
|
||
it('does NOT simplify on exact match when useHeadingAsDisplayText is false', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
expect(result.link).toBe('[MyNote#MyNote](MyNote#MyNote)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('does NOT simplify on space-removed match when useHeadingAsDisplayText is false', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
expect(result.link).toBe('[SomeThing#Some Thing](SomeThing#Some%20Thing)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('does NOT simplify on substring match when useHeadingAsDisplayText is false', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Java',
|
||
filename: 'JavaScript',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
expect(result.link).toBe('[JavaScript#Java](JavaScript#Java)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('honors custom separator in display text when not simplified', () => {
|
||
const result = buildHeadingLink({
|
||
...md,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: ' > ',
|
||
});
|
||
expect(result.link).toBe('[SomeThing > Some Thing](SomeThing#Some%20Thing)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
});
|
||
});
|
||
|
||
// -- 链接目标中的特殊字符净化 ----------------------
|
||
|
||
describe('special character sanitization', () => {
|
||
it('wiki format: strips special chars from link target, preserves display text', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Test | If # This ^ Heading : Works',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Test If This Heading Works|Test | If # This ^ Heading : Works]]');
|
||
});
|
||
|
||
it('markdown format: strips special chars from URL target', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
heading: 'Test | If # This ^ Heading : Works',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[Test | If # This ^ Heading : Works](MyNote#Test%20If%20This%20Heading%20Works)');
|
||
});
|
||
|
||
it('wiki format: strips chars without surrounding whitespace', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Test|If#This^Heading:Works, Too',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Test If This Heading Works, Too|Test|If#This^Heading:Works, Too]]');
|
||
});
|
||
|
||
it('markdown format: display text preserves original heading', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
heading: 'Test|If#This^Heading:Works, Too',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[Test|If#This^Heading:Works, Too](MyNote#Test%20If%20This%20Heading%20Works,%20Too)');
|
||
});
|
||
});
|
||
|
||
// -- compareIgnoreCase:默认(包含匹配)与严格匹配的对照 -----------
|
||
|
||
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]]');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('does not simplify when heading contains filename as substring', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Notebook',
|
||
filename: 'Note',
|
||
});
|
||
expect(result.link).toBe('[[Note#Notebook|Notebook]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
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);
|
||
});
|
||
});
|
||
|
||
// -- A1.5 闸门:简化受 useHeadingAsDisplayText 约束(wiki 侧) -----
|
||
|
||
describe('simplifiedHeadingToNoteLink gated on useHeadingAsDisplayText=true (A1.5, wiki side)', () => {
|
||
// 对应 markdown 侧的同名套件。当用户要求显示「filename#heading」时,
|
||
// 提前简化分支会被跳过,从而保留标题链接。
|
||
// 例外:WIKI 精确匹配的特例 [[filename]] 始终保留——
|
||
// 它在 Obsidian 中会被渲染为干净的文件名。
|
||
|
||
const wikiNoDisplay = {
|
||
...defaults,
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
};
|
||
|
||
it('PRESERVES the [[filename]] special case on exact match (the A1.5 exception)', () => {
|
||
const result = buildHeadingLink({
|
||
...wikiNoDisplay,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote]]');
|
||
expect(result.isNoteLink).toBe(true);
|
||
});
|
||
|
||
it('does NOT simplify on case-insensitive match', () => {
|
||
const result = buildHeadingLink({
|
||
...wikiNoDisplay,
|
||
heading: 'mynote',
|
||
filename: 'MyNote',
|
||
});
|
||
// displayText === linkContent → alias 折叠,但标题片段保留
|
||
expect(result.link).toBe('[[MyNote#mynote]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('does NOT simplify on space-removed match', () => {
|
||
const result = buildHeadingLink({
|
||
...wikiNoDisplay,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
});
|
||
expect(result.link).toBe('[[SomeThing#Some Thing]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('does NOT simplify on substring match', () => {
|
||
const result = buildHeadingLink({
|
||
...wikiNoDisplay,
|
||
heading: 'Java',
|
||
filename: 'JavaScript',
|
||
});
|
||
expect(result.link).toBe('[[JavaScript#Java]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('honors custom separator alias when not simplified', () => {
|
||
const result = buildHeadingLink({
|
||
...wikiNoDisplay,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
headingLinkSeparator: ' > ',
|
||
});
|
||
// displayText 「SomeThing > Some Thing」!== linkContent「SomeThing#Some Thing」
|
||
expect(result.link).toBe('[[SomeThing#Some Thing|SomeThing > Some Thing]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
});
|
||
|
||
// -- simplifiedHeadingToNoteLink 关闭时 ---------------------------------
|
||
|
||
describe('when simplifiedHeadingToNoteLink is false', () => {
|
||
const noSimplify = { ...defaults, simplifiedHeadingToNoteLink: false };
|
||
|
||
it('keeps full heading link when filename equals heading exactly (wiki)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#MyNote|MyNote]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('keeps full heading link on case-insensitive match (wiki)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
heading: 'mynote',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#mynote|mynote]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('keeps full heading link on space-removed match (wiki)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
heading: 'Some Thing',
|
||
filename: 'SomeThing',
|
||
});
|
||
expect(result.link).toBe('[[SomeThing#Some Thing|Some Thing]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('keeps full heading link on filename-includes-heading match (wiki)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
heading: 'note',
|
||
filename: '260422_note',
|
||
});
|
||
expect(result.link).toBe('[[260422_note#note|note]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('does not affect non-matching filename/heading (sanity check)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
heading: 'Getting Started',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#Getting Started|Getting Started]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('markdown format keeps heading fragment when simplification is off (sanity check)', () => {
|
||
const result = buildHeadingLink({
|
||
...noSimplify,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
heading: 'MyNote',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[MyNote](MyNote#MyNote)');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
});
|
||
|
||
// -- 组合场景 --------------------------------------------------------
|
||
|
||
describe('combinatorial', () => {
|
||
it('frontmatterTitle has no effect when useHeadingAsDisplayText is true', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'Setup',
|
||
filename: 'my-note',
|
||
frontmatterTitle: 'My Custom Title',
|
||
useHeadingAsDisplayText: true,
|
||
});
|
||
// 显示文本是 heading,而不是 frontmatter title
|
||
expect(result.link).toBe('[[my-note#Setup|Setup]]');
|
||
});
|
||
|
||
it('frontmatterTitle + heading link kept under useHeadingAsDisplayText=false (wiki, A1.5)', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: 'myNote',
|
||
filename: 'MyNote',
|
||
frontmatterTitle: 'My Pretty Title',
|
||
useHeadingAsDisplayText: false,
|
||
headingLinkSeparator: '#',
|
||
});
|
||
// A1.5 之前会简化为 [[MyNote|My Pretty Title#myNote]](目标=文件,显示=标题样式)。
|
||
// A1.5 保留标题锚点,使目标与显示意图保持一致。
|
||
expect(result.link).toBe('[[MyNote#myNote|My Pretty Title#myNote]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('empty heading produces a heading link with empty fragment (known bug)', () => {
|
||
// 空 heading 来自 Obsidian UI 的边缘情况。
|
||
// 链接目标 (MyNote#) 严格说是畸形的,但当前行为如此。
|
||
// 应在后续 PR 中处理。
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
heading: '',
|
||
filename: 'MyNote',
|
||
});
|
||
expect(result.link).toBe('[[MyNote#|]]');
|
||
expect(result.isNoteLink).toBe(false);
|
||
});
|
||
|
||
it('markdown format URL-encodes spaces in link URL', () => {
|
||
const result = buildHeadingLink({
|
||
...defaults,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
heading: 'My Heading',
|
||
filename: 'My Note',
|
||
});
|
||
expect(result.link).toBe('[My Heading](My%20Note#My%20Heading)');
|
||
});
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// extractBlockDisplayText
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('extractBlockDisplayText', () => {
|
||
describe('text cleaning', () => {
|
||
it('removes block ID suffix (^...)', () => {
|
||
const result = extractBlockDisplayText('Some text ^abc123', 'fallback', 3, 5);
|
||
expect(result).toBe('Some text');
|
||
});
|
||
|
||
it('removes checkbox prefix (- [x] )', () => {
|
||
const result = extractBlockDisplayText('- [x] Task done', 'fallback', 3, 5);
|
||
expect(result).toBe('Task done');
|
||
});
|
||
|
||
it('removes list prefix (- )', () => {
|
||
const result = extractBlockDisplayText('- List item here today', 'fallback', 3, 5);
|
||
expect(result).toBe('List item here');
|
||
});
|
||
|
||
it('removes markdown syntax characters', () => {
|
||
const result = extractBlockDisplayText('**bold** and `code`', 'fallback', 5, 5);
|
||
expect(result).toBe('bold and code');
|
||
});
|
||
|
||
it('returns blockId for empty/whitespace-only text', () => {
|
||
expect(extractBlockDisplayText('', 'myId', 3, 5)).toBe('myId');
|
||
expect(extractBlockDisplayText(' ', 'myId', 3, 5)).toBe('myId');
|
||
});
|
||
|
||
it('removes callout prefix (> )', () => {
|
||
const result = extractBlockDisplayText('> Some quoted text here', 'fallback', 3, 5);
|
||
expect(result).toBe('Some quoted text');
|
||
});
|
||
});
|
||
|
||
describe('multi-line blocks', () => {
|
||
it('joins soft-wrapped continuation lines with spaces', () => {
|
||
const result = extractBlockDisplayText('- Evolutionary values\n zeroth existence', 'fallback', 99, 99);
|
||
expect(result).toBe('Evolutionary values zeroth existence');
|
||
});
|
||
|
||
it('strips a trailing block id from any line of the block', () => {
|
||
const result = extractBlockDisplayText('- first part ^abc123\n second part ^def-456', 'fallback', 99, 99);
|
||
expect(result).toBe('first part second part');
|
||
});
|
||
|
||
it('drops blank lines when joining', () => {
|
||
const result = extractBlockDisplayText('first paragraph\n\n second paragraph', 'fallback', 99, 99);
|
||
expect(result).toBe('first paragraph second paragraph');
|
||
});
|
||
|
||
it('still applies the word limit to the joined text', () => {
|
||
const result = extractBlockDisplayText('one two\n three four five', 'fallback', 3, 5);
|
||
expect(result).toBe('one two three');
|
||
});
|
||
|
||
it('keeps a mid-line caret intact', () => {
|
||
const result = extractBlockDisplayText('value is 2^10 here', 'fallback', 99, 99);
|
||
expect(result).toBe('value is 2^10 here');
|
||
});
|
||
|
||
it('keeps an end-of-line caret expression intact (block ids need a separator)', () => {
|
||
const result = extractBlockDisplayText('first line ^abc123\n the result is 2^10', 'fallback', 99, 99);
|
||
expect(result).toBe('first line the result is 2^10');
|
||
});
|
||
|
||
it('strips pipes that would break the wiki link alias', () => {
|
||
const result = extractBlockDisplayText('| col1 | col2 |', 'fallback', 99, 99);
|
||
expect(result).not.toContain('|');
|
||
expect(result).toContain('col1');
|
||
expect(result).toContain('col2');
|
||
});
|
||
});
|
||
|
||
describe('English text', () => {
|
||
it('extracts first 3 words by default', () => {
|
||
const result = extractBlockDisplayText('The quick brown fox jumps', 'fallback', 3, 5);
|
||
expect(result).toBe('The quick brown');
|
||
});
|
||
|
||
it('respects custom word limit', () => {
|
||
const result = extractBlockDisplayText('The quick brown fox jumps', 'fallback', 5, 5);
|
||
expect(result).toBe('The quick brown fox jumps');
|
||
});
|
||
|
||
it('handles text with fewer words than limit', () => {
|
||
const result = extractBlockDisplayText('Hello world', 'fallback', 3, 5);
|
||
expect(result).toBe('Hello world');
|
||
});
|
||
|
||
it('handles single word', () => {
|
||
const result = extractBlockDisplayText('Hello', 'fallback', 3, 5);
|
||
expect(result).toBe('Hello');
|
||
});
|
||
|
||
it('defaults word limit to 3 if zero is passed', () => {
|
||
const result = extractBlockDisplayText('one two three four', 'fallback', 0, 5);
|
||
expect(result).toBe('one two three');
|
||
});
|
||
});
|
||
|
||
describe('CJK text', () => {
|
||
it('returns text as-is when within char limit', () => {
|
||
const result = extractBlockDisplayText('Hello', 'fallback', 3, 5);
|
||
// "Hello" 是 ASCII / 英文,会走英文分支
|
||
expect(result).toBe('Hello');
|
||
});
|
||
|
||
it('truncates with ellipsis when text exceeds char limit and no separator found', () => {
|
||
const result = extractBlockDisplayText('这是一段很长的中文文本没有标点', 'fallback', 3, 5);
|
||
expect(result).toBe('这是一段很...');
|
||
});
|
||
|
||
it('uses punctuation as separator when match is 3+ chars', () => {
|
||
// '三个字' 共 3 个字符(>= 3 且 <= charLimit),命中分隔符匹配分支
|
||
const result = extractBlockDisplayText('三个字,后面还有很多内容', 'fallback', 3, 5);
|
||
expect(result).toBe('三个字');
|
||
});
|
||
|
||
it('falls back to char limit slice when separator match is too short (< 3 chars)', () => {
|
||
// '短语' 共 2 个字符(< 3),回退到 text.slice(0, charLimit)
|
||
const result = extractBlockDisplayText('短语,后面还有很多内容', 'fallback', 3, 5);
|
||
expect(result).toBe('短语,后面');
|
||
});
|
||
|
||
it('returns full text when within char limit', () => {
|
||
const result = extractBlockDisplayText('短い文', 'fallback', 3, 5);
|
||
expect(result).toBe('短い文');
|
||
});
|
||
|
||
it('defaults char limit to 5 if zero is passed', () => {
|
||
const result = extractBlockDisplayText('这是一段很长的中文文本', 'fallback', 3, 0);
|
||
expect(result).toBe('这是一段很...');
|
||
});
|
||
});
|
||
|
||
// -- 边界值 ------------------------------------------------------
|
||
|
||
describe('boundary values', () => {
|
||
it('English: exactly wordLimit words returns all words', () => {
|
||
const result = extractBlockDisplayText('one two three', 'fallback', 3, 5);
|
||
expect(result).toBe('one two three');
|
||
});
|
||
|
||
it('English: wordLimit + 1 words truncates', () => {
|
||
const result = extractBlockDisplayText('one two three four', 'fallback', 3, 5);
|
||
expect(result).toBe('one two three');
|
||
});
|
||
|
||
it('English: word limit of 1', () => {
|
||
const result = extractBlockDisplayText('Hello world', 'fallback', 1, 5);
|
||
expect(result).toBe('Hello');
|
||
});
|
||
|
||
it('CJK: exactly charLimit chars returns text as-is', () => {
|
||
const result = extractBlockDisplayText('这是一段文', 'fallback', 3, 5);
|
||
expect(result).toBe('这是一段文');
|
||
});
|
||
|
||
it('CJK: charLimit + 1 chars with no separator triggers truncation', () => {
|
||
const result = extractBlockDisplayText('这是一段文本', 'fallback', 3, 5);
|
||
expect(result).toBe('这是一段文...');
|
||
});
|
||
|
||
it('CJK: char limit of 1 truncates with ellipsis', () => {
|
||
const result = extractBlockDisplayText('很长的文本', 'fallback', 3, 1);
|
||
expect(result).toBe('很...');
|
||
});
|
||
});
|
||
|
||
// -- 混合内容 --------------------------------------------------------
|
||
|
||
describe('mixed content', () => {
|
||
it('mixed English/CJK is treated as non-English', () => {
|
||
// 出现任意非 ASCII 字符时,纯 ASCII 正则会失败
|
||
const result = extractBlockDisplayText('Hello 世界 and more text', 'fallback', 3, 5);
|
||
expect(result).toBe('Hello');
|
||
});
|
||
|
||
it('text that is all markdown syntax becomes empty after cleaning', () => {
|
||
const result = extractBlockDisplayText('**[]**', 'myId', 3, 5);
|
||
expect(result).toBe('myId');
|
||
});
|
||
|
||
it('checkbox with different state markers', () => {
|
||
expect(extractBlockDisplayText('- [>] Deferred task here', 'id', 3, 5))
|
||
.toBe('Deferred task here');
|
||
expect(extractBlockDisplayText('- [ ] Unchecked task here', 'id', 3, 5))
|
||
.toBe('Unchecked task here');
|
||
});
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// buildBlockLink
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('buildBlockLink', () => {
|
||
const defaults = {
|
||
filename: 'MyNote',
|
||
useBrief: true,
|
||
blockText: 'The quick brown fox',
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
autoBlockDisplayText: true,
|
||
autoEmbedBlockLink: false,
|
||
blockDisplayWordLimit: 3,
|
||
blockDisplayCharLimit: 5,
|
||
};
|
||
|
||
describe('wiki format', () => {
|
||
it('creates a block link with brief display text', () => {
|
||
const result = buildBlockLink({ ...defaults, blockId: 'abc123' });
|
||
expect(result).toBe('[[MyNote#^abc123|The quick brown]]');
|
||
});
|
||
|
||
it('uses blockId as display text when useBrief is false', () => {
|
||
const result = buildBlockLink({ ...defaults, blockId: 'abc123', useBrief: false });
|
||
expect(result).toBe('[[MyNote#^abc123|abc123]]');
|
||
});
|
||
|
||
it('omits display text when autoBlockDisplayText is false', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'abc123',
|
||
autoBlockDisplayText: false,
|
||
});
|
||
expect(result).toBe('[[MyNote#^abc123]]');
|
||
});
|
||
|
||
it('adds ! embed prefix', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'abc123',
|
||
autoEmbedBlockLink: true,
|
||
});
|
||
expect(result).toBe('![[MyNote#^abc123|The quick brown]]');
|
||
});
|
||
});
|
||
|
||
describe('markdown format', () => {
|
||
const md = { ...defaults, linkFormat: LinkFormat.MDLINK };
|
||
|
||
it('creates a markdown block link with display text', () => {
|
||
const result = buildBlockLink({ ...md, blockId: 'abc123' });
|
||
expect(result).toBe('[The quick brown](MyNote#^abc123)');
|
||
});
|
||
|
||
it('omits display text when autoBlockDisplayText is false', () => {
|
||
const result = buildBlockLink({
|
||
...md,
|
||
blockId: 'abc123',
|
||
autoBlockDisplayText: false,
|
||
});
|
||
expect(result).toBe('[](MyNote#^abc123)');
|
||
});
|
||
|
||
it('adds ! embed prefix in markdown format', () => {
|
||
const result = buildBlockLink({
|
||
...md,
|
||
blockId: 'abc123',
|
||
autoEmbedBlockLink: true,
|
||
});
|
||
expect(result).toBe('');
|
||
});
|
||
});
|
||
|
||
it('uses blockId as display when blockText is empty', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'abc123',
|
||
blockText: '',
|
||
});
|
||
expect(result).toBe('[[MyNote#^abc123|abc123]]');
|
||
});
|
||
|
||
// -- 组合场景 --------------------------------------------------------
|
||
|
||
describe('combinatorial', () => {
|
||
it('embed + no display text (wiki) produces ![[...]]', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'abc123',
|
||
autoBlockDisplayText: false,
|
||
autoEmbedBlockLink: true,
|
||
});
|
||
expect(result).toBe('![[MyNote#^abc123]]');
|
||
});
|
||
|
||
it('embed + no display text (markdown) produces image-like syntax', () => {
|
||
//  在标准 markdown 中其实是图片嵌入语法——
|
||
// 此处仅记录该组合会生成这种语法
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
blockId: 'abc123',
|
||
autoBlockDisplayText: false,
|
||
autoEmbedBlockLink: true,
|
||
});
|
||
expect(result).toBe('');
|
||
});
|
||
|
||
it('block ID with hyphens and underscores', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'quote-of_the-day',
|
||
});
|
||
expect(result).toBe('[[MyNote#^quote-of_the-day|The quick brown]]');
|
||
});
|
||
|
||
it('markdown format encodes spaces in filename', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
linkFormat: LinkFormat.MDLINK,
|
||
blockId: 'abc123',
|
||
filename: 'My Note',
|
||
});
|
||
expect(result).toBe('[The quick brown](My%20Note#^abc123)');
|
||
});
|
||
|
||
it('wiki format does NOT encode spaces in filename', () => {
|
||
const result = buildBlockLink({
|
||
...defaults,
|
||
blockId: 'abc123',
|
||
filename: 'My Note',
|
||
});
|
||
expect(result).toBe('[[My Note#^abc123|The quick brown]]');
|
||
});
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// buildFileLink
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('buildFileLink', () => {
|
||
describe('wiki format', () => {
|
||
it('creates a wiki file link', () => {
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'folder/MyNote.md',
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
});
|
||
expect(result).toBe('[[MyNote|MyNote]]');
|
||
});
|
||
|
||
it('uses custom display text', () => {
|
||
const result = buildFileLink({
|
||
filename: 'my-note',
|
||
filePath: 'folder/my-note.md',
|
||
displayText: 'My Note',
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
});
|
||
expect(result).toBe('[[my-note|My Note]]');
|
||
});
|
||
});
|
||
|
||
describe('markdown format', () => {
|
||
it('creates a markdown file link and strips .md', () => {
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'folder/MyNote.md',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[MyNote](folder/MyNote)');
|
||
});
|
||
|
||
it('normalizes backslashes in path', () => {
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'folder\\subfolder\\MyNote.md',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[MyNote](folder/subfolder/MyNote)');
|
||
});
|
||
|
||
it('uses custom display text', () => {
|
||
const result = buildFileLink({
|
||
filename: 'my-note',
|
||
filePath: 'folder/my-note.md',
|
||
displayText: 'My Note',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[My Note](folder/my-note)');
|
||
});
|
||
|
||
it('handles paths without .md extension', () => {
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'folder/MyNote',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[MyNote](folder/MyNote)');
|
||
});
|
||
|
||
it('encodes spaces as %20 in path', () => {
|
||
const result = buildFileLink({
|
||
filename: 'My Note',
|
||
filePath: 'my folder/My Note.md',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[My Note](my%20folder/My%20Note)');
|
||
});
|
||
|
||
it('root-level file with no directory prefix', () => {
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'MyNote.md',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[MyNote](MyNote)');
|
||
});
|
||
});
|
||
|
||
// -- 边缘情况 -----------------------------------------------------------
|
||
|
||
describe('edge cases', () => {
|
||
it('wiki format without displayText produces redundant alias', () => {
|
||
// [[name|name]]——alias 与文件名重复
|
||
const result = buildFileLink({
|
||
filename: 'MyNote',
|
||
filePath: 'folder/MyNote.md',
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
});
|
||
expect(result).toBe('[[MyNote|MyNote]]');
|
||
});
|
||
|
||
it('filename with parentheses in markdown format', () => {
|
||
// 严格的解析器中,括号会破坏 [text](path) 语法
|
||
const result = buildFileLink({
|
||
filename: 'Note (draft)',
|
||
filePath: 'folder/Note (draft).md',
|
||
linkFormat: LinkFormat.MDLINK,
|
||
});
|
||
expect(result).toBe('[Note (draft)](folder/Note%20(draft))');
|
||
});
|
||
|
||
it('filename with pipe in wiki format', () => {
|
||
// | 是 alias 分隔符——文件名中含 | 会造成歧义
|
||
const result = buildFileLink({
|
||
filename: 'Pros | Cons',
|
||
filePath: 'Pros | Cons.md',
|
||
displayText: 'Comparison',
|
||
linkFormat: LinkFormat.WIKILINK,
|
||
});
|
||
expect(result).toBe('[[Pros | Cons|Comparison]]');
|
||
});
|
||
});
|
||
});
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// buildExplicitPasteLink
|
||
// ---------------------------------------------------------------------------
|
||
|
||
describe('buildExplicitPasteLink', () => {
|
||
describe('wiki format', () => {
|
||
it('cross-file with subpath + alias produces [[path#H|alias]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[[SomeThing#Other Heading|Other Heading]]');
|
||
});
|
||
|
||
it('cross-file with subpath, omitAlias=true produces [[path#H]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: false,
|
||
omitAlias: true,
|
||
})).toBe('[[SomeThing#Other Heading]]');
|
||
});
|
||
|
||
it('same-file with subpath drops path portion to [[#H|alias]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: true,
|
||
omitAlias: false,
|
||
})).toBe('[[#Other Heading|Other Heading]]');
|
||
});
|
||
|
||
it('same-file with subpath + omitAlias produces [[#H]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: true,
|
||
omitAlias: true,
|
||
})).toBe('[[#Other Heading]]');
|
||
});
|
||
|
||
it('cross-file file link (empty subpath) with alias', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '',
|
||
alias: 'Some Thing',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[[SomeThing|Some Thing]]');
|
||
});
|
||
|
||
it('cross-file file link with empty alias yields [[path]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '',
|
||
alias: '',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[[SomeThing]]');
|
||
});
|
||
|
||
it('block-link cross-file: omitAlias=false (caller-decided) keeps alias', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#^abc123',
|
||
alias: 'The quick brown',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[[SomeThing#^abc123|The quick brown]]');
|
||
});
|
||
|
||
it('block-link same-file produces [[#^id|alias]]', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'SomeThing',
|
||
subpath: '#^abc123',
|
||
alias: 'The quick brown',
|
||
sameFile: true,
|
||
omitAlias: false,
|
||
})).toBe('[[#^abc123|The quick brown]]');
|
||
});
|
||
|
||
it('does not encode spaces in path (wiki-link convention)', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.WIKILINK,
|
||
path: 'My Note',
|
||
subpath: '#Some Heading',
|
||
alias: 'Some Heading',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[[My Note#Some Heading|Some Heading]]');
|
||
});
|
||
});
|
||
|
||
describe('markdown format', () => {
|
||
it('cross-file with subpath + alias produces [alias](path#H)', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[Other Heading](SomeThing#Other%20Heading)');
|
||
});
|
||
|
||
it('same-file produces [alias](#H) with no path', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: true,
|
||
omitAlias: false,
|
||
})).toBe('[Other Heading](#Other%20Heading)');
|
||
});
|
||
|
||
it('omitAlias=true preserves alias as display (MD never produces empty [])', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '#Other Heading',
|
||
alias: 'Other Heading',
|
||
sameFile: true,
|
||
omitAlias: true,
|
||
})).toBe('[Other Heading](#Other%20Heading)');
|
||
});
|
||
|
||
it('cross-file file link (empty subpath) with alias', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '',
|
||
alias: 'Some Thing',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[Some Thing](SomeThing)');
|
||
});
|
||
|
||
it('encodes spaces in path as %20', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'My Note',
|
||
subpath: '#Some Heading',
|
||
alias: 'Some Heading',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[Some Heading](My%20Note#Some%20Heading)');
|
||
});
|
||
|
||
it('encodes spaces in same-file fragment', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'irrelevant',
|
||
subpath: '#Some Heading',
|
||
alias: 'Some Heading',
|
||
sameFile: true,
|
||
omitAlias: false,
|
||
})).toBe('[Some Heading](#Some%20Heading)');
|
||
});
|
||
|
||
it('block-link cross-file', () => {
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '#^abc123',
|
||
alias: 'The quick brown',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[The quick brown](SomeThing#^abc123)');
|
||
});
|
||
|
||
it('empty alias yields []() — caller is expected to provide non-empty alias', () => {
|
||
// 边界记录:buildHeadingCopyMetadata 在 isNoteLink 且
|
||
// filename===heading 时会清空 alias。handlePaste 应在抵达
|
||
// 这里之前作为退化的自引用情况(skip)处理掉。
|
||
expect(buildExplicitPasteLink({
|
||
format: LinkFormat.MDLINK,
|
||
path: 'SomeThing',
|
||
subpath: '',
|
||
alias: '',
|
||
sameFile: false,
|
||
omitAlias: false,
|
||
})).toBe('[](SomeThing)');
|
||
});
|
||
});
|
||
});
|