rss-viewer-browser-extension/src/main.ts

155 lines
4.0 KiB
TypeScript

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 = () => {
const xmlDoc = document.documentElement
if (xmlDoc.tagName === '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 <pre> in a normal HTML DOM
const preElements = document.getElementsByTagName('pre')
if (preElements.length !== 1)
return
const text = preElements[0].innerText
const parser = new DOMParser()
let innerXmlDoc = null
try {
// @ts-ignore
innerXmlDoc = parser.parseFromString(text, 'text/xml')
} catch (e) { }
if (!innerXmlDoc)
return
// @ts-ignore
const root = innerXmlDoc.documentElement
if (root.tagName === 'rss')
return root
}
browser.runtime.onMessage.addListener(async (message: {type: Object}) => {
if (message.type !== 'renderFeed')
return
const xmlDoc = 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')
})