import { formatDate, isDate } from './date.js' type DataSummary = { title?: string image?: string icon?: string url?: string date?: string description?: string status?: string category?: string number?: string extras: string[] unknown: string[] } const propertyMap: Partial> = { title: ['name', 'productName', 'title', 'heading', 'label', 'caption', 'headline', 'heading', 'header', 'subject'], image: [ 'image', 'picture', 'photo', 'thumbnail', 'illustration', 'visual', 'pic', 'graphic', 'artwork', 'promoicon', 'media' ], url: ['url', 'link', 'website', 'href', 'source', 'site', 'homepage', 'address'], date: ['createdat', 'createdon', 'lastmodified', '_at', 'issued_at', 'date'], description: [ 'description', 'summary', 'content', 'details', 'info', 'overview', 'abstract', 'context', 'synopsis', 'explanation' ], status: ['badge', 'status', 'state', 'condition', 'availability', 'tag'], category: ['category', 'classification', 'type', 'kind', 'group', 'classification', 'variant'], number: [ 'price', 'quantity', 'amount', 'number', 'count', 'value', 'numeric', 'figure', 'digit', 'total', 'measurement' ], icon: ['icon', 'logo', 'marker', 'avatar', 'emblem', 'figurine', 'pictogram', 'insignia'] } const extrasList: string[] = [ ...propertyMap.status, ...propertyMap.category, ...propertyMap.description, 'author', 'date', 'price', 'rating', 'location', 'ratingCount', 'reviews', 'testimonial', 'comment', 'opinion', 'features', 'featureList', 'specs', 'specifications', 'attributes', 'size', 'dimensions', 'color', 'shade', 'palette', 'brand', 'manufacturer', 'maker', 'origin', 'availability', 'stock', 'model', 'variant', 'style', 'design', 'weight', 'mass', 'load', 'capacity', 'duration', 'length', 'height', 'width', 'depth', 'diameter', 'genre', 'category', 'classification', 'series', 'theme', 'topic', 'subject', 'director', 'cast', 'actors', 'actresses', 'year', 'language', 'country', 'platform', 'publisher', 'producer', 'artist', 'author', 'composer', 'musician', 'singer', 'album', 'song', 'track', 'format', 'medium', 'resolution', 'quality', 'definition', 'bitrate', 'frameRate', 'duration', 'playtime', 'runtime', 'upvotes', 'likes', 'views', 'hits', 'comments', 'feedback', 'votes', 'reactions', 'tags', 'keywords', 'related', 'similar', 'score', 'rating', 'ranking', 'popularity', 'price', 'cost', 'value', 'discount', 'sale', 'promo', 'quantity', 'stock', 'inventory', 'availability', 'delivery', 'shipping', 'coupon', 'article', 'news', 'headline', 'featured', 'trending', 'latest', 'popular', 'hot', 'breaking', 'announcement', 'update', 'alert' ] const urlAliases = ['src', 'href', 'url', 'link'] export function findObjectValue(obj: any, key: any, property: keyof DataSummary) { var value = obj[key] if ( property == 'status' || property == 'category' || property == 'date' || property == 'number' || property == 'title' ) { if (String(value).startsWith('http')) return null } if (property == 'image' || property == 'url' || property == 'icon') { if (typeof value == 'object' && value) { // handle contenthub1 collections if (Object.keys(value).length == 1 && value.results) { value = findContentfulObject(value) } // dive deeper one level, as image sometimes are objects with extra props for (var k in value) { if (matchAliases(k, propertyMap[property]) || matchAliases(k, urlAliases)) { if (String(value[k]).startsWith('http')) { value = value[k] break } } } } if (typeof value == 'string') { if (String(value).startsWith('http')) return value } return null } if (matchAliases(key, propertyMap.date)) { if (isDate(value)) return formatDate(new Date(value)) return false } if (typeof value == 'object' || typeof value == 'boolean' || (typeof value == 'string' && !String(value).trim())) return false return value } function matchAliases(candidate: string, aliases: string[]) { return aliases.filter((k) => candidate.toLocaleLowerCase().includes(k)).sort((a, b) => a.length - b.length)[0] } export function createObjectSummary(obj: Record): { summary: DataSummary; keyMap: DataSummary } { const summary: DataSummary = { extras: [], unknown: [] } const keyMap: DataSummary = { extras: [], unknown: [] } const consumedProperties: Set = new Set() // Part 1: Consume regular properties for (const property in propertyMap) { const aliases = propertyMap[property as keyof DataSummary] for (const candidate in obj) { const value = findObjectValue(obj, candidate, property as keyof DataSummary) if (value == null) continue const key = matchAliases(candidate, aliases) if (key) { summary[property as keyof DataSummary] = value as any keyMap[property as keyof DataSummary] = candidate as any consumedProperties.add(candidate) break } } } // Part 2: Consume extras for (const candidate in obj) { const value = findObjectValue(obj, candidate, null) if (value == null) continue if (!consumedProperties.has(candidate) && consumedProperties.size < 7) { const key = matchAliases(candidate, extrasList) if (key) { summary.extras.push(value as string) keyMap.extras.push(candidate) consumedProperties.add(candidate) } } } for (const candidate in obj) { const value = findObjectValue(obj, candidate, null) if (value == null) continue if (!consumedProperties.has(candidate) && consumedProperties.size < 7) { summary.unknown.push(value as string) keyMap.unknown.push(candidate) consumedProperties.add(candidate) } } return { summary, keyMap } } // Find object that matches the most content properties export function findContentfulObject(obj: any): any { let maxMatchedProperties = 0 let contentfulObject = null function countMatchedProperties(obj: any) { if (Array.isArray(obj)) { obj.map(countMatchedProperties) } else if (typeof obj === 'object') { const { summary } = createObjectSummary(obj) const numMatchedProperties = Object.keys(summary).length + (summary.extras?.length || 0) if (numMatchedProperties > maxMatchedProperties) { maxMatchedProperties = numMatchedProperties contentfulObject = obj } for (const key in obj) { countMatchedProperties(obj[key]) } } } countMatchedProperties(obj) return contentfulObject } export const colorAliases: Record = { green: [ 'success', 'published', 'active', 'enabled', 'available', 'valid', 'approved', 'completed', 'finished', 'succeeded', 'okay', 'good', 'positive', 'confirmed', 'accepted', 'available', 'in stock', 'production' ], red: [ 'failure', 'error', 'invalid', 'rejected', 'unsuccessful', 'forbidden', 'denied', 'failed', 'fault', 'wrong', 'negative', 'disapproved', 'incomplete', 'error', 'unavailable', 'out of stock', 'sold out', 'unauthorized', 'deleted' ], yellow: [ 'test', 'draft', 'upcoming', 'pending', 'staged', 'in progress', 'unfinished', 'incomplete', 'warning', 'caution', 'attention', 'provisional', 'tentative', 'undecided', 'uncertain', 'unsettled' ] } export function getColorForValue(value: string) { for (const color in colorAliases) { if (colorAliases[color].find((alias) => String(value).toLowerCase().includes(alias))) { return color } } return 'blackAlpha' } export function formatSideDimensions(top: string, bottom: string, left: string, right: string) { if (top === bottom && bottom === left && left === right) { return `${top}` } else if (top === bottom && left === right) { return `${top} × ${left}` } else { return `Custom` } }