-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfrontmatter.ts
More file actions
65 lines (60 loc) · 2.55 KB
/
Copy pathfrontmatter.ts
File metadata and controls
65 lines (60 loc) · 2.55 KB
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
/**
* Frontmatter splitting and parsing — Verifiable OKF.
*
* Splits an OKF concept (YAML frontmatter + markdown body) at the closing
* `---` delimiter (SPEC §5 step 1), and parses the frontmatter into the
* JSON data model used for JCS (SPEC §6): plain objects / arrays / strings /
* numbers / booleans / null. Timestamps remain RFC 3339 strings.
*/
import YAML from "yaml";
import { decodeUtf8 } from "./body.js";
export interface SplitConcept {
/** Whether a leading `---` frontmatter block was found at offset 0. */
readonly hasFrontmatter: boolean;
/** Raw YAML text between the delimiters (empty if no frontmatter). */
readonly frontmatterText: string;
/** Body string: everything after the closing `---` delimiter and its newline. */
readonly body: string;
}
// Leading `---` line, frontmatter (non-greedy), closing `---` line + its newline (or EOF).
const FRONTMATTER_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/u;
/** Split a concept into frontmatter text and body. Validates UTF-8 for byte input. */
export function splitConcept(input: Uint8Array | string): SplitConcept {
const text = typeof input === "string" ? input : decodeUtf8(input);
const m = FRONTMATTER_RE.exec(text);
if (!m || m.index !== 0) {
return { hasFrontmatter: false, frontmatterText: "", body: text };
}
return {
hasFrontmatter: true,
frontmatterText: m[1] ?? "",
body: text.slice(m[0].length),
};
}
/** Recursively coerce values into the JSON data model (Date → RFC 3339 UTC `Z`). */
function toJsonModel(value: unknown): unknown {
if (value instanceof Date) return value.toISOString().replace(/\.\d{3}Z$/u, "Z");
if (Array.isArray(value)) return value.map(toJsonModel);
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
out[k] = toJsonModel(v);
}
return out;
}
return value;
}
/**
* Parse frontmatter YAML into the JSON data model. Uses YAML's `core` schema so
* ISO timestamps stay strings (not Date), then defensively coerces any Date.
* Returns `{}` for empty frontmatter.
*/
export function parseFrontmatter(frontmatterText: string): Record<string, unknown> {
if (frontmatterText.trim() === "") return {};
const parsed = YAML.parse(frontmatterText, { schema: "core" }) as unknown;
const model = toJsonModel(parsed);
if (model === null || typeof model !== "object" || Array.isArray(model)) {
throw new TypeError("frontmatter must be a YAML mapping");
}
return model as Record<string, unknown>;
}