Changes
5 changed files (+198/-15)
-
-
@@ -45,6 +45,7 @@ en: "English",ja: "日本語", }, true), ], resolveShortestPathWhenPossible: true, }); const contentParser = oneof( new JSONCanvasParser(),
-
-
-
@@ -11,12 +11,12 @@ - [ ] Diagrams (Mermaid) ... Maybe no support, as Mermaid's overall quality is questionable.- [ ] MathJax ... No support if it can't generate MathML at build time. - [ ] Syntax Highlighting - [ ] Obsidian Extensions - [ ] Internal Link path resolution - [x] Internal Link path resolution - [x] Absolute path in vault - [x] Absolute path in vault (extension-less) - [x] Relative path to file - [x] Relative path to file (extension-less) - [ ] Shortest path when possible - [x] Shortest path when possible - [ ] Wikilink - [ ] Label - [ ] Heading
-
-
-
@@ -41,6 +41,17 @@ ```[Overview](/en/Overview) ### Shortest path when possible Macana also supports "Shortest path when possible" links, which is the default value for the newer Obsidian versions. When Vault has more than one file having same name, resolution will result in error. ```markdown [Overview](Overview) ``` [Overview](Overview) ## Limitation ### Filename confusion on extension-less path
-
-
-
@@ -371,6 +371,93 @@ path: ["Qux.md"],}); }); Deno.test(`Should support "Shortest path when possible" resolution`, async () => { const fileSystemReader = new MemoryFsReader([ { path: "Foo/Bar/Baz.md", content: "" }, { path: "Qux.md", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en", strategies: [fileExtensions([".md"])], resolveShortestPathWhenPossible: true, }); const contentParser = linkCheckParser({ fromPath: ["Foo", "Bar", "Baz.md"], toPath: ["Qux"], }); const tree = await builder.build({ fileSystemReader, contentParser, }); assertNotEquals(contentParser.token, null); // `as` is required as Deno's assertion function does not return `a is B`. const found = tree.exchangeToken(contentParser.token as DocumentToken); assertObjectMatch(found, { path: ["Qux.md"], }); }); Deno.test("Shortest path resolution look down directories as-well", async () => { const fileSystemReader = new MemoryFsReader([ { path: "Foo/Bar/Baz.md", content: "" }, { path: "Qux.md", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en", strategies: [fileExtensions([".md"])], resolveShortestPathWhenPossible: true, }); const contentParser = linkCheckParser({ fromPath: ["Qux.md"], toPath: ["Baz"], }); const tree = await builder.build({ fileSystemReader, contentParser, }); assertNotEquals(contentParser.token, null); // `as` is required as Deno's assertion function does not return `a is B`. const found = tree.exchangeToken(contentParser.token as DocumentToken); assertObjectMatch(found, { path: ["Foo", "Bar", "Baz.md"], }); }); Deno.test("Shortest path resolution rejects when there more than one files with same name", async () => { const fileSystemReader = new MemoryFsReader([ { path: "Foo/Bar/Baz.md", content: "" }, { path: "Foo/Baz.md", content: "" }, { path: "Qux.md", content: "" }, ]); const builder = new DefaultTreeBuilder({ defaultLanguage: "en", strategies: [fileExtensions([".md"])], resolveShortestPathWhenPossible: true, }); const contentParser = linkCheckParser({ fromPath: ["Qux.md"], toPath: ["Baz"], }); await assertRejects(() => builder.build({ fileSystemReader, contentParser, }) ); }); Deno.test("ignore() and ignoreDotfiles() should ignore files and directories", async () => { const fileSystemReader = new MemoryFsReader([ { path: "foo/bar/baz.md", content: "" },
-
-
-
@@ -192,6 +192,16 @@ return buf;} /** * Trim file extension from filename. * * - "foo.png" -> "foo" * - "foo.config.js" -> "foo.config" */ function getStem(filename: string): string { return filename.split(".").slice(0, -1).join("."); } /** * @param root - Vault root directory. * @param path - Resolved path. */
-
@@ -214,8 +224,7 @@ if (entry.type !== "file") {return false; } const stem = entry.name.split(".").slice(0, -1).join("."); return stem === filename; return getStem(entry.name) === filename; }); if (match.length > 1) {
-
@@ -254,6 +263,67 @@return findClosestFile(dir); } async function findFileByName( name: string, dir: DirectoryReader | RootDirectoryReader, ): Promise<FileReader[]> { const found: FileReader[] = []; for (const entry of await dir.read()) { if (entry.type === "directory") { found.push(...(await findFileByName(name, entry))); continue; } if (getStem(entry.name) === name) { found.push(entry); continue; } } return found; } // Based on: https://forum.obsidian.md/t/settings-new-link-format-what-is-shortest-path-when-possible/6748 function resolveShortestPath( root: RootDirectoryReader, path: readonly string[], base: readonly string[], ): readonly string[] | Promise<readonly string[]> { const [name] = path; switch (name) { case "": case ".": case "..": return resolveExtensionLessPath(root, resolveFsrPath(path, base)); } // Absolute path from Vault root if (path.length > 1) { return resolveExtensionLessPath(root, path); } return findFileByName(name, root).then((found) => { if (!found.length) { throw new Error( `DefaultTreeBuilder: no file named "${name}" found,` + ` requested by ${base.join(INTERNAL_PATH_SEPARATOR)}.`, ); } if (found.length > 1) { throw new Error( `DefaultTreeBuilder: Your Vault has more than one files named "${name}": ` + found.map((entry) => entry.path.join(INTERNAL_PATH_SEPARATOR)).join( ", ", ), ); } return found[0].path; }); } interface InternalBuildParameters { contentParser: BuildParameters["contentParser"];
-
@@ -287,6 +357,13 @@ sorter?(a: Document | DocumentDirectory, b: Document | DocumentDirectory, ): number; /** * Whether to enable "Shortest path when possible" link resolution. * This impacts performance. * @default false */ resolveShortestPathWhenPossible?: boolean; } export class DefaultTreeBuilder implements TreeBuilder {
-
@@ -296,9 +373,11 @@ #sorter: (a: Document | DocumentDirectory, b: Document | DocumentDirectory, ) => number; #resolveShortestPath: boolean; constructor( { defaultLanguage, strategies, sorter }: DefaultTreeBuilderConfig, { defaultLanguage, strategies, sorter, resolveShortestPathWhenPossible }: DefaultTreeBuilderConfig, ) { this.#defaultLanguage = defaultLanguage; this.#strategies = strategies || [];
-
@@ -308,6 +387,7 @@ a.metadata.title.localeCompare(b.metadata.title, this.#defaultLanguage, )); this.#resolveShortestPath = resolveShortestPathWhenPossible ?? false; } async build(
-
@@ -414,7 +494,7 @@ if (node.type === "file") {const result = await contentParser.parse({ fileReader: node, documentMetadata: metadata, async getAssetToken(path) { getAssetToken: async (path) => { if (!path.length) { throw new Error( `Asset link cannot be empty (processing ${
-
@@ -426,10 +506,12 @@const id = crypto.randomUUID(); const token: AssetToken = `mxa_${id}`; const resolvedPath = await resolveExtensionLessPath( root, resolveFsrPath(path, node.path), ); const resolvedPath = this.#resolveShortestPath ? await resolveShortestPath(root, path, node.path) : await resolveExtensionLessPath( root, resolveFsrPath(path, node.path), ); assetTokensToFiles.set( token,
-
@@ -438,7 +520,7 @@ );return token; }, async getDocumentToken(path) { getDocumentToken: async (path) => { if (!path.length) { throw new Error( `Document link cannot be empty (processing ${
-
@@ -450,10 +532,12 @@const id = crypto.randomUUID(); const token: DocumentToken = `mxt_${id}`; const resolvedPath = await resolveExtensionLessPath( root, resolveFsrPath(path, node.path), ); const resolvedPath = this.#resolveShortestPath ? await resolveShortestPath(root, path, node.path) : await resolveExtensionLessPath( root, resolveFsrPath(path, node.path), ); documentTokenToPaths.set( token,
-