appview-less bluesky client
1import RichtextBuilder, { type BakedRichtext } from '@atcute/bluesky-richtext-builder';
2import { tokenize, type Token } from '$lib/richtext/parser';
3import type { Did, GenericUri, Handle } from '@atcute/lexicons';
4import { resolveHandle } from '$lib/at/client.svelte';
5
6export const parseToRichText = (text: string): ReturnType<typeof processTokens> =>
7 processTokens(tokenize(text));
8
9const processTokens = async (tokens: Token[]): Promise<BakedRichtext> => {
10 const rt = new RichtextBuilder();
11
12 for (const token of tokens) {
13 switch (token.type) {
14 case 'text':
15 rt.addText(token.content);
16 break;
17 case 'mention': {
18 let did: Did | undefined = token.did as Did | undefined;
19 if (!did) {
20 const handle = token.handle as Handle;
21 const result = await resolveHandle(handle);
22 if (result.ok) did = result.value;
23 }
24 if (did) rt.addMention(token.raw, did);
25 else rt.addText(token.raw);
26 break;
27 }
28 case 'topic':
29 rt.addTag(token.name);
30 break;
31 case 'autolink':
32 rt.addLink(token.url, token.url as GenericUri);
33 break;
34 case 'link': {
35 // flatten children to text
36 const text = flattenToText(token.children);
37 rt.addLink(text, token.url as GenericUri);
38 break;
39 }
40 case 'escape':
41 rt.addText(token.escaped);
42 break;
43 // formatting tokens (strong, emphasis, etc.) don't map to facets
44 // so just extract their text content
45 case 'strong':
46 case 'emphasis':
47 case 'underline':
48 case 'delete':
49 rt.addText(flattenToText(token.children));
50 break;
51 case 'code':
52 rt.addText(token.content);
53 break;
54 case 'emote':
55 // handle emotes as needed
56 rt.addText(token.raw);
57 break;
58 }
59 }
60
61 return rt.build();
62};
63
64const flattenToText = (tokens: Token[]): string => {
65 return tokens
66 .map((t) => {
67 if ('content' in t) return t.content;
68 if ('children' in t) return flattenToText(t.children);
69 return t.raw;
70 })
71 .join('');
72};