Parent directory

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}