parser.ts
2398 bytes
1import type { FrontmatterDocument } from "../types.ts";
2
3export interface FrontmatterCodec {
4 parse(raw: string): FrontmatterDocument;
5}
6
7export class SimpleFrontmatterCodec implements FrontmatterCodec {
8 parse(raw: string): FrontmatterDocument {
9 const lineEnding: "\n" | "\r\n" = raw.includes("\r\n") ? "\r\n" : "\n";
10 const opening = raw.match(/^---[ \t]*(\r?\n)/);
11 if (!opening) {
12 return {
13 raw,
14 hasFrontmatter: false,
15 frontmatterStart: 0,
16 frontmatterEnd: 0,
17 contentStart: 0,
18 frontmatterText: "",
19 bodyText: raw,
20 fields: {},
21 lineEnding,
22 };
23 }
24
25 const frontmatterStart = opening[0].length;
26 const rest = raw.slice(frontmatterStart);
27 const closing = /^---[ \t]*(?:\r?\n|$)/m.exec(rest);
28 if (!closing || closing.index === undefined) {
29 return {
30 raw,
31 hasFrontmatter: false,
32 frontmatterStart: 0,
33 frontmatterEnd: 0,
34 contentStart: 0,
35 frontmatterText: "",
36 bodyText: raw,
37 fields: {},
38 lineEnding,
39 };
40 }
41
42 const frontmatterEnd = frontmatterStart + closing.index;
43 const contentStart = frontmatterEnd + closing[0].length;
44 const frontmatterText = raw.slice(frontmatterStart, frontmatterEnd);
45
46 return {
47 raw,
48 hasFrontmatter: true,
49 frontmatterStart,
50 frontmatterEnd,
51 contentStart,
52 frontmatterText,
53 bodyText: raw.slice(contentStart),
54 fields: parseYamlLikeFields(frontmatterText),
55 lineEnding,
56 };
57 }
58}
59
60function parseYamlLikeFields(frontmatterText: string): Record<string, unknown> {
61 const fields: Record<string, unknown> = {};
62 for (const rawLine of frontmatterText.split(/\r?\n/)) {
63 const line = rawLine.trim();
64 if (!line || line.startsWith("#")) continue;
65 const match = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(rawLine);
66 if (!match) continue;
67 const key = match[1];
68 if (!key) continue;
69 fields[key] = parseScalar(match[2] ?? "");
70 }
71 return fields;
72}
73
74function parseScalar(raw: string): unknown {
75 const value = raw.trim();
76 if (value === "true") return true;
77 if (value === "false") return false;
78 if (value === "null" || value === "~") return null;
79 if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
80 return value.slice(1, -1);
81 }
82 return value;
83}