Changes
8 changed files (+404/-12)
-
-
@@ -33,6 +33,9 @@ "https://deno.land/std@0.221.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e","https://deno.land/std@0.221.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", "https://deno.land/std@0.221.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270", "https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", "https://deno.land/std@0.221.0/front_matter/_formats.ts": "9a8ac1524f93b3ae093bd66864a49fc0088037920c6d60863da136d10f92e04d", "https://deno.land/std@0.221.0/front_matter/create_extractor.ts": "642e6e55cd07864b7c8068f88d271290d5d0a13d979ad335e10a7f52046b1f80", "https://deno.land/std@0.221.0/front_matter/yaml.ts": "103b8338bec480c6b7a7e245cf6bda72682eb78ed2231c799a4526d52cb6888a", "https://deno.land/std@0.221.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", "https://deno.land/std@0.221.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", "https://deno.land/std@0.221.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c",
-
@@ -109,6 +112,38 @@ "https://deno.land/std@0.221.0/path/windows/parse.ts": "dbdfe2bc6db482d755b5f63f7207cd019240fcac02ad2efa582adf67ff10553a","https://deno.land/std@0.221.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", "https://deno.land/std@0.221.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", "https://deno.land/std@0.221.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", "https://deno.land/std@0.221.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c" "https://deno.land/std@0.221.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", "https://deno.land/std@0.221.0/yaml/_error.ts": "f38cdebdb69cde16903d9aa2f3b8a3dd9d13e5f7f3570bf662bfaca69fef669e", "https://deno.land/std@0.221.0/yaml/_loader/loader.ts": "bf9e8a99770b59bc887b43ebccea108cbe9146ae32d91f7ce558d62c946d3fe3", "https://deno.land/std@0.221.0/yaml/_loader/loader_state.ts": "ee216de6040551940b85473c3185fdb7a6f3030b77153f87a6b7f63f82e489ea", "https://deno.land/std@0.221.0/yaml/_mark.ts": "61097a614857fcebf7b2ecad057916d74c90cd160117a33c9e74bac60457410a", "https://deno.land/std@0.221.0/yaml/_state.ts": "f3b1c1fd11860302f1f33e35e9ce089bf069d4943e8d67516cd6bedbba058c13", "https://deno.land/std@0.221.0/yaml/_type/binary.ts": "f1a6e1d83dcc52b21cc3639cd98be44051cfc54065cc4f2a42065bce07ebc07d", "https://deno.land/std@0.221.0/yaml/_type/bool.ts": "121743b23ba82a27ad6a3ec6298c7f5b0908f90e52707f8644a91f7ad51ed2ef", "https://deno.land/std@0.221.0/yaml/_type/float.ts": "c5ed84b0aec1ec5dc05f6abfaaff672e8890d4d44a42120b4445c9754fca4eba", "https://deno.land/std@0.221.0/yaml/_type/function.ts": "bbf705058942bf3370604b37eb77a10aadd72f986c237c9f69b43378a42202c1", "https://deno.land/std@0.221.0/yaml/_type/int.ts": "c2dc88438a60fccc8d2226042bd18b9967753adaf6bd145feb8b99d567e432ce", "https://deno.land/std@0.221.0/yaml/_type/map.ts": "ae2acb1cb837fb8e96c75c98611cfd45af847d0114ab5336333c318e7d4b12f4", "https://deno.land/std@0.221.0/yaml/_type/merge.ts": "ad0d971f91d2fb9f4ab3eba0c837eae357b1804d6b798adc99dc917bc5306b11", "https://deno.land/std@0.221.0/yaml/_type/mod.ts": "e8929d7b1c969a74f76338d4eb380ef8c4a26cd6441117d521f076b766e9c265", "https://deno.land/std@0.221.0/yaml/_type/nil.ts": "cbe4387d02d5933322c21b25d8955c5e6228c492e391a6fb82dcf4f498cc421c", "https://deno.land/std@0.221.0/yaml/_type/omap.ts": "cda915105ab22ba9e1d6317adacee8eec2d8ddaf864cc2f814e3e476946e72c6", "https://deno.land/std@0.221.0/yaml/_type/pairs.ts": "dd39bb44c1b9abaf6172c63f73350475933151f07e05253b81f7860c9b507177", "https://deno.land/std@0.221.0/yaml/_type/regexp.ts": "e49eb9e1c9356fd142bc15f7f323820d411fcc537b5ba3896df9a8b812d270a4", "https://deno.land/std@0.221.0/yaml/_type/seq.ts": "2deffc7f970869bc01a1541b4961d076329a1c2b30b95e07918f3132db7c3fe2", "https://deno.land/std@0.221.0/yaml/_type/set.ts": "be8a9e7237a7ffc92dfbe7f5e552d84b7eeba60f3f73cc77fc3c59d3506c74ea", "https://deno.land/std@0.221.0/yaml/_type/str.ts": "88f0a1ba12295520cd57e96cd78d53aa0787d53c7a1c506155f418c496c2f550", "https://deno.land/std@0.221.0/yaml/_type/timestamp.ts": "277a41a40fb93c3b2b3f5c373bf11b0b7856cc6a7b919e8ea130755e4029edc5", "https://deno.land/std@0.221.0/yaml/_type/undefined.ts": "9d215953c65740f1764e0bdca021007573473f0c49e087f00d9ff02817ecfc97", "https://deno.land/std@0.221.0/yaml/_utils.ts": "91bbe28b5e7000b9594e40ff5353f8fe7a7ba914eec917e1202cbaf5ac931c58", "https://deno.land/std@0.221.0/yaml/parse.ts": "f45278d9ebccb789af4eceeffa5c291e194bcf1fa9aab1b34ff52c2bd4a9d886", "https://deno.land/std@0.221.0/yaml/schema.ts": "a0f7956d997852b5d1c6564bd73eb7352175cfba439707ac819b65b5a2ec173a", "https://deno.land/std@0.221.0/yaml/schema/core.ts": "1222f9401e2a0c1d38e63d753da98be333e61a6032335e9c46a68bd45ecce85a", "https://deno.land/std@0.221.0/yaml/schema/default.ts": "b77c71cfd453951dd828e5f2f02f9f37335c9c0a49c8051d1a9653fa82357740", "https://deno.land/std@0.221.0/yaml/schema/extended.ts": "996da59626409047b5c1a2d68bdbeead43914cedede47c5923e80ae4febe7d24", "https://deno.land/std@0.221.0/yaml/schema/failsafe.ts": "24b2b630cef6fcce7de6d29db651523b0f49e5691d690931c42ecf4823837fdb", "https://deno.land/std@0.221.0/yaml/schema/json.ts": "0fb9268282d266c24d963e75ef77f51accbbb74f40713a99e83ad621a81bc9ae", "https://deno.land/std@0.221.0/yaml/schema/mod.ts": "9bf7ff80c2a246f781bdcab979211d0389760831a974cf5883bf2016567e3507", "https://deno.land/std@0.221.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c" } }
-
-
-
@@ -0,0 +1,5 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 export * from "https://deno.land/std@0.221.0/front_matter/yaml.ts";
-
-
-
@@ -5,8 +5,7 @@import { assertEquals, assertExists, assertObjectMatch, } from "../../deps/deno.land/std/assert/mod.ts"; } from "../deps/deno.land/std/assert/mod.ts"; import { DenoFsReader } from "./deno_fs.ts"; import type { DirectoryReader } from "./interface.ts";
-
@@ -24,13 +23,11 @@const root = await reader.getRootDirectory(); const rootEntries = await root.read(); assertEquals(rootEntries.length, 1); assertObjectMatch(rootEntries[0], { type: "directory", name: "filesystem_reader", }); const thisDirectory = rootEntries[0] as DirectoryReader; const thisDirectory = rootEntries.find((entry) => entry.name === "filesystem_reader" ) as DirectoryReader; assertExists(thisDirectory); assertEquals(thisDirectory.type, "directory"); const thisDirectoryContents = await thisDirectory.read(); const thisFile = thisDirectoryContents.find((entry) =>
-
-
-
@@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>// // SPDX-License-Identifier: Apache-2.0 import { SEPARATOR } from "../../deps/deno.land/std/path/mod.ts"; import { SEPARATOR } from "../deps/deno.land/std/path/mod.ts"; import type { DirectoryReader,
-
-
-
@@ -5,7 +5,7 @@import { assertEquals, assertObjectMatch, } from "../../deps/deno.land/std/assert/mod.ts"; } from "../deps/deno.land/std/assert/mod.ts"; import { MemoryFsReader } from "./memory_fs.ts"; import type { DirectoryReader, FileReader } from "./interface.ts";
-
-
-
@@ -0,0 +1,50 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import type { DirectoryReader, FileReader, } from "../filesystem_reader/interface.ts"; export interface DocumentMetadata { /** * An identifier for a document, unique among a directory the document belongs to. * A *document name* appears in a generated URL, thus available characters are limited to: * * - Alphabet (`a-z`, `A-Z`) * - Digit (`0-9`) * - Percent symbol (`%`) * - Hyphen (`-`) * - Dot (`.`) * - Underscore (`_`) * - Tilde (`~`) */ readonly name: string; /** * Human-readable text representing a title of the *document*. * Although there is no restriction on available characters, you should avoid using * control characters. * ([Unicode control characters - Wikipedia](https://en.wikipedia.org/wiki/Unicode_control_characters)) */ readonly title: string; } /** * Skip this parser. * If no parsers left, do not include the file in a document tree. */ export interface Skip { skip: true; } export interface MetadataParser { /** * Parses a file or directory then returns metadata for the file or directory. * Throws when the file or directory does not meet the expectation. */ parse( fileOrDirectory: FileReader | DirectoryReader, ): Promise<DocumentMetadata | Skip>; }
-
-
-
@@ -0,0 +1,197 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { assertObjectMatch } from "../deps/deno.land/std/assert/mod.ts"; import { MemoryFsReader } from "../filesystem_reader/memory_fs.ts"; import { VaultParser } from "./vault_parser.ts"; Deno.test("Should use filename as title", async () => { const fs = new MemoryFsReader([ { path: "foo.md", content: "", }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch(await new VaultParser().parse(file), { name: "foo", title: "foo", }); }); Deno.test("Should use directory name as title", async () => { const fs = new MemoryFsReader([ { path: "bar/foo.md", content: "", }, ]); const root = await fs.getRootDirectory(); const [dir] = await root.read(); assertObjectMatch(await new VaultParser().parse(dir), { name: "bar", title: "bar", }); }); Deno.test("Should lowercase filename for name", async () => { const fs = new MemoryFsReader([ { path: "Foo.md", content: "", }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch(await new VaultParser().parse(file), { name: "foo", title: "Foo", }); }); Deno.test("Should encode to URI-safe name", async () => { const fs = new MemoryFsReader([ { path: "My Awesome Document, Progress 75%.md", content: "", }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch(await new VaultParser().parse(file), { name: "my%20awesome%20document%2C%20progress%2075%25", title: "My Awesome Document, Progress 75%", }); }); Deno.test("Should parse canvas file", async () => { const fs = new MemoryFsReader([ { path: "foo.canvas", content: "", }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch(await new VaultParser().parse(file), { name: "foo", title: "foo", }); }); Deno.test("Should skip files other than note and canvas", async () => { const fs = new MemoryFsReader([ { path: "main.tsx", content: "", }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch(await new VaultParser().parse(file), { skip: true }); }); Deno.test("Should use name defined in YAML frontmatter", async () => { const fs = new MemoryFsReader([ { path: "Foo Bar.md", content: `--- name: foo-bar ---`, }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch( await new VaultParser({ readFrontMatter: true }).parse(file), { name: "foo-bar", title: "Foo Bar", }, ); }); Deno.test("Should use title defined in YAML frontmatter", async () => { const fs = new MemoryFsReader([ { path: "Foo Bar.md", content: `--- title: Baz ---`, }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch( await new VaultParser({ readFrontMatter: true }).parse(file), { name: "foo%20bar", title: "Baz", }, ); }); Deno.test("Should use both name and title defined in YAML frontmatter", async () => { const fs = new MemoryFsReader([ { path: "Foo Bar.md", content: `--- name: foo-bar title: Baz ---`, }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch( await new VaultParser({ readFrontMatter: true }).parse(file), { name: "foo-bar", title: "Baz", }, ); }); Deno.test("Should not read frontmatter if the flag is not on", async () => { const fs = new MemoryFsReader([ { path: "Foo Bar.md", content: `--- name: foo-bar title: Baz ---`, }, ]); const root = await fs.getRootDirectory(); const [file] = await root.read(); assertObjectMatch( await new VaultParser().parse(file), { name: "foo%20bar", title: "Foo Bar", }, ); });
-
-
-
@@ -0,0 +1,108 @@// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com> // // SPDX-License-Identifier: Apache-2.0 import { extname } from "../deps/deno.land/std/path/mod.ts"; import * as yamlFrontmatter from "../deps/deno.land/std/front_matter/yaml.ts"; import type { DirectoryReader, FileReader, } from "../filesystem_reader/interface.ts"; import type { DocumentMetadata, MetadataParser, Skip } from "./interface.ts"; function escapeNodeName(nodeName: string): string { return encodeURIComponent(nodeName.toLowerCase()); } export interface VaultParserOptions { /** * Whether to read YAML frontmatter of notes. * When enabled, * * - Use `name` property for document name if defined. * - Use `title` property for document title if defined. * * This flag is off by-default for performance reasons. */ readFrontMatter?: boolean; } /** * A parser for Obsidian Vault. * * By default, this parser uses file and directory name as document title * and lowercased escaped one as document name. */ export class VaultParser implements MetadataParser { #readFrontMatter: boolean; constructor({ readFrontMatter = false }: VaultParserOptions = {}) { this.#readFrontMatter = readFrontMatter; } async parse( node: FileReader | DirectoryReader, ): Promise<DocumentMetadata | Skip> { if (node.type === "directory") { return { name: escapeNodeName(node.name), title: node.name, }; } const ext = extname(node.name); const basename = ext ? node.name.slice(0, -ext.length) : node.name; switch (ext) { case ".md": { const fromFileName: DocumentMetadata = { name: escapeNodeName(basename), title: basename, }; if (this.#readFrontMatter) { const parsed = await this.#parseFrontMatter(node); return { name: parsed.name || fromFileName.name, title: parsed.title || fromFileName.title, }; } return fromFileName; } case ".canvas": { return { name: escapeNodeName(basename), title: basename, }; } // Not an Obsidian document. default: { return { skip: true, }; } } } async #parseFrontMatter( file: FileReader, ): Promise<Partial<DocumentMetadata>> { const markdown = new TextDecoder().decode(await file.read()); // Obsidian currently supports YAML frontmatter only. const frontmatter = yamlFrontmatter.extract(markdown); const name = ("name" in frontmatter.attrs && typeof frontmatter.attrs.name === "string" && frontmatter.attrs.name) || undefined; const title = ("title" in frontmatter.attrs && typeof frontmatter.attrs.title === "string" && frontmatter.attrs.title) || undefined; return { name, title }; } }
-