155 lines
4.0 KiB
TypeScript
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')
|
||
|
})
|