packages/cli/test/markdown.test.ts 9.2 K raw
1
import { describe, expect, it } from "bun:test";
2
import { parseFrontmatter } from "../src/lib/markdown";
3
4
describe("parseFrontmatter", () => {
5
	describe("delimiters", () => {
6
		it("parses YAML frontmatter (--- delimiter)", () => {
7
			const content = `---
8
title: Hello World
9
---
10
Body 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 = `+++
18
title = "Hugo Post"
19
+++
20
Body 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 = `***
28
title: Alt Post
29
***
30
Body 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("throws when no frontmatter is present", () => {
37
			const content = "Just plain content with no frontmatter.";
38
			expect(() => parseFrontmatter(content)).toThrow(
39
				"Could not parse frontmatter",
40
			);
41
		});
42
	});
43
44
	describe("scalar values", () => {
45
		it("parses a string value", () => {
46
			const content = `---
47
title: My Post
48
description: 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 = `---
58
title: "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 = `---
67
title: '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 = `---
76
excerpt: >
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 = `---
89
excerpt: >-
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 = `---
102
excerpt: |
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 = `---
115
excerpt: |+
116
  This is a kept literal
117
  multiline string
118
119
end: 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 = `---
130
draft: true
131
---
132
`;
133
			const { frontmatter } = parseFrontmatter(content);
134
			expect(frontmatter.draft).toBe(true);
135
		});
136
137
		it("parses boolean false", () => {
138
			const content = `---
139
draft: 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 = `---
148
draft: 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 = `---
159
tags: [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 = `---
168
tags: ["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 = `---
177
tags:
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 = `---
189
tags:
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 = `+++
200
tags = ["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 = `---
211
publishDate: 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 = `---
220
pubDate: 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 = `---
229
date: 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 = `---
238
createdAt: 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 = `---
247
created_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 = `---
256
publishDate: 2024-01-01
257
date: 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 = `---
268
title: Raw Test
269
custom: 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 = `---
279
title: Post
280
atUri: 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 = `---
296
name: 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 = `---
305
summary: 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 = `---
316
publishedOn: 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 = `---
327
heroImage: /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 = `---
338
categories: [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 = `---
347
unpublished: 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 = `---
358
title: 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 = `---
369
title: Post
370
---
371
# Heading
372
373
Some 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 = `---
380
title: Post
381
---
382
`;
383
			const { body } = parseFrontmatter(content);
384
			expect(body).toBe("");
385
		});
386
	});
387
});