chore: add stylistic eslint plugin and format code

This commit is contained in:
Kodai Nakamura 2025-12-20 20:40:18 +09:00
parent 7b4f42dafe
commit 2943d80742
53 changed files with 8430 additions and 8371 deletions

View file

@ -1 +0,0 @@
{}

View file

@ -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()
}

View file

@ -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
View file

@ -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
}
}

View file

@ -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",

View file

@ -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

View file

@ -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")
})
})

View file

@ -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("")
}

View file

@ -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
)
}

File diff suppressed because it is too large Load diff

View file

@ -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
}

View file

@ -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)
})
})

View file

@ -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")
})
})

View file

@ -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

View file

@ -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

View file

@ -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]]")
})
})
})

View file

@ -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)
})
})
})

View file

@ -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/関係性の勇者|関係性の勇者]] は自分の鎧について深く学びそれを脱ぎ去る勇気を持っている",
)
})
})
})

View file

@ -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```")
})
})

View file

@ -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]]")
})
})

View file

@ -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)
}
})
})

View file

@ -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)]]",
)
})
})
})

View file

@ -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")
})
})
})

View file

@ -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
})
})
})

View file

@ -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]]")
})
})

View file

@ -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]]")
})
})
})

View file

@ -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)
})
})
})

View file

@ -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")
})
})
})

View file

@ -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]] | |
`);
});
});
`)
})
})

View file

@ -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]]",
)
})
})

View file

@ -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

View file

@ -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)",
)
})
})

View file

@ -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
}

View file

@ -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("")
})
})

View file

@ -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")
})
})

View file

@ -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 ""
}

View file

@ -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
}

View file

@ -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)
})
})
})

View file

@ -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)
})
})
})

View file

@ -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)
})
})
})

View file

@ -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,
)
})
})

View file

@ -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
}

View file

@ -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
})
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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)
})
})
}
}

View file

@ -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)
})
})
}

View file

@ -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
}

View file

@ -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}"],
},
})