├── .gitignore ├── LICENSE ├── README.md ├── background.js ├── icons ├── file.png ├── home.png ├── rss-128.png ├── rss-19.png ├── rss-256.png ├── rss-32.png ├── rss-38.png ├── rss-48.png ├── rss-64.png ├── rss-gray-19.png └── rss-gray-38.png ├── manifest.json ├── popup ├── popup.html └── popup.js ├── preview.css ├── rss.xsl ├── rsspreview.js └── settings ├── options.html └── options.js /.gitignore: -------------------------------------------------------------------------------- 1 | web-ext-artifacts 2 | todo.txt 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aurélien David 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rsspreview 2 | 3 | Firefox has removed support for RSS in versions 64+, including the useful preview feature. 4 | 5 | This extension attempts to recreate it. 6 | 7 | Download at: https://addons.mozilla.org/en-US/firefox/addon/rsspreview/ 8 | 9 | Additional features: 10 | * feed detection and address bar button 11 | * detect feeds from itunes podcast pages 12 | * custom css support 13 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | function detectFeed(event) { 2 | 3 | if (event.statusCode == 301 || event.statusCode == 302) 4 | return { responseHeaders: event.responseHeaders }; 5 | 6 | 7 | // force application/rss+xml to text/xml so the browser displays it instead of downloading 8 | let isfeed = false; 9 | 10 | for (let header of event.responseHeaders) { 11 | if (header.name.toLowerCase() == 'content-type') { 12 | if (header.value.match(/application\/((x-)?rss|atom)\+xml/)) { 13 | header.value = header.value.replace( 14 | /application\/((x-)?rss|atom)\+xml/, 15 | 'text/xml' 16 | ); 17 | isfeed = true; 18 | } 19 | else if (header.value.toLowerCase() == 'text/xml' || header.value.toLowerCase() == 'application/xml' ) { 20 | if (event.url.endsWith(".rss") || event.url.endsWith(".rss.xml") || event.url.endsWith(".atom") || event.url.endsWith(".atom.xml")) { 21 | isfeed = true; 22 | } 23 | } 24 | break; 25 | } 26 | } 27 | 28 | if (isfeed) { 29 | 30 | var cache_idx = null; 31 | 32 | for (let i = 0; i < event.responseHeaders.length; i++) { 33 | if (event.responseHeaders[i].name.toLowerCase() == 'cache-control') { 34 | cache_idx = i; 35 | } 36 | else if (event.responseHeaders[i].name.toLowerCase() == 'content-security-policy') { 37 | 38 | try { 39 | let options = JSON.parse(localStorage.getItem('options')); 40 | 41 | if (options.enableCss && options.bypassCSP) 42 | event.responseHeaders[i].value = patchCSP(event.responseHeaders[i].value); 43 | } 44 | catch(e) { 45 | console.log(e); 46 | } 47 | } 48 | } 49 | 50 | if (cache_idx) { 51 | event.responseHeaders.splice(cache_idx, 1); 52 | } 53 | 54 | // don't cache requests we modified 55 | // otherwise on reload the content-type won't be modified again 56 | event.responseHeaders.push({ 57 | name: 'Cache-Control', 58 | value: 'no-cache, no-store, must-revalidate', 59 | }); 60 | } 61 | 62 | return { responseHeaders: event.responseHeaders }; 63 | 64 | 65 | } 66 | 67 | const browser = window.browser || window.chrome; 68 | 69 | browser.webRequest.onHeadersReceived.addListener( 70 | detectFeed, 71 | { urls: [''], types: ['main_frame'] }, 72 | ['blocking', 'responseHeaders'] 73 | ); 74 | 75 | 76 | function handleMessage(request, sender, sendResponse) { 77 | 78 | browser.runtime.getPlatformInfo().then((info) => { 79 | 80 | let android = info.os == "android" 81 | browser.storage.sync.get({orangeIcon: android}).then(function(options){ 82 | 83 | let popup = new URL(browser.runtime.getURL('popup/popup.html')); 84 | popup.searchParams.set('tabId', sender.tab.id.toString()); 85 | popup.searchParams.set('feeds', JSON.stringify(request)); 86 | 87 | if (options.orangeIcon) { 88 | browser.pageAction.setIcon({tabId: sender.tab.id, path: { 89 | "19": "icons/rss-19.png", 90 | "38": "icons/rss-38.png" 91 | } 92 | }); 93 | } 94 | browser.pageAction.setPopup( {tabId: sender.tab.id, popup: popup.toString() }); 95 | browser.pageAction.show(sender.tab.id); 96 | 97 | //sendResponse({response: "Response from background script to tab " + sender.tab.url , id: sender.tab.id }); 98 | 99 | }); 100 | }); 101 | } 102 | 103 | browser.runtime.onMessage.addListener(handleMessage); 104 | 105 | 106 | function parseCSP(csp) { 107 | let res = {}; 108 | 109 | let directives = csp.split(";"); 110 | for (let directive of directives) { 111 | let kw = directive.trim().split(/\s+/g); 112 | let key = kw.shift(); 113 | let values = res[key] || []; 114 | res[key] = values.concat(kw); 115 | } 116 | 117 | return res; 118 | } 119 | 120 | function patchCSP(csp) { 121 | let parsed_csp = parseCSP(csp); 122 | 123 | let stylesrc = parsed_csp['style-src'] || []; 124 | if (! stylesrc.includes("'unsafe-inline'") ) { 125 | let newstylesrc = ["'unsafe-inline'"]; 126 | 127 | for (let src of stylesrc) { 128 | if (!src.startsWith("'nonce") && !src.startsWith('sha')) 129 | newstylesrc.push(src); 130 | } 131 | 132 | parsed_csp['style-src'] = newstylesrc; 133 | 134 | let new_csp = ""; 135 | 136 | for (let kw in parsed_csp) { 137 | new_csp += kw + " " + parsed_csp[kw].join(" ") + "; "; 138 | } 139 | new_csp = new_csp.substring(0, new_csp.length-2); 140 | return new_csp; 141 | } 142 | return csp; 143 | } 144 | -------------------------------------------------------------------------------- /icons/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/file.png -------------------------------------------------------------------------------- /icons/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/home.png -------------------------------------------------------------------------------- /icons/rss-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-128.png -------------------------------------------------------------------------------- /icons/rss-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-19.png -------------------------------------------------------------------------------- /icons/rss-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-256.png -------------------------------------------------------------------------------- /icons/rss-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-32.png -------------------------------------------------------------------------------- /icons/rss-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-38.png -------------------------------------------------------------------------------- /icons/rss-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-48.png -------------------------------------------------------------------------------- /icons/rss-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-64.png -------------------------------------------------------------------------------- /icons/rss-gray-19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-gray-19.png -------------------------------------------------------------------------------- /icons/rss-gray-38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aureliendavid/rsspreview/a124ab201b45ee80101817886386ccfade595bab/icons/rss-gray-38.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "manifest_version": 2, 4 | "name": "RSSPreview", 5 | "version": "3.33.1", 6 | "author": "Aurelien David", 7 | "homepage_url": "https://github.com/aureliendavid/rsspreview", 8 | 9 | "description": "Preview RSS feeds in browser", 10 | 11 | 12 | "icons": { 13 | "32": "icons/rss-32.png", 14 | "48": "icons/rss-48.png", 15 | "64": "icons/rss-64.png", 16 | "128": "icons/rss-128.png", 17 | "256": "icons/rss-256.png" 18 | }, 19 | 20 | "background": { 21 | "scripts": ["background.js"] 22 | }, 23 | 24 | 25 | "content_scripts": [ 26 | { 27 | "matches": [""], 28 | "js": ["rsspreview.js"] 29 | } 30 | ], 31 | 32 | "web_accessible_resources": ["preview.css", "rss.xsl", "icons/*.png"], 33 | 34 | 35 | "options_ui": { 36 | "page": "settings/options.html" 37 | }, 38 | 39 | "page_action": { 40 | "browser_style": true, 41 | "default_icon": { 42 | "19": "icons/rss-gray-19.png", 43 | "38": "icons/rss-gray-38.png" 44 | }, 45 | "default_title": "Feeds in page" 46 | }, 47 | 48 | "browser_specific_settings": { 49 | "gecko": { 50 | "id": "{7799824a-30fe-4c67-8b3e-7094ea203c94}" 51 | }, 52 | "gecko_android": { 53 | "strict_min_version": "113.0" 54 | } 55 | }, 56 | 57 | "permissions": ["", "webRequest", "webRequestBlocking", "storage", "tabs"] 58 | 59 | } 60 | -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | 2 | var android = false; 3 | 4 | browser.runtime.getPlatformInfo().then((info) => { 5 | android = info.os == "android" 6 | }); 7 | 8 | 9 | document.addEventListener("DOMContentLoaded", function(event) { 10 | 11 | 12 | const feedList = document.getElementById('feedList'); 13 | 14 | const url = new URL(location.href); 15 | // `+` converts the string to an number 16 | const tabId = +url.searchParams.get('tabId'); 17 | const feeds = JSON.parse(url.searchParams.get('feeds')); 18 | 19 | browser.runtime.getPlatformInfo().then((info) => { 20 | android = info.os == "android"; 21 | 22 | for (feed_url in feeds) { 23 | if (feeds.hasOwnProperty(feed_url)) { 24 | 25 | let li = document.createElement("div"); 26 | li.classList.add("panel-list-item"); 27 | li.setAttribute("data-href", feed_url); 28 | 29 | let a = document.createElement("div"); 30 | a.classList.add("text"); 31 | a.innerText = feeds[feed_url]; 32 | 33 | li.appendChild(a); 34 | 35 | if (android) 36 | li.classList.add("android-feed-btn"); 37 | 38 | feedList.appendChild(li); 39 | } 40 | } 41 | 42 | 43 | 44 | 45 | browser.storage.sync.get({newTab: !android}).then(function(options) { 46 | 47 | document.querySelectorAll(".panel-list-item").forEach( (elem) => { 48 | 49 | function onUpdated(tab) { 50 | } 51 | 52 | function onError(error) { 53 | } 54 | 55 | elem.addEventListener('click', (event) => { 56 | 57 | let url = elem.getAttribute("data-href"); 58 | if (url) { 59 | if (options.newTab) { 60 | var params = { url: url } ; 61 | if (!android) { 62 | params.openerTabId = tabId ; 63 | } 64 | browser.tabs.create(params); 65 | } 66 | else 67 | browser.tabs.update({url: url}).then(onUpdated, onError); 68 | } 69 | if (android) 70 | window.close(); 71 | 72 | }); 73 | 74 | }); // end forall 75 | 76 | }); // end options 77 | 78 | }); // and getplatform 79 | 80 | }); 81 | -------------------------------------------------------------------------------- /preview.css: -------------------------------------------------------------------------------- 1 | html { 2 | font: 3mm tahoma,arial,helvetica,sans-serif; 3 | background-color: rgb(240, 240, 240); 4 | color: rgb(0, 0, 0); 5 | box-sizing: border-box; 6 | } 7 | 8 | *, *:before, *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0 3em; 15 | font: message-box; 16 | } 17 | 18 | h1 { 19 | font-size: 160%; 20 | border-bottom: 2px solid ThreeDLightShadow; 21 | margin: 0 0 .2em 0; 22 | } 23 | 24 | h1 a { 25 | color: inherit; 26 | text-decoration: none; 27 | } 28 | 29 | h1 a:hover { 30 | text-decoration: underline; 31 | } 32 | 33 | h2 { 34 | color: GrayText; 35 | font-size: 110%; 36 | font-weight: normal; 37 | margin: 0 0 .6em 0; 38 | } 39 | 40 | a[href] img { 41 | border: none; 42 | } 43 | 44 | pre { 45 | word-wrap: break-word; 46 | word-break: break-all; 47 | } 48 | 49 | 50 | code { 51 | overflow: auto; 52 | white-space: inherit; 53 | } 54 | 55 | code > pre, pre > code { 56 | display: block; 57 | overflow: auto; 58 | white-space: pre; 59 | } 60 | 61 | img { 62 | height: auto; 63 | } 64 | 65 | #feedBody { 66 | border: 1px solid THreeDShadow; 67 | padding: 3em; 68 | padding-inline-start: 30px; 69 | margin: 2em auto; 70 | background-color: white; 71 | } 72 | 73 | #feedTitleLink { 74 | float: right; 75 | margin-inline-start: .6em; 76 | margin-inline-end: 0; 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | } 80 | 81 | #feedTitleContainer { 82 | margin-inline-start: 0; 83 | margin-inline-end: .6em; 84 | margin-top: 0; 85 | margin-bottom: 0; 86 | } 87 | 88 | #feedTitleImage { 89 | margin-inline-start: .6em; 90 | margin-inline-end: 0; 91 | margin-top: 0; 92 | margin-bottom: 0; 93 | max-width: 300px; 94 | max-height: 150px; 95 | } 96 | 97 | .feedEntryContent { 98 | font-size: 110%; 99 | } 100 | 101 | .link { 102 | color: #0000FF; 103 | text-decoration: underline; 104 | cursor: pointer; 105 | } 106 | 107 | .link:hover:active { 108 | color: #FF0000; 109 | } 110 | 111 | .lastUpdated { 112 | font-size: 85%; 113 | font-weight: normal; 114 | } 115 | 116 | .type-icon { 117 | vertical-align: bottom; 118 | height: 16px; 119 | width: 16px; 120 | } 121 | 122 | .enclosures { 123 | border: 1px solid THreeDShadow; 124 | padding: 1em; 125 | margin: 1em auto; 126 | background-color: rgb(240, 240, 240); 127 | } 128 | 129 | .enclosure { 130 | vertical-align: middle; 131 | margin-left: 2px; 132 | } 133 | 134 | .enclosureIcon { 135 | vertical-align: bottom; 136 | height: 16px; 137 | width: 16px; 138 | margin-right: 3px; 139 | } 140 | 141 | .headerIcon { 142 | vertical-align: bottom; 143 | height: 25px; 144 | width: 25px; 145 | margin-right: 2px; 146 | } 147 | 148 | .feedRawContent { 149 | display: none; 150 | } 151 | 152 | #feedSubtitleRaw { 153 | display: none; 154 | } 155 | 156 | #feedSubtitleText { 157 | margin-top: 5px; 158 | } 159 | 160 | #feedLastUpdate { 161 | color: GrayText; 162 | font-size: 87%; 163 | 164 | } 165 | 166 | .author { 167 | font-style: italic; 168 | font-weight: normal; 169 | padding-left: 5px; 170 | padding-top: 3px; 171 | } 172 | 173 | 174 | 175 | 176 | 177 | /** 178 | * browser style classes not present on android and might be deprecated at some point 179 | * (here as reference for now) 180 | */ 181 | 182 | /* 183 | .panel-section { 184 | display: flex; 185 | flex-direction: row; 186 | } 187 | 188 | .panel-section-separator { 189 | background-color: rgba(0, 0, 0, 0.15); 190 | min-height: 1px; 191 | } 192 | 193 | .panel-section-header { 194 | border-bottom: 1px solid rgba(0, 0, 0, 0.15); 195 | padding: 16px; 196 | } 197 | 198 | .panel-section-header > .icon-section-header { 199 | background-position: center center; 200 | background-repeat: no-repeat; 201 | height: 32px; 202 | margin-right: 16px; 203 | position: relative; 204 | width: 32px; 205 | } 206 | 207 | .panel-section-header > .text-section-header { 208 | align-self: center; 209 | font-size: 1.385em; 210 | font-weight: lighter; 211 | } 212 | 213 | 214 | .panel-section-list { 215 | flex-direction: column; 216 | padding: 4px 0; 217 | } 218 | 219 | .panel-list-item { 220 | align-items: center; 221 | display: flex; 222 | flex-direction: row; 223 | height: 24px; 224 | padding: 0 16px; 225 | } 226 | 227 | .panel-list-item:not(.disabled):hover { 228 | background-color: rgba(0, 0, 0, 0.06); 229 | border-block: 1px solid rgba(0, 0, 0, 0.1); 230 | } 231 | 232 | .panel-list-item:not(.disabled):hover:active { 233 | background-color: rgba(0, 0, 0, 0.1); 234 | } 235 | 236 | .panel-list-item.disabled { 237 | color: #999; 238 | } 239 | 240 | .panel-list-item > .icon { 241 | flex-grow: 0; 242 | flex-shrink: 0; 243 | } 244 | 245 | .panel-list-item > .text { 246 | flex-grow: 10; 247 | } 248 | 249 | .panel-list-item > .text-shortcut { 250 | color: #808080; 251 | font-family: "Lucida Grande", caption; 252 | font-size: .847em; 253 | justify-content: flex-end; 254 | } 255 | 256 | .panel-section-list .panel-section-separator { 257 | margin: 4px 0; 258 | } */ 259 | -------------------------------------------------------------------------------- /rss.xsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 |
26 |

27 | 28 | 29 | 30 |

31 |
32 |

33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 | 44 | 45 |
46 | 47 |

48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 |
65 | 66 | 67 |
68 |
69 |

70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 |
82 |
83 | 84 |
85 | 86 |
87 |
88 |
89 |
90 | 91 | 92 | 93 |
94 | 95 |
96 |
97 | 98 |
99 | 100 |
101 |
102 |
103 |
104 |
105 | 106 |
107 | 108 | 109 |
110 | 111 | 112 | (, ) 113 |
114 | 115 |
116 |
117 | 118 |
119 |
120 | 121 |
122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
130 | -------------------------------------------------------------------------------- /rsspreview.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | /** 3 | * Check and set a global guard variable. 4 | * If this content script is injected into the same page again, 5 | * it will do nothing next time. 6 | */ 7 | if (window.hasRun) { 8 | console.log('already run'); 9 | return; 10 | } 11 | 12 | window.hasRun = true; 13 | 14 | // defaults 15 | var options = { 16 | doThumb: false, 17 | doMaxWidth: true, 18 | valMaxWidth: "900px", 19 | doDetect: true, 20 | preventPreview: false, 21 | fullPreview: false, 22 | doAuthor: false, 23 | enableCss: false, 24 | bypassCSP: false, 25 | customCss: null, 26 | newTab: true 27 | }; 28 | 29 | let xml_parser = new XMLSerializer(); 30 | let html_parser = new DOMParser(); 31 | 32 | function xhrdoc(url, type, cb) { 33 | let xhr = new XMLHttpRequest(); 34 | xhr.open('GET', url, true); 35 | 36 | xhr.responseType = 'document'; 37 | xhr.overrideMimeType('text/' + type); 38 | 39 | xhr.onload = () => { 40 | if (xhr.readyState === xhr.DONE) { 41 | if (xhr.status === 200) { 42 | let resp = type == 'xml' ? xhr.responseXML : xhr.response; 43 | cb(resp); 44 | } 45 | } 46 | }; 47 | 48 | xhr.send(null); 49 | } 50 | 51 | function applyxsl(xmlin, xsl, node, doc = document) { 52 | let xsltProcessor = new XSLTProcessor(); 53 | xsltProcessor.importStylesheet(xsl); 54 | xsltProcessor.setParameter(null, 'fullPreview', options.fullPreview); 55 | xsltProcessor.setParameter(null, 'doAuthor', options.doAuthor); 56 | let fragment = xsltProcessor.transformToFragment(xmlin, doc); 57 | node.appendChild(fragment); 58 | } 59 | 60 | function getlang() { 61 | return browser.i18n.getUILanguage(); 62 | } 63 | 64 | function formatsubtitle() { 65 | try { 66 | let feed_desc = document.getElementById('feedSubtitleRaw'); 67 | 68 | let html_desc = html_parser.parseFromString( 69 | '

' + feed_desc.innerText + '

', 70 | 'text/html' 71 | ); 72 | let xml_desc = xml_parser.serializeToString(html_desc.body.firstChild); 73 | 74 | feed_desc.insertAdjacentHTML('afterend', xml_desc); 75 | 76 | feed_desc.parentNode.removeChild(feed_desc); 77 | } catch (e) { 78 | console.error(e); 79 | } 80 | } 81 | 82 | function formatdescriptions(el = document) { 83 | // unescapes descriptions to html then to xml 84 | let tohtml = el.getElementsByClassName('feedRawContent'); 85 | 86 | for (let i = 0; i < tohtml.length; i++) { 87 | 88 | try { 89 | let html_txt = ''; 90 | if (tohtml[i].getAttribute('desctype') == 'text/plain') { 91 | html_txt = '
' + tohtml[i].innerHTML + '
'; 92 | } 93 | else if (tohtml[i].getAttribute('desctype') == 'xhtml') { 94 | html_txt = '
' + tohtml[i].innerHTML + '
'; 95 | } 96 | else { 97 | html_txt = '
' + tohtml[i].textContent + '
'; 98 | } 99 | 100 | let html_desc = html_parser.parseFromString(html_txt, 'text/html'); 101 | let xml_desc = xml_parser.serializeToString( 102 | html_desc.body.firstChild 103 | ); 104 | 105 | tohtml[i].insertAdjacentHTML('afterend', xml_desc); 106 | tohtml[i].setAttribute('todel', 1); 107 | } catch (e) { 108 | console.error(e); 109 | console.log(tohtml[i]); 110 | } 111 | 112 | } 113 | 114 | el.querySelectorAll('.feedRawContent').forEach(a => { 115 | if (a.getAttribute('todel') == '1') { 116 | a.remove(); 117 | } 118 | }); 119 | } 120 | 121 | function removeemptyenclosures(el = document) { 122 | let encs = el.getElementsByClassName('enclosures'); 123 | 124 | for (let i = 0; i < encs.length; i++) 125 | if (!encs[i].firstChild) encs[i].style.display = 'none'; 126 | } 127 | 128 | function formatfilenames(el = document) { 129 | let encfn = el.getElementsByClassName('enclosureFilename'); 130 | 131 | for (let i = 0; i < encfn.length; i++) { 132 | let url = new URL(encfn[i].innerText); 133 | 134 | if (url) { 135 | let fn = url.pathname.split('/').pop(); 136 | 137 | if (fn != '') encfn[i].innerText = fn; 138 | } 139 | } 140 | } 141 | 142 | function formatfilesizes(el = document) { 143 | function humanfilesize(size) { 144 | let i = 0; 145 | 146 | if (size && size != '' && size > 0) 147 | i = Math.floor(Math.log(size) / Math.log(1024)); 148 | 149 | return ( 150 | (size / Math.pow(1024, i)).toFixed(2) * 1 + 151 | ' ' + 152 | ['B', 'kB', 'MB', 'GB', 'TB'][i] 153 | ); 154 | } 155 | 156 | let encsz = el.getElementsByClassName('enclosureSize'); 157 | for (let i = 0; i < encsz.length; i++) { 158 | let hsize = humanfilesize(encsz[i].innerText); 159 | 160 | if (hsize) encsz[i].innerText = hsize; 161 | } 162 | } 163 | 164 | function formattitles(el = document) { 165 | let et = el.getElementsByClassName('entrytitle'); 166 | 167 | for (let i = 0; i < et.length; i++) { 168 | //basically removes html content if there is some 169 | //only do it if there's a tag to avoid doing it when text titles cointain a '&' 170 | //(which can be caught but still displays an error in console, which is annoying) 171 | if (et[i].innerText.indexOf('<') >= 0 || et[i].innerText.indexOf('&')) { 172 | 173 | let tmp = document.createElement('span'); 174 | try { 175 | tmp.innerHTML = et[i].innerText; 176 | et[i].innerText = tmp.textContent; 177 | } catch (e) { 178 | // if not parsable, display as text 179 | console.error(e); 180 | console.log(et[i].innerText); 181 | } 182 | } 183 | } 184 | } 185 | 186 | function formatdates(el = document) { 187 | let lang = getlang(); 188 | if (!lang) return; 189 | 190 | let opts = { 191 | weekday: 'long', 192 | year: 'numeric', 193 | month: 'long', 194 | day: 'numeric', 195 | }; 196 | 197 | let ed = el.getElementsByClassName('lastUpdated'); 198 | for (let i = 0; i < ed.length; i++) { 199 | let d = new Date(ed[i].innerText); 200 | if (isNaN(d)) continue; 201 | 202 | let dstr = 203 | d.toLocaleDateString(lang, opts) + ' ' + d.toLocaleTimeString(lang); 204 | 205 | ed[i].innerText = dstr; 206 | } 207 | 208 | let lu = el.getElementById('feedLastUpdate'); 209 | if (lu && lu.innerText.trim() != "") { 210 | lu.innerText = "Last updated: " + lu.innerText; 211 | } 212 | } 213 | 214 | function extensionimages(el = document) { 215 | let extimgs = el.getElementsByClassName('extImg'); 216 | 217 | for (let i = 0; i < extimgs.length; i++) 218 | extimgs[i].src = chrome.runtime.getURL( 219 | extimgs[i].attributes['data-src'].nodeValue 220 | ); 221 | } 222 | 223 | function applysettings() { 224 | 225 | document.querySelectorAll('.mediaThumb').forEach((elem) => { 226 | elem.style.display = options.doThumb ? "block" : "none"; 227 | }); 228 | 229 | 230 | document.querySelectorAll('img').forEach((elem) => { 231 | if (options.doMaxWidth) 232 | elem.style["max-width"] = options.valMaxWidth; 233 | }); 234 | 235 | } 236 | 237 | function makepreviewhtml() { 238 | let doc = document.implementation.createHTMLDocument(''); 239 | doc.body.id = "rsspreviewBody"; 240 | 241 | let feedBody = doc.createElement('div'); 242 | feedBody.id = 'feedBody'; 243 | doc.body.appendChild(feedBody); 244 | 245 | let css = doc.createElement('link'); 246 | css.setAttribute('rel', 'stylesheet'); 247 | css.setAttribute('href', chrome.runtime.getURL('preview.css')); 248 | doc.head.appendChild(css); 249 | 250 | if (options.enableCss && options.customCss) { 251 | let node = doc.createElement('style'); 252 | node.innerHTML = options.customCss; 253 | doc.head.appendChild(node); 254 | } 255 | 256 | return doc; 257 | } 258 | 259 | function detect() { 260 | let rootNode = document.getRootNode(); 261 | 262 | // for chrome 263 | let d = document.getElementById('webkit-xml-viewer-source-xml'); 264 | if (d && d.firstChild) rootNode = d.firstChild; 265 | 266 | const rootName = rootNode.documentElement.nodeName.toLowerCase(); 267 | 268 | let isRSS1 = false; 269 | 270 | if (rootName == 'rdf' || rootName == 'rdf:rdf') { 271 | if (rootNode.documentElement.attributes['xmlns']) { 272 | isRSS1 = rootNode.documentElement.attributes['xmlns'].nodeValue.search('rss') > 0; 273 | } 274 | 275 | } 276 | 277 | if ( 278 | rootName == 'rss' || 279 | rootName == 'channel' || // rss2 280 | rootName == 'feed' || // atom 281 | isRSS1 282 | ) 283 | return rootNode; 284 | 285 | return null; 286 | } 287 | 288 | function main(feedNode) { 289 | let feed_url = window.location.href; 290 | let preview = makepreviewhtml(); 291 | 292 | xhrdoc(chrome.runtime.getURL('rss.xsl'), 'xml', xsl_xml => { 293 | applyxsl(feedNode, xsl_xml, preview.getElementById('feedBody'), preview); 294 | 295 | // replace the content with the preview document 296 | document.replaceChild( 297 | document.importNode(preview.documentElement, true), 298 | document.documentElement 299 | ); 300 | 301 | let t0 = performance.now(); 302 | 303 | formatsubtitle(); 304 | 305 | formatdescriptions(); 306 | removeemptyenclosures(); 307 | formatfilenames(); 308 | formatfilesizes(); 309 | formattitles(); 310 | formatdates(); 311 | extensionimages(); 312 | applysettings(); 313 | 314 | let t1 = performance.now(); 315 | //console.log("exec in: " + (t1 - t0) + "ms"); 316 | 317 | document.title = document.getElementById('feedTitleText').innerText; 318 | }); 319 | } 320 | 321 | 322 | function onOptions(opts) { 323 | options = opts; 324 | 325 | let feedRoot = detect(); 326 | 327 | if (feedRoot && !options.preventPreview) { 328 | 329 | main(feedRoot); 330 | } 331 | 332 | else if (options.doDetect) { 333 | 334 | findFeeds(); 335 | } 336 | 337 | } 338 | 339 | function onError(error) { 340 | console.log(`Error on get options: ${error}`); 341 | } 342 | 343 | let getting = browser.storage.sync.get(options); 344 | getting.then(onOptions, onError); 345 | 346 | 347 | function registerFeeds(feeds) { 348 | if (Object.keys(feeds).length > 0) { 349 | function handleResponse(message) { 350 | } 351 | 352 | function handleError(error) { 353 | //console.log(error); 354 | } 355 | 356 | browser.runtime.sendMessage(feeds).then(handleResponse, handleError); 357 | } 358 | } 359 | 360 | 361 | function findiTunesPodcastsFeeds() { 362 | let match = document.URL.match(/id(\d+)/) 363 | if (match) { 364 | let feeds = {}; 365 | let itunesid = match[1]; 366 | 367 | var xhr = new XMLHttpRequest(); 368 | xhr.open('GET', "https://itunes.apple.com/lookup?id="+itunesid+"&entity=podcast"); 369 | 370 | xhr.onload = function () { 371 | if (xhr.readyState === xhr.DONE) { 372 | if (xhr.status === 200) { 373 | let res = JSON.parse(xhr.responseText); 374 | 375 | if ("results" in res) { 376 | let pod = res["results"][0]; 377 | let title = pod["collectionName"] || document.title; 378 | let url = pod["feedUrl"]; 379 | if (url) { 380 | feeds[url] = title; 381 | } 382 | } 383 | } 384 | } 385 | 386 | registerFeeds(feeds); 387 | }; 388 | xhr.send(); 389 | } 390 | } 391 | 392 | function findYouTubeFeeds() { 393 | // YouTube's canonical channel URLs look like /channel/AlphaNumericID 394 | // It also supports named channels of the form /c/MyChannelName 395 | // and handle links of the form /@MyChannelHandle. 396 | // Match also on '%' to handle non-latin character codes 397 | // Match on both of these to autodetect channel feeds on either URL 398 | let idPattern = /channel\/([a-zA-Z0-9%_-]+)/; 399 | let namePattern = /(?:c|user)\/[a-zA-Z0-9%_-]+/; 400 | let handlePattern = /@[a-zA-Z0-9%_-]+/; 401 | let urlPattern = new RegExp(`${idPattern.source}|${namePattern.source}|${handlePattern.source}`); 402 | if (document.URL.match(urlPattern)) { 403 | let feeds = {}; 404 | let canonicalUrl = document.querySelector("link[rel='canonical']").href; 405 | let channelId = canonicalUrl.match(idPattern)[1]; 406 | let url = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`; 407 | let title = document.title; 408 | feeds[url] = title; 409 | registerFeeds(feeds); 410 | } 411 | } 412 | 413 | // The default function used to find feeds if a domain-specific function doesn't exist. 414 | // Parse the document's HTML looking for link tags pointing to the feed URL. 415 | function defaultFindFeeds() { 416 | let feeds = {}; 417 | document.querySelectorAll("link[rel='alternate']").forEach( (elem) => { 418 | let type_attr = elem.getAttribute('type'); 419 | if (!type_attr) { 420 | return; 421 | } 422 | 423 | let type = type_attr.toLowerCase(); 424 | if (type.includes('rss') || type.includes('atom') || type.includes('feed')) { 425 | let title = elem.getAttribute('title'); 426 | let url = elem.href; 427 | 428 | if (url) { 429 | feeds[url] = (title ? title : url); 430 | } 431 | } 432 | }); 433 | registerFeeds(feeds); 434 | } 435 | 436 | const domainFeedFinders = new Map([ 437 | ["itunes.apple.com", findiTunesPodcastsFeeds], 438 | ["podcasts.apple.com", findiTunesPodcastsFeeds], 439 | ["www.youtube.com", findYouTubeFeeds], 440 | ]); 441 | 442 | function findFeeds() { 443 | // Look up a feed detection function based on the domain. 444 | // If a domain-specific function doesn't exist, fall back to a default. 445 | let finder = domainFeedFinders.get(document.domain) || defaultFindFeeds; 446 | finder(); 447 | } 448 | 449 | })(); 450 | -------------------------------------------------------------------------------- /settings/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 |
18 | 19 |
20 | 21 |
22 | 23 | 24 | 25 | 29 |
30 | 31 |
32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /settings/options.js: -------------------------------------------------------------------------------- 1 | 2 | var android = false; 3 | 4 | browser.runtime.getPlatformInfo().then((info) => { 5 | android = info.os == "android" 6 | }); 7 | 8 | function saveOptions(e) { 9 | e.preventDefault(); 10 | 11 | let options = { 12 | doThumb: document.querySelector("#doThumb").checked, 13 | doMaxWidth: document.querySelector("#doMaxWidth").checked, 14 | valMaxWidth: document.querySelector("#valMaxWidth").value, 15 | doDetect: document.querySelector("#doDetect").checked, 16 | preventPreview: document.querySelector("#preventPreview").checked, 17 | fullPreview: document.querySelector("#fullPreview").checked, 18 | doAuthor: document.querySelector("#doAuthor").checked, 19 | orangeIcon: document.querySelector("#orangeIcon").checked, 20 | enableCss: document.querySelector("#enableCss").checked, 21 | bypassCSP: document.querySelector("#bypassCSP").checked, 22 | customCss: document.querySelector("#customCss").value, 23 | newTab: document.querySelector("#newTab").checked 24 | }; 25 | 26 | browser.storage.sync.set(options); 27 | localStorage.setItem('options', JSON.stringify(options)); 28 | 29 | } 30 | 31 | 32 | function restoreOptions() { 33 | 34 | browser.runtime.getPlatformInfo().then((info) => { 35 | android = info.os == "android" 36 | 37 | 38 | function onResult(result) { 39 | document.querySelector("#doThumb").checked = result.doThumb; 40 | document.querySelector("#doMaxWidth").checked = result.doMaxWidth; 41 | document.querySelector("#valMaxWidth").value = result.valMaxWidth; 42 | document.querySelector("#doDetect").checked = result.doDetect; 43 | document.querySelector("#preventPreview").checked = result.preventPreview; 44 | document.querySelector("#fullPreview").checked = result.fullPreview; 45 | document.querySelector("#doAuthor").checked = result.doAuthor; 46 | document.querySelector("#orangeIcon").checked = result.orangeIcon; 47 | document.querySelector("#enableCss").checked = result.enableCss; 48 | document.querySelector("#bypassCSP").checked = result.bypassCSP; 49 | document.querySelector("#customCss").value = result.customCss; 50 | document.querySelector("#newTab").checked = result.newTab; 51 | 52 | localStorage.setItem('options', JSON.stringify(result)); 53 | } 54 | 55 | function onError(error) { 56 | console.log(`Error: ${error}`); 57 | } 58 | 59 | var getting = browser.storage.sync.get({ 60 | doThumb: false, 61 | doMaxWidth: true, 62 | valMaxWidth: "900px", 63 | doDetect: true, 64 | preventPreview: false, 65 | fullPreview: false, 66 | doAuthor: false, 67 | orangeIcon: android, 68 | enableCss: false, 69 | bypassCSP: false, 70 | customCss: null, 71 | newTab: !android 72 | }); 73 | getting.then(onResult, onError); 74 | 75 | }); 76 | 77 | } 78 | 79 | 80 | 81 | document.addEventListener("DOMContentLoaded", restoreOptions); 82 | 83 | document.querySelectorAll('.validate').forEach((elem) => { 84 | elem.addEventListener('change', saveOptions); 85 | }); 86 | --------------------------------------------------------------------------------