this repo has no description
1import { describe, expect, test } from "bun:test";
2import {
3 getContentHash,
4 getSlugFromFilename,
5 getSlugFromOptions,
6 getTextContent,
7 parseFrontmatter,
8 stripMarkdownForText,
9 updateFrontmatterWithAtUri,
10} from "./markdown";
11
12describe("parseFrontmatter", () => {
13 test("parses YAML frontmatter with --- delimiters", () => {
14 const content = `---
15title: My Post
16description: A description
17publishDate: 2024-01-15
18---
19Hello world`;
20
21 const result = parseFrontmatter(content);
22 expect(result.frontmatter.title).toBe("My Post");
23 expect(result.frontmatter.description).toBe("A description");
24 expect(result.frontmatter.publishDate).toBe("2024-01-15");
25 expect(result.body).toBe("Hello world");
26 expect(result.rawFrontmatter.title).toBe("My Post");
27 });
28
29 test("parses TOML frontmatter with +++ delimiters", () => {
30 const content = `+++
31title = "My Post"
32description = "A description"
33date = "2024-01-15"
34+++
35Body content`;
36
37 const result = parseFrontmatter(content);
38 expect(result.frontmatter.title).toBe("My Post");
39 expect(result.frontmatter.description).toBe("A description");
40 expect(result.frontmatter.publishDate).toBe("2024-01-15");
41 expect(result.body).toBe("Body content");
42 });
43
44 test("parses *** delimited frontmatter", () => {
45 const content = `***
46title: Test
47***
48Body`;
49
50 const result = parseFrontmatter(content);
51 expect(result.frontmatter.title).toBe("Test");
52 expect(result.body).toBe("Body");
53 });
54
55 test("handles no frontmatter - extracts title from heading", () => {
56 const content = `# My Heading
57
58Some body text`;
59
60 const result = parseFrontmatter(content);
61 expect(result.frontmatter.title).toBe("My Heading");
62 expect(result.frontmatter.publishDate).toBeTruthy();
63 expect(result.body).toBe(content);
64 });
65
66 test("handles no frontmatter and no heading", () => {
67 const content = "Just plain text";
68
69 const result = parseFrontmatter(content);
70 expect(result.frontmatter.title).toBe("");
71 expect(result.body).toBe(content);
72 });
73
74 test("handles quoted string values", () => {
75 const content = `---
76title: "Quoted Title"
77description: 'Single Quoted'
78---
79Body`;
80
81 const result = parseFrontmatter(content);
82 expect(result.rawFrontmatter.title).toBe("Quoted Title");
83 expect(result.rawFrontmatter.description).toBe("Single Quoted");
84 });
85
86 test("parses inline arrays", () => {
87 const content = `---
88title: Post
89tags: [javascript, typescript, "web dev"]
90---
91Body`;
92
93 const result = parseFrontmatter(content);
94 expect(result.rawFrontmatter.tags).toEqual([
95 "javascript",
96 "typescript",
97 "web dev",
98 ]);
99 });
100
101 test("parses YAML multiline arrays", () => {
102 const content = `---
103title: Post
104tags:
105 - javascript
106 - typescript
107 - web dev
108---
109Body`;
110
111 const result = parseFrontmatter(content);
112 expect(result.rawFrontmatter.tags).toEqual([
113 "javascript",
114 "typescript",
115 "web dev",
116 ]);
117 });
118
119 test("parses boolean values", () => {
120 const content = `---
121title: Draft Post
122draft: true
123published: false
124---
125Body`;
126
127 const result = parseFrontmatter(content);
128 expect(result.rawFrontmatter.draft).toBe(true);
129 expect(result.rawFrontmatter.published).toBe(false);
130 });
131
132 test("applies frontmatter field mappings", () => {
133 const content = `---
134nombre: Custom Title
135descripcion: Custom Desc
136fecha: 2024-06-01
137imagen: cover.jpg
138etiquetas: [a, b]
139borrador: true
140---
141Body`;
142
143 const mapping = {
144 title: "nombre",
145 description: "descripcion",
146 publishDate: "fecha",
147 coverImage: "imagen",
148 tags: "etiquetas",
149 draft: "borrador",
150 };
151
152 const result = parseFrontmatter(content, mapping);
153 expect(result.frontmatter.title).toBe("Custom Title");
154 expect(result.frontmatter.description).toBe("Custom Desc");
155 expect(result.frontmatter.publishDate).toBe("2024-06-01");
156 expect(result.frontmatter.ogImage).toBe("cover.jpg");
157 expect(result.frontmatter.tags).toEqual(["a", "b"]);
158 expect(result.frontmatter.draft).toBe(true);
159 });
160
161 test("falls back to common date field names", () => {
162 const content = `---
163title: Post
164date: 2024-03-20
165---
166Body`;
167
168 const result = parseFrontmatter(content);
169 expect(result.frontmatter.publishDate).toBe("2024-03-20");
170 });
171
172 test("falls back to pubDate", () => {
173 const content = `---
174title: Post
175pubDate: 2024-04-10
176---
177Body`;
178
179 const result = parseFrontmatter(content);
180 expect(result.frontmatter.publishDate).toBe("2024-04-10");
181 });
182
183 test("preserves atUri field", () => {
184 const content = `---
185title: Post
186atUri: at://did:plc:abc/site.standard.post/123
187---
188Body`;
189
190 const result = parseFrontmatter(content);
191 expect(result.frontmatter.atUri).toBe(
192 "at://did:plc:abc/site.standard.post/123",
193 );
194 });
195
196 test("maps draft field correctly", () => {
197 const content = `---
198title: Post
199draft: true
200---
201Body`;
202
203 const result = parseFrontmatter(content);
204 expect(result.frontmatter.draft).toBe(true);
205 });
206});
207
208describe("getSlugFromFilename", () => {
209 test("removes .md extension", () => {
210 expect(getSlugFromFilename("my-post.md")).toBe("my-post");
211 });
212
213 test("removes .mdx extension", () => {
214 expect(getSlugFromFilename("my-post.mdx")).toBe("my-post");
215 });
216
217 test("converts to lowercase", () => {
218 expect(getSlugFromFilename("My-Post.md")).toBe("my-post");
219 });
220
221 test("replaces spaces with dashes", () => {
222 expect(getSlugFromFilename("my cool post.md")).toBe("my-cool-post");
223 });
224});
225
226describe("getSlugFromOptions", () => {
227 test("uses filepath by default", () => {
228 const slug = getSlugFromOptions("blog/my-post.md", {});
229 expect(slug).toBe("blog/my-post");
230 });
231
232 test("uses slugField from frontmatter when set", () => {
233 const slug = getSlugFromOptions(
234 "blog/my-post.md",
235 { slug: "/custom-slug" },
236 { slugField: "slug" },
237 );
238 expect(slug).toBe("custom-slug");
239 });
240
241 test("falls back to filepath when slugField not found in frontmatter", () => {
242 const slug = getSlugFromOptions(
243 "blog/my-post.md",
244 {},
245 { slugField: "slug" },
246 );
247 expect(slug).toBe("blog/my-post");
248 });
249
250 test("removes /index suffix when removeIndexFromSlug is true", () => {
251 const slug = getSlugFromOptions(
252 "blog/my-post/index.md",
253 {},
254 { removeIndexFromSlug: true },
255 );
256 expect(slug).toBe("blog/my-post");
257 });
258
259 test("removes /_index suffix when removeIndexFromSlug is true", () => {
260 const slug = getSlugFromOptions(
261 "blog/my-post/_index.md",
262 {},
263 { removeIndexFromSlug: true },
264 );
265 expect(slug).toBe("blog/my-post");
266 });
267
268 test("strips date prefix when stripDatePrefix is true", () => {
269 const slug = getSlugFromOptions(
270 "2024-01-15-my-post.md",
271 {},
272 { stripDatePrefix: true },
273 );
274 expect(slug).toBe("my-post");
275 });
276
277 test("strips date prefix in nested paths", () => {
278 const slug = getSlugFromOptions(
279 "blog/2024-01-15-my-post.md",
280 {},
281 { stripDatePrefix: true },
282 );
283 expect(slug).toBe("blog/my-post");
284 });
285
286 test("combines removeIndexFromSlug and stripDatePrefix", () => {
287 const slug = getSlugFromOptions(
288 "blog/2024-01-15-my-post/index.md",
289 {},
290 { removeIndexFromSlug: true, stripDatePrefix: true },
291 );
292 expect(slug).toBe("blog/my-post");
293 });
294
295 test("lowercases and replaces spaces", () => {
296 const slug = getSlugFromOptions("Blog/My Post.md", {});
297 expect(slug).toBe("blog/my-post");
298 });
299});
300
301describe("getContentHash", () => {
302 test("returns a hex string", async () => {
303 const hash = await getContentHash("hello");
304 expect(hash).toMatch(/^[0-9a-f]+$/);
305 });
306
307 test("returns consistent results", async () => {
308 const hash1 = await getContentHash("test content");
309 const hash2 = await getContentHash("test content");
310 expect(hash1).toBe(hash2);
311 });
312
313 test("returns different hashes for different content", async () => {
314 const hash1 = await getContentHash("content a");
315 const hash2 = await getContentHash("content b");
316 expect(hash1).not.toBe(hash2);
317 });
318});
319
320describe("updateFrontmatterWithAtUri", () => {
321 test("inserts atUri into YAML frontmatter", () => {
322 const content = `---
323title: My Post
324---
325Body`;
326
327 const result = updateFrontmatterWithAtUri(
328 content,
329 "at://did:plc:abc/post/123",
330 );
331 expect(result).toContain('atUri: "at://did:plc:abc/post/123"');
332 expect(result).toContain("title: My Post");
333 });
334
335 test("inserts atUri into TOML frontmatter", () => {
336 const content = `+++
337title = My Post
338+++
339Body`;
340
341 const result = updateFrontmatterWithAtUri(
342 content,
343 "at://did:plc:abc/post/123",
344 );
345 expect(result).toContain('atUri = "at://did:plc:abc/post/123"');
346 });
347
348 test("creates frontmatter with atUri when none exists", () => {
349 const content = "# My Post\n\nSome body text";
350
351 const result = updateFrontmatterWithAtUri(
352 content,
353 "at://did:plc:abc/post/123",
354 );
355 expect(result).toContain('atUri: "at://did:plc:abc/post/123"');
356 expect(result).toContain("---");
357 expect(result).toContain("# My Post\n\nSome body text");
358 });
359
360 test("replaces existing atUri in YAML", () => {
361 const content = `---
362title: My Post
363atUri: "at://did:plc:old/post/000"
364---
365Body`;
366
367 const result = updateFrontmatterWithAtUri(
368 content,
369 "at://did:plc:new/post/999",
370 );
371 expect(result).toContain('atUri: "at://did:plc:new/post/999"');
372 expect(result).not.toContain("old");
373 });
374
375 test("replaces existing atUri in TOML", () => {
376 const content = `+++
377title = My Post
378atUri = "at://did:plc:old/post/000"
379+++
380Body`;
381
382 const result = updateFrontmatterWithAtUri(
383 content,
384 "at://did:plc:new/post/999",
385 );
386 expect(result).toContain('atUri = "at://did:plc:new/post/999"');
387 expect(result).not.toContain("old");
388 });
389});
390
391describe("stripMarkdownForText", () => {
392 test("removes headings", () => {
393 expect(stripMarkdownForText("## Hello")).toBe("Hello");
394 });
395
396 test("removes bold", () => {
397 expect(stripMarkdownForText("**bold text**")).toBe("bold text");
398 });
399
400 test("removes italic", () => {
401 expect(stripMarkdownForText("*italic text*")).toBe("italic text");
402 });
403
404 test("removes links but keeps text", () => {
405 expect(stripMarkdownForText("[click here](https://example.com)")).toBe(
406 "click here",
407 );
408 });
409
410 test("removes images", () => {
411 // Note: link regex runs before image regex, so  partially matches as a link first
412 expect(stripMarkdownForText("text  more")).toBe(
413 "text !alt more",
414 );
415 });
416
417 test("removes code blocks", () => {
418 const input = "Before\n```js\nconst x = 1;\n```\nAfter";
419 expect(stripMarkdownForText(input)).toContain("Before");
420 expect(stripMarkdownForText(input)).toContain("After");
421 expect(stripMarkdownForText(input)).not.toContain("const x");
422 });
423
424 test("removes inline code formatting", () => {
425 expect(stripMarkdownForText("use `npm install`")).toBe("use npm install");
426 });
427
428 test("normalizes multiple newlines", () => {
429 const input = "Line 1\n\n\n\n\nLine 2";
430 expect(stripMarkdownForText(input)).toBe("Line 1\n\nLine 2");
431 });
432});
433
434describe("getTextContent", () => {
435 test("uses textContentField from frontmatter when specified", () => {
436 const post = {
437 content: "# Markdown body",
438 rawFrontmatter: { excerpt: "Custom excerpt text" },
439 };
440 expect(getTextContent(post, "excerpt")).toBe("Custom excerpt text");
441 });
442
443 test("falls back to stripped markdown when textContentField not found", () => {
444 const post = {
445 content: "**Bold text** and [a link](url)",
446 rawFrontmatter: {},
447 };
448 expect(getTextContent(post, "missing")).toBe("Bold text and a link");
449 });
450
451 test("falls back to stripped markdown when no textContentField specified", () => {
452 const post = {
453 content: "## Heading\n\nParagraph",
454 rawFrontmatter: {},
455 };
456 expect(getTextContent(post)).toBe("Heading\n\nParagraph");
457 });
458});