import { stringify } from "codec/query_string" ; import { oembed } from "./oembed" ; import { parseHtml , getTag , getText , attributes , decodeHtml , getTags , } from "./parser" ; import { Tag } from "htmlparser" ; export interface Preview { url : string ; site : string ; domain : string ; title ? : string ; image ? : string ; video ? : string ; description ? : string ; author ? : string ; authorUrl ? : string ; icon ? : string ; } const priority = [ { result : "title" , fields : [ "oembed:title" , "og:title" , "twitter:title" , "title" ] , } , { result : "url" , fields : [ "oembed:url" , "og:url" , "canonical" , "url" ] } , { result : "image" , fields : [ "oembed:image" , "og:image:secure_url" , "og:image" , "twitter:image:src" , "twitter:image" , ] , } , { result : "video" , fields : [ "og:video:secure_url" , "og:video" , "twitter:player" ] , } , { result : "description" , fields : [ "oembed:description" , "og:description" , "twitter:description" , "description" , ] , } , { result : "site" , fields : [ "oembed:site" , "og:site_name" , "site" ] } , { result : "domain" , fields : [ "oembed:domain" , "domain" ] } , { result : "author" , fields : [ "oembed:author" , "author" ] } , { result : "authorUrl" , fields : [ "oembed:authorUrl" , "article:author" ] } , { result : "icon" , fields : [ "icon" , "itemprop:image" ] } , ] ; const oembedProviders = [ { match : /https:\/\/(?:.+\.)?twitter.com\/.*\/status\/.*/ , url : ( url ) : string => `https://publish.twitter.com/oembed?${ stringify ( { url , omit_script : 1 , } ) }` , } , ] ; const getProvider = ( url ) : string => { return oembedProviders . filter ( ( provider ) => url . match ( provider . match ) ) [ 0 ] ?. url ( url ) ; } ; const resolveFields = ( meta ) : Preview => { return priority . reduce ( ( final , { fields , result } ) => { let value = fields . reduce ( ( value , field ) => value || ( meta [ field ] && decodeHtml ( meta [ field ] ) ) , undefined ) ; if ( value && ( result === "image" || result === "icon" ) ) { if ( value . startsWith ( "//" ) ) { value = "https:" + value ; } else if ( value . startsWith ( "/" ) ) { value = meta . domain + value ; } else if ( ! ( value . startsWith ( "http://" ) || value . startsWith ( "https://" ) ) ) { value = meta . url + value ; } } final [ result ] = value ; return final ; } , { } as Preview ) ; } ; const getMeta = ( root ) : { [ field ] : string ; } => { const meta = getTags ( root , "meta" ) . map ( attributes ) . reduce ( ( complete , tag ) => { complete [ tag . name || tag . property || `itemprop:${ tag . itemprop }` ] = tag . content ; return complete ; } , { } ) ; meta . title = getText ( getTag ( root , "title" ) ) ; const links = getTags ( root , "link" ) . map ( attributes ) ; meta . icon = links . filter ( ( attributes ) => attributes . rel === "shortcut icon" || attributes . rel === "icon" ) [ 0 ] ?. href ; meta . canonical = links . filter ( ( attributes ) => attributes . rel === "canonical" ) [ 0 ] ?. href ; meta . oembedUrl = links . filter ( ( attributes ) => attributes . rel === "alternate" && attributes . type === "application/json+oembed" ) [ 0 ] ?. href ; return meta ; } ; export const unfurl = async ( document , url ) : Promise < Preview | null > => { try { const [ domain , site ] = url . match ( /https?:\/\/(?:www\.)?([-a-zA-Z0-9.]{3,253})/ ) ; const urlData = { domain , site , url , } ; const ast = await parseHtml ( document ) ; const html = getTag ( ast , "html" ) ; const head = getTag ( html , "head" ) || html ; const meta = getMeta ( head ) ; const oembedUrl = meta . oembedUrl || getProvider ( url ) ; const oembedData = oembedUrl ? oembed ( oembedUrl ) : { } ; const unfurled = resolveFields ( { ... urlData , ... meta , ... oembedData } ) ; return unfurled ; } catch ( e ) { console . log ( e ) ; return null ; } } ;