tangled
alpha
login
or
join now
apoena.dev
/
sequoia
forked from
stevedylan.dev/sequoia
A CLI for publishing standard.site documents to ATProto
0
fork
atom
overview
issues
pulls
pipelines
feat: link to at links
Julien Calixte
1 day ago
4f7c3a62
55282804
0/1
lint.yml
failed
4s
+141
-5
2 changed files
expand all
collapse all
unified
split
packages
cli
src
extensions
litenote.test.ts
litenote.ts
+131
packages/cli/src/extensions/litenote.test.ts
···
1
1
+
import { describe, expect, test } from "bun:test";
2
2
+
import { resolveInternalLinks } from "./litenote";
3
3
+
import type { BlogPost } from "../lib/types";
4
4
+
5
5
+
function makePost(slug: string, atUri?: string): BlogPost {
6
6
+
return {
7
7
+
filePath: `content/${slug}.md`,
8
8
+
slug,
9
9
+
frontmatter: {
10
10
+
title: slug,
11
11
+
publishDate: "2024-01-01",
12
12
+
atUri,
13
13
+
},
14
14
+
content: "",
15
15
+
rawContent: "",
16
16
+
rawFrontmatter: {},
17
17
+
};
18
18
+
}
19
19
+
20
20
+
describe("resolveInternalLinks", () => {
21
21
+
test("strips link for unpublished local path", () => {
22
22
+
const posts = [makePost("other-post")];
23
23
+
const content = "See [my post](./other-post)";
24
24
+
expect(resolveInternalLinks(content, posts)).toBe("See my post");
25
25
+
});
26
26
+
27
27
+
test("rewrites published link to litenote atUri", () => {
28
28
+
const posts = [
29
29
+
makePost(
30
30
+
"other-post",
31
31
+
"at://did:plc:abc/site.standard.document/abc123",
32
32
+
),
33
33
+
];
34
34
+
const content = "See [my post](./other-post)";
35
35
+
expect(resolveInternalLinks(content, posts)).toBe(
36
36
+
"See [my post](at://did:plc:abc/space.litenote.note/abc123)",
37
37
+
);
38
38
+
});
39
39
+
40
40
+
test("leaves external links unchanged", () => {
41
41
+
const posts = [makePost("other-post")];
42
42
+
const content = "See [example](https://example.com)";
43
43
+
expect(resolveInternalLinks(content, posts)).toBe(
44
44
+
"See [example](https://example.com)",
45
45
+
);
46
46
+
});
47
47
+
48
48
+
test("leaves anchor links unchanged", () => {
49
49
+
const posts: BlogPost[] = [];
50
50
+
const content = "See [section](#heading)";
51
51
+
expect(resolveInternalLinks(content, posts)).toBe(
52
52
+
"See [section](#heading)",
53
53
+
);
54
54
+
});
55
55
+
56
56
+
test("handles .md extension in link path", () => {
57
57
+
const posts = [
58
58
+
makePost(
59
59
+
"guide",
60
60
+
"at://did:plc:abc/site.standard.document/guide123",
61
61
+
),
62
62
+
];
63
63
+
const content = "Read the [guide](guide.md)";
64
64
+
expect(resolveInternalLinks(content, posts)).toBe(
65
65
+
"Read the [guide](at://did:plc:abc/space.litenote.note/guide123)",
66
66
+
);
67
67
+
});
68
68
+
69
69
+
test("handles nested slug matching", () => {
70
70
+
const posts = [
71
71
+
makePost(
72
72
+
"blog/my-post",
73
73
+
"at://did:plc:abc/site.standard.document/rkey1",
74
74
+
),
75
75
+
];
76
76
+
const content = "See [post](my-post)";
77
77
+
expect(resolveInternalLinks(content, posts)).toBe(
78
78
+
"See [post](at://did:plc:abc/space.litenote.note/rkey1)",
79
79
+
);
80
80
+
});
81
81
+
82
82
+
test("does not rewrite image embeds", () => {
83
83
+
const posts = [
84
84
+
makePost(
85
85
+
"photo",
86
86
+
"at://did:plc:abc/site.standard.document/photo1",
87
87
+
),
88
88
+
];
89
89
+
const content = "";
90
90
+
expect(resolveInternalLinks(content, posts)).toBe("");
91
91
+
});
92
92
+
93
93
+
test("does not rewrite @mention links", () => {
94
94
+
const posts = [
95
95
+
makePost(
96
96
+
"mention",
97
97
+
"at://did:plc:abc/site.standard.document/m1",
98
98
+
),
99
99
+
];
100
100
+
const content = "@[name](mention)";
101
101
+
expect(resolveInternalLinks(content, posts)).toBe("@[name](mention)");
102
102
+
});
103
103
+
104
104
+
test("handles multiple links in same content", () => {
105
105
+
const posts = [
106
106
+
makePost(
107
107
+
"published",
108
108
+
"at://did:plc:abc/site.standard.document/pub1",
109
109
+
),
110
110
+
makePost("unpublished"),
111
111
+
];
112
112
+
const content =
113
113
+
"See [a](published) and [b](unpublished) and [c](https://ext.com)";
114
114
+
expect(resolveInternalLinks(content, posts)).toBe(
115
115
+
"See [a](at://did:plc:abc/space.litenote.note/pub1) and b and [c](https://ext.com)",
116
116
+
);
117
117
+
});
118
118
+
119
119
+
test("handles index path normalization", () => {
120
120
+
const posts = [
121
121
+
makePost(
122
122
+
"docs",
123
123
+
"at://did:plc:abc/site.standard.document/docs1",
124
124
+
),
125
125
+
];
126
126
+
const content = "See [docs](./docs/index)";
127
127
+
expect(resolveInternalLinks(content, posts)).toBe(
128
128
+
"See [docs](at://did:plc:abc/space.litenote.note/docs1)",
129
129
+
);
130
130
+
});
131
131
+
});
+10
-5
packages/cli/src/extensions/litenote.ts
···
122
122
return { content: processedContent, images }
123
123
}
124
124
125
125
-
function removeUnpublishedLinks(
125
125
+
export function resolveInternalLinks(
126
126
content: string,
127
127
allPosts: BlogPost[],
128
128
): string {
···
138
138
.replace(/\.mdx?$/, "")
139
139
.replace(/\/index$/, "")
140
140
141
141
-
const isPublished = allPosts.some((p) => {
141
141
+
const matchedPost = allPosts.find((p) => {
142
142
if (!p.frontmatter.atUri) return false
143
143
return (
144
144
p.slug === normalized ||
···
147
147
)
148
148
})
149
149
150
150
-
if (!isPublished) return text
151
151
-
return fullMatch
150
150
+
if (!matchedPost) return text
151
151
+
152
152
+
const noteUri = matchedPost.frontmatter.atUri!.replace(
153
153
+
/\/[^/]+\/([^/]+)$/,
154
154
+
`/space.litenote.note/$1`,
155
155
+
)
156
156
+
return `[${text}](${noteUri})`
152
157
})
153
158
}
154
159
···
159
164
): Promise<{ content: string; images: ImageRecord[] }> {
160
165
let content = post.content.trim()
161
166
162
162
-
content = removeUnpublishedLinks(content, options.allPosts)
167
167
+
content = resolveInternalLinks(content, options.allPosts)
163
168
164
169
const result = await processImages(
165
170
agent, content, post.filePath, options.contentDir, options.imagesDir,