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}