this repo has no description
at main 458 lines 12 kB view raw
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 ![alt](url) partially matches as a link first 412 expect(stripMarkdownForText("text ![alt](image.png) 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});