import browser from 'webextension-polyfill'; const parseItemImage = (item: Element) => { const images = Array.from(item.getElementsByTagName('media:content')) .filter((content) => (content.getAttribute('type') || '').startsWith('image/') || content.getAttribute('medium') === 'image' ) if (!images.length) return const { url } = images.reduce((maxImage, content) => { const width = parseFloat(content.getAttribute('width') || '0') if (width > maxImage.width) { maxImage.url = content.getAttribute('url') || '' maxImage.width = width } return maxImage }, { width: parseFloat(images[0].getAttribute('width') || '0'), url: images[0].getAttribute('url'), }) return { url: url } } const pubDateToInterval = (item: Element) => { const dateStr = getNodeContent(item, 'pubDate') if (!dateStr?.length) return // @ts-ignore let interval = ((new Date()) - (new Date(dateStr))) / 1000 let unit = 'seconds' if (interval >= 60) { interval /= 60 unit = 'minutes' } if (unit == 'minutes' && interval >= 60) { interval /= 60 unit = 'hours' } if (unit == 'hours' && interval >= 24) { interval /= 24 unit = 'days' } if (unit == 'days' && interval >= 30) { interval /= 30 unit = 'months' } return `${interval.toFixed(0)} ${unit}` } const getNodeContent = (parent: Element, tagName: string) => // @ts-ignore parent.getElementsByTagName(tagName)[0]?.firstChild?.wholeText const parseFeed = (channel: Element) => { const imageElement = channel.getElementsByTagName('image')[0] const itemTime = (item: {pubDate: string}) => { const dateStr = item.pubDate if (!dateStr?.length) return 0 return (new Date(dateStr)).getTime() } return { feedData: { title: getNodeContent(channel, 'title'), description: getNodeContent(channel, 'description'), feedUrl: window.location.href, homeUrl: getNodeContent(channel, 'link'), image: imageElement ? { title: getNodeContent(imageElement, 'title'), imageUrl: getNodeContent(imageElement, 'url'), targetUrl: getNodeContent(imageElement, 'link'), } : null, items: Array.from(channel.getElementsByTagName('item')).map((item) => { return { title: getNodeContent(item, 'title'), description: getNodeContent(item, 'description'), url: getNodeContent(item, 'link'), image: parseItemImage(item), pubDate: getNodeContent(item, 'pubDate'), age: pubDateToInterval(item), categories: Array.from(item.getElementsByTagName('category')).map((cat) => cat.firstChild?.textContent ), } }).sort((a, b) => itemTime(b) - itemTime(a)) } } } const getFeedRoot = (): HTMLElement | null => { const xmlDoc = document.documentElement if (xmlDoc.tagName.toLowerCase() === 'rss') return xmlDoc // Chrome-based browsers may wrap the XML into an HTML view const webkitSource = document.getElementById('webkit-xml-viewer-source-xml') if (webkitSource) return webkitSource // For some ugly reasons, some RSS feeds are rendered inside of a
in a normal HTML DOM const preElements = document.getElementsByTagName('pre') if (preElements.length !== 1) return null return preElements[0] } const textToDOM = (text: string) => { const parser = new DOMParser() let xmlDoc = null try { // @ts-ignore xmlDoc = parser.parseFromString(text, 'text/xml') } catch (e) { } if (!xmlDoc) return // @ts-ignore const root = xmlDoc.documentElement if (root.tagName.toLowerCase() === 'rss') return root } const renderFeed = (text: string) => { const xmlDoc = text?.length ? textToDOM(text) : getFeedRoot() if (!xmlDoc) // Not an RSS feed return const channel = xmlDoc.getElementsByTagName('channel')[0] if (!channel) return browser.storage.local.set(parseFeed(channel)) window.location.href = browser.runtime.getURL('viewer/index.html') } const extractFeedUrl = () => { const links = Array.from(document.getElementsByTagName('link')) .filter((link) => link.getAttribute('rel') === 'alternate' && link.getAttribute('type')?.startsWith('application/rss+xml') ) if (!links.length) return let link = links[0].getAttribute('href') || '' if (link.length && !link.match(/^https?:\/\//)) { let port = window.location.port if (port.length) port = `:${port}` link = `${window.location.protocol}//${window.location.hostname}${port}${link}` } return link.length ? link : null } browser.runtime.onMessage.addListener( async ( message: { type: string, url: string, document: string, } ) => { if (message.type === 'renderFeed') return renderFeed(message.document) if (message.type === 'extractFeedUrl') return extractFeedUrl() console.warn(`Received unknown message type: ${message.type}`) } )