@@ -9,6 +9,7 @@ export type ParsedSharedMapLink = {
99
1010type FetchResponseLike = {
1111 url ?: string ;
12+ text ?: ( ) => Promise < string > ;
1213} ;
1314
1415type FetchLike = ( input : string , init ?: Record < string , unknown > ) => Promise < FetchResponseLike > ;
@@ -32,6 +33,15 @@ const GOOGLE_PATH_COORD_RE = new RegExp(
3233 `@(${ NUMBER_PART } )\\s*,\\s*(${ NUMBER_PART } )(?:\\s*,|\\s*$)` ,
3334 "i" ,
3435) ;
36+ const GOOGLE_PB_COORD_RE = new RegExp (
37+ `(?:%21|!)2d(${ NUMBER_PART } )(?:%21|!)3d(${ NUMBER_PART } )` ,
38+ "i" ,
39+ ) ;
40+
41+ type ExpandedShortMapUrlResult = {
42+ expandedUrl : string ;
43+ responseText : string | null ;
44+ } ;
3545
3646function normalizeHost ( hostname : string ) : string {
3747 return hostname . trim ( ) . replace ( / \. + $ / , "" ) . toLowerCase ( ) ;
@@ -137,6 +147,77 @@ function parseSharedMapLinkFromUrl(url: URL, rawUrl: string): ParsedSharedMapLin
137147 } ;
138148}
139149
150+ function parseGooglePreviewCoordinatesFromText ( input : string ) : { lat : number ; lon : number } | null {
151+ if ( ! input ) return null ;
152+
153+ const match = input . match ( GOOGLE_PB_COORD_RE ) ;
154+ if ( ! match ) return null ;
155+
156+ const [ , lonRaw = "" , latRaw = "" ] = match ;
157+ const lon = Number ( lonRaw ) ;
158+ const lat = Number ( latRaw ) ;
159+ if ( ! Number . isFinite ( lat ) || ! Number . isFinite ( lon ) ) return null ;
160+
161+ return sanitizeCoordinates ( { lat, lon } ) ;
162+ }
163+
164+ async function expandShortMapUrlWithResponse (
165+ url : string ,
166+ options : ExpandShortMapUrlOptions = { } ,
167+ readResponseText = true ,
168+ ) : Promise < ExpandedShortMapUrlResult > {
169+ const parsed = parseUrl ( url ) ;
170+ if ( ! parsed || ! isGoogleShortHost ( parsed . hostname ) ) {
171+ return { expandedUrl : url , responseText : null } ;
172+ }
173+
174+ const fetchImpl =
175+ options . fetchImpl ??
176+ ( typeof fetch === "function" ? ( fetch as unknown as FetchLike ) : undefined ) ;
177+ if ( ! fetchImpl ) {
178+ return { expandedUrl : url , responseText : null } ;
179+ }
180+
181+ const timeoutMs =
182+ typeof options . timeoutMs === "number" && Number . isFinite ( options . timeoutMs )
183+ ? Math . max ( 1 , Math . floor ( options . timeoutMs ) )
184+ : DEFAULT_EXPAND_TIMEOUT_MS ;
185+
186+ try {
187+ const response = await withTimeout (
188+ fetchImpl ( parsed . toString ( ) , {
189+ method : "GET" ,
190+ redirect : "follow" ,
191+ } ) ,
192+ timeoutMs ,
193+ ) ;
194+
195+ let responseText : string | null = null ;
196+ if ( readResponseText && typeof response . text === "function" ) {
197+ try {
198+ responseText = await withTimeout ( Promise . resolve ( response . text ( ) ) , timeoutMs ) ;
199+ } catch {
200+ responseText = null ;
201+ }
202+ }
203+
204+ if ( typeof response . url === "string" && response . url . trim ( ) ) {
205+ return {
206+ expandedUrl : response . url ,
207+ responseText,
208+ } ;
209+ }
210+
211+ return {
212+ expandedUrl : url ,
213+ responseText,
214+ } ;
215+ } catch {
216+ // Fall back to the original short URL on timeout/fetch failure.
217+ return { expandedUrl : url , responseText : null } ;
218+ }
219+ }
220+
140221export function extractFirstUrl ( input : string ) : string | null {
141222 const text = input . trim ( ) ;
142223 if ( ! text ) return null ;
@@ -189,35 +270,8 @@ export async function expandShortMapUrl(
189270 url : string ,
190271 options : ExpandShortMapUrlOptions = { } ,
191272) : Promise < string > {
192- const parsed = parseUrl ( url ) ;
193- if ( ! parsed || ! isGoogleShortHost ( parsed . hostname ) ) return url ;
194-
195- const fetchImpl =
196- options . fetchImpl ??
197- ( typeof fetch === "function" ? ( fetch as unknown as FetchLike ) : undefined ) ;
198- if ( ! fetchImpl ) return url ;
199-
200- const timeoutMs =
201- typeof options . timeoutMs === "number" && Number . isFinite ( options . timeoutMs )
202- ? Math . max ( 1 , Math . floor ( options . timeoutMs ) )
203- : DEFAULT_EXPAND_TIMEOUT_MS ;
204-
205- try {
206- const response = await withTimeout (
207- fetchImpl ( parsed . toString ( ) , {
208- method : "GET" ,
209- redirect : "follow" ,
210- } ) ,
211- timeoutMs ,
212- ) ;
213- if ( typeof response . url === "string" && response . url . trim ( ) ) {
214- return response . url ;
215- }
216- } catch {
217- // Fall back to the original short URL on timeout/fetch failure.
218- }
219-
220- return url ;
273+ const result = await expandShortMapUrlWithResponse ( url , options , false ) ;
274+ return result . expandedUrl ;
221275}
222276
223277export function parseSharedMapLink ( input : string ) : ParsedSharedMapLink | null {
@@ -244,11 +298,22 @@ export async function parseSharedMapLinkAsync(
244298
245299 if ( ! isGoogleShortHost ( url . hostname ) ) return null ;
246300
247- const expanded = await expandShortMapUrl ( extracted , options ) ;
248- if ( expanded === extracted ) return null ;
301+ const { expandedUrl, responseText } = await expandShortMapUrlWithResponse ( extracted , options ) ;
302+ if ( expandedUrl !== extracted ) {
303+ const expandedParsedUrl = parseUrl ( expandedUrl ) ;
304+ if ( ! expandedParsedUrl ) return null ;
249305
250- const expandedUrl = parseUrl ( expanded ) ;
251- if ( ! expandedUrl ) return null ;
306+ const parsedExpandedLink = parseSharedMapLinkFromUrl ( expandedParsedUrl , extracted ) ;
307+ if ( parsedExpandedLink ) return parsedExpandedLink ;
308+ }
309+
310+ const parsedFromPreviewPayload = parseGooglePreviewCoordinatesFromText ( responseText ?? "" ) ;
311+ if ( ! parsedFromPreviewPayload ) return null ;
252312
253- return parseSharedMapLinkFromUrl ( expandedUrl , extracted ) ;
313+ return {
314+ provider : "google" ,
315+ lat : parsedFromPreviewPayload . lat ,
316+ lon : parsedFromPreviewPayload . lon ,
317+ rawUrl : extracted ,
318+ } ;
254319}
0 commit comments