@bryanguffey/astro-standard-site 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +348 -0
- package/components/Comments.astro +330 -0
- package/dist/comments.d.ts +97 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +221 -0
- package/dist/comments.js.map +1 -0
- package/dist/content.d.ts +141 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +202 -0
- package/dist/content.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +181 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +207 -0
- package/dist/loader.js.map +1 -0
- package/dist/publisher.d.ts +157 -0
- package/dist/publisher.d.ts.map +1 -0
- package/dist/publisher.js +326 -0
- package/dist/publisher.js.map +1 -0
- package/dist/schemas.d.ts +1079 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +154 -0
- package/dist/schemas.js.map +1 -0
- package/dist/verification.d.ts +103 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +111 -0
- package/dist/verification.js.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment aggregation for standard.site documents
|
|
3
|
+
*
|
|
4
|
+
* Fetches replies/comments from various ATProto platforms:
|
|
5
|
+
* - Bluesky posts that link to your document
|
|
6
|
+
* - Direct replies to announcement posts (via bskyPostRef)
|
|
7
|
+
* - Future: Leaflet comments, WhiteWind reactions
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { fetchComments } from 'astro-standard-site/comments';
|
|
12
|
+
*
|
|
13
|
+
* const comments = await fetchComments({
|
|
14
|
+
* documentUri: 'at://did:plc:xxx/site.standard.document/abc123',
|
|
15
|
+
* bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/xyz789',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // comments is an array of unified Comment objects
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface Author {
|
|
22
|
+
did: string;
|
|
23
|
+
handle: string;
|
|
24
|
+
displayName?: string;
|
|
25
|
+
avatar?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Comment {
|
|
28
|
+
/** Unique identifier (AT-URI) */
|
|
29
|
+
uri: string;
|
|
30
|
+
/** Content ID */
|
|
31
|
+
cid: string;
|
|
32
|
+
/** Comment text */
|
|
33
|
+
text: string;
|
|
34
|
+
/** Author information */
|
|
35
|
+
author: Author;
|
|
36
|
+
/** When the comment was created */
|
|
37
|
+
createdAt: Date;
|
|
38
|
+
/** Source platform */
|
|
39
|
+
source: 'bluesky' | 'leaflet' | 'whitewind' | 'unknown';
|
|
40
|
+
/** URL to view the comment on its platform */
|
|
41
|
+
sourceUrl: string;
|
|
42
|
+
/** Parent comment URI (for threaded replies) */
|
|
43
|
+
parentUri?: string;
|
|
44
|
+
/** Like count (if available) */
|
|
45
|
+
likeCount?: number;
|
|
46
|
+
/** Reply count (if available) */
|
|
47
|
+
replyCount?: number;
|
|
48
|
+
/** Nested replies */
|
|
49
|
+
replies?: Comment[];
|
|
50
|
+
}
|
|
51
|
+
export interface FetchCommentsOptions {
|
|
52
|
+
/** AT-URI of the site.standard.document record */
|
|
53
|
+
documentUri?: string;
|
|
54
|
+
/** AT-URI of a Bluesky post that announces/links the document */
|
|
55
|
+
bskyPostUri?: string;
|
|
56
|
+
/** Canonical URL of the blog post (for finding mentions) */
|
|
57
|
+
canonicalUrl?: string;
|
|
58
|
+
/** Maximum depth for nested replies */
|
|
59
|
+
maxDepth?: number;
|
|
60
|
+
/** Maximum total comments to fetch */
|
|
61
|
+
maxComments?: number;
|
|
62
|
+
/** PDS service for API calls */
|
|
63
|
+
service?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Fetch replies to a Bluesky post
|
|
67
|
+
*/
|
|
68
|
+
export declare function fetchBlueskyReplies(postUri: string, options?: {
|
|
69
|
+
maxDepth?: number;
|
|
70
|
+
service?: string;
|
|
71
|
+
}): Promise<Comment[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Search Bluesky for posts mentioning a URL
|
|
74
|
+
* Useful for finding discussions about your blog post
|
|
75
|
+
*/
|
|
76
|
+
export declare function searchBlueskyMentions(url: string, options?: {
|
|
77
|
+
maxResults?: number;
|
|
78
|
+
service?: string;
|
|
79
|
+
}): Promise<Comment[]>;
|
|
80
|
+
/**
|
|
81
|
+
* Build a tree structure from flat comments
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildCommentTree(comments: Comment[]): Comment[];
|
|
84
|
+
/**
|
|
85
|
+
* Fetch all comments for a document from multiple sources
|
|
86
|
+
*/
|
|
87
|
+
export declare function fetchComments(options: FetchCommentsOptions): Promise<Comment[]>;
|
|
88
|
+
/**
|
|
89
|
+
* Flatten a comment tree back to an array (for simple list display)
|
|
90
|
+
*/
|
|
91
|
+
export declare function flattenComments(tree: Comment[]): Comment[];
|
|
92
|
+
/**
|
|
93
|
+
* Get comment count (including nested replies)
|
|
94
|
+
*/
|
|
95
|
+
export declare function countComments(tree: Comment[]): number;
|
|
96
|
+
export type { FetchCommentsOptions as CommentFetchOptions };
|
|
97
|
+
//# sourceMappingURL=comments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,SAAS,EAAE,IAAI,CAAC;IAChB,sBAAsB;IACtB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IACxD,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACb,GACL,OAAO,CAAC,OAAO,EAAE,CAAC,CAuBpB;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACb,GACL,OAAO,CAAC,OAAO,EAAE,CAAC,CAsBpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAsC/D;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,OAAO,EAAE,CAAC,CA4CpB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAc1D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAcrD;AAED,YAAY,EAAE,oBAAoB,IAAI,mBAAmB,EAAE,CAAC"}
|
package/dist/comments.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment aggregation for standard.site documents
|
|
3
|
+
*
|
|
4
|
+
* Fetches replies/comments from various ATProto platforms:
|
|
5
|
+
* - Bluesky posts that link to your document
|
|
6
|
+
* - Direct replies to announcement posts (via bskyPostRef)
|
|
7
|
+
* - Future: Leaflet comments, WhiteWind reactions
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { fetchComments } from 'astro-standard-site/comments';
|
|
12
|
+
*
|
|
13
|
+
* const comments = await fetchComments({
|
|
14
|
+
* documentUri: 'at://did:plc:xxx/site.standard.document/abc123',
|
|
15
|
+
* bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/xyz789',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // comments is an array of unified Comment objects
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { AtpAgent } from '@atproto/api';
|
|
22
|
+
/**
|
|
23
|
+
* Convert a Bluesky post to our unified Comment format
|
|
24
|
+
*/
|
|
25
|
+
function bskyPostToComment(post, depth = 0) {
|
|
26
|
+
const author = {
|
|
27
|
+
did: post.author.did,
|
|
28
|
+
handle: post.author.handle,
|
|
29
|
+
displayName: post.author.displayName,
|
|
30
|
+
avatar: post.author.avatar,
|
|
31
|
+
};
|
|
32
|
+
// Build the Bluesky URL
|
|
33
|
+
const postId = post.uri.split('/').pop();
|
|
34
|
+
const sourceUrl = `https://bsky.app/profile/${post.author.handle}/post/${postId}`;
|
|
35
|
+
const comment = {
|
|
36
|
+
uri: post.uri,
|
|
37
|
+
cid: post.cid,
|
|
38
|
+
text: post.record?.text || '',
|
|
39
|
+
author,
|
|
40
|
+
createdAt: new Date(post.record?.createdAt || post.indexedAt),
|
|
41
|
+
source: 'bluesky',
|
|
42
|
+
sourceUrl,
|
|
43
|
+
likeCount: post.likeCount,
|
|
44
|
+
replyCount: post.replyCount,
|
|
45
|
+
};
|
|
46
|
+
// Extract parent if this is a reply
|
|
47
|
+
if (post.record?.reply?.parent?.uri) {
|
|
48
|
+
comment.parentUri = post.record.reply.parent.uri;
|
|
49
|
+
}
|
|
50
|
+
return comment;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Recursively process a thread tree from Bluesky
|
|
54
|
+
*/
|
|
55
|
+
function processThread(thread, maxDepth, currentDepth = 0) {
|
|
56
|
+
const comments = [];
|
|
57
|
+
if (!thread || currentDepth > maxDepth)
|
|
58
|
+
return comments;
|
|
59
|
+
// Process the current post if it's a reply (not the root)
|
|
60
|
+
if (thread.post && currentDepth > 0) {
|
|
61
|
+
comments.push(bskyPostToComment(thread.post, currentDepth));
|
|
62
|
+
}
|
|
63
|
+
// Process replies
|
|
64
|
+
if (thread.replies && Array.isArray(thread.replies)) {
|
|
65
|
+
for (const reply of thread.replies) {
|
|
66
|
+
const replyComments = processThread(reply, maxDepth, currentDepth + 1);
|
|
67
|
+
comments.push(...replyComments);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return comments;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Fetch replies to a Bluesky post
|
|
74
|
+
*/
|
|
75
|
+
export async function fetchBlueskyReplies(postUri, options = {}) {
|
|
76
|
+
const { maxDepth = 3, service = 'https://public.api.bsky.app' } = options;
|
|
77
|
+
const agent = new AtpAgent({ service });
|
|
78
|
+
try {
|
|
79
|
+
const response = await agent.api.app.bsky.feed.getPostThread({
|
|
80
|
+
uri: postUri,
|
|
81
|
+
depth: maxDepth,
|
|
82
|
+
});
|
|
83
|
+
if (!response.data.thread || response.data.thread.$type === 'app.bsky.feed.defs#notFoundPost') {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
// Process the thread, skipping the root post (we only want replies)
|
|
87
|
+
const comments = processThread(response.data.thread, maxDepth, 0);
|
|
88
|
+
return comments;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('Failed to fetch Bluesky replies:', error);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Search Bluesky for posts mentioning a URL
|
|
97
|
+
* Useful for finding discussions about your blog post
|
|
98
|
+
*/
|
|
99
|
+
export async function searchBlueskyMentions(url, options = {}) {
|
|
100
|
+
const { maxResults = 25, service = 'https://public.api.bsky.app' } = options;
|
|
101
|
+
const agent = new AtpAgent({ service });
|
|
102
|
+
try {
|
|
103
|
+
// Search for posts containing the URL
|
|
104
|
+
const response = await agent.api.app.bsky.feed.searchPosts({
|
|
105
|
+
q: url,
|
|
106
|
+
limit: maxResults,
|
|
107
|
+
});
|
|
108
|
+
if (!response.data.posts) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
// Convert to our Comment format
|
|
112
|
+
return response.data.posts.map(post => bskyPostToComment(post));
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error('Failed to search Bluesky mentions:', error);
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Build a tree structure from flat comments
|
|
121
|
+
*/
|
|
122
|
+
export function buildCommentTree(comments) {
|
|
123
|
+
const commentMap = new Map();
|
|
124
|
+
const rootComments = [];
|
|
125
|
+
// First pass: index all comments
|
|
126
|
+
for (const comment of comments) {
|
|
127
|
+
commentMap.set(comment.uri, { ...comment, replies: [] });
|
|
128
|
+
}
|
|
129
|
+
// Second pass: build tree
|
|
130
|
+
for (const comment of comments) {
|
|
131
|
+
const node = commentMap.get(comment.uri);
|
|
132
|
+
if (comment.parentUri && commentMap.has(comment.parentUri)) {
|
|
133
|
+
const parent = commentMap.get(comment.parentUri);
|
|
134
|
+
parent.replies = parent.replies || [];
|
|
135
|
+
parent.replies.push(node);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
rootComments.push(node);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Sort by date (oldest first for conversations)
|
|
142
|
+
const sortByDate = (a, b) => a.createdAt.getTime() - b.createdAt.getTime();
|
|
143
|
+
const sortTree = (comments) => {
|
|
144
|
+
comments.sort(sortByDate);
|
|
145
|
+
for (const comment of comments) {
|
|
146
|
+
if (comment.replies?.length) {
|
|
147
|
+
sortTree(comment.replies);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
sortTree(rootComments);
|
|
152
|
+
return rootComments;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fetch all comments for a document from multiple sources
|
|
156
|
+
*/
|
|
157
|
+
export async function fetchComments(options) {
|
|
158
|
+
const { bskyPostUri, canonicalUrl, maxDepth = 3, maxComments = 100, service = 'https://public.api.bsky.app', } = options;
|
|
159
|
+
const allComments = [];
|
|
160
|
+
// Fetch replies to the announcement post
|
|
161
|
+
if (bskyPostUri) {
|
|
162
|
+
const replies = await fetchBlueskyReplies(bskyPostUri, { maxDepth, service });
|
|
163
|
+
allComments.push(...replies);
|
|
164
|
+
}
|
|
165
|
+
// Search for mentions of the canonical URL
|
|
166
|
+
if (canonicalUrl) {
|
|
167
|
+
const mentions = await searchBlueskyMentions(canonicalUrl, {
|
|
168
|
+
maxResults: Math.max(10, maxComments - allComments.length),
|
|
169
|
+
service,
|
|
170
|
+
});
|
|
171
|
+
// Filter out duplicates and the original post
|
|
172
|
+
const existingUris = new Set(allComments.map(c => c.uri));
|
|
173
|
+
if (bskyPostUri)
|
|
174
|
+
existingUris.add(bskyPostUri);
|
|
175
|
+
for (const mention of mentions) {
|
|
176
|
+
if (!existingUris.has(mention.uri)) {
|
|
177
|
+
allComments.push(mention);
|
|
178
|
+
existingUris.add(mention.uri);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Future: Add Leaflet comment fetching
|
|
183
|
+
// Future: Add WhiteWind reaction fetching
|
|
184
|
+
// Limit total comments
|
|
185
|
+
const limitedComments = allComments.slice(0, maxComments);
|
|
186
|
+
// Build into a tree structure
|
|
187
|
+
return buildCommentTree(limitedComments);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Flatten a comment tree back to an array (for simple list display)
|
|
191
|
+
*/
|
|
192
|
+
export function flattenComments(tree) {
|
|
193
|
+
const flat = [];
|
|
194
|
+
const walk = (comments, depth = 0) => {
|
|
195
|
+
for (const comment of comments) {
|
|
196
|
+
flat.push({ ...comment, replies: undefined });
|
|
197
|
+
if (comment.replies?.length) {
|
|
198
|
+
walk(comment.replies, depth + 1);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
walk(tree);
|
|
203
|
+
return flat;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get comment count (including nested replies)
|
|
207
|
+
*/
|
|
208
|
+
export function countComments(tree) {
|
|
209
|
+
let count = 0;
|
|
210
|
+
const walk = (comments) => {
|
|
211
|
+
for (const comment of comments) {
|
|
212
|
+
count++;
|
|
213
|
+
if (comment.replies?.length) {
|
|
214
|
+
walk(comment.replies);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
walk(tree);
|
|
219
|
+
return count;
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=comments.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAiDxC;;GAEG;AACH,SAAS,iBAAiB,CAAC,IAAS,EAAE,KAAK,GAAG,CAAC;IAC7C,MAAM,MAAM,GAAW;QACrB,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,GAAG;QACpB,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;QAC1B,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,WAAW;QACpC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,MAAM;KAC3B,CAAC;IAEF,wBAAwB;IACxB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,4BAA4B,IAAI,CAAC,MAAM,CAAC,MAAM,SAAS,MAAM,EAAE,CAAC;IAElF,MAAM,OAAO,GAAY;QACvB,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE;QAC7B,MAAM;QACN,SAAS,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,SAAS,IAAI,IAAI,CAAC,SAAS,CAAC;QAC7D,MAAM,EAAE,SAAS;QACjB,SAAS;QACT,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,UAAU,EAAE,IAAI,CAAC,UAAU;KAC5B,CAAC;IAEF,oCAAoC;IACpC,IAAI,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QACpC,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC;IACnD,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,MAAW,EAAE,QAAgB,EAAE,YAAY,GAAG,CAAC;IACpE,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,IAAI,CAAC,MAAM,IAAI,YAAY,GAAG,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAExD,0DAA0D;IAC1D,IAAI,MAAM,CAAC,IAAI,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QACpC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC,CAAC;IAC9D,CAAC;IAED,kBAAkB;IAClB,IAAI,MAAM,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnC,MAAM,aAAa,GAAG,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC;YACvE,QAAQ,CAAC,IAAI,CAAC,GAAG,aAAa,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,OAAe,EACf,UAGI,EAAE;IAEN,MAAM,EAAE,QAAQ,GAAG,CAAC,EAAE,OAAO,GAAG,6BAA6B,EAAE,GAAG,OAAO,CAAC;IAE1E,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC;YAC3D,GAAG,EAAE,OAAO;YACZ,KAAK,EAAE,QAAQ;SAChB,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,iCAAiC,EAAE,CAAC;YAC9F,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,oEAAoE;QACpE,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAElE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kCAAkC,EAAE,KAAK,CAAC,CAAC;QACzD,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,GAAW,EACX,UAGI,EAAE;IAEN,MAAM,EAAE,UAAU,GAAG,EAAE,EAAE,OAAO,GAAG,6BAA6B,EAAE,GAAG,OAAO,CAAC;IAE7E,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IAExC,IAAI,CAAC;QACH,sCAAsC;QACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;YACzD,CAAC,EAAE,GAAG;YACN,KAAK,EAAE,UAAU;SAClB,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YACzB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,gCAAgC;QAChC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IAClE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,KAAK,CAAC,CAAC;QAC3D,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAmB;IAClD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAmB,CAAC;IAC9C,MAAM,YAAY,GAAc,EAAE,CAAC;IAEnC,iCAAiC;IACjC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,0BAA0B;IAC1B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAE,CAAC;QAE1C,IAAI,OAAO,CAAC,SAAS,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3D,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAE,CAAC;YAClD,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;YACtC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,gDAAgD;IAChD,MAAM,UAAU,GAAG,CAAC,CAAU,EAAE,CAAU,EAAE,EAAE,CAC5C,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAEhD,MAAM,QAAQ,GAAG,CAAC,QAAmB,EAAE,EAAE;QACvC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC1B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;gBAC5B,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,QAAQ,CAAC,YAAY,CAAC,CAAC;IAEvB,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAA6B;IAE7B,MAAM,EACJ,WAAW,EACX,YAAY,EACZ,QAAQ,GAAG,CAAC,EACZ,WAAW,GAAG,GAAG,EACjB,OAAO,GAAG,6BAA6B,GACxC,GAAG,OAAO,CAAC;IAEZ,MAAM,WAAW,GAAc,EAAE,CAAC;IAElC,yCAAyC;IACzC,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC,WAAW,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QAC9E,WAAW,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;IAC/B,CAAC;IAED,2CAA2C;IAC3C,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,QAAQ,GAAG,MAAM,qBAAqB,CAAC,YAAY,EAAE;YACzD,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC;YAC1D,OAAO;SACR,CAAC,CAAC;QAEH,8CAA8C;QAC9C,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1D,IAAI,WAAW;YAAE,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAE/C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACnC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC1B,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAChC,CAAC;QACH,CAAC;IACH,CAAC;IAED,uCAAuC;IACvC,0CAA0C;IAE1C,uBAAuB;IACvB,MAAM,eAAe,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;IAE1D,8BAA8B;IAC9B,OAAO,gBAAgB,CAAC,eAAe,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,IAAe;IAC7C,MAAM,IAAI,GAAc,EAAE,CAAC;IAE3B,MAAM,IAAI,GAAG,CAAC,QAAmB,EAAE,KAAK,GAAG,CAAC,EAAE,EAAE;QAC9C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;YAC9C,IAAI,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;gBAC5B,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,CAAC;IACX,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAAC,IAAe;IAC3C,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,MAAM,IAAI,GAAG,CAAC,QAAmB,EAAE,EAAE;QACnC,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,KAAK,EAAE,CAAC;YACR,IAAI,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;gBAC5B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACxB,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,IAAI,CAAC,CAAC;IACX,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content transformation utilities for standard.site
|
|
3
|
+
*
|
|
4
|
+
* Converts Astro blog post content into formats suitable for ATProto:
|
|
5
|
+
* - Strips markdown to plain text for `textContent` (search/indexing)
|
|
6
|
+
* - Converts custom sidenotes to standard markdown
|
|
7
|
+
* - Resolves relative links to absolute URLs
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { transformContent, stripToPlainText } from 'astro-standard-site/content';
|
|
12
|
+
*
|
|
13
|
+
* const result = transformContent(markdown, {
|
|
14
|
+
* siteUrl: 'https://bryanguffey.com',
|
|
15
|
+
* postPath: '/blog/my-post',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // result.markdown - cleaned markdown for ATProto
|
|
19
|
+
* // result.textContent - plain text for search
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export interface TransformOptions {
|
|
23
|
+
/** Base URL of your site (e.g., 'https://bryanguffey.com') */
|
|
24
|
+
siteUrl: string;
|
|
25
|
+
/** Path to the current post (e.g., '/blog/my-post') */
|
|
26
|
+
postPath?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface TransformResult {
|
|
29
|
+
/** Transformed markdown suitable for ATProto */
|
|
30
|
+
markdown: string;
|
|
31
|
+
/** Plain text version for textContent field */
|
|
32
|
+
textContent: string;
|
|
33
|
+
/** Word count */
|
|
34
|
+
wordCount: number;
|
|
35
|
+
/** Estimated reading time in minutes */
|
|
36
|
+
readingTime: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Convert HTML sidenotes to markdown blockquotes
|
|
40
|
+
*
|
|
41
|
+
* Transforms:
|
|
42
|
+
* ```html
|
|
43
|
+
* <div class="sidenote sidenote--tip">
|
|
44
|
+
* <span class="sidenote-label">Tip</span>
|
|
45
|
+
* <p>Content here</p>
|
|
46
|
+
* </div>
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* Into:
|
|
50
|
+
* ```markdown
|
|
51
|
+
* > **Tip:** Content here
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export declare function convertSidenotes(markdown: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* Convert HTML sidenotes (multi-paragraph) to markdown
|
|
57
|
+
* Handles more complex sidenote structures
|
|
58
|
+
*/
|
|
59
|
+
export declare function convertComplexSidenotes(markdown: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* Resolve relative URLs to absolute URLs
|
|
62
|
+
*
|
|
63
|
+
* Converts:
|
|
64
|
+
* - `[Link](/page)` → `[Link](https://example.com/page)`
|
|
65
|
+
* - `` → ``
|
|
66
|
+
*/
|
|
67
|
+
export declare function resolveRelativeLinks(markdown: string, siteUrl: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Strip markdown to plain text for indexing/search
|
|
70
|
+
*
|
|
71
|
+
* Removes:
|
|
72
|
+
* - Markdown formatting (**, *, #, etc.)
|
|
73
|
+
* - Links (keeps link text)
|
|
74
|
+
* - Images
|
|
75
|
+
* - Code blocks
|
|
76
|
+
* - HTML tags
|
|
77
|
+
*/
|
|
78
|
+
export declare function stripToPlainText(markdown: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* Calculate word count from text
|
|
81
|
+
*/
|
|
82
|
+
export declare function countWords(text: string): number;
|
|
83
|
+
/**
|
|
84
|
+
* Calculate reading time in minutes (assuming 200 words per minute)
|
|
85
|
+
*/
|
|
86
|
+
export declare function calculateReadingTime(wordCount: number, wordsPerMinute?: number): number;
|
|
87
|
+
/**
|
|
88
|
+
* Full content transformation pipeline
|
|
89
|
+
*
|
|
90
|
+
* Takes raw markdown (with HTML sidenotes) and produces:
|
|
91
|
+
* - Clean markdown suitable for ATProto platforms
|
|
92
|
+
* - Plain text for search/indexing
|
|
93
|
+
* - Metadata (word count, reading time)
|
|
94
|
+
*/
|
|
95
|
+
export declare function transformContent(rawMarkdown: string, options: TransformOptions): TransformResult;
|
|
96
|
+
/**
|
|
97
|
+
* Transform an Astro blog post entry for standard.site
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { getCollection } from 'astro:content';
|
|
102
|
+
* import { transformPost } from 'astro-standard-site/content';
|
|
103
|
+
*
|
|
104
|
+
* const posts = await getCollection('blog');
|
|
105
|
+
* for (const post of posts) {
|
|
106
|
+
* const standardDoc = transformPost(post, {
|
|
107
|
+
* siteUrl: 'https://bryanguffey.com',
|
|
108
|
+
* });
|
|
109
|
+
* await publisher.publishDocument(standardDoc);
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export interface AstroBlogPost {
|
|
114
|
+
slug: string;
|
|
115
|
+
body: string;
|
|
116
|
+
data: {
|
|
117
|
+
title: string;
|
|
118
|
+
description?: string;
|
|
119
|
+
date: Date;
|
|
120
|
+
tags?: string[];
|
|
121
|
+
draft?: boolean;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export interface StandardSiteDocumentInput {
|
|
125
|
+
site: string;
|
|
126
|
+
title: string;
|
|
127
|
+
publishedAt: string;
|
|
128
|
+
path?: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
tags?: string[];
|
|
131
|
+
textContent?: string;
|
|
132
|
+
content?: {
|
|
133
|
+
$type: string;
|
|
134
|
+
text: string;
|
|
135
|
+
version?: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export declare function transformPost(post: AstroBlogPost, options: {
|
|
139
|
+
siteUrl: string;
|
|
140
|
+
}): StandardSiteDocumentInput;
|
|
141
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAqChE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAY9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA0CzD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,SAAM,GAAG,MAAM,CAEpF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,eAAe,CAuBjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,IAAI,CAAC;QACX,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3B,yBAAyB,CAuB3B"}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content transformation utilities for standard.site
|
|
3
|
+
*
|
|
4
|
+
* Converts Astro blog post content into formats suitable for ATProto:
|
|
5
|
+
* - Strips markdown to plain text for `textContent` (search/indexing)
|
|
6
|
+
* - Converts custom sidenotes to standard markdown
|
|
7
|
+
* - Resolves relative links to absolute URLs
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { transformContent, stripToPlainText } from 'astro-standard-site/content';
|
|
12
|
+
*
|
|
13
|
+
* const result = transformContent(markdown, {
|
|
14
|
+
* siteUrl: 'https://bryanguffey.com',
|
|
15
|
+
* postPath: '/blog/my-post',
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // result.markdown - cleaned markdown for ATProto
|
|
19
|
+
* // result.textContent - plain text for search
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Convert HTML sidenotes to markdown blockquotes
|
|
24
|
+
*
|
|
25
|
+
* Transforms:
|
|
26
|
+
* ```html
|
|
27
|
+
* <div class="sidenote sidenote--tip">
|
|
28
|
+
* <span class="sidenote-label">Tip</span>
|
|
29
|
+
* <p>Content here</p>
|
|
30
|
+
* </div>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* Into:
|
|
34
|
+
* ```markdown
|
|
35
|
+
* > **Tip:** Content here
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export function convertSidenotes(markdown) {
|
|
39
|
+
// Match sidenote divs with various formats
|
|
40
|
+
const sidenoteRegex = /<div\s+class="sidenote(?:\s+sidenote--(warning|tip))?">\s*<span\s+class="sidenote-label">([^<]+)<\/span>\s*<p>([^<]+)<\/p>\s*<\/div>/gi;
|
|
41
|
+
return markdown.replace(sidenoteRegex, (_, type, label, content) => {
|
|
42
|
+
// Clean up the content
|
|
43
|
+
const cleanContent = content.trim();
|
|
44
|
+
const cleanLabel = label.trim();
|
|
45
|
+
// Convert to blockquote with label
|
|
46
|
+
return `\n> **${cleanLabel}:** ${cleanContent}\n`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert HTML sidenotes (multi-paragraph) to markdown
|
|
51
|
+
* Handles more complex sidenote structures
|
|
52
|
+
*/
|
|
53
|
+
export function convertComplexSidenotes(markdown) {
|
|
54
|
+
// First pass: simple sidenotes
|
|
55
|
+
let result = convertSidenotes(markdown);
|
|
56
|
+
// Second pass: sidenotes with multiple paragraphs or nested content
|
|
57
|
+
const complexSidenoteRegex = /<div\s+class="sidenote[^"]*">([\s\S]*?)<\/div>/gi;
|
|
58
|
+
result = result.replace(complexSidenoteRegex, (match, innerContent) => {
|
|
59
|
+
// Extract label if present
|
|
60
|
+
const labelMatch = innerContent.match(/<span\s+class="sidenote-label">([^<]+)<\/span>/i);
|
|
61
|
+
const label = labelMatch ? labelMatch[1].trim() : 'Note';
|
|
62
|
+
// Remove the label span
|
|
63
|
+
let content = innerContent.replace(/<span\s+class="sidenote-label">[^<]+<\/span>/gi, '');
|
|
64
|
+
// Convert remaining HTML to plain text with basic formatting
|
|
65
|
+
content = content
|
|
66
|
+
.replace(/<p>/gi, '')
|
|
67
|
+
.replace(/<\/p>/gi, '\n')
|
|
68
|
+
.replace(/<strong>/gi, '**')
|
|
69
|
+
.replace(/<\/strong>/gi, '**')
|
|
70
|
+
.replace(/<em>/gi, '*')
|
|
71
|
+
.replace(/<\/em>/gi, '*')
|
|
72
|
+
.replace(/<a\s+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi, '[$2]($1)')
|
|
73
|
+
.replace(/<[^>]+>/g, '') // Remove any remaining HTML tags
|
|
74
|
+
.trim();
|
|
75
|
+
// Format as blockquote
|
|
76
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
77
|
+
const quotedLines = lines.map((line, i) => i === 0 ? `> **${label}:** ${line}` : `> ${line}`);
|
|
78
|
+
return '\n' + quotedLines.join('\n') + '\n';
|
|
79
|
+
});
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Resolve relative URLs to absolute URLs
|
|
84
|
+
*
|
|
85
|
+
* Converts:
|
|
86
|
+
* - `[Link](/page)` → `[Link](https://example.com/page)`
|
|
87
|
+
* - `` → ``
|
|
88
|
+
*/
|
|
89
|
+
export function resolveRelativeLinks(markdown, siteUrl) {
|
|
90
|
+
const baseUrl = siteUrl.replace(/\/$/, '');
|
|
91
|
+
// Match markdown links and images with relative URLs
|
|
92
|
+
// [text](/path) or 
|
|
93
|
+
const linkRegex = /(!?\[[^\]]*\])\((?!https?:\/\/|mailto:|#)([^)]+)\)/g;
|
|
94
|
+
return markdown.replace(linkRegex, (_, prefix, path) => {
|
|
95
|
+
// Ensure path starts with /
|
|
96
|
+
const absolutePath = path.startsWith('/') ? path : `/${path}`;
|
|
97
|
+
return `${prefix}(${baseUrl}${absolutePath})`;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Strip markdown to plain text for indexing/search
|
|
102
|
+
*
|
|
103
|
+
* Removes:
|
|
104
|
+
* - Markdown formatting (**, *, #, etc.)
|
|
105
|
+
* - Links (keeps link text)
|
|
106
|
+
* - Images
|
|
107
|
+
* - Code blocks
|
|
108
|
+
* - HTML tags
|
|
109
|
+
*/
|
|
110
|
+
export function stripToPlainText(markdown) {
|
|
111
|
+
let text = markdown;
|
|
112
|
+
// Remove code blocks first (preserve nothing)
|
|
113
|
+
text = text.replace(/```[\s\S]*?```/g, '');
|
|
114
|
+
text = text.replace(/`[^`]+`/g, '');
|
|
115
|
+
// Remove images
|
|
116
|
+
text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, '');
|
|
117
|
+
// Convert links to just their text
|
|
118
|
+
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
|
119
|
+
// Remove HTML tags
|
|
120
|
+
text = text.replace(/<[^>]+>/g, '');
|
|
121
|
+
// Remove heading markers
|
|
122
|
+
text = text.replace(/^#{1,6}\s+/gm, '');
|
|
123
|
+
// Remove bold/italic markers
|
|
124
|
+
text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
|
|
125
|
+
text = text.replace(/\*([^*]+)\*/g, '$1');
|
|
126
|
+
text = text.replace(/__([^_]+)__/g, '$1');
|
|
127
|
+
text = text.replace(/_([^_]+)_/g, '$1');
|
|
128
|
+
// Remove blockquote markers
|
|
129
|
+
text = text.replace(/^>\s*/gm, '');
|
|
130
|
+
// Remove list markers
|
|
131
|
+
text = text.replace(/^[\s]*[-*+]\s+/gm, '');
|
|
132
|
+
text = text.replace(/^[\s]*\d+\.\s+/gm, '');
|
|
133
|
+
// Remove horizontal rules
|
|
134
|
+
text = text.replace(/^[-*_]{3,}$/gm, '');
|
|
135
|
+
// Collapse multiple newlines
|
|
136
|
+
text = text.replace(/\n{3,}/g, '\n\n');
|
|
137
|
+
// Trim whitespace
|
|
138
|
+
text = text.trim();
|
|
139
|
+
return text;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Calculate word count from text
|
|
143
|
+
*/
|
|
144
|
+
export function countWords(text) {
|
|
145
|
+
return text.split(/\s+/).filter(word => word.length > 0).length;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Calculate reading time in minutes (assuming 200 words per minute)
|
|
149
|
+
*/
|
|
150
|
+
export function calculateReadingTime(wordCount, wordsPerMinute = 200) {
|
|
151
|
+
return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Full content transformation pipeline
|
|
155
|
+
*
|
|
156
|
+
* Takes raw markdown (with HTML sidenotes) and produces:
|
|
157
|
+
* - Clean markdown suitable for ATProto platforms
|
|
158
|
+
* - Plain text for search/indexing
|
|
159
|
+
* - Metadata (word count, reading time)
|
|
160
|
+
*/
|
|
161
|
+
export function transformContent(rawMarkdown, options) {
|
|
162
|
+
// Step 1: Convert sidenotes from HTML to markdown blockquotes
|
|
163
|
+
let markdown = convertComplexSidenotes(rawMarkdown);
|
|
164
|
+
// Step 2: Resolve relative links to absolute URLs
|
|
165
|
+
markdown = resolveRelativeLinks(markdown, options.siteUrl);
|
|
166
|
+
// Step 3: Clean up extra whitespace
|
|
167
|
+
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
|
|
168
|
+
// Step 4: Generate plain text version
|
|
169
|
+
const textContent = stripToPlainText(markdown);
|
|
170
|
+
// Step 5: Calculate metadata
|
|
171
|
+
const wordCount = countWords(textContent);
|
|
172
|
+
const readingTime = calculateReadingTime(wordCount);
|
|
173
|
+
return {
|
|
174
|
+
markdown,
|
|
175
|
+
textContent,
|
|
176
|
+
wordCount,
|
|
177
|
+
readingTime,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
export function transformPost(post, options) {
|
|
181
|
+
const postPath = `/blog/${post.slug}`;
|
|
182
|
+
// Transform the content
|
|
183
|
+
const transformed = transformContent(post.body, {
|
|
184
|
+
siteUrl: options.siteUrl,
|
|
185
|
+
postPath,
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
site: options.siteUrl,
|
|
189
|
+
title: post.data.title,
|
|
190
|
+
publishedAt: post.data.date.toISOString(),
|
|
191
|
+
path: postPath,
|
|
192
|
+
description: post.data.description,
|
|
193
|
+
tags: post.data.tags,
|
|
194
|
+
textContent: transformed.textContent,
|
|
195
|
+
content: {
|
|
196
|
+
$type: 'site.standard.content.markdown',
|
|
197
|
+
text: transformed.markdown,
|
|
198
|
+
version: '1.0',
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=content.js.map
|