A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
58
fork

Configure Feed

Select the types of activity you want to include in your feed.

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