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