1
0
mirror of https://github.com/aureliendavid/rsspreview.git synced 2025-08-22 11:18:37 +00:00
rsspreview/rsspreview.js
2025-04-22 13:20:16 +02:00

450 lines
12 KiB
JavaScript

(function() {
/**
* Check and set a global guard variable.
* If this content script is injected into the same page again,
* it will do nothing next time.
*/
if (window.hasRun) {
console.log('already run');
return;
}
window.hasRun = true;
// defaults
var options = {
doThumb: false,
doMaxWidth: true,
valMaxWidth: "900px",
doDetect: true,
preventPreview: false,
fullPreview: false,
doAuthor: false,
enableCss: false,
bypassCSP: false,
customCss: null,
newTab: true
};
let xml_parser = new XMLSerializer();
let html_parser = new DOMParser();
function xhrdoc(url, type, cb) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'document';
xhr.overrideMimeType('text/' + type);
xhr.onload = () => {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
let resp = type == 'xml' ? xhr.responseXML : xhr.response;
cb(resp);
}
}
};
xhr.send(null);
}
function applyxsl(xmlin, xsl, node, doc = document) {
let xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(xsl);
xsltProcessor.setParameter(null, 'fullPreview', options.fullPreview);
xsltProcessor.setParameter(null, 'doAuthor', options.doAuthor);
let fragment = xsltProcessor.transformToFragment(xmlin, doc);
node.appendChild(fragment);
}
function getlang() {
return browser.i18n.getUILanguage();
}
function formatsubtitle() {
try {
let feed_desc = document.getElementById('feedSubtitleRaw');
let html_desc = html_parser.parseFromString(
'<h2 id="feedSubtitleText">' + feed_desc.innerText + '</h2>',
'text/html'
);
let xml_desc = xml_parser.serializeToString(html_desc.body.firstChild);
feed_desc.insertAdjacentHTML('afterend', xml_desc);
feed_desc.parentNode.removeChild(feed_desc);
} catch (e) {
console.error(e);
}
}
function formatdescriptions(el = document) {
// unescapes descriptions to html then to xml
let tohtml = el.getElementsByClassName('feedRawContent');
for (let i = 0; i < tohtml.length; i++) {
try {
let html_txt = '';
if (tohtml[i].getAttribute('desctype') == 'text/plain') {
html_txt = '<div class="feedEntryContent" style="white-space: pre-wrap;" >' + tohtml[i].innerHTML + '</div>';
}
else if (tohtml[i].getAttribute('desctype') == 'xhtml') {
html_txt = '<div class="feedEntryContent">' + tohtml[i].innerHTML + '</div>';
}
else {
html_txt = '<div class="feedEntryContent">' + tohtml[i].textContent + '</div>';
}
let html_desc = html_parser.parseFromString(html_txt, 'text/html');
let xml_desc = xml_parser.serializeToString(
html_desc.body.firstChild
);
tohtml[i].insertAdjacentHTML('afterend', xml_desc);
tohtml[i].setAttribute('todel', 1);
} catch (e) {
console.error(e);
console.log(tohtml[i]);
}
}
el.querySelectorAll('.feedRawContent').forEach(a => {
if (a.getAttribute('todel') == '1') {
a.remove();
}
});
}
function removeemptyenclosures(el = document) {
let encs = el.getElementsByClassName('enclosures');
for (let i = 0; i < encs.length; i++)
if (!encs[i].firstChild) encs[i].style.display = 'none';
}
function formatfilenames(el = document) {
let encfn = el.getElementsByClassName('enclosureFilename');
for (let i = 0; i < encfn.length; i++) {
let url = new URL(encfn[i].innerText);
if (url) {
let fn = url.pathname.split('/').pop();
if (fn != '') encfn[i].innerText = fn;
}
}
}
function formatfilesizes(el = document) {
function humanfilesize(size) {
let i = 0;
if (size && size != '' && size > 0)
i = Math.floor(Math.log(size) / Math.log(1024));
return (
(size / Math.pow(1024, i)).toFixed(2) * 1 +
' ' +
['B', 'kB', 'MB', 'GB', 'TB'][i]
);
}
let encsz = el.getElementsByClassName('enclosureSize');
for (let i = 0; i < encsz.length; i++) {
let hsize = humanfilesize(encsz[i].innerText);
if (hsize) encsz[i].innerText = hsize;
}
}
function formattitles(el = document) {
let et = el.getElementsByClassName('entrytitle');
for (let i = 0; i < et.length; i++) {
//basically removes html content if there is some
//only do it if there's a tag to avoid doing it when text titles cointain a '&'
//(which can be caught but still displays an error in console, which is annoying)
if (et[i].innerText.indexOf('<') >= 0 || et[i].innerText.indexOf('&amp;')) {
let tmp = document.createElement('span');
try {
tmp.innerHTML = et[i].innerText;
et[i].innerText = tmp.textContent;
} catch (e) {
// if not parsable, display as text
console.error(e);
console.log(et[i].innerText);
}
}
}
}
function formatdates(el = document) {
let lang = getlang();
if (!lang) return;
let opts = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
};
let ed = el.getElementsByClassName('lastUpdated');
for (let i = 0; i < ed.length; i++) {
let d = new Date(ed[i].innerText);
if (isNaN(d)) continue;
let dstr =
d.toLocaleDateString(lang, opts) + ' ' + d.toLocaleTimeString(lang);
ed[i].innerText = dstr;
}
let lu = el.getElementById('feedLastUpdate');
if (lu && lu.innerText.trim() != "") {
lu.innerText = "Last updated: " + lu.innerText;
}
}
function extensionimages(el = document) {
let extimgs = el.getElementsByClassName('extImg');
for (let i = 0; i < extimgs.length; i++)
extimgs[i].src = chrome.runtime.getURL(
extimgs[i].attributes['data-src'].nodeValue
);
}
function applysettings() {
document.querySelectorAll('.mediaThumb').forEach((elem) => {
elem.style.display = options.doThumb ? "block" : "none";
});
document.querySelectorAll('img').forEach((elem) => {
if (options.doMaxWidth)
elem.style["max-width"] = options.valMaxWidth;
});
}
function makepreviewhtml() {
let doc = document.implementation.createHTMLDocument('');
doc.body.id = "rsspreviewBody";
let feedBody = doc.createElement('div');
feedBody.id = 'feedBody';
doc.body.appendChild(feedBody);
let css = doc.createElement('link');
css.setAttribute('rel', 'stylesheet');
css.setAttribute('href', chrome.runtime.getURL('preview.css'));
doc.head.appendChild(css);
if (options.enableCss && options.customCss) {
let node = doc.createElement('style');
node.innerHTML = options.customCss;
doc.head.appendChild(node);
}
return doc;
}
function detect() {
let rootNode = document.getRootNode();
// for chrome
let d = document.getElementById('webkit-xml-viewer-source-xml');
if (d && d.firstChild) rootNode = d.firstChild;
const rootName = rootNode.documentElement.nodeName.toLowerCase();
let isRSS1 = false;
if (rootName == 'rdf' || rootName == 'rdf:rdf') {
if (rootNode.documentElement.attributes['xmlns']) {
isRSS1 = rootNode.documentElement.attributes['xmlns'].nodeValue.search('rss') > 0;
}
}
if (
rootName == 'rss' ||
rootName == 'channel' || // rss2
rootName == 'feed' || // atom
isRSS1
)
return rootNode;
return null;
}
function main(feedNode) {
let feed_url = window.location.href;
let preview = makepreviewhtml();
xhrdoc(chrome.runtime.getURL('rss.xsl'), 'xml', xsl_xml => {
applyxsl(feedNode, xsl_xml, preview.getElementById('feedBody'), preview);
// replace the content with the preview document
document.replaceChild(
document.importNode(preview.documentElement, true),
document.documentElement
);
let t0 = performance.now();
formatsubtitle();
formatdescriptions();
removeemptyenclosures();
formatfilenames();
formatfilesizes();
formattitles();
formatdates();
extensionimages();
applysettings();
let t1 = performance.now();
//console.log("exec in: " + (t1 - t0) + "ms");
document.title = document.getElementById('feedTitleText').innerText;
});
}
function onOptions(opts) {
options = opts;
let feedRoot = detect();
if (feedRoot && !options.preventPreview) {
main(feedRoot);
}
else if (options.doDetect) {
findFeeds();
}
}
function onError(error) {
console.log(`Error on get options: ${error}`);
}
let getting = browser.storage.sync.get(options);
getting.then(onOptions, onError);
function registerFeeds(feeds) {
if (Object.keys(feeds).length > 0) {
function handleResponse(message) {
}
function handleError(error) {
//console.log(error);
}
browser.runtime.sendMessage(feeds).then(handleResponse, handleError);
}
}
function findiTunesPodcastsFeeds() {
let match = document.URL.match(/id(\d+)/)
if (match) {
let feeds = {};
let itunesid = match[1];
var xhr = new XMLHttpRequest();
xhr.open('GET', "https://itunes.apple.com/lookup?id="+itunesid+"&entity=podcast");
xhr.onload = function () {
if (xhr.readyState === xhr.DONE) {
if (xhr.status === 200) {
let res = JSON.parse(xhr.responseText);
if ("results" in res) {
let pod = res["results"][0];
let title = pod["collectionName"] || document.title;
let url = pod["feedUrl"];
if (url) {
feeds[url] = title;
}
}
}
}
registerFeeds(feeds);
};
xhr.send();
}
}
function findYouTubeFeeds() {
// YouTube's canonical channel URLs look like /channel/AlphaNumericID
// It also supports named channels of the form /c/MyChannelName
// and handle links of the form /@MyChannelHandle.
// Match also on '%' to handle non-latin character codes
// Match on both of these to autodetect channel feeds on either URL
let idPattern = /channel\/([a-zA-Z0-9%_-]+)/;
let namePattern = /(?:c|user)\/[a-zA-Z0-9%_-]+/;
let handlePattern = /@[a-zA-Z0-9%_-]+/;
let urlPattern = new RegExp(`${idPattern.source}|${namePattern.source}|${handlePattern.source}`);
if (document.URL.match(urlPattern)) {
let feeds = {};
let canonicalUrl = document.querySelector("link[rel='canonical']").href;
let channelId = canonicalUrl.match(idPattern)[1];
let url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;
let title = document.title;
feeds[url] = title;
registerFeeds(feeds);
}
}
// The default function used to find feeds if a domain-specific function doesn't exist.
// Parse the document's HTML looking for link tags pointing to the feed URL.
function defaultFindFeeds() {
let feeds = {};
document.querySelectorAll("link[rel='alternate']").forEach( (elem) => {
let type_attr = elem.getAttribute('type');
if (!type_attr) {
return;
}
let type = type_attr.toLowerCase();
if (type.includes('rss') || type.includes('atom') || type.includes('feed')) {
let title = elem.getAttribute('title');
let url = elem.href;
if (url) {
feeds[url] = (title ? title : url);
}
}
});
registerFeeds(feeds);
}
const domainFeedFinders = new Map([
["itunes.apple.com", findiTunesPodcastsFeeds],
["podcasts.apple.com", findiTunesPodcastsFeeds],
["www.youtube.com", findYouTubeFeeds],
]);
function findFeeds() {
// Look up a feed detection function based on the domain.
// If a domain-specific function doesn't exist, fall back to a default.
let finder = domainFeedFinders.get(document.domain) || defaultFindFeeds;
finder();
}
})();