From 9ab9cc56796fbddf8017fbcccc080a788978b091 Mon Sep 17 00:00:00 2001 From: Kodai Nakamura Date: Thu, 27 Nov 2025 23:47:07 +0900 Subject: [PATCH] feat: remove aliases --- src/main.ts | 2 + .../replace-links.remove-alias.test.ts | 353 ++++++++++++++++++ src/replace-links/replace-links.ts | 34 ++ src/settings/settings-info.ts | 2 + src/settings/settings.ts | 22 ++ 5 files changed, 413 insertions(+) create mode 100644 src/replace-links/__tests__/replace-links.remove-alias.test.ts diff --git a/src/main.ts b/src/main.ts index d8d9218..d1d3cf5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -142,6 +142,7 @@ export default class AutomaticLinkerPlugin extends Plugin { ignoreDateFormats: this.settings.ignoreDateFormats, ignoreCase: this.settings.ignoreCase, preventSelfLinking: this.settings.preventSelfLinking, + removeAliasInDirs: this.settings.removeAliasInDirs, }, }); @@ -203,6 +204,7 @@ export default class AutomaticLinkerPlugin extends Plugin { ignoreDateFormats: this.settings.ignoreDateFormats, ignoreCase: this.settings.ignoreCase, preventSelfLinking: this.settings.preventSelfLinking, + removeAliasInDirs: this.settings.removeAliasInDirs, }, }); cm.replaceSelection(updatedText); diff --git a/src/replace-links/__tests__/replace-links.remove-alias.test.ts b/src/replace-links/__tests__/replace-links.remove-alias.test.ts new file mode 100644 index 0000000..0266c5a --- /dev/null +++ b/src/replace-links/__tests__/replace-links.remove-alias.test.ts @@ -0,0 +1,353 @@ +import { describe, expect, it } from "vitest"; +import { replaceLinks } from "../replace-links"; +import { buildCandidateTrieForTest } from "./test-helpers"; + +describe("replaceLinks - directory-specific alias removal", () => { + describe("basic alias removal", () => { + it("removes alias for links in specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[dir/xxx]]"); + }); + + it("keeps alias for links not in specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "other/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[other/xxx|xxx]]"); + }); + + it("removes alias for links in subdirectories", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/subdir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[dir/subdir/xxx]]"); + }); + }); + + describe("multiple directories", () => { + it("removes alias for links in any specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "dir1/xxx" }, + { path: "dir2/yyy" }, + { path: "other/zzz" }, + ], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx yyy zzz", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: ["dir1", "dir2"], + }, + }); + expect(result).toBe("[[dir1/xxx]] [[dir2/yyy]] [[other/zzz|zzz]]"); + }); + }); + + describe("with baseDir", () => { + it("removes alias based on normalized path with baseDir", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: "pages", + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + baseDir: "pages", + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[dir/xxx]]"); + }); + + it("keeps alias when path starts with baseDir but not with specified dir", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/other/xxx" }], + settings: { + restrictNamespace: false, + baseDir: "pages", + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + baseDir: "pages", + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[other/xxx|xxx]]"); + }); + }); + + describe("with frontmatter aliases", () => { + it("removes alias from frontmatter alias in specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/HelloWorld", aliases: ["HW"] }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[dir/HelloWorld]]"); + }); + + it("keeps alias from frontmatter alias not in specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "other/HelloWorld", aliases: ["HW"] }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[other/HelloWorld|HW]]"); + }); + }); + + describe("edge cases", () => { + it("handles empty removeAliasInDirs array", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: [], + }, + }); + expect(result).toBe("[[dir/xxx|xxx]]"); + }); + + it("handles undefined removeAliasInDirs", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + }, + }); + expect(result).toBe("[[dir/xxx|xxx]]"); + }); + + it("handles links without alias in specified directory", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "dir/xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("[[dir/xxx]]"); + }); + + it("works with ignoreCase option", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "Dir/Xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + ignoreCase: true, + }, + }); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + removeAliasInDirs: ["Dir"], + ignoreCase: true, + }, + }); + expect(result).toBe("[[Dir/Xxx]]"); + }); + }); + + describe("in markdown tables", () => { + it("removes alias in markdown tables", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + const result = replaceLinks({ + body: "| xxx |", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: ["dir"], + }, + }); + expect(result).toBe("| [[dir/xxx]] |"); + }); + }); + + describe("performance", () => { + it("handles large number of directories efficiently", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir50/xxx" }], + settings: { + restrictNamespace: false, + baseDir: undefined, + }, + }); + + // Create array of 100 directories + const manyDirs = Array.from({ length: 100 }, (_, i) => `dir${i}`); + + const startTime = performance.now(); + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings: { + namespaceResolution: true, + removeAliasInDirs: manyDirs, + }, + }); + const endTime = performance.now(); + + expect(result).toBe("[[dir50/xxx]]"); + // Should complete in reasonable time (less than 10ms) + expect(endTime - startTime).toBeLessThan(10); + }); + }); +}); diff --git a/src/replace-links/replace-links.ts b/src/replace-links/replace-links.ts index 9ff182a..153d4c5 100644 --- a/src/replace-links/replace-links.ts +++ b/src/replace-links/replace-links.ts @@ -14,6 +14,7 @@ export interface ReplaceLinksSettings { ignoreDateFormats?: boolean; ignoreCase?: boolean; preventSelfLinking?: boolean; + removeAliasInDirs?: string[]; } export interface ReplaceLinksOptions { @@ -165,6 +166,27 @@ const isSelfLink = ( return normalizedLinkPath === normalizedCurrentPath; }; +// Helper function to check if a path should have its alias removed +const shouldRemoveAlias = (normalizedPath: string, removeAliasInDirs?: string[]): boolean => { + if (!removeAliasInDirs || removeAliasInDirs.length === 0) { + return false; + } + + // Early return for paths without slashes + if (!normalizedPath.includes("/")) { + return false; + } + + // Check if the normalized path starts with any of the specified directories + for (const dir of removeAliasInDirs) { + if (normalizedPath === dir || normalizedPath.startsWith(dir + "/")) { + return true; + } + } + + return false; +}; + // Link Content Creation const createLinkContent = ( candidateData: CandidateData, @@ -174,11 +196,23 @@ const createLinkContent = ( const { linkPath, alias, hasAlias } = extractLinkParts(candidateData.canonical); const normalizedPath = normalizeCanonicalPath(linkPath, settings.baseDir); + // Check if alias should be removed for this directory + const removeAlias = shouldRemoveAlias(normalizedPath, settings.removeAliasInDirs); + if (hasAlias) { + // If alias removal is enabled for this directory, return path without alias + if (removeAlias) { + return normalizedPath; + } return `${normalizedPath}|${alias}`; } if (normalizedPath.includes("/")) { + // If alias removal is enabled for this directory, return path without alias + if (removeAlias) { + return normalizedPath; + } + // For paths with slashes, use the last segment as the display text const lastSegment = normalizedPath.split("/").pop() || originalMatchedText; diff --git a/src/settings/settings-info.ts b/src/settings/settings-info.ts index 1bbc2c8..df60a94 100644 --- a/src/settings/settings-info.ts +++ b/src/settings/settings-info.ts @@ -17,6 +17,7 @@ export type AutomaticLinkerSettings = { replaceUrlWithTitleIgnoreDomains: string[]; // List of domains to ignore when replacing URLs with titles excludeDirsFromAutoLinking: string[]; // Optional: List of directories to exclude from auto-linking preventSelfLinking: boolean; // Prevent linking text to its own file + removeAliasInDirs: string[]; // Remove aliases for links in specified directories }; export const DEFAULT_SETTINGS: AutomaticLinkerSettings = { @@ -38,4 +39,5 @@ export const DEFAULT_SETTINGS: AutomaticLinkerSettings = { replaceUrlWithTitleIgnoreDomains: [], excludeDirsFromAutoLinking: [], // Default: no excluded directories preventSelfLinking: false, // Default: allow self-linking (backward compatibility) + removeAliasInDirs: [], // Default: no directories for alias removal }; diff --git a/src/settings/settings.ts b/src/settings/settings.ts index c52791f..21f8d9b 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -162,6 +162,28 @@ export class AutomaticLinkerPluginSettingsTab extends PluginSettingTab { }); }); + // Remove aliases for links in specified directories + new Setting(containerEl) + .setName("Remove aliases in directories") + .setDesc( + "Directories where link aliases should be removed, one per line (e.g. 'dir' will convert [[dir/xxx|yyy]] to [[dir/xxx]]). This affects both auto-generated aliases and frontmatter aliases.", + ) + .addTextArea((text) => { + text.setPlaceholder("dir1\ndir2/subdir") + .setValue( + this.plugin.settings.removeAliasInDirs.join("\n"), + ) + .onChange(async (value) => { + // Split by newlines and filter out empty lines + const dirs = value + .split("\n") + .map((dir) => dir.trim()) + .filter(Boolean); + this.plugin.settings.removeAliasInDirs = dirs; + await this.plugin.saveData(this.plugin.settings); + }); + }); + new Setting(containerEl) .setName("URL Formatting for GitHub") .setHeading();