Something went wrong. Try again.
A CLI for publishing standard.site documents to ATProto
Something went wrong. Try again.
TypeScript
at issue35-docs
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734import { describe, expect, it } from "bun:test";import { getContentHash, parseFrontmatter, resolvePostPath, stripMarkdownForText, updateFrontmatterWithAtUri,} from "../src/lib/markdown";import type { BlogPost } from "../src/lib/types";
describe("parseFrontmatter", () => { describe("delimiters", () => { it("parses YAML frontmatter (--- delimiter)", () => { const content = `---title: Hello World---Body content here.`; const { frontmatter, body } = parseFrontmatter(content); expect(frontmatter.title).toBe("Hello World"); expect(body).toBe("Body content here."); });
it("parses TOML frontmatter (+++ delimiter)", () => { const content = `+++title = "Hugo Post"+++Body content here.`; const { frontmatter, body } = parseFrontmatter(content); expect(frontmatter.title).toBe("Hugo Post"); expect(body).toBe("Body content here."); });
it("parses alternative frontmatter (*** delimiter)", () => { const content = `***title: Alt Post***Body content here.`; const { frontmatter, body } = parseFrontmatter(content); expect(frontmatter.title).toBe("Alt Post"); expect(body).toBe("Body content here."); });
it("throws when no frontmatter is present", () => { const content = "Just plain content with no frontmatter."; expect(() => parseFrontmatter(content)).toThrow( "Could not parse frontmatter", ); }); });
describe("scalar values", () => { it("parses a string value", () => { const content = `---title: My Postdescription: A short description---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.title).toBe("My Post"); expect(frontmatter.description).toBe("A short description"); });
it("strips double quotes from values", () => { const content = `---title: "Quoted Title"---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.title).toBe("Quoted Title"); });
it("strips single quotes from values", () => { const content = `---title: 'Single Quoted'---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.title).toBe("Single Quoted"); });
it("parses YAML folded multiline string", () => { const content = `---excerpt: > This is a folded multiline string---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.excerpt).toBe( "This is a folded multiline string\n", ); });
it("parses YAML stripped folded multiline string", () => { const content = `---excerpt: >- This is a stripped folded multiline string---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.excerpt).toBe( "This is a stripped folded multiline string", ); });
it("parses YAML literal multiline string", () => { const content = `---excerpt: | This is a literal multiline string---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.excerpt).toBe( "This is a literal\nmultiline string\n", ); });
it("parses YAML kept literal multiline string", () => { const content = `---excerpt: |+ This is a kept literal multiline string
end: true---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.excerpt).toBe( "This is a kept literal\nmultiline string\n\n", ); });
it("parses boolean true", () => { const content = `---draft: true---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.draft).toBe(true); });
it("parses boolean false", () => { const content = `---draft: false---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.draft).toBe(false); });
it('parses string "true" in draft field as boolean true', () => { const content = `---draft: true---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.draft).toBe(true); }); });
describe("arrays", () => { it("parses inline YAML arrays", () => { const content = `---tags: [typescript, bun, testing]---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]); });
it("parses inline YAML arrays with quoted items", () => { const content = `---tags: ["typescript", "bun", "testing"]---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]); });
it("parses YAML block arrays", () => { const content = `---tags: - typescript - bun - testing---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.tags).toEqual(["typescript", "bun", "testing"]); });
it("parses YAML block arrays with quoted items", () => { const content = `---tags: - "typescript" - 'bun'---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.tags).toEqual(["typescript", "bun"]); });
it("parses inline TOML arrays", () => { const content = `+++tags = ["typescript", "bun"]+++`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.tags).toEqual(["typescript", "bun"]); }); });
describe("publish date fallbacks", () => { it("uses publishDate field directly", () => { const content = `---publishDate: 2024-01-15---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-01-15"); });
it("falls back to pubDate", () => { const content = `---pubDate: 2024-02-01---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-02-01"); });
it("falls back to date", () => { const content = `---date: 2024-03-10---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-03-10"); });
it("falls back to createdAt", () => { const content = `---createdAt: 2024-04-20---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-04-20"); });
it("falls back to created_at", () => { const content = `---created_at: 2024-05-30---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-05-30"); });
it("prefers publishDate over other fallbacks", () => { const content = `---publishDate: 2024-01-01date: 2023-01-01---`; const { frontmatter } = parseFrontmatter(content); expect(frontmatter.publishDate).toBe("2024-01-01"); }); });
describe("rawFrontmatter", () => { it("returns all raw fields", () => { const content = `---title: Raw Testcustom: value---`; const { rawFrontmatter } = parseFrontmatter(content); expect(rawFrontmatter.title).toBe("Raw Test"); expect(rawFrontmatter.custom).toBe("value"); });
it("preserves atUri in both frontmatter and rawFrontmatter", () => { const content = `---title: PostatUri: at://did:plc:abc123/app.bsky.feed.post/xyz---`; const { frontmatter, rawFrontmatter } = parseFrontmatter(content); expect(frontmatter.atUri).toBe( "at://did:plc:abc123/app.bsky.feed.post/xyz", ); expect(rawFrontmatter.atUri).toBe( "at://did:plc:abc123/app.bsky.feed.post/xyz", ); }); });
describe("FrontmatterMapping", () => { it("maps a custom title field", () => { const content = `---name: My Mapped Title---`; const { frontmatter } = parseFrontmatter(content, { title: "name" }); expect(frontmatter.title).toBe("My Mapped Title"); });
it("maps a custom description field", () => { const content = `---summary: Custom description---`; const { frontmatter } = parseFrontmatter(content, { description: "summary", }); expect(frontmatter.description).toBe("Custom description"); });
it("maps a custom publishDate field", () => { const content = `---publishedOn: 2024-06-15---`; const { frontmatter } = parseFrontmatter(content, { publishDate: "publishedOn", }); expect(frontmatter.publishDate).toBe("2024-06-15"); });
it("maps a custom coverImage field", () => { const content = `---heroImage: /images/cover.jpg---`; const { frontmatter } = parseFrontmatter(content, { coverImage: "heroImage", }); expect(frontmatter.ogImage).toBe("/images/cover.jpg"); });
it("maps a custom tags field", () => { const content = `---categories: [news, updates]---`; const { frontmatter } = parseFrontmatter(content, { tags: "categories" }); expect(frontmatter.tags).toEqual(["news", "updates"]); });
it("maps a custom draft field", () => { const content = `---unpublished: true---`; const { frontmatter } = parseFrontmatter(content, { draft: "unpublished", }); expect(frontmatter.draft).toBe(true); });
it("falls back to standard field name when mapped field is absent", () => { const content = `---title: Standard Title---`; const { frontmatter } = parseFrontmatter(content, { title: "heading" }); expect(frontmatter.title).toBe("Standard Title"); }); });
describe("body", () => { it("returns the body content after the closing delimiter", () => { const content = `---title: Post---# Heading
Some paragraph text.`; const { body } = parseFrontmatter(content); expect(body).toBe("# Heading\n\nSome paragraph text."); });
it("returns an empty body when there is no content after frontmatter", () => { const content = `---title: Post---`; const { body } = parseFrontmatter(content); expect(body).toBe(""); });
it("preserves TOML tables", () => { const content = `+++title = "Example"coverImage = "static/covers/example.png"
[cover] image = "/covers/example.png" alt = "…" hidden = true+++Body content here.`; const { frontmatter, body, rawFrontmatter } = parseFrontmatter(content); expect(frontmatter.title).toBe("Example"); expect(body).toBe("Body content here.");
const coverTable = rawFrontmatter?.cover as | Record<string, unknown> | undefined; expect(coverTable).not.toBe(undefined); const coverImageField = coverTable?.image as string | undefined; expect(coverImageField).toBe("/covers/example.png"); }); });});
describe("resolvePostPath", () => { const post: BlogPost = { filePath: "/tmp/hello.md", slug: "hello-world", frontmatter: { title: "Hello" } as BlogPost["frontmatter"], content: "", rawContent: "", rawFrontmatter: {}, };
it("defaults to /posts when pathPrefix is undefined", () => { expect(resolvePostPath(post)).toBe("/posts/hello-world"); });
it("uses custom prefix when provided", () => { expect(resolvePostPath(post, "/blog")).toBe("/blog/hello-world"); });
it("omits prefix when pathPrefix is an empty string", () => { expect(resolvePostPath(post, "")).toBe("/hello-world"); });
it("pathTemplate overrides pathPrefix", () => { expect(resolvePostPath(post, "", "/custom/{slug}")).toBe( "/custom/hello-world", ); });});
describe("stripMarkdownForText", () => { it("removes h1 through h6 headers", async () => { expect( await stripMarkdownForText( "# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6", ), ).toBe("H1\n\nH2\n\nH3\n\nH4\n\nH5\n\nH6"); });
it("removes bold formatting", async () => { expect(await stripMarkdownForText("This is **bold** text.")).toBe( "This is bold text.", ); });
it("removes italic formatting", async () => { expect(await stripMarkdownForText("This is *italic* text.")).toBe( "This is italic text.", ); });
it("removes links but keeps link text", async () => { expect( await stripMarkdownForText("[click here](https://example.com)"), ).toBe("click here"); });
it("removes fenced code blocks", async () => { const input = "before\n```\nconst x = 1;\n```\nafter"; expect(await stripMarkdownForText(input)).toBe("before\n\nafter"); });
it("removes inline code formatting", async () => { expect(await stripMarkdownForText("Run `npm install` to install.")).toBe( "Run npm install to install.", ); });
it("removes images", async () => { expect( await stripMarkdownForText("before  after"), ).toBe("before alt text after"); });
it("normalizes three or more consecutive newlines to two", async () => { expect(await stripMarkdownForText("line 1\n\n\n\nline 2")).toBe( "line 1\n\nline 2", ); });
it("removes single tilde strikethrough", async () => { expect(await stripMarkdownForText("~strikethrough~")).toBe("strikethrough"); });
it("removes double tilde strikethrough", async () => { expect(await stripMarkdownForText("~~strikethrough~~")).toBe( "strikethrough", ); });
it("removes HTML comments but preserves -- outside comments", async () => { expect(await stripMarkdownForText("a -- b <!-- c -- d --> e -- f")).toBe( "a -- b e -- f", ); });
it("removes footnote definitions and inline references", async () => { const input = "Text with[^1] a reference.\n\n[^1]: The footnote definition."; expect(await stripMarkdownForText(input)).toBe("Text with a reference."); });
it("trims leading and trailing whitespace", async () => { expect(await stripMarkdownForText("\n\nhello\n\n")).toBe("hello"); });
it("keeps HTML figure captions", async () => { const input = `# Title_Emphasized text._
<figure><figcaption>Figure caption:- Foo- Bar</figcaption></figure>
List:1. One2. Two`; expect(await stripMarkdownForText(input)).toBe(`Title
Emphasized text.
Figure caption:
Foo
Bar
List:
One
Two`); });
it("keeps nested HTML DIV content", async () => { const input = `# Title_Emphasized text._
<div class="test"><p>Text in a DIV.</p></div>
Footer`; expect(await stripMarkdownForText(input)).toBe(`Title
Emphasized text.
Text in a DIV.
Footer`); });});
describe("updateFrontmatterWithAtUri", () => { it("inserts atUri into YAML frontmatter when it does not exist", () => { const content = `---title: Hello World---Body content here.`; const updatedContent = updateFrontmatterWithAtUri( content, "at://did:plc:abc123/app.bsky.feed.post/xyz", );
expect(updatedContent).toBe(`---title: Hello WorldatUri: at://did:plc:abc123/app.bsky.feed.post/xyz---Body content here.`); });
it("replaces an existing atUri in YAML frontmatter", () => { const content = `---title: Hello WorldatUri: "at://did:plc:old/app.bsky.feed.post/old"---Body content here.`; const updatedContent = updateFrontmatterWithAtUri( content, "at://did:plc:new/app.bsky.feed.post/new", );
expect(updatedContent).toBe(`---title: Hello WorldatUri: at://did:plc:new/app.bsky.feed.post/new---Body content here.`); });
it("inserts atUri into TOML frontmatter when it does not exist", () => { const content = `+++title = "Hello World"+++Body content here.`; const updatedContent = updateFrontmatterWithAtUri( content, "at://did:plc:abc123/app.bsky.feed.post/xyz", );
expect(updatedContent).toBe(`+++title = "Hello World"atUri = "at://did:plc:abc123/app.bsky.feed.post/xyz"+++Body content here.`); });
it("replaces an existing atUri in TOML frontmatter", () => { const content = `+++title = "Hello World"atUri = "at://did:plc:old/app.bsky.feed.post/old"+++Body content here.`; const updatedContent = updateFrontmatterWithAtUri( content, "at://did:plc:new/app.bsky.feed.post/new", );
expect(updatedContent).toBe(`+++title = "Hello World"atUri = "at://did:plc:new/app.bsky.feed.post/new"+++Body content here.`); });
it("inserts atUri into TOML frontmatter when it include another table", () => { const content = `+++title = "Example"coverImage = "static/covers/example.png"
[cover] image = "/covers/example.png" alt = "…" hidden = true+++Body content here.`; const updatedContent = updateFrontmatterWithAtUri( content, "at://did:plc:new/app.bsky.feed.post/new", ); expect(updatedContent).toBe(`+++title = "Example"coverImage = "static/covers/example.png"atUri = "at://did:plc:new/app.bsky.feed.post/new"
[cover]image = "/covers/example.png"alt = "…"hidden = true+++Body content here.`); });});
describe("getContentHash", () => { it("returns the expected SHA-256 hash as lowercase hex", async () => { const post: Partial<BlogPost> = { rawContent: "hello world", }; expect(await getContentHash(post)).toBe( "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", ); });
it("returns the same hash for identical content and different hashes for different content", async () => { const firstPost: Partial<BlogPost> = { rawContent: "same content", }; const secondPost: Partial<BlogPost> = { rawContent: "same content", }; const thirdPost: Partial<BlogPost> = { rawContent: "different content", }; const first = await getContentHash(firstPost); const second = await getContentHash(secondPost); const third = await getContentHash(thirdPost);
expect(first).toBe(second); expect(first).not.toBe(third); expect(first).toMatch(/^[a-f0-9]{64}$/); });
it("returns a different hash with a coverImagePath", async () => { const textPost: Partial<BlogPost> = { rawContent: "hello world", }; const imagePost: Partial<BlogPost> = { rawContent: "hello world", coverImagePath: __filename, }; const textHash = await getContentHash(textPost); const imageHash = await getContentHash(imagePost);
expect(textHash).not.toBe(imageHash); });
it("returns a different hash with a different coverImagePath", async () => { const firstPost: Partial<BlogPost> = { rawContent: "hello world", coverImagePath: __filename, }; const secondPost: Partial<BlogPost> = { rawContent: "hello world", coverImagePath: `${__dirname}/config.test.ts`, }; const firstHash = await getContentHash(firstPost); const secondHash = await getContentHash(secondPost);
expect(firstHash).not.toBe(secondHash); });});