A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

Select the types of activity you want to include in your feed.

at trunk 2.8 kB View raw
1import type { Mention } from './mentions'; 2 3/** A richtext facet over a UTF-8 byte range of the post text. */ 4export interface PostFacet { 5 $type: 'app.bsky.richtext.facet'; 6 index: { byteStart: number; byteEnd: number }; 7 features: Array< 8 | { $type: 'app.bsky.richtext.facet#link'; uri: string } 9 | { $type: 'app.bsky.richtext.facet#mention'; did: string } 10 >; 11} 12 13const SEP = '\n\n'; 14 15function utf8ByteLength( value: string ): number { 16 return new TextEncoder().encode( value ).length; 17} 18 19/** 20 * Build the companion Bluesky post's `text` and `facets`. The body is 21 * `title \n\n {lede?} \n\n cc @a @b \n\n {url}`, with the lede omitted when blank and the 22 * cc line omitted when there are no mentions. Byte offsets are computed during assembly so 23 * the link and mention facets always line up with the final UTF-8 string. 24 */ 25export function assemblePostText( input: { 26 title: string; 27 articleUrl: string; 28 bodyLede?: string; 29 mentions?: Mention[]; 30} ): { text: string; facets: PostFacet[] } { 31 const mentions = input.mentions ?? []; 32 const lede = input.bodyLede?.trim() ?? ''; 33 const ccLine = mentions.length 34 ? `cc ${ mentions.map( ( m ) => `@${ m.handle }` ).join( ' ' ) }` 35 : ''; 36 37 const segments: string[] = [ input.title ]; 38 if ( lede ) { 39 segments.push( lede ); 40 } 41 if ( ccLine ) { 42 segments.push( ccLine ); 43 } 44 segments.push( input.articleUrl ); 45 46 const text = segments.join( SEP ); 47 48 // Byte start of each segment in the joined string. 49 const segByteStart: number[] = []; 50 let cursor = 0; 51 segments.forEach( ( segment, i ) => { 52 if ( i > 0 ) { 53 cursor += utf8ByteLength( SEP ); 54 } 55 segByteStart[ i ] = cursor; 56 cursor += utf8ByteLength( segment ); 57 } ); 58 59 const facets: PostFacet[] = []; 60 61 // Mention facets (each @handle inside the cc line). 62 if ( ccLine ) { 63 // The cc line, when present, is always the second-to-last segment (before the URL). 64 const ccIndex = segments.length - 2; 65 let local = utf8ByteLength( 'cc ' ); 66 mentions.forEach( ( mention, i ) => { 67 if ( i > 0 ) { 68 local += utf8ByteLength( ' ' ); 69 } 70 const token = `@${ mention.handle }`; 71 const byteStart = segByteStart[ ccIndex ] + local; 72 facets.push( { 73 $type: 'app.bsky.richtext.facet', 74 index: { byteStart, byteEnd: byteStart + utf8ByteLength( token ) }, 75 features: [ { $type: 'app.bsky.richtext.facet#mention', did: mention.did } ], 76 } ); 77 local += utf8ByteLength( token ); 78 } ); 79 } 80 81 // Link facet (the article URL, always the last segment). 82 const urlIndex = segments.length - 1; 83 const urlStart = segByteStart[ urlIndex ]; 84 facets.push( { 85 $type: 'app.bsky.richtext.facet', 86 index: { byteStart: urlStart, byteEnd: urlStart + utf8ByteLength( input.articleUrl ) }, 87 features: [ { $type: 'app.bsky.richtext.facet#link', uri: input.articleUrl } ], 88 } ); 89 90 return { text, facets }; 91}