-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
// 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());
}
function getFrontMatterValue(
frontmatter: Record<string, unknown>,
key: string,
): string | undefined {
if (!(key in frontmatter)) {
return undefined;
}
const value = frontmatter[key];
if (typeof value !== "string") {
return undefined;
}
return value;
}
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.
* - Use `lang` property or `language` property as a document language if defined.
*
* This flag is off by-default for performance reasons.
*/
readFrontMatter?: boolean;
/**
* A optional function to determine the language of a directory or a file.
* Macana does not check whether the resulted language tag is valid.
* @returns If this function returned a falsy value, Macana treat the directory or the file
* has no specified language. If this function returned `true`, Macana uses the file
* name or directory name as a language. If this function returned string, Macana
* uses the string as a language.
*/
language?(
node: FileReader | DirectoryReader,
): string | true | null | undefined | false;
}
/**
* 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;
#language: VaultParserOptions["language"];
constructor({ readFrontMatter = false, language }: VaultParserOptions = {}) {
this.#readFrontMatter = readFrontMatter;
this.#language = language;
}
async parse(
node: FileReader | DirectoryReader,
): Promise<DocumentMetadata | Skip> {
if (node.type === "directory") {
return {
name: escapeNodeName(node.name),
title: node.name,
language: this.#getLanguage(node),
};
}
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,
language: this.#getLanguage(node),
};
if (this.#readFrontMatter) {
const parsed = await this.#parseFrontMatter(node);
return {
name: parsed.name || fromFileName.name,
title: parsed.title || fromFileName.title,
language: parsed.language || fromFileName.language,
};
}
return fromFileName;
}
case ".canvas": {
return {
name: escapeNodeName(basename),
title: basename,
language: this.#getLanguage(node),
};
}
// Not an Obsidian document.
default: {
return {
skip: true,
};
}
}
}
#getLanguage(node: FileReader | DirectoryReader): string | undefined {
if (!this.#language) {
return undefined;
}
const result = this.#language(node);
if (!result) {
return undefined;
}
if (typeof result === "string") {
return result;
}
return node.name;
}
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 = getFrontMatterValue(frontmatter.attrs, "name");
const title = getFrontMatterValue(frontmatter.attrs, "title");
const language = getFrontMatterValue(frontmatter.attrs, "lang") ||
getFrontMatterValue(frontmatter.attrs, "language");
return { name, title, language };
}
}