forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
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 = "";
95 expect(resolveInternalLinks(content, posts)).toBe("");
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: "",
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});