this repo has no description
1import type { Facet } from './types';
2
3export function escapeHtml(str: string): string {
4 return str
5 .replace(/&/g, '&')
6 .replace(/</g, '<')
7 .replace(/>/g, '>')
8 .replace(/"/g, '"')
9 .replace(/'/g, ''');
10}
11
12export function renderFacets(text: string, facets?: Facet[]): string {
13 if (!facets?.length) return escapeHtml(text);
14
15 const encoder = new TextEncoder();
16 const decoder = new TextDecoder();
17 const bytes = encoder.encode(text);
18 const sorted = [...facets].sort((a, b) => a.index.byteStart - b.index.byteStart);
19
20 let result = '';
21 let lastEnd = 0;
22
23 for (const facet of sorted) {
24 const { byteStart, byteEnd } = facet.index;
25 result += escapeHtml(decoder.decode(bytes.slice(lastEnd, byteStart)));
26 const facetText = escapeHtml(decoder.decode(bytes.slice(byteStart, byteEnd)));
27 const feature = facet.features?.[0];
28
29 if (feature?.$type === 'app.bsky.richtext.facet#link') {
30 result += `<a href="${escapeHtml(feature.uri!)}" target="_blank" rel="noopener">${facetText}</a>`;
31 } else if (feature?.$type === 'app.bsky.richtext.facet#mention') {
32 result += `<a href="https://bsky.app/profile/${feature.did}" target="_blank" rel="noopener">${facetText}</a>`;
33 } else if (feature?.$type === 'app.bsky.richtext.facet#tag') {
34 result += `<a href="https://bsky.app/hashtag/${feature.tag}" target="_blank" rel="noopener">${facetText}</a>`;
35 } else {
36 result += facetText;
37 }
38 lastEnd = byteEnd;
39 }
40
41 result += escapeHtml(decoder.decode(bytes.slice(lastEnd)));
42 return result;
43}
44
45export function timeAgo(dateStr: string): string {
46 const now = Date.now();
47 const then = new Date(dateStr).getTime();
48 const diff = now - then;
49 const mins = Math.floor(diff / 60000);
50 const hours = Math.floor(diff / 3600000);
51 const days = Math.floor(diff / 86400000);
52 if (mins < 1) return 'just now';
53 if (mins < 60) return `${mins}m`;
54 if (hours < 24) return `${hours}h`;
55 if (days < 7) return `${days}d`;
56 return new Date(dateStr).toLocaleDateString('en-US', {
57 month: 'short',
58 day: 'numeric',
59 ...(days > 365 ? { year: 'numeric' } : {}),
60 });
61}
62
63export function formatDate(dateStr: string): string {
64 return new Date(dateStr).toLocaleDateString('en-US', {
65 year: 'numeric',
66 month: 'long',
67 day: 'numeric',
68 });
69}
70
71export function formatDuration(seconds: number): string {
72 const m = Math.floor(seconds / 60);
73 const s = Math.floor(seconds % 60);
74 return `${m}:${s.toString().padStart(2, '0')}`;
75}
76
77export function formatCount(n: number): string {
78 if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
79 if (n >= 10000) return (n / 1000).toFixed(0) + 'K';
80 if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
81 return String(n || 0);
82}
83
84export function slugify(str: string): string {
85 return str
86 .toLowerCase()
87 .replace(/[^a-z0-9]+/g, '-')
88 .replace(/^-|-$/g, '');
89}