this repo has no description
at main 89 lines 2.9 kB view raw
1import type { Facet } from './types'; 2 3export function escapeHtml(str: string): string { 4 return str 5 .replace(/&/g, '&amp;') 6 .replace(/</g, '&lt;') 7 .replace(/>/g, '&gt;') 8 .replace(/"/g, '&quot;') 9 .replace(/'/g, '&#39;'); 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}