mirror of
https://github.com/kdnk/obsidian-automatic-linker.git
synced 2026-07-22 05:37:46 +00:00
chore: add stylistic eslint plugin and format code
This commit is contained in:
parent
7b4f42dafe
commit
2943d80742
53 changed files with 8430 additions and 8371 deletions
|
|
@ -1 +0,0 @@
|
|||
{}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
],
|
||||
},
|
||||
])
|
||||
|
|
|
|||
36
obsidian-ext.d.ts
vendored
36
obsidian-ext.d.ts
vendored
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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("")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,35 +2,35 @@
|
|||
* Check if the file has the "off" frontmatter property
|
||||
*/
|
||||
export const isLinkingOff = (
|
||||
frontmatter: Record<string, unknown> | undefined,
|
||||
frontmatter: Record<string, unknown> | 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<string, unknown> | undefined,
|
||||
frontmatter: Record<string, unknown> | 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<string, unknown> | undefined,
|
||||
frontmatter: Record<string, unknown> | 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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
1061
src/main.ts
1061
src/main.ts
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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]]",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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]]")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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/関係性の勇者|関係性の勇者]] は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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```")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]]")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)]]",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
// Second call should be faster or similar (cache benefit)
|
||||
expect(duration2).toBeLessThanOrEqual(duration1 * 1.2) // Allow 20% variance
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]]")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]]")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]] | |
|
||||
`);
|
||||
});
|
||||
});
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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]]",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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)",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,114 +1,115 @@
|
|||
type Url = string;
|
||||
type Title = string;
|
||||
type Url = string
|
||||
type Title = string
|
||||
interface ReplaceUrlWithTitleOptions {
|
||||
body: string;
|
||||
urlTitleMap: Map<Url, Title>;
|
||||
body: string
|
||||
urlTitleMap: Map<Url, Title>
|
||||
}
|
||||
|
||||
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 `(?<!\\)` ensures we don't count escaped ones like \`
|
||||
const backticksCount = (segmentBefore.match(/(?<!\\)`/g) || [])
|
||||
.length;
|
||||
// 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 `(?<!\\)` 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("`")
|
||||
) {
|
||||
shouldReplace = 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("`")
|
||||
) {
|
||||
shouldReplace = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check if inside fenced code block: ``` ... url ... ```
|
||||
// This check is complex and not implemented here for simplicity.
|
||||
// Assumes URLs within fenced code blocks should not be replaced by default.
|
||||
// A simple heuristic could check the lines around the match for ```,
|
||||
// but a proper parser state would be needed for accuracy.
|
||||
// 3. Check if inside fenced code block: ``` ... url ... ```
|
||||
// This check is complex and not implemented here for simplicity.
|
||||
// Assumes URLs within fenced code blocks should not be replaced by default.
|
||||
// A simple heuristic could check the lines around the match for ```,
|
||||
// but a proper parser state would be needed for accuracy.
|
||||
|
||||
// --- Apply Replacement ---
|
||||
if (shouldReplace) {
|
||||
newBodyParts.push(markdownLink);
|
||||
} else {
|
||||
// Keep the original URL if context checks failed
|
||||
newBodyParts.push(url);
|
||||
}
|
||||
// --- Apply Replacement ---
|
||||
if (shouldReplace) {
|
||||
newBodyParts.push(markdownLink)
|
||||
}
|
||||
else {
|
||||
// Keep the original URL if context checks failed
|
||||
newBodyParts.push(url)
|
||||
}
|
||||
|
||||
// Move index past the current match (either the original url or the replaced markdownLink)
|
||||
// Use url.length because that's what we searched for.
|
||||
currentIndex = nextOccurrence + url.length;
|
||||
}
|
||||
// Update resultBody for the next URL in the outer loop
|
||||
resultBody = newBodyParts.join("");
|
||||
}
|
||||
// Move index past the current match (either the original url or the replaced markdownLink)
|
||||
// Use url.length because that's what we searched for.
|
||||
currentIndex = nextOccurrence + url.length
|
||||
}
|
||||
// Update resultBody for the next URL in the outer loop
|
||||
resultBody = newBodyParts.join("")
|
||||
}
|
||||
|
||||
return resultBody;
|
||||
};
|
||||
return resultBody
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { getTitleFromHtml } from "../get-title-from-html";
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { getTitleFromHtml } from "../get-title-from-html"
|
||||
|
||||
describe("getTitleFromHtml", () => {
|
||||
it("should extract title from HTML", () => {
|
||||
const html = "<html><head><title>Example Title</title></head></html>";
|
||||
const result = getTitleFromHtml(html);
|
||||
expect(result).toBe("Example Title");
|
||||
});
|
||||
it("should extract title from HTML", () => {
|
||||
const html = "<html><head><title>Example Title</title></head></html>"
|
||||
const result = getTitleFromHtml(html)
|
||||
expect(result).toBe("Example Title")
|
||||
})
|
||||
|
||||
it("should handle empty title tag", () => {
|
||||
const html = "<html><head><title></title></head></html>";
|
||||
const result = getTitleFromHtml(html);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
it("should handle empty title tag", () => {
|
||||
const html = "<html><head><title></title></head></html>"
|
||||
const result = getTitleFromHtml(html)
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should handle no title tag", () => {
|
||||
const html = "<html><head></head></html>";
|
||||
const result = getTitleFromHtml(html);
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
it("should handle no title tag", () => {
|
||||
const html = "<html><head></head></html>"
|
||||
const result = getTitleFromHtml(html)
|
||||
expect(result).toBe("")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: <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: <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 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")
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
// It handles potential attributes within the title tag (though unlikely)
|
||||
// and captures the content between <title...> and </title>
|
||||
// Case-insensitive matching for <title> 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 ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
400
src/trie.ts
400
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}"],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue