this repo has no description
1import { describe, expect, it } from "bun:test";
2import { parseFrontmatter } from "../src/lib/markdown";
3
4describe("parseFrontmatter", () => {
5 describe("delimiters", () => {
6 it("parses YAML frontmatter (--- delimiter)", () => {
7 const content = `---
8title: Hello World
9---
10Body content here.`;
11 const { frontmatter, body } = parseFrontmatter(content);
12 expect(frontmatter.title).toBe("Hello World");
13 expect(body).toBe("Body content here.");
14 });
15
16 it("parses TOML frontmatter (+++ delimiter)", () => {
17 const content = `+++
18title = "Hugo Post"
19+++
20Body content here.`;
21 const { frontmatter, body } = parseFrontmatter(content);
22 expect(frontmatter.title).toBe("Hugo Post");
23 expect(body).toBe("Body content here.");
24 });
25
26 it("parses alternative frontmatter (*** delimiter)", () => {
27 const content = `***
28title: Alt Post
29***
30Body content here.`;
31 const { frontmatter, body } = parseFrontmatter(content);
32 expect(frontmatter.title).toBe("Alt Post");
33 expect(body).toBe("Body content here.");
34 });
35
36 it("returns defaults when no frontmatter is present", () => {
37 const content = "Just plain content with no frontmatter.";
38 const { frontmatter, body } = parseFrontmatter(content);
39 expect(frontmatter.title).toBe("");
40 expect(body).toBe(content);
41 });
42 });
43
44 describe("scalar values", () => {
45 it("parses a string value", () => {
46 const content = `---
47title: My Post
48description: A short description
49---
50`;
51 const { frontmatter } = parseFrontmatter(content);
52 expect(frontmatter.title).toBe("My Post");
53 expect(frontmatter.description).toBe("A short description");
54 });
55
56 it("strips double quotes from values", () => {
57 const content = `---
58title: "Quoted Title"
59---
60`;
61 const { frontmatter } = parseFrontmatter(content);
62 expect(frontmatter.title).toBe("Quoted Title");
63 });
64
65 it("strips single quotes from values", () => {
66 const content = `---
67title: 'Single Quoted'
68---
69`;
70 const { frontmatter } = parseFrontmatter(content);
71 expect(frontmatter.title).toBe("Single Quoted");
72 });
73
74 it("parses YAML folded multiline string", () => {
75 const content = `---
76excerpt: >
77 This is a folded
78 multiline string
79---
80`;
81 const { rawFrontmatter } = parseFrontmatter(content);
82 expect(rawFrontmatter.excerpt).toBe(
83 "This is a folded multiline string\n",
84 );
85 });
86
87 it("parses YAML stripped folded multiline string", () => {
88 const content = `---
89excerpt: >-
90 This is a stripped folded
91 multiline string
92---
93`;
94 const { rawFrontmatter } = parseFrontmatter(content);
95 expect(rawFrontmatter.excerpt).toBe(
96 "This is a stripped folded multiline string",
97 );
98 });
99
100 it("parses YAML literal multiline string", () => {
101 const content = `---
102excerpt: |
103 This is a literal
104 multiline string
105---
106`;
107 const { rawFrontmatter } = parseFrontmatter(content);
108 expect(rawFrontmatter.excerpt).toBe(
109 "This is a literal\nmultiline string\n",
110 );
111 });
112
113 it("parses YAML kept literal multiline string", () => {
114 const content = `---
115excerpt: |+
116 This is a kept literal
117 multiline string
118
119end: true
120---
121`;
122 const { rawFrontmatter } = parseFrontmatter(content);
123 expect(rawFrontmatter.excerpt).toBe(
124 "This is a kept literal\nmultiline string\n\n",
125 );
126 });
127
128 it("parses boolean true", () => {
129 const content = `---
130draft: true
131---
132`;
133 const { frontmatter } = parseFrontmatter(content);
134 expect(frontmatter.draft).toBe(true);
135 });
136
137 it("parses boolean false", () => {
138 const content = `---
139draft: false
140---
141`;
142 const { frontmatter } = parseFrontmatter(content);
143 expect(frontmatter.draft).toBe(false);
144 });
145
146 it('parses string "true" in draft field as boolean true', () => {
147 const content = `---
148draft: true
149---
150`;
151 const { rawFrontmatter } = parseFrontmatter(content);
152 expect(rawFrontmatter.draft).toBe(true);
153 });
154 });
155
156 describe("arrays", () => {
157 it("parses inline YAML arrays", () => {
158 const content = `---
159tags: [typescript, bun, testing]
160---
161`;
162 const { frontmatter } = parseFrontmatter(content);
163 expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]);
164 });
165
166 it("parses inline YAML arrays with quoted items", () => {
167 const content = `---
168tags: ["typescript", "bun", "testing"]
169---
170`;
171 const { frontmatter } = parseFrontmatter(content);
172 expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]);
173 });
174
175 it("parses YAML block arrays", () => {
176 const content = `---
177tags:
178 - typescript
179 - bun
180 - testing
181---
182`;
183 const { frontmatter } = parseFrontmatter(content);
184 expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]);
185 });
186
187 it("parses YAML block arrays with quoted items", () => {
188 const content = `---
189tags:
190 - "typescript"
191 - 'bun'
192---
193`;
194 const { frontmatter } = parseFrontmatter(content);
195 expect(frontmatter.tags).toEqual(["typescript", "bun"]);
196 });
197
198 it("parses inline TOML arrays", () => {
199 const content = `+++
200tags = ["typescript", "bun"]
201+++
202`;
203 const { frontmatter } = parseFrontmatter(content);
204 expect(frontmatter.tags).toEqual(["typescript", "bun"]);
205 });
206 });
207
208 describe("publish date fallbacks", () => {
209 it("uses publishDate field directly", () => {
210 const content = `---
211publishDate: 2024-01-15
212---
213`;
214 const { frontmatter } = parseFrontmatter(content);
215 expect(frontmatter.publishDate).toBe("2024-01-15");
216 });
217
218 it("falls back to pubDate", () => {
219 const content = `---
220pubDate: 2024-02-01
221---
222`;
223 const { frontmatter } = parseFrontmatter(content);
224 expect(frontmatter.publishDate).toBe("2024-02-01");
225 });
226
227 it("falls back to date", () => {
228 const content = `---
229date: 2024-03-10
230---
231`;
232 const { frontmatter } = parseFrontmatter(content);
233 expect(frontmatter.publishDate).toBe("2024-03-10");
234 });
235
236 it("falls back to createdAt", () => {
237 const content = `---
238createdAt: 2024-04-20
239---
240`;
241 const { frontmatter } = parseFrontmatter(content);
242 expect(frontmatter.publishDate).toBe("2024-04-20");
243 });
244
245 it("falls back to created_at", () => {
246 const content = `---
247created_at: 2024-05-30
248---
249`;
250 const { frontmatter } = parseFrontmatter(content);
251 expect(frontmatter.publishDate).toBe("2024-05-30");
252 });
253
254 it("prefers publishDate over other fallbacks", () => {
255 const content = `---
256publishDate: 2024-01-01
257date: 2023-01-01
258---
259`;
260 const { frontmatter } = parseFrontmatter(content);
261 expect(frontmatter.publishDate).toBe("2024-01-01");
262 });
263 });
264
265 describe("rawFrontmatter", () => {
266 it("returns all raw fields", () => {
267 const content = `---
268title: Raw Test
269custom: value
270---
271`;
272 const { rawFrontmatter } = parseFrontmatter(content);
273 expect(rawFrontmatter.title).toBe("Raw Test");
274 expect(rawFrontmatter.custom).toBe("value");
275 });
276
277 it("preserves atUri in both frontmatter and rawFrontmatter", () => {
278 const content = `---
279title: Post
280atUri: at://did:plc:abc123/app.bsky.feed.post/xyz
281---
282`;
283 const { frontmatter, rawFrontmatter } = parseFrontmatter(content);
284 expect(frontmatter.atUri).toBe(
285 "at://did:plc:abc123/app.bsky.feed.post/xyz",
286 );
287 expect(rawFrontmatter.atUri).toBe(
288 "at://did:plc:abc123/app.bsky.feed.post/xyz",
289 );
290 });
291 });
292
293 describe("FrontmatterMapping", () => {
294 it("maps a custom title field", () => {
295 const content = `---
296name: My Mapped Title
297---
298`;
299 const { frontmatter } = parseFrontmatter(content, { title: "name" });
300 expect(frontmatter.title).toBe("My Mapped Title");
301 });
302
303 it("maps a custom description field", () => {
304 const content = `---
305summary: Custom description
306---
307`;
308 const { frontmatter } = parseFrontmatter(content, {
309 description: "summary",
310 });
311 expect(frontmatter.description).toBe("Custom description");
312 });
313
314 it("maps a custom publishDate field", () => {
315 const content = `---
316publishedOn: 2024-06-15
317---
318`;
319 const { frontmatter } = parseFrontmatter(content, {
320 publishDate: "publishedOn",
321 });
322 expect(frontmatter.publishDate).toBe("2024-06-15");
323 });
324
325 it("maps a custom coverImage field", () => {
326 const content = `---
327heroImage: /images/cover.jpg
328---
329`;
330 const { frontmatter } = parseFrontmatter(content, {
331 coverImage: "heroImage",
332 });
333 expect(frontmatter.ogImage).toBe("/images/cover.jpg");
334 });
335
336 it("maps a custom tags field", () => {
337 const content = `---
338categories: [news, updates]
339---
340`;
341 const { frontmatter } = parseFrontmatter(content, { tags: "categories" });
342 expect(frontmatter.tags).toEqual(["news", "updates"]);
343 });
344
345 it("maps a custom draft field", () => {
346 const content = `---
347unpublished: true
348---
349`;
350 const { frontmatter } = parseFrontmatter(content, {
351 draft: "unpublished",
352 });
353 expect(frontmatter.draft).toBe(true);
354 });
355
356 it("falls back to standard field name when mapped field is absent", () => {
357 const content = `---
358title: Standard Title
359---
360`;
361 const { frontmatter } = parseFrontmatter(content, { title: "heading" });
362 expect(frontmatter.title).toBe("Standard Title");
363 });
364 });
365
366 describe("body", () => {
367 it("returns the body content after the closing delimiter", () => {
368 const content = `---
369title: Post
370---
371# Heading
372
373Some paragraph text.`;
374 const { body } = parseFrontmatter(content);
375 expect(body).toBe("# Heading\n\nSome paragraph text.");
376 });
377
378 it("returns an empty body when there is no content after frontmatter", () => {
379 const content = `---
380title: Post
381---
382`;
383 const { body } = parseFrontmatter(content);
384 expect(body).toBe("");
385 });
386 });
387});