A CLI for publishing standard.site documents to ATProto
at main 238 lines 6.5 kB view raw
1import { describe, expect, test } from "bun:test"; 2import { resolveInternalLinks, findPostsWithStaleLinks } from "./litenote"; 3import type { BlogPost } from "../lib/types"; 4 5function makePost( 6 slug: string, 7 atUri?: string, 8 options?: { content?: string; draft?: boolean; filePath?: string }, 9): BlogPost { 10 return { 11 filePath: options?.filePath ?? `content/${slug}.md`, 12 slug, 13 frontmatter: { 14 title: slug, 15 publishDate: "2024-01-01", 16 atUri, 17 draft: options?.draft, 18 }, 19 content: options?.content ?? "", 20 rawContent: "", 21 rawFrontmatter: {}, 22 }; 23} 24 25describe("resolveInternalLinks", () => { 26 test("strips link for unpublished local path", () => { 27 const posts = [makePost("other-post")]; 28 const content = "See [my post](./other-post)"; 29 expect(resolveInternalLinks(content, posts)).toBe("See my post"); 30 }); 31 32 test("rewrites published link to litenote atUri", () => { 33 const posts = [ 34 makePost( 35 "other-post", 36 "at://did:plc:abc/site.standard.document/abc123", 37 ), 38 ]; 39 const content = "See [my post](./other-post)"; 40 expect(resolveInternalLinks(content, posts)).toBe( 41 "See [my post](at://did:plc:abc/space.litenote.note/abc123)", 42 ); 43 }); 44 45 test("leaves external links unchanged", () => { 46 const posts = [makePost("other-post")]; 47 const content = "See [example](https://example.com)"; 48 expect(resolveInternalLinks(content, posts)).toBe( 49 "See [example](https://example.com)", 50 ); 51 }); 52 53 test("leaves anchor links unchanged", () => { 54 const posts: BlogPost[] = []; 55 const content = "See [section](#heading)"; 56 expect(resolveInternalLinks(content, posts)).toBe( 57 "See [section](#heading)", 58 ); 59 }); 60 61 test("handles .md extension in link path", () => { 62 const posts = [ 63 makePost( 64 "guide", 65 "at://did:plc:abc/site.standard.document/guide123", 66 ), 67 ]; 68 const content = "Read the [guide](guide.md)"; 69 expect(resolveInternalLinks(content, posts)).toBe( 70 "Read the [guide](at://did:plc:abc/space.litenote.note/guide123)", 71 ); 72 }); 73 74 test("handles nested slug matching", () => { 75 const posts = [ 76 makePost( 77 "blog/my-post", 78 "at://did:plc:abc/site.standard.document/rkey1", 79 ), 80 ]; 81 const content = "See [post](my-post)"; 82 expect(resolveInternalLinks(content, posts)).toBe( 83 "See [post](at://did:plc:abc/space.litenote.note/rkey1)", 84 ); 85 }); 86 87 test("does not rewrite image embeds", () => { 88 const posts = [ 89 makePost( 90 "photo", 91 "at://did:plc:abc/site.standard.document/photo1", 92 ), 93 ]; 94 const content = "![alt](photo)"; 95 expect(resolveInternalLinks(content, posts)).toBe("![alt](photo)"); 96 }); 97 98 test("does not rewrite @mention links", () => { 99 const posts = [ 100 makePost( 101 "mention", 102 "at://did:plc:abc/site.standard.document/m1", 103 ), 104 ]; 105 const content = "@[name](mention)"; 106 expect(resolveInternalLinks(content, posts)).toBe("@[name](mention)"); 107 }); 108 109 test("handles multiple links in same content", () => { 110 const posts = [ 111 makePost( 112 "published", 113 "at://did:plc:abc/site.standard.document/pub1", 114 ), 115 makePost("unpublished"), 116 ]; 117 const content = 118 "See [a](published) and [b](unpublished) and [c](https://ext.com)"; 119 expect(resolveInternalLinks(content, posts)).toBe( 120 "See [a](at://did:plc:abc/space.litenote.note/pub1) and b and [c](https://ext.com)", 121 ); 122 }); 123 124 test("handles index path normalization", () => { 125 const posts = [ 126 makePost( 127 "docs", 128 "at://did:plc:abc/site.standard.document/docs1", 129 ), 130 ]; 131 const content = "See [docs](./docs/index)"; 132 expect(resolveInternalLinks(content, posts)).toBe( 133 "See [docs](at://did:plc:abc/space.litenote.note/docs1)", 134 ); 135 }); 136}); 137 138describe("findPostsWithStaleLinks", () => { 139 test("finds published post containing link to a newly created slug", () => { 140 const posts = [ 141 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 142 content: "Check out [post B](./post-b)", 143 }), 144 ]; 145 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 146 expect(result).toHaveLength(1); 147 expect(result[0]!.slug).toBe("post-a"); 148 }); 149 150 test("excludes posts in the exclude set (current batch)", () => { 151 const posts = [ 152 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 153 content: "Check out [post B](./post-b)", 154 }), 155 ]; 156 const result = findPostsWithStaleLinks( 157 posts, 158 ["post-b"], 159 new Set(["content/post-a.md"]), 160 ); 161 expect(result).toHaveLength(0); 162 }); 163 164 test("excludes unpublished posts (no atUri)", () => { 165 const posts = [ 166 makePost("post-a", undefined, { 167 content: "Check out [post B](./post-b)", 168 }), 169 ]; 170 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 171 expect(result).toHaveLength(0); 172 }); 173 174 test("excludes drafts", () => { 175 const posts = [ 176 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 177 content: "Check out [post B](./post-b)", 178 draft: true, 179 }), 180 ]; 181 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 182 expect(result).toHaveLength(0); 183 }); 184 185 test("ignores external links", () => { 186 const posts = [ 187 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 188 content: "Check out [post B](https://example.com/post-b)", 189 }), 190 ]; 191 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 192 expect(result).toHaveLength(0); 193 }); 194 195 test("ignores image embeds", () => { 196 const posts = [ 197 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 198 content: "![post B](./post-b)", 199 }), 200 ]; 201 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 202 expect(result).toHaveLength(0); 203 }); 204 205 test("ignores @mention links", () => { 206 const posts = [ 207 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 208 content: "@[post B](./post-b)", 209 }), 210 ]; 211 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 212 expect(result).toHaveLength(0); 213 }); 214 215 test("handles nested slug matching", () => { 216 const posts = [ 217 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 218 content: "Check out [post](my-post)", 219 }), 220 ]; 221 const result = findPostsWithStaleLinks( 222 posts, 223 ["blog/my-post"], 224 new Set(), 225 ); 226 expect(result).toHaveLength(1); 227 }); 228 229 test("does not match posts without matching links", () => { 230 const posts = [ 231 makePost("post-a", "at://did:plc:abc/site.standard.document/a1", { 232 content: "Check out [post C](./post-c)", 233 }), 234 ]; 235 const result = findPostsWithStaleLinks(posts, ["post-b"], new Set()); 236 expect(result).toHaveLength(0); 237 }); 238});