Leaflet Blog in Deno Fresh
1import { PubLeafletBlocksText } from "npm:@atcute/leaflet"; 2 3interface TextBlockProps { 4 plaintext: string; 5 facets?: PubLeafletBlocksText.Main["facets"]; 6} 7 8interface LinkFeature { 9 $type: "pub.leaflet.richtext.facet#link"; 10 uri: string; 11} 12 13function byteToCharIndex(text: string, byteIndex: number): number { 14 const textEncoder = new TextEncoder(); 15 const textDecoder = new TextDecoder(); 16 const fullBytes = textEncoder.encode(text); 17 const bytes = fullBytes.slice(0, byteIndex); 18 return textDecoder.decode(bytes).length; 19} 20 21export function TextBlock({ plaintext, facets }: TextBlockProps) { 22 // Only process facets if at least one facet has features 23 if (!facets || !facets.some((f) => f.features && f.features.length > 0)) { 24 return <>{plaintext}</>; 25 } 26 27 const parts: (string | { text: string; type: string; uri?: string })[] = []; 28 let lastIndex = 0; 29 30 facets.forEach((facet) => { 31 // Convert byte positions to character positions 32 const charStart = byteToCharIndex(plaintext, facet.index.byteStart); 33 const charEnd = byteToCharIndex(plaintext, facet.index.byteEnd); 34 const charLastIndex = byteToCharIndex(plaintext, lastIndex); 35 36 if (charStart > charLastIndex) { 37 parts.push(plaintext.slice(charLastIndex, charStart)); 38 } 39 40 const text = plaintext.slice(charStart, charEnd); 41 const feature = facet.features?.[0]; 42 43 if (!feature) { 44 parts.push(text); 45 return; 46 } 47 48 if (feature.$type === "pub.leaflet.richtext.facet#bold") { 49 parts.push({ text, type: feature.$type }); 50 } else if (feature.$type === "pub.leaflet.richtext.facet#highlight") { 51 parts.push({ text, type: feature.$type }); 52 } else if (feature.$type === "pub.leaflet.richtext.facet#italic") { 53 parts.push({ text, type: feature.$type }); 54 } else if (feature.$type === "pub.leaflet.richtext.facet#strikethrough") { 55 parts.push({ text, type: feature.$type }); 56 } else if (feature.$type === "pub.leaflet.richtext.facet#underline") { 57 parts.push({ text, type: feature.$type }); 58 } else if (feature.$type === "pub.leaflet.richtext.facet#link") { 59 const linkFeature = feature as LinkFeature; 60 parts.push({ text, type: feature.$type, uri: linkFeature.uri }); 61 } else { 62 parts.push(text); 63 } 64 65 lastIndex = facet.index.byteEnd; 66 }); 67 68 // Convert final lastIndex from bytes to characters 69 const charLastIndex = byteToCharIndex(plaintext, lastIndex); 70 71 if (charLastIndex < plaintext.length) { 72 parts.push(plaintext.slice(charLastIndex)); 73 } 74 75 return ( 76 <> 77 {parts.map((part, i) => { 78 if (typeof part === "string") { 79 return part; 80 } 81 82 switch (part.type) { 83 case "pub.leaflet.richtext.facet#bold": 84 return <strong key={i}>{part.text}</strong>; 85 case "pub.leaflet.richtext.facet#highlight": 86 return ( 87 <mark 88 key={i} 89 className="bg-blue-100 dark:bg-blue-900 text-inherit rounded px-1" 90 style={{ borderRadius: "0.375rem" }} 91 > 92 {part.text} 93 </mark> 94 ); 95 case "pub.leaflet.richtext.facet#italic": 96 return <em key={i}>{part.text}</em>; 97 case "pub.leaflet.richtext.facet#strikethrough": 98 return <s key={i}>{part.text}</s>; 99 case "pub.leaflet.richtext.facet#underline": 100 return <u key={i}>{part.text}</u>; 101 case "pub.leaflet.richtext.facet#link": 102 return ( 103 <a 104 key={i} 105 href={part.uri} 106 target="_blank" 107 rel="noopener noreferrer" 108 className="text-blue-600 dark:text-blue-400 hover:underline" 109 > 110 {part.text} 111 </a> 112 ); 113 default: 114 return part.text; 115 } 116 })} 117 </> 118 ); 119}