2022-12-04 03:09:40 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-12-04 15:29:48 +01:00
|
|
|
const renderFeed = () => {
|
2022-12-04 03:09:40 +01:00
|
|
|
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')
|
2022-12-04 15:29:48 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const extractFeedUrl = () => {
|
|
|
|
const links = Array.from(document.getElementsByTagName('link'))
|
|
|
|
.filter((link) =>
|
|
|
|
link.getAttribute('rel') === 'alternate' &&
|
|
|
|
link.getAttribute('type') === '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: Object}) => {
|
|
|
|
if (message.type === 'renderFeed')
|
|
|
|
return renderFeed()
|
|
|
|
if (message.type === 'extractFeedUrl')
|
|
|
|
return extractFeedUrl()
|
2022-12-04 03:09:40 +01:00
|
|
|
})
|
2022-12-04 15:29:48 +01:00
|
|
|
|