From 2943d8074251466031d4195105f3620a2ee3c0a2 Mon Sep 17 00:00:00 2001 From: Kodai Nakamura Date: Sat, 20 Dec 2025 20:40:18 +0900 Subject: [PATCH] chore: add stylistic eslint plugin and format code --- .prettierrc | 1 - esbuild.config.mjs | 77 +- eslint.config.mjs | 93 +- obsidian-ext.d.ts | 36 +- package.json | 1 + pnpm-lock.yaml | 19 + .../__tests__/exclude-links.test.ts | 110 +- src/exclude-links/index.ts | 99 +- src/frontmatter-utils.ts | 38 +- src/main.ts | 1061 +++---- src/path-and-aliases.types.ts | 10 +- .../__tests__/integration.test.ts | 94 +- .../__tests__/remove-minimal-indent.test.ts | 106 +- src/remove-minimal-indent/index.ts | 91 +- src/replace-links/__tests__/prev.test.ts | 2490 ++++++++--------- .../__tests__/replace-links.alias.test.ts | 438 +-- .../__tests__/replace-links.base-dir.test.ts | 1020 +++---- .../__tests__/replace-links.basic.test.ts | 594 ++-- .../__tests__/replace-links.callout.test.ts | 780 +++--- .../__tests__/replace-links.cjk.test.ts | 854 +++--- .../__tests__/replace-links.code.test.ts | 88 +- .../replace-links.exclude-dirs.test.ts | 388 +-- .../replace-links.ignore-case.test.ts | 194 +- ...replace-links.namespace-resolution.test.ts | 680 ++--- .../__tests__/replace-links.namespace.test.ts | 244 +- .../replace-links.performance.test.ts | 565 ++-- .../replace-links.prevent-linking.test.ts | 264 +- ...replace-links.prevent-self-linking.test.ts | 406 +-- .../replace-links.remove-alias.test.ts | 664 ++--- .../replace-links.restrict-namespace.test.ts | 158 +- .../__tests__/replace-links.table.test.ts | 52 +- .../__tests__/replace-links.url.test.ts | 456 +-- src/replace-links/__tests__/test-helpers.ts | 74 +- src/replace-links/replace-links.ts | 1507 +++++----- .../__tests__/replace-url-with-title.test.ts | 122 +- src/replace-url-with-title/index.ts | 189 +- .../__tests__/get-title-from-html.test.ts | 36 +- .../utils/__tests__/list-up-all-urls.test.ts | 66 +- .../utils/get-title-from-html.ts | 20 +- .../utils/list-up-all-urls.ts | 299 +- .../__tests__/replace-urls.github.test.ts | 178 +- .../__tests__/replace-urls.jira.test.ts | 150 +- .../__tests__/replace-urls.linear.test.ts | 198 +- .../__tests__/replace-urls.test.ts | 40 +- src/replace-urls/github.ts | 189 +- src/replace-urls/jira.ts | 109 +- src/replace-urls/linear.ts | 113 +- src/replace-urls/replace-urls.ts | 20 +- src/settings/settings-info.ts | 88 +- src/settings/settings.ts | 728 ++--- src/trie.ts | 400 +-- src/update-editor.ts | 94 +- vitest.config.ts | 10 +- 53 files changed, 8430 insertions(+), 8371 deletions(-) delete mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0967ef4..0000000 --- a/.prettierrc +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/esbuild.config.mjs b/esbuild.config.mjs index fa16170..7e97c1e 100644 --- a/esbuild.config.mjs +++ b/esbuild.config.mjs @@ -1,49 +1,50 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from "builtin-modules"; +import esbuild from "esbuild" +import process from "process" +import builtins from "builtin-modules" const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ -`; +` -const prod = process.argv[2] === "production"; +const prod = process.argv[2] === "production" const context = await esbuild.context({ - banner: { - js: banner, - }, - entryPoints: ["src/main.ts"], - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins, - ], - format: "cjs", - target: "esnext", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", - minify: prod, -}); + banner: { + js: banner, + }, + entryPoints: ["src/main.ts"], + bundle: true, + external: [ + "obsidian", + "electron", + "@codemirror/autocomplete", + "@codemirror/collab", + "@codemirror/commands", + "@codemirror/language", + "@codemirror/lint", + "@codemirror/search", + "@codemirror/state", + "@codemirror/view", + "@lezer/common", + "@lezer/highlight", + "@lezer/lr", + ...builtins, + ], + format: "cjs", + target: "esnext", + logLevel: "info", + sourcemap: prod ? false : "inline", + treeShaking: true, + outfile: "main.js", + minify: prod, +}) if (prod) { - await context.rebuild(); - process.exit(0); -} else { - await context.watch(); + await context.rebuild() + process.exit(0) +} +else { + await context.watch() } diff --git a/eslint.config.mjs b/eslint.config.mjs index f089ff5..37ad1e4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,41 +1,54 @@ -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; -import { defineConfig } from "eslint/config"; -import globals from "globals"; +import eslint from "@eslint/js" +import tseslint from "typescript-eslint" +import { defineConfig } from "eslint/config" +import globals from "globals" +import stylistic from "@stylistic/eslint-plugin" -export default defineConfig( - eslint.configs.recommended, - ...tseslint.configs.recommended, - { - languageOptions: { - globals: { - ...globals.node, - ...globals.browser, - }, - parserOptions: { - sourceType: "module", - }, - }, - rules: { - "no-unused-vars": "off", - "@typescript-eslint/ban-ts-comment": "off", - "no-prototype-builtins": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - args: "all", - argsIgnorePattern: "^_", - caughtErrors: "all", - caughtErrorsIgnorePattern: "^_", - destructuredArrayIgnorePattern: "^_", - varsIgnorePattern: "^_", - ignoreRestSiblings: true, - }, - ], - }, - }, - { - ignores: ["node_modules/", "main.js", "src/main.js"], - }, -); +export default defineConfig([ + { + ...stylistic.configs.customize({ + indent: 4, + quotes: "double", + semi: false, + }), + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.browser, + }, + parserOptions: { + sourceType: "module", + }, + }, + rules: { + "no-unused-vars": "off", + "@typescript-eslint/ban-ts-comment": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + }, + }, + { + ignores: [ + "node_modules/", + "main.js", + "src/main.js", + "version-bump.mjs", + ], + }, +]) diff --git a/obsidian-ext.d.ts b/obsidian-ext.d.ts index 733dd17..22102c5 100644 --- a/obsidian-ext.d.ts +++ b/obsidian-ext.d.ts @@ -1,23 +1,23 @@ -import "obsidian"; -import { EditorView } from "@codemirror/view"; +import "obsidian" +import { EditorView } from "@codemirror/view" declare module "obsidian" { - interface App { - commands: { - commands: { - "editor:save-file": { - callback?: () => void; - checkCallback?: (checking: boolean) => void; - }; - }; - }; - } + interface App { + commands: { + commands: { + "editor:save-file": { + callback?: () => void + checkCallback?: (checking: boolean) => void + } + } + } + } - interface Editor { - cm?: EditorView; - } + interface Editor { + cm?: EditorView + } - interface Vault { - getConfig(id: string): string; - } + interface Vault { + getConfig(id: string): string + } } diff --git a/package.json b/package.json index de41817..e222b33 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "", "license": "Appache-2.0", "devDependencies": { + "@stylistic/eslint-plugin": "^5.6.1", "@types/diff-match-patch": "^1.0.36", "@types/node": "^22.12.0", "@typescript-eslint/eslint-plugin": "^8.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d384db..28c5585 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: specifier: ^1.0.5 version: 1.0.5 devDependencies: + '@stylistic/eslint-plugin': + specifier: ^5.6.1 + version: 5.6.1(eslint@9.39.2) '@types/diff-match-patch': specifier: ^1.0.36 version: 1.0.36 @@ -848,6 +851,12 @@ packages: resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} engines: {node: '>=10'} + '@stylistic/eslint-plugin@5.6.1': + resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: '>=9.0.0' + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -2956,6 +2965,16 @@ snapshots: '@sindresorhus/is@4.6.0': {} + '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@typescript-eslint/types': 8.50.0 + eslint: 9.39.2 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + estraverse: 5.3.0 + picomatch: 4.0.3 + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 diff --git a/src/exclude-links/__tests__/exclude-links.test.ts b/src/exclude-links/__tests__/exclude-links.test.ts index f3b1a57..e5af247 100644 --- a/src/exclude-links/__tests__/exclude-links.test.ts +++ b/src/exclude-links/__tests__/exclude-links.test.ts @@ -1,69 +1,69 @@ -import { describe, expect, it } from "vitest"; -import { excludeLinks } from ".."; +import { describe, expect, it } from "vitest" +import { excludeLinks } from ".." describe("exclude links", () => { - it("replaces links", () => { - const result = excludeLinks("[[hello]]"); - expect(result).toBe("hello"); - }); + it("replaces links", () => { + const result = excludeLinks("[[hello]]") + expect(result).toBe("hello") + }) - it("replaces links with space", () => { - const result = excludeLinks("[[tidy first]]"); - expect(result).toBe("tidy first"); - }); + it("replaces links with space", () => { + const result = excludeLinks("[[tidy first]]") + expect(result).toBe("tidy first") + }) - it("replaces links with bullet", () => { - const result = excludeLinks("- hello"); - expect(result).toBe("- hello"); - }); + it("replaces links with bullet", () => { + const result = excludeLinks("- hello") + expect(result).toBe("- hello") + }) - it("replaces multiple links", () => { - const result = excludeLinks("[[hello]] [[world]]"); - expect(result).toBe("hello world"); - }); + it("replaces multiple links", () => { + const result = excludeLinks("[[hello]] [[world]]") + expect(result).toBe("hello world") + }) - it("replaces multiple lines", () => { - const result = excludeLinks("[[hello]]\n[[world]]"); - expect(result).toBe("hello\nworld"); - }); + it("replaces multiple lines", () => { + const result = excludeLinks("[[hello]]\n[[world]]") + expect(result).toBe("hello\nworld") + }) - it("replaces CJK links", () => { - const result = excludeLinks("[[你好]]"); - expect(result).toBe("你好"); - }); + it("replaces CJK links", () => { + const result = excludeLinks("[[你好]]") + expect(result).toBe("你好") + }) - it("ignores inline code", () => { - const result = excludeLinks("`[[hello]]`"); - expect(result).toBe("`[[hello]]`"); - }); + it("ignores inline code", () => { + const result = excludeLinks("`[[hello]]`") + expect(result).toBe("`[[hello]]`") + }) - it("ignores code block", () => { - const result = excludeLinks("```\n[[hello]]\n```"); - expect(result).toBe("```\n[[hello]]\n```"); - }); + it("ignores code block", () => { + const result = excludeLinks("```\n[[hello]]\n```") + expect(result).toBe("```\n[[hello]]\n```") + }) - it("replaces alias", () => { - const result = excludeLinks("[[hello|world]]"); - expect(result).toBe("world"); - }); + it("replaces alias", () => { + const result = excludeLinks("[[hello|world]]") + expect(result).toBe("world") + }) - it("extracts basename from path-style links", () => { - const result = excludeLinks("[[xxx/yyy/zzz]]"); - expect(result).toBe("zzz"); - }); + it("extracts basename from path-style links", () => { + const result = excludeLinks("[[xxx/yyy/zzz]]") + expect(result).toBe("zzz") + }) - it("extracts basename from two-level path links", () => { - const result = excludeLinks("[[folder/file]]"); - expect(result).toBe("file"); - }); + it("extracts basename from two-level path links", () => { + const result = excludeLinks("[[folder/file]]") + expect(result).toBe("file") + }) - it("uses alias for path-style links when provided", () => { - const result = excludeLinks("[[xxx/yyy/zzz|alias]]"); - expect(result).toBe("alias"); - }); + it("uses alias for path-style links when provided", () => { + const result = excludeLinks("[[xxx/yyy/zzz|alias]]") + expect(result).toBe("alias") + }) - it("handles multiple path-style links", () => { - const result = excludeLinks("[[path/to/file1]] and [[another/path/to/file2]]"); - expect(result).toBe("file1 and file2"); - }); -}); + it("handles multiple path-style links", () => { + const result = excludeLinks("[[path/to/file1]] and [[another/path/to/file2]]") + expect(result).toBe("file1 and file2") + }) +}) diff --git a/src/exclude-links/index.ts b/src/exclude-links/index.ts index 140e629..7673ca3 100644 --- a/src/exclude-links/index.ts +++ b/src/exclude-links/index.ts @@ -1,56 +1,57 @@ export const excludeLinks = (text: string) => { - // Split the text into segments of inline code and regular text - const segments: { isCode: boolean; content: string }[] = []; - let currentPos = 0; - const codeBlockRegex = /`([^`]+)`/g; - let match; + // Split the text into segments of inline code and regular text + const segments: { isCode: boolean, content: string }[] = [] + let currentPos = 0 + const codeBlockRegex = /`([^`]+)`/g + let match - while ((match = codeBlockRegex.exec(text)) !== null) { - // Add text before code block - if (match.index > currentPos) { - segments.push({ - isCode: false, - content: text.substring(currentPos, match.index), - }); - } + while ((match = codeBlockRegex.exec(text)) !== null) { + // Add text before code block + if (match.index > currentPos) { + segments.push({ + isCode: false, + content: text.substring(currentPos, match.index), + }) + } - // Add code block (which should be preserved as is) - segments.push({ - isCode: true, - content: match[0], - }); + // Add code block (which should be preserved as is) + segments.push({ + isCode: true, + content: match[0], + }) - currentPos = match.index + match[0].length; - } + currentPos = match.index + match[0].length + } - // Add remaining text - if (currentPos < text.length) { - segments.push({ - isCode: false, - content: text.substring(currentPos), - }); - } + // Add remaining text + if (currentPos < text.length) { + segments.push({ + isCode: false, + content: text.substring(currentPos), + }) + } - // Process each segment - // Regex that matches both simple links and links with aliases [[link]] or [[link|alias]] - const regex = /\[\[(.*?)(?:\|(.*?))?\]\]/g; - const processedSegments = segments.map((segment) => { - if (segment.isCode) { - // Preserve code blocks - return segment.content; - } else { - // Replace links in non-code segments, handling aliases - return segment.content.replace(regex, (_match, link, alias) => { - // If there's an alias, use it - if (alias) { - return alias; - } - // Extract the last part after the last '/' (basename) - const parts = link.split("/"); - return parts[parts.length - 1] || link; - }); - } - }); + // Process each segment + // Regex that matches both simple links and links with aliases [[link]] or [[link|alias]] + const regex = /\[\[(.*?)(?:\|(.*?))?\]\]/g + const processedSegments = segments.map((segment) => { + if (segment.isCode) { + // Preserve code blocks + return segment.content + } + else { + // Replace links in non-code segments, handling aliases + return segment.content.replace(regex, (_match, link, alias) => { + // If there's an alias, use it + if (alias) { + return alias + } + // Extract the last part after the last '/' (basename) + const parts = link.split("/") + return parts[parts.length - 1] || link + }) + } + }) - return processedSegments.join(""); -}; + return processedSegments.join("") +} diff --git a/src/frontmatter-utils.ts b/src/frontmatter-utils.ts index 73c82ff..612bb46 100644 --- a/src/frontmatter-utils.ts +++ b/src/frontmatter-utils.ts @@ -2,35 +2,35 @@ * Check if the file has the "off" frontmatter property */ export const isLinkingOff = ( - frontmatter: Record | undefined, + frontmatter: Record | undefined, ): boolean => { - return ( - frontmatter?.["automatic-linker-disabled"] === true || - frontmatter?.["automatic-linker-off"] === true - ); -}; + return ( + frontmatter?.["automatic-linker-disabled"] === true + || frontmatter?.["automatic-linker-off"] === true + ) +} /** * Check if the file has the "scoped" frontmatter property */ export const isNamespaceScoped = ( - frontmatter: Record | undefined, + frontmatter: Record | undefined, ): boolean => { - return ( - frontmatter?.["automatic-linker-restrict-namespace"] === true || - frontmatter?.["automatic-linker-limited-namespace"] === true || - frontmatter?.["automatic-linker-scoped"] === true - ); -}; + return ( + frontmatter?.["automatic-linker-restrict-namespace"] === true + || frontmatter?.["automatic-linker-limited-namespace"] === true + || frontmatter?.["automatic-linker-scoped"] === true + ) +} /** * Check if the file has the "exclude" frontmatter property */ export const isLinkingExcluded = ( - frontmatter: Record | undefined, + frontmatter: Record | undefined, ): boolean => { - return ( - frontmatter?.["automatic-linker-prevent-linking"] === true || - frontmatter?.["automatic-linker-exclude"] === true - ); -}; + return ( + frontmatter?.["automatic-linker-prevent-linking"] === true + || frontmatter?.["automatic-linker-exclude"] === true + ) +} diff --git a/src/main.ts b/src/main.ts index 8f99bfd..91dcfc0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,594 +1,601 @@ import { - App, - Editor, - getFrontMatterInfo, - MarkdownView, - Notice, - parseFrontMatterAliases, - Plugin, - PluginManifest, - request, - TFile, -} from "obsidian"; -import { excludeLinks } from "./exclude-links"; + App, + Editor, + getFrontMatterInfo, + MarkdownView, + Notice, + parseFrontMatterAliases, + Plugin, + PluginManifest, + request, + TFile, +} from "obsidian" +import { excludeLinks } from "./exclude-links" import { - isLinkingOff, - isLinkingExcluded, - isNamespaceScoped, -} from "./frontmatter-utils"; -import { PathAndAliases } from "./path-and-aliases.types"; -import { removeMinimalIndent } from "./remove-minimal-indent"; + isLinkingOff, + isLinkingExcluded, + isNamespaceScoped, +} from "./frontmatter-utils" +import { PathAndAliases } from "./path-and-aliases.types" +import { removeMinimalIndent } from "./remove-minimal-indent" import { - LinkGenerator, - LinkGeneratorParams, - replaceLinks, -} from "./replace-links/replace-links"; -import { replaceUrlWithTitle } from "./replace-url-with-title"; -import { getTitleFromHtml } from "./replace-url-with-title/utils/get-title-from-html"; -import { listupAllUrls } from "./replace-url-with-title/utils/list-up-all-urls"; -import { formatGitHubURL } from "./replace-urls/github"; -import { formatJiraURL } from "./replace-urls/jira"; -import { formatLinearURL } from "./replace-urls/linear"; -import { replaceURLs } from "./replace-urls/replace-urls"; -import { AutomaticLinkerPluginSettingsTab } from "./settings/settings"; + LinkGenerator, + LinkGeneratorParams, + replaceLinks, +} from "./replace-links/replace-links" +import { replaceUrlWithTitle } from "./replace-url-with-title" +import { getTitleFromHtml } from "./replace-url-with-title/utils/get-title-from-html" +import { listupAllUrls } from "./replace-url-with-title/utils/list-up-all-urls" +import { formatGitHubURL } from "./replace-urls/github" +import { formatJiraURL } from "./replace-urls/jira" +import { formatLinearURL } from "./replace-urls/linear" +import { replaceURLs } from "./replace-urls/replace-urls" +import { AutomaticLinkerPluginSettingsTab } from "./settings/settings" import { - AutomaticLinkerSettings, - DEFAULT_SETTINGS, -} from "./settings/settings-info"; -import { buildCandidateTrie, CandidateData, TrieNode } from "./trie"; -import { updateEditor } from "./update-editor"; + AutomaticLinkerSettings, + DEFAULT_SETTINGS, +} from "./settings/settings-info" +import { buildCandidateTrie, CandidateData, TrieNode } from "./trie" +import { updateEditor } from "./update-editor" const sleep = (ms: number) => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; + return new Promise(resolve => setTimeout(resolve, ms)) +} export default class AutomaticLinkerPlugin extends Plugin { - settings: AutomaticLinkerSettings; - // Pre-built Trie for link candidate lookup - private trie: TrieNode | null = null; - private candidateMap: Map | null = null; - // Preserved callback for the original save command - private originalSaveCallback: (checking: boolean) => boolean | void; - private urlTitleMap: Map = new Map(); - // Cache of frontmatter values that affect the Trie - private frontmatterCache: Map = new Map(); + settings: AutomaticLinkerSettings + // Pre-built Trie for link candidate lookup + private trie: TrieNode | null = null + private candidateMap: Map | null = null + // Preserved callback for the original save command + private originalSaveCallback: (checking: boolean) => boolean | void + private urlTitleMap: Map = new Map() + // Cache of frontmatter values that affect the Trie + private frontmatterCache: Map = new Map() - constructor(app: App, pluginManifest: PluginManifest) { - super(app, pluginManifest); - } + constructor(app: App, pluginManifest: PluginManifest) { + super(app, pluginManifest) + } - private getEditor(): Editor | null { - const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!activeLeaf) return null; - return activeLeaf.editor; - } + private getEditor(): Editor | null { + const activeLeaf = this.app.workspace.getActiveViewOfType(MarkdownView) + if (!activeLeaf) return null + return activeLeaf.editor + } - /** + /** * Creates a LinkGenerator that uses Obsidian's generateMarkdownLink API. * Falls back to default wikilink format if the file cannot be resolved. */ - private createLinkGenerator(sourcePath: string): LinkGenerator { - return ({ - linkPath, - alias, - isInTable, - }: LinkGeneratorParams): string => { - // Try to get the TFile for the link path - const targetFile = this.app.vault.getAbstractFileByPath( - linkPath + ".md", - ); + private createLinkGenerator(sourcePath: string): LinkGenerator { + return ({ + linkPath, + alias, + isInTable, + }: LinkGeneratorParams): string => { + // Try to get the TFile for the link path + const targetFile = this.app.vault.getAbstractFileByPath( + linkPath + ".md", + ) - if (targetFile instanceof TFile) { - // File exists, use Obsidian's generateMarkdownLink API - try { - const link = this.app.fileManager.generateMarkdownLink( - targetFile, - sourcePath, - "", - alias || "", - ); - return link; - } catch (error) { - // Fall back to default format if API fails - console.warn( - "Failed to generate link using Obsidian API:", - error, - ); - } - } + if (targetFile instanceof TFile) { + // File exists, use Obsidian's generateMarkdownLink API + try { + const link = this.app.fileManager.generateMarkdownLink( + targetFile, + sourcePath, + "", + alias || "", + ) + return link + } + catch (error) { + // Fall back to default format if API fails + console.warn( + "Failed to generate link using Obsidian API:", + error, + ) + } + } - // Fallback: use default wikilink format - let linkContent = linkPath; - if (alias) { - linkContent = `${linkPath}|${alias}`; - } - if (isInTable && linkContent.includes("|")) { - linkContent = linkContent.replace(/\|/g, "\\|"); - } - return `[[${linkContent}]]`; - }; - } + // Fallback: use default wikilink format + let linkContent = linkPath + if (alias) { + linkContent = `${linkPath}|${alias}` + } + if (isInTable && linkContent.includes("|")) { + linkContent = linkContent.replace(/\|/g, "\\|") + } + return `[[${linkContent}]]` + } + } - modifyLinks(fileContent: string, filePath: string): string { - if (this.settings.formatGitHubURLs) { - fileContent = replaceURLs( - fileContent, - this.settings, - formatGitHubURL, - ); - } + modifyLinks(fileContent: string, filePath: string): string { + if (this.settings.formatGitHubURLs) { + fileContent = replaceURLs( + fileContent, + this.settings, + formatGitHubURL, + ) + } - if (this.settings.formatJiraURLs) { - fileContent = replaceURLs( - fileContent, - this.settings, - formatJiraURL, - ); - } + if (this.settings.formatJiraURLs) { + fileContent = replaceURLs( + fileContent, + this.settings, + formatJiraURL, + ) + } - if (this.settings.formatLinearURLs) { - fileContent = replaceURLs( - fileContent, - this.settings, - formatLinearURL, - ); - } + if (this.settings.formatLinearURLs) { + fileContent = replaceURLs( + fileContent, + this.settings, + formatLinearURL, + ) + } - if (this.settings.replaceUrlWithTitle) { - const { contentStart } = getFrontMatterInfo(fileContent); - const frontmatter = fileContent.slice(0, contentStart); - const body = fileContent.slice(contentStart); - const updatedBody = replaceUrlWithTitle({ - body, - urlTitleMap: this.urlTitleMap, - }); - fileContent = frontmatter + updatedBody; - } + if (this.settings.replaceUrlWithTitle) { + const { contentStart } = getFrontMatterInfo(fileContent) + const frontmatter = fileContent.slice(0, contentStart) + const body = fileContent.slice(contentStart) + const updatedBody = replaceUrlWithTitle({ + body, + urlTitleMap: this.urlTitleMap, + }) + fileContent = frontmatter + updatedBody + } - if (!this.trie || !this.candidateMap) { - return fileContent; - } + if (!this.trie || !this.candidateMap) { + return fileContent + } - if (this.settings.debug) { - console.log("this.trie: ", this.trie); - console.log("this.candidateMap: ", this.candidateMap); - console.log(new Date().toISOString(), "modifyLinks started"); - new Notice( - `Automatic Linker: ${new Date().toISOString()} modifyLinks started.`, - ); - } + if (this.settings.debug) { + console.log("this.trie: ", this.trie) + console.log("this.candidateMap: ", this.candidateMap) + console.log(new Date().toISOString(), "modifyLinks started") + new Notice( + `Automatic Linker: ${new Date().toISOString()} modifyLinks started.`, + ) + } - const { contentStart } = getFrontMatterInfo(fileContent); - const frontmatter = fileContent.slice(0, contentStart); - const linkGenerator = this.createLinkGenerator(filePath); - const baseDir = this.settings.respectNewFileFolderPath - ? this.app.vault.getConfig("newFileFolderPath") - : undefined; - const updatedBody = replaceLinks({ - body: fileContent.slice(contentStart), - linkResolverContext: { - filePath: filePath.replace(/\.md$/, ""), - trie: this.trie, - candidateMap: this.candidateMap, - }, - settings: { - namespaceResolution: this.settings.namespaceResolution, - baseDir, - ignoreDateFormats: this.settings.ignoreDateFormats, - ignoreCase: this.settings.ignoreCase, - preventSelfLinking: this.settings.preventSelfLinking, - removeAliasInDirs: this.settings.removeAliasInDirs, - }, - linkGenerator, - }); - fileContent = frontmatter + updatedBody; + const { contentStart } = getFrontMatterInfo(fileContent) + const frontmatter = fileContent.slice(0, contentStart) + const linkGenerator = this.createLinkGenerator(filePath) + const baseDir = this.settings.respectNewFileFolderPath + ? this.app.vault.getConfig("newFileFolderPath") + : undefined + const updatedBody = replaceLinks({ + body: fileContent.slice(contentStart), + linkResolverContext: { + filePath: filePath.replace(/\.md$/, ""), + trie: this.trie, + candidateMap: this.candidateMap, + }, + settings: { + namespaceResolution: this.settings.namespaceResolution, + baseDir, + ignoreDateFormats: this.settings.ignoreDateFormats, + ignoreCase: this.settings.ignoreCase, + preventSelfLinking: this.settings.preventSelfLinking, + removeAliasInDirs: this.settings.removeAliasInDirs, + }, + linkGenerator, + }) + fileContent = frontmatter + updatedBody - if (this.settings.debug) { - console.log(new Date().toISOString(), "modifyLinks finished"); - new Notice( - `Automatic Linker: ${new Date().toISOString()} modifyLinks finished.`, - ); - } - return fileContent; - } + if (this.settings.debug) { + console.log(new Date().toISOString(), "modifyLinks finished") + new Notice( + `Automatic Linker: ${new Date().toISOString()} modifyLinks finished.`, + ) + } + return fileContent + } - async modifyLinksForActiveFile() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return; - } + async modifyLinksForActiveFile() { + const activeFile = this.app.workspace.getActiveFile() + if (!activeFile) { + return + } - const metadata = - this.app.metadataCache.getFileCache(activeFile)?.frontmatter; - if (isLinkingOff(metadata)) { - return; - } + const metadata + = this.app.metadataCache.getFileCache(activeFile)?.frontmatter + if (isLinkingOff(metadata)) { + return + } - const editor = this.getEditor(); - if (!editor) return; + const editor = this.getEditor() + if (!editor) return - const fileContent = editor.getValue(); - const oldText = fileContent; - const newText = this.modifyLinks(fileContent, activeFile.path); - updateEditor(oldText, newText, editor); - } + const fileContent = editor.getValue() + const oldText = fileContent + const newText = this.modifyLinks(fileContent, activeFile.path) + updateEditor(oldText, newText, editor) + } - async modifyLinksForVault() { - this.refreshFileDataAndTrie(); - const allMarkdownFiles = this.app.vault.getMarkdownFiles(); - for (const file of allMarkdownFiles) { - await this.app.vault.process(file, (fileContent) => { - return this.modifyLinks(fileContent, file.path); - }); - } - } + async modifyLinksForVault() { + this.refreshFileDataAndTrie() + const allMarkdownFiles = this.app.vault.getMarkdownFiles() + for (const file of allMarkdownFiles) { + await this.app.vault.process(file, (fileContent) => { + return this.modifyLinks(fileContent, file.path) + }) + } + } - async buildUrlTitleMap() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return; - } - const fileContent = await this.app.vault.read(activeFile); - const { contentStart } = getFrontMatterInfo(fileContent); - const body = fileContent.slice(contentStart); + async buildUrlTitleMap() { + const activeFile = this.app.workspace.getActiveFile() + if (!activeFile) { + return + } + const fileContent = await this.app.vault.read(activeFile) + const { contentStart } = getFrontMatterInfo(fileContent) + const body = fileContent.slice(contentStart) - const urls = listupAllUrls( - body, - this.settings.replaceUrlWithTitleIgnoreDomains, - ); - for (const url of urls) { - if (this.urlTitleMap.has(url)) { - continue; - } - const response = await request(url); - const title = getTitleFromHtml(response); - this.urlTitleMap.set(url, title); - } - } + const urls = listupAllUrls( + body, + this.settings.replaceUrlWithTitleIgnoreDomains, + ) + for (const url of urls) { + if (this.urlTitleMap.has(url)) { + continue + } + const response = await request(url) + const title = getTitleFromHtml(response) + this.urlTitleMap.set(url, title) + } + } - async formatThenRunPrettierAndLinter() { - await this.buildUrlTitleMap(); - await this.modifyLinksForActiveFile(); + async formatThenRunPrettierAndLinter() { + await this.buildUrlTitleMap() + await this.modifyLinksForActiveFile() - if (this.settings.runPrettierAfterFormatting) { - await sleep(this.settings.formatDelayMs ?? 100); - //@ts-expect-error - await this.app?.commands?.executeCommandById( - "prettier-format:format-file", - ); - } - if (this.settings.runLinterAfterFormatting) { - await sleep(this.settings.formatDelayMs ?? 100); - //@ts-expect-error - await this.app?.commands?.executeCommandById( - "obsidian-linter:lint-file", - ); - } - } + if (this.settings.runPrettierAfterFormatting) { + await sleep(this.settings.formatDelayMs ?? 100) + // @ts-expect-error + await this.app?.commands?.executeCommandById( + "prettier-format:format-file", + ) + } + if (this.settings.runLinterAfterFormatting) { + await sleep(this.settings.formatDelayMs ?? 100) + // @ts-expect-error + await this.app?.commands?.executeCommandById( + "obsidian-linter:lint-file", + ) + } + } - async mofifyLinksSelection() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return; - } - const editor = this.app.workspace.activeEditor; - if (!editor) { - return; - } - const cm = editor.editor; - if (!cm) { - return; - } + async mofifyLinksSelection() { + const activeFile = this.app.workspace.getActiveFile() + if (!activeFile) { + return + } + const editor = this.app.workspace.activeEditor + if (!editor) { + return + } + const cm = editor.editor + if (!cm) { + return + } - const selectedText = cm.getSelection(); + const selectedText = cm.getSelection() - if (!this.trie || !this.candidateMap) { - return; - } + if (!this.trie || !this.candidateMap) { + return + } - const linkGenerator = this.createLinkGenerator(activeFile.path); - const baseDir = this.settings.respectNewFileFolderPath - ? this.app.vault.getConfig("newFileFolderPath") - : undefined; - const updatedText = replaceLinks({ - body: selectedText, - linkResolverContext: { - filePath: activeFile.path.replace(/\.md$/, ""), - trie: this.trie, - candidateMap: this.candidateMap, - }, - settings: { - namespaceResolution: this.settings.namespaceResolution, - baseDir, - ignoreDateFormats: this.settings.ignoreDateFormats, - ignoreCase: this.settings.ignoreCase, - preventSelfLinking: this.settings.preventSelfLinking, - removeAliasInDirs: this.settings.removeAliasInDirs, - }, - linkGenerator, - }); - cm.replaceSelection(updatedText); - } + const linkGenerator = this.createLinkGenerator(activeFile.path) + const baseDir = this.settings.respectNewFileFolderPath + ? this.app.vault.getConfig("newFileFolderPath") + : undefined + const updatedText = replaceLinks({ + body: selectedText, + linkResolverContext: { + filePath: activeFile.path.replace(/\.md$/, ""), + trie: this.trie, + candidateMap: this.candidateMap, + }, + settings: { + namespaceResolution: this.settings.namespaceResolution, + baseDir, + ignoreDateFormats: this.settings.ignoreDateFormats, + ignoreCase: this.settings.ignoreCase, + preventSelfLinking: this.settings.preventSelfLinking, + removeAliasInDirs: this.settings.removeAliasInDirs, + }, + linkGenerator, + }) + cm.replaceSelection(updatedText) + } - refreshFileDataAndTrie() { - const allMarkdownFiles = this.app.vault.getMarkdownFiles(); - const allFiles: PathAndAliases[] = allMarkdownFiles - .filter((file) => { - // Filter out files in excluded directories - const path = file.path.replace(/\.md$/, ""); - return !this.settings.excludeDirsFromAutoLinking.some( - (excludeDir) => { - return ( - path.startsWith(excludeDir + "/") || - path === excludeDir - ); - }, - ); - }) - .map((file) => { - // Remove the .md extension - const path = file.path.replace(/\.md$/, ""); - const metadata = - this.app.metadataCache.getFileCache(file)?.frontmatter; - const scoped = isNamespaceScoped(metadata); - // if this property exists, prevent this file from being linked from other files - const exclude = isLinkingExcluded(metadata); + refreshFileDataAndTrie() { + const allMarkdownFiles = this.app.vault.getMarkdownFiles() + const allFiles: PathAndAliases[] = allMarkdownFiles + .filter((file) => { + // Filter out files in excluded directories + const path = file.path.replace(/\.md$/, "") + return !this.settings.excludeDirsFromAutoLinking.some( + (excludeDir) => { + return ( + path.startsWith(excludeDir + "/") + || path === excludeDir + ) + }, + ) + }) + .map((file) => { + // Remove the .md extension + const path = file.path.replace(/\.md$/, "") + const metadata + = this.app.metadataCache.getFileCache(file)?.frontmatter + const scoped = isNamespaceScoped(metadata) + // if this property exists, prevent this file from being linked from other files + const exclude = isLinkingExcluded(metadata) - const aliases = (() => { - if (this.settings.considerAliases) { - const frontmatter = - this.app.metadataCache.getFileCache( - file, - )?.frontmatter; - const aliases = parseFrontMatterAliases(frontmatter); - return aliases; - } else { - return null; - } - })(); - return { - path, - aliases, - scoped, - exclude, - }; - }); - // Sort filenames in descending order (longer paths first) - allFiles.sort((a, b) => b.path.length - a.path.length); + const aliases = (() => { + if (this.settings.considerAliases) { + const frontmatter + = this.app.metadataCache.getFileCache( + file, + )?.frontmatter + const aliases = parseFrontMatterAliases(frontmatter) + return aliases + } + else { + return null + } + })() + return { + path, + aliases, + scoped, + exclude, + } + }) + // Sort filenames in descending order (longer paths first) + allFiles.sort((a, b) => b.path.length - a.path.length) - if (this.settings.debug) { - console.log( - "Automatic Linker: allFiles for Trie building: ", - allFiles, - ); - } + if (this.settings.debug) { + console.log( + "Automatic Linker: allFiles for Trie building: ", + allFiles, + ) + } - // Build candidateMap and Trie using the helper function. - const baseDir = this.settings.respectNewFileFolderPath - ? this.app.vault.getConfig("newFileFolderPath") - : undefined; - const { candidateMap, trie } = buildCandidateTrie( - allFiles, - baseDir, - this.settings.ignoreCase ?? false, - ); - this.candidateMap = candidateMap; - this.trie = trie; + // Build candidateMap and Trie using the helper function. + const baseDir = this.settings.respectNewFileFolderPath + ? this.app.vault.getConfig("newFileFolderPath") + : undefined + const { candidateMap, trie } = buildCandidateTrie( + allFiles, + baseDir, + this.settings.ignoreCase ?? false, + ) + this.candidateMap = candidateMap + this.trie = trie - // Clear frontmatter cache when Trie is rebuilt - this.frontmatterCache.clear(); + // Clear frontmatter cache when Trie is rebuilt + this.frontmatterCache.clear() - if (this.settings.showNotice) { - new Notice( - `Automatic Linker: Loaded all markdown files. (${allFiles.length} files)`, - ); - } - if (this.settings.debug) { - console.log( - `Automatic Linker: Loaded all markdown files. (${allFiles.length} files)`, - ); - } - } + if (this.settings.showNotice) { + new Notice( + `Automatic Linker: Loaded all markdown files. (${allFiles.length} files)`, + ) + } + if (this.settings.debug) { + console.log( + `Automatic Linker: Loaded all markdown files. (${allFiles.length} files)`, + ) + } + } - private refreshFileDataAndTrieOnFrontmatterChange(file: TFile) { - const metadata = - this.app.metadataCache.getFileCache(file)?.frontmatter; + private refreshFileDataAndTrieOnFrontmatterChange(file: TFile) { + const metadata + = this.app.metadataCache.getFileCache(file)?.frontmatter - // Extract frontmatter fields that affect the Trie - const relevantFields = { - aliases: metadata?.aliases - ? JSON.stringify(metadata.aliases) - : undefined, - scoped: isNamespaceScoped(metadata), - exclude: isLinkingExcluded(metadata), - }; + // Extract frontmatter fields that affect the Trie + const relevantFields = { + aliases: metadata?.aliases + ? JSON.stringify(metadata.aliases) + : undefined, + scoped: isNamespaceScoped(metadata), + exclude: isLinkingExcluded(metadata), + } - // Create a hash of the relevant fields - const currentHash = JSON.stringify(relevantFields); - const cachedHash = this.frontmatterCache.get(file.path); + // Create a hash of the relevant fields + const currentHash = JSON.stringify(relevantFields) + const cachedHash = this.frontmatterCache.get(file.path) - // If the hash has changed, refresh the Trie - if (currentHash !== cachedHash) { - this.frontmatterCache.set(file.path, currentHash); - this.refreshFileDataAndTrie(); + // If the hash has changed, refresh the Trie + if (currentHash !== cachedHash) { + this.frontmatterCache.set(file.path, currentHash) + this.refreshFileDataAndTrie() - if (this.settings.debug) { - console.log( - `Automatic Linker: Refreshing Trie due to frontmatter change in ${file.path}`, - ); - } - } - } + if (this.settings.debug) { + console.log( + `Automatic Linker: Refreshing Trie due to frontmatter change in ${file.path}`, + ) + } + } + } - async onload() { - await this.loadSettings(); - this.addSettingTab( - new AutomaticLinkerPluginSettingsTab(this.app, this), - ); + async onload() { + await this.loadSettings() + this.addSettingTab( + new AutomaticLinkerPluginSettingsTab(this.app, this), + ) - // Load file data and build the Trie when the layout is ready. - this.app.workspace.onLayoutReady(() => { - this.refreshFileDataAndTrie(); + // Load file data and build the Trie when the layout is ready. + this.app.workspace.onLayoutReady(() => { + this.refreshFileDataAndTrie() - this.registerEvent( - this.app.vault.on("delete", () => - this.refreshFileDataAndTrie(), - ), - ); - this.registerEvent( - this.app.vault.on("create", () => - this.refreshFileDataAndTrie(), - ), - ); - this.registerEvent( - this.app.vault.on("rename", () => - this.refreshFileDataAndTrie(), - ), - ); - this.registerEvent( - this.app.metadataCache.on("changed", (file) => - this.refreshFileDataAndTrieOnFrontmatterChange(file), - ), - ); - }); + this.registerEvent( + this.app.vault.on("delete", () => + this.refreshFileDataAndTrie(), + ), + ) + this.registerEvent( + this.app.vault.on("create", () => + this.refreshFileDataAndTrie(), + ), + ) + this.registerEvent( + this.app.vault.on("rename", () => + this.refreshFileDataAndTrie(), + ), + ) + this.registerEvent( + this.app.metadataCache.on("changed", file => + this.refreshFileDataAndTrieOnFrontmatterChange(file), + ), + ) + }) - // Command: Manually trigger link replacement for the current file. - this.addCommand({ - id: "format-file", - name: "Format file", - icon: "wand-sparkles", - editorCallback: async () => { - try { - await this.formatThenRunPrettierAndLinter(); - } catch (error) { - console.error(error); - } - }, - }); + // Command: Manually trigger link replacement for the current file. + this.addCommand({ + id: "format-file", + name: "Format file", + icon: "wand-sparkles", + editorCallback: async () => { + try { + await this.formatThenRunPrettierAndLinter() + } + catch (error) { + console.error(error) + } + }, + }) - this.addCommand({ - id: "format-vault", - name: "Format vault", - icon: "drill", - editorCallback: async () => { - try { - await this.modifyLinksForVault(); - } catch (error) { - console.error(error); - } - }, - }); + this.addCommand({ + id: "format-vault", + name: "Format vault", + icon: "drill", + editorCallback: async () => { + try { + await this.modifyLinksForVault() + } + catch (error) { + console.error(error) + } + }, + }) - this.addCommand({ - id: "rebuild-index", - name: "Rebuild index", - icon: "refresh-ccw", - editorCallback: async () => { - try { - this.refreshFileDataAndTrie(); - } catch (error) { - console.error(error); - } - }, - }); + this.addCommand({ + id: "rebuild-index", + name: "Rebuild index", + icon: "refresh-ccw", + editorCallback: async () => { + try { + this.refreshFileDataAndTrie() + } + catch (error) { + console.error(error) + } + }, + }) - this.addCommand({ - id: "format-selection", - name: "Format selection", - editorCallback: async () => { - try { - await this.mofifyLinksSelection(); - } catch (error) { - console.error(error); - } - }, - }); + this.addCommand({ + id: "format-selection", + name: "Format selection", + editorCallback: async () => { + try { + await this.mofifyLinksSelection() + } + catch (error) { + console.error(error) + } + }, + }) - this.addCommand({ - id: "copy-file-without-links", - name: "Copy file without links", - editorCallback: async () => { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile) { - return; - } - const fileContent = await this.app.vault.read(activeFile); - const { contentStart } = getFrontMatterInfo(fileContent); - const body = fileContent.slice(contentStart); - const bodyWithoutLinks = excludeLinks(body); - navigator.clipboard.writeText(bodyWithoutLinks); - }, - }); + this.addCommand({ + id: "copy-file-without-links", + name: "Copy file without links", + editorCallback: async () => { + const activeFile = this.app.workspace.getActiveFile() + if (!activeFile) { + return + } + const fileContent = await this.app.vault.read(activeFile) + const { contentStart } = getFrontMatterInfo(fileContent) + const body = fileContent.slice(contentStart) + const bodyWithoutLinks = excludeLinks(body) + navigator.clipboard.writeText(bodyWithoutLinks) + }, + }) - this.addCommand({ - id: "copy-selection-without-links", - name: "Copy selection without links", - editorCallback: async (editor: Editor) => { - // Get the start and end positions of the selection - const from = editor.getCursor("from"); - const to = editor.getCursor("to"); + this.addCommand({ + id: "copy-selection-without-links", + name: "Copy selection without links", + editorCallback: async (editor: Editor) => { + // Get the start and end positions of the selection + const from = editor.getCursor("from") + const to = editor.getCursor("to") - // Get the full lines that contain the selection - const selectedText = editor.getRange( - { line: from.line, ch: 0 }, - { line: to.line, ch: editor.getLine(to.line).length }, - ); + // Get the full lines that contain the selection + const selectedText = editor.getRange( + { line: from.line, ch: 0 }, + { line: to.line, ch: editor.getLine(to.line).length }, + ) - if (!selectedText) { - return; - } + if (!selectedText) { + return + } - // Remove minimal indent - const textWithMinimalIndent = removeMinimalIndent(selectedText); - // Remove wikilinks - const textWithoutLinks = excludeLinks(textWithMinimalIndent); + // Remove minimal indent + const textWithMinimalIndent = removeMinimalIndent(selectedText) + // Remove wikilinks + const textWithoutLinks = excludeLinks(textWithMinimalIndent) - await navigator.clipboard.writeText(textWithoutLinks); - }, - }); + await navigator.clipboard.writeText(textWithoutLinks) + }, + }) - // Optionally, override the default save command to run modifyLinks (throttled). - const saveCommandDefinition = - // @ts-ignore - this.app?.commands?.commands?.["editor:save-file"]; - const saveCallback = saveCommandDefinition?.checkCallback; - if (typeof saveCallback === "function") { - // Preserve the original save callback to call it after modifying links. - this.originalSaveCallback = saveCallback; - } + // Optionally, override the default save command to run modifyLinks (throttled). + const saveCommandDefinition + // @ts-ignore + = this.app?.commands?.commands?.["editor:save-file"] + const saveCallback = saveCommandDefinition?.checkCallback + if (typeof saveCallback === "function") { + // Preserve the original save callback to call it after modifying links. + this.originalSaveCallback = saveCallback + } - saveCommandDefinition.checkCallback = async (checking: boolean) => { - if (checking) { - return saveCallback?.(checking); - } else { - if (!this.settings.formatOnSave) { - return; - } - await sleep(this.settings.formatDelayMs ?? 100); - await this.formatThenRunPrettierAndLinter(); - } - }; - } + saveCommandDefinition.checkCallback = async (checking: boolean) => { + if (checking) { + return saveCallback?.(checking) + } + else { + if (!this.settings.formatOnSave) { + return + } + await sleep(this.settings.formatDelayMs ?? 100) + await this.formatThenRunPrettierAndLinter() + } + } + } - async onunload() { - // Restore original save command callback - const saveCommandDefinition = - // @ts-ignore - this.app?.commands?.commands?.["editor:save-file"]; - if (saveCommandDefinition && this.originalSaveCallback) { - saveCommandDefinition.checkCallback = this.originalSaveCallback; - } - } + async onunload() { + // Restore original save command callback + const saveCommandDefinition + // @ts-ignore + = this.app?.commands?.commands?.["editor:save-file"] + if (saveCommandDefinition && this.originalSaveCallback) { + saveCommandDefinition.checkCallback = this.originalSaveCallback + } + } - async loadSettings() { - this.settings = Object.assign( - {}, - DEFAULT_SETTINGS, - await this.loadData(), - ); - } + async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ) + } - async saveSettings() { - await this.saveData(this.settings); - } + async saveSettings() { + await this.saveData(this.settings) + } } diff --git a/src/path-and-aliases.types.ts b/src/path-and-aliases.types.ts index 5a103db..5e0e62f 100644 --- a/src/path-and-aliases.types.ts +++ b/src/path-and-aliases.types.ts @@ -1,6 +1,6 @@ export type PathAndAliases = { - path: string; - aliases: string[] | null; - scoped: boolean; - exclude?: boolean; -}; + path: string + aliases: string[] | null + scoped: boolean + exclude?: boolean +} diff --git a/src/remove-minimal-indent/__tests__/integration.test.ts b/src/remove-minimal-indent/__tests__/integration.test.ts index 341c533..897dec4 100644 --- a/src/remove-minimal-indent/__tests__/integration.test.ts +++ b/src/remove-minimal-indent/__tests__/integration.test.ts @@ -1,56 +1,56 @@ -import { describe, expect, it } from "vitest"; -import { excludeLinks } from "../../exclude-links"; -import { removeMinimalIndent } from ".."; +import { describe, expect, it } from "vitest" +import { excludeLinks } from "../../exclude-links" +import { removeMinimalIndent } from ".." describe("copy selection integration test", () => { - it("removes indent and wikilinks from list selection", () => { - const text = " - [[item1]]\n - [[item2]]\n - [[item3]]"; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - expect(result).toBe("- item1\n - item2\n- item3"); - }); + it("removes indent and wikilinks from list selection", () => { + const text = " - [[item1]]\n - [[item2]]\n - [[item3]]" + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + expect(result).toBe("- item1\n - item2\n- item3") + }) - it("removes indent and path-style wikilinks", () => { - const text = " - [[path/to/file1]]\n - [[another/file2]]"; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - expect(result).toBe("- file1\n- file2"); - }); + it("removes indent and path-style wikilinks", () => { + const text = " - [[path/to/file1]]\n - [[another/file2]]" + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + expect(result).toBe("- file1\n- file2") + }) - it("handles mixed content with links and code", () => { - const text = - " Some text with [[link]]\n `[[code link]]` should stay\n Another [[path/to/file]]"; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - expect(result).toBe( - "Some text with link\n`[[code link]]` should stay\nAnother file", - ); - }); + it("handles mixed content with links and code", () => { + const text + = " Some text with [[link]]\n `[[code link]]` should stay\n Another [[path/to/file]]" + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + expect(result).toBe( + "Some text with link\n`[[code link]]` should stay\nAnother file", + ) + }) - it("preserves relative indent in nested lists", () => { - const text = - " - [[item1]]\n - [[subitem1]]\n - [[subitem2]]\n - [[item2]]"; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - expect(result).toBe("- item1\n - subitem1\n - subitem2\n- item2"); - }); + it("preserves relative indent in nested lists", () => { + const text + = " - [[item1]]\n - [[subitem1]]\n - [[subitem2]]\n - [[item2]]" + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + expect(result).toBe("- item1\n - subitem1\n - subitem2\n- item2") + }) - it("handles empty lines in selection", () => { - const text = " [[link1]]\n\n [[link2]]"; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - expect(result).toBe("link1\n\nlink2"); - }); + it("handles empty lines in selection", () => { + const text = " [[link1]]\n\n [[link2]]" + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + expect(result).toBe("link1\n\nlink2") + }) - it("", () => { - const text = ` + it("", () => { + const text = ` - hello - - hello [[world]]`; - const withoutIndent = removeMinimalIndent(text); - const result = excludeLinks(withoutIndent); - const expected = ` + - hello [[world]]` + const withoutIndent = removeMinimalIndent(text) + const result = excludeLinks(withoutIndent) + const expected = ` - hello - - hello world`; - expect(result).toBe(expected); - }); -}); + - hello world` + expect(result).toBe(expected) + }) +}) diff --git a/src/remove-minimal-indent/__tests__/remove-minimal-indent.test.ts b/src/remove-minimal-indent/__tests__/remove-minimal-indent.test.ts index ff4ccc6..0ce1dbe 100644 --- a/src/remove-minimal-indent/__tests__/remove-minimal-indent.test.ts +++ b/src/remove-minimal-indent/__tests__/remove-minimal-indent.test.ts @@ -1,64 +1,64 @@ -import { describe, expect, it } from "vitest"; -import { removeMinimalIndent } from ".."; +import { describe, expect, it } from "vitest" +import { removeMinimalIndent } from ".." describe("removeMinimalIndent", () => { - it("removes common indent from all lines", () => { - const text = " line1\n line2\n line3"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\nline2\nline3"); - }); + it("removes common indent from all lines", () => { + const text = " line1\n line2\n line3" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\nline2\nline3") + }) - it("removes minimal indent when lines have different indentation", () => { - const text = " line1\n line2\n line3"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\n line2\nline3"); - }); + it("removes minimal indent when lines have different indentation", () => { + const text = " line1\n line2\n line3" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\n line2\nline3") + }) - it("handles tab indentation", () => { - const text = "\t\tline1\n\t\tline2"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\nline2"); - }); + it("handles tab indentation", () => { + const text = "\t\tline1\n\t\tline2" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\nline2") + }) - it("handles mixed spaces and tabs by treating tab as single character", () => { - const text = "\t line1\n\t line2"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\nline2"); - }); + it("handles mixed spaces and tabs by treating tab as single character", () => { + const text = "\t line1\n\t line2" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\nline2") + }) - it("preserves relative indentation", () => { - const text = " - item1\n - subitem\n - item2"; - const result = removeMinimalIndent(text); - expect(result).toBe("- item1\n - subitem\n- item2"); - }); + it("preserves relative indentation", () => { + const text = " - item1\n - subitem\n - item2" + const result = removeMinimalIndent(text) + expect(result).toBe("- item1\n - subitem\n- item2") + }) - it("ignores empty lines when calculating minimal indent", () => { - const text = " line1\n\n line2"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\n\nline2"); - }); + it("ignores empty lines when calculating minimal indent", () => { + const text = " line1\n\n line2" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\n\nline2") + }) - it("ignores whitespace-only lines when calculating minimal indent", () => { - const text = " line1\n \n line2"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\n\nline2"); - }); + it("ignores whitespace-only lines when calculating minimal indent", () => { + const text = " line1\n \n line2" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\n\nline2") + }) - it("returns text as-is when no indent", () => { - const text = "line1\nline2"; - const result = removeMinimalIndent(text); - expect(result).toBe("line1\nline2"); - }); + it("returns text as-is when no indent", () => { + const text = "line1\nline2" + const result = removeMinimalIndent(text) + expect(result).toBe("line1\nline2") + }) - it("handles single line", () => { - const text = " single line"; - const result = removeMinimalIndent(text); - expect(result).toBe("single line"); - }); + it("handles single line", () => { + const text = " single line" + const result = removeMinimalIndent(text) + expect(result).toBe("single line") + }) - it("handles list in the middle of a document", () => { - const text = " - item1\n - subitem1\n - subitem2\n - item2"; - const result = removeMinimalIndent(text); - expect(result).toBe("- item1\n - subitem1\n - subitem2\n- item2"); - }); -}); + it("handles list in the middle of a document", () => { + const text = " - item1\n - subitem1\n - subitem2\n - item2" + const result = removeMinimalIndent(text) + expect(result).toBe("- item1\n - subitem1\n - subitem2\n- item2") + }) +}) diff --git a/src/remove-minimal-indent/index.ts b/src/remove-minimal-indent/index.ts index bfd9142..f5842e1 100644 --- a/src/remove-minimal-indent/index.ts +++ b/src/remove-minimal-indent/index.ts @@ -1,57 +1,58 @@ -const TAB_SIZE = 4; +const TAB_SIZE = 4 export const removeMinimalIndent = (text: string): string => { - const lines = text.split("\n"); + const lines = text.split("\n") - // Convert tabs to spaces for consistent handling - const expandedLines = lines.map((line) => { - return line.replace(/\t/g, " ".repeat(TAB_SIZE)); - }); + // Convert tabs to spaces for consistent handling + const expandedLines = lines.map((line) => { + return line.replace(/\t/g, " ".repeat(TAB_SIZE)) + }) - // Find minimal indent (ignoring empty or whitespace-only lines) - let minIndent = Infinity; - for (const line of expandedLines) { - // Skip empty or whitespace-only lines - if (line.trim().length === 0) { - continue; - } + // Find minimal indent (ignoring empty or whitespace-only lines) + let minIndent = Infinity + for (const line of expandedLines) { + // Skip empty or whitespace-only lines + if (line.trim().length === 0) { + continue + } - // Count leading spaces - let indent = 0; - for (const char of line) { - if (char === " ") { - indent++; - } else { - break; - } - } + // Count leading spaces + let indent = 0 + for (const char of line) { + if (char === " ") { + indent++ + } + else { + break + } + } - minIndent = Math.min(minIndent, indent); - } + minIndent = Math.min(minIndent, indent) + } - // If no indented lines found, return as-is - if (minIndent === Infinity || minIndent === 0) { - return text; - } + // If no indented lines found, return as-is + if (minIndent === Infinity || minIndent === 0) { + return text + } - // Remove minimal indent from all lines and convert spaces back to tabs - const processedLines = expandedLines.map((line) => { - // Preserve empty or whitespace-only lines - if (line.trim().length === 0) { - return ""; - } + // Remove minimal indent from all lines and convert spaces back to tabs + const processedLines = expandedLines.map((line) => { + // Preserve empty or whitespace-only lines + if (line.trim().length === 0) { + return "" + } - // Remove minIndent spaces from the beginning - const dedented = line.slice(minIndent); + // Remove minIndent spaces from the beginning + const dedented = line.slice(minIndent) - // Convert leading spaces back to tabs - const leadingSpaces = dedented.match(/^ */)?.[0].length || 0; - const tabs = Math.floor(leadingSpaces / TAB_SIZE); - const remainingSpaces = leadingSpaces % TAB_SIZE; - const rest = dedented.slice(leadingSpaces); + // Convert leading spaces back to tabs + const leadingSpaces = dedented.match(/^ */)?.[0].length || 0 + const tabs = Math.floor(leadingSpaces / TAB_SIZE) + const remainingSpaces = leadingSpaces % TAB_SIZE + const rest = dedented.slice(leadingSpaces) - return "\t".repeat(tabs) + " ".repeat(remainingSpaces) + rest; - }); + return "\t".repeat(tabs) + " ".repeat(remainingSpaces) + rest + }) - return processedLines.join("\n"); -}; + return processedLines.join("\n") +} diff --git a/src/replace-links/__tests__/prev.test.ts b/src/replace-links/__tests__/prev.test.ts index 7a11378..4dbd830 100644 --- a/src/replace-links/__tests__/prev.test.ts +++ b/src/replace-links/__tests__/prev.test.ts @@ -1,1308 +1,1308 @@ -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"; +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", () => { + 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 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", () => { + { + 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 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"); - } - }); -}); + 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("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("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]]"); - }); -}); + 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("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]]", - ); - }); -}); + 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("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("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 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 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("- [[ひらがな]]と[[ひらがな]]"); - }); -}); + 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 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("이 문서는 [[문서]]이다."); - }); -}); + 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 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("这个[[文档]]很好。"); - }); -}); + 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("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]]"); -}); + 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("", () => { + 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("[[アジャイルリーダーコンピテンシーマップ]]"); - }); -}); + 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]]"); + 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]]"); - }); + 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]]"); - }); -}); + 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, - namespaceResolution: 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 with namespace when full candidate is provided", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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("replaces candidate without namespace correctly", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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"); - }); -}); + it("should not replace YYY-MM-DD formatted text when it doesn't match the candidate's shorthand", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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, - }); + it("closest siblings namespace should be used", () => { + { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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, - namespaceResolution: 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]]"); - } - }); + 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, + namespaceResolution: 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, + namespaceResolution: 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, - namespaceResolution: 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("closest children namespace should be used", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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", - namespaceResolution: 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]]", - ); + it("find closest path if the current path is in base dir and the candidate is not", () => { + const settings = { + scoped: false, + baseDir: "base", + namespaceResolution: 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", - namespaceResolution: false, - }; - const result2 = replaceLinks({ - body: "link link2", - linkResolverContext: { - filePath: "base/current-file", - trie, - candidateMap, - }, - settings: settings2, - }); - expect(result2).toBe("link link2"); - }); -}); + const settings2 = { + scoped: false, + baseDir: "base", + namespaceResolution: 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]]"); -}); + 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, - namespaceResolution: 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 not replace date format when ignoreDateFormats is true", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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]]"); - }); -}); + it("should replace date format when ignoreDateFormats is false", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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([ - [ - "x", - { - canonical: "namespace/x", - scoped: true, - namespace: "namespace", - }, - ], - [ - "z", - { - canonical: "namespace/y/z", - scoped: true, - namespace: "namespace", - }, - ], - [ - "root", - { - canonical: "root-note", - scoped: true, - namespace: "", - }, - ], - // Candidate without namespace restriction. - [ - "free", - { - canonical: "free-note", - scoped: false, - namespace: "other", - }, - ], - // For alias testing: - // Assume file "pages/HelloWorld" with shorthand "HelloWorld" - [ - "pages/HelloWorld", - { - canonical: "pages/HelloWorld", - scoped: false, - namespace: "pages", - }, - ], - // Alias "Hello" is different from the shorthand, so canonical becomes "pages/HelloWorld|Hello". - [ - "Hello", - { - canonical: "pages/HelloWorld|Hello", - scoped: false, - namespace: "pages", - }, - ], - // Also register the shorthand candidate. - [ - "HelloWorld", - { - canonical: "HelloWorld", - scoped: false, - namespace: "pages", - }, - ], - // For "tags" test: candidate key "tags" should map to canonical "tags" - [ - "pages/tags", - { - canonical: "pages/tags", - scoped: false, - namespace: "pages", - }, - ], - [ - "tags", - { - canonical: "tags", - scoped: false, - namespace: "pages", - }, - ], - // For Korean test, add candidate "문서" - [ - "문서", - { - canonical: "문서", - scoped: false, - namespace: "namespace", - }, - ], - // For Japanese test, add candidate "ひらがな" - [ - "ひらがな", - { - canonical: "ひらがな", - scoped: false, - namespace: "namespace", - }, - ], - // For Chinese test, add candidate "文档" - [ - "文档", - { - canonical: "文档", - scoped: false, - namespace: "namespace", - }, - ], - ]); + const candidateMap = new Map([ + [ + "x", + { + canonical: "namespace/x", + scoped: true, + namespace: "namespace", + }, + ], + [ + "z", + { + canonical: "namespace/y/z", + scoped: true, + namespace: "namespace", + }, + ], + [ + "root", + { + canonical: "root-note", + scoped: true, + namespace: "", + }, + ], + // Candidate without namespace restriction. + [ + "free", + { + canonical: "free-note", + scoped: false, + namespace: "other", + }, + ], + // For alias testing: + // Assume file "pages/HelloWorld" with shorthand "HelloWorld" + [ + "pages/HelloWorld", + { + canonical: "pages/HelloWorld", + scoped: false, + namespace: "pages", + }, + ], + // Alias "Hello" is different from the shorthand, so canonical becomes "pages/HelloWorld|Hello". + [ + "Hello", + { + canonical: "pages/HelloWorld|Hello", + scoped: false, + namespace: "pages", + }, + ], + // Also register the shorthand candidate. + [ + "HelloWorld", + { + canonical: "HelloWorld", + scoped: false, + namespace: "pages", + }, + ], + // For "tags" test: candidate key "tags" should map to canonical "tags" + [ + "pages/tags", + { + canonical: "pages/tags", + scoped: false, + namespace: "pages", + }, + ], + [ + "tags", + { + canonical: "tags", + scoped: false, + namespace: "pages", + }, + ], + // For Korean test, add candidate "문서" + [ + "문서", + { + canonical: "문서", + scoped: false, + namespace: "namespace", + }, + ], + // For Japanese test, add candidate "ひらがな" + [ + "ひらがな", + { + canonical: "ひらがな", + scoped: false, + namespace: "namespace", + }, + ], + // For Chinese test, add candidate "文档" + [ + "文档", + { + canonical: "文档", + scoped: false, + namespace: "namespace", + }, + ], + ]) - it("CJK - Korean > converts Korean words within sentence", () => { - const settings = { namespaceResolution: 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 - Korean > converts Korean words within sentence", () => { + const settings = { namespaceResolution: 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 = { namespaceResolution: 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 = { namespaceResolution: 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 = { namespaceResolution: 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 = { namespaceResolution: 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 = { namespaceResolution: 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("base character (pages) > unmatched namespace", () => { + const settings = { namespaceResolution: 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 = { namespaceResolution: 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 alias with canonical form using file path and alias", () => { + const settings = { namespaceResolution: 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 = { namespaceResolution: 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("aliases > replaces multiple occurrences of alias and normal candidate", () => { + const settings = { namespaceResolution: 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 = { namespaceResolution: 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"); - }); + it("replaceLinks > should not replace when inside a protected segment", () => { + const settings = { namespaceResolution: 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", { - canonical: "set/a", - scoped: true, - namespace: "set", - }); - const trie = buildTrie(Array.from(candidateMap.keys())); + 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", { + 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 = { - namespaceResolution: 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 replace candidate with scoped when effective namespace matches", () => { + // Current file is in "pages/set/...", so effective namespace is "set" + const settings = { + namespaceResolution: 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 = { - namespaceResolution: 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"); - }); - }); -}); + 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 = { + namespaceResolution: 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") + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.alias.test.ts b/src/replace-links/__tests__/replace-links.alias.test.ts index c4cf7a5..b794da4 100644 --- a/src/replace-links/__tests__/replace-links.alias.test.ts +++ b/src/replace-links/__tests__/replace-links.alias.test.ts @@ -1,230 +1,230 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - alias handling", () => { - describe("basic alias", () => { - it("replaces alias", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[HelloWorld|HW]]"); - }); + describe("basic alias", () => { + it("replaces alias", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[HelloWorld|HW]]") + }) - it("prefers exact match over alias", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "HelloWorld" }, { path: "HW" }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[HW]]"); - }); - }); + it("prefers exact match over alias", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "HelloWorld" }, { path: "HW" }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[HW]]") + }) + }) - describe("namespaced alias", () => { - it("replaces namespaced alias", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[pages/HelloWorld|HW]]"); - }); + describe("namespaced alias", () => { + it("replaces namespaced alias", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[pages/HelloWorld|HW]]") + }) - it("replaces multiple occurrences of alias and normal candidate (with baseDir)", () => { - const settings = { - scoped: false, - baseDir: "pages", - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "HelloWorld", aliases: ["Hello"] }], - settings, - }); - const result = replaceLinks({ - body: "Hello HelloWorld", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[HelloWorld|Hello]] [[HelloWorld]]"); - }); - }); + it("replaces multiple occurrences of alias and normal candidate (with baseDir)", () => { + const settings = { + scoped: false, + baseDir: "pages", + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "HelloWorld", aliases: ["Hello"] }], + settings, + }) + const result = replaceLinks({ + body: "Hello HelloWorld", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[HelloWorld|Hello]] [[HelloWorld]]") + }) + }) - describe("alias with scoped", () => { - it("respects scoped for alias", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "pages/set/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set/HelloWorld|HW]]"); - }); + describe("alias with scoped", () => { + it("respects scoped for alias", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "pages/set/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set/HelloWorld|HW]]") + }) - it("replace alias when scoped is false", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "pages/set/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set/HelloWorld|HW]]"); - }); + it("replace alias when scoped is false", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "pages/set/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set/HelloWorld|HW]]") + }) - it("does not replace alias when namespace does not match", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "pages/other/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("HW"); - }); - }); + it("does not replace alias when namespace does not match", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "pages/other/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("HW") + }) + }) - describe("alias and baseDir", () => { - it("should replace alias with baseDir", () => { - const settings = { - scoped: false, - baseDir: "pages", - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "pages/set/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set/HelloWorld|HW]]"); - }); - }); + describe("alias and baseDir", () => { + it("should replace alias with baseDir", () => { + const settings = { + scoped: false, + baseDir: "pages", + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/set/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "pages/set/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set/HelloWorld|HW]]") + }) + }) - it("Aliases with ignoreCase: false", () => { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: false, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/ティーチング", aliases: ["Teaching"] }], - settings, - }); - const result = replaceLinks({ - body: "Teaching teaching", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[ティーチング|Teaching]] teaching"); - }); + it("Aliases with ignoreCase: false", () => { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: false, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/ティーチング", aliases: ["Teaching"] }], + settings, + }) + const result = replaceLinks({ + body: "Teaching teaching", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[ティーチング|Teaching]] teaching") + }) - it("Aliases with ignoreCase: true", () => { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/ティーチング", aliases: ["Teaching"] }], - settings, - }); - const result = replaceLinks({ - body: "Teaching teaching", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "[[ティーチング|Teaching]] [[ティーチング|teaching]]", - ); - }); -}); + it("Aliases with ignoreCase: true", () => { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/ティーチング", aliases: ["Teaching"] }], + settings, + }) + const result = replaceLinks({ + body: "Teaching teaching", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "[[ティーチング|Teaching]] [[ティーチング|teaching]]", + ) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.base-dir.test.ts b/src/replace-links/__tests__/replace-links.base-dir.test.ts index 3582f82..bd7bdca 100644 --- a/src/replace-links/__tests__/replace-links.base-dir.test.ts +++ b/src/replace-links/__tests__/replace-links.base-dir.test.ts @@ -1,533 +1,533 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks with baseDir", () => { - describe("basic baseDir stripping", () => { - it("strips baseDir prefix from simple links", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - expect(result).toBe("[[xxx]]"); - }); + describe("basic baseDir stripping", () => { + it("strips baseDir prefix from simple links", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("strips baseDir prefix from nested paths", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/foo/bar" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "foo/bar", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - expect(result).toBe("[[foo/bar|bar]]"); - }); + }, + }) + expect(result).toBe("[[xxx]]") + }) - it("preserves links without baseDir when baseDir is undefined", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx" }], - settings: { - scoped: false, - baseDir: undefined, - }, - }); - const result = replaceLinks({ - body: "pages/xxx", - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings: { - baseDir: undefined, - - }, - }); - expect(result).toBe("[[pages/xxx|xxx]]"); - }); + it("strips baseDir prefix from nested paths", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/foo/bar" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "foo/bar", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("preserves links when baseDir is empty string", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx" }], - settings: { - scoped: false, - baseDir: "", - }, - }); - const result = replaceLinks({ - body: "pages/xxx", - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings: { - baseDir: "", - - }, - }); - expect(result).toBe("[[pages/xxx|xxx]]"); - }); - }); + namespaceResolution: true, + }, + }) + expect(result).toBe("[[foo/bar|bar]]") + }) - describe("baseDir with aliases", () => { - it("strips baseDir prefix from links with aliases", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx", aliases: ["alias-xxx"] }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "alias-xxx", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - expect(result).toBe("[[xxx|alias-xxx]]"); - }); + it("preserves links without baseDir when baseDir is undefined", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx" }], + settings: { + scoped: false, + baseDir: undefined, + }, + }) + const result = replaceLinks({ + body: "pages/xxx", + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings: { + baseDir: undefined, - it("handles nested path with aliases", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/foo/bar", aliases: ["alias-bar"] }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "alias-bar", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - expect(result).toBe("[[foo/bar|alias-bar]]"); - }); - }); + }, + }) + expect(result).toBe("[[pages/xxx|xxx]]") + }) - describe("baseDir with markdown tables", () => { - it("escapes pipes in links within tables when baseDir is set", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/foo/bar" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "| foo/bar |", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - expect(result).toBe("| [[foo/bar\\|bar]] |"); - }); + it("preserves links when baseDir is empty string", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx" }], + settings: { + scoped: false, + baseDir: "", + }, + }) + const result = replaceLinks({ + body: "pages/xxx", + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings: { + baseDir: "", - it("handles complex table with multiple links", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/foo" }, - { path: "pages/bar/baz" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "| foo | bar/baz |\n|---|---|\n| data | data |", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - expect(result).toBe("| [[foo]] | [[bar/baz\\|baz]] |\n|---|---|\n| data | data |"); - }); - }); + }, + }) + expect(result).toBe("[[pages/xxx|xxx]]") + }) + }) - describe("baseDir with self-linking prevention", () => { - it("prevents self-linking with baseDir enabled", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/xxx", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - preventSelfLinking: true, - }, - }); - expect(result).toBe("xxx"); - }); + describe("baseDir with aliases", () => { + it("strips baseDir prefix from links with aliases", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx", aliases: ["alias-xxx"] }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "alias-xxx", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("prevents self-linking for nested paths", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/foo/bar" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "foo/bar", - linkResolverContext: { - filePath: "pages/foo/bar", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - preventSelfLinking: true, - namespaceResolution: true, - }, - }); - expect(result).toBe("foo/bar"); - }); + }, + }) + expect(result).toBe("[[xxx|alias-xxx]]") + }) - it("allows linking to other files with same base name", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/xxx" }, - { path: "pages/foo/xxx" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/foo/bar", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - preventSelfLinking: true, - namespaceResolution: true, - }, - }); - // When baseDir is set, both "xxx" and "foo/xxx" are normalized and inserted as "xxx" and "foo/xxx" - // The trie lookup finds "xxx" first, which is the root level file - expect(result).toBe("[[xxx]]"); - }); - }); + it("handles nested path with aliases", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/foo/bar", aliases: ["alias-bar"] }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "alias-bar", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - describe("baseDir with namespace resolution", () => { - it("resolves to closest match in same namespace hierarchy", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/xxx" }, - { path: "pages/foo/xxx" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/foo/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - // Trie matches "xxx" directly since it's inserted without baseDir prefix - expect(result).toBe("[[xxx]]"); - }); + }, + }) + expect(result).toBe("[[foo/bar|alias-bar]]") + }) + }) - it("falls back to root level when no namespace match", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/xxx" }, - { path: "pages/bar/yyy" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/foo/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - expect(result).toBe("[[xxx]]"); - }); + describe("baseDir with markdown tables", () => { + it("escapes pipes in links within tables when baseDir is set", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/foo/bar" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "| foo/bar |", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("handles deeply nested namespace resolution", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/a/b/c/target" }, - { path: "pages/a/target" }, - { path: "pages/target" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "target", - linkResolverContext: { - filePath: "pages/a/b/c/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - // Trie matches "target" directly - root level file is matched first - expect(result).toBe("[[target]]"); - }); - }); + namespaceResolution: true, + }, + }) + expect(result).toBe("| [[foo/bar\\|bar]] |") + }) - describe("baseDir with case-insensitive matching", () => { - it("strips baseDir prefix with case-insensitive match", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/XXX" }], - settings: { - scoped: false, - baseDir: "pages", - ignoreCase: true, - }, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - ignoreCase: true, - }, - }); - // When normalized paths match exactly (case-insensitively), no alias is added - // The original text "xxx" is used as the link text - expect(result).toBe("[[xxx]]"); - }); + it("handles complex table with multiple links", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/foo" }, + { path: "pages/bar/baz" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "| foo | bar/baz |\n|---|---|\n| data | data |", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("preserves original case in text with nested paths", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/Foo/Bar" }], - settings: { - scoped: false, - baseDir: "pages", - ignoreCase: true, - }, - }); - const result = replaceLinks({ - body: "foo/bar", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - ignoreCase: true, - namespaceResolution: true, - }, - }); - expect(result).toBe("[[Foo/Bar|bar]]"); - }); - }); + namespaceResolution: true, + }, + }) + expect(result).toBe("| [[foo]] | [[bar/baz\\|baz]] |\n|---|---|\n| data | data |") + }) + }) - describe("baseDir edge cases", () => { - it("handles links that don't start with baseDir", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/xxx" }, - { path: "other/yyy" }, - ], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "other/yyy", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - expect(result).toBe("[[other/yyy|yyy]]"); - }); + describe("baseDir with self-linking prevention", () => { + it("prevents self-linking with baseDir enabled", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/xxx", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - it("handles empty body", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/xxx" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - expect(result).toBe(""); - }); + preventSelfLinking: true, + }, + }) + expect(result).toBe("xxx") + }) - it("handles multiple words with spaces", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/multi word file" }], - settings: { - scoped: false, - baseDir: "pages", - }, - }); - const result = replaceLinks({ - body: "multi word file", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - namespaceResolution: true, - }, - }); - expect(result).toBe("[[multi word file]]"); - }); - }); + it("prevents self-linking for nested paths", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/foo/bar" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "foo/bar", + linkResolverContext: { + filePath: "pages/foo/bar", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - describe("baseDir performance considerations", () => { - it("handles large number of files efficiently", () => { - const files = Array.from({ length: 100 }, (_, i) => ({ - path: `pages/file-${i}`, - })); - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings: { - scoped: false, - baseDir: "pages", - }, - }); + preventSelfLinking: true, + namespaceResolution: true, + }, + }) + expect(result).toBe("foo/bar") + }) - const startTime = performance.now(); - const result = replaceLinks({ - body: "file-50 and file-99", - linkResolverContext: { - filePath: "pages/test", - trie, - candidateMap, - }, - settings: { - baseDir: "pages", - - }, - }); - const endTime = performance.now(); + it("allows linking to other files with same base name", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/xxx" }, + { path: "pages/foo/xxx" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/foo/bar", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", - expect(result).toBe("[[file-50]] and [[file-99]]"); - expect(endTime - startTime).toBeLessThan(100); // Should be fast - }); - }); -}); + preventSelfLinking: true, + namespaceResolution: true, + }, + }) + // When baseDir is set, both "xxx" and "foo/xxx" are normalized and inserted as "xxx" and "foo/xxx" + // The trie lookup finds "xxx" first, which is the root level file + expect(result).toBe("[[xxx]]") + }) + }) + + describe("baseDir with namespace resolution", () => { + it("resolves to closest match in same namespace hierarchy", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/xxx" }, + { path: "pages/foo/xxx" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/foo/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + namespaceResolution: true, + }, + }) + // Trie matches "xxx" directly since it's inserted without baseDir prefix + expect(result).toBe("[[xxx]]") + }) + + it("falls back to root level when no namespace match", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/xxx" }, + { path: "pages/bar/yyy" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/foo/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + namespaceResolution: true, + }, + }) + expect(result).toBe("[[xxx]]") + }) + + it("handles deeply nested namespace resolution", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/a/b/c/target" }, + { path: "pages/a/target" }, + { path: "pages/target" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "target", + linkResolverContext: { + filePath: "pages/a/b/c/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + namespaceResolution: true, + }, + }) + // Trie matches "target" directly - root level file is matched first + expect(result).toBe("[[target]]") + }) + }) + + describe("baseDir with case-insensitive matching", () => { + it("strips baseDir prefix with case-insensitive match", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/XXX" }], + settings: { + scoped: false, + baseDir: "pages", + ignoreCase: true, + }, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + ignoreCase: true, + }, + }) + // When normalized paths match exactly (case-insensitively), no alias is added + // The original text "xxx" is used as the link text + expect(result).toBe("[[xxx]]") + }) + + it("preserves original case in text with nested paths", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/Foo/Bar" }], + settings: { + scoped: false, + baseDir: "pages", + ignoreCase: true, + }, + }) + const result = replaceLinks({ + body: "foo/bar", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + ignoreCase: true, + namespaceResolution: true, + }, + }) + expect(result).toBe("[[Foo/Bar|bar]]") + }) + }) + + describe("baseDir edge cases", () => { + it("handles links that don't start with baseDir", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/xxx" }, + { path: "other/yyy" }, + ], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "other/yyy", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + }, + }) + expect(result).toBe("[[other/yyy|yyy]]") + }) + + it("handles empty body", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/xxx" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + }, + }) + expect(result).toBe("") + }) + + it("handles multiple words with spaces", () => { + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/multi word file" }], + settings: { + scoped: false, + baseDir: "pages", + }, + }) + const result = replaceLinks({ + body: "multi word file", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + namespaceResolution: true, + }, + }) + expect(result).toBe("[[multi word file]]") + }) + }) + + describe("baseDir performance considerations", () => { + it("handles large number of files efficiently", () => { + const files = Array.from({ length: 100 }, (_, i) => ({ + path: `pages/file-${i}`, + })) + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings: { + scoped: false, + baseDir: "pages", + }, + }) + + const startTime = performance.now() + const result = replaceLinks({ + body: "file-50 and file-99", + linkResolverContext: { + filePath: "pages/test", + trie, + candidateMap, + }, + settings: { + baseDir: "pages", + + }, + }) + const endTime = performance.now() + + expect(result).toBe("[[file-50]] and [[file-99]]") + expect(endTime - startTime).toBeLessThan(100) // Should be fast + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.basic.test.ts b/src/replace-links/__tests__/replace-links.basic.test.ts index dde7f05..cd02cd8 100644 --- a/src/replace-links/__tests__/replace-links.basic.test.ts +++ b/src/replace-links/__tests__/replace-links.basic.test.ts @@ -1,311 +1,311 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks", () => { - 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]]"); - }); + 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 space", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/tidy first" }], - settings, - }); - console.log(candidateMap); - const result = replaceLinks({ - body: "tidy first", - linkResolverContext: { - filePath: "pages/Books", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[tidy first]]"); - }); + it("replaces links with space", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/tidy first" }], + settings, + }) + console.log(candidateMap) + const result = replaceLinks({ + body: "tidy first", + linkResolverContext: { + filePath: "pages/Books", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[tidy first]]") + }) - 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 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 number", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "hello" }], - settings, - }); - const result = replaceLinks({ - body: "1. hello", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("1. [[hello]]"); - }); + it("replaces links with number", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "hello" }], + settings, + }) + const result = replaceLinks({ + body: "1. hello", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("1. [[hello]]") + }) - it("does not replace links in code blocks", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "hello" }], - settings, - }); - const result = replaceLinks({ - body: "```\nhello\n```", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("```\nhello\n```"); - }); + it("does not replace links in code blocks", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "hello" }], + settings, + }) + const result = replaceLinks({ + body: "```\nhello\n```", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("```\nhello\n```") + }) - it("does not replace links in inline code", () => { - 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("does not replace links in inline code", () => { + 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("does not replace existing wikilinks", () => { - 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("does not replace existing wikilinks", () => { + 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("does not replace existing markdown links", () => { - 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("does not replace existing markdown links", () => { + 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("does not replace text in single brackets", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "hello" }, { path: "world" }], - settings, - }); - const result = replaceLinks({ - body: "[hello]", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[hello]"); - }); + it("does not replace text in single brackets", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "hello" }, { path: "world" }], + settings, + }) + const result = replaceLinks({ + body: "[hello]", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[hello]") + }) - it("does not replace consecutive single brackets", () => { - 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]"); - }); - }); + it("does not replace consecutive single brackets", () => { + 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]") + }) + }) - describe("with space", () => { - it("space without namespace", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "automatic linker" }, { path: "automatic" }], - settings, - }); - const result = replaceLinks({ - body: "automatic linker", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[automatic linker]]"); - }); - it("space with namespace", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "obsidian/automatic linker" }, - { path: "obsidian" }, - ], - settings, - }); - const result = replaceLinks({ - body: "obsidian/automatic linker", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "[[obsidian/automatic linker|automatic linker]]", - ); - }); - }); + describe("with space", () => { + it("space without namespace", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "automatic linker" }, { path: "automatic" }], + settings, + }) + const result = replaceLinks({ + body: "automatic linker", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[automatic linker]]") + }) + it("space with namespace", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "obsidian/automatic linker" }, + { path: "obsidian" }, + ], + settings, + }) + const result = replaceLinks({ + body: "obsidian/automatic linker", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "[[obsidian/automatic linker|automatic linker]]", + ) + }) + }) - describe("multiple links", () => { - it("replaces multiple links in the same line", () => { - 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]]"); - }); + describe("multiple links", () => { + it("replaces multiple links in the same line", () => { + 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]]") + }) - it("replaces multiple links in different lines", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "hello" }, { path: "world" }], - settings, - }); - const result = replaceLinks({ - body: "hello\nworld", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[hello]]\n[[world]]"); - }); - }); -}); + it("replaces multiple links in different lines", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "hello" }, { path: "world" }], + settings, + }) + const result = replaceLinks({ + body: "hello\nworld", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[hello]]\n[[world]]") + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.callout.test.ts b/src/replace-links/__tests__/replace-links.callout.test.ts index 8979287..063c437 100644 --- a/src/replace-links/__tests__/replace-links.callout.test.ts +++ b/src/replace-links/__tests__/replace-links.callout.test.ts @@ -1,504 +1,504 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - callout handling", () => { - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "VPN" }, - { path: "SSH" }, - { path: "Networking" }, - { path: "Security" }, - { path: "note" }, - { path: "info" }, - { path: "warning" }, - { path: "tip" }, - ], - settings: { - scoped: false, - baseDir: undefined, - }, - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "VPN" }, + { path: "SSH" }, + { path: "Networking" }, + { path: "Security" }, + { path: "note" }, + { path: "info" }, + { path: "warning" }, + { path: "tip" }, + ], + settings: { + scoped: false, + baseDir: undefined, + }, + }) - describe("basic callout types", () => { - it("should not link text inside [!note] callout", () => { - const input = `> [!note] -> This is about VPN and SSH`; + describe("basic callout types", () => { + it("should not link text inside [!note] callout", () => { + const input = `> [!note] +> This is about VPN and SSH` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should not link text inside [!info] callout", () => { - const input = `> [!info] -> VPN is important for Security`; + it("should not link text inside [!info] callout", () => { + const input = `> [!info] +> VPN is important for Security` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should not link text inside [!warning] callout", () => { - const input = `> [!warning] -> Check your SSH configuration`; + it("should not link text inside [!warning] callout", () => { + const input = `> [!warning] +> Check your SSH configuration` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should not link text inside [!tip] callout", () => { - const input = `> [!tip] -> Use VPN for better Networking`; + it("should not link text inside [!tip] callout", () => { + const input = `> [!tip] +> Use VPN for better Networking` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); + expect(result).toBe(input) + }) + }) - describe("collapsible callouts", () => { - it("should not link text inside collapsible callout with minus", () => { - const input = `> [!note]- -> VPN and SSH details here`; + describe("collapsible callouts", () => { + it("should not link text inside collapsible callout with minus", () => { + const input = `> [!note]- +> VPN and SSH details here` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should not link text inside collapsible callout with plus", () => { - const input = `> [!info]+ -> Networking with VPN`; + it("should not link text inside collapsible callout with plus", () => { + const input = `> [!info]+ +> Networking with VPN` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); + expect(result).toBe(input) + }) + }) - describe("callouts with custom titles", () => { - it("should not link text in callout with custom title", () => { - const input = `> [!note] Custom Title Here -> This is about VPN`; + describe("callouts with custom titles", () => { + it("should not link text in callout with custom title", () => { + const input = `> [!note] Custom Title Here +> This is about VPN` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should not link text in custom title if it matches a file", () => { - const input = `> [!note] VPN Configuration -> Details about SSH`; + it("should not link text in custom title if it matches a file", () => { + const input = `> [!note] VPN Configuration +> Details about SSH` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); + expect(result).toBe(input) + }) + }) - describe("multi-line callouts", () => { - it("should not link text in multi-line callout", () => { - const input = `> [!note] + describe("multi-line callouts", () => { + it("should not link text in multi-line callout", () => { + const input = `> [!note] > First line about VPN > Second line about SSH -> Third line about Networking`; +> Third line about Networking` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should handle callout with empty lines", () => { - const input = `> [!info] + it("should handle callout with empty lines", () => { + const input = `> [!info] > First line with VPN > -> Third line with SSH`; +> Third line with SSH` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); + expect(result).toBe(input) + }) + }) - describe("callouts with markdown formatting", () => { - it("should not link text inside callout with bold text", () => { - const input = `> [!note] -> **VPN** is important for **Security**`; + describe("callouts with markdown formatting", () => { + it("should not link text inside callout with bold text", () => { + const input = `> [!note] +> **VPN** is important for **Security**` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should preserve existing links inside callout", () => { - const input = `> [!note] -> Check [[VPN]] and SSH for details`; + it("should preserve existing links inside callout", () => { + const input = `> [!note] +> Check [[VPN]] and SSH for details` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); + expect(result).toBe(input) + }) + }) - describe("text outside callouts", () => { - it("should link text before callout", () => { - const input = `VPN is important + describe("text outside callouts", () => { + it("should link text before callout", () => { + const input = `VPN is important > [!note] -> Details about VPN`; +> Details about VPN` - const expected = `[[VPN]] is important + const expected = `[[VPN]] is important > [!note] -> Details about VPN`; +> Details about VPN` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); + expect(result).toBe(expected) + }) - it("should link text after callout", () => { - const input = `> [!note] + it("should link text after callout", () => { + const input = `> [!note] > Details about VPN -SSH is also important`; +SSH is also important` - const expected = `> [!note] + const expected = `> [!note] > Details about VPN -[[SSH]] is also important`; +[[SSH]] is also important` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); + expect(result).toBe(expected) + }) - it("should link text between two callouts", () => { - const input = `> [!note] + it("should link text between two callouts", () => { + const input = `> [!note] > First callout with VPN Networking is important here > [!info] -> Second callout with SSH`; +> Second callout with SSH` - const expected = `> [!note] + const expected = `> [!note] > First callout with VPN [[Networking]] is important here > [!info] -> Second callout with SSH`; +> Second callout with SSH` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); - }); + expect(result).toBe(expected) + }) + }) - describe("regular blockquotes (not callouts)", () => { - it("should link text in regular blockquote without callout syntax", () => { - const input = `> This is a regular quote about VPN`; + describe("regular blockquotes (not callouts)", () => { + it("should link text in regular blockquote without callout syntax", () => { + const input = `> This is a regular quote about VPN` - const expected = `> This is a regular quote about [[VPN]]`; + const expected = `> This is a regular quote about [[VPN]]` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); + expect(result).toBe(expected) + }) - it("should link text in multi-line regular blockquote", () => { - const input = `> This is about VPN -> and SSH too`; + it("should link text in multi-line regular blockquote", () => { + const input = `> This is about VPN +> and SSH too` - const expected = `> This is about [[VPN]] -> and [[SSH]] too`; + const expected = `> This is about [[VPN]] +> and [[SSH]] too` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); - }); + expect(result).toBe(expected) + }) + }) - describe("callout type names that are also file names", () => { - it("should not link 'note' in [!note] callout even if note.md exists", () => { - const input = `> [!note] -> This is a note about VPN`; + describe("callout type names that are also file names", () => { + it("should not link 'note' in [!note] callout even if note.md exists", () => { + const input = `> [!note] +> This is a note about VPN` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - // Should not link 'note' even though note.md exists - expect(result).toBe(input); - expect(result).not.toContain("[[note]]"); - }); + // Should not link 'note' even though note.md exists + expect(result).toBe(input) + expect(result).not.toContain("[[note]]") + }) - it("should not link 'info' in [!info] callout even if info.md exists", () => { - const input = `> [!info] -> This is info about SSH`; + it("should not link 'info' in [!info] callout even if info.md exists", () => { + const input = `> [!info] +> This is info about SSH` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - expect(result).not.toContain("[[info]]"); - }); + expect(result).toBe(input) + expect(result).not.toContain("[[info]]") + }) - it("should not link callout type in custom title", () => { - const input = `> [!warning] Read this warning -> Details here`; + it("should not link callout type in custom title", () => { + const input = `> [!warning] Read this warning +> Details here` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - expect(result).not.toContain("[[warning]]"); - }); + expect(result).toBe(input) + expect(result).not.toContain("[[warning]]") + }) - it("should link 'note' when used outside callout", () => { - const input = `Please read this note about VPN`; + it("should link 'note' when used outside callout", () => { + const input = `Please read this note about VPN` - const expected = `Please read this [[note]] about [[VPN]]`; + const expected = `Please read this [[note]] about [[VPN]]` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); + expect(result).toBe(expected) + }) - it("should link 'info' when used outside callout", () => { - const input = `Check the info page for details`; + it("should link 'info' when used outside callout", () => { + const input = `Check the info page for details` - const expected = `Check the [[info]] page for details`; + const expected = `Check the [[info]] page for details` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); - }); + expect(result).toBe(expected) + }) + }) - describe("edge cases", () => { - it("should handle callout at the start of document", () => { - const input = `> [!note] -> VPN details`; + describe("edge cases", () => { + it("should handle callout at the start of document", () => { + const input = `> [!note] +> VPN details` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should handle callout at the end of document", () => { - const input = `Some text here + it("should handle callout at the end of document", () => { + const input = `Some text here > [!note] -> VPN details`; +> VPN details` - const expected = `Some text here + const expected = `Some text here > [!note] -> VPN details`; +> VPN details` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(expected); - }); + expect(result).toBe(expected) + }) - it("should handle callout with different case in type", () => { - const input = `> [!NOTE] -> VPN configuration`; + it("should handle callout with different case in type", () => { + const input = `> [!NOTE] +> VPN configuration` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); + expect(result).toBe(input) + }) - it("should handle callout with hyphens in type", () => { - const input = `> [!my-custom-type] -> SSH details`; + it("should handle callout with hyphens in type", () => { + const input = `> [!my-custom-type] +> SSH details` - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - }); + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + }) - expect(result).toBe(input); - }); - }); -}); + expect(result).toBe(input) + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.cjk.test.ts b/src/replace-links/__tests__/replace-links.cjk.test.ts index 3a22262..78065e0 100644 --- a/src/replace-links/__tests__/replace-links.cjk.test.ts +++ b/src/replace-links/__tests__/replace-links.cjk.test.ts @@ -1,439 +1,439 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - CJK handling", () => { - describe("containing CJK", () => { - it("complex word boundary", () => { - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "第 3 の存在" }], - settings, - }); - const result = replaceLinks({ - body: "- 第 3 の存在から伝える", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- [[第 3 の存在]]から伝える"); - } - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "第 3 の存在" }], - settings, - }); - const result = replaceLinks({ - body: "- 第 3 の存在から伝える", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- [[第 3 の存在]]から伝える"); - } - { - 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( - "- ダニとハウスダスト[[アレルギー]]があった", - ); - } - { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: true, - namespaceResolution: true, - ignoreDateFormats: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "アレルギー" }], - settings, - }); - const result = replaceLinks({ - body: "アレルギーとアレルギー", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[アレルギー]]と[[アレルギー]]"); - } - { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: true, - namespaceResolution: true, - ignoreDateFormats: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "アレルギー" }], - settings, - }); - const result = replaceLinks({ - body: "- ダニとハウスダストアレルギーがあった", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- ダニとハウスダスト[[アレルギー]]があった", - ); - } - { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: true, - namespaceResolution: true, - ignoreDateFormats: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "person/taro-san" }], - settings, - }); - const result = replaceLinks({ - body: "- 東京出身の taro-san は大阪で働いている", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- 東京出身の [[person/taro-san|taro-san]] は大阪で働いている", - ); - } - { - const settings = { - scoped: false, - baseDir: "pages", - ignoreCase: false, - namespaceResolution: true, - ignoreDateFormats: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "person/taro-san" }], - settings, - }); - const result = replaceLinks({ - body: "- 東京出身の taro-san は大阪で働いている", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- 東京出身の [[person/taro-san|taro-san]] は大阪で働いている", - ); - } - }); - 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"); - }); + describe("containing CJK", () => { + it("complex word boundary", () => { + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "第 3 の存在" }], + settings, + }) + const result = replaceLinks({ + body: "- 第 3 の存在から伝える", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- [[第 3 の存在]]から伝える") + } + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "第 3 の存在" }], + settings, + }) + const result = replaceLinks({ + body: "- 第 3 の存在から伝える", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- [[第 3 の存在]]から伝える") + } + { + 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( + "- ダニとハウスダスト[[アレルギー]]があった", + ) + } + { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: true, + namespaceResolution: true, + ignoreDateFormats: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "アレルギー" }], + settings, + }) + const result = replaceLinks({ + body: "アレルギーとアレルギー", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[アレルギー]]と[[アレルギー]]") + } + { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: true, + namespaceResolution: true, + ignoreDateFormats: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "アレルギー" }], + settings, + }) + const result = replaceLinks({ + body: "- ダニとハウスダストアレルギーがあった", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- ダニとハウスダスト[[アレルギー]]があった", + ) + } + { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: true, + namespaceResolution: true, + ignoreDateFormats: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "person/taro-san" }], + settings, + }) + const result = replaceLinks({ + body: "- 東京出身の taro-san は大阪で働いている", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- 東京出身の [[person/taro-san|taro-san]] は大阪で働いている", + ) + } + { + const settings = { + scoped: false, + baseDir: "pages", + ignoreCase: false, + namespaceResolution: true, + ignoreDateFormats: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "person/taro-san" }], + settings, + }) + const result = replaceLinks({ + body: "- 東京出身の taro-san は大阪で働いている", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- 東京出身の [[person/taro-san|taro-san]] は大阪で働いている", + ) + } + }) + 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]]", - ); - }); - }); + 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("名前空間"); - }); + 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("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 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]]", + ) + }) + }) - describe("automatic-linker-scoped with CJK", () => { - it("should respect scoped for CJK with baseDir", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/セット/タグ", aliases: [] }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "タグ", - linkResolverContext: { - filePath: "pages/セット/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[セット/タグ|タグ]]"); - }); + describe("automatic-linker-scoped with CJK", () => { + it("should respect scoped for CJK with baseDir", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/セット/タグ", aliases: [] }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "タグ", + linkResolverContext: { + filePath: "pages/セット/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[セット/タグ|タグ]]") + }) - it("should not replace CJK when namespace does not match with baseDir", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/セット/タグ", aliases: [] }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "タグ", - linkResolverContext: { - filePath: "pages/other/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("タグ"); - }); - }); + it("should not replace CJK when namespace does not match with baseDir", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/セット/タグ", aliases: [] }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "タグ", + linkResolverContext: { + filePath: "pages/other/current", + 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("- [[ひらがな]][[ひらがな]]"); - }); + 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 with namespaces", () => { - it("should convert CJK text with namespace prefix", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "RM/関係性の勇者", aliases: [] }], - settings, - }); - const result = replaceLinks({ - body: "- 関係性の勇者は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - linkResolverContext: { - filePath: "pages/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- [[RM/関係性の勇者|関係性の勇者]]は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - ); - }); + describe("CJK with namespaces", () => { + it("should convert CJK text with namespace prefix", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "RM/関係性の勇者", aliases: [] }], + settings, + }) + const result = replaceLinks({ + body: "- 関係性の勇者は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- [[RM/関係性の勇者|関係性の勇者]]は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + ) + }) - it("should convert CJK text with namespace and alias", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "RM/関係性の勇者", aliases: ["勇者"] }], - settings, - }); - const result = replaceLinks({ - body: "- 勇者は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - linkResolverContext: { - filePath: "pages/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- [[RM/関係性の勇者|勇者]]は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - ); - }); + it("should convert CJK text with namespace and alias", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "RM/関係性の勇者", aliases: ["勇者"] }], + settings, + }) + const result = replaceLinks({ + body: "- 勇者は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- [[RM/関係性の勇者|勇者]]は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + ) + }) - it("should convert CJK text with namespace and spaces", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "RM/関係性の勇者", aliases: [] }], - settings, - }); - const result = replaceLinks({ - body: "- 関係性の勇者 は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - linkResolverContext: { - filePath: "pages/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- [[RM/関係性の勇者|関係性の勇者]] は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", - ); - }); - }); -}); + it("should convert CJK text with namespace and spaces", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "RM/関係性の勇者", aliases: [] }], + settings, + }) + const result = replaceLinks({ + body: "- 関係性の勇者 は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- [[RM/関係性の勇者|関係性の勇者]] は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている", + ) + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.code.test.ts b/src/replace-links/__tests__/replace-links.code.test.ts index d1b947d..8a90d4b 100644 --- a/src/replace-links/__tests__/replace-links.code.test.ts +++ b/src/replace-links/__tests__/replace-links.code.test.ts @@ -1,47 +1,47 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("ignore code", () => { - it("inline code", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "example" }, { path: "code" }], - settings, - }); - const result = replaceLinks({ - body: "`code` example", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("`code` [[example]]"); - }); + it("inline code", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "example" }, { path: "code" }], + settings, + }) + const result = replaceLinks({ + body: "`code` example", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("`code` [[example]]") + }) - it("code block", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "example" }, { path: "typescript" }], - settings, - }); - const result = replaceLinks({ - body: "```typescript\nexample\n```", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("```typescript\nexample\n```"); - }); -}); + it("code block", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "example" }, { path: "typescript" }], + settings, + }) + const result = replaceLinks({ + body: "```typescript\nexample\n```", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("```typescript\nexample\n```") + }) +}) diff --git a/src/replace-links/__tests__/replace-links.exclude-dirs.test.ts b/src/replace-links/__tests__/replace-links.exclude-dirs.test.ts index 5132251..97f3509 100644 --- a/src/replace-links/__tests__/replace-links.exclude-dirs.test.ts +++ b/src/replace-links/__tests__/replace-links.exclude-dirs.test.ts @@ -1,211 +1,211 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - excludeDirsFromAutoLinking", () => { - it("excludes files from specified directories", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "Templates/meeting-notes" }, - { path: "project" }, - ], - settings, - excludeDirs: ["Templates"], - }); + it("excludes files from specified directories", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "Templates/meeting-notes" }, + { path: "project" }, + ], + settings, + excludeDirs: ["Templates"], + }) - // "meeting-notes" should not be linked because it's in the Templates directory - const result1 = replaceLinks({ - body: "meeting-notes", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result1).toBe("meeting-notes"); + // "meeting-notes" should not be linked because it's in the Templates directory + const result1 = replaceLinks({ + body: "meeting-notes", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result1).toBe("meeting-notes") - // "project" should be linked because it's not in an excluded directory - const result2 = replaceLinks({ - body: "project", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result2).toBe("[[project]]"); - }); + // "project" should be linked because it's not in an excluded directory + const result2 = replaceLinks({ + body: "project", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result2).toBe("[[project]]") + }) - it("excludes multiple directories", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "Templates/daily" }, - { path: "Archive/old-note" }, - { path: "active-note" }, - ], - settings, - excludeDirs: ["Templates", "Archive"], - }); + it("excludes multiple directories", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "Templates/daily" }, + { path: "Archive/old-note" }, + { path: "active-note" }, + ], + settings, + excludeDirs: ["Templates", "Archive"], + }) - // Files in excluded directories should not be linked - const result1 = replaceLinks({ - body: "daily", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result1).toBe("daily"); + // Files in excluded directories should not be linked + const result1 = replaceLinks({ + body: "daily", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result1).toBe("daily") - const result2 = replaceLinks({ - body: "old-note", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result2).toBe("old-note"); + const result2 = replaceLinks({ + body: "old-note", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result2).toBe("old-note") - // Files in non-excluded directories should be linked - const result3 = replaceLinks({ - body: "active-note", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result3).toBe("[[active-note]]"); - }); + // Files in non-excluded directories should be linked + const result3 = replaceLinks({ + body: "active-note", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result3).toBe("[[active-note]]") + }) - it("excludes nested directories", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "Templates/Meetings/weekly" }, - { path: "regular" }, - ], - settings, - excludeDirs: ["Templates"], - }); + it("excludes nested directories", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "Templates/Meetings/weekly" }, + { path: "regular" }, + ], + settings, + excludeDirs: ["Templates"], + }) - // Files in nested excluded directories should not be linked - const result1 = replaceLinks({ - body: "weekly", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result1).toBe("weekly"); + // Files in nested excluded directories should not be linked + const result1 = replaceLinks({ + body: "weekly", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result1).toBe("weekly") - // Files outside excluded directories should be linked - const result2 = replaceLinks({ - body: "regular", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result2).toBe("[[regular]]"); - }); + // Files outside excluded directories should be linked + const result2 = replaceLinks({ + body: "regular", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result2).toBe("[[regular]]") + }) - it("works with empty exclude list", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "meeting" }, - { path: "project" }, - ], - settings, - excludeDirs: [], - }); + it("works with empty exclude list", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "meeting" }, + { path: "project" }, + ], + settings, + excludeDirs: [], + }) - // All files should be linked when exclude list is empty - const result1 = replaceLinks({ - body: "meeting", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result1).toBe("[[meeting]]"); + // All files should be linked when exclude list is empty + const result1 = replaceLinks({ + body: "meeting", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result1).toBe("[[meeting]]") - const result2 = replaceLinks({ - body: "project", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result2).toBe("[[project]]"); - }); + const result2 = replaceLinks({ + body: "project", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result2).toBe("[[project]]") + }) - it("works with baseDir and excludeDirs together", () => { - const settings = { - scoped: false, - baseDir: "pages", - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/Templates/daily" }, - { path: "pages/work" }, - ], - settings, - excludeDirs: ["pages/Templates"], - }); + it("works with baseDir and excludeDirs together", () => { + const settings = { + scoped: false, + baseDir: "pages", + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/Templates/daily" }, + { path: "pages/work" }, + ], + settings, + excludeDirs: ["pages/Templates"], + }) - // Excluded directory files should not be linked (even with short path) - const result1 = replaceLinks({ - body: "Templates/daily", - linkResolverContext: { - filePath: "pages/journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result1).toBe("Templates/daily"); + // Excluded directory files should not be linked (even with short path) + const result1 = replaceLinks({ + body: "Templates/daily", + linkResolverContext: { + filePath: "pages/journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result1).toBe("Templates/daily") - // Non-excluded directory files should be linked (using short path) - const result2 = replaceLinks({ - body: "work", - linkResolverContext: { - filePath: "pages/journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result2).toBe("[[work]]"); - }); -}); + // Non-excluded directory files should be linked (using short path) + const result2 = replaceLinks({ + body: "work", + linkResolverContext: { + filePath: "pages/journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result2).toBe("[[work]]") + }) +}) diff --git a/src/replace-links/__tests__/replace-links.ignore-case.test.ts b/src/replace-links/__tests__/replace-links.ignore-case.test.ts index 4100818..59c053e 100644 --- a/src/replace-links/__tests__/replace-links.ignore-case.test.ts +++ b/src/replace-links/__tests__/replace-links.ignore-case.test.ts @@ -1,107 +1,107 @@ -import { describe, it, expect } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, it, expect } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("ignore case", () => { - it("respects case when ignoreCase is enabled", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "hello" }], - settings, - }); + it("respects case when ignoreCase is enabled", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "hello" }], + settings, + }) - const tests = [ - { input: "hello", expected: "[[hello]]" }, - { input: "HELLO", expected: "[[HELLO]]" }, - { input: "Hello", expected: "[[Hello]]" }, - ]; + const tests = [ + { input: "hello", expected: "[[hello]]" }, + { input: "HELLO", expected: "[[HELLO]]" }, + { input: "Hello", expected: "[[Hello]]" }, + ] - for (const { input, expected } of tests) { - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe(expected); - } - }); + for (const { input, expected } of tests) { + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe(expected) + } + }) - it("handles spaces with ignoreCase enabled", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "tidy first" }], - settings, - }); + it("handles spaces with ignoreCase enabled", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "tidy first" }], + settings, + }) - const tests = [ - { input: "tidy first", expected: "[[tidy first]]" }, - { input: "TIDY FIRST", expected: "[[TIDY FIRST]]" }, - { input: "Tidy First", expected: "[[Tidy First]]" }, - ]; + const tests = [ + { input: "tidy first", expected: "[[tidy first]]" }, + { input: "TIDY FIRST", expected: "[[TIDY FIRST]]" }, + { input: "Tidy First", expected: "[[Tidy First]]" }, + ] - for (const { input, expected } of tests) { - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe(expected); - } - }); + for (const { input, expected } of tests) { + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe(expected) + } + }) - it("handles namespaces with ignoreCase enabled", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "books/clean code" }], - settings, - }); + it("handles namespaces with ignoreCase enabled", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "books/clean code" }], + settings, + }) - const tests = [ - { - input: "clean code", - expected: "[[books/clean code|clean code]]", - }, - { - input: "CLEAN CODE", - expected: "[[books/clean code|CLEAN CODE]]", - }, - { - input: "Clean Code", - expected: "[[books/clean code|Clean Code]]", - }, - ]; + const tests = [ + { + input: "clean code", + expected: "[[books/clean code|clean code]]", + }, + { + input: "CLEAN CODE", + expected: "[[books/clean code|CLEAN CODE]]", + }, + { + input: "Clean Code", + expected: "[[books/clean code|Clean Code]]", + }, + ] - for (const { input, expected } of tests) { - const result = replaceLinks({ - body: input, - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe(expected); - } - }); -}); + for (const { input, expected } of tests) { + const result = replaceLinks({ + body: input, + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe(expected) + } + }) +}) diff --git a/src/replace-links/__tests__/replace-links.namespace-resolution.test.ts b/src/replace-links/__tests__/replace-links.namespace-resolution.test.ts index 4908c2f..d6c21bf 100644 --- a/src/replace-links/__tests__/replace-links.namespace-resolution.test.ts +++ b/src/replace-links/__tests__/replace-links.namespace-resolution.test.ts @@ -1,351 +1,351 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - namespace resolution", () => { - describe("basic namespace resolution", () => { - 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"); - }); + describe("basic namespace resolution", () => { + 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("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]]", - ); - }); - }); + 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("namespace resolution nearest file path", () => { - it("closest siblings namespace should be used", () => { - { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: 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, - }); + describe("namespace resolution nearest file path", () => { + it("closest siblings namespace should be used", () => { + { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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, - namespaceResolution: 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, - namespaceResolution: 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]]"); - } - }); + 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, + namespaceResolution: 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, + namespaceResolution: 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, - namespaceResolution: 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("closest children namespace should be used", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: 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", - namespaceResolution: 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, - }); - expect(result).toBe( - "[[looooooooooooooooooooooooooooooooooooooong/link|link]] [[namespace1/link2|link2]]", - ); + it("find closest path if the current path is in base dir and the candidate is not", () => { + const settings = { + scoped: false, + baseDir: "base", + namespaceResolution: 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, + }) + expect(result).toBe( + "[[looooooooooooooooooooooooooooooooooooooong/link|link]] [[namespace1/link2|link2]]", + ) - const settings2 = { - scoped: false, - baseDir: "base", - namespaceResolution: false, - }; - const result2 = replaceLinks({ - body: "link link2", - linkResolverContext: { - filePath: "base/current-file", - trie, - candidateMap, - }, - settings: settings2, - }); - expect(result2).toBe("link link2"); - }); - }); + const settings2 = { + scoped: false, + baseDir: "base", + namespaceResolution: false, + } + const result2 = replaceLinks({ + body: "link link2", + linkResolverContext: { + filePath: "base/current-file", + trie, + candidateMap, + }, + settings: settings2, + }) + expect(result2).toBe("link link2") + }) + }) - describe("namespace resoluton with aliases", () => { - it("should resolve without aliases", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "namespace/xx/yy/link" }, - { path: "namespace/xx/link" }, - { path: "namespace/link2" }, - ], - settings, - }); - const result = replaceLinks({ - body: "link", - linkResolverContext: { - filePath: "namespace/xx/current-file", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[namespace/xx/link|link]]"); - }); + describe("namespace resoluton with aliases", () => { + it("should resolve without aliases", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "namespace/xx/yy/link" }, + { path: "namespace/xx/link" }, + { path: "namespace/link2" }, + ], + settings, + }) + const result = replaceLinks({ + body: "link", + linkResolverContext: { + filePath: "namespace/xx/current-file", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[namespace/xx/link|link]]") + }) - it("should resolve aliases", () => { - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "namespace/xx/yy/link" }, - { path: "namespace/xx/link", aliases: ["alias"] }, - { path: "namespace/link2" }, - ], - settings, - }); - const result = replaceLinks({ - body: "alias", - linkResolverContext: { - filePath: "namespace/xx/current-file", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[namespace/xx/link|alias]]"); - } - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "namespace/xx/yy/zz/link" }, - { path: "namespace/xx/yy/link", aliases: ["alias"] }, - { path: "namespace/xx/link" }, - { path: "namespace/link" }, - { path: "namespace/link2" }, - ], - settings, - }); - const result = replaceLinks({ - body: "alias", - linkResolverContext: { - filePath: "namespace/xx/yy/current-file", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[namespace/xx/yy/link|alias]]"); - } - }); - }); - describe("namespace resoluton with multiple words", () => { - it("should properly link multiple words", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "Biomarkers/ATP Levels" }, - { path: "Biomarkers/cerebral blood flow (CBF)" }, - ], - settings, - }); - const result = replaceLinks({ - body: "ATP Levels cerebral blood flow (CBF)", - linkResolverContext: { - filePath: "namespace/xx/current-file", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "[[Biomarkers/ATP Levels|ATP Levels]] [[Biomarkers/cerebral blood flow (CBF)|cerebral blood flow (CBF)]]", - ); - }); - }); -}); + it("should resolve aliases", () => { + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "namespace/xx/yy/link" }, + { path: "namespace/xx/link", aliases: ["alias"] }, + { path: "namespace/link2" }, + ], + settings, + }) + const result = replaceLinks({ + body: "alias", + linkResolverContext: { + filePath: "namespace/xx/current-file", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[namespace/xx/link|alias]]") + } + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "namespace/xx/yy/zz/link" }, + { path: "namespace/xx/yy/link", aliases: ["alias"] }, + { path: "namespace/xx/link" }, + { path: "namespace/link" }, + { path: "namespace/link2" }, + ], + settings, + }) + const result = replaceLinks({ + body: "alias", + linkResolverContext: { + filePath: "namespace/xx/yy/current-file", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[namespace/xx/yy/link|alias]]") + } + }) + }) + describe("namespace resoluton with multiple words", () => { + it("should properly link multiple words", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "Biomarkers/ATP Levels" }, + { path: "Biomarkers/cerebral blood flow (CBF)" }, + ], + settings, + }) + const result = replaceLinks({ + body: "ATP Levels cerebral blood flow (CBF)", + linkResolverContext: { + filePath: "namespace/xx/current-file", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "[[Biomarkers/ATP Levels|ATP Levels]] [[Biomarkers/cerebral blood flow (CBF)|cerebral blood flow (CBF)]]", + ) + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.namespace.test.ts b/src/replace-links/__tests__/replace-links.namespace.test.ts index f34a7da..ac2550a 100644 --- a/src/replace-links/__tests__/replace-links.namespace.test.ts +++ b/src/replace-links/__tests__/replace-links.namespace.test.ts @@ -1,128 +1,128 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - namespace resolution", () => { - 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"); - }); + 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("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]]", - ); - }); - }); + 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("automatic-linker-scoped and base dir", () => { - it("should replace candidate with scoped when effective namespace matches", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/set/a" }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "a", - linkResolverContext: { - filePath: "pages/set/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set/a|a]]"); - }); + describe("automatic-linker-scoped and base dir", () => { + it("should replace candidate with scoped when effective namespace matches", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/set/a" }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "a", + linkResolverContext: { + filePath: "pages/set/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set/a|a]]") + }) - it("should not replace candidate with scoped when effective namespace does not match", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/set/a" }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "a", - linkResolverContext: { - filePath: "pages/other/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("a"); - }); - }); -}); + it("should not replace candidate with scoped when effective namespace does not match", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/set/a" }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "a", + linkResolverContext: { + filePath: "pages/other/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("a") + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.performance.test.ts b/src/replace-links/__tests__/replace-links.performance.test.ts index 7707de7..53eddb3 100644 --- a/src/replace-links/__tests__/replace-links.performance.test.ts +++ b/src/replace-links/__tests__/replace-links.performance.test.ts @@ -1,177 +1,178 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks performance tests", () => { - const generateFiles = (count: number) => { - const files = []; - for (let i = 0; i < count; i++) { - files.push({ - path: `file${i}`, - }); - files.push({ - path: `namespace/file${i}`, - }); - files.push({ - path: `deep/nested/path/file${i}`, - }); - } - return files; - }; + const generateFiles = (count: number) => { + const files = [] + for (let i = 0; i < count; i++) { + files.push({ + path: `file${i}`, + }) + files.push({ + path: `namespace/file${i}`, + }) + files.push({ + path: `deep/nested/path/file${i}`, + }) + } + return files + } - const generateBody = (wordCount: number, linkWords: string[]) => { - const words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"]; - const result = []; - - for (let i = 0; i < wordCount; i++) { - if (i % 10 === 0 && linkWords.length > 0) { - result.push(linkWords[i % linkWords.length]); - } else { - result.push(words[i % words.length]); - } - } - - return result.join(" "); - }; + const generateBody = (wordCount: number, linkWords: string[]) => { + const words = ["the", "quick", "brown", "fox", "jumps", "over", "lazy", "dog"] + const result = [] - const generateLargeBody = (paragraphs: number) => { - const sentences = [ - "This is a test document with many words.", - "It contains various terms that might be linked.", - "The performance test should measure how quickly the function processes large texts.", - "Multiple paragraphs help simulate real-world usage scenarios.", - ]; - - const result = []; - for (let p = 0; p < paragraphs; p++) { - const paragraph = []; - for (let s = 0; s < 5; s++) { - paragraph.push(sentences[s % sentences.length]); - } - result.push(paragraph.join(" ")); - } - - return result.join("\n\n"); - }; + for (let i = 0; i < wordCount; i++) { + if (i % 10 === 0 && linkWords.length > 0) { + result.push(linkWords[i % linkWords.length]) + } + else { + result.push(words[i % words.length]) + } + } - describe("small dataset performance", () => { - it("should process 100 files and 1000 words efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const files = generateFiles(100); - const linkWords = files.slice(0, 10).map(f => f.path.split("/").pop()!); - const body = generateBody(1000, linkWords); + return result.join(" ") + } - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const generateLargeBody = (paragraphs: number) => { + const sentences = [ + "This is a test document with many words.", + "It contains various terms that might be linked.", + "The performance test should measure how quickly the function processes large texts.", + "Multiple paragraphs help simulate real-world usage scenarios.", + ] - const startTime = performance.now(); + const result = [] + for (let p = 0; p < paragraphs; p++) { + const paragraph = [] + for (let s = 0; s < 5; s++) { + paragraph.push(sentences[s % sentences.length]) + } + result.push(paragraph.join(" ")) + } - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); + return result.join("\n\n") + } - const endTime = performance.now(); - const duration = endTime - startTime; + describe("small dataset performance", () => { + it("should process 100 files and 1000 words efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const files = generateFiles(100) + const linkWords = files.slice(0, 10).map(f => f.path.split("/").pop()!) + const body = generateBody(1000, linkWords) - expect(result).toBeTruthy(); - expect(duration).toBeLessThan(100); // Should complete in less than 100ms - console.log(`Small dataset processed in ${duration.toFixed(2)}ms`); - }); - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - describe("medium dataset performance", () => { - it("should process 500 files and 5000 words efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const files = generateFiles(500); - const linkWords = files.slice(0, 50).map(f => f.path.split("/").pop()!); - const body = generateBody(5000, linkWords); + const startTime = performance.now() - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) - const startTime = performance.now(); + const endTime = performance.now() + const duration = endTime - startTime - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); + expect(result).toBeTruthy() + expect(duration).toBeLessThan(100) // Should complete in less than 100ms + console.log(`Small dataset processed in ${duration.toFixed(2)}ms`) + }) + }) - const endTime = performance.now(); - const duration = endTime - startTime; + describe("medium dataset performance", () => { + it("should process 500 files and 5000 words efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const files = generateFiles(500) + const linkWords = files.slice(0, 50).map(f => f.path.split("/").pop()!) + const body = generateBody(5000, linkWords) - expect(result).toBeTruthy(); - expect(duration).toBeLessThan(500); // Should complete in less than 500ms - console.log(`Medium dataset processed in ${duration.toFixed(2)}ms`); - }); - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - describe("large dataset performance", () => { - it("should process 1000 files and 10000 words efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const files = generateFiles(1000); - const linkWords = files.slice(0, 100).map(f => f.path.split("/").pop()!); - const body = generateBody(10000, linkWords); + const startTime = performance.now() - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) - const startTime = performance.now(); + const endTime = performance.now() + const duration = endTime - startTime - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); + expect(result).toBeTruthy() + expect(duration).toBeLessThan(500) // Should complete in less than 500ms + console.log(`Medium dataset processed in ${duration.toFixed(2)}ms`) + }) + }) - const endTime = performance.now(); - const duration = endTime - startTime; + describe("large dataset performance", () => { + it("should process 1000 files and 10000 words efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const files = generateFiles(1000) + const linkWords = files.slice(0, 100).map(f => f.path.split("/").pop()!) + const body = generateBody(10000, linkWords) - expect(result).toBeTruthy(); - expect(duration).toBeLessThan(1000); // Should complete in less than 1000ms - console.log(`Large dataset processed in ${duration.toFixed(2)}ms`); - }); - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - describe("real-world document performance", () => { - it("should process document with code blocks and links efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const files = generateFiles(200); - const linkWords = files.slice(0, 20).map(f => f.path.split("/").pop()!); + const startTime = performance.now() - const body = ` + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) + + const endTime = performance.now() + const duration = endTime - startTime + + expect(result).toBeTruthy() + expect(duration).toBeLessThan(1000) // Should complete in less than 1000ms + console.log(`Large dataset processed in ${duration.toFixed(2)}ms`) + }) + }) + + describe("real-world document performance", () => { + it("should process document with code blocks and links efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const files = generateFiles(200) + const linkWords = files.slice(0, 20).map(f => f.path.split("/").pop()!) + + const body = ` # Document Title This is a test document with ${linkWords[0]} and ${linkWords[1]}. @@ -193,157 +194,157 @@ Here are some more references to ${linkWords[4]} and ${linkWords[5]}. ${generateLargeBody(10)} Final paragraph mentions ${linkWords[9]} again. - `.trim(); + `.trim() - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - const startTime = performance.now(); + const startTime = performance.now() - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) - const endTime = performance.now(); - const duration = endTime - startTime; + const endTime = performance.now() + const duration = endTime - startTime - expect(result).toBeTruthy(); - expect(result).toContain("[[file0]]"); - expect(result).toContain("[[file1]]"); - expect(result).not.toContain("```javascript\nfunction test() {\n\t// This should not be processed: [[file2]]"); - expect(duration).toBeLessThan(200); // Should complete in less than 200ms - console.log(`Real-world document processed in ${duration.toFixed(2)}ms`); - }); - }); + expect(result).toBeTruthy() + expect(result).toContain("[[file0]]") + expect(result).toContain("[[file1]]") + expect(result).not.toContain("```javascript\nfunction test() {\n\t// This should not be processed: [[file2]]") + expect(duration).toBeLessThan(200) // Should complete in less than 200ms + console.log(`Real-world document processed in ${duration.toFixed(2)}ms`) + }) + }) - describe("namespace resolution performance", () => { - it("should process namespace resolution efficiently", () => { - const settings = { - scoped: true, - baseDir: "namespace", - namespaceResolution: true, - }; - const files = generateFiles(300); - const linkWords = files.slice(0, 30).map(f => f.path.split("/").pop()!); - const body = generateBody(3000, linkWords); + describe("namespace resolution performance", () => { + it("should process namespace resolution efficiently", () => { + const settings = { + scoped: true, + baseDir: "namespace", + namespaceResolution: true, + } + const files = generateFiles(300) + const linkWords = files.slice(0, 30).map(f => f.path.split("/").pop()!) + const body = generateBody(3000, linkWords) - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - const startTime = performance.now(); + const startTime = performance.now() - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "namespace/test/document", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "namespace/test/document", + trie, + candidateMap, + }, + settings, + }) - const endTime = performance.now(); - const duration = endTime - startTime; + const endTime = performance.now() + const duration = endTime - startTime - expect(result).toBeTruthy(); - expect(duration).toBeLessThan(300); // Should complete in less than 300ms - console.log(`Namespace resolution processed in ${duration.toFixed(2)}ms`); - }); - }); + expect(result).toBeTruthy() + expect(duration).toBeLessThan(300) // Should complete in less than 300ms + console.log(`Namespace resolution processed in ${duration.toFixed(2)}ms`) + }) + }) - describe("ignore case performance", () => { - it("should process case-insensitive matching efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - }; - const files = generateFiles(200); - const linkWords = files.slice(0, 20).map(f => f.path.split("/").pop()!); - const body = generateBody(2000, linkWords.map(w => w.toUpperCase())); + describe("ignore case performance", () => { + it("should process case-insensitive matching efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + } + const files = generateFiles(200) + const linkWords = files.slice(0, 20).map(f => f.path.split("/").pop()!) + const body = generateBody(2000, linkWords.map(w => w.toUpperCase())) - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - const startTime = performance.now(); + const startTime = performance.now() - const result = replaceLinks({ - body, - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body, + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) - const endTime = performance.now(); - const duration = endTime - startTime; + const endTime = performance.now() + const duration = endTime - startTime - expect(result).toBeTruthy(); - expect(duration).toBeLessThan(250); // Should complete in less than 250ms - console.log(`Case-insensitive matching processed in ${duration.toFixed(2)}ms`); - }); - }); + expect(result).toBeTruthy() + expect(duration).toBeLessThan(250) // Should complete in less than 250ms + console.log(`Case-insensitive matching processed in ${duration.toFixed(2)}ms`) + }) + }) - describe("memory usage optimization", () => { - it("should handle fallback index caching efficiently", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - }; - const files = generateFiles(500); - const { candidateMap, trie } = buildCandidateTrieForTest({ - files, - settings, - }); + describe("memory usage optimization", () => { + it("should handle fallback index caching efficiently", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + } + const files = generateFiles(500) + const { candidateMap, trie } = buildCandidateTrieForTest({ + files, + settings, + }) - // First call should build cache - const startTime1 = performance.now(); - replaceLinks({ - body: "test file0 file1", - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); - const endTime1 = performance.now(); - const duration1 = endTime1 - startTime1; + // First call should build cache + const startTime1 = performance.now() + replaceLinks({ + body: "test file0 file1", + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) + const endTime1 = performance.now() + const duration1 = endTime1 - startTime1 - // Second call should use cache - const startTime2 = performance.now(); - replaceLinks({ - body: "test file2 file3", - linkResolverContext: { - filePath: "test/document", - trie, - candidateMap, - }, - settings, - }); - const endTime2 = performance.now(); - const duration2 = endTime2 - startTime2; + // Second call should use cache + const startTime2 = performance.now() + replaceLinks({ + body: "test file2 file3", + linkResolverContext: { + filePath: "test/document", + trie, + candidateMap, + }, + settings, + }) + const endTime2 = performance.now() + const duration2 = endTime2 - startTime2 - console.log(`First call (cache build): ${duration1.toFixed(2)}ms`); - console.log(`Second call (cache hit): ${duration2.toFixed(2)}ms`); + console.log(`First call (cache build): ${duration1.toFixed(2)}ms`) + console.log(`Second call (cache hit): ${duration2.toFixed(2)}ms`) - // Second call should be faster or similar (cache benefit) - expect(duration2).toBeLessThanOrEqual(duration1 * 1.2); // Allow 20% variance - }); - }); -}); \ No newline at end of file + // Second call should be faster or similar (cache benefit) + expect(duration2).toBeLessThanOrEqual(duration1 * 1.2) // Allow 20% variance + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.prevent-linking.test.ts b/src/replace-links/__tests__/replace-links.prevent-linking.test.ts index 9084230..1e86995 100644 --- a/src/replace-links/__tests__/replace-links.prevent-linking.test.ts +++ b/src/replace-links/__tests__/replace-links.prevent-linking.test.ts @@ -1,148 +1,148 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks with prevent-linking", () => { - it("does not link to files with exclude: true", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "private-note", exclude: true }, - { path: "public-note", exclude: false }, - ], - settings, - }); + it("does not link to files with exclude: true", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "private-note", exclude: true }, + { path: "public-note", exclude: false }, + ], + settings, + }) - const result = replaceLinks({ - body: "private-note and public-note", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body: "private-note and public-note", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) - // private-note should NOT be linked, but public-note should be linked - expect(result).toBe("private-note and [[public-note]]"); - }); + // private-note should NOT be linked, but public-note should be linked + expect(result).toBe("private-note and [[public-note]]") + }) - it("does not link to files with exclude: true even with aliases", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { - path: "private-note", - aliases: ["secret"], - exclude: true, - }, - { - path: "public-note", - aliases: ["open"], - exclude: false, - }, - ], - settings, - }); + it("does not link to files with exclude: true even with aliases", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { + path: "private-note", + aliases: ["secret"], + exclude: true, + }, + { + path: "public-note", + aliases: ["open"], + exclude: false, + }, + ], + settings, + }) - const result = replaceLinks({ - body: "secret and open", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body: "secret and open", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) - // "secret" should NOT be linked, but "open" should be linked - expect(result).toBe("secret and [[public-note|open]]"); - }); + // "secret" should NOT be linked, but "open" should be linked + expect(result).toBe("secret and [[public-note|open]]") + }) - it("does not link to files with exclude: true in namespace context", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/private-doc", exclude: true }, - { path: "pages/public-doc", exclude: false }, - ], - settings, - }); + it("does not link to files with exclude: true in namespace context", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/private-doc", exclude: true }, + { path: "pages/public-doc", exclude: false }, + ], + settings, + }) - const result = replaceLinks({ - body: "private-doc and public-doc", - linkResolverContext: { - filePath: "pages/index", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body: "private-doc and public-doc", + linkResolverContext: { + filePath: "pages/index", + trie, + candidateMap, + }, + settings, + }) - // private-doc should NOT be linked, but public-doc should be linked - expect(result).toBe("private-doc and [[public-doc]]"); - }); + // private-doc should NOT be linked, but public-doc should be linked + expect(result).toBe("private-doc and [[public-doc]]") + }) - it("handles exclude with mixed content", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "private", exclude: true }, - { path: "public", exclude: false }, - { path: "another-public" }, - ], - settings, - }); + it("handles exclude with mixed content", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "private", exclude: true }, + { path: "public", exclude: false }, + { path: "another-public" }, + ], + settings, + }) - const result = replaceLinks({ - body: "private public another-public", - linkResolverContext: { - filePath: "test", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body: "private public another-public", + linkResolverContext: { + filePath: "test", + trie, + candidateMap, + }, + settings, + }) - expect(result).toBe("private [[public]] [[another-public]]"); - }); + expect(result).toBe("private [[public]] [[another-public]]") + }) - it("does not link to exclude files in bullet points", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "private-note", exclude: true }, - { path: "public-note", exclude: false }, - ], - settings, - }); + it("does not link to exclude files in bullet points", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "private-note", exclude: true }, + { path: "public-note", exclude: false }, + ], + settings, + }) - const result = replaceLinks({ - body: "- private-note\n- public-note", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); + const result = replaceLinks({ + body: "- private-note\n- public-note", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) - expect(result).toBe("- private-note\n- [[public-note]]"); - }); -}); + expect(result).toBe("- private-note\n- [[public-note]]") + }) +}) diff --git a/src/replace-links/__tests__/replace-links.prevent-self-linking.test.ts b/src/replace-links/__tests__/replace-links.prevent-self-linking.test.ts index 069fa2d..29844f5 100644 --- a/src/replace-links/__tests__/replace-links.prevent-self-linking.test.ts +++ b/src/replace-links/__tests__/replace-links.prevent-self-linking.test.ts @@ -1,222 +1,222 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - prevent self-linking", () => { - describe("when preventSelfLinking is true", () => { - it("should not link text to its own file", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }, { path: "Welcome" }], - settings, - }); + describe("when preventSelfLinking is true", () => { + it("should not link text to its own file", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }, { path: "Welcome" }], + settings, + }) - // In VPN.md file, "VPN" should not be linked - const result = replaceLinks({ - body: "This is a note about VPN", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is a note about VPN"); - }); + // In VPN.md file, "VPN" should not be linked + const result = replaceLinks({ + body: "This is a note about VPN", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is a note about VPN") + }) - it("should link text in other files", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }, { path: "Welcome" }], - settings, - }); + it("should link text in other files", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }, { path: "Welcome" }], + settings, + }) - // In Welcome.md file, "VPN" should be linked - const result = replaceLinks({ - body: "Here is my VPN Note", - linkResolverContext: { - filePath: "Welcome", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("Here is my [[VPN]] Note"); - }); + // In Welcome.md file, "VPN" should be linked + const result = replaceLinks({ + body: "Here is my VPN Note", + linkResolverContext: { + filePath: "Welcome", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("Here is my [[VPN]] Note") + }) - it("should handle files with namespaces", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "docs/VPN" }, { path: "docs/Welcome" }], - settings, - }); + it("should handle files with namespaces", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "docs/VPN" }, { path: "docs/Welcome" }], + settings, + }) - // In docs/VPN.md file, "VPN" should not be linked - const result = replaceLinks({ - body: "This is about VPN", - linkResolverContext: { - filePath: "docs/VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is about VPN"); - }); + // In docs/VPN.md file, "VPN" should not be linked + const result = replaceLinks({ + body: "This is about VPN", + linkResolverContext: { + filePath: "docs/VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is about VPN") + }) - it("should handle files with baseDir", () => { - const settings = { - scoped: false, - baseDir: "pages", - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/VPN" }, { path: "pages/Welcome" }], - settings, - }); + it("should handle files with baseDir", () => { + const settings = { + scoped: false, + baseDir: "pages", + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/VPN" }, { path: "pages/Welcome" }], + settings, + }) - // In pages/VPN.md file, "VPN" should not be linked - const result = replaceLinks({ - body: "This is about VPN", - linkResolverContext: { - filePath: "pages/VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is about VPN"); - }); + // In pages/VPN.md file, "VPN" should not be linked + const result = replaceLinks({ + body: "This is about VPN", + linkResolverContext: { + filePath: "pages/VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is about VPN") + }) - it("should handle multiple occurrences in the same file", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }], - settings, - }); + it("should handle multiple occurrences in the same file", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }], + settings, + }) - const result = replaceLinks({ - body: "VPN is important. Always use VPN when connecting.", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("VPN is important. Always use VPN when connecting."); - }); - }); + const result = replaceLinks({ + body: "VPN is important. Always use VPN when connecting.", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("VPN is important. Always use VPN when connecting.") + }) + }) - describe("when preventSelfLinking is false", () => { - it("should link text to its own file (default behavior)", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: false, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }], - settings, - }); + describe("when preventSelfLinking is false", () => { + it("should link text to its own file (default behavior)", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: false, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }], + settings, + }) - const result = replaceLinks({ - body: "This is about VPN", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is about [[VPN]]"); - }); + const result = replaceLinks({ + body: "This is about VPN", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is about [[VPN]]") + }) - it("should link text to its own file when setting is undefined", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }], - settings, - }); + it("should link text to its own file when setting is undefined", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }], + settings, + }) - const result = replaceLinks({ - body: "This is about VPN", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is about [[VPN]]"); - }); - }); + const result = replaceLinks({ + body: "This is about VPN", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is about [[VPN]]") + }) + }) - describe("edge cases", () => { - it("should work with case-insensitive matching", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }], - settings, - }); + describe("edge cases", () => { + it("should work with case-insensitive matching", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }], + settings, + }) - const result = replaceLinks({ - body: "This is about vpn", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("This is about vpn"); - }); + const result = replaceLinks({ + body: "This is about vpn", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("This is about vpn") + }) - it("should only prevent self-links, not other links", () => { - const settings = { - scoped: false, - baseDir: undefined, - preventSelfLinking: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "VPN" }, { path: "SSH" }, { path: "Networking" }], - settings, - }); + it("should only prevent self-links, not other links", () => { + const settings = { + scoped: false, + baseDir: undefined, + preventSelfLinking: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "VPN" }, { path: "SSH" }, { path: "Networking" }], + settings, + }) - const result = replaceLinks({ - body: "VPN and SSH are both important for Networking", - linkResolverContext: { - filePath: "VPN", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("VPN and [[SSH]] are both important for [[Networking]]"); - }); - }); -}); + const result = replaceLinks({ + body: "VPN and SSH are both important for Networking", + linkResolverContext: { + filePath: "VPN", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("VPN and [[SSH]] are both important for [[Networking]]") + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.remove-alias.test.ts b/src/replace-links/__tests__/replace-links.remove-alias.test.ts index efe73d7..0d71703 100644 --- a/src/replace-links/__tests__/replace-links.remove-alias.test.ts +++ b/src/replace-links/__tests__/replace-links.remove-alias.test.ts @@ -1,349 +1,349 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +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 settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/xxx]]"); - }); + describe("basic alias removal", () => { + it("removes alias for links in specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/xxx]]") + }) - it("keeps alias for links not in specified directory", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "other/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[other/xxx|xxx]]"); - }); + it("keeps alias for links not in specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "other/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[other/xxx|xxx]]") + }) - it("removes alias for links in subdirectories", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/subdir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/subdir/xxx]]"); - }); - }); + it("removes alias for links in subdirectories", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/subdir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/subdir/xxx]]") + }) + }) - describe("multiple directories", () => { - it("removes alias for links in any specified directory", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: ["dir1", "dir2"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "dir1/xxx" }, - { path: "dir2/yyy" }, - { path: "other/zzz" }, - ], - settings, - }); - const result = replaceLinks({ - body: "xxx yyy zzz", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir1/xxx]] [[dir2/yyy]] [[other/zzz|zzz]]"); - }); - }); + describe("multiple directories", () => { + it("removes alias for links in any specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: ["dir1", "dir2"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "dir1/xxx" }, + { path: "dir2/yyy" }, + { path: "other/zzz" }, + ], + settings, + }) + const result = replaceLinks({ + body: "xxx yyy zzz", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir1/xxx]] [[dir2/yyy]] [[other/zzz|zzz]]") + }) + }) - describe("with baseDir", () => { - it("removes alias based on normalized path with baseDir", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/xxx]]"); - }); + describe("with baseDir", () => { + it("removes alias based on normalized path with baseDir", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/xxx]]") + }) - it("keeps alias when path starts with baseDir but not with specified dir", () => { - const settings = { - scoped: false, - baseDir: "pages", - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "pages/other/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "pages/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[other/xxx|xxx]]"); - }); - }); + it("keeps alias when path starts with baseDir but not with specified dir", () => { + const settings = { + scoped: false, + baseDir: "pages", + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "pages/other/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "pages/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[other/xxx|xxx]]") + }) + }) - describe("with frontmatter aliases", () => { - it("removes alias from frontmatter alias in specified directory", () => { - const settings = { - scoped: false, - baseDir: undefined, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/HelloWorld]]"); - }); + describe("with frontmatter aliases", () => { + it("removes alias from frontmatter alias in specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/HelloWorld]]") + }) - it("keeps alias from frontmatter alias not in specified directory", () => { - const settings = { - scoped: false, - baseDir: undefined, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "other/HelloWorld", aliases: ["HW"] }], - settings, - }); - const result = replaceLinks({ - body: "HW", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[other/HelloWorld|HW]]"); - }); - }); + it("keeps alias from frontmatter alias not in specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "other/HelloWorld", aliases: ["HW"] }], + settings, + }) + const result = replaceLinks({ + body: "HW", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[other/HelloWorld|HW]]") + }) + }) - describe("edge cases", () => { - it("handles empty removeAliasInDirs array", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: [], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/xxx|xxx]]"); - }); + describe("edge cases", () => { + it("handles empty removeAliasInDirs array", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: [], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/xxx|xxx]]") + }) - it("handles undefined removeAliasInDirs", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/xxx|xxx]]"); - }); + it("handles undefined removeAliasInDirs", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/xxx|xxx]]") + }) - it("handles links without alias in specified directory", () => { - const settings = { - scoped: false, - baseDir: undefined, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "dir/xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[dir/xxx]]"); - }); + it("handles links without alias in specified directory", () => { + const settings = { + scoped: false, + baseDir: undefined, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "dir/xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[dir/xxx]]") + }) - it("works with ignoreCase option", () => { - const settings = { - scoped: false, - baseDir: undefined, - ignoreCase: true, - removeAliasInDirs: ["Dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "Dir/Xxx" }], - settings, - }); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[Dir/Xxx]]"); - }); - }); + it("works with ignoreCase option", () => { + const settings = { + scoped: false, + baseDir: undefined, + ignoreCase: true, + removeAliasInDirs: ["Dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "Dir/Xxx" }], + settings, + }) + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[Dir/Xxx]]") + }) + }) - describe("in markdown tables", () => { - it("removes alias in markdown tables", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: ["dir"], - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir/xxx" }], - settings, - }); - const result = replaceLinks({ - body: "| xxx |", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("| [[dir/xxx]] |"); - }); - }); + describe("in markdown tables", () => { + it("removes alias in markdown tables", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: ["dir"], + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir/xxx" }], + settings, + }) + const result = replaceLinks({ + body: "| xxx |", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("| [[dir/xxx]] |") + }) + }) - describe("performance", () => { - it("handles large number of directories efficiently", () => { - // Create array of 100 directories - const manyDirs = Array.from({ length: 100 }, (_, i) => `dir${i}`); - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - removeAliasInDirs: manyDirs, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "dir50/xxx" }], - settings, - }); + describe("performance", () => { + it("handles large number of directories efficiently", () => { + // Create array of 100 directories + const manyDirs = Array.from({ length: 100 }, (_, i) => `dir${i}`) + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + removeAliasInDirs: manyDirs, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "dir50/xxx" }], + settings, + }) - const startTime = performance.now(); - const result = replaceLinks({ - body: "xxx", - linkResolverContext: { - filePath: "current", - trie, - candidateMap, - }, - settings, - }); - const endTime = performance.now(); + const startTime = performance.now() + const result = replaceLinks({ + body: "xxx", + linkResolverContext: { + filePath: "current", + trie, + candidateMap, + }, + settings, + }) + const endTime = performance.now() - expect(result).toBe("[[dir50/xxx]]"); - // Should complete in reasonable time (less than 10ms) - expect(endTime - startTime).toBeLessThan(10); - }); - }); -}); + expect(result).toBe("[[dir50/xxx]]") + // Should complete in reasonable time (less than 10ms) + expect(endTime - startTime).toBeLessThan(10) + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.restrict-namespace.test.ts b/src/replace-links/__tests__/replace-links.restrict-namespace.test.ts index e015ab7..6990ba2 100644 --- a/src/replace-links/__tests__/replace-links.restrict-namespace.test.ts +++ b/src/replace-links/__tests__/replace-links.restrict-namespace.test.ts @@ -1,83 +1,83 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("replaceLinks - restrict namespace", () => { - describe("automatic-linker-scoped with baseDir", () => { - it("should respect scoped with baseDir", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/set/tag", aliases: [] }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "tag", - linkResolverContext: { - filePath: "pages/set/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set/tag|tag]]"); - }); + describe("automatic-linker-scoped with baseDir", () => { + it("should respect scoped with baseDir", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/set/tag", aliases: [] }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "tag", + linkResolverContext: { + filePath: "pages/set/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set/tag|tag]]") + }) - it("should not replace when namespace does not match with baseDir", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/set/tag" }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "tag", - linkResolverContext: { - filePath: "pages/other/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("tag"); - }); + it("should not replace when namespace does not match with baseDir", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/set/tag" }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "tag", + linkResolverContext: { + filePath: "pages/other/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("tag") + }) - it("should handle multiple namespaces with scoped", () => { - const settings = { - scoped: true, - baseDir: "pages", - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "pages/set1/tag1" }, - { path: "pages/set2/tag2" }, - { path: "pages/other/current" }, - ], - settings, - }); - const result = replaceLinks({ - body: "tag1 tag2", - linkResolverContext: { - filePath: "pages/set1/current", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("[[set1/tag1|tag1]] tag2"); - }); - }); -}); + it("should handle multiple namespaces with scoped", () => { + const settings = { + scoped: true, + baseDir: "pages", + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "pages/set1/tag1" }, + { path: "pages/set2/tag2" }, + { path: "pages/other/current" }, + ], + settings, + }) + const result = replaceLinks({ + body: "tag1 tag2", + linkResolverContext: { + filePath: "pages/set1/current", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("[[set1/tag1|tag1]] tag2") + }) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.table.test.ts b/src/replace-links/__tests__/replace-links.table.test.ts index b39fb28..73b785e 100644 --- a/src/replace-links/__tests__/replace-links.table.test.ts +++ b/src/replace-links/__tests__/replace-links.table.test.ts @@ -1,37 +1,37 @@ -import { describe, it, expect } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, it, expect } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("table", () => { - it("escape pipe inside table", () => { - const settings = { - scoped: false, - baseDir: undefined, - namespaceResolution: true, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "ns/note1" }, { path: "ns/note2" }], - settings, - }); - const result = replaceLinks({ - body: ` + it("escape pipe inside table", () => { + const settings = { + scoped: false, + baseDir: undefined, + namespaceResolution: true, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "ns/note1" }, { path: "ns/note2" }], + settings, + }) + const result = replaceLinks({ + body: ` | Test Item | | | --------------------------------- | --- | | note1 | | | note2 | | `, - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe(` + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe(` | Test Item | | | --------------------------------- | --- | | [[ns/note1\\|note1]] | | | [[ns/note2\\|note2]] | | -`); - }); -}); +`) + }) +}) diff --git a/src/replace-links/__tests__/replace-links.url.test.ts b/src/replace-links/__tests__/replace-links.url.test.ts index c73d36f..fa23d99 100644 --- a/src/replace-links/__tests__/replace-links.url.test.ts +++ b/src/replace-links/__tests__/replace-links.url.test.ts @@ -1,236 +1,236 @@ -import { describe, expect, it } from "vitest"; -import { replaceLinks } from "../replace-links"; -import { buildCandidateTrieForTest } from "./test-helpers"; +import { describe, expect, it } from "vitest" +import { replaceLinks } from "../replace-links" +import { buildCandidateTrieForTest } from "./test-helpers" describe("ignore url", () => { - it("one url", () => { - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example" }, - { path: "http" }, - { path: "https" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- https://example.com", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- https://example.com"); - } - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "st" }], - settings, - }); - const result = replaceLinks({ - body: "- https://x.com/xxxx/status/12345?t=25S02Tda", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- https://x.com/xxxx/status/12345?t=25S02Tda"); - } - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "http" }], - settings, - }); - const result = replaceLinks({ - body: "Hello https://claude.ai/chat/xxx", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("Hello https://claude.ai/chat/xxx"); - } - { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [{ path: "http" }], - settings, - }); - const result = replaceLinks({ - body: "こんにちは https://claude.ai/chat/xxx", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("こんにちは https://claude.ai/chat/xxx"); - } - }); + it("one url", () => { + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example" }, + { path: "http" }, + { path: "https" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- https://example.com", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- https://example.com") + } + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "st" }], + settings, + }) + const result = replaceLinks({ + body: "- https://x.com/xxxx/status/12345?t=25S02Tda", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- https://x.com/xxxx/status/12345?t=25S02Tda") + } + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "http" }], + settings, + }) + const result = replaceLinks({ + body: "Hello https://claude.ai/chat/xxx", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("Hello https://claude.ai/chat/xxx") + } + { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [{ path: "http" }], + settings, + }) + const result = replaceLinks({ + body: "こんにちは https://claude.ai/chat/xxx", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("こんにちは https://claude.ai/chat/xxx") + } + }) - it("multiple urls", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example" }, - { path: "example1" }, - { path: "https" }, - { path: "http" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- https://example.com https://example1.com", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- https://example.com https://example1.com"); - }); + it("multiple urls", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example" }, + { path: "example1" }, + { path: "https" }, + { path: "http" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- https://example.com https://example1.com", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- https://example.com https://example1.com") + }) - it("multiple urls with links", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example1" }, - { path: "example" }, - { path: "link" }, - { path: "https" }, - { path: "http" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- https://example.com https://example1.com link", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- https://example.com https://example1.com [[link]]", - ); - }); -}); + it("multiple urls with links", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example1" }, + { path: "example" }, + { path: "link" }, + { path: "https" }, + { path: "http" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- https://example.com https://example1.com link", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- https://example.com https://example1.com [[link]]", + ) + }) +}) describe("ignore markdown url", () => { - it("one url", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example" }, - { path: "title" }, - { path: "https" }, - { path: "http" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- [title](https://example.com)", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe("- [title](https://example.com)"); - }); + it("one url", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example" }, + { path: "title" }, + { path: "https" }, + { path: "http" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- [title](https://example.com)", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe("- [title](https://example.com)") + }) - it("multiple urls", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example1" }, - { path: "example2" }, - { path: "title1" }, - { path: "title2" }, - { path: "https" }, - { path: "http" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- [title1](https://example1.com) [title2](https://example2.com)", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- [title1](https://example1.com) [title2](https://example2.com)", - ); - }); + it("multiple urls", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example1" }, + { path: "example2" }, + { path: "title1" }, + { path: "title2" }, + { path: "https" }, + { path: "http" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- [title1](https://example1.com) [title2](https://example2.com)", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- [title1](https://example1.com) [title2](https://example2.com)", + ) + }) - it("multiple urls with links", () => { - const settings = { - scoped: false, - baseDir: undefined, - }; - const { candidateMap, trie } = buildCandidateTrieForTest({ - files: [ - { path: "example1" }, - { path: "example2" }, - { path: "title1" }, - { path: "title2" }, - { path: "https" }, - { path: "http" }, - { path: "link" }, - ], - settings, - }); - const result = replaceLinks({ - body: "- [title1](https://example1.com) [title2](https://example2.com) link", - linkResolverContext: { - filePath: "journals/2022-01-01", - trie, - candidateMap, - }, - settings, - }); - expect(result).toBe( - "- [title1](https://example1.com) [title2](https://example2.com) [[link]]", - ); - }); -}); + it("multiple urls with links", () => { + const settings = { + scoped: false, + baseDir: undefined, + } + const { candidateMap, trie } = buildCandidateTrieForTest({ + files: [ + { path: "example1" }, + { path: "example2" }, + { path: "title1" }, + { path: "title2" }, + { path: "https" }, + { path: "http" }, + { path: "link" }, + ], + settings, + }) + const result = replaceLinks({ + body: "- [title1](https://example1.com) [title2](https://example2.com) link", + linkResolverContext: { + filePath: "journals/2022-01-01", + trie, + candidateMap, + }, + settings, + }) + expect(result).toBe( + "- [title1](https://example1.com) [title2](https://example2.com) [[link]]", + ) + }) +}) diff --git a/src/replace-links/__tests__/test-helpers.ts b/src/replace-links/__tests__/test-helpers.ts index e6594de..e48e9c5 100644 --- a/src/replace-links/__tests__/test-helpers.ts +++ b/src/replace-links/__tests__/test-helpers.ts @@ -1,43 +1,43 @@ -import { PathAndAliases } from "../../path-and-aliases.types"; -import { buildCandidateTrie } from "../../trie"; +import { PathAndAliases } from "../../path-and-aliases.types" +import { buildCandidateTrie } from "../../trie" export const buildCandidateTrieForTest = ({ - files, - settings: { scoped, baseDir, ignoreCase }, - excludeDirs = [], + files, + settings: { scoped, baseDir, ignoreCase }, + excludeDirs = [], }: { - files: { path: string; aliases?: string[]; exclude?: boolean }[]; - settings: { - scoped: boolean; - baseDir: string | undefined; - ignoreCase?: boolean; - }; - excludeDirs?: string[]; + files: { path: string, aliases?: string[], exclude?: boolean }[] + settings: { + scoped: boolean + baseDir: string | undefined + ignoreCase?: boolean + } + excludeDirs?: string[] }) => { - // Filter out files that are in excluded directories - const filteredFiles = files.filter((file) => { - return !excludeDirs.some((excludeDir) => { - return ( - file.path.startsWith(excludeDir + "/") || - file.path === excludeDir - ); - }); - }); + // Filter out files that are in excluded directories + const filteredFiles = files.filter((file) => { + return !excludeDirs.some((excludeDir) => { + return ( + file.path.startsWith(excludeDir + "/") + || file.path === excludeDir + ) + }) + }) - const sortedFiles: PathAndAliases[] = filteredFiles - .slice() - .sort((a, b) => b.path.length - a.path.length) - .map(({ path, aliases, exclude }) => ({ - path, - aliases: aliases || null, - scoped, - exclude, - })); + const sortedFiles: PathAndAliases[] = filteredFiles + .slice() + .sort((a, b) => b.path.length - a.path.length) + .map(({ path, aliases, exclude }) => ({ + path, + aliases: aliases || null, + scoped, + exclude, + })) - const { candidateMap, trie } = buildCandidateTrie( - sortedFiles, - baseDir, - ignoreCase ?? false, - ); - return { candidateMap, trie }; -}; + const { candidateMap, trie } = buildCandidateTrie( + sortedFiles, + baseDir, + ignoreCase ?? false, + ) + return { candidateMap, trie } +} diff --git a/src/replace-links/replace-links.ts b/src/replace-links/replace-links.ts index 7bad0ee..cd7dca8 100644 --- a/src/replace-links/replace-links.ts +++ b/src/replace-links/replace-links.ts @@ -1,935 +1,944 @@ -import { CandidateData, getTopLevelDirectoryName, TrieNode } from "../trie"; +import { CandidateData, getTopLevelDirectoryName, TrieNode } from "../trie" // Types for the replaceLinks function export interface LinkResolverContext { - filePath: string; - trie: TrieNode; - candidateMap: Map; + filePath: string + trie: TrieNode + candidateMap: Map } export interface ReplaceLinksSettings { - namespaceResolution?: boolean; - baseDir?: string; - ignoreDateFormats?: boolean; - ignoreCase?: boolean; - preventSelfLinking?: boolean; - removeAliasInDirs?: string[]; + namespaceResolution?: boolean + baseDir?: string + ignoreDateFormats?: boolean + ignoreCase?: boolean + preventSelfLinking?: boolean + removeAliasInDirs?: string[] } export interface LinkGeneratorParams { - linkPath: string; - sourcePath: string; - alias?: string; - isInTable?: boolean; + linkPath: string + sourcePath: string + alias?: string + isInTable?: boolean } -export type LinkGenerator = (params: LinkGeneratorParams) => string; +export type LinkGenerator = (params: LinkGeneratorParams) => string export interface ReplaceLinksOptions { - body: string; - linkResolverContext: LinkResolverContext; - settings?: ReplaceLinksSettings; - linkGenerator?: LinkGenerator; + body: string + linkResolverContext: LinkResolverContext + settings?: ReplaceLinksSettings + linkGenerator?: LinkGenerator } // Constants and Regular Expressions const REGEX_PATTERNS = { - PROTECTED: + PROTECTED: /(```[\s\S]*?```|`[^`]*`|\[\[[^\]]+\]\]|\[[^\]]+\]\([^)]+\)|\[[^\]]+\]|https?:\/\/[^\s]+)/g, - DATE_FORMAT: /^\d{4}-\d{2}-\d{2}$/, - MONTH_NOTE: /^[0-9]{1,2}$/, - CJK: /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u, - CJK_CANDIDATE: + DATE_FORMAT: /^\d{4}-\d{2}-\d{2}$/, + MONTH_NOTE: /^[0-9]{1,2}$/, + CJK: /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u, + CJK_CANDIDATE: /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\s\d]+$/u, - KOREAN: /^[\p{Script=Hangul}]+$/u, - JAPANESE: /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\s\d]+$/u, - URL: /^(https?:\/\/[^\s]+)/, - PROTECTED_LINK: /^\s*(\[\[[^\]]+\]\]|\[[^\]]+\]\([^)]+\))\s*$/, - KOREAN_SUFFIX: /^(이다\.?)/, - KOREAN_PARTICLES: /^(는|은)/, - JAPANESE_PARTICLES: + KOREAN: /^[\p{Script=Hangul}]+$/u, + JAPANESE: /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\s\d]+$/u, + URL: /^(https?:\/\/[^\s]+)/, + PROTECTED_LINK: /^\s*(\[\[[^\]]+\]\]|\[[^\]]+\]\([^)]+\))\s*$/, + KOREAN_SUFFIX: /^(이다\.?)/, + KOREAN_PARTICLES: /^(는|은)/, + JAPANESE_PARTICLES: /^(가|는|을|에|서|와|로부터|까지|보다|로|의|나|도|또한)/, - TABLE_SEPARATOR: /^[|:\s-]+$/, - WORD_BOUNDARY: /[\p{L}\p{N}_/-]/u, - WHITESPACE: /[\t\n\r ]/, -} as const; + TABLE_SEPARATOR: /^[|:\s-]+$/, + WORD_BOUNDARY: /[\p{L}\p{N}_/-]/u, + WHITESPACE: /[\t\n\r ]/, +} as const // Text Analysis Utilities const isWordBoundary = (char: string | undefined): boolean => { - if (char === undefined) return true; - if (REGEX_PATTERNS.CJK.test(char)) return true; - return ( - !REGEX_PATTERNS.WORD_BOUNDARY.test(char) || - REGEX_PATTERNS.WHITESPACE.test(char) - ); -}; + if (char === undefined) return true + if (REGEX_PATTERNS.CJK.test(char)) return true + return ( + !REGEX_PATTERNS.WORD_BOUNDARY.test(char) + || REGEX_PATTERNS.WHITESPACE.test(char) + ) +} const isMonthNote = (candidate: string): boolean => - REGEX_PATTERNS.MONTH_NOTE.test(candidate) && - parseInt(candidate, 10) >= 1 && - parseInt(candidate, 10) <= 12; + REGEX_PATTERNS.MONTH_NOTE.test(candidate) + && parseInt(candidate, 10) >= 1 + && parseInt(candidate, 10) <= 12 const isProtectedLink = (body: string): boolean => - REGEX_PATTERNS.PROTECTED_LINK.test(body); + REGEX_PATTERNS.PROTECTED_LINK.test(body) -const isCjkText = (text: string): boolean => REGEX_PATTERNS.CJK.test(text); +const isCjkText = (text: string): boolean => REGEX_PATTERNS.CJK.test(text) const isCjkCandidate = (candidate: string): boolean => - REGEX_PATTERNS.CJK_CANDIDATE.test(candidate); + REGEX_PATTERNS.CJK_CANDIDATE.test(candidate) const isKoreanText = (text: string): boolean => - REGEX_PATTERNS.KOREAN.test(text); + REGEX_PATTERNS.KOREAN.test(text) const isJapaneseText = (text: string): boolean => - REGEX_PATTERNS.JAPANESE.test(text) && !REGEX_PATTERNS.KOREAN.test(text); + REGEX_PATTERNS.JAPANESE.test(text) && !REGEX_PATTERNS.KOREAN.test(text) // Cache for fallback index to avoid rebuilding const fallbackIndexCache = new WeakMap< - Map, - Map>> ->(); + Map, + Map>> +>() const buildFallbackIndex = ( - candidateMap: Map, - ignoreCase?: boolean, + candidateMap: Map, + ignoreCase?: boolean, ): Map> => { - // Get or create cache for this candidateMap - let cacheForMap = fallbackIndexCache.get(candidateMap); - if (!cacheForMap) { - cacheForMap = new Map(); - fallbackIndexCache.set(candidateMap, cacheForMap); - } + // Get or create cache for this candidateMap + let cacheForMap = fallbackIndexCache.get(candidateMap) + if (!cacheForMap) { + cacheForMap = new Map() + fallbackIndexCache.set(candidateMap, cacheForMap) + } - // Check if we have cached result for this ignoreCase setting - const cacheKey = ignoreCase ? "ignoreCase" : "normal"; - const cached = cacheForMap.get(cacheKey); - if (cached) { - return cached; - } + // Check if we have cached result for this ignoreCase setting + const cacheKey = ignoreCase ? "ignoreCase" : "normal" + const cached = cacheForMap.get(cacheKey) + if (cached) { + return cached + } - // Build new fallback index - const fallbackIndex = new Map>(); + // Build new fallback index + const fallbackIndex = new Map>() - for (const [key, data] of candidateMap.entries()) { - const slashIndex = key.lastIndexOf("/"); - if (slashIndex === -1) continue; + for (const [key, data] of candidateMap.entries()) { + const slashIndex = key.lastIndexOf("/") + if (slashIndex === -1) continue - const shorthand = key.slice(slashIndex + 1); - const indexKey = ignoreCase ? shorthand.toLowerCase() : shorthand; + const shorthand = key.slice(slashIndex + 1) + const indexKey = ignoreCase ? shorthand.toLowerCase() : shorthand - let arr = fallbackIndex.get(indexKey); - if (!arr) { - arr = []; - fallbackIndex.set(indexKey, arr); - } - arr.push([key, data]); - } + let arr = fallbackIndex.get(indexKey) + if (!arr) { + arr = [] + fallbackIndex.set(indexKey, arr) + } + arr.push([key, data]) + } - // Cache the result - cacheForMap.set(cacheKey, fallbackIndex); - return fallbackIndex; -}; + // Cache the result + cacheForMap.set(cacheKey, fallbackIndex) + return fallbackIndex +} const getCurrentNamespace = (filePath: string, baseDir?: string): string => { - if (baseDir) { - return getTopLevelDirectoryName(filePath, baseDir); - } + if (baseDir) { + return getTopLevelDirectoryName(filePath, baseDir) + } - const segments = filePath.split("/"); - return segments[0] || ""; -}; + const segments = filePath.split("/") + return segments[0] || "" +} const normalizeCanonicalPath = (linkPath: string, baseDir?: string): string => { - if (baseDir && linkPath.startsWith(baseDir + "/")) { - return linkPath.slice((baseDir + "/").length); - } - return linkPath; -}; + if (baseDir && linkPath.startsWith(baseDir + "/")) { + return linkPath.slice((baseDir + "/").length) + } + return linkPath +} const extractLinkParts = ( - canonicalPath: string, -): { linkPath: string; alias: string; hasAlias: boolean } => { - const pipeIndex = canonicalPath.indexOf("|"); - const hasAlias = pipeIndex !== -1; + canonicalPath: string, +): { linkPath: string, alias: string, hasAlias: boolean } => { + const pipeIndex = canonicalPath.indexOf("|") + const hasAlias = pipeIndex !== -1 - if (hasAlias) { - const linkPath = canonicalPath.slice(0, pipeIndex); - const alias = canonicalPath.slice(pipeIndex + 1); - return { linkPath, alias, hasAlias }; - } + if (hasAlias) { + const linkPath = canonicalPath.slice(0, pipeIndex) + const alias = canonicalPath.slice(pipeIndex + 1) + return { linkPath, alias, hasAlias } + } - return { linkPath: canonicalPath, alias: "", hasAlias }; -}; + return { linkPath: canonicalPath, alias: "", hasAlias } +} // Self-linking Prevention const isSelfLink = ( - candidateData: CandidateData, - currentFilePath: string, - settings: ReplaceLinksSettings = {}, + candidateData: CandidateData, + currentFilePath: string, + settings: ReplaceLinksSettings = {}, ): boolean => { - if (!settings.preventSelfLinking) { - return false; - } + if (!settings.preventSelfLinking) { + return false + } - // Extract the link path from the canonical path - const { linkPath } = extractLinkParts(candidateData.canonical); + // Extract the link path from the canonical path + const { linkPath } = extractLinkParts(candidateData.canonical) - // Normalize paths for comparison - const normalizedLinkPath = normalizeCanonicalPath( - linkPath, - settings.baseDir, - ); - const normalizedCurrentPath = normalizeCanonicalPath( - currentFilePath, - settings.baseDir, - ); + // Normalize paths for comparison + const normalizedLinkPath = normalizeCanonicalPath( + linkPath, + settings.baseDir, + ) + const normalizedCurrentPath = normalizeCanonicalPath( + currentFilePath, + settings.baseDir, + ) - // Compare the paths - return normalizedLinkPath === normalizedCurrentPath; -}; + // Compare the paths + return normalizedLinkPath === normalizedCurrentPath +} // Helper function to check if a path should have its alias removed const shouldRemoveAlias = ( - normalizedPath: string, - removeAliasInDirs?: string[], + normalizedPath: string, + removeAliasInDirs?: string[], ): boolean => { - if (!removeAliasInDirs || removeAliasInDirs.length === 0) { - return false; - } + if (!removeAliasInDirs || removeAliasInDirs.length === 0) { + return false + } - // Early return for paths without slashes - if (!normalizedPath.includes("/")) { - 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; - } - } + // 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; -}; + return false +} // Link Content Creation const createLinkContent = ( - candidateData: CandidateData, - originalMatchedText: string, - settings: ReplaceLinksSettings = {}, -): { linkPath: string; alias?: string } => { - const { linkPath, alias, hasAlias } = extractLinkParts( - candidateData.canonical, - ); - const normalizedPath = normalizeCanonicalPath(linkPath, settings.baseDir); + candidateData: CandidateData, + originalMatchedText: string, + settings: ReplaceLinksSettings = {}, +): { linkPath: string, alias?: string } => { + 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, - ); + // 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 { linkPath: normalizedPath }; - } - // Use originalMatchedText to preserve case when ignoreCase is enabled - const displayAlias = settings.ignoreCase ? originalMatchedText : alias; - return { linkPath: normalizedPath, alias: displayAlias }; - } + if (hasAlias) { + // If alias removal is enabled for this directory, return path without alias + if (removeAlias) { + return { linkPath: normalizedPath } + } + // Use originalMatchedText to preserve case when ignoreCase is enabled + const displayAlias = settings.ignoreCase ? originalMatchedText : alias + return { linkPath: normalizedPath, alias: displayAlias } + } - if (normalizedPath.includes("/")) { - // If alias removal is enabled for this directory, return path without alias - if (removeAlias) { - return { linkPath: normalizedPath }; - } + if (normalizedPath.includes("/")) { + // If alias removal is enabled for this directory, return path without alias + if (removeAlias) { + return { linkPath: normalizedPath } + } - // For paths with slashes, use the last segment as the display text - const lastSegment = - normalizedPath.split("/").pop() || originalMatchedText; + // For paths with slashes, use the last segment as the display text + const lastSegment + = normalizedPath.split("/").pop() || originalMatchedText - // If ignoreCase is enabled and originalMatchedText contains a slash, - // use the last segment of originalMatchedText to preserve case - let displayText = lastSegment; - if (settings.ignoreCase && originalMatchedText.includes("/")) { - const originalLastSegment = originalMatchedText.split("/").pop(); - if (originalLastSegment) { - displayText = originalLastSegment; - } - } else if (settings.ignoreCase) { - // If originalMatchedText doesn't contain a slash, use it as-is - displayText = originalMatchedText; - } + // If ignoreCase is enabled and originalMatchedText contains a slash, + // use the last segment of originalMatchedText to preserve case + let displayText = lastSegment + if (settings.ignoreCase && originalMatchedText.includes("/")) { + const originalLastSegment = originalMatchedText.split("/").pop() + if (originalLastSegment) { + displayText = originalLastSegment + } + } + else if (settings.ignoreCase) { + // If originalMatchedText doesn't contain a slash, use it as-is + displayText = originalMatchedText + } - return { linkPath: normalizedPath, alias: displayText }; - } + return { linkPath: normalizedPath, alias: displayText } + } - // No explicit alias, no '/' in normalizedPath - if (settings.ignoreCase) { - if ( - originalMatchedText.toLowerCase() === normalizedPath.toLowerCase() - ) { - return { linkPath: originalMatchedText }; - } else { - return { linkPath: normalizedPath, alias: originalMatchedText }; - } - } else { - if (originalMatchedText !== normalizedPath) { - return { linkPath: normalizedPath, alias: originalMatchedText }; - } else { - return { linkPath: normalizedPath }; - } - } -}; + // No explicit alias, no '/' in normalizedPath + if (settings.ignoreCase) { + if ( + originalMatchedText.toLowerCase() === normalizedPath.toLowerCase() + ) { + return { linkPath: originalMatchedText } + } + else { + return { linkPath: normalizedPath, alias: originalMatchedText } + } + } + else { + if (originalMatchedText !== normalizedPath) { + return { linkPath: normalizedPath, alias: originalMatchedText } + } + else { + return { linkPath: normalizedPath } + } + } +} // Default link generator that creates standard Obsidian wikilinks export const defaultLinkGenerator: LinkGenerator = ({ - linkPath, - alias, - isInTable = false, + linkPath, + alias, + isInTable = false, }: LinkGeneratorParams): string => { - let linkContent = linkPath; + let linkContent = linkPath - if (alias) { - linkContent = `${linkPath}|${alias}`; - } + if (alias) { + linkContent = `${linkPath}|${alias}` + } - if (isInTable && linkContent.includes("|")) { - linkContent = linkContent.replace(/\|/g, "\\|"); - } + if (isInTable && linkContent.includes("|")) { + linkContent = linkContent.replace(/\|/g, "\\|") + } - return `[[${linkContent}]]`; -}; + return `[[${linkContent}]]` +} // Candidate Validation const shouldSkipCandidate = ( - candidate: string, - settings: ReplaceLinksSettings, + candidate: string, + settings: ReplaceLinksSettings, ): boolean => { - if ( - settings.ignoreDateFormats && - REGEX_PATTERNS.DATE_FORMAT.test(candidate) - ) { - return true; - } - return isMonthNote(candidate); -}; + if ( + settings.ignoreDateFormats + && REGEX_PATTERNS.DATE_FORMAT.test(candidate) + ) { + return true + } + return isMonthNote(candidate) +} // Markdown Table Detection const isMarkdownTableLine = (line: string): boolean => { - const trimmedLine = line.trim(); - if (!trimmedLine || !trimmedLine.includes("|")) { - return false; - } + const trimmedLine = line.trim() + if (!trimmedLine || !trimmedLine.includes("|")) { + return false + } - if ( - trimmedLine.startsWith("|") && - trimmedLine.endsWith("|") && - REGEX_PATTERNS.TABLE_SEPARATOR.test(trimmedLine) - ) { - return true; - } + if ( + trimmedLine.startsWith("|") + && trimmedLine.endsWith("|") + && REGEX_PATTERNS.TABLE_SEPARATOR.test(trimmedLine) + ) { + return true + } - return trimmedLine.startsWith("|") && trimmedLine.endsWith("|"); -}; + return trimmedLine.startsWith("|") && trimmedLine.endsWith("|") +} const isIndexInsideMarkdownTable = (text: string, index: number): boolean => { - // Find the start of the line containing the index - let lineStart = text.lastIndexOf("\n", index - 1) + 1; - if (lineStart === 0 && text[0] !== "\n") { - lineStart = 0; - } + // Find the start of the line containing the index + let lineStart = text.lastIndexOf("\n", index - 1) + 1 + if (lineStart === 0 && text[0] !== "\n") { + lineStart = 0 + } - // Find the end of the line containing the index - let lineEnd = text.indexOf("\n", index); - if (lineEnd === -1) { - lineEnd = text.length; - } + // Find the end of the line containing the index + let lineEnd = text.indexOf("\n", index) + if (lineEnd === -1) { + lineEnd = text.length + } - // Extract the line and check if it's a table line - const line = text.slice(lineStart, lineEnd); - return isMarkdownTableLine(line); -}; + // Extract the line and check if it's a table line + const line = text.slice(lineStart, lineEnd) + return isMarkdownTableLine(line) +} // Processing functions for different text types const processCjkText = ( - text: string, - trie: TrieNode, - candidateMap: Map, - currentNamespace: string, - filePath: string, - linkGenerator: LinkGenerator, - settings: ReplaceLinksSettings = {}, + text: string, + trie: TrieNode, + candidateMap: Map, + currentNamespace: string, + filePath: string, + linkGenerator: LinkGenerator, + settings: ReplaceLinksSettings = {}, ): string => { - // For CJK texts that might contain non-CJK terms like "taro-san", ensure we use a consistent approach - // Pass the proper filePath to maintain correct namespace resolution - return processStandardText( - text, - trie, - candidateMap, - buildFallbackIndex(candidateMap, settings.ignoreCase), - filePath, - currentNamespace, - linkGenerator, - settings, - ); -}; + // For CJK texts that might contain non-CJK terms like "taro-san", ensure we use a consistent approach + // Pass the proper filePath to maintain correct namespace resolution + return processStandardText( + text, + trie, + candidateMap, + buildFallbackIndex(candidateMap, settings.ignoreCase), + filePath, + currentNamespace, + linkGenerator, + settings, + ) +} // Fallback Search Processing const processFallbackSearch = ( - text: string, - startIndex: number, - fallbackIndex: Map>, - filePath: string, - currentNamespace: string, - linkGenerator: LinkGenerator, - settings: ReplaceLinksSettings, -): { result: string; newIndex: number } | null => { - // Early boundary check - if start isn't a word boundary, skip - const prevChar = text[startIndex - 1]; - if (!isWordBoundary(prevChar)) { - return null; - } + text: string, + startIndex: number, + fallbackIndex: Map>, + filePath: string, + currentNamespace: string, + linkGenerator: LinkGenerator, + settings: ReplaceLinksSettings, +): { result: string, newIndex: number } | null => { + // Early boundary check - if start isn't a word boundary, skip + const prevChar = text[startIndex - 1] + if (!isWordBoundary(prevChar)) { + return null + } - let longestMatch: { - word: string; - length: number; - key: string; - candidateList: Array<[string, CandidateData]>; - } | null = null; + let longestMatch: { + word: string + length: number + key: string + candidateList: Array<[string, CandidateData]> + } | null = null - // Iterate through potential multi-word sequences starting from startIndex - const maxSearchLength = Math.min(text.length - startIndex, 100); // Limit search length for performance + // Iterate through potential multi-word sequences starting from startIndex + const maxSearchLength = Math.min(text.length - startIndex, 100) // Limit search length for performance - let potentialMatch = ""; - let searchWord = ""; + let potentialMatch = "" + let searchWord = "" - for (let length = 1; length <= maxSearchLength; length++) { - const endIndex = startIndex + length; - const currentChar = text[startIndex + length - 1]; - potentialMatch += currentChar; - searchWord = settings.ignoreCase - ? searchWord + currentChar.toLowerCase() - : potentialMatch; + for (let length = 1; length <= maxSearchLength; length++) { + const endIndex = startIndex + length + const currentChar = text[startIndex + length - 1] + potentialMatch += currentChar + searchWord = settings.ignoreCase + ? searchWord + currentChar.toLowerCase() + : potentialMatch - // Check if this potential match exists in fallback index - const candidateList = fallbackIndex.get(searchWord); - if (!candidateList) { - continue; - } + // Check if this potential match exists in fallback index + const candidateList = fallbackIndex.get(searchWord) + if (!candidateList) { + continue + } - // Basic boundary check: next char should be a boundary if not end of text - const nextChar = text[endIndex]; - if (!isWordBoundary(nextChar)) { - continue; // This isn't a valid match end - } + // Basic boundary check: next char should be a boundary if not end of text + const nextChar = text[endIndex] + if (!isWordBoundary(nextChar)) { + continue // This isn't a valid match end + } - // Skip date formats and month notes - if (shouldSkipCandidate(potentialMatch, settings)) { - continue; // Try longer match - } + // Skip date formats and month notes + if (shouldSkipCandidate(potentialMatch, settings)) { + continue // Try longer match + } - // Found a valid candidate - longestMatch = { - word: potentialMatch, - length: length, - key: searchWord, - candidateList: candidateList, - }; - // Continue checking for even longer matches - } + // Found a valid candidate + longestMatch = { + word: potentialMatch, + length: length, + key: searchWord, + candidateList: candidateList, + } + // Continue checking for even longer matches + } - // Process the longest valid match found - if (!longestMatch) return null; + // Process the longest valid match found + if (!longestMatch) return null - // Filter candidates based on namespace restrictions - const filteredCandidates = longestMatch.candidateList.filter( - ([, data]) => !(data.scoped && data.namespace !== currentNamespace), - ); + // Filter candidates based on namespace restrictions + const filteredCandidates = longestMatch.candidateList.filter( + ([, data]) => !(data.scoped && data.namespace !== currentNamespace), + ) - let bestCandidateData: CandidateData | null = null; + let bestCandidateData: CandidateData | null = null - if (filteredCandidates.length === 1) { - bestCandidateData = filteredCandidates[0][1]; - } else if (filteredCandidates.length > 1) { - const bestCandidateResult = findBestCandidateInSameNamespace( - filteredCandidates, - filePath, - settings, - ); - if (bestCandidateResult) { - bestCandidateData = bestCandidateResult[1]; - } - } + if (filteredCandidates.length === 1) { + bestCandidateData = filteredCandidates[0][1] + } + else if (filteredCandidates.length > 1) { + const bestCandidateResult = findBestCandidateInSameNamespace( + filteredCandidates, + filePath, + settings, + ) + if (bestCandidateResult) { + bestCandidateData = bestCandidateResult[1] + } + } - if (!bestCandidateData) return null; + if (!bestCandidateData) return null - // Check if this is a self-link and should be prevented - if (isSelfLink(bestCandidateData, filePath, settings)) { - return { - result: longestMatch.word, - newIndex: startIndex + longestMatch.length, - }; - } + // Check if this is a self-link and should be prevented + if (isSelfLink(bestCandidateData, filePath, settings)) { + return { + result: longestMatch.word, + newIndex: startIndex + longestMatch.length, + } + } - // Create the link - const { linkPath, alias } = createLinkContent( - bestCandidateData, - longestMatch.word, - settings, - ); - const isInTable = isIndexInsideMarkdownTable(text, startIndex); - const finalLink = linkGenerator({ - linkPath, - sourcePath: filePath, - alias, - isInTable, - }); + // Create the link + const { linkPath, alias } = createLinkContent( + bestCandidateData, + longestMatch.word, + settings, + ) + const isInTable = isIndexInsideMarkdownTable(text, startIndex) + const finalLink = linkGenerator({ + linkPath, + sourcePath: filePath, + alias, + isInTable, + }) - return { - result: finalLink, - newIndex: startIndex + longestMatch.length, - }; -}; + return { + result: finalLink, + newIndex: startIndex + longestMatch.length, + } +} // Korean Language Processing const handleKoreanSpecialCases = ( - text: string, - i: number, - candidate: string, - candidateData: CandidateData, - filePath: string, - linkGenerator: LinkGenerator, - settings: ReplaceLinksSettings = {}, -): { result: string; newIndex: number } | null => { - const remaining = text.slice(i + candidate.length); + text: string, + i: number, + candidate: string, + candidateData: CandidateData, + filePath: string, + linkGenerator: LinkGenerator, + settings: ReplaceLinksSettings = {}, +): { result: string, newIndex: number } | null => { + const remaining = text.slice(i + candidate.length) - // Special handling when followed by "이다" - const suffixMatch = remaining.match(REGEX_PATTERNS.KOREAN_SUFFIX); - if (suffixMatch) { - // Check if this is a self-link and should be prevented - if (isSelfLink(candidateData, filePath, settings)) { - return { - result: candidate + suffixMatch[0], - newIndex: i + candidate.length + suffixMatch[0].length, - }; - } + // Special handling when followed by "이다" + const suffixMatch = remaining.match(REGEX_PATTERNS.KOREAN_SUFFIX) + if (suffixMatch) { + // Check if this is a self-link and should be prevented + if (isSelfLink(candidateData, filePath, settings)) { + return { + result: candidate + suffixMatch[0], + newIndex: i + candidate.length + suffixMatch[0].length, + } + } - const { linkPath, alias } = createLinkContent( - candidateData, - candidate, - settings, - ); - const finalLink = linkGenerator({ - linkPath, - sourcePath: filePath, - alias, - isInTable: false, - }); + const { linkPath, alias } = createLinkContent( + candidateData, + candidate, + settings, + ) + const finalLink = linkGenerator({ + linkPath, + sourcePath: filePath, + alias, + isInTable: false, + }) - return { - result: finalLink + suffixMatch[0], - newIndex: i + candidate.length + suffixMatch[0].length, - }; - } + return { + result: finalLink + suffixMatch[0], + newIndex: i + candidate.length + suffixMatch[0].length, + } + } - // Special handling when followed by particles like "는" or "은" - if (remaining.match(REGEX_PATTERNS.KOREAN_PARTICLES)) { - return { - result: text[i], - newIndex: i + 1, - }; - } + // Special handling when followed by particles like "는" or "은" + if (remaining.match(REGEX_PATTERNS.KOREAN_PARTICLES)) { + return { + result: text[i], + newIndex: i + 1, + } + } - return null; -}; + return null +} const findBestCandidateInSameNamespace = ( - filteredCandidates: Array<[string, CandidateData]>, - filePath: string, - settings: ReplaceLinksSettings = {}, + filteredCandidates: Array<[string, CandidateData]>, + filePath: string, + settings: ReplaceLinksSettings = {}, ): [string, CandidateData] | null => { - let bestCandidate: [string, CandidateData] | null = null; - let bestScore = -1; + let bestCandidate: [string, CandidateData] | null = null + let bestScore = -1 - // Get the directory portion of the current file (if any) - const filePathDir = filePath.includes("/") - ? filePath.slice(0, filePath.lastIndexOf("/")) - : ""; - const filePathSegments = filePathDir ? filePathDir.split("/") : []; + // Get the directory portion of the current file (if any) + const filePathDir = filePath.includes("/") + ? filePath.slice(0, filePath.lastIndexOf("/")) + : "" + const filePathSegments = filePathDir ? filePathDir.split("/") : [] - for (const [key, data] of filteredCandidates) { - const slashIndex = key.lastIndexOf("/"); - const candidateDir = key.slice(0, slashIndex); - const candidateSegments = candidateDir.split("/"); - let score = 0; + for (const [key, data] of filteredCandidates) { + const slashIndex = key.lastIndexOf("/") + const candidateDir = key.slice(0, slashIndex) + const candidateSegments = candidateDir.split("/") + let score = 0 - // Calculate common prefix score - for ( - let idx = 0; - idx < Math.min(candidateSegments.length, filePathSegments.length); - idx++ - ) { - if (candidateSegments[idx] === filePathSegments[idx]) { - score++; - } else { - break; - } - } + // Calculate common prefix score + for ( + let idx = 0; + idx < Math.min(candidateSegments.length, filePathSegments.length); + idx++ + ) { + if (candidateSegments[idx] === filePathSegments[idx]) { + score++ + } + else { + break + } + } - if (score > bestScore) { - bestScore = score; - bestCandidate = [key, data]; - } else if (score === bestScore && bestCandidate !== null) { - if (filePathDir === "" && settings.baseDir) { - // When the current file is in the base directory, compare candidates by relative depth - const basePrefix = settings.baseDir + "/"; - const getRelativeDepth = (k: string): number => { - if (k.startsWith(basePrefix)) { - // Remove the baseDir part and count the remaining segments - const relativeParts = k - .slice(basePrefix.length) - .split("/"); - return relativeParts.length - 1; - } - return Infinity; - }; + if (score > bestScore) { + bestScore = score + bestCandidate = [key, data] + } + else if (score === bestScore && bestCandidate !== null) { + if (filePathDir === "" && settings.baseDir) { + // When the current file is in the base directory, compare candidates by relative depth + const basePrefix = settings.baseDir + "/" + const getRelativeDepth = (k: string): number => { + if (k.startsWith(basePrefix)) { + // Remove the baseDir part and count the remaining segments + const relativeParts = k + .slice(basePrefix.length) + .split("/") + return relativeParts.length - 1 + } + return Infinity + } - const candidateDepth = getRelativeDepth(key); - const bestCandidateDepth = getRelativeDepth(bestCandidate[0]); + const candidateDepth = getRelativeDepth(key) + const bestCandidateDepth = getRelativeDepth(bestCandidate[0]) - // Prefer the candidate with lower depth or shorter path - if ( - candidateDepth < bestCandidateDepth || - (candidateDepth === bestCandidateDepth && - key.length < bestCandidate[0].length) - ) { - bestCandidate = [key, data]; - } - } else { - // Otherwise, choose the candidate with fewer directory segments - const currentBestDir = bestCandidate[0].slice( - 0, - bestCandidate[0].lastIndexOf("/"), - ); - const currentBestSegments = currentBestDir.split("/"); + // Prefer the candidate with lower depth or shorter path + if ( + candidateDepth < bestCandidateDepth + || (candidateDepth === bestCandidateDepth + && key.length < bestCandidate[0].length) + ) { + bestCandidate = [key, data] + } + } + else { + // Otherwise, choose the candidate with fewer directory segments + const currentBestDir = bestCandidate[0].slice( + 0, + bestCandidate[0].lastIndexOf("/"), + ) + const currentBestSegments = currentBestDir.split("/") - if ( - candidateSegments.length < currentBestSegments.length || - (candidateSegments.length === currentBestSegments.length && - key.length < bestCandidate[0].length) - ) { - bestCandidate = [key, data]; - } - } - } - } + if ( + candidateSegments.length < currentBestSegments.length + || (candidateSegments.length === currentBestSegments.length + && key.length < bestCandidate[0].length) + ) { + bestCandidate = [key, data] + } + } + } + } - return bestCandidate; -}; + return bestCandidate +} const processStandardText = ( - text: string, - trie: TrieNode, - candidateMap: Map, - fallbackIndex: Map>, - filePath: string, - currentNamespace: string, - linkGenerator: LinkGenerator, - settings: ReplaceLinksSettings = {}, + text: string, + trie: TrieNode, + candidateMap: Map, + fallbackIndex: Map>, + filePath: string, + currentNamespace: string, + linkGenerator: LinkGenerator, + settings: ReplaceLinksSettings = {}, ): string => { - let result = ""; - let i = 0; + let result = "" + let i = 0 - outer: while (i < text.length) { - // Check for URLs first - only if current character could start a URL - if (text[i] === "h" && text.slice(i, i + 4) === "http") { - const urlMatch = text.slice(i).match(REGEX_PATTERNS.URL); - if (urlMatch) { - result += urlMatch[0]; - i += urlMatch[0].length; - continue; - } - } + outer: while (i < text.length) { + // Check for URLs first - only if current character could start a URL + if (text[i] === "h" && text.slice(i, i + 4) === "http") { + const urlMatch = text.slice(i).match(REGEX_PATTERNS.URL) + if (urlMatch) { + result += urlMatch[0] + i += urlMatch[0].length + continue + } + } - // Try to find a candidate using the trie - let node = trie; - let lastCandidate: { candidate: string; length: number } | null = null; - let j = i; - let candidateBuilder = ""; + // Try to find a candidate using the trie + let node = trie + let lastCandidate: { candidate: string, length: number } | null = null + let j = i + let candidateBuilder = "" - while (j < text.length) { - const ch = text[j]; - const chLower = settings.ignoreCase ? ch.toLowerCase() : ch; - candidateBuilder += ch; + while (j < text.length) { + const ch = text[j] + const chLower = settings.ignoreCase ? ch.toLowerCase() : ch + candidateBuilder += ch - const child = node.children.get(chLower); - if (!child) break; + const child = node.children.get(chLower) + if (!child) break - node = child; - if (node.candidate) { - const candidateIsCjk = isCjkCandidate(candidateBuilder); + node = child + if (node.candidate) { + const candidateIsCjk = isCjkCandidate(candidateBuilder) - if (candidateIsCjk || isWordBoundary(text[j + 1])) { - lastCandidate = { - candidate: node.candidate, - length: j - i + 1, - }; - } - } - j++; - } + if (candidateIsCjk || isWordBoundary(text[j + 1])) { + lastCandidate = { + candidate: node.candidate, + length: j - i + 1, + } + } + } + j++ + } - if (lastCandidate) { - const candidate = candidateBuilder.slice(0, lastCandidate.length); + if (lastCandidate) { + const candidate = candidateBuilder.slice(0, lastCandidate.length) - // Skip if it's a date format - if ( - settings.ignoreDateFormats && - REGEX_PATTERNS.DATE_FORMAT.test(candidate) - ) { - result += candidate; - i += lastCandidate.length; - continue outer; - } + // Skip if it's a date format + if ( + settings.ignoreDateFormats + && REGEX_PATTERNS.DATE_FORMAT.test(candidate) + ) { + result += candidate + i += lastCandidate.length + continue outer + } - // Skip month notes - if (isMonthNote(candidate)) { - result += candidate; - i += lastCandidate.length; - continue; - } + // Skip month notes + if (isMonthNote(candidate)) { + result += candidate + i += lastCandidate.length + continue + } - // Use the candidate found in the trie (lastCandidate.candidate) to look up in candidateMap - const trieCandidateKey = lastCandidate.candidate; + // Use the candidate found in the trie (lastCandidate.candidate) to look up in candidateMap + const trieCandidateKey = lastCandidate.candidate - // candidateMap lookup should always use the exact key from the trie result. - // Case comparison happened during trie traversal if ignoreCase is true. - const candidateData = candidateMap.get(trieCandidateKey); + // candidateMap lookup should always use the exact key from the trie result. + // Case comparison happened during trie traversal if ignoreCase is true. + const candidateData = candidateMap.get(trieCandidateKey) - // Store the original text matched for potential use as display text - const originalMatchedText = text.substring( - i, - i + lastCandidate.length, - ); + // Store the original text matched for potential use as display text + const originalMatchedText = text.substring( + i, + i + lastCandidate.length, + ) - if (candidateData) { - // Check if this is a self-link and should be prevented - if (isSelfLink(candidateData, filePath, settings)) { - result += candidate; - i += candidate.length; - continue outer; - } + if (candidateData) { + // Check if this is a self-link and should be prevented + if (isSelfLink(candidateData, filePath, settings)) { + result += candidate + i += candidate.length + continue outer + } - // Handle Korean special cases - const isKorean = isKoreanText(candidate); - if (isKorean) { - const koreanResult = handleKoreanSpecialCases( - text, - i, - candidate, - candidateData, - filePath, - linkGenerator, - settings, - ); - if (koreanResult) { - result += koreanResult.result; - i = koreanResult.newIndex; - continue outer; - } - } + // Handle Korean special cases + const isKorean = isKoreanText(candidate) + if (isKorean) { + const koreanResult = handleKoreanSpecialCases( + text, + i, + candidate, + candidateData, + filePath, + linkGenerator, + settings, + ) + if (koreanResult) { + result += koreanResult.result + i = koreanResult.newIndex + continue outer + } + } - // Word boundary check for non-CJK text - const candidateIsCjk = isCjkCandidate(candidate); - if (!candidateIsCjk) { - const left = i > 0 ? text[i - 1] : undefined; - const right = - i + candidate.length < text.length - ? text[i + candidate.length] - : undefined; + // Word boundary check for non-CJK text + const candidateIsCjk = isCjkCandidate(candidate) + if (!candidateIsCjk) { + const left = i > 0 ? text[i - 1] : undefined + const right + = i + candidate.length < text.length + ? text[i + candidate.length] + : undefined - if (!isWordBoundary(left) || !isWordBoundary(right)) { - result += text[i]; - i++; - continue outer; - } - } + if (!isWordBoundary(left) || !isWordBoundary(right)) { + result += text[i] + i++ + continue outer + } + } - // Check for Japanese particles - const isJapaneseCandidate = isJapaneseText(candidate); - if (isJapaneseCandidate) { - const right = - i + candidate.length < text.length - ? text.slice( - i + candidate.length, - i + candidate.length + 10, - ) - : ""; + // Check for Japanese particles + const isJapaneseCandidate = isJapaneseText(candidate) + if (isJapaneseCandidate) { + const right + = i + candidate.length < text.length + ? text.slice( + i + candidate.length, + i + candidate.length + 10, + ) + : "" - // Check for Japanese particles (no action needed, just a check point) - if (right.match(REGEX_PATTERNS.JAPANESE_PARTICLES)) { - // Skip word boundary check for Japanese particles - } - } + // Check for Japanese particles (no action needed, just a check point) + if (right.match(REGEX_PATTERNS.JAPANESE_PARTICLES)) { + // Skip word boundary check for Japanese particles + } + } - // Skip if namespace restriction applies - if ( - settings.namespaceResolution && - candidateData.scoped && - candidateData.namespace !== currentNamespace - ) { - result += candidate; - i += candidate.length; - continue outer; - } + // Skip if namespace restriction applies + if ( + settings.namespaceResolution + && candidateData.scoped + && candidateData.namespace !== currentNamespace + ) { + result += candidate + i += candidate.length + continue outer + } - // Create the link - const { linkPath, alias } = createLinkContent( - candidateData, - originalMatchedText, - settings, - ); - const isInTable = isIndexInsideMarkdownTable(text, i); - const finalLink = linkGenerator({ - linkPath, - sourcePath: filePath, - alias, - isInTable, - }); - result += finalLink; + // Create the link + const { linkPath, alias } = createLinkContent( + candidateData, + originalMatchedText, + settings, + ) + const isInTable = isIndexInsideMarkdownTable(text, i) + const finalLink = linkGenerator({ + linkPath, + sourcePath: filePath, + alias, + isInTable, + }) + result += finalLink - i += candidate.length; - continue outer; - } - } + i += candidate.length + continue outer + } + } - // Fallback: multi-word lookup using fallback index - if (settings.namespaceResolution) { - const fallbackResult = processFallbackSearch( - text, - i, - fallbackIndex, - filePath, - currentNamespace, - linkGenerator, - settings, - ); - if (fallbackResult) { - result += fallbackResult.result; - i = fallbackResult.newIndex; - continue outer; - } - } + // Fallback: multi-word lookup using fallback index + if (settings.namespaceResolution) { + const fallbackResult = processFallbackSearch( + text, + i, + fallbackIndex, + filePath, + currentNamespace, + linkGenerator, + settings, + ) + if (fallbackResult) { + result += fallbackResult.result + i = fallbackResult.newIndex + continue outer + } + } - // If no rule applies, output the current character - result += text[i]; - i++; - } + // If no rule applies, output the current character + result += text[i] + i++ + } - return result; -}; + return result +} // Main function export const replaceLinks = ({ - body, - linkResolverContext: { filePath, trie, candidateMap }, - settings = { - namespaceResolution: true, - baseDir: undefined, - ignoreDateFormats: true, - }, - linkGenerator = defaultLinkGenerator, + body, + linkResolverContext: { filePath, trie, candidateMap }, + settings = { + namespaceResolution: true, + baseDir: undefined, + ignoreDateFormats: true, + }, + linkGenerator = defaultLinkGenerator, }: ReplaceLinksOptions): string => { - // Normalize the body text to NFC - body = body.normalize("NFC"); + // Normalize the body text to NFC + body = body.normalize("NFC") - // If the body consists solely of a protected link, return it unchanged - if (isProtectedLink(body)) { - return body; - } + // If the body consists solely of a protected link, return it unchanged + if (isProtectedLink(body)) { + return body + } - // Build the fallback index - const fallbackIndex = buildFallbackIndex(candidateMap, settings.ignoreCase); + // Build the fallback index + const fallbackIndex = buildFallbackIndex(candidateMap, settings.ignoreCase) - // Get the current namespace - const currentNamespace = getCurrentNamespace(filePath, settings.baseDir); + // Get the current namespace + const currentNamespace = getCurrentNamespace(filePath, settings.baseDir) - // Process segments of text - const processTextSegment = (text: string): string => { - // Check if the text contains CJK characters - const hasCjkText = isCjkText(text); + // Process segments of text + const processTextSegment = (text: string): string => { + // Check if the text contains CJK characters + const hasCjkText = isCjkText(text) - if (hasCjkText) { - return processCjkText( - text, - trie, - candidateMap, - currentNamespace, - filePath, - linkGenerator, - settings, - ); - } else { - return processStandardText( - text, - trie, - candidateMap, - fallbackIndex, - filePath, - currentNamespace, - linkGenerator, - settings, - ); - } - }; + if (hasCjkText) { + return processCjkText( + text, + trie, + candidateMap, + currentNamespace, + filePath, + linkGenerator, + settings, + ) + } + else { + return processStandardText( + text, + trie, + candidateMap, + fallbackIndex, + filePath, + currentNamespace, + linkGenerator, + settings, + ) + } + } - // Extract and protect callout blocks first - // Match callout blocks: starts with > [!type] and continues with lines starting with > - const calloutPattern = /^>[ \t]*\[![\w-]+\].*?(\n>.*?)*(?=\n(?!>)|$)/gm; - const callouts: Array<{ placeholder: string; content: string }> = []; - let calloutIndex = 0; + // Extract and protect callout blocks first + // Match callout blocks: starts with > [!type] and continues with lines starting with > + const calloutPattern = /^>[ \t]*\[![\w-]+\].*?(\n>.*?)*(?=\n(?!>)|$)/gm + const callouts: Array<{ placeholder: string, content: string }> = [] + let calloutIndex = 0 - // Replace callouts with placeholders - const bodyWithPlaceholders = body.replace(calloutPattern, (match) => { - const placeholder = `__CALLOUT_${calloutIndex}__`; - callouts.push({ placeholder, content: match }); - calloutIndex++; - return placeholder; - }); + // Replace callouts with placeholders + const bodyWithPlaceholders = body.replace(calloutPattern, (match) => { + const placeholder = `__CALLOUT_${calloutIndex}__` + callouts.push({ placeholder, content: match }) + calloutIndex++ + return placeholder + }) - // Process the entire body while preserving protected segments - let resultBody = ""; - let lastIndex = 0; - let match: RegExpExecArray | null; + // Process the entire body while preserving protected segments + let resultBody = "" + let lastIndex = 0 + let match: RegExpExecArray | null - // Reset the regex to start from the beginning - REGEX_PATTERNS.PROTECTED.lastIndex = 0; + // Reset the regex to start from the beginning + REGEX_PATTERNS.PROTECTED.lastIndex = 0 - while ( - (match = REGEX_PATTERNS.PROTECTED.exec(bodyWithPlaceholders)) !== null - ) { - const mIndex = match.index; - const segment = bodyWithPlaceholders.slice(lastIndex, mIndex); - resultBody += processTextSegment(segment); - // Append the protected segment unchanged - resultBody += match[0]; - lastIndex = mIndex + match[0].length; + while ( + (match = REGEX_PATTERNS.PROTECTED.exec(bodyWithPlaceholders)) !== null + ) { + const mIndex = match.index + const segment = bodyWithPlaceholders.slice(lastIndex, mIndex) + resultBody += processTextSegment(segment) + // Append the protected segment unchanged + resultBody += match[0] + lastIndex = mIndex + match[0].length - // Prevent infinite loop on zero-length matches - if (match[0].length === 0) { - REGEX_PATTERNS.PROTECTED.lastIndex++; - } - } + // Prevent infinite loop on zero-length matches + if (match[0].length === 0) { + REGEX_PATTERNS.PROTECTED.lastIndex++ + } + } - // Process the remaining text - resultBody += processTextSegment(bodyWithPlaceholders.slice(lastIndex)); + // Process the remaining text + resultBody += processTextSegment(bodyWithPlaceholders.slice(lastIndex)) - // Restore callouts - for (const { placeholder, content } of callouts) { - resultBody = resultBody.replace(placeholder, content); - } + // Restore callouts + for (const { placeholder, content } of callouts) { + resultBody = resultBody.replace(placeholder, content) + } - return resultBody; -}; + return resultBody +} diff --git a/src/replace-url-with-title/__tests__/replace-url-with-title.test.ts b/src/replace-url-with-title/__tests__/replace-url-with-title.test.ts index 28356ff..7455cae 100644 --- a/src/replace-url-with-title/__tests__/replace-url-with-title.test.ts +++ b/src/replace-url-with-title/__tests__/replace-url-with-title.test.ts @@ -1,66 +1,66 @@ -import { describe, expect, it } from "vitest"; -import { replaceUrlWithTitle } from ".."; +import { describe, expect, it } from "vitest" +import { replaceUrlWithTitle } from ".." describe("replaceUrlWithTitle", () => { - it("should replace URLs with titles", () => { - const body = "Check this link: https://example.com"; - const result = replaceUrlWithTitle({ - body, - urlTitleMap: new Map( - Object.entries({ - "https://example.com": "Example Title", - }), - ), - }); - expect(result).toBe( - "Check this link: [Example Title](https://example.com)", - ); - }); + it("should replace URLs with titles", () => { + const body = "Check this link: https://example.com" + const result = replaceUrlWithTitle({ + body, + urlTitleMap: new Map( + Object.entries({ + "https://example.com": "Example Title", + }), + ), + }) + expect(result).toBe( + "Check this link: [Example Title](https://example.com)", + ) + }) - it("should handle multiple URLs", () => { - const body = "Links: https://example.com and https://another.com"; - const result = replaceUrlWithTitle({ - body, - urlTitleMap: new Map( - Object.entries({ - "https://example.com": "Example Title", - "https://another.com": "Another Title", - }), - ), - }); - expect(result).toBe( - "Links: [Example Title](https://example.com) and [Another Title](https://another.com)", - ); - }); + it("should handle multiple URLs", () => { + const body = "Links: https://example.com and https://another.com" + const result = replaceUrlWithTitle({ + body, + urlTitleMap: new Map( + Object.entries({ + "https://example.com": "Example Title", + "https://another.com": "Another Title", + }), + ), + }) + expect(result).toBe( + "Links: [Example Title](https://example.com) and [Another Title](https://another.com)", + ) + }) - it("should ignore markdown link []()", () => { - const body = "Check this link: [Example Title](https://example.com)"; - const result = replaceUrlWithTitle({ - body, - urlTitleMap: new Map( - Object.entries({ - "https://example.com": "Example Title", - }), - ), - }); - expect(result).toBe( - "Check this link: [Example Title](https://example.com)", - ); - }); + it("should ignore markdown link []()", () => { + const body = "Check this link: [Example Title](https://example.com)" + const result = replaceUrlWithTitle({ + body, + urlTitleMap: new Map( + Object.entries({ + "https://example.com": "Example Title", + }), + ), + }) + expect(result).toBe( + "Check this link: [Example Title](https://example.com)", + ) + }) - it("should handle multiple lines", () => { - const body = "Line 1: https://example.com\nLine 2: https://another.com"; - const result = replaceUrlWithTitle({ - body, - urlTitleMap: new Map( - Object.entries({ - "https://example.com": "Example Title", - "https://another.com": "Another Title", - }), - ), - }); - expect(result).toBe( - "Line 1: [Example Title](https://example.com)\nLine 2: [Another Title](https://another.com)", - ); - }); -}); + it("should handle multiple lines", () => { + const body = "Line 1: https://example.com\nLine 2: https://another.com" + const result = replaceUrlWithTitle({ + body, + urlTitleMap: new Map( + Object.entries({ + "https://example.com": "Example Title", + "https://another.com": "Another Title", + }), + ), + }) + expect(result).toBe( + "Line 1: [Example Title](https://example.com)\nLine 2: [Another Title](https://another.com)", + ) + }) +}) diff --git a/src/replace-url-with-title/index.ts b/src/replace-url-with-title/index.ts index 738e3fa..831f650 100644 --- a/src/replace-url-with-title/index.ts +++ b/src/replace-url-with-title/index.ts @@ -1,114 +1,115 @@ -type Url = string; -type Title = string; +type Url = string +type Title = string interface ReplaceUrlWithTitleOptions { - body: string; - urlTitleMap: Map; + body: string + urlTitleMap: Map } export const replaceUrlWithTitle = ({ - body, - urlTitleMap, + body, + urlTitleMap, }: ReplaceUrlWithTitleOptions): string => { - if (urlTitleMap.size === 0) { - return body; - } + if (urlTitleMap.size === 0) { + return body + } - let resultBody = body; + let resultBody = body - // Sort URLs by length descending to replace longer URLs first - // This helps prevent partial replacements (e.g., replacing 'example.com' before 'sub.example.com') - const sortedUrls = Array.from(urlTitleMap.keys()).sort( - (a, b) => b.length - a.length, - ); + // Sort URLs by length descending to replace longer URLs first + // This helps prevent partial replacements (e.g., replacing 'example.com' before 'sub.example.com') + const sortedUrls = Array.from(urlTitleMap.keys()).sort( + (a, b) => b.length - a.length, + ) - for (const url of sortedUrls) { - const title = urlTitleMap.get(url); - // Should not happen with Map iteration, but good practice - if (!title) continue; + for (const url of sortedUrls) { + const title = urlTitleMap.get(url) + // Should not happen with Map iteration, but good practice + if (!title) continue - // Escape backslashes and special characters in title for link text safety if needed - // For now, assume title is safe. - const markdownLink = `[${title}](${url})`; - let currentIndex = 0; - const newBodyParts: string[] = []; + // Escape backslashes and special characters in title for link text safety if needed + // For now, assume title is safe. + const markdownLink = `[${title}](${url})` + let currentIndex = 0 + const newBodyParts: string[] = [] - // Find all occurrences of the current URL in the resultBody - // resultBody is updated in each iteration of the outer loop - while (currentIndex < resultBody.length) { - // Find the next occurrence of the URL, case-sensitive. - const nextOccurrence = resultBody.indexOf(url, currentIndex); + // Find all occurrences of the current URL in the resultBody + // resultBody is updated in each iteration of the outer loop + while (currentIndex < resultBody.length) { + // Find the next occurrence of the URL, case-sensitive. + const nextOccurrence = resultBody.indexOf(url, currentIndex) - if (nextOccurrence === -1) { - // No more occurrences found, add the rest of the string - newBodyParts.push(resultBody.substring(currentIndex)); - break; - } + if (nextOccurrence === -1) { + // No more occurrences found, add the rest of the string + newBodyParts.push(resultBody.substring(currentIndex)) + break + } - // Add the text segment before the match - newBodyParts.push( - resultBody.substring(currentIndex, nextOccurrence), - ); + // Add the text segment before the match + newBodyParts.push( + resultBody.substring(currentIndex, nextOccurrence), + ) - // --- Context Check --- - let shouldReplace = true; + // --- Context Check --- + let shouldReplace = true - // 1. Check if already part of a Markdown link: [...](url) - // Look for `](` immediately before the URL and `)` immediately after. - const precedingChars = resultBody.substring( - nextOccurrence - 2, - nextOccurrence, - ); - const followingChar = resultBody[nextOccurrence + url.length]; - if (precedingChars === "](" && followingChar === ")") { - shouldReplace = false; - } + // 1. Check if already part of a Markdown link: [...](url) + // Look for `](` immediately before the URL and `)` immediately after. + const precedingChars = resultBody.substring( + nextOccurrence - 2, + nextOccurrence, + ) + const followingChar = resultBody[nextOccurrence + url.length] + if (precedingChars === "](" && followingChar === ")") { + shouldReplace = false + } - // 2. Check if inside inline code: `... url ...` - // Count non-escaped backticks before the match. Odd count means inside code. - if (shouldReplace) { - const segmentBefore = resultBody.substring(0, nextOccurrence); - // Count non-escaped backticks `(? { - it("should extract title from HTML", () => { - const html = "Example Title"; - const result = getTitleFromHtml(html); - expect(result).toBe("Example Title"); - }); + it("should extract title from HTML", () => { + const html = "Example Title" + const result = getTitleFromHtml(html) + expect(result).toBe("Example Title") + }) - it("should handle empty title tag", () => { - const html = ""; - const result = getTitleFromHtml(html); - expect(result).toBe(""); - }); + it("should handle empty title tag", () => { + const html = "" + const result = getTitleFromHtml(html) + expect(result).toBe("") + }) - it("should handle no title tag", () => { - const html = ""; - const result = getTitleFromHtml(html); - expect(result).toBe(""); - }); -}); + it("should handle no title tag", () => { + const html = "" + const result = getTitleFromHtml(html) + expect(result).toBe("") + }) +}) diff --git a/src/replace-url-with-title/utils/__tests__/list-up-all-urls.test.ts b/src/replace-url-with-title/utils/__tests__/list-up-all-urls.test.ts index 4fac0d1..327fa93 100644 --- a/src/replace-url-with-title/utils/__tests__/list-up-all-urls.test.ts +++ b/src/replace-url-with-title/utils/__tests__/list-up-all-urls.test.ts @@ -1,40 +1,40 @@ -import { describe, expect, it } from "vitest"; -import { listupAllUrls } from "../list-up-all-urls"; +import { describe, expect, it } from "vitest" +import { listupAllUrls } from "../list-up-all-urls" describe("listupAllUrls", () => { - it("should find a URL in the text", () => { - const body = "Check this link: https://example.com"; - const result = listupAllUrls(body); - expect(result).toContain("https://example.com"); - }); + it("should find a URL in the text", () => { + const body = "Check this link: https://example.com" + const result = listupAllUrls(body) + expect(result).toContain("https://example.com") + }) - it("should ignore URLs inside markdown links", () => { - const body = "Check this link: [Example](https://example.com)"; - const result = listupAllUrls(body); - expect(result).not.toContain("https://example.com"); - }); + it("should ignore URLs inside markdown links", () => { + const body = "Check this link: [Example](https://example.com)" + const result = listupAllUrls(body) + expect(result).not.toContain("https://example.com") + }) - it("should ignore URLs inside angle brackets", () => { - const body = "Check this link: "; - const result = listupAllUrls(body); - expect(result).not.toContain("https://example.com"); - }); + it("should ignore URLs inside angle brackets", () => { + const body = "Check this link: " + const result = listupAllUrls(body) + expect(result).not.toContain("https://example.com") + }) - it("should ignore URLs inside inline code", () => { - const body = "Check this link: `https://example.com`"; - const result = listupAllUrls(body); - expect(result).not.toContain("https://example.com"); - }); + it("should ignore URLs inside inline code", () => { + const body = "Check this link: `https://example.com`" + const result = listupAllUrls(body) + expect(result).not.toContain("https://example.com") + }) - it("should ignore URLs inside fenced code blocks", () => { - const body = "```\nhttps://example.com\n```"; - const result = listupAllUrls(body); - expect(result).not.toContain("https://example.com"); - }); + it("should ignore URLs inside fenced code blocks", () => { + const body = "```\nhttps://example.com\n```" + const result = listupAllUrls(body) + expect(result).not.toContain("https://example.com") + }) - it("ignore domains", () => { - const body = "Check this link: https://example.com"; - const result = listupAllUrls(body, ["example.com"]); - expect(result).not.toContain("https://example.com"); - }); -}); + it("ignore domains", () => { + const body = "Check this link: https://example.com" + const result = listupAllUrls(body, ["example.com"]) + expect(result).not.toContain("https://example.com") + }) +}) diff --git a/src/replace-url-with-title/utils/get-title-from-html.ts b/src/replace-url-with-title/utils/get-title-from-html.ts index 6fd131f..8c882ac 100644 --- a/src/replace-url-with-title/utils/get-title-from-html.ts +++ b/src/replace-url-with-title/utils/get-title-from-html.ts @@ -2,17 +2,17 @@ // It handles potential attributes within the title tag (though unlikely) // and captures the content between and // Case-insensitive matching for tag -const TITLE_REGEX = /<title[^>]*>([^<]+)<\/title>/i; +const TITLE_REGEX = /<title[^>]*>([^<]+)<\/title>/i export const getTitleFromHtml = (html: string): string => { - const match = html.match(TITLE_REGEX); + const match = html.match(TITLE_REGEX) - if (match && match[1]) { - // match[1] contains the captured group (the content of the title tag) - // Trim whitespace from the extracted title - return match[1].trim(); - } + if (match && match[1]) { + // match[1] contains the captured group (the content of the title tag) + // Trim whitespace from the extracted title + return match[1].trim() + } - // Return empty string if no title tag is found or it's empty - return ""; -}; + // Return empty string if no title tag is found or it's empty + return "" +} diff --git a/src/replace-url-with-title/utils/list-up-all-urls.ts b/src/replace-url-with-title/utils/list-up-all-urls.ts index 5442948..acaf9c7 100644 --- a/src/replace-url-with-title/utils/list-up-all-urls.ts +++ b/src/replace-url-with-title/utils/list-up-all-urls.ts @@ -1,174 +1,175 @@ -type Url = string; +type Url = string // Regular expression to find URLs starting with http:// or https:// // It avoids matching URLs immediately preceded by `](` or `<` or followed by `)` or `>` // It also tries to avoid including common trailing punctuation as part of the URL. // Still simplified and might need refinement for complex edge cases. // Added ')' to the negated set to prevent matching the closing parenthesis of a Markdown link. -const URL_REGEX = /https?:\/\/[^\s<>"'`)]+/g; +const URL_REGEX = /https?:\/\/[^\s<>"'`)]+/g // Regex to identify common trailing punctuation that shouldn't be part of the URL -const TRAILING_PUNCTUATION_REGEX = /[.,;!?\]}]+$/; // Removed ')' as it's now handled by URL_REGEX exclusion +const TRAILING_PUNCTUATION_REGEX = /[.,;!?\]}]+$/ // Removed ')' as it's now handled by URL_REGEX exclusion export const listupAllUrls = ( - body: string, - ignoredDomains?: string[], + body: string, + ignoredDomains?: string[], ): Set<Url> => { - const urls = new Set<Url>(); - let match; + const urls = new Set<Url>() + let match - // --- Pre-calculate Fenced Code Block Ranges --- - const codeBlockRanges: { start: number; end: number }[] = []; - // Regex to find fenced code blocks (handles different fence lengths and optional language specifiers) - // Matches from ``` or ~~~ at the start of a line to the next ``` or ~~~ at the start of a line - const codeBlockRegex = - /^(?:```|~~~)[^\r\n]*?\r?\n([\s\S]*?)\r?\n^(?:```|~~~)$/gm; - let blockMatch; - while ((blockMatch = codeBlockRegex.exec(body)) !== null) { - codeBlockRanges.push({ - start: blockMatch.index, - end: blockMatch.index + blockMatch[0].length, - }); - } - // Reset regex state if needed, though new exec calls should handle this - codeBlockRegex.lastIndex = 0; + // --- Pre-calculate Fenced Code Block Ranges --- + const codeBlockRanges: { start: number, end: number }[] = [] + // Regex to find fenced code blocks (handles different fence lengths and optional language specifiers) + // Matches from ``` or ~~~ at the start of a line to the next ``` or ~~~ at the start of a line + const codeBlockRegex + = /^(?:```|~~~)[^\r\n]*?\r?\n([\s\S]*?)\r?\n^(?:```|~~~)$/gm + let blockMatch + while ((blockMatch = codeBlockRegex.exec(body)) !== null) { + codeBlockRanges.push({ + start: blockMatch.index, + end: blockMatch.index + blockMatch[0].length, + }) + } + // Reset regex state if needed, though new exec calls should handle this + codeBlockRegex.lastIndex = 0 - while ((match = URL_REGEX.exec(body)) !== null) { - const url = match[0]; - const matchIndex = match.index; + while ((match = URL_REGEX.exec(body)) !== null) { + const url = match[0] + const matchIndex = match.index - // --- Fenced Code Block Check --- - // Check if the match index falls within any calculated code block range - let isInCodeBlock = false; - for (const range of codeBlockRanges) { - if (matchIndex >= range.start && matchIndex < range.end) { - isInCodeBlock = true; - break; - } - } - if (isInCodeBlock) { - continue; // Skip this URL if it's inside a fenced code block - } + // --- Fenced Code Block Check --- + // Check if the match index falls within any calculated code block range + let isInCodeBlock = false + for (const range of codeBlockRanges) { + if (matchIndex >= range.start && matchIndex < range.end) { + isInCodeBlock = true + break + } + } + if (isInCodeBlock) { + continue // Skip this URL if it's inside a fenced code block + } - // --- Context Check --- - let isBareUrl = true; + // --- Context Check --- + let isBareUrl = true - // 1. Check if already part of a Markdown link: [...](url) - if (matchIndex >= 2) { // Need space for "](" - // Check if the URL is potentially followed by ')' - const followingCharIndex = matchIndex + url.length; - if (followingCharIndex < body.length && body[followingCharIndex] === ')') { - // If followed by ')', check if preceded by "](" - const precedingChars = body.substring(matchIndex - 2, matchIndex); - if (precedingChars === "](") { - // Only if both conditions are met, it's a Markdown link - isBareUrl = false; - } - } - } + // 1. Check if already part of a Markdown link: [...](url) + if (matchIndex >= 2) { // Need space for "](" + // Check if the URL is potentially followed by ')' + const followingCharIndex = matchIndex + url.length + if (followingCharIndex < body.length && body[followingCharIndex] === ")") { + // If followed by ')', check if preceded by "](" + const precedingChars = body.substring(matchIndex - 2, matchIndex) + if (precedingChars === "](") { + // Only if both conditions are met, it's a Markdown link + isBareUrl = false + } + } + } - // 2. Check if enclosed in angle brackets: <url> - if (isBareUrl && matchIndex >= 1) { - const precedingChar = body[matchIndex - 1]; - const followingChar = body[matchIndex + url.length]; - if (precedingChar === "<" && followingChar === ">") { - isBareUrl = false; - } - } + // 2. Check if enclosed in angle brackets: <url> + if (isBareUrl && matchIndex >= 1) { + const precedingChar = body[matchIndex - 1] + const followingChar = body[matchIndex + url.length] + if (precedingChar === "<" && followingChar === ">") { + isBareUrl = false + } + } - // 3. Check if inside inline code: `... url ...` - // Count non-escaped backticks before the match. Odd count means inside code. - if (isBareUrl) { - const segmentBefore = body.substring(0, matchIndex); - // Count non-escaped backticks `(?<!\\)` ensures we don't count escaped ones like \` - const backticksCount = (segmentBefore.match(/(?<!\\)`/g) || []) - .length; + // 3. Check if inside inline code: `... url ...` + // Count non-escaped backticks before the match. Odd count means inside code. + if (isBareUrl) { + const segmentBefore = body.substring(0, matchIndex) + // Count non-escaped backticks `(?<!\\)` ensures we don't count escaped ones like \` + const backticksCount = (segmentBefore.match(/(?<!\\)`/g) || []) + .length - if (backticksCount % 2 !== 0) { - // Odd number of backticks means we might be inside a code span. - // We need to ensure the code span doesn't close *before* our match. - const lastBacktickIndex = segmentBefore.lastIndexOf("`"); - // Check if there's another backtick between the last one and the match. - // If not, we are inside the code span. - if ( - lastBacktickIndex !== -1 && - !segmentBefore - .substring(lastBacktickIndex + 1) - .includes("`") - ) { - isBareUrl = false; - } - } - } + if (backticksCount % 2 !== 0) { + // Odd number of backticks means we might be inside a code span. + // We need to ensure the code span doesn't close *before* our match. + const lastBacktickIndex = segmentBefore.lastIndexOf("`") + // Check if there's another backtick between the last one and the match. + // If not, we are inside the code span. + if ( + lastBacktickIndex !== -1 + && !segmentBefore + .substring(lastBacktickIndex + 1) + .includes("`") + ) { + isBareUrl = false + } + } + } - // 4. Check if inside fenced code block: ``` ... url ... ``` - // This check remains complex. A simple heuristic: check if the line - // containing the URL is within a ``` block. This is not foolproof. - // For now, we'll skip this check for simplicity, but acknowledge it's a limitation. - // A more robust solution would involve parsing the Markdown structure. + // 4. Check if inside fenced code block: ``` ... url ... ``` + // This check remains complex. A simple heuristic: check if the line + // containing the URL is within a ``` block. This is not foolproof. + // For now, we'll skip this check for simplicity, but acknowledge it's a limitation. + // A more robust solution would involve parsing the Markdown structure. - // --- Check Ignored Domains & Clean URL --- - if (isBareUrl) { - let finalUrl = url; - let shouldAdd = true; + // --- Check Ignored Domains & Clean URL --- + if (isBareUrl) { + let finalUrl = url + let shouldAdd = true - // 5. Check Ignored Domains - if (ignoredDomains && ignoredDomains.length > 0) { - try { - const parsedUrl = new URL(url); - const hostname = parsedUrl.hostname; - if ( - ignoredDomains.some( - (domain) => - hostname === domain || - hostname.endsWith(`.${domain}`), - ) - ) { - shouldAdd = false; - } - } catch (e) { - // If URL parsing fails, it's likely not a valid URL to add anyway - console.warn( - `Failed to parse URL for domain check: ${url}`, - e, - ); - shouldAdd = false; - } - } + // 5. Check Ignored Domains + if (ignoredDomains && ignoredDomains.length > 0) { + try { + const parsedUrl = new URL(url) + const hostname = parsedUrl.hostname + if ( + ignoredDomains.some( + domain => + hostname === domain + || hostname.endsWith(`.${domain}`), + ) + ) { + shouldAdd = false + } + } + catch (e) { + // If URL parsing fails, it's likely not a valid URL to add anyway + console.warn( + `Failed to parse URL for domain check: ${url}`, + e, + ) + shouldAdd = false + } + } - // 6. Clean Trailing Punctuation (only if not ignored) - // We only clean punctuation if the URL wasn't already excluded by context checks. - if (shouldAdd) { - const cleanedUrl = url.replace(TRAILING_PUNCTUATION_REGEX, ""); - // Ensure cleaning didn't make it invalid (e.g., just "http://") - // or remove essential parts if the regex was too broad. - if (cleanedUrl.includes("://")) { - finalUrl = cleanedUrl; // Use the cleaned URL only if it's still valid-looking - } else { - // If cleaning resulted in an invalid URL, maybe don't add it, - // or reconsider the TRAILING_PUNCTUATION_REGEX. - // For now, let's stick with the original URL if cleaning fails badly. - // This case shouldn't happen often with the current regex. - finalUrl = url; // Revert to original if cleaning broke it - console.warn(`URL cleaning potentially broke the URL: ${url} -> ${cleanedUrl}`); - // Decide if we should still add the original potentially dirty url - // shouldAdd = false; // Option: Don't add if cleaning failed - } - } + // 6. Clean Trailing Punctuation (only if not ignored) + // We only clean punctuation if the URL wasn't already excluded by context checks. + if (shouldAdd) { + const cleanedUrl = url.replace(TRAILING_PUNCTUATION_REGEX, "") + // Ensure cleaning didn't make it invalid (e.g., just "http://") + // or remove essential parts if the regex was too broad. + if (cleanedUrl.includes("://")) { + finalUrl = cleanedUrl // Use the cleaned URL only if it's still valid-looking + } + else { + // If cleaning resulted in an invalid URL, maybe don't add it, + // or reconsider the TRAILING_PUNCTUATION_REGEX. + // For now, let's stick with the original URL if cleaning fails badly. + // This case shouldn't happen often with the current regex. + finalUrl = url // Revert to original if cleaning broke it + console.warn(`URL cleaning potentially broke the URL: ${url} -> ${cleanedUrl}`) + // Decide if we should still add the original potentially dirty url + // shouldAdd = false; // Option: Don't add if cleaning failed + } + } + // --- Add URL if context checks pass and not ignored --- + if (shouldAdd) { + urls.add(finalUrl) + } + } - // --- Add URL if context checks pass and not ignored --- - if (shouldAdd) { - urls.add(finalUrl); - } - } + // Reset regex lastIndex to avoid issues with overlapping matches or zero-length matches + // Although our URL regex shouldn't produce zero-length matches. + // If the regex finds a match at index `i`, the next search starts at `i + 1`. + // If the match was length `l`, `exec` updates `lastIndex` to `i + l`. + // We need to ensure progress even if `l` is 0, but `URL_REGEX` won't match empty. + // If `isBareUrl` logic modified the string or indices, care would be needed. + } - // Reset regex lastIndex to avoid issues with overlapping matches or zero-length matches - // Although our URL regex shouldn't produce zero-length matches. - // If the regex finds a match at index `i`, the next search starts at `i + 1`. - // If the match was length `l`, `exec` updates `lastIndex` to `i + l`. - // We need to ensure progress even if `l` is 0, but `URL_REGEX` won't match empty. - // If `isBareUrl` logic modified the string or indices, care would be needed. - } - - return urls; -}; + return urls +} diff --git a/src/replace-urls/__tests__/replace-urls.github.test.ts b/src/replace-urls/__tests__/replace-urls.github.test.ts index 097bb89..70f8a6b 100644 --- a/src/replace-urls/__tests__/replace-urls.github.test.ts +++ b/src/replace-urls/__tests__/replace-urls.github.test.ts @@ -1,104 +1,104 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest" import { - AutomaticLinkerSettings, - DEFAULT_SETTINGS, -} from "../../settings/settings-info"; -import { formatGitHubURL } from "../github"; + AutomaticLinkerSettings, + DEFAULT_SETTINGS, +} from "../../settings/settings-info" +import { formatGitHubURL } from "../github" describe("formatGitHubURL", () => { - const baseSettings: AutomaticLinkerSettings = { - ...DEFAULT_SETTINGS, - githubEnterpriseURLs: ["github.enterprise.com", "github.company.com"], - }; + const baseSettings: AutomaticLinkerSettings = { + ...DEFAULT_SETTINGS, + githubEnterpriseURLs: ["github.enterprise.com", "github.company.com"], + } - describe("Basic repository URL formatting", () => { - it("should format basic repository URL", () => { - const input = "https://github.com/kdnk/obsidian-automatic-linker"; - const expected = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + describe("Basic repository URL formatting", () => { + it("should format basic repository URL", () => { + const input = "https://github.com/kdnk/obsidian-automatic-linker" + const expected + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should format repository URL with trailing slash", () => { - const input = "https://github.com/kdnk/obsidian-automatic-linker/"; - const expected = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + it("should format repository URL with trailing slash", () => { + const input = "https://github.com/kdnk/obsidian-automatic-linker/" + const expected + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should not modify non-GitHub URLs", () => { - const input = "https://example.com/some-path/"; - expect(formatGitHubURL(input, baseSettings)).toBe(input); - }); + it("should not modify non-GitHub URLs", () => { + const input = "https://example.com/some-path/" + expect(formatGitHubURL(input, baseSettings)).toBe(input) + }) - it("should handle invalid URLs", () => { - const input = "not-a-url"; - expect(formatGitHubURL(input, baseSettings)).toBe(input); - }); - }); + it("should handle invalid URLs", () => { + const input = "not-a-url" + expect(formatGitHubURL(input, baseSettings)).toBe(input) + }) + }) - describe("Pull Request and Issue URL formatting", () => { - it("should format pull request URLs", () => { - const input = - "https://github.com/kdnk/obsidian-automatic-linker/pull/123?diff=split"; - const expected = - "[[github/kdnk/obsidian-automatic-linker/pull/123]] [🔗](https://github.com/kdnk/obsidian-automatic-linker/pull/123)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + describe("Pull Request and Issue URL formatting", () => { + it("should format pull request URLs", () => { + const input + = "https://github.com/kdnk/obsidian-automatic-linker/pull/123?diff=split" + const expected + = "[[github/kdnk/obsidian-automatic-linker/pull/123]] [🔗](https://github.com/kdnk/obsidian-automatic-linker/pull/123)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should format issue URLs", () => { - const input = - "https://github.com/kdnk/obsidian-automatic-linker/issues/456#issuecomment-1234567"; - const expected = - "[[github/kdnk/obsidian-automatic-linker/issues/456]] [🔗](https://github.com/kdnk/obsidian-automatic-linker/issues/456)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); - }); + it("should format issue URLs", () => { + const input + = "https://github.com/kdnk/obsidian-automatic-linker/issues/456#issuecomment-1234567" + const expected + = "[[github/kdnk/obsidian-automatic-linker/issues/456]] [🔗](https://github.com/kdnk/obsidian-automatic-linker/issues/456)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) + }) - describe("GitHub Enterprise URL formatting", () => { - it("should format enterprise repository URLs", () => { - const input = "https://github.enterprise.com/kdnk/project/"; - const expected = - "[[ghe/kdnk/project]] [🔗](https://github.enterprise.com/kdnk/project)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + describe("GitHub Enterprise URL formatting", () => { + it("should format enterprise repository URLs", () => { + const input = "https://github.enterprise.com/kdnk/project/" + const expected + = "[[ghe/kdnk/project]] [🔗](https://github.enterprise.com/kdnk/project)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should format enterprise pull request URLs", () => { - const input = - "https://github.enterprise.com/kdnk/project/pull/789?diff=split"; - const expected = - "[[ghe/kdnk/project/pull/789]] [🔗](https://github.enterprise.com/kdnk/project/pull/789)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + it("should format enterprise pull request URLs", () => { + const input + = "https://github.enterprise.com/kdnk/project/pull/789?diff=split" + const expected + = "[[ghe/kdnk/project/pull/789]] [🔗](https://github.enterprise.com/kdnk/project/pull/789)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should handle custom enterprise URLs", () => { - const input = "https://github.company.com/team/project/issues/123"; - const expected = - "[[ghe/team/project/issues/123]] [🔗](https://github.company.com/team/project/issues/123)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + it("should handle custom enterprise URLs", () => { + const input = "https://github.company.com/team/project/issues/123" + const expected + = "[[ghe/team/project/issues/123]] [🔗](https://github.company.com/team/project/issues/123)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should not format URLs from non-configured enterprise domains", () => { - const input = "https://github.other-company.com/team/project/"; - expect(formatGitHubURL(input, baseSettings)).toBe(input); - }); - }); + it("should not format URLs from non-configured enterprise domains", () => { + const input = "https://github.other-company.com/team/project/" + expect(formatGitHubURL(input, baseSettings)).toBe(input) + }) + }) - describe("URL with query parameters", () => { - it("should remove query parameters from repository URLs", () => { - const input = - "https://github.com/kdnk/obsidian-automatic-linker?tab=repositories"; - const expected = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); + describe("URL with query parameters", () => { + it("should remove query parameters from repository URLs", () => { + const input + = "https://github.com/kdnk/obsidian-automatic-linker?tab=repositories" + const expected + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) - it("should remove hash from URLs", () => { - const input = - "https://github.com/kdnk/obsidian-automatic-linker#readme"; - const expected = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - expect(formatGitHubURL(input, baseSettings)).toBe(expected); - }); - }); -}); + it("should remove hash from URLs", () => { + const input + = "https://github.com/kdnk/obsidian-automatic-linker#readme" + const expected + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + expect(formatGitHubURL(input, baseSettings)).toBe(expected) + }) + }) +}) diff --git a/src/replace-urls/__tests__/replace-urls.jira.test.ts b/src/replace-urls/__tests__/replace-urls.jira.test.ts index f6099c1..259c633 100644 --- a/src/replace-urls/__tests__/replace-urls.jira.test.ts +++ b/src/replace-urls/__tests__/replace-urls.jira.test.ts @@ -1,90 +1,90 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest" import { - AutomaticLinkerSettings, - DEFAULT_SETTINGS, -} from "../../settings/settings-info"; -import { formatJiraURL } from "../jira"; + AutomaticLinkerSettings, + DEFAULT_SETTINGS, +} from "../../settings/settings-info" +import { formatJiraURL } from "../jira" describe("formatJiraURL", () => { - const baseSettings: AutomaticLinkerSettings = { - ...DEFAULT_SETTINGS, - jiraURLs: ["sub-domain.work.com", "jira.company.com"], - }; + const baseSettings: AutomaticLinkerSettings = { + ...DEFAULT_SETTINGS, + jiraURLs: ["sub-domain.work.com", "jira.company.com"], + } - describe("Basic Jira URL formatting", () => { - it("should format basic Jira issue URL", () => { - const input = "https://sub-domain.work.com/browse/XYZ-123"; - const expected = - "[[work/jira/XYZ/123]] [🔗](https://sub-domain.work.com/browse/XYZ-123)"; - expect(formatJiraURL(input, baseSettings)).toBe(expected); - }); + describe("Basic Jira URL formatting", () => { + it("should format basic Jira issue URL", () => { + const input = "https://sub-domain.work.com/browse/XYZ-123" + const expected + = "[[work/jira/XYZ/123]] [🔗](https://sub-domain.work.com/browse/XYZ-123)" + expect(formatJiraURL(input, baseSettings)).toBe(expected) + }) - it("should not modify non-Jira URLs", () => { - const input = "https://example.com/browse/XYZ-123"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + it("should not modify non-Jira URLs", () => { + const input = "https://example.com/browse/XYZ-123" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should handle invalid URLs", () => { - const input = "not-a-url"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); - }); + it("should handle invalid URLs", () => { + const input = "not-a-url" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) + }) - describe("URL pattern validation", () => { - it("should not format URLs without browse path", () => { - const input = "https://sub-domain.work.com/XYZ-123"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + describe("URL pattern validation", () => { + it("should not format URLs without browse path", () => { + const input = "https://sub-domain.work.com/XYZ-123" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should not format URLs with invalid issue format", () => { - const input = "https://sub-domain.work.com/browse/XYZ123"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); - }); + it("should not format URLs with invalid issue format", () => { + const input = "https://sub-domain.work.com/browse/XYZ123" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) + }) - describe("Multiple Jira domains", () => { - it("should format URLs from different configured domains", () => { - const input = "https://jira.company.com/browse/ABC-456"; - const expected = - "[[company/jira/ABC/456]] [🔗](https://jira.company.com/browse/ABC-456)"; - expect(formatJiraURL(input, baseSettings)).toBe(expected); - }); + describe("Multiple Jira domains", () => { + it("should format URLs from different configured domains", () => { + const input = "https://jira.company.com/browse/ABC-456" + const expected + = "[[company/jira/ABC/456]] [🔗](https://jira.company.com/browse/ABC-456)" + expect(formatJiraURL(input, baseSettings)).toBe(expected) + }) - it("should not format URLs from non-configured domains", () => { - const input = "https://jira.other-company.com/browse/DEF-789"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); - }); + it("should not format URLs from non-configured domains", () => { + const input = "https://jira.other-company.com/browse/DEF-789" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) + }) - describe("URL with query parameters", () => { - it("should not format URLs with query parameters (focusedCommentId)", () => { - const input = - "https://sub-domain.work.com/browse/XYZ-123?focusedCommentId=12345"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + describe("URL with query parameters", () => { + it("should not format URLs with query parameters (focusedCommentId)", () => { + const input + = "https://sub-domain.work.com/browse/XYZ-123?focusedCommentId=12345" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should not format URLs with query parameters (selectedIssue)", () => { - const input = - "https://sub-domain.work.com/browse/XYZ-123?selectedIssue=XYZ-123"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + it("should not format URLs with query parameters (selectedIssue)", () => { + const input + = "https://sub-domain.work.com/browse/XYZ-123?selectedIssue=XYZ-123" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should not format URLs with multiple query parameters", () => { - const input = - "https://sub-domain.work.com/browse/XYZ-123?focusedCommentId=12345&selectedIssue=XYZ-123"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + it("should not format URLs with multiple query parameters", () => { + const input + = "https://sub-domain.work.com/browse/XYZ-123?focusedCommentId=12345&selectedIssue=XYZ-123" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should not format URLs with empty query parameters", () => { - const input = "https://sub-domain.work.com/browse/XYZ-123?"; - expect(formatJiraURL(input, baseSettings)).toBe(input); - }); + it("should not format URLs with empty query parameters", () => { + const input = "https://sub-domain.work.com/browse/XYZ-123?" + expect(formatJiraURL(input, baseSettings)).toBe(input) + }) - it("should format URLs without query parameters", () => { - const input = "https://sub-domain.work.com/browse/XYZ-123"; - const expected = - "[[work/jira/XYZ/123]] [🔗](https://sub-domain.work.com/browse/XYZ-123)"; - expect(formatJiraURL(input, baseSettings)).toBe(expected); - }); - }); -}); + it("should format URLs without query parameters", () => { + const input = "https://sub-domain.work.com/browse/XYZ-123" + const expected + = "[[work/jira/XYZ/123]] [🔗](https://sub-domain.work.com/browse/XYZ-123)" + expect(formatJiraURL(input, baseSettings)).toBe(expected) + }) + }) +}) diff --git a/src/replace-urls/__tests__/replace-urls.linear.test.ts b/src/replace-urls/__tests__/replace-urls.linear.test.ts index e43643f..2f76101 100644 --- a/src/replace-urls/__tests__/replace-urls.linear.test.ts +++ b/src/replace-urls/__tests__/replace-urls.linear.test.ts @@ -1,116 +1,116 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest" import { - AutomaticLinkerSettings, - DEFAULT_SETTINGS, -} from "../../settings/settings-info"; -import { formatLinearURL } from "../linear"; + AutomaticLinkerSettings, + DEFAULT_SETTINGS, +} from "../../settings/settings-info" +import { formatLinearURL } from "../linear" describe("formatLinearURL", () => { - const baseSettings: AutomaticLinkerSettings = { - ...DEFAULT_SETTINGS, - formatLinearURLs: true, - }; + const baseSettings: AutomaticLinkerSettings = { + ...DEFAULT_SETTINGS, + formatLinearURLs: true, + } - describe("Basic Linear URL formatting", () => { - it("should format Linear issue URL with title", () => { - const input = - "https://linear.app/andrewmcodes/issue/ACME-123/title-of-issue"; - const expected = - "[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + describe("Basic Linear URL formatting", () => { + it("should format Linear issue URL with title", () => { + const input + = "https://linear.app/andrewmcodes/issue/ACME-123/title-of-issue" + const expected + = "[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should format Linear issue URL without title", () => { - const input = "https://linear.app/workspace/issue/PROJ-456"; - const expected = - "[[linear/workspace/PROJ-456]] [🔗](https://linear.app/workspace/issue/PROJ-456)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + it("should format Linear issue URL without title", () => { + const input = "https://linear.app/workspace/issue/PROJ-456" + const expected + = "[[linear/workspace/PROJ-456]] [🔗](https://linear.app/workspace/issue/PROJ-456)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should format linear:// protocol URL", () => { - const input = "linear://andrewmcodes/issue/ACME-123"; - const expected = - "[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + it("should format linear:// protocol URL", () => { + const input = "linear://andrewmcodes/issue/ACME-123" + const expected + = "[[linear/andrewmcodes/ACME-123]] [🔗](https://linear.app/andrewmcodes/issue/ACME-123)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should not modify non-Linear URLs", () => { - const input = "https://example.com/issue/ACME-123"; - expect(formatLinearURL(input, baseSettings)).toBe(input); - }); + it("should not modify non-Linear URLs", () => { + const input = "https://example.com/issue/ACME-123" + expect(formatLinearURL(input, baseSettings)).toBe(input) + }) - it("should handle invalid URLs", () => { - const input = "not-a-url"; - expect(formatLinearURL(input, baseSettings)).toBe(input); - }); - }); + it("should handle invalid URLs", () => { + const input = "not-a-url" + expect(formatLinearURL(input, baseSettings)).toBe(input) + }) + }) - describe("URL pattern validation", () => { - it("should not format URLs without issue path", () => { - const input = "https://linear.app/workspace"; - expect(formatLinearURL(input, baseSettings)).toBe(input); - }); + describe("URL pattern validation", () => { + it("should not format URLs without issue path", () => { + const input = "https://linear.app/workspace" + expect(formatLinearURL(input, baseSettings)).toBe(input) + }) - it("should not format URLs with invalid issue format", () => { - const input = "https://linear.app/workspace/issue/INVALID"; - expect(formatLinearURL(input, baseSettings)).toBe(input); - }); + it("should not format URLs with invalid issue format", () => { + const input = "https://linear.app/workspace/issue/INVALID" + expect(formatLinearURL(input, baseSettings)).toBe(input) + }) - it("should format URLs with different workspace names", () => { - const input = "https://linear.app/my-company/issue/BUG-789"; - const expected = - "[[linear/my-company/BUG-789]] [🔗](https://linear.app/my-company/issue/BUG-789)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); - }); + it("should format URLs with different workspace names", () => { + const input = "https://linear.app/my-company/issue/BUG-789" + const expected + = "[[linear/my-company/BUG-789]] [🔗](https://linear.app/my-company/issue/BUG-789)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) + }) - describe("Issue ID format validation", () => { - it("should handle issue IDs with numbers", () => { - const input = "https://linear.app/workspace/issue/ABC-123"; - const expected = - "[[linear/workspace/ABC-123]] [🔗](https://linear.app/workspace/issue/ABC-123)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + describe("Issue ID format validation", () => { + it("should handle issue IDs with numbers", () => { + const input = "https://linear.app/workspace/issue/ABC-123" + const expected + = "[[linear/workspace/ABC-123]] [🔗](https://linear.app/workspace/issue/ABC-123)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should handle issue IDs with multiple digits", () => { - const input = "https://linear.app/workspace/issue/PROJ-99999"; - const expected = - "[[linear/workspace/PROJ-99999]] [🔗](https://linear.app/workspace/issue/PROJ-99999)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + it("should handle issue IDs with multiple digits", () => { + const input = "https://linear.app/workspace/issue/PROJ-99999" + const expected + = "[[linear/workspace/PROJ-99999]] [🔗](https://linear.app/workspace/issue/PROJ-99999)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should not format issue IDs without hyphen", () => { - const input = "https://linear.app/workspace/issue/ABC123"; - expect(formatLinearURL(input, baseSettings)).toBe(input); - }); - }); + it("should not format issue IDs without hyphen", () => { + const input = "https://linear.app/workspace/issue/ABC123" + expect(formatLinearURL(input, baseSettings)).toBe(input) + }) + }) - describe("URL with query parameters", () => { - it("should format URLs with query parameters", () => { - const input = - "https://linear.app/workspace/issue/ACME-123?something=value"; - const expected = - "[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); + describe("URL with query parameters", () => { + it("should format URLs with query parameters", () => { + const input + = "https://linear.app/workspace/issue/ACME-123?something=value" + const expected + = "[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) - it("should format URLs with hash fragments", () => { - const input = - "https://linear.app/workspace/issue/ACME-123#comment-123"; - const expected = - "[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)"; - expect(formatLinearURL(input, baseSettings)).toBe(expected); - }); - }); + it("should format URLs with hash fragments", () => { + const input + = "https://linear.app/workspace/issue/ACME-123#comment-123" + const expected + = "[[linear/workspace/ACME-123]] [🔗](https://linear.app/workspace/issue/ACME-123)" + expect(formatLinearURL(input, baseSettings)).toBe(expected) + }) + }) - describe("When formatLinearURLs is disabled", () => { - it("should not format Linear URLs when setting is false", () => { - const settingsDisabled: AutomaticLinkerSettings = { - ...DEFAULT_SETTINGS, - formatLinearURLs: false, - }; - const input = "https://linear.app/workspace/issue/ACME-123"; - expect(formatLinearURL(input, settingsDisabled)).toBe(input); - }); - }); -}); + describe("When formatLinearURLs is disabled", () => { + it("should not format Linear URLs when setting is false", () => { + const settingsDisabled: AutomaticLinkerSettings = { + ...DEFAULT_SETTINGS, + formatLinearURLs: false, + } + const input = "https://linear.app/workspace/issue/ACME-123" + expect(formatLinearURL(input, settingsDisabled)).toBe(input) + }) + }) +}) diff --git a/src/replace-urls/__tests__/replace-urls.test.ts b/src/replace-urls/__tests__/replace-urls.test.ts index 99c2897..d8a96c9 100644 --- a/src/replace-urls/__tests__/replace-urls.test.ts +++ b/src/replace-urls/__tests__/replace-urls.test.ts @@ -1,24 +1,24 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it } from "vitest" import { - AutomaticLinkerSettings, - DEFAULT_SETTINGS, -} from "../../settings/settings-info"; -import { formatGitHubURL } from "../github"; -import { replaceURLs } from "../replace-urls"; + AutomaticLinkerSettings, + DEFAULT_SETTINGS, +} from "../../settings/settings-info" +import { formatGitHubURL } from "../github" +import { replaceURLs } from "../replace-urls" describe("replace-urls", () => { - const baseSettings: AutomaticLinkerSettings = { - ...DEFAULT_SETTINGS, - githubEnterpriseURLs: ["github.enterprise.com", "github.company.com"], - }; + const baseSettings: AutomaticLinkerSettings = { + ...DEFAULT_SETTINGS, + githubEnterpriseURLs: ["github.enterprise.com", "github.company.com"], + } - it("should replace GitHub URLs", () => { - const input = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - const expected = - "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)"; - expect(replaceURLs(input, baseSettings, formatGitHubURL)).toBe( - expected, - ); - }); -}); + it("should replace GitHub URLs", () => { + const input + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + const expected + = "[[github/kdnk/obsidian-automatic-linker]] [🔗](https://github.com/kdnk/obsidian-automatic-linker)" + expect(replaceURLs(input, baseSettings, formatGitHubURL)).toBe( + expected, + ) + }) +}) diff --git a/src/replace-urls/github.ts b/src/replace-urls/github.ts index 4fe3bec..d1aebba 100644 --- a/src/replace-urls/github.ts +++ b/src/replace-urls/github.ts @@ -1,13 +1,13 @@ -import { AutomaticLinkerSettings } from "../settings/settings-info"; -import { formatJiraURL } from "./jira"; -import { formatLinearURL } from "./linear"; +import { AutomaticLinkerSettings } from "../settings/settings-info" +import { formatJiraURL } from "./jira" +import { formatLinearURL } from "./linear" type GitHubURLInfo = { - owner: string; - repository: string; - type?: "pull" | "issues"; - id?: string; -}; + owner: string + repository: string + type?: "pull" | "issues" + id?: string +} /** * Format a GitHub URL by normalizing the format and converting to Obsidian link format: @@ -17,137 +17,138 @@ type GitHubURLInfo = { * @returns The formatted URL in Obsidian link format */ export function formatGitHubURL( - url: string, - settings: AutomaticLinkerSettings, + url: string, + settings: AutomaticLinkerSettings, ): string { - try { - const githubURL = new URL(url); + try { + const githubURL = new URL(url) - // Check if it's a GitHub URL (including Enterprise) - if (!isGitHubURL(githubURL, settings.githubEnterpriseURLs)) { - return url; - } + // Check if it's a GitHub URL (including Enterprise) + if (!isGitHubURL(githubURL, settings.githubEnterpriseURLs)) { + return url + } - const urlInfo = parseGitHubURL(githubURL); - if (!urlInfo) { - return url; - } + const urlInfo = parseGitHubURL(githubURL) + if (!urlInfo) { + return url + } - const cleanURL = getCleanURL(githubURL, urlInfo); - return formatToObsidianLink(urlInfo, cleanURL); - } catch (_e) { - // If URL is invalid, return original string - return url; - } + const cleanURL = getCleanURL(githubURL, urlInfo) + return formatToObsidianLink(urlInfo, cleanURL) + } + catch (_e) { + // If URL is invalid, return original string + return url + } } /** * Parse GitHub URL into its components */ function parseGitHubURL(url: URL): GitHubURLInfo | null { - const parts = url.pathname.split("/").filter(Boolean); - if (parts.length < 2) { - return null; - } + const parts = url.pathname.split("/").filter(Boolean) + if (parts.length < 2) { + return null + } - const [owner, repository, type, id] = parts; - const urlInfo: GitHubURLInfo = { - owner, - repository, - }; + const [owner, repository, type, id] = parts + const urlInfo: GitHubURLInfo = { + owner, + repository, + } - if (isPullRequestOrIssueURL(url)) { - urlInfo.type = type as "pull" | "issues"; - urlInfo.id = id; - } + if (isPullRequestOrIssueURL(url)) { + urlInfo.type = type as "pull" | "issues" + urlInfo.id = id + } - return urlInfo; + return urlInfo } /** * Get clean URL without query parameters and trailing slashes */ function getCleanURL(url: URL, urlInfo: GitHubURLInfo): string { - if (urlInfo.type && urlInfo.id) { - const basePath = `/${urlInfo.owner}/${urlInfo.repository}/${urlInfo.type}/${urlInfo.id}`; - return `${url.origin}${basePath}`; - } - return `${url.origin}/${urlInfo.owner}/${urlInfo.repository}`.replace( - /\/$/, - "", - ); + if (urlInfo.type && urlInfo.id) { + const basePath = `/${urlInfo.owner}/${urlInfo.repository}/${urlInfo.type}/${urlInfo.id}` + return `${url.origin}${basePath}` + } + return `${url.origin}/${urlInfo.owner}/${urlInfo.repository}`.replace( + /\/$/, + "", + ) } /** * Format URL info into Obsidian link format */ function formatToObsidianLink( - urlInfo: GitHubURLInfo, - cleanURL: string, + urlInfo: GitHubURLInfo, + cleanURL: string, ): string { - const url = new URL(cleanURL); - const isEnterpriseURL = url.hostname !== "github.com"; - const prefix = isEnterpriseURL ? "ghe" : "github"; + const url = new URL(cleanURL) + const isEnterpriseURL = url.hostname !== "github.com" + const prefix = isEnterpriseURL ? "ghe" : "github" - let wikiLink = `[[${prefix}/${urlInfo.owner}/${urlInfo.repository}`; - if (urlInfo.type && urlInfo.id) { - wikiLink += `/${urlInfo.type}/${urlInfo.id}`; - } - wikiLink += `]] [🔗](${cleanURL})`; - return wikiLink; + let wikiLink = `[[${prefix}/${urlInfo.owner}/${urlInfo.repository}` + if (urlInfo.type && urlInfo.id) { + wikiLink += `/${urlInfo.type}/${urlInfo.id}` + } + wikiLink += `]] [🔗](${cleanURL})` + return wikiLink } /** * Check if the URL is a GitHub URL (including Enterprise) */ function isGitHubURL(url: URL, enterpriseURLs: string[]): boolean { - // Check if it's github.com - if (url.hostname === "github.com") { - return true; - } + // Check if it's github.com + if (url.hostname === "github.com") { + return true + } - // Check if it matches any of the configured enterprise URLs - return enterpriseURLs.some((enterpriseURL) => { - // Remove any protocol and trailing slashes from the enterprise URL - const cleanEnterpriseURL = enterpriseURL - .replace(/^https?:\/\//, "") - .replace(/\/$/, ""); - return url.hostname === cleanEnterpriseURL; - }); + // Check if it matches any of the configured enterprise URLs + return enterpriseURLs.some((enterpriseURL) => { + // Remove any protocol and trailing slashes from the enterprise URL + const cleanEnterpriseURL = enterpriseURL + .replace(/^https?:\/\//, "") + .replace(/\/$/, "") + return url.hostname === cleanEnterpriseURL + }) } /** * Check if the URL is a pull request or issue URL */ function isPullRequestOrIssueURL(url: URL): boolean { - const path = url.pathname.toLowerCase(); - return path.includes("/pull/") || path.includes("/issues/"); + const path = url.pathname.toLowerCase() + return path.includes("/pull/") || path.includes("/issues/") } export function formatURL( - url: string, - settings: AutomaticLinkerSettings, + url: string, + settings: AutomaticLinkerSettings, ): string { - if (settings.formatGitHubURLs) { - const formattedGitHubURL = formatGitHubURL(url, settings); - if (formattedGitHubURL !== url) { - return formattedGitHubURL; - } - } + if (settings.formatGitHubURLs) { + const formattedGitHubURL = formatGitHubURL(url, settings) + if (formattedGitHubURL !== url) { + return formattedGitHubURL + } + } - if (settings.formatJiraURLs) { - const formattedJiraURL = formatJiraURL(url, settings); - if (formattedJiraURL !== url) { - return formattedJiraURL; - } - } + if (settings.formatJiraURLs) { + const formattedJiraURL = formatJiraURL(url, settings) + if (formattedJiraURL !== url) { + return formattedJiraURL + } + } - if (settings.formatLinearURLs) { - const formattedLinearURL = formatLinearURL(url, settings); - if (formattedLinearURL !== url) { - return formattedLinearURL; - } - } + if (settings.formatLinearURLs) { + const formattedLinearURL = formatLinearURL(url, settings) + if (formattedLinearURL !== url) { + return formattedLinearURL + } + } - return url; + return url } diff --git a/src/replace-urls/jira.ts b/src/replace-urls/jira.ts index def53e2..5c2ccaf 100644 --- a/src/replace-urls/jira.ts +++ b/src/replace-urls/jira.ts @@ -1,9 +1,9 @@ -import { AutomaticLinkerSettings } from "../settings/settings-info"; +import { AutomaticLinkerSettings } from "../settings/settings-info" type JiraURLInfo = { - project: string; - issueId: string; -}; + project: string + issueId: string +} /** * Format a Jira URL by converting to Obsidian link format: @@ -13,87 +13,88 @@ type JiraURLInfo = { * @returns The formatted URL in Obsidian link format */ export function formatJiraURL( - url: string, - settings: AutomaticLinkerSettings, + url: string, + settings: AutomaticLinkerSettings, ): string { - try { - const jiraURL = new URL(url); + try { + const jiraURL = new URL(url) - // Check if it's a configured Jira URL - if (!isJiraURL(jiraURL, settings.jiraURLs)) { - return url; - } + // Check if it's a configured Jira URL + if (!isJiraURL(jiraURL, settings.jiraURLs)) { + return url + } - // Don't format if URL has query parameters (including empty query) - if (url.includes("?")) { - return url; - } + // Don't format if URL has query parameters (including empty query) + if (url.includes("?")) { + return url + } - const urlInfo = parseJiraURL(jiraURL); - if (!urlInfo) { - return url; - } + const urlInfo = parseJiraURL(jiraURL) + if (!urlInfo) { + return url + } - const cleanURL = getCleanURL(jiraURL, urlInfo); - return formatToObsidianLink(jiraURL, urlInfo, cleanURL); - } catch (_e) { - // If URL is invalid, return original string - return url; - } + const cleanURL = getCleanURL(jiraURL, urlInfo) + return formatToObsidianLink(jiraURL, urlInfo, cleanURL) + } + catch (_e) { + // If URL is invalid, return original string + return url + } } /** * Parse Jira URL into its components */ function parseJiraURL(url: URL): JiraURLInfo | null { - const parts = url.pathname.split("/").filter(Boolean); + const parts = url.pathname.split("/").filter(Boolean) - // Check if the URL matches the expected pattern /browse/PROJECT-123 - if (parts[0] !== "browse" || parts.length !== 2) { - return null; - } + // Check if the URL matches the expected pattern /browse/PROJECT-123 + if (parts[0] !== "browse" || parts.length !== 2) { + return null + } - const issueParts = parts[1].split("-"); - if (issueParts.length !== 2) { - return null; - } + const issueParts = parts[1].split("-") + if (issueParts.length !== 2) { + return null + } - return { - project: issueParts[0], - issueId: issueParts[1], - }; + return { + project: issueParts[0], + issueId: issueParts[1], + } } /** * Get clean URL without query parameters and trailing slashes */ function getCleanURL(url: URL, urlInfo: JiraURLInfo): string { - return `${url.origin}/browse/${urlInfo.project}-${urlInfo.issueId}`; + return `${url.origin}/browse/${urlInfo.project}-${urlInfo.issueId}` } /** * Format URL info into Obsidian link format */ function formatToObsidianLink( - url: URL, - urlInfo: JiraURLInfo, - cleanURL: string, + url: URL, + urlInfo: JiraURLInfo, + cleanURL: string, ): string { - // Extract domain name (e.g., "work" from "sub-domain.work.com") - const domain = url.hostname.split(".")[1]; - const wikiLink = `[[${domain}/jira/${urlInfo.project}/${urlInfo.issueId}]] [🔗](${cleanURL})`; - return wikiLink; + // Extract domain name (e.g., "work" from "sub-domain.work.com") + const domain = url.hostname.split(".")[1] + const wikiLink = `[[${domain}/jira/${urlInfo.project}/${urlInfo.issueId}]] [🔗](${cleanURL})` + return wikiLink } /** * Check if the URL is a configured Jira URL */ function isJiraURL(url: URL, jiraURLs: string[]): boolean { - return jiraURLs.some((jiraURL) => { - // Remove any protocol and trailing slashes from the Jira URL - const cleanJiraURL = jiraURL - .replace(/^https?:\/\//, "") - .replace(/\/$/, ""); - return url.hostname === cleanJiraURL; - }); + return jiraURLs.some((jiraURL) => { + // Remove any protocol and trailing slashes from the Jira URL + const cleanJiraURL = jiraURL + .replace(/^https?:\/\//, "") + .replace(/\/$/, "") + return url.hostname === cleanJiraURL + }) } diff --git a/src/replace-urls/linear.ts b/src/replace-urls/linear.ts index 23a1bd1..efff734 100644 --- a/src/replace-urls/linear.ts +++ b/src/replace-urls/linear.ts @@ -1,9 +1,9 @@ -import { AutomaticLinkerSettings } from "../settings/settings-info"; +import { AutomaticLinkerSettings } from "../settings/settings-info" type LinearURLInfo = { - workspace: string; - issueId: string; -}; + workspace: string + issueId: string +} /** * Format a Linear URL by converting to Obsidian link format: @@ -13,92 +13,93 @@ type LinearURLInfo = { * @returns The formatted URL in Obsidian link format */ export function formatLinearURL( - url: string, - settings: AutomaticLinkerSettings, + url: string, + settings: AutomaticLinkerSettings, ): string { - // Check if Linear URL formatting is enabled - if (!settings.formatLinearURLs) { - return url; - } + // Check if Linear URL formatting is enabled + if (!settings.formatLinearURLs) { + return url + } - try { - // Handle linear:// protocol by converting to https:// - let processedURL = url; - if (url.startsWith("linear://")) { - processedURL = url.replace("linear://", "https://linear.app/"); - } + try { + // Handle linear:// protocol by converting to https:// + let processedURL = url + if (url.startsWith("linear://")) { + processedURL = url.replace("linear://", "https://linear.app/") + } - const linearURL = new URL(processedURL); + const linearURL = new URL(processedURL) - // Check if it's a Linear URL - if (!isLinearURL(linearURL)) { - return url; - } + // Check if it's a Linear URL + if (!isLinearURL(linearURL)) { + return url + } - const urlInfo = parseLinearURL(linearURL); - if (!urlInfo) { - return url; - } + const urlInfo = parseLinearURL(linearURL) + if (!urlInfo) { + return url + } - const cleanURL = getCleanURL(urlInfo); - return formatToObsidianLink(urlInfo, cleanURL); - } catch (_e) { - // If URL is invalid, return original string - return url; - } + const cleanURL = getCleanURL(urlInfo) + return formatToObsidianLink(urlInfo, cleanURL) + } + catch (_e) { + // If URL is invalid, return original string + return url + } } /** * Parse Linear URL into its components */ function parseLinearURL(url: URL): LinearURLInfo | null { - const parts = url.pathname.split("/").filter(Boolean); + const parts = url.pathname.split("/").filter(Boolean) - // Expected pattern: /workspace/issue/ISSUE-123 or /workspace/issue/ISSUE-123/title - if (parts.length < 3) { - return null; - } + // Expected pattern: /workspace/issue/ISSUE-123 or /workspace/issue/ISSUE-123/title + if (parts.length < 3) { + return null + } - const [workspace, type, issueId] = parts; + const [workspace, type, issueId] = parts - // Check if the URL matches the expected pattern - if (type !== "issue") { - return null; - } + // Check if the URL matches the expected pattern + if (type !== "issue") { + return null + } - // Validate issue ID format (should be like PROJ-123) - const issueIdPattern = /^[A-Z]+-\d+$/; - if (!issueIdPattern.test(issueId)) { - return null; - } + // Validate issue ID format (should be like PROJ-123) + const issueIdPattern = /^[A-Z]+-\d+$/ + if (!issueIdPattern.test(issueId)) { + return null + } - return { - workspace, - issueId, - }; + return { + workspace, + issueId, + } } /** * Get clean URL without query parameters, hash fragments, and title */ function getCleanURL(urlInfo: LinearURLInfo): string { - return `https://linear.app/${urlInfo.workspace}/issue/${urlInfo.issueId}`; + return `https://linear.app/${urlInfo.workspace}/issue/${urlInfo.issueId}` } /** * Format URL info into Obsidian link format */ function formatToObsidianLink( - urlInfo: LinearURLInfo, - cleanURL: string, + urlInfo: LinearURLInfo, + cleanURL: string, ): string { - const wikiLink = `[[linear/${urlInfo.workspace}/${urlInfo.issueId}]] [🔗](${cleanURL})`; - return wikiLink; + const wikiLink = `[[linear/${urlInfo.workspace}/${urlInfo.issueId}]] [🔗](${cleanURL})` + return wikiLink } /** * Check if the URL is a Linear URL */ function isLinearURL(url: URL): boolean { - return url.hostname === "linear.app"; + return url.hostname === "linear.app" } diff --git a/src/replace-urls/replace-urls.ts b/src/replace-urls/replace-urls.ts index 737865b..fabf103 100644 --- a/src/replace-urls/replace-urls.ts +++ b/src/replace-urls/replace-urls.ts @@ -1,13 +1,13 @@ -import { AutomaticLinkerSettings } from "../settings/settings-info"; +import { AutomaticLinkerSettings } from "../settings/settings-info" export const replaceURLs = ( - fileContent: string, - settings: AutomaticLinkerSettings, - formatter: (url: string, settings: AutomaticLinkerSettings) => string, + fileContent: string, + settings: AutomaticLinkerSettings, + formatter: (url: string, settings: AutomaticLinkerSettings) => string, ) => { - const githubUrlPattern = /(?<!\[\[.*)(https?:\/\/[^\s\]]+)(?!.*\]\])/g; - fileContent = fileContent.replace(githubUrlPattern, (match) => { - return formatter(match, settings); - }); - return fileContent; -}; + const githubUrlPattern = /(?<!\[\[.*)(https?:\/\/[^\s\]]+)(?!.*\]\])/g + fileContent = fileContent.replace(githubUrlPattern, (match) => { + return formatter(match, settings) + }) + return fileContent +} diff --git a/src/settings/settings-info.ts b/src/settings/settings-info.ts index 09ee7ae..e3a409a 100644 --- a/src/settings/settings-info.ts +++ b/src/settings/settings-info.ts @@ -1,47 +1,47 @@ export type AutomaticLinkerSettings = { - formatOnSave: boolean; - showNotice: boolean; - respectNewFileFolderPath: boolean; // Respect Obsidian's "Folder to create new notes in" setting as base directory - considerAliases: boolean; // Consider aliases when linking - namespaceResolution: boolean; // Automatically resolve namespaces for shorthand links - ignoreDateFormats: boolean; // Ignore date formatted links (e.g. 2025-02-10) - formatGitHubURLs: boolean; // Format GitHub URLs on save - githubEnterpriseURLs: string[]; // List of GitHub Enterprise URLs - formatJiraURLs: boolean; // Format Jira URLs on save - jiraURLs: string[]; // List of Jira URLs (domains) - formatLinearURLs: boolean; // Format Linear URLs on save - debug: boolean; // Enable debug logging - ignoreCase: boolean; // Ignore case when matching links - replaceUrlWithTitle: boolean; // Replace raw URLs with [Title](URL) - 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 - runLinterAfterFormatting: boolean; // Run Obsidian Linter after automatic linker formatting - runPrettierAfterFormatting: boolean; // Run Prettier after automatic linker formatting - formatDelayMs: number; // Delay in milliseconds before running linter after formatting -}; + formatOnSave: boolean + showNotice: boolean + respectNewFileFolderPath: boolean // Respect Obsidian's "Folder to create new notes in" setting as base directory + considerAliases: boolean // Consider aliases when linking + namespaceResolution: boolean // Automatically resolve namespaces for shorthand links + ignoreDateFormats: boolean // Ignore date formatted links (e.g. 2025-02-10) + formatGitHubURLs: boolean // Format GitHub URLs on save + githubEnterpriseURLs: string[] // List of GitHub Enterprise URLs + formatJiraURLs: boolean // Format Jira URLs on save + jiraURLs: string[] // List of Jira URLs (domains) + formatLinearURLs: boolean // Format Linear URLs on save + debug: boolean // Enable debug logging + ignoreCase: boolean // Ignore case when matching links + replaceUrlWithTitle: boolean // Replace raw URLs with [Title](URL) + 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 + runLinterAfterFormatting: boolean // Run Obsidian Linter after automatic linker formatting + runPrettierAfterFormatting: boolean // Run Prettier after automatic linker formatting + formatDelayMs: number // Delay in milliseconds before running linter after formatting +} export const DEFAULT_SETTINGS: AutomaticLinkerSettings = { - formatOnSave: false, - showNotice: false, - respectNewFileFolderPath: false, // Default: do not respect Obsidian's "Folder to create new notes in" setting - considerAliases: true, // Default: consider aliases - namespaceResolution: true, // Default: enable automatic namespace resolution - ignoreDateFormats: true, // Default: ignore date formats (e.g. 2025-02-10) - formatGitHubURLs: true, // Default: format GitHub URLs - githubEnterpriseURLs: [], // Default: empty list - formatJiraURLs: true, // Default: format Jira URLs - jiraURLs: [], // Default: empty list - formatLinearURLs: false, // Default: format Linear URLs - debug: false, // Default: disable debug logging - ignoreCase: true, // Default: case-sensitive matching - replaceUrlWithTitle: true, // Default: enable replacing URLs with titles - replaceUrlWithTitleIgnoreDomains: [], - excludeDirsFromAutoLinking: [], // Default: no excluded directories - preventSelfLinking: false, // Default: allow self-linking (backward compatibility) - removeAliasInDirs: [], // Default: no directories for alias removal - runLinterAfterFormatting: false, // Default: do not run linter after formatting - runPrettierAfterFormatting: false, // Run Prettier after automatic linker formatting - formatDelayMs: 1, // Default: 1ms delay before running linter -}; + formatOnSave: false, + showNotice: false, + respectNewFileFolderPath: false, // Default: do not respect Obsidian's "Folder to create new notes in" setting + considerAliases: true, // Default: consider aliases + namespaceResolution: true, // Default: enable automatic namespace resolution + ignoreDateFormats: true, // Default: ignore date formats (e.g. 2025-02-10) + formatGitHubURLs: true, // Default: format GitHub URLs + githubEnterpriseURLs: [], // Default: empty list + formatJiraURLs: true, // Default: format Jira URLs + jiraURLs: [], // Default: empty list + formatLinearURLs: false, // Default: format Linear URLs + debug: false, // Default: disable debug logging + ignoreCase: true, // Default: case-sensitive matching + replaceUrlWithTitle: true, // Default: enable replacing URLs with titles + replaceUrlWithTitleIgnoreDomains: [], + excludeDirsFromAutoLinking: [], // Default: no excluded directories + preventSelfLinking: false, // Default: allow self-linking (backward compatibility) + removeAliasInDirs: [], // Default: no directories for alias removal + runLinterAfterFormatting: false, // Default: do not run linter after formatting + runPrettierAfterFormatting: false, // Run Prettier after automatic linker formatting + formatDelayMs: 1, // Default: 1ms delay before running linter +} diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 286b755..22bba52 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -1,395 +1,395 @@ -import { App, PluginSettingTab, Setting } from "obsidian"; -import AutomaticLinkerPlugin from "../main"; +import { App, PluginSettingTab, Setting } from "obsidian" +import AutomaticLinkerPlugin from "../main" export class AutomaticLinkerPluginSettingsTab extends PluginSettingTab { - plugin: AutomaticLinkerPlugin; - constructor(app: App, plugin: AutomaticLinkerPlugin) { - super(app, plugin); - this.plugin = plugin; - } + plugin: AutomaticLinkerPlugin + constructor(app: App, plugin: AutomaticLinkerPlugin) { + super(app, plugin) + this.plugin = plugin + } - display(): void { - const { containerEl } = this; - containerEl.empty(); + display(): void { + const { containerEl } = this + containerEl.empty() - new Setting(containerEl).setName("General").setHeading(); + new Setting(containerEl).setName("General").setHeading() - // Toggle for "Format on Save" setting. - new Setting(containerEl) - .setName("Format on save") - .setDesc( - "When enabled, the file will be automatically formatted (links replaced) when saving.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.formatOnSave) - .onChange(async (value) => { - this.plugin.settings.formatOnSave = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for "Format on Save" setting. + new Setting(containerEl) + .setName("Format on save") + .setDesc( + "When enabled, the file will be automatically formatted (links replaced) when saving.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.formatOnSave) + .onChange(async (value) => { + this.plugin.settings.formatOnSave = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for respecting Obsidian's "Folder to create new notes in" setting - new Setting(containerEl) - .setName("Respect 'Folder to create new notes in' setting") - .setDesc( - "When enabled, the plugin will use Obsidian's 'Folder to create new notes in' setting as the base directory for omitting folder prefixes in links.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.respectNewFileFolderPath) - .onChange(async (value) => { - this.plugin.settings.respectNewFileFolderPath = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for respecting Obsidian's "Folder to create new notes in" setting + new Setting(containerEl) + .setName("Respect 'Folder to create new notes in' setting") + .setDesc( + "When enabled, the plugin will use Obsidian's 'Folder to create new notes in' setting as the base directory for omitting folder prefixes in links.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.respectNewFileFolderPath) + .onChange(async (value) => { + this.plugin.settings.respectNewFileFolderPath = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for running linter after formatting - new Setting(containerEl) - .setName("Run Obsidian Linter after formatting") - .setDesc( - "When enabled, Obsidian Linter will be executed after Automatic Linker formatting. This requires the Obsidian Linter plugin to be installed.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.runLinterAfterFormatting) - .onChange(async (value) => { - this.plugin.settings.runLinterAfterFormatting = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for running linter after formatting + new Setting(containerEl) + .setName("Run Obsidian Linter after formatting") + .setDesc( + "When enabled, Obsidian Linter will be executed after Automatic Linker formatting. This requires the Obsidian Linter plugin to be installed.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.runLinterAfterFormatting) + .onChange(async (value) => { + this.plugin.settings.runLinterAfterFormatting = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for running prettier after formatting - new Setting(containerEl) - .setName("Run Prettier after formatting") - .setDesc( - "When enabled, Prettier will be executed after Automatic Linker formatting. This requires prettier-format plugin to be installed. https://github.com/dylanarmstrong/obsidian-prettier-plugin", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.runPrettierAfterFormatting) - .onChange(async (value) => { - this.plugin.settings.runPrettierAfterFormatting = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for running prettier after formatting + new Setting(containerEl) + .setName("Run Prettier after formatting") + .setDesc( + "When enabled, Prettier will be executed after Automatic Linker formatting. This requires prettier-format plugin to be installed. https://github.com/dylanarmstrong/obsidian-prettier-plugin", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.runPrettierAfterFormatting) + .onChange(async (value) => { + this.plugin.settings.runPrettierAfterFormatting = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Setting for linter delay - new Setting(containerEl) - .setName("Format delay (ms)") - .setDesc( - "Delay in milliseconds before formatting. Increase this value if the linter/prettier runs before the file is fully saved.", - ) - .addText((text) => { - text.setPlaceholder("e.g. 100") - .setValue(this.plugin.settings.formatDelayMs.toString()) - .onChange(async (value) => { - const parsedValue = parseInt(value); - if (!isNaN(parsedValue) && parsedValue >= 0) { - this.plugin.settings.formatDelayMs = parsedValue; - await this.plugin.saveData(this.plugin.settings); - } - }); - }); + // Setting for linter delay + new Setting(containerEl) + .setName("Format delay (ms)") + .setDesc( + "Delay in milliseconds before formatting. Increase this value if the linter/prettier runs before the file is fully saved.", + ) + .addText((text) => { + text.setPlaceholder("e.g. 100") + .setValue(this.plugin.settings.formatDelayMs.toString()) + .onChange(async (value) => { + const parsedValue = parseInt(value) + if (!isNaN(parsedValue) && parsedValue >= 0) { + this.plugin.settings.formatDelayMs = parsedValue + await this.plugin.saveData(this.plugin.settings) + } + }) + }) - // Toggle for considering aliases. - new Setting(containerEl) - .setName("Consider aliases") - .setDesc( - "When enabled, aliases will be taken into account when processing links. Note: A restart is required for changes to take effect.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.considerAliases) - .onChange(async (value) => { - this.plugin.settings.considerAliases = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for considering aliases. + new Setting(containerEl) + .setName("Consider aliases") + .setDesc( + "When enabled, aliases will be taken into account when processing links. Note: A restart is required for changes to take effect.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.considerAliases) + .onChange(async (value) => { + this.plugin.settings.considerAliases = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for automatic namespace resolution. - new Setting(containerEl) - .setName("Automatic namespace resolution") - .setDesc( - "When enabled, the plugin will automatically resolve namespaces for shorthand links. If multiple candidates share the same shorthand, the candidate with the most common path segments relative to the current file will be selected.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.namespaceResolution) - .onChange(async (value) => { - this.plugin.settings.namespaceResolution = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for automatic namespace resolution. + new Setting(containerEl) + .setName("Automatic namespace resolution") + .setDesc( + "When enabled, the plugin will automatically resolve namespaces for shorthand links. If multiple candidates share the same shorthand, the candidate with the most common path segments relative to the current file will be selected.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.namespaceResolution) + .onChange(async (value) => { + this.plugin.settings.namespaceResolution = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for ignoring date formats. - new Setting(containerEl) - .setName("Ignore date formats") - .setDesc( - "When enabled, links that match date formats (e.g. 2025-02-10) will be ignored. This helps maintain compatibility with Obsidian Tasks.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.ignoreDateFormats) - .onChange(async (value) => { - this.plugin.settings.ignoreDateFormats = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for ignoring date formats. + new Setting(containerEl) + .setName("Ignore date formats") + .setDesc( + "When enabled, links that match date formats (e.g. 2025-02-10) will be ignored. This helps maintain compatibility with Obsidian Tasks.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.ignoreDateFormats) + .onChange(async (value) => { + this.plugin.settings.ignoreDateFormats = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for ignoring case in link matching - new Setting(containerEl) - .setName("Ignore case") - .setDesc( - "When enabled, link matching will be case-insensitive. The original case of the text will be preserved in the link.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.ignoreCase) - .onChange(async (value) => { - this.plugin.settings.ignoreCase = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for ignoring case in link matching + new Setting(containerEl) + .setName("Ignore case") + .setDesc( + "When enabled, link matching will be case-insensitive. The original case of the text will be preserved in the link.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.ignoreCase) + .onChange(async (value) => { + this.plugin.settings.ignoreCase = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for preventing self-linking - new Setting(containerEl) - .setName("Prevent self-linking") - .setDesc( - "When enabled, text will not be linked to its own file. For example, the word 'VPN' inside VPN.md will not be converted to a link.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.preventSelfLinking) - .onChange(async (value) => { - this.plugin.settings.preventSelfLinking = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for preventing self-linking + new Setting(containerEl) + .setName("Prevent self-linking") + .setDesc( + "When enabled, text will not be linked to its own file. For example, the word 'VPN' inside VPN.md will not be converted to a link.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.preventSelfLinking) + .onChange(async (value) => { + this.plugin.settings.preventSelfLinking = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Add excluding dirs that you wish to exclude from the automatic linking - new Setting(containerEl) - .setName("Exclude directories from automatic linking") - .setDesc( - "Directories to be excluded from automatic linking, one per line (e.g. 'Templates')", - ) - .addTextArea((text) => { - text.setPlaceholder("Templates\nArchive") - .setValue( - this.plugin.settings.excludeDirsFromAutoLinking.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.excludeDirsFromAutoLinking = dirs; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Add excluding dirs that you wish to exclude from the automatic linking + new Setting(containerEl) + .setName("Exclude directories from automatic linking") + .setDesc( + "Directories to be excluded from automatic linking, one per line (e.g. 'Templates')", + ) + .addTextArea((text) => { + text.setPlaceholder("Templates\nArchive") + .setValue( + this.plugin.settings.excludeDirsFromAutoLinking.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.excludeDirsFromAutoLinking = dirs + await this.plugin.saveData(this.plugin.settings) + }) + }) - // 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); - }); - }); + // 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 Replacement with Title") - .setHeading(); + new Setting(containerEl) + .setName("URL Replacement with Title") + .setHeading() - // Toggle for replacing URLs with titles - new Setting(containerEl) - .setName("Replace URL with title") - .setDesc( - "When enabled, raw URLs will be replaced with [Page Title](URL). Requires fetching the URL content.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.replaceUrlWithTitle) - .onChange(async (value) => { - this.plugin.settings.replaceUrlWithTitle = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for replacing URLs with titles + new Setting(containerEl) + .setName("Replace URL with title") + .setDesc( + "When enabled, raw URLs will be replaced with [Page Title](URL). Requires fetching the URL content.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.replaceUrlWithTitle) + .onChange(async (value) => { + this.plugin.settings.replaceUrlWithTitle = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - new Setting(containerEl) - .setName("Igonre domains") - .setDesc( - "Ignore domains for replacing URLs with titles, one per line (e.g. x.com)", - ) - .addTextArea((text) => { - text.setPlaceholder("") - .setValue( - this.plugin.settings.replaceUrlWithTitleIgnoreDomains.join( - "\n", - ), - ) - .onChange(async (value) => { - // Split by newlines and filter out empty lines - const urls = value - .split("\n") - .map((url) => url.trim()) - .filter(Boolean); - this.plugin.settings.replaceUrlWithTitleIgnoreDomains = - urls; - await this.plugin.saveData(this.plugin.settings); - }); - // Make the text area taller - text.inputEl.rows = 4; - text.inputEl.cols = 50; - }); + new Setting(containerEl) + .setName("Igonre domains") + .setDesc( + "Ignore domains for replacing URLs with titles, one per line (e.g. x.com)", + ) + .addTextArea((text) => { + text.setPlaceholder("") + .setValue( + this.plugin.settings.replaceUrlWithTitleIgnoreDomains.join( + "\n", + ), + ) + .onChange(async (value) => { + // Split by newlines and filter out empty lines + const urls = value + .split("\n") + .map(url => url.trim()) + .filter(Boolean) + this.plugin.settings.replaceUrlWithTitleIgnoreDomains + = urls + await this.plugin.saveData(this.plugin.settings) + }) + // Make the text area taller + text.inputEl.rows = 4 + text.inputEl.cols = 50 + }) - new Setting(containerEl) - .setName("URL Formatting for GitHub") - .setHeading(); + new Setting(containerEl) + .setName("URL Formatting for GitHub") + .setHeading() - // Toggle for formatting GitHub URLs on save - new Setting(containerEl) - .setName("Format GitHub URLs on save") - .setDesc( - "When enabled, GitHub URLs will be formatted when saving the file.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.formatGitHubURLs) - .onChange(async (value) => { - this.plugin.settings.formatGitHubURLs = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for formatting GitHub URLs on save + new Setting(containerEl) + .setName("Format GitHub URLs on save") + .setDesc( + "When enabled, GitHub URLs will be formatted when saving the file.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.formatGitHubURLs) + .onChange(async (value) => { + this.plugin.settings.formatGitHubURLs = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Add GitHub Enterprise URLs setting - new Setting(containerEl) - .setName("GitHub Enterprise URLs") - .setDesc( - "Add your GitHub Enterprise URLs, one per line (e.g. github.enterprise.com)", - ) - .addTextArea((text) => { - text.setPlaceholder("github.enterprise.com\ngithub.company.com") - .setValue( - this.plugin.settings.githubEnterpriseURLs.join("\n"), - ) - .onChange(async (value) => { - // Split by newlines and filter out empty lines - const urls = value - .split("\n") - .map((url) => url.trim()) - .filter(Boolean); - this.plugin.settings.githubEnterpriseURLs = urls; - await this.plugin.saveData(this.plugin.settings); - }); - // Make the text area taller - text.inputEl.rows = 4; - text.inputEl.cols = 50; - }); + // Add GitHub Enterprise URLs setting + new Setting(containerEl) + .setName("GitHub Enterprise URLs") + .setDesc( + "Add your GitHub Enterprise URLs, one per line (e.g. github.enterprise.com)", + ) + .addTextArea((text) => { + text.setPlaceholder("github.enterprise.com\ngithub.company.com") + .setValue( + this.plugin.settings.githubEnterpriseURLs.join("\n"), + ) + .onChange(async (value) => { + // Split by newlines and filter out empty lines + const urls = value + .split("\n") + .map(url => url.trim()) + .filter(Boolean) + this.plugin.settings.githubEnterpriseURLs = urls + await this.plugin.saveData(this.plugin.settings) + }) + // Make the text area taller + text.inputEl.rows = 4 + text.inputEl.cols = 50 + }) - new Setting(containerEl) - .setName("URL Formatting for Jira") - .setHeading(); + new Setting(containerEl) + .setName("URL Formatting for Jira") + .setHeading() - // Toggle for formatting JIRA URLs on save - new Setting(containerEl) - .setName("Format JIRA URLs on save") - .setDesc( - "When enabled, JIRA URLs will be formatted when saving the file.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.formatJiraURLs) - .onChange(async (value) => { - this.plugin.settings.formatJiraURLs = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for formatting JIRA URLs on save + new Setting(containerEl) + .setName("Format JIRA URLs on save") + .setDesc( + "When enabled, JIRA URLs will be formatted when saving the file.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.formatJiraURLs) + .onChange(async (value) => { + this.plugin.settings.formatJiraURLs = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Add JIRA URLs setting - new Setting(containerEl) - .setName("JIRA URLs") - .setDesc( - "Add your JIRA URLs, one per line (e.g. jira.enterprise.com)", - ) - .addTextArea((text) => { - text.setPlaceholder("jira.enterprise.com\njira.company.com") - .setValue(this.plugin.settings.jiraURLs.join("\n")) - .onChange(async (value) => { - // Split by newlines and filter out empty lines - const urls = value - .split("\n") - .map((url) => url.trim()) - .filter(Boolean); - this.plugin.settings.jiraURLs = urls; - await this.plugin.saveData(this.plugin.settings); - }); - // Make the text area taller - text.inputEl.rows = 4; - text.inputEl.cols = 50; - }); + // Add JIRA URLs setting + new Setting(containerEl) + .setName("JIRA URLs") + .setDesc( + "Add your JIRA URLs, one per line (e.g. jira.enterprise.com)", + ) + .addTextArea((text) => { + text.setPlaceholder("jira.enterprise.com\njira.company.com") + .setValue(this.plugin.settings.jiraURLs.join("\n")) + .onChange(async (value) => { + // Split by newlines and filter out empty lines + const urls = value + .split("\n") + .map(url => url.trim()) + .filter(Boolean) + this.plugin.settings.jiraURLs = urls + await this.plugin.saveData(this.plugin.settings) + }) + // Make the text area taller + text.inputEl.rows = 4 + text.inputEl.cols = 50 + }) - new Setting(containerEl) - .setName("URL Formatting for Linear") - .setHeading(); + new Setting(containerEl) + .setName("URL Formatting for Linear") + .setHeading() - // Toggle for formatting Linear URLs on save - new Setting(containerEl) - .setName("Format Linear URLs on save") - .setDesc( - "When enabled, Linear URLs will be formatted when saving the file.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.formatLinearURLs) - .onChange(async (value) => { - this.plugin.settings.formatLinearURLs = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for formatting Linear URLs on save + new Setting(containerEl) + .setName("Format Linear URLs on save") + .setDesc( + "When enabled, Linear URLs will be formatted when saving the file.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.formatLinearURLs) + .onChange(async (value) => { + this.plugin.settings.formatLinearURLs = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - new Setting(containerEl).setName("Debug").setHeading(); + new Setting(containerEl).setName("Debug").setHeading() - // Toggle for showing the load notice. - new Setting(containerEl) - .setName("Show load notice") - .setDesc("Display a notice when markdown files are loaded.") - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.showNotice) - .onChange(async (value) => { - this.plugin.settings.showNotice = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); + // Toggle for showing the load notice. + new Setting(containerEl) + .setName("Show load notice") + .setDesc("Display a notice when markdown files are loaded.") + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.showNotice) + .onChange(async (value) => { + this.plugin.settings.showNotice = value + await this.plugin.saveData(this.plugin.settings) + }) + }) - // Toggle for debug logging - new Setting(containerEl) - .setName("Debug mode") - .setDesc( - "When enabled, debug information will be logged to the console.", - ) - .addToggle((toggle) => { - toggle - .setValue(this.plugin.settings.debug) - .onChange(async (value) => { - this.plugin.settings.debug = value; - await this.plugin.saveData(this.plugin.settings); - }); - }); - } + // Toggle for debug logging + new Setting(containerEl) + .setName("Debug mode") + .setDesc( + "When enabled, debug information will be logged to the console.", + ) + .addToggle((toggle) => { + toggle + .setValue(this.plugin.settings.debug) + .onChange(async (value) => { + this.plugin.settings.debug = value + await this.plugin.saveData(this.plugin.settings) + }) + }) + } } diff --git a/src/trie.ts b/src/trie.ts index e628054..cfe7445 100644 --- a/src/trie.ts +++ b/src/trie.ts @@ -13,11 +13,11 @@ */ // src/trie.ts -import { PathAndAliases } from "./path-and-aliases.types"; +import { PathAndAliases } from "./path-and-aliases.types" export interface TrieNode { - children: Map<string, TrieNode>; - candidate?: string; + children: Map<string, TrieNode> + candidate?: string } /** @@ -26,231 +26,231 @@ export interface TrieNode { * under the baseDir. Otherwise, returns the first directory segment. */ export const getTopLevelDirectoryName = ( - path: string, - baseDir?: string, + path: string, + baseDir?: string, ): string => { - const prefix = baseDir + "/"; - if (baseDir) { - if (path.startsWith(prefix)) { - const rest = path.slice(prefix.length); - const segments = rest.split("/"); - return segments[0] || ""; - } - } - // Fallback: return the first segment - const segments = path.split("/"); - return segments[0] || ""; -}; + const prefix = baseDir + "/" + if (baseDir) { + if (path.startsWith(prefix)) { + const rest = path.slice(prefix.length) + const segments = rest.split("/") + return segments[0] || "" + } + } + // Fallback: return the first segment + const segments = path.split("/") + return segments[0] || "" +} export const buildTrie = (words: string[], ignoreCase = false): TrieNode => { - const root: TrieNode = { children: new Map() }; + const root: TrieNode = { children: new Map() } - for (const word of words) { - let node = root; - const chars = ignoreCase ? word.toLowerCase() : word; - for (const char of chars) { - let child = node.children.get(char); - if (!child) { - child = { children: new Map() }; - node.children.set(char, child); - } - node = child; - } - node.candidate = word; // preserve the original case - } + for (const word of words) { + let node = root + const chars = ignoreCase ? word.toLowerCase() : word + for (const char of chars) { + let child = node.children.get(char) + if (!child) { + child = { children: new Map() } + node.children.set(char, child) + } + node = child + } + node.candidate = word // preserve the original case + } - return root; -}; + return root +} // CandidateData holds the canonical replacement string as well as namespace‐設定 export interface CandidateData { - canonical: string; - scoped: boolean; - namespace: string; + canonical: string + scoped: boolean + namespace: string } export const buildCandidateTrie = ( - allFiles: PathAndAliases[], - baseDir: string | undefined, - ignoreCase = false, + allFiles: PathAndAliases[], + baseDir: string | undefined, + ignoreCase = false, ) => { - // Filter out files with exclude: true - const linkableFiles = allFiles.filter((f) => !f.exclude); + // Filter out files with exclude: true + const linkableFiles = allFiles.filter(f => !f.exclude) - // Process candidate strings from file paths. - type Candidate = { - full: string; - short: string | null; - scoped: boolean; - // Effective namespace computed relative to baseDir. - namespace: string; - }; - const basePrefix = baseDir ? `${baseDir}/` : null; - const basePrefixLength = basePrefix?.length || 0; + // Process candidate strings from file paths. + type Candidate = { + full: string + short: string | null + scoped: boolean + // Effective namespace computed relative to baseDir. + namespace: string + } + const basePrefix = baseDir ? `${baseDir}/` : null + const basePrefixLength = basePrefix?.length || 0 - const candidates: Candidate[] = linkableFiles.map((f) => { - const candidate: Candidate = { - full: f.path, - short: null, - scoped: f.scoped, - namespace: getTopLevelDirectoryName(f.path, baseDir), - }; - if (basePrefix && f.path.startsWith(basePrefix)) { - candidate.short = f.path.slice(basePrefixLength); - } - return candidate; - }); + const candidates: Candidate[] = linkableFiles.map((f) => { + const candidate: Candidate = { + full: f.path, + short: null, + scoped: f.scoped, + namespace: getTopLevelDirectoryName(f.path, baseDir), + } + if (basePrefix && f.path.startsWith(basePrefix)) { + candidate.short = f.path.slice(basePrefixLength) + } + return candidate + }) - // Build a mapping from candidate string to its CandidateData. - const candidateMap = new Map<string, CandidateData>(); + // Build a mapping from candidate string to its CandidateData. + const candidateMap = new Map<string, CandidateData>() - // Register normal candidates. - for (const { full, short, scoped, namespace } of candidates) { - // Register the full path and its case-insensitive variant if needed - candidateMap.set(full, { - canonical: full, - scoped, - namespace, - }); + // Register normal candidates. + for (const { full, short, scoped, namespace } of candidates) { + // Register the full path and its case-insensitive variant if needed + candidateMap.set(full, { + canonical: full, + scoped, + namespace, + }) - // For paths with a slash, register the last segment - const lastSlashIndex = full.lastIndexOf("/"); - if (lastSlashIndex !== -1) { - const lastSegment = full.slice(lastSlashIndex + 1); - // For CJK paths or when ignoreCase is enabled - if ( - ignoreCase || - /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u.test( - lastSegment, - ) - ) { - // Register both the original case and lowercase versions - candidateMap.set(lastSegment, { - canonical: full, - scoped, - namespace, - }); - if (ignoreCase) { - candidateMap.set(lastSegment.toLowerCase(), { - canonical: full, - scoped, - namespace, - }); - } - } - } + // For paths with a slash, register the last segment + const lastSlashIndex = full.lastIndexOf("/") + if (lastSlashIndex !== -1) { + const lastSegment = full.slice(lastSlashIndex + 1) + // For CJK paths or when ignoreCase is enabled + if ( + ignoreCase + || /^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]+$/u.test( + lastSegment, + ) + ) { + // Register both the original case and lowercase versions + candidateMap.set(lastSegment, { + canonical: full, + scoped, + namespace, + }) + if (ignoreCase) { + candidateMap.set(lastSegment.toLowerCase(), { + canonical: full, + scoped, + namespace, + }) + } + } + } - // Register the short path if available - if (short) { - candidateMap.set(short, { - canonical: full, - scoped, - namespace, - }); - } - } + // Register the short path if available + if (short) { + candidateMap.set(short, { + canonical: full, + scoped, + namespace, + }) + } + } - // Register alias candidates. - for (const file of linkableFiles) { - if (file.aliases) { - // Determine shorthand candidate for the file if available. - let short: string | null = null; - if (basePrefix && file.path.startsWith(basePrefix)) { - short = file.path.slice(basePrefixLength); - } - for (const alias of file.aliases) { - // If alias equals the shorthand, use alias as canonical; otherwise use "full|alias". - const canonicalForAlias = - short && alias === short ? alias : `${file.path}|${alias}`; - if (!candidateMap.has(alias)) { - candidateMap.set(alias, { - canonical: canonicalForAlias, - scoped: file.scoped, - namespace: getTopLevelDirectoryName(file.path, baseDir), - }); - } - // Register lowercase version when ignoreCase is enabled - if (ignoreCase && !candidateMap.has(alias.toLowerCase())) { - candidateMap.set(alias.toLowerCase(), { - canonical: canonicalForAlias, - scoped: file.scoped, - namespace: getTopLevelDirectoryName(file.path, baseDir), - }); - } - } - } - } + // Register alias candidates. + for (const file of linkableFiles) { + if (file.aliases) { + // Determine shorthand candidate for the file if available. + let short: string | null = null + if (basePrefix && file.path.startsWith(basePrefix)) { + short = file.path.slice(basePrefixLength) + } + for (const alias of file.aliases) { + // If alias equals the shorthand, use alias as canonical; otherwise use "full|alias". + const canonicalForAlias + = short && alias === short ? alias : `${file.path}|${alias}` + if (!candidateMap.has(alias)) { + candidateMap.set(alias, { + canonical: canonicalForAlias, + scoped: file.scoped, + namespace: getTopLevelDirectoryName(file.path, baseDir), + }) + } + // Register lowercase version when ignoreCase is enabled + if (ignoreCase && !candidateMap.has(alias.toLowerCase())) { + candidateMap.set(alias.toLowerCase(), { + canonical: canonicalForAlias, + scoped: file.scoped, + namespace: getTopLevelDirectoryName(file.path, baseDir), + }) + } + } + } + } - // Build a trie from all candidate strings - const words = Array.from(candidateMap.keys()); - const trie = buildTrie(words, ignoreCase); + // Build a trie from all candidate strings + const words = Array.from(candidateMap.keys()) + const trie = buildTrie(words, ignoreCase) - return { candidateMap, trie }; -}; + return { candidateMap, trie } +} if (import.meta.vitest) { - const { it, expect, describe } = import.meta.vitest; + const { it, expect, describe } = import.meta.vitest - // Test for getTopLevelDirectoryName - describe("getTopLevelDirectoryName", () => { - it("should return the first directory after the baseDir", () => { - expect(getTopLevelDirectoryName("pages/docs/file", "pages")).toBe( - "docs", - ); - expect(getTopLevelDirectoryName("pages/home/readme", "pages")).toBe( - "home", - ); - }); + // Test for getTopLevelDirectoryName + describe("getTopLevelDirectoryName", () => { + it("should return the first directory after the baseDir", () => { + expect(getTopLevelDirectoryName("pages/docs/file", "pages")).toBe( + "docs", + ) + expect(getTopLevelDirectoryName("pages/home/readme", "pages")).toBe( + "home", + ) + }) - it("should return the first segment if baseDir is not found", () => { - expect(getTopLevelDirectoryName("docs/file")).toBe("docs"); - expect(getTopLevelDirectoryName("home/readme")).toBe("home"); - }); - }); + it("should return the first segment if baseDir is not found", () => { + expect(getTopLevelDirectoryName("docs/file")).toBe("docs") + expect(getTopLevelDirectoryName("home/readme")).toBe("home") + }) + }) - // Test for buildTrie - describe("buildTrie", () => { - it("should build a Trie with the given words", () => { - const words = ["hello", "world", "hi"]; - const trie = buildTrie(words); + // Test for buildTrie + describe("buildTrie", () => { + it("should build a Trie with the given words", () => { + const words = ["hello", "world", "hi"] + const trie = buildTrie(words) - expect(trie.children.has("h")).toBe(true); - expect(trie.children.has("w")).toBe(true); - expect(trie.children.get("h")?.children.has("e")).toBe(true); - expect(trie.children.get("h")?.children.get("i")?.candidate).toBe( - "hi", - ); - }); - }); + expect(trie.children.has("h")).toBe(true) + expect(trie.children.has("w")).toBe(true) + expect(trie.children.get("h")?.children.has("e")).toBe(true) + expect(trie.children.get("h")?.children.get("i")?.candidate).toBe( + "hi", + ) + }) + }) - // Test for buildCandidateTrie - describe("buildCandidateTrie", () => { - it("should build a candidate map and trie", () => { - const allFiles: PathAndAliases[] = [ - { - path: "pages/docs/readme", - scoped: false, - aliases: ["intro"], - }, - { - path: "pages/home/index", - scoped: false, - aliases: [], - }, - ]; + // Test for buildCandidateTrie + describe("buildCandidateTrie", () => { + it("should build a candidate map and trie", () => { + const allFiles: PathAndAliases[] = [ + { + path: "pages/docs/readme", + scoped: false, + aliases: ["intro"], + }, + { + path: "pages/home/index", + scoped: false, + aliases: [], + }, + ] - const { candidateMap, trie } = buildCandidateTrie( - allFiles, - "pages", - ); + const { candidateMap, trie } = buildCandidateTrie( + allFiles, + "pages", + ) - expect(candidateMap.has("pages/docs/readme")).toBe(true); - expect(candidateMap.has("docs/readme")).toBe(true); - expect(candidateMap.get("intro")?.canonical).toBe( - "pages/docs/readme|intro", - ); - expect(trie.children.has("d")).toBe(true); - expect(trie.children.has("h")).toBe(true); - }); - }); + expect(candidateMap.has("pages/docs/readme")).toBe(true) + expect(candidateMap.has("docs/readme")).toBe(true) + expect(candidateMap.get("intro")?.canonical).toBe( + "pages/docs/readme|intro", + ) + expect(trie.children.has("d")).toBe(true) + expect(trie.children.has("h")).toBe(true) + }) + }) } diff --git a/src/update-editor.ts b/src/update-editor.ts index dd06c53..5b4fc5e 100644 --- a/src/update-editor.ts +++ b/src/update-editor.ts @@ -1,56 +1,58 @@ -import { ChangeSpec } from "@codemirror/state"; -import { Editor } from "obsidian"; -import DiffMatchPatch from "diff-match-patch"; +import { ChangeSpec } from "@codemirror/state" +import { Editor } from "obsidian" +import DiffMatchPatch from "diff-match-patch" function endOfDocument(doc: string) { - const lines = doc.split("\n"); - return { line: lines.length - 1, ch: lines[lines.length - 1].length }; + const lines = doc.split("\n") + return { line: lines.length - 1, ch: lines[lines.length - 1].length } } export function updateEditor( - oldText: string, - newText: string, - editor: Editor, + oldText: string, + newText: string, + editor: Editor, ): DiffMatchPatch.Diff[] { - const dmp = new DiffMatchPatch.diff_match_patch(); - const changes = dmp.diff_main(oldText, newText); - let curText = ""; - changes.forEach((change) => { - const [type, value] = change; + const dmp = new DiffMatchPatch.diff_match_patch() + const changes = dmp.diff_main(oldText, newText) + let curText = "" + changes.forEach((change) => { + const [type, value] = change - if (type == DiffMatchPatch.DIFF_INSERT) { - // use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview - editor?.cm?.dispatch({ - changes: [ - { - from: editor.posToOffset(endOfDocument(curText)), - insert: value, - } as ChangeSpec, - ], - filter: false, - }); - curText += value; - } else if (type == DiffMatchPatch.DIFF_DELETE) { - const start = endOfDocument(curText); - let tempText = curText; - tempText += value; - const end = endOfDocument(tempText); + if (type == DiffMatchPatch.DIFF_INSERT) { + // use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview + editor?.cm?.dispatch({ + changes: [ + { + from: editor.posToOffset(endOfDocument(curText)), + insert: value, + } as ChangeSpec, + ], + filter: false, + }) + curText += value + } + else if (type == DiffMatchPatch.DIFF_DELETE) { + const start = endOfDocument(curText) + let tempText = curText + tempText += value + const end = endOfDocument(tempText) - // use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview - editor?.cm?.dispatch({ - changes: [ - { - from: editor.posToOffset(start), - to: editor.posToOffset(end), - insert: "", - } as ChangeSpec, - ], - filter: false, - }); - } else { - curText += value; - } - }); + // use codemirror dispatch in order to bypass the filter on transactions that causes editor.replaceRange not to not work in Live Preview + editor?.cm?.dispatch({ + changes: [ + { + from: editor.posToOffset(start), + to: editor.posToOffset(end), + insert: "", + } as ChangeSpec, + ], + filter: false, + }) + } + else { + curText += value + } + }) - return changes; + return changes } diff --git a/vitest.config.ts b/vitest.config.ts index 4b8ee40..6cdf52f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from "vitest/config"; +import { defineConfig } from "vitest/config" export default defineConfig({ - test: { - includeSource: ["src/**/*.{js,ts}"], - }, -}); + test: { + includeSource: ["src/**/*.{js,ts}"], + }, +})