1
0
mirror of https://github.com/aureliendavid/rsspreview.git synced 2025-08-22 11:18:37 +00:00
rsspreview/rsspreview.js
Jon Banafato 08435a3921 Support handle URLs for YouTube detection
YouTube recently added support for handles and has updated some links in
the UI to default to these formats. The existing logic works without
modification once a new regex for matching against handle URLs is added.
2022-11-18 14:56:09 -05:00

446 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);
console.log(feed_desc.innerText);
}
}
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) {
let tmp = document.createElement('span');
try {
tmp.innerHTML = et[i].innerText;
et[i].innerText = tmp.textContent;
} catch (e) {
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().documentElement;
// for chrome
let d = document.getElementById('webkit-xml-viewer-source-xml');
if (d && d.firstChild) rootNode = d.firstChild;
const rootName = rootNode.nodeName.toLowerCase();
let isRSS1 = false;
if (rootName == 'rdf' || rootName == 'rdf:rdf')
if (rootNode.attributes['xmlns'])
isRSS1 = rootNode.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();
}
})();