your personal website on atproto - mirror blento.app
at next 167 lines 4.7 kB view raw
1import type { PostData, PostEmbed } from '../post'; 2import type { PostView } from '@atcute/bluesky/types/app/feed/defs'; 3import { segmentize, type Facet, type RichtextSegment } from '@atcute/bluesky-richtext-segmenter'; 4 5function escapeHtml(str: string): string { 6 return str 7 .replace(/&/g, '&amp;') 8 .replace(/</g, '&lt;') 9 .replace(/>/g, '&gt;') 10 .replace(/"/g, '&quot;') 11 .replace(/'/g, '&#39;'); 12} 13 14function blueskyEmbedTypeToEmbedType(type: string) { 15 switch (type) { 16 case 'app.bsky.embed.external#view': 17 case 'app.bsky.embed.external': 18 return 'external'; 19 case 'app.bsky.embed.images#view': 20 case 'app.bsky.embed.images': 21 return 'images'; 22 case 'app.bsky.embed.video#view': 23 case 'app.bsky.embed.video': 24 return 'video'; 25 default: 26 return 'unknown'; 27 } 28} 29 30export function blueskyPostToPostData( 31 data: PostView, 32 baseUrl: string = 'https://bsky.app' 33): PostData { 34 const post = data; 35 // const reason = data.reason; 36 // const reply = data.reply?.parent; 37 // const replyId = reply?.uri?.split('/').pop(); 38 console.log(JSON.parse(JSON.stringify(data))); 39 40 const id = post.uri.split('/').pop(); 41 42 return { 43 id, 44 href: `${baseUrl}/profile/${post.author.handle}/post/${id}`, 45 // reposted: 46 // reason && reason.$type === 'app.bsky.feed.defs#reasonRepost' 47 // ? { 48 // handle: reason.by.handle, 49 // href: `${baseUrl}/profile/${reason.by.handle}` 50 // } 51 // : undefined, 52 53 // replyTo: 54 // reply && replyId 55 // ? { 56 // handle: reply.author.handle, 57 // href: `${baseUrl}/profile/${reply.author.handle}/post/${replyId}` 58 // } 59 // : undefined, 60 author: { 61 displayName: post.author.displayName || '', 62 handle: post.author.handle, 63 avatar: post.author.avatar, 64 href: `${baseUrl}/profile/${post.author.did}` 65 }, 66 replyCount: post.replyCount ?? 0, 67 repostCount: post.repostCount ?? 0, 68 likeCount: post.likeCount ?? 0, 69 createdAt: post.record.createdAt as string, 70 71 embed: post.embed 72 ? ({ 73 type: blueskyEmbedTypeToEmbedType(post.embed?.$type), 74 // Cast to any to handle union type - properties are conditionally accessed 75 images: (post.embed as any)?.images?.map((image: any) => ({ 76 alt: image.alt, 77 thumb: image.thumb, 78 aspectRatio: image.aspectRatio, 79 fullsize: image.fullsize 80 })), 81 external: (post.embed as any)?.external 82 ? { 83 href: (post.embed as any).external.uri, 84 title: (post.embed as any).external.title, 85 description: (post.embed as any).external.description, 86 thumb: (post.embed as any).external.thumb 87 } 88 : undefined, 89 video: (post.embed as any)?.playlist 90 ? { 91 playlist: (post.embed as any).playlist, 92 thumb: (post.embed as any).thumbnail, 93 alt: (post.embed as any).alt, 94 aspectRatio: (post.embed as any).aspectRatio 95 } 96 : undefined 97 } as PostEmbed) 98 : undefined, 99 100 htmlContent: blueskyPostToHTML(post, baseUrl), 101 labels: post.labels ? post.labels.map((label) => label.val) : undefined 102 }; 103} 104 105interface MentionFeature { 106 $type: 'app.bsky.richtext.facet#mention'; 107 did: string; 108} 109 110interface LinkFeature { 111 $type: 'app.bsky.richtext.facet#link'; 112 uri: string; 113} 114 115interface TagFeature { 116 $type: 'app.bsky.richtext.facet#tag'; 117 tag: string; 118} 119 120type Feature = MentionFeature | LinkFeature | TagFeature; 121 122const renderSegment = (segment: RichtextSegment, baseUrl: string) => { 123 const { text, features } = segment; 124 const escaped = escapeHtml(text); 125 126 if (!features) { 127 return `<span>${escaped}</span>`; 128 } 129 130 // segments can have multiple features, use the first one 131 const feature = features[0] as Feature; 132 133 const createLink = (href: string, text: string) => { 134 return `<a target="_blank" rel="noopener noreferrer nofollow" href="${encodeURI(href)}">${text}</a>`; 135 }; 136 137 switch (feature.$type) { 138 case 'app.bsky.richtext.facet#mention': 139 return createLink(`${baseUrl}/profile/${feature.did}`, escaped); 140 case 'app.bsky.richtext.facet#link': 141 return createLink(feature.uri, escaped); 142 case 'app.bsky.richtext.facet#tag': 143 return createLink(`${baseUrl}/hashtag/${feature.tag}`, escaped); 144 default: 145 return `<span>${escaped}</span>`; 146 } 147}; 148 149const RichText = ({ text, facets }: { text: string; facets?: Facet[] }, baseUrl: string) => { 150 const segments = segmentize(text, facets); 151 return segments.map((v) => renderSegment(v, baseUrl)).join(''); 152}; 153 154export function blueskyPostToHTML(post: PostView, baseUrl: string = 'https://bsky.app') { 155 if (!post?.record) { 156 return ''; 157 } 158 159 const html = RichText( 160 { text: post.record.text as string, facets: post.record.facets as Facet[] }, 161 baseUrl 162 ); 163 164 return html.replace(/\n/g, '<br>'); 165} 166 167export { default as BlueskyPost } from './BlueskyPost.svelte';