kdnk_obsidian-automatic-linker/src/replace-links/__tests__/prev.test.ts
Kodai Nakamura 706bbf91ef feat: add AI Link Enhancer for resolving ambiguous links
- Implemented AI-powered link disambiguation using local LLMs (Gemma 4 / LM Studio).
- Refactored Trie and candidate mapping to support multiple link candidates per word.
- Added 'AI Link Enhancer' command with a progress bar UI in Obsidian.
- Enhanced link replacement logic to verify existing links and resolve ambiguities using context.
- Added comprehensive unit tests for AI disambiguation and candidate mapping.
- Updated settings to include AI configuration (endpoint, model, context length).
2026-04-06 22:59:52 +09:00

1334 lines
40 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, expect, it } from "vitest"
import { PathAndAliases } from "../../path-and-aliases.types"
import { buildCandidateTrie, buildTrie, CandidateData } from "../../trie"
import { replaceLinks } from "../replace-links"
import { buildCandidateTrieForTest } from "./test-helpers"
describe("basic", () => {
it("replaces links", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "hello",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[hello]]")
})
it("replaces links with bullet", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "- hello",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- [[hello]]")
})
it("replaces links with other texts", () => {
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "world hello",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("world [[hello]]")
}
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "hello world",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[hello]] world")
}
})
it("replaces links with other texts and bullet", () => {
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "- world hello",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- world [[hello]]")
}
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }],
settings,
})
const result = replaceLinks({
body: "- hello world",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- [[hello]] world")
}
})
it("replaces multiple links", () => {
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }, { path: "world" }],
settings,
})
const result = replaceLinks({
body: "hello world",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[hello]] [[world]]")
}
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }, { path: "world" }],
settings,
})
const result = replaceLinks({
body: "\nhello\nworld\n",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("\n[[hello]]\n[[world]]\n")
}
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }, { path: "world" }],
settings,
})
const result = replaceLinks({
body: "\nhello\nworld aaaaa\n",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("\n[[hello]]\n[[world]] aaaaa\n")
}
{
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "hello" }, { path: "world" }],
settings,
})
const result = replaceLinks({
body: "\n aaaaa hello\nworld bbbbb\n",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("\n aaaaa [[hello]]\n[[world]] bbbbb\n")
}
})
})
describe("complex fileNames", () => {
it("unmatched namespace", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "namespace/tag1" }, { path: "namespace/tag2" }],
settings,
})
const result = replaceLinks({
body: "namespace",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("namespace")
})
it("single namespace", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "namespace/tag1" }, { path: "namespace/tag2" }],
settings,
})
const result = replaceLinks({
body: "namespace/tag1",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/tag1|tag1]]")
})
it("multiple namespaces", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace/tag1" },
{ path: "namespace/tag2" },
{ path: "namespace" },
],
settings,
})
const result = replaceLinks({
body: "namespace/tag1 namespace/tag2",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/tag1|tag1]] [[namespace/tag2|tag2]]")
})
})
describe("containing CJK", () => {
it("unmatched namespace", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "namespace/タグ" }],
settings,
})
const result = replaceLinks({
body: "namespace",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("namespace")
})
it("multiple namespaces", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace/tag1" },
{ path: "namespace/tag2" },
{ path: "namespace/タグ3" },
],
settings,
})
const result = replaceLinks({
body: "namespace/tag1 namespace/tag2 namespace/タグ3",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe(
"[[namespace/tag1|tag1]] [[namespace/tag2|tag2]] [[namespace/タグ3|タグ3]]",
)
})
})
describe("starting CJK", () => {
it("unmatched namespace", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "namespace/タグ" }],
settings,
})
const result = replaceLinks({
body: "名前空間",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("名前空間")
})
it("single namespace", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "名前空間/tag1" },
{ path: "名前空間/tag2" },
{ path: "名前空間/タグ3" },
],
settings,
})
const result = replaceLinks({
body: "名前空間/tag1",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[名前空間/tag1|tag1]]")
})
it("multiple namespaces", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "名前空間/tag1" },
{ path: "名前空間/tag2" },
{ path: "名前空間/タグ3" },
],
settings,
})
const result = replaceLinks({
body: "名前空間/tag1 名前空間/tag2 名前空間/タグ3",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe(
"[[名前空間/tag1|tag1]] [[名前空間/tag2|tag2]] [[名前空間/タグ3|タグ3]]",
)
})
it("multiple CJK words", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "漢字" }, { path: "ひらがな" }],
settings,
})
const result = replaceLinks({
body: "- 漢字 ひらがな",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- [[漢字]] [[ひらがな]]")
})
it("multiple same CJK words", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "ひらがな" }],
settings,
})
const result = replaceLinks({
body: "- ひらがなとひらがな",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- [[ひらがな]]と[[ひらがな]]")
})
})
describe("CJK - Korean", () => {
it("converts Korean words to links", () => {
// 韓国語の候補ファイル
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "한글" }, { path: "테스트" }, { path: "예시" }],
settings,
})
const result = replaceLinks({
body: "한글 테스트 예시",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[한글]] [[테스트]] [[예시]]")
})
it("converts Korean words within sentence", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "문서" }],
settings,
})
const result = replaceLinks({
body: "이 문서는 문서이다.",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("이 문서는 [[문서]]이다.")
})
})
describe("CJK - Chinese", () => {
it("converts Chinese words to links", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "汉字" }, { path: "测试" }, { path: "示例" }],
settings,
})
const result = replaceLinks({
body: "汉字 测试 示例",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[汉字]] [[测试]] [[示例]]")
})
it("converts Chinese words within sentence", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "文档" }],
settings,
})
const result = replaceLinks({
body: "这个文档很好。",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("这个[[文档]]很好。")
})
})
describe("base character (pages)", () => {
it("unmatched namespace", () => {
const settings = {
scoped: false,
baseDir: "pages",
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "pages/tags" }],
settings,
})
const result = replaceLinks({
body: "tags",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[tags]]")
})
})
it("multiple links in the same line", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "pages/tags" }, { path: "サウナ" }, { path: "tags" }],
settings,
})
const result = replaceLinks({
body: "サウナ tags pages/tags",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[サウナ]] [[tags]] [[pages/tags|tags]]")
})
describe("nested links", () => {
it("", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "アジャイルリーダーコンピテンシーマップ" },
{ path: "リーダー" },
],
settings,
})
const result = replaceLinks({
body: "アジャイルリーダーコンピテンシーマップ",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[アジャイルリーダーコンピテンシーマップ]]")
})
it("existing links", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "アジャイルリーダーコンピテンシーマップ" },
{ path: "リーダー" },
],
settings,
})
const result = replaceLinks({
body: "[[アジャイルリーダーコンピテンシーマップ]]",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[アジャイルリーダーコンピテンシーマップ]]")
})
})
describe("aliases", () => {
it("replaces alias with canonical form using file path and alias", () => {
const settings = {
scoped: false,
baseDir: "pages",
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "pages/HelloWorld", aliases: ["Hello", "HW"] }],
settings,
})
const result1 = replaceLinks({
body: "Hello",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result1).toBe("[[HelloWorld|Hello]]")
const result2 = replaceLinks({
body: "HelloWorld",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result2).toBe("[[HelloWorld]]")
})
it("replaces multiple occurrences of alias and normal candidate", () => {
const files: PathAndAliases[] = [
{
path: "pages/HelloWorld",
aliases: ["Hello"],
scoped: false,
},
]
const settings = {
baseDir: "pages",
}
const { candidateMap, trie } = buildCandidateTrie(files, "pages")
const result = replaceLinks({
body: "Hello HelloWorld",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[HelloWorld|Hello]] [[HelloWorld]]")
})
})
describe("namespace resolution", () => {
it("replaces candidate with namespace when full candidate is provided", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "namespaces/link" }],
settings,
})
const result = replaceLinks({
body: "namespaces/link",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespaces/link|link]]")
})
it("replaces candidate without namespace correctly", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "link" }],
settings,
})
const result = replaceLinks({
body: "link",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[link]]")
})
it("should not replace YYY-MM-DD formatted text when it doesn't match the candidate's shorthand", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "2025/02/08" }],
settings,
})
const result = replaceLinks({
body: "2025-02-08",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("2025-02-08")
})
})
describe("namespace resolution nearlest file path", () => {
it("closest siblings namespace should be used", () => {
{
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace/a/b/c/d/link" },
{ path: "namespace/a/b/c/d/e/f/link" },
{ path: "namespace/a/b/c/link" },
],
settings,
})
const result = replaceLinks({
body: "link",
linkResolverContext: {
filePath: "namespace/a/b/c/current-file",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/a/b/c/link|link]]")
}
{
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace/a/b/c/link" },
{ path: "namespace/a/b/c/d/link" },
{ path: "namespace/a/b/c/d/e/f/link" },
],
settings,
})
const result = replaceLinks({
body: "link",
linkResolverContext: {
filePath: "namespace/a/b/c/d/current-file",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/a/b/c/d/link|link]]")
}
{
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace/xxx/link" },
{ path: "another-namespace/link" },
{ path: "another-namespace/a/b/c/link" },
{ path: "another-namespace/a/b/c/d/link" },
{ path: "another-namespace/a/b/c/d/e/f/link" },
],
settings,
})
const result = replaceLinks({
body: "link",
linkResolverContext: {
filePath: "namespace/current-file",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/xxx/link|link]]")
}
})
it("closest children namespace should be used", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace1/subnamespace/link" },
{ path: "namespace2/super-super-long-long-directory/link" },
{ path: "namespace3/link" },
{ path: "namespace/a/b/c/link" },
{ path: "namespace/a/b/c/d/link" },
{ path: "namespace/a/b/c/d/e/f/link" },
],
settings,
})
const result = replaceLinks({
body: "link",
linkResolverContext: {
filePath: "namespace/a/b/current-file",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[namespace/a/b/c/link|link]]")
})
it("find closest path if the current path is in base dir and the candidate is not", () => {
const settings = {
scoped: false,
baseDir: "base",
proximityBasedLinking: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "namespace1/aaaaaaaaaaaaaaaaaaaaaaaaa/link" },
{ path: "namespace1/link2" },
{ path: "namespace2/link2" },
{ path: "namespace3/aaaaaa/bbbbbb/link2" },
{
path: "base/looooooooooooooooooooooooooooooooooooooong/link",
},
{
path: "base/looooooooooooooooooooooooooooooooooooooong/super-super-long-long-long-long-closest-sub-dir/link",
},
{ path: "base/a/b/c/link" },
{ path: "base/a/b/c/d/link" },
{ path: "base/a/b/c/d/e/f/link" },
],
settings,
})
const result = replaceLinks({
body: "link link2",
linkResolverContext: {
filePath: "base/current-file",
trie,
candidateMap,
},
settings,
})
// longset path should be used
expect(result).toBe(
"[[looooooooooooooooooooooooooooooooooooooong/link|link]] [[namespace1/link2|link2]]",
)
const settings2 = {
scoped: false,
baseDir: "base",
proximityBasedLinking: false,
}
const result2 = replaceLinks({
body: "link link2",
linkResolverContext: {
filePath: "base/current-file",
trie,
candidateMap,
},
settings: settings2,
})
expect(result2).toBe("link link2")
})
})
it("ignore month notes", () => {
const settings = {
scoped: false,
baseDir: undefined,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [
{ path: "01" },
{ path: "02" },
{ path: "03" },
{ path: "04" },
{ path: "05" },
{ path: "06" },
{ path: "07" },
{ path: "08" },
{ path: "09" },
{ path: "10" },
{ path: "11" },
{ path: "12" },
{ path: "1" },
{ path: "2" },
{ path: "3" },
{ path: "4" },
{ path: "5" },
{ path: "6" },
{ path: "7" },
{ path: "8" },
{ path: "9" },
{ path: "namespace/01" },
{ path: "namespace/02" },
{ path: "namespace/03" },
{ path: "namespace/04" },
{ path: "namespace/05" },
{ path: "namespace/06" },
{ path: "namespace/07" },
{ path: "namespace/08" },
{ path: "namespace/09" },
{ path: "namespace/10" },
{ path: "namespace/11" },
{ path: "namespace/12" },
{ path: "namespace/1" },
{ path: "namespace/2" },
{ path: "namespace/3" },
{ path: "namespace/4" },
{ path: "namespace/5" },
{ path: "namespace/6" },
{ path: "namespace/7" },
{ path: "namespace/8" },
{ path: "namespace/9" },
],
settings,
})
const result = replaceLinks({
body: "01 1 12 namespace/01",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("01 1 12 [[namespace/01|01]]")
})
describe("ignoreDateFormats setting", () => {
it("should not replace date format when ignoreDateFormats is true", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
ignoreDateFormats: true,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "2025-02-10" }, { path: "journals/2025-02-10" }],
settings,
})
const result = replaceLinks({
body: "2025-02-10",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("2025-02-10")
})
it("should replace date format when ignoreDateFormats is false", () => {
const settings = {
scoped: false,
baseDir: undefined,
proximityBasedLinking: true,
ignoreDateFormats: false,
}
const { candidateMap, trie } = buildCandidateTrieForTest({
files: [{ path: "2025-02-10" }],
settings,
})
const result = replaceLinks({
body: "2025-02-10",
linkResolverContext: {
filePath: "journals/2022-01-01",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[2025-02-10]]")
})
})
describe("replaceLinks (manual candidateMap/trie)", () => {
const candidateMap = new Map<string, CandidateData>([
[
"x",
{
candidates: [{
canonical: "namespace/x",
scoped: true,
namespace: "namespace",
}],
},
],
[
"z",
{
candidates: [{
canonical: "namespace/y/z",
scoped: true,
namespace: "namespace",
}],
},
],
[
"root",
{
candidates: [{
canonical: "root-note",
scoped: true,
namespace: "",
}],
},
],
// Candidate without namespace restriction.
[
"free",
{
candidates: [{
canonical: "free-note",
scoped: false,
namespace: "other",
}],
},
],
// For alias testing:
// Assume file "pages/HelloWorld" with shorthand "HelloWorld"
[
"pages/HelloWorld",
{
candidates: [{
canonical: "pages/HelloWorld",
scoped: false,
namespace: "pages",
}],
},
],
// Alias "Hello" is different from the shorthand, so canonical becomes "pages/HelloWorld|Hello".
[
"Hello",
{
candidates: [{
canonical: "pages/HelloWorld|Hello",
scoped: false,
namespace: "pages",
}],
},
],
// Also register the shorthand candidate.
[
"HelloWorld",
{
candidates: [{
canonical: "HelloWorld",
scoped: false,
namespace: "pages",
}],
},
],
// For "tags" test: candidate key "tags" should map to canonical "tags"
[
"pages/tags",
{
candidates: [{
canonical: "pages/tags",
scoped: false,
namespace: "pages",
}],
},
],
[
"tags",
{
candidates: [{
canonical: "tags",
scoped: false,
namespace: "pages",
}],
},
],
// For Korean test, add candidate "문서"
[
"문서",
{
candidates: [{
canonical: "문서",
scoped: false,
namespace: "namespace",
}],
},
],
// For Japanese test, add candidate "ひらがな"
[
"ひらがな",
{
candidates: [{
canonical: "ひらがな",
scoped: false,
namespace: "namespace",
}],
},
],
// For Chinese test, add candidate "文档"
[
"文档",
{
candidates: [{
canonical: "文档",
scoped: false,
namespace: "namespace",
}],
},
],
])
it("CJK - Korean > converts Korean words within sentence", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "이 문서는 문서이다."
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "namespace/note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("이 문서는 [[문서]]이다.")
})
it("starting CJK > multiple same CJK words", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "- ひらがなひらがな"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "namespace/note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("- [[ひらがな]][[ひらがな]]")
})
it("CJK - Chinese > converts Chinese words within sentence", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "这个文档很好。"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "namespace/note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("这个[[文档]]很好。")
})
it("base character (pages) > unmatched namespace", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "tags"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "root-note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[tags]]")
})
it("aliases > replaces alias with canonical form using file path and alias", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "HelloWorld"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "pages/Note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[HelloWorld]]")
})
it("aliases > replaces multiple occurrences of alias and normal candidate", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "Hello HelloWorld"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "pages/Note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("[[pages/HelloWorld|Hello]] [[HelloWorld]]")
})
it("replaceLinks > should not replace when inside a protected segment", () => {
const settings = { proximityBasedLinking: true }
const trie = buildTrie(Array.from(candidateMap.keys()))
const body = "Some text `x` more text"
const result = replaceLinks({
body,
linkResolverContext: {
filePath: "namespace/note",
trie,
candidateMap,
},
settings,
})
expect(result).toBe("Some text `x` more text")
})
describe("automatic-linker-scoped and base dir", () => {
// Add candidate "a" corresponding to a file at "pages/set/a"
// with scoped enabled and an effective namespace of "set".
candidateMap.set("a", {
candidates: [{
canonical: "set/a",
scoped: true,
namespace: "set",
}],
})
const trie = buildTrie(Array.from(candidateMap.keys()))
it("should replace candidate with scoped when effective namespace matches", () => {
// Current file is in "pages/set/...", so effective namespace is "set"
const settings = {
proximityBasedLinking: true,
baseDir: "pages",
}
const body = "a"
const filePath = "pages/set/current"
const result = replaceLinks({
body,
linkResolverContext: { filePath, trie, candidateMap },
settings,
})
expect(result).toBe("[[set/a|a]]")
})
it("should not replace candidate with scoped when effective namespace does not match", () => {
// Current file is in "pages/other/...", so effective namespace is "other"
const settings = {
proximityBasedLinking: true,
baseDir: "pages",
}
const body = "a"
const filePath = "pages/other/current"
const result = replaceLinks({
body,
linkResolverContext: { filePath, trie, candidateMap },
settings,
})
// Since effective namespace does not match ("set" vs "other"), no replacement occurs.
expect(result).toBe("a")
})
})
})