@ewanc26/svelte-standard-site 0.2.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.
Files changed (160) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +108 -0
  3. package/dist/__tests__/content.test.d.ts +4 -0
  4. package/dist/__tests__/content.test.js +128 -0
  5. package/dist/client.d.ts +71 -0
  6. package/dist/client.js +307 -0
  7. package/dist/components/Comments.svelte +277 -0
  8. package/dist/components/Comments.svelte.d.ts +17 -0
  9. package/dist/components/DocumentCard.svelte +95 -0
  10. package/dist/components/DocumentCard.svelte.d.ts +11 -0
  11. package/dist/components/PublicationCard.svelte +54 -0
  12. package/dist/components/PublicationCard.svelte.d.ts +9 -0
  13. package/dist/components/StandardSiteLayout.svelte +102 -0
  14. package/dist/components/StandardSiteLayout.svelte.d.ts +18 -0
  15. package/dist/components/ThemeToggle.svelte +55 -0
  16. package/dist/components/ThemeToggle.svelte.d.ts +6 -0
  17. package/dist/components/common/DateDisplay.svelte +38 -0
  18. package/dist/components/common/DateDisplay.svelte.d.ts +11 -0
  19. package/dist/components/common/TagList.svelte +31 -0
  20. package/dist/components/common/TagList.svelte.d.ts +8 -0
  21. package/dist/components/common/ThemedCard.svelte +65 -0
  22. package/dist/components/common/ThemedCard.svelte.d.ts +11 -0
  23. package/dist/components/common/ThemedContainer.svelte +55 -0
  24. package/dist/components/common/ThemedContainer.svelte.d.ts +11 -0
  25. package/dist/components/common/ThemedText.svelte +75 -0
  26. package/dist/components/common/ThemedText.svelte.d.ts +11 -0
  27. package/dist/components/document/BlockRenderer.svelte +67 -0
  28. package/dist/components/document/BlockRenderer.svelte.d.ts +9 -0
  29. package/dist/components/document/CanvasRenderer.svelte +41 -0
  30. package/dist/components/document/CanvasRenderer.svelte.d.ts +22 -0
  31. package/dist/components/document/DocumentRenderer.svelte +68 -0
  32. package/dist/components/document/DocumentRenderer.svelte.d.ts +17 -0
  33. package/dist/components/document/InlineMath.svelte +41 -0
  34. package/dist/components/document/InlineMath.svelte.d.ts +7 -0
  35. package/dist/components/document/LeafletContentRenderer.svelte +64 -0
  36. package/dist/components/document/LeafletContentRenderer.svelte.d.ts +36 -0
  37. package/dist/components/document/LinearDocumentRenderer.svelte +45 -0
  38. package/dist/components/document/LinearDocumentRenderer.svelte.d.ts +18 -0
  39. package/dist/components/document/MarkdownRenderer.svelte +62 -0
  40. package/dist/components/document/MarkdownRenderer.svelte.d.ts +10 -0
  41. package/dist/components/document/RichText.svelte +272 -0
  42. package/dist/components/document/RichText.svelte.d.ts +18 -0
  43. package/dist/components/document/blocks/BlockquoteBlock.svelte +29 -0
  44. package/dist/components/document/blocks/BlockquoteBlock.svelte.d.ts +10 -0
  45. package/dist/components/document/blocks/BskyPostBlock.svelte +202 -0
  46. package/dist/components/document/blocks/BskyPostBlock.svelte.d.ts +13 -0
  47. package/dist/components/document/blocks/ButtonBlock.svelte +24 -0
  48. package/dist/components/document/blocks/ButtonBlock.svelte.d.ts +10 -0
  49. package/dist/components/document/blocks/CodeBlock.svelte +68 -0
  50. package/dist/components/document/blocks/CodeBlock.svelte.d.ts +12 -0
  51. package/dist/components/document/blocks/HeaderBlock.svelte +56 -0
  52. package/dist/components/document/blocks/HeaderBlock.svelte.d.ts +11 -0
  53. package/dist/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
  54. package/dist/components/document/blocks/HorizontalRuleBlock.svelte.d.ts +6 -0
  55. package/dist/components/document/blocks/IframeBlock.svelte +32 -0
  56. package/dist/components/document/blocks/IframeBlock.svelte.d.ts +10 -0
  57. package/dist/components/document/blocks/ImageBlock.svelte +55 -0
  58. package/dist/components/document/blocks/ImageBlock.svelte.d.ts +25 -0
  59. package/dist/components/document/blocks/MathBlock.svelte +34 -0
  60. package/dist/components/document/blocks/MathBlock.svelte.d.ts +10 -0
  61. package/dist/components/document/blocks/PageBlock.svelte +66 -0
  62. package/dist/components/document/blocks/PageBlock.svelte.d.ts +10 -0
  63. package/dist/components/document/blocks/PollBlock.svelte +122 -0
  64. package/dist/components/document/blocks/PollBlock.svelte.d.ts +27 -0
  65. package/dist/components/document/blocks/TextBlock.svelte +26 -0
  66. package/dist/components/document/blocks/TextBlock.svelte.d.ts +11 -0
  67. package/dist/components/document/blocks/UnorderedListBlock.svelte +71 -0
  68. package/dist/components/document/blocks/UnorderedListBlock.svelte.d.ts +9 -0
  69. package/dist/components/document/blocks/WebsiteBlock.svelte +81 -0
  70. package/dist/components/document/blocks/WebsiteBlock.svelte.d.ts +21 -0
  71. package/dist/components/index.d.ts +11 -0
  72. package/dist/components/index.js +13 -0
  73. package/dist/config/env.d.ts +11 -0
  74. package/dist/config/env.js +26 -0
  75. package/dist/index.d.ts +20 -0
  76. package/dist/index.js +23 -0
  77. package/dist/publisher.d.ts +193 -0
  78. package/dist/publisher.js +349 -0
  79. package/dist/schemas.d.ts +626 -0
  80. package/dist/schemas.js +113 -0
  81. package/dist/stores/index.d.ts +1 -0
  82. package/dist/stores/index.js +1 -0
  83. package/dist/stores/theme.d.ts +11 -0
  84. package/dist/stores/theme.js +67 -0
  85. package/dist/styles/base.css +188 -0
  86. package/dist/styles/themes.css +5 -0
  87. package/dist/types.d.ts +106 -0
  88. package/dist/types.js +4 -0
  89. package/dist/utils/agents.d.ts +35 -0
  90. package/dist/utils/agents.js +96 -0
  91. package/dist/utils/at-uri.d.ts +50 -0
  92. package/dist/utils/at-uri.js +71 -0
  93. package/dist/utils/cache.d.ts +14 -0
  94. package/dist/utils/cache.js +33 -0
  95. package/dist/utils/comments.d.ts +61 -0
  96. package/dist/utils/comments.js +159 -0
  97. package/dist/utils/content.d.ts +94 -0
  98. package/dist/utils/content.js +178 -0
  99. package/dist/utils/document.d.ts +23 -0
  100. package/dist/utils/document.js +33 -0
  101. package/dist/utils/theme-helpers.d.ts +34 -0
  102. package/dist/utils/theme-helpers.js +63 -0
  103. package/dist/utils/theme.d.ts +18 -0
  104. package/dist/utils/theme.js +24 -0
  105. package/dist/utils/verification.d.ts +129 -0
  106. package/dist/utils/verification.js +157 -0
  107. package/package.json +139 -0
  108. package/src/lib/__tests__/content.test.ts +155 -0
  109. package/src/lib/client.ts +368 -0
  110. package/src/lib/components/Comments.svelte +277 -0
  111. package/src/lib/components/DocumentCard.svelte +95 -0
  112. package/src/lib/components/PublicationCard.svelte +54 -0
  113. package/src/lib/components/StandardSiteLayout.svelte +102 -0
  114. package/src/lib/components/ThemeToggle.svelte +55 -0
  115. package/src/lib/components/common/DateDisplay.svelte +38 -0
  116. package/src/lib/components/common/TagList.svelte +31 -0
  117. package/src/lib/components/common/ThemedCard.svelte +65 -0
  118. package/src/lib/components/common/ThemedContainer.svelte +55 -0
  119. package/src/lib/components/common/ThemedText.svelte +75 -0
  120. package/src/lib/components/document/BlockRenderer.svelte +67 -0
  121. package/src/lib/components/document/CanvasRenderer.svelte +41 -0
  122. package/src/lib/components/document/DocumentRenderer.svelte +68 -0
  123. package/src/lib/components/document/InlineMath.svelte +41 -0
  124. package/src/lib/components/document/LeafletContentRenderer.svelte +64 -0
  125. package/src/lib/components/document/LinearDocumentRenderer.svelte +45 -0
  126. package/src/lib/components/document/MarkdownRenderer.svelte +62 -0
  127. package/src/lib/components/document/RichText.svelte +272 -0
  128. package/src/lib/components/document/blocks/BlockquoteBlock.svelte +29 -0
  129. package/src/lib/components/document/blocks/BskyPostBlock.svelte +202 -0
  130. package/src/lib/components/document/blocks/ButtonBlock.svelte +24 -0
  131. package/src/lib/components/document/blocks/CodeBlock.svelte +68 -0
  132. package/src/lib/components/document/blocks/HeaderBlock.svelte +56 -0
  133. package/src/lib/components/document/blocks/HorizontalRuleBlock.svelte +14 -0
  134. package/src/lib/components/document/blocks/IframeBlock.svelte +32 -0
  135. package/src/lib/components/document/blocks/ImageBlock.svelte +55 -0
  136. package/src/lib/components/document/blocks/MathBlock.svelte +34 -0
  137. package/src/lib/components/document/blocks/PageBlock.svelte +66 -0
  138. package/src/lib/components/document/blocks/PollBlock.svelte +122 -0
  139. package/src/lib/components/document/blocks/TextBlock.svelte +26 -0
  140. package/src/lib/components/document/blocks/UnorderedListBlock.svelte +71 -0
  141. package/src/lib/components/document/blocks/WebsiteBlock.svelte +81 -0
  142. package/src/lib/components/index.ts +15 -0
  143. package/src/lib/config/env.ts +31 -0
  144. package/src/lib/index.ts +104 -0
  145. package/src/lib/publisher.ts +489 -0
  146. package/src/lib/schemas.ts +137 -0
  147. package/src/lib/stores/index.ts +1 -0
  148. package/src/lib/stores/theme.ts +80 -0
  149. package/src/lib/styles/base.css +188 -0
  150. package/src/lib/styles/themes.css +5 -0
  151. package/src/lib/types.ts +116 -0
  152. package/src/lib/utils/agents.ts +124 -0
  153. package/src/lib/utils/at-uri.ts +89 -0
  154. package/src/lib/utils/cache.ts +46 -0
  155. package/src/lib/utils/comments.ts +217 -0
  156. package/src/lib/utils/content.ts +234 -0
  157. package/src/lib/utils/document.ts +41 -0
  158. package/src/lib/utils/theme-helpers.ts +87 -0
  159. package/src/lib/utils/theme.ts +33 -0
  160. package/src/lib/utils/verification.ts +180 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * AT URI parsing and conversion utilities
3
+ */
4
+ /**
5
+ * Parse an AT URI into its components
6
+ * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey
7
+ * @returns Parsed components or null if invalid
8
+ */
9
+ export function parseAtUri(atUri) {
10
+ if (!atUri.startsWith('at://'))
11
+ return null;
12
+ const withoutProtocol = atUri.slice(5); // Remove "at://"
13
+ const parts = withoutProtocol.split('/');
14
+ if (parts.length !== 3)
15
+ return null;
16
+ const [did, collection, rkey] = parts;
17
+ if (!did.startsWith('did:') || !collection || !rkey)
18
+ return null;
19
+ return { did, collection, rkey };
20
+ }
21
+ /**
22
+ * Convert AT URI to HTTPS URL for com.atproto.repo.getRecord
23
+ * @param atUri - AT URI in format: at://did:plc:xxx/collection/rkey
24
+ * @param pdsEndpoint - PDS endpoint (e.g., "https://cortinarius.us-west.host.bsky.network")
25
+ * @returns HTTPS URL for getRecord XRPC call
26
+ */
27
+ export function atUriToHttps(atUri, pdsEndpoint) {
28
+ const parsed = parseAtUri(atUri);
29
+ if (!parsed)
30
+ return null;
31
+ // Ensure PDS endpoint doesn't have trailing slash
32
+ const pds = pdsEndpoint.replace(/\/$/, '');
33
+ return `${pds}/xrpc/com.atproto.repo.getRecord?repo=${encodeURIComponent(parsed.did)}&collection=${encodeURIComponent(parsed.collection)}&rkey=${encodeURIComponent(parsed.rkey)}`;
34
+ }
35
+ /**
36
+ * Construct an AT URI from components
37
+ * @param did - DID of the repository
38
+ * @param collection - Collection name
39
+ * @param rkey - Record key
40
+ * @returns AT URI string
41
+ */
42
+ export function buildAtUri(did, collection, rkey) {
43
+ return `at://${did}/${collection}/${rkey}`;
44
+ }
45
+ /**
46
+ * Extract rkey from an AT URI
47
+ * @param atUri - AT URI
48
+ * @returns rkey or null if invalid
49
+ */
50
+ export function extractRkey(atUri) {
51
+ const parsed = parseAtUri(atUri);
52
+ return parsed?.rkey ?? null;
53
+ }
54
+ /**
55
+ * Validate if string is a valid AT URI
56
+ * @param uri - String to validate
57
+ * @returns true if valid AT URI
58
+ */
59
+ export function isAtUri(uri) {
60
+ return parseAtUri(uri) !== null;
61
+ }
62
+ /**
63
+ * Convert DID to PDS hostname format
64
+ * @param did - DID to convert
65
+ * @returns PDS hostname or null if unable to derive
66
+ */
67
+ export function didToPdsHostname(did) {
68
+ // This is a simplified version - real implementation should use DID resolution
69
+ // For now, we'll return null and rely on explicit PDS configuration
70
+ return null;
71
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Simple in-memory cache with TTL support
3
+ */
4
+ declare class Cache {
5
+ private store;
6
+ private defaultTTL;
7
+ get<T>(key: string): T | null;
8
+ set<T>(key: string, value: T, ttl?: number): void;
9
+ delete(key: string): void;
10
+ clear(): void;
11
+ setDefaultTTL(ttl: number): void;
12
+ }
13
+ export declare const cache: Cache;
14
+ export {};
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Simple in-memory cache with TTL support
3
+ */
4
+ class Cache {
5
+ store = new Map();
6
+ defaultTTL = 5 * 60 * 1000; // 5 minutes
7
+ get(key) {
8
+ const entry = this.store.get(key);
9
+ if (!entry)
10
+ return null;
11
+ if (Date.now() > entry.expires) {
12
+ this.store.delete(key);
13
+ return null;
14
+ }
15
+ return entry.value;
16
+ }
17
+ set(key, value, ttl) {
18
+ this.store.set(key, {
19
+ value,
20
+ expires: Date.now() + (ttl ?? this.defaultTTL)
21
+ });
22
+ }
23
+ delete(key) {
24
+ this.store.delete(key);
25
+ }
26
+ clear() {
27
+ this.store.clear();
28
+ }
29
+ setDefaultTTL(ttl) {
30
+ this.defaultTTL = ttl;
31
+ }
32
+ }
33
+ export const cache = new Cache();
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Comments utilities for fetching Bluesky replies
3
+ *
4
+ * Fetches threaded replies from Bluesky to display as comments on your blog.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { fetchComments } from 'svelte-standard-site/comments';
9
+ *
10
+ * const comments = await fetchComments({
11
+ * bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/abc123',
12
+ * canonicalUrl: 'https://yourblog.com/posts/my-post',
13
+ * maxDepth: 3,
14
+ * });
15
+ * ```
16
+ */
17
+ export interface CommentAuthor {
18
+ did: string;
19
+ handle: string;
20
+ displayName?: string;
21
+ avatar?: string;
22
+ }
23
+ export interface Comment {
24
+ uri: string;
25
+ cid: string;
26
+ author: CommentAuthor;
27
+ text: string;
28
+ createdAt: string;
29
+ likeCount: number;
30
+ replyCount: number;
31
+ replies?: Comment[];
32
+ depth: number;
33
+ }
34
+ export interface FetchCommentsOptions {
35
+ /** AT-URI of the announcement post (e.g., at://did:plc:xxx/app.bsky.feed.post/abc123) */
36
+ bskyPostUri: string;
37
+ /** Optional canonical URL to search for mentions */
38
+ canonicalUrl?: string;
39
+ /** Maximum depth for nested replies (default: 3) */
40
+ maxDepth?: number;
41
+ /** Optional fetch function for SSR */
42
+ fetchFn?: typeof fetch;
43
+ }
44
+ /**
45
+ * Fetch comments for a blog post
46
+ *
47
+ * @param options - Configuration options
48
+ * @returns Array of top-level comments with nested replies
49
+ */
50
+ export declare function fetchComments(options: FetchCommentsOptions): Promise<Comment[]>;
51
+ /**
52
+ * Search for mentions of a URL and fetch those threads as comments
53
+ *
54
+ * This is useful if people share your blog post on Bluesky without
55
+ * replying to a specific announcement post.
56
+ */
57
+ export declare function fetchMentionComments(canonicalUrl: string, maxDepth?: number): Promise<Comment[]>;
58
+ /**
59
+ * Format a relative time string (e.g., "2 hours ago")
60
+ */
61
+ export declare function formatRelativeTime(dateString: string): string;
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Comments utilities for fetching Bluesky replies
3
+ *
4
+ * Fetches threaded replies from Bluesky to display as comments on your blog.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { fetchComments } from 'svelte-standard-site/comments';
9
+ *
10
+ * const comments = await fetchComments({
11
+ * bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/abc123',
12
+ * canonicalUrl: 'https://yourblog.com/posts/my-post',
13
+ * maxDepth: 3,
14
+ * });
15
+ * ```
16
+ */
17
+ import { AtpAgent } from '@atproto/api';
18
+ /**
19
+ * Parse an AT-URI to extract components
20
+ */
21
+ function parseAtUri(uri) {
22
+ const match = uri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
23
+ if (!match)
24
+ return null;
25
+ return {
26
+ did: match[1],
27
+ collection: match[2],
28
+ rkey: match[3]
29
+ };
30
+ }
31
+ /**
32
+ * Fetch a single thread of replies
33
+ */
34
+ async function fetchThread(agent, uri, maxDepth, currentDepth = 0) {
35
+ try {
36
+ const response = await agent.getPostThread({
37
+ uri,
38
+ depth: maxDepth - currentDepth,
39
+ parentHeight: 0
40
+ });
41
+ const thread = response.data.thread;
42
+ if (thread.$type !== 'app.bsky.feed.defs#threadViewPost') {
43
+ return null;
44
+ }
45
+ const post = thread.post;
46
+ // Build comment object
47
+ const comment = {
48
+ uri: post.uri,
49
+ cid: post.cid,
50
+ author: {
51
+ did: post.author.did,
52
+ handle: post.author.handle,
53
+ displayName: post.author.displayName,
54
+ avatar: post.author.avatar
55
+ },
56
+ text: post.record?.text || '',
57
+ createdAt: post.indexedAt,
58
+ likeCount: post.likeCount || 0,
59
+ replyCount: post.replyCount || 0,
60
+ depth: currentDepth,
61
+ replies: []
62
+ };
63
+ // Process replies if within depth limit
64
+ if (thread.replies && currentDepth < maxDepth) {
65
+ for (const reply of thread.replies) {
66
+ if (reply.$type === 'app.bsky.feed.defs#threadViewPost') {
67
+ const replyComment = await fetchThread(agent, reply.post.uri, maxDepth, currentDepth + 1);
68
+ if (replyComment) {
69
+ comment.replies.push(replyComment);
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return comment;
75
+ }
76
+ catch (error) {
77
+ console.error(`Failed to fetch thread for ${uri}:`, error);
78
+ return null;
79
+ }
80
+ }
81
+ /**
82
+ * Fetch comments for a blog post
83
+ *
84
+ * @param options - Configuration options
85
+ * @returns Array of top-level comments with nested replies
86
+ */
87
+ export async function fetchComments(options) {
88
+ const { bskyPostUri, canonicalUrl, maxDepth = 3 } = options;
89
+ // Parse the post URI
90
+ const parsed = parseAtUri(bskyPostUri);
91
+ if (!parsed) {
92
+ console.error('Invalid AT-URI:', bskyPostUri);
93
+ return [];
94
+ }
95
+ try {
96
+ // Create agent
97
+ const agent = new AtpAgent({ service: 'https://public.api.bsky.app' });
98
+ // Fetch the main thread
99
+ const mainComment = await fetchThread(agent, bskyPostUri, maxDepth, 0);
100
+ if (!mainComment || !mainComment.replies) {
101
+ return [];
102
+ }
103
+ // Return only the replies (not the original post)
104
+ return mainComment.replies;
105
+ }
106
+ catch (error) {
107
+ console.error('Failed to fetch comments:', error);
108
+ return [];
109
+ }
110
+ }
111
+ /**
112
+ * Search for mentions of a URL and fetch those threads as comments
113
+ *
114
+ * This is useful if people share your blog post on Bluesky without
115
+ * replying to a specific announcement post.
116
+ */
117
+ export async function fetchMentionComments(canonicalUrl, maxDepth = 3) {
118
+ try {
119
+ const agent = new AtpAgent({ service: 'https://public.api.bsky.app' });
120
+ // Search for posts mentioning the URL
121
+ const searchResponse = await agent.app.bsky.feed.searchPosts({
122
+ q: canonicalUrl,
123
+ limit: 25
124
+ });
125
+ const comments = [];
126
+ for (const post of searchResponse.data.posts) {
127
+ const comment = await fetchThread(agent, post.uri, maxDepth, 0);
128
+ if (comment) {
129
+ comments.push(comment);
130
+ }
131
+ }
132
+ return comments;
133
+ }
134
+ catch (error) {
135
+ console.error('Failed to fetch mention comments:', error);
136
+ return [];
137
+ }
138
+ }
139
+ /**
140
+ * Format a relative time string (e.g., "2 hours ago")
141
+ */
142
+ export function formatRelativeTime(dateString) {
143
+ const date = new Date(dateString);
144
+ const now = new Date();
145
+ const diffMs = now.getTime() - date.getTime();
146
+ const diffSecs = Math.floor(diffMs / 1000);
147
+ const diffMins = Math.floor(diffSecs / 60);
148
+ const diffHours = Math.floor(diffMins / 60);
149
+ const diffDays = Math.floor(diffHours / 24);
150
+ if (diffSecs < 60)
151
+ return 'just now';
152
+ if (diffMins < 60)
153
+ return `${diffMins} minute${diffMins === 1 ? '' : 's'} ago`;
154
+ if (diffHours < 24)
155
+ return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
156
+ if (diffDays < 7)
157
+ return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
158
+ return date.toLocaleDateString();
159
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Content transformation utilities for standard.site
3
+ *
4
+ * Converts 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 'svelte-standard-site/content';
12
+ *
13
+ * const result = transformContent(markdown, {
14
+ * baseUrl: 'https://yourblog.com',
15
+ * });
16
+ *
17
+ * // result.markdown - cleaned markdown for ATProto
18
+ * // result.textContent - plain text for search
19
+ * ```
20
+ */
21
+ export interface TransformOptions {
22
+ /** Base URL of your site (e.g., 'https://yourblog.com') */
23
+ baseUrl: string;
24
+ /** Optional path to the current post (e.g., '/blog/my-post') */
25
+ postPath?: string;
26
+ }
27
+ export interface TransformResult {
28
+ /** Transformed markdown suitable for ATProto */
29
+ markdown: string;
30
+ /** Plain text version for textContent field */
31
+ textContent: string;
32
+ /** Word count */
33
+ wordCount: number;
34
+ /** Estimated reading time in minutes */
35
+ readingTime: number;
36
+ }
37
+ /**
38
+ * Convert HTML sidenotes to markdown blockquotes
39
+ *
40
+ * Transforms:
41
+ * ```html
42
+ * <div class="sidenote sidenote--tip">
43
+ * <span class="sidenote-label">Tip</span>
44
+ * <p>Content here</p>
45
+ * </div>
46
+ * ```
47
+ *
48
+ * Into:
49
+ * ```markdown
50
+ * > **Tip:** Content here
51
+ * ```
52
+ */
53
+ export declare function convertSidenotes(markdown: string): string;
54
+ /**
55
+ * Convert HTML sidenotes (multi-paragraph) to markdown
56
+ * Handles more complex sidenote structures
57
+ */
58
+ export declare function convertComplexSidenotes(markdown: string): string;
59
+ /**
60
+ * Resolve relative URLs to absolute URLs
61
+ *
62
+ * Converts:
63
+ * - `[Link](/page)` → `[Link](https://example.com/page)`
64
+ * - `![Image](/img.png)` → `![Image](https://example.com/img.png)`
65
+ */
66
+ export declare function resolveRelativeLinks(markdown: string, baseUrl: string): string;
67
+ /**
68
+ * Strip markdown to plain text for indexing/search
69
+ *
70
+ * Removes:
71
+ * - Markdown formatting (**, *, #, etc.)
72
+ * - Links (keeps link text)
73
+ * - Images
74
+ * - Code blocks
75
+ * - HTML tags
76
+ */
77
+ export declare function stripToPlainText(markdown: string): string;
78
+ /**
79
+ * Calculate word count from text
80
+ */
81
+ export declare function countWords(text: string): number;
82
+ /**
83
+ * Calculate reading time in minutes (assuming 200 words per minute)
84
+ */
85
+ export declare function calculateReadingTime(wordCount: number, wordsPerMinute?: number): number;
86
+ /**
87
+ * Full content transformation pipeline
88
+ *
89
+ * Takes raw markdown (with HTML sidenotes) and produces:
90
+ * - Clean markdown suitable for ATProto platforms
91
+ * - Plain text for search/indexing
92
+ * - Metadata (word count, reading time)
93
+ */
94
+ export declare function transformContent(rawMarkdown: string, options: TransformOptions): TransformResult;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Content transformation utilities for standard.site
3
+ *
4
+ * Converts 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 'svelte-standard-site/content';
12
+ *
13
+ * const result = transformContent(markdown, {
14
+ * baseUrl: 'https://yourblog.com',
15
+ * });
16
+ *
17
+ * // result.markdown - cleaned markdown for ATProto
18
+ * // result.textContent - plain text for search
19
+ * ```
20
+ */
21
+ /**
22
+ * Convert HTML sidenotes to markdown blockquotes
23
+ *
24
+ * Transforms:
25
+ * ```html
26
+ * <div class="sidenote sidenote--tip">
27
+ * <span class="sidenote-label">Tip</span>
28
+ * <p>Content here</p>
29
+ * </div>
30
+ * ```
31
+ *
32
+ * Into:
33
+ * ```markdown
34
+ * > **Tip:** Content here
35
+ * ```
36
+ */
37
+ export function convertSidenotes(markdown) {
38
+ // Match sidenote divs with various formats
39
+ const sidenoteRegex = /<div\s+class="sidenote(?:\s+sidenote--(warning|tip))?">\s*<span\s+class="sidenote-label">([^<]+)<\/span>\s*<p>([^<]+)<\/p>\s*<\/div>/gi;
40
+ return markdown.replace(sidenoteRegex, (_, type, label, content) => {
41
+ // Clean up the content
42
+ const cleanContent = content.trim();
43
+ const cleanLabel = label.trim();
44
+ // Convert to blockquote with label
45
+ return `\n> **${cleanLabel}:** ${cleanContent}\n`;
46
+ });
47
+ }
48
+ /**
49
+ * Convert HTML sidenotes (multi-paragraph) to markdown
50
+ * Handles more complex sidenote structures
51
+ */
52
+ export function convertComplexSidenotes(markdown) {
53
+ // First pass: simple sidenotes
54
+ let result = convertSidenotes(markdown);
55
+ // Second pass: sidenotes with multiple paragraphs or nested content
56
+ const complexSidenoteRegex = /<div\s+class="sidenote[^"]*">([\s\S]*?)<\/div>/gi;
57
+ result = result.replace(complexSidenoteRegex, (match, innerContent) => {
58
+ // Extract label if present
59
+ const labelMatch = innerContent.match(/<span\s+class="sidenote-label">([^<]+)<\/span>/i);
60
+ const label = labelMatch ? labelMatch[1].trim() : 'Note';
61
+ // Remove the label span
62
+ let content = innerContent.replace(/<span\s+class="sidenote-label">[^<]+<\/span>/gi, '');
63
+ // Convert remaining HTML to plain text with basic formatting
64
+ content = content
65
+ .replace(/<p>/gi, '')
66
+ .replace(/<\/p>/gi, '\n')
67
+ .replace(/<strong>/gi, '**')
68
+ .replace(/<\/strong>/gi, '**')
69
+ .replace(/<em>/gi, '*')
70
+ .replace(/<\/em>/gi, '*')
71
+ .replace(/<a\s+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi, '[$2]($1)')
72
+ .replace(/<[^>]+>/g, '') // Remove any remaining HTML tags
73
+ .trim();
74
+ // Format as blockquote
75
+ const lines = content.split('\n').filter((line) => line.trim());
76
+ const quotedLines = lines.map((line, i) => i === 0 ? `> **${label}:** ${line}` : `> ${line}`);
77
+ return '\n' + quotedLines.join('\n') + '\n';
78
+ });
79
+ return result;
80
+ }
81
+ /**
82
+ * Resolve relative URLs to absolute URLs
83
+ *
84
+ * Converts:
85
+ * - `[Link](/page)` → `[Link](https://example.com/page)`
86
+ * - `![Image](/img.png)` → `![Image](https://example.com/img.png)`
87
+ */
88
+ export function resolveRelativeLinks(markdown, baseUrl) {
89
+ const cleanBaseUrl = baseUrl.replace(/\/$/, '');
90
+ // Match markdown links and images with relative URLs
91
+ // [text](/path) or ![alt](/path)
92
+ const linkRegex = /(!?\[[^\]]*\])\((?!https?:\/\/|mailto:|#)([^)]+)\)/g;
93
+ return markdown.replace(linkRegex, (_, prefix, path) => {
94
+ // Ensure path starts with /
95
+ const absolutePath = path.startsWith('/') ? path : `/${path}`;
96
+ return `${prefix}(${cleanBaseUrl}${absolutePath})`;
97
+ });
98
+ }
99
+ /**
100
+ * Strip markdown to plain text for indexing/search
101
+ *
102
+ * Removes:
103
+ * - Markdown formatting (**, *, #, etc.)
104
+ * - Links (keeps link text)
105
+ * - Images
106
+ * - Code blocks
107
+ * - HTML tags
108
+ */
109
+ export function stripToPlainText(markdown) {
110
+ let text = markdown;
111
+ // Remove code blocks first (preserve nothing)
112
+ text = text.replace(/```[\s\S]*?```/g, '');
113
+ text = text.replace(/`[^`]+`/g, '');
114
+ // Remove images
115
+ text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, '');
116
+ // Convert links to just their text
117
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
118
+ // Remove HTML tags
119
+ text = text.replace(/<[^>]+>/g, '');
120
+ // Remove heading markers
121
+ text = text.replace(/^#{1,6}\s+/gm, '');
122
+ // Remove bold/italic markers
123
+ text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
124
+ text = text.replace(/\*([^*]+)\*/g, '$1');
125
+ text = text.replace(/__([^_]+)__/g, '$1');
126
+ text = text.replace(/_([^_]+)_/g, '$1');
127
+ // Remove blockquote markers
128
+ text = text.replace(/^>\s*/gm, '');
129
+ // Remove list markers
130
+ text = text.replace(/^[\s]*[-*+]\s+/gm, '');
131
+ text = text.replace(/^[\s]*\d+\.\s+/gm, '');
132
+ // Remove horizontal rules
133
+ text = text.replace(/^[-*_]{3,}$/gm, '');
134
+ // Collapse multiple newlines
135
+ text = text.replace(/\n{3,}/g, '\n\n');
136
+ // Trim whitespace
137
+ text = text.trim();
138
+ return text;
139
+ }
140
+ /**
141
+ * Calculate word count from text
142
+ */
143
+ export function countWords(text) {
144
+ return text.split(/\s+/).filter((word) => word.length > 0).length;
145
+ }
146
+ /**
147
+ * Calculate reading time in minutes (assuming 200 words per minute)
148
+ */
149
+ export function calculateReadingTime(wordCount, wordsPerMinute = 200) {
150
+ return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
151
+ }
152
+ /**
153
+ * Full content transformation pipeline
154
+ *
155
+ * Takes raw markdown (with HTML sidenotes) and produces:
156
+ * - Clean markdown suitable for ATProto platforms
157
+ * - Plain text for search/indexing
158
+ * - Metadata (word count, reading time)
159
+ */
160
+ export function transformContent(rawMarkdown, options) {
161
+ // Step 1: Convert sidenotes from HTML to markdown blockquotes
162
+ let markdown = convertComplexSidenotes(rawMarkdown);
163
+ // Step 2: Resolve relative links to absolute URLs
164
+ markdown = resolveRelativeLinks(markdown, options.baseUrl);
165
+ // Step 3: Clean up extra whitespace
166
+ markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
167
+ // Step 4: Generate plain text version
168
+ const textContent = stripToPlainText(markdown);
169
+ // Step 5: Calculate metadata
170
+ const wordCount = countWords(textContent);
171
+ const readingTime = calculateReadingTime(wordCount);
172
+ return {
173
+ markdown,
174
+ textContent,
175
+ wordCount,
176
+ readingTime
177
+ };
178
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Extract rkey from AT URI
3
+ */
4
+ export declare function extractRkey(uri: string): string | null;
5
+ /**
6
+ * Get document slug (uses path if available, otherwise rkey)
7
+ */
8
+ export declare function getDocumentSlug(document: {
9
+ uri: string;
10
+ value: {
11
+ path?: string;
12
+ };
13
+ }): string;
14
+ /**
15
+ * Get document canonical URL
16
+ */
17
+ export declare function getDocumentUrl(document: {
18
+ value: {
19
+ site: string;
20
+ path?: string;
21
+ };
22
+ uri: string;
23
+ }): string;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Extract rkey from AT URI
3
+ */
4
+ export function extractRkey(uri) {
5
+ const match = uri.match(/at:\/\/[^\/]+\/[^\/]+\/(.+)$/);
6
+ return match ? match[1] : null;
7
+ }
8
+ /**
9
+ * Get document slug (uses path if available, otherwise rkey)
10
+ */
11
+ export function getDocumentSlug(document) {
12
+ if (document.value.path) {
13
+ // Remove leading slash if present
14
+ return document.value.path.replace(/^\//, '');
15
+ }
16
+ return extractRkey(document.uri) || '';
17
+ }
18
+ /**
19
+ * Get document canonical URL
20
+ */
21
+ export function getDocumentUrl(document) {
22
+ const { site, path } = document.value;
23
+ // If site is an AT URI, we'll use our local route
24
+ if (site.startsWith('at://')) {
25
+ return `/documents/${getDocumentSlug(document)}`;
26
+ }
27
+ // If site is HTTPS and we have a path, combine them
28
+ if (path) {
29
+ return `${site.replace(/\/$/, '')}/${path.replace(/^\//, '')}`;
30
+ }
31
+ // Fallback to local route
32
+ return `/documents/${extractRkey(document.uri)}`;
33
+ }