this repo has no description
at main 387 lines 9.4 kB view raw
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});