Laravel AT Protocol Client (alpha & unstable)
at dev 4.8 kB view raw
1<?php 2 3namespace SocialDept\AtpClient\RichText; 4 5use SocialDept\AtpResolver\Facades\Resolver; 6 7class FacetDetector 8{ 9 /** 10 * Auto-detect all facets (mentions, links, tags) from text 11 */ 12 public static function detect(string $text): array 13 { 14 $facets = []; 15 16 // Detect mentions 17 $facets = array_merge($facets, static::detectMentions($text)); 18 19 // Detect URLs 20 $facets = array_merge($facets, static::detectLinks($text)); 21 22 // Detect tags 23 $facets = array_merge($facets, static::detectTags($text)); 24 25 // Sort facets by byte position 26 usort($facets, fn ($a, $b) => $a['index']['byteStart'] <=> $b['index']['byteStart']); 27 28 return $facets; 29 } 30 31 /** 32 * Detect mentions (@handle.bsky.social) 33 */ 34 public static function detectMentions(string $text): array 35 { 36 $facets = []; 37 $pattern = '/@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?/'; 38 39 preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE); 40 41 foreach ($matches[0] as $match) { 42 $handle = ltrim($match[0], '@'); 43 $byteStart = $match[1]; 44 $byteEnd = $byteStart + strlen($match[0]); 45 46 try { 47 $did = Resolver::handleToDid($handle); 48 49 $facets[] = [ 50 'index' => [ 51 'byteStart' => $byteStart, 52 'byteEnd' => $byteEnd, 53 ], 54 'features' => [ 55 [ 56 '$type' => 'app.bsky.richtext.facet#mention', 57 'did' => $did, 58 ], 59 ], 60 ]; 61 } catch (\Exception $e) { 62 // Skip if handle cannot be resolved 63 continue; 64 } 65 } 66 67 return $facets; 68 } 69 70 /** 71 * Detect URLs (http:// and https://) 72 */ 73 public static function detectLinks(string $text): array 74 { 75 $facets = []; 76 $pattern = '/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/'; 77 78 preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE); 79 80 foreach ($matches[0] as $match) { 81 $url = $match[0]; 82 $byteStart = $match[1]; 83 $byteEnd = $byteStart + strlen($url); 84 85 $facets[] = [ 86 'index' => [ 87 'byteStart' => $byteStart, 88 'byteEnd' => $byteEnd, 89 ], 90 'features' => [ 91 [ 92 '$type' => 'app.bsky.richtext.facet#link', 93 'uri' => $url, 94 ], 95 ], 96 ]; 97 } 98 99 return $facets; 100 } 101 102 /** 103 * Detect hashtags (#tag) 104 */ 105 public static function detectTags(string $text): array 106 { 107 $facets = []; 108 $pattern = '/#([a-zA-Z0-9_]+)/u'; 109 110 preg_match_all($pattern, $text, $matches, PREG_OFFSET_CAPTURE); 111 112 foreach ($matches[0] as $i => $match) { 113 $fullTag = $match[0]; // includes # 114 $tag = $matches[1][$i][0]; // without # 115 $byteStart = $match[1]; 116 $byteEnd = $byteStart + strlen($fullTag); 117 118 $facets[] = [ 119 'index' => [ 120 'byteStart' => $byteStart, 121 'byteEnd' => $byteEnd, 122 ], 123 'features' => [ 124 [ 125 '$type' => 'app.bsky.richtext.facet#tag', 126 'tag' => $tag, 127 ], 128 ], 129 ]; 130 } 131 132 return $facets; 133 } 134 135 /** 136 * Merge overlapping or adjacent facets 137 */ 138 public static function mergeFacets(array $facets): array 139 { 140 if (empty($facets)) { 141 return []; 142 } 143 144 // Sort by byte position 145 usort($facets, fn ($a, $b) => $a['index']['byteStart'] <=> $b['index']['byteStart']); 146 147 $merged = []; 148 $current = $facets[0]; 149 150 for ($i = 1; $i < count($facets); $i++) { 151 $next = $facets[$i]; 152 153 // Check for overlap 154 if ($current['index']['byteEnd'] >= $next['index']['byteStart']) { 155 // Merge features 156 $current['features'] = array_merge($current['features'], $next['features']); 157 $current['index']['byteEnd'] = max($current['index']['byteEnd'], $next['index']['byteEnd']); 158 } else { 159 $merged[] = $current; 160 $current = $next; 161 } 162 } 163 164 $merged[] = $current; 165 166 return $merged; 167 } 168}